aboutsummaryrefslogtreecommitdiff
path: root/cmd/web/server.go
blob: c573244f37be4715d6711a2a476ccbb66936aa63 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
package web

import (
	"context"
	"io/fs"
	"log"
	"os"
	"time"

	"code.crute.us/mcrute/cloud-identity-broker/app"
	"code.crute.us/mcrute/cloud-identity-broker/app/controllers"
	"code.crute.us/mcrute/cloud-identity-broker/app/middleware"
	"code.crute.us/mcrute/cloud-identity-broker/app/models"
	"code.crute.us/mcrute/cloud-identity-broker/auth"
	"code.crute.us/mcrute/cloud-identity-broker/auth/github"

	"code.crute.us/mcrute/golib/crypto/tls"
	"code.crute.us/mcrute/golib/db/mongodb"
	glecho "code.crute.us/mcrute/golib/echo"
	glmw "code.crute.us/mcrute/golib/echo/middleware"
	"code.crute.us/mcrute/golib/service"
	"github.com/labstack/echo/v4"
	echomw "github.com/labstack/echo/v4/middleware"
	"github.com/spf13/cobra"
	"golang.org/x/time/rate"
)

func Register(root *cobra.Command, embeddedTemplates fs.FS, version string) {
	webCmd := &cobra.Command{
		Use:   "web [options]",
		Short: "Run web server",
		Run: func(c *cobra.Command, args []string) {
			webMain(app.NewConfigFromCmd(c), embeddedTemplates, version)
		},
	}

	webCmd.Flags().StringSlice("bind", []string{":8169"}, "Addresses and ports to bind http server")
	webCmd.Flags().StringSlice("bind-tls", []string{":8170"}, "Addresses and ports to bind https server")
	webCmd.Flags().String("log-file", "", "Log file for combined host logs")
	webCmd.Flags().String("tls-cache-dir", "ssl/", "Cache directory for TLS certificates")
	webCmd.Flags().StringSlice("trusted-ip-ranges", []string{"172.19.0.0/22", "2602:803:4072::/48"}, "Comma separated list of IP ranges for trusted XFF proxies")
	webCmd.Flags().StringSlice("management-ip-ranges", []string{"127.0.0.1/32", "::1/128", "172.19.0.0/22", "2602:803:4072::/48"}, "IP ranges for management systems")
	webCmd.Flags().StringSlice("hostname", []string{"dev.aws-access.crute.us"}, "Hostname this server serves (can be specified multiple times)")
	webCmd.Flags().Duration("rate-limit", 30*time.Second, "Number seconds between requests for credential resources")
	webCmd.Flags().Duration("auth-cookie-duration", 24*time.Hour, "Expiration duration of the auth cookies")
	webCmd.Flags().Int("rate-limit-burst", 30, "Number of burst requests allowed to credential endpoints")
	webCmd.Flags().String("issuer-endpoint", "https://aws-access.crute.us", "Oauth issuer endpoint")
	webCmd.Flags().String("jwt-audience", "aws-access", "Audience for issued JWTs")

	webCmd.Flags().String("github-oauth-vault-path", "", "Vault material name for GitHub auth credentials")
	webCmd.MarkFlagRequired("github-oauth-vault-path")

	root.AddCommand(webCmd)
}

func webMain(cfg app.Config, embeddedTemplates fs.FS, version string) {
	ctx := context.Background()

	s, err := glecho.NewDefaultEchoWithConfig(glecho.EchoConfig{
		Debug:                cfg.Debug,
		Hostnames:            cfg.Hostnames,
		BindAddresses:        cfg.Bind,
		BindTLSAddresses:     cfg.BindTLS,
		TLSCacheDir:          cfg.TLSCacheDir,
		TrustedProxyIPRanges: cfg.TrustedIPRanges,
		ManagementIPRanges:   cfg.ManagementIPRanges,
		DiskTemplates:        os.DirFS(cfg.TemplatePath),
		EmbeddedTemplates:    embeddedTemplates,
		TemplateGlob:         &cfg.TemplateGlob,
		CombinedHostLogFile:  cfg.LogFile,
		ContentSecurityPolicy: &glmw.ContentSecurityPolicyConfig{
			DefaultSrc: []glmw.CSPDirective{
				glmw.CSPSelf,
				glmw.CSPUnsafeInline,
			},
			StyleSrc: []glmw.CSPDirective{
				glmw.CSPSelf,
				glmw.CSPData,
				glmw.CSPUnsafeInline,
			},
		},
	})
	if err != nil {
		log.Fatalf("Error building echo: %w", err)
	}
	if err = s.Init(); err != nil {
		log.Fatalf("Error initializing echo: %w", err)
	}
	s.Use(middleware.AddServerHeader(version))

	mongo, err := mongodb.Connect(ctx, cfg.MongoDbUri, cfg.MongodbVaultPath)
	if err != nil {
		log.Fatalf("Error connecting to mongodb: %w", err)
	}

	rateLimit := echomw.RateLimiter(
		echomw.NewRateLimiterMemoryStoreWithConfig(
			echomw.RateLimiterMemoryStoreConfig{
				Rate:      rate.Every(cfg.RateLimit),
				Burst:     cfg.RateLimitBurst,
				ExpiresIn: time.Hour,
			},
		),
	)

	adminAccountStore := &models.MongoDbAccountStore{
		Db:            mongo,
		ReturnDeleted: true,
	}
	as := &models.MongoDbAccountStore{Db: mongo}
	us := &models.MongoDbUserStore{Db: mongo}

	aws := &controllers.AWSAPI{Store: as}

	am := &middleware.AuthenticationMiddleware{
		Store:          us,
		CookieDuration: cfg.AuthCookieDuration,
		GitHub: &github.GitHubAuthenticator{
			ClientId:     cfg.GitHubOauthCreds.ClientId,
			ClientSecret: cfg.GitHubOauthCreds.ClientSecret,
		},
		JWTManager: &auth.JWTManager{
			Store:        us,
			Audience:     cfg.JWTAudience,
			TokenExpires: cfg.AuthCookieDuration,
		},
	}
	am.RegisterUrls(s)

	api := s.Group("/api")
	api.Use(glmw.VaryCookie())
	api.Use(glmw.CacheNeverMiddleware)
	api.Use(am.Middleware)
	{
		api.GET("", controllers.APIIndexHandler)

		account := api.Group("/account")
		{
			account.GET("", controllers.NewAPIAccountListHandler(as))
			account.GET(
				"/:account/credentials",
				controllers.NewAPIRegionListHandler(aws),
			)
			account.GET(
				"/:account/console",
				controllers.NewAPIConsoleRedirectHandler(aws, cfg.IssuerEndpoint),
				rateLimit,
			)
			account.GET(
				"/:account/credentials/:region",
				controllers.NewAPICredentialsHandler(aws),
				rateLimit,
			)
			(&controllers.APIAccountHandler{
				Store:      as,
				AdminStore: adminAccountStore,
			}).Register("/:account", account)
		}
	}
	s.GET("/favicon.ico", echo.NotFoundHandler)
	s.GET("/logout", controllers.LogoutHandler)
	s.CachedStaticRoute("/assets", "assets")
	s.GET("/", controllers.IndexHandler, am.Middleware)

	runner := service.NewAppRunner(ctx, s.Logger)
	runner.AddJob(s.RunCertificateManager) // Cert manager has to start before server
	runner.AddJob(tls.OcspErrorLogger(s.Logger, s.OcspErrors()))
	runner.AddJobs(s.MakeServerJobs())
	runner.RunForever(!cfg.DisableBackgroundJobs)
}