diff --git a/cmd/pomerium/main.go b/cmd/pomerium/main.go index 8dcd50eb1..153605e19 100644 --- a/cmd/pomerium/main.go +++ b/cmd/pomerium/main.go @@ -1,6 +1,7 @@ package main // import "github.com/pomerium/pomerium/cmd/pomerium" import ( + "errors" "flag" "fmt" "net/http" @@ -21,116 +22,148 @@ import ( "github.com/pomerium/pomerium/proxy" ) -var ( - debugFlag = flag.Bool("debug", false, "run server in debug mode, changes log output to STDOUT and level to info") - versionFlag = flag.Bool("version", false, "prints the version") -) +var versionFlag = flag.Bool("version", false, "prints the version") func main() { flag.Parse() if *versionFlag { - fmt.Printf("%s\n", version.FullVersion()) + fmt.Println(version.FullVersion()) os.Exit(0) } - mainOpts, err := optionsFromEnvConfig() + opt, err := parseOptions() if err != nil { - log.Fatal().Err(err).Msg("cmd/pomerium: settings error") + log.Fatal().Err(err).Msg("cmd/pomerium: options") } - if *debugFlag || mainOpts.Debug { - log.SetDebugMode() - } - if mainOpts.LogLevel != "" { - log.SetLevel(mainOpts.LogLevel) - } - log.Info().Str("version", version.FullVersion()).Str("user-agent", version.UserAgent()).Str("service", mainOpts.Services).Msg("cmd/pomerium") - grpcAuth := middleware.NewSharedSecretCred(mainOpts.SharedKey) + grpcAuth := middleware.NewSharedSecretCred(opt.SharedKey) grpcOpts := []grpc.ServerOption{grpc.UnaryInterceptor(grpcAuth.ValidateRequest)} grpcServer := grpc.NewServer(grpcOpts...) - var authenticateService *authenticate.Authenticate - var authHost string - if mainOpts.Services == "all" || mainOpts.Services == "authenticate" { - opts, err := authenticate.OptionsFromEnvConfig() - if err != nil { - log.Fatal().Err(err).Msg("cmd/pomerium: authenticate settings") - } - authenticateService, err = authenticate.New(opts) - if err != nil { - log.Fatal().Err(err).Msg("cmd/pomerium: new authenticate") - } - authHost = urlutil.StripPort(opts.AuthenticateURL.Host) - pbAuthenticate.RegisterAuthenticatorServer(grpcServer, authenticateService) - } - - var authorizeService *authorize.Authorize - if mainOpts.Services == "all" || mainOpts.Services == "authorize" { - opts, err := authorize.OptionsFromEnvConfig() - if err != nil { - log.Fatal().Err(err).Msg("cmd/pomerium: authorize settings") - } - authorizeService, err = authorize.New(opts) - if err != nil { - log.Fatal().Err(err).Msg("cmd/pomerium: new authorize") - } - pbAuthorize.RegisterAuthorizerServer(grpcServer, authorizeService) - } - - var proxyService *proxy.Proxy - if mainOpts.Services == "all" || mainOpts.Services == "proxy" { - proxyOpts, err := proxy.OptionsFromEnvConfig() - if err != nil { - log.Fatal().Err(err).Msg("cmd/pomerium: proxy settings") - } - - proxyService, err = proxy.New(proxyOpts) - if err != nil { - log.Fatal().Err(err).Msg("cmd/pomerium: new proxy") - } - // cleanup our RPC services - defer proxyService.AuthenticateClient.Close() - defer proxyService.AuthorizeClient.Close() - - } - - topMux := http.NewServeMux() - topMux.HandleFunc("/ping", func(rw http.ResponseWriter, _ *http.Request) { + mux := http.NewServeMux() + mux.HandleFunc("/ping", func(rw http.ResponseWriter, _ *http.Request) { rw.WriteHeader(http.StatusOK) - fmt.Fprintf(rw, "OK") + fmt.Fprintf(rw, version.UserAgent()) }) - if authenticateService != nil { - topMux.Handle(authHost+"/", authenticateService.Handler()) + + _, err = newAuthenticateService(opt.Services, mux, grpcServer) + if err != nil { + log.Fatal().Err(err).Msg("cmd/pomerium: authenticate") } - if proxyService != nil { - topMux.Handle("/", proxyService.Handler()) + + _, err = newAuthorizeService(opt.Services, grpcServer) + if err != nil { + log.Fatal().Err(err).Msg("cmd/pomerium: authorize") } + _, err = newProxyService(opt.Services, mux) + if err != nil { + log.Fatal().Err(err).Msg("cmd/pomerium: proxy") + } + // defer statements ignored anyway : https://stackoverflow.com/a/17888654 + // defer proxyService.AuthenticateClient.Close() + // defer proxyService.AuthorizeClient.Close() + httpOpts := &https.Options{ - Addr: mainOpts.Addr, - Cert: mainOpts.Cert, - Key: mainOpts.Key, - CertFile: mainOpts.CertFile, - KeyFile: mainOpts.KeyFile, + Addr: opt.Addr, + Cert: opt.Cert, + Key: opt.Key, + CertFile: opt.CertFile, + KeyFile: opt.KeyFile, + ReadTimeout: opt.ReadTimeout, + WriteTimeout: opt.WriteTimeout, + ReadHeaderTimeout: opt.ReadHeaderTimeout, + IdleTimeout: opt.IdleTimeout, } - if mainOpts.HTTPRedirectAddr != "" { - // stand up another http server that just redirect HTTP to HTTPS traffic - srv := &http.Server{ - Addr: mainOpts.HTTPRedirectAddr, - ReadTimeout: 5 * time.Second, - WriteTimeout: 5 * time.Second, - Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Connection", "close") - url := fmt.Sprintf("https://%s%s", urlutil.StripPort(r.Host), r.URL.String()) - http.Redirect(w, r, url, http.StatusMovedPermanently) - }), - } - log.Info().Str("Addr", mainOpts.HTTPRedirectAddr).Msg("cmd/pomerium: http redirect server started") - go func() { log.Fatal().Err(srv.ListenAndServe()).Msg("cmd/pomerium: http server") }() + if srv, err := startRedirectServer(opt.HTTPRedirectAddr); err != nil { + log.Debug().Err(err).Msg("cmd/pomerium: http redirect server not started") } else { - log.Debug().Msg("cmd/pomerium: http redirect server not started") + defer srv.Close() + } + if err := https.ListenAndServeTLS(httpOpts, mux, grpcServer); err != nil { + log.Fatal().Err(err).Msg("cmd/pomerium: https server") } - - log.Fatal().Err(https.ListenAndServeTLS(httpOpts, topMux, grpcServer)).Msg("cmd/pomerium: https server") - +} + +// startRedirectServer starts a http server that redirect HTTP to HTTPS traffic +func startRedirectServer(addr string) (*http.Server, error) { + if addr == "" { + return nil, errors.New("no http redirect addr provided") + } + srv := &http.Server{ + Addr: addr, + ReadTimeout: 5 * time.Second, + WriteTimeout: 5 * time.Second, + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Connection", "close") + url := fmt.Sprintf("https://%s%s", urlutil.StripPort(r.Host), r.URL.String()) + http.Redirect(w, r, url, http.StatusMovedPermanently) + }), + } + log.Info().Str("Addr", addr).Msg("cmd/pomerium: http redirect server started") + go func() { log.Error().Err(srv.ListenAndServe()).Msg("cmd/pomerium: http server closed") }() + return srv, nil +} + +func newAuthenticateService(s string, mux *http.ServeMux, rpc *grpc.Server) (*authenticate.Authenticate, error) { + if !isAuthenticate(s) { + return nil, nil + } + opts, err := authenticate.OptionsFromEnvConfig() + if err != nil { + return nil, err + } + service, err := authenticate.New(opts) + if err != nil { + return nil, err + } + pbAuthenticate.RegisterAuthenticatorServer(rpc, service) + mux.Handle(urlutil.StripPort(opts.AuthenticateURL.Host)+"/", service.Handler()) + return service, nil +} + +func newAuthorizeService(s string, rpc *grpc.Server) (*authorize.Authorize, error) { + if !isAuthorize(s) { + return nil, nil + } + opts, err := authorize.OptionsFromEnvConfig() + if err != nil { + return nil, err + } + service, err := authorize.New(opts) + if err != nil { + return nil, err + } + pbAuthorize.RegisterAuthorizerServer(rpc, service) + return service, nil +} + +func newProxyService(s string, mux *http.ServeMux) (*proxy.Proxy, error) { + if !isProxy(s) { + return nil, nil + } + opts, err := proxy.OptionsFromEnvConfig() + if err != nil { + return nil, err + } + service, err := proxy.New(opts) + if err != nil { + return nil, err + } + mux.Handle("/", service.Handler()) + return service, nil +} + +func parseOptions() (*Options, error) { + o, err := optionsFromEnvConfig() + if err != nil { + return nil, err + } + if o.Debug { + log.SetDebugMode() + } + if o.LogLevel != "" { + log.SetLevel(o.LogLevel) + } + return o, nil } diff --git a/cmd/pomerium/main_test.go b/cmd/pomerium/main_test.go new file mode 100644 index 000000000..40edb3bd9 --- /dev/null +++ b/cmd/pomerium/main_test.go @@ -0,0 +1,199 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "os" + "reflect" + "strings" + "testing" + + "github.com/pomerium/pomerium/internal/middleware" + "google.golang.org/grpc" +) + +func Test_startRedirectServer(t *testing.T) { + + tests := []struct { + name string + addr string + want string + wantErr bool + }{ + {"empty", "", "", true}, + {":http", ":http", ":http", false}, + {"localhost:80", "localhost:80", "localhost:80", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := startRedirectServer(tt.addr) + if (err != nil) != tt.wantErr { + t.Errorf("startRedirectServer() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != nil { + defer got.Close() + ts := httptest.NewServer(got.Handler) + defer ts.Close() + _, err := http.Get(ts.URL) + if !strings.Contains(err.Error(), "https") { + t.Errorf("startRedirectServer() = %v, want %v", err, tt.want) + return + } + } + }) + } +} + +func Test_newAuthenticateService(t *testing.T) { + os.Clearenv() + grpcAuth := middleware.NewSharedSecretCred("test") + grpcOpts := []grpc.ServerOption{grpc.UnaryInterceptor(grpcAuth.ValidateRequest)} + grpcServer := grpc.NewServer(grpcOpts...) + mux := http.NewServeMux() + + tests := []struct { + name string + s string + envKey string + envValue string + + wantHostname string + wantErr bool + }{ + {"wrong service", "proxy", "", "", "", false}, + {"bad", "authenticate", "SHARED_SECRET", "error!", "", true}, + {"bad emv", "authenticate", "COOKIE_REFRESH", "error!", "", true}, + {"good", "authenticate", "IDP_CLIENT_ID", "test", "auth.server.com", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + os.Setenv("IDP_PROVIDER", "google") + os.Setenv("IDP_CLIENT_SECRET", "TEST") + os.Setenv("SHARED_SECRET", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=") + os.Setenv("COOKIE_SECRET", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=") + os.Setenv("AUTHENTICATE_SERVICE_URL", "http://auth.server.com") + defer os.Unsetenv("IDP_CLIENT_ID") + defer os.Unsetenv("IDP_CLIENT_SECRET") + defer os.Unsetenv("SHARED_SECRET") + defer os.Unsetenv("COOKIE_SECRET") + + os.Setenv(tt.envKey, tt.envValue) + defer os.Unsetenv(tt.envKey) + + _, err := newAuthenticateService(tt.s, mux, grpcServer) + if (err != nil) != tt.wantErr { + t.Errorf("newAuthenticateService() error = %v, wantErr %v", err, tt.wantErr) + return + } + + }) + } +} + +func Test_newAuthorizeService(t *testing.T) { + os.Clearenv() + grpcAuth := middleware.NewSharedSecretCred("test") + grpcOpts := []grpc.ServerOption{grpc.UnaryInterceptor(grpcAuth.ValidateRequest)} + grpcServer := grpc.NewServer(grpcOpts...) + + tests := []struct { + name string + s string + envKey string + envValue string + + wantErr bool + }{ + {"wrong service", "proxy", "", "", false}, + {"bad option parsing", "authorize", "SHARED_SECRET", "false", true}, + {"bad env", "authorize", "POLICY", "error!", true}, + {"good", "authorize", "SHARED_SECRET", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + os.Setenv("POLICY", "LSBmcm9tOiBodHRwYmluLmNvcnAuYmV5b25kcGVyaW1ldGVyLmNvbQogIHRvOiBodHRwOi8vaHR0cGJpbgogIGFsbG93ZWRfZG9tYWluczoKICAgIC0gcG9tZXJpdW0uaW8KICBjb3JzX2FsbG93X3ByZWZsaWdodDogdHJ1ZQogIHRpbWVvdXQ6IDMwcwotIGZyb206IGV4dGVybmFsLWh0dHBiaW4uY29ycC5iZXlvbmRwZXJpbWV0ZXIuY29tCiAgdG86IGh0dHBiaW4ub3JnCiAgYWxsb3dlZF9kb21haW5zOgogICAgLSBnbWFpbC5jb20KLSBmcm9tOiB3ZWlyZGx5c3NsLmNvcnAuYmV5b25kcGVyaW1ldGVyLmNvbQogIHRvOiBodHRwOi8vbmV2ZXJzc2wuY29tCiAgYWxsb3dlZF91c2VyczoKICAgIC0gYmRkQHBvbWVyaXVtLmlvCiAgYWxsb3dlZF9ncm91cHM6CiAgICAtIGFkbWlucwogICAgLSBkZXZlbG9wZXJzCi0gZnJvbTogaGVsbG8uY29ycC5iZXlvbmRwZXJpbWV0ZXIuY29tCiAgdG86IGh0dHA6Ly9oZWxsbzo4MDgwCiAgYWxsb3dlZF9ncm91cHM6CiAgICAtIGFkbWlucw==") + os.Setenv("COOKIE_SECRET", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=") + defer os.Unsetenv("SHARED_SECRET") + defer os.Unsetenv("COOKIE_SECRET") + os.Setenv(tt.envKey, tt.envValue) + defer os.Unsetenv(tt.envKey) + _, err := newAuthorizeService(tt.s, grpcServer) + if (err != nil) != tt.wantErr { + t.Errorf("newAuthorizeService() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} + +func Test_newProxyeService(t *testing.T) { + os.Clearenv() + tests := []struct { + name string + s string + envKey string + envValue string + + wantErr bool + }{ + {"wrong service", "authenticate", "", "", false}, + {"bad option parsing", "proxy", "SHARED_SECRET", "false", true}, + {"bad env", "proxy", "POLICY", "error!", true}, + {"bad encoding for envar", "proxy", "COOKIE_REFRESH", "error!", true}, + {"good", "proxy", "SHARED_SECRET", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mux := http.NewServeMux() + + os.Setenv("AUTHENTICATE_SERVICE_URL", "https://authenticate.example.com") + os.Setenv("AUTHORIZE_SERVICE_URL", "https://authorize.example.com") + os.Setenv("POLICY", "LSBmcm9tOiBodHRwYmluLmNvcnAuYmV5b25kcGVyaW1ldGVyLmNvbQogIHRvOiBodHRwOi8vaHR0cGJpbgogIGFsbG93ZWRfZG9tYWluczoKICAgIC0gcG9tZXJpdW0uaW8KICBjb3JzX2FsbG93X3ByZWZsaWdodDogdHJ1ZQogIHRpbWVvdXQ6IDMwcwotIGZyb206IGV4dGVybmFsLWh0dHBiaW4uY29ycC5iZXlvbmRwZXJpbWV0ZXIuY29tCiAgdG86IGh0dHBiaW4ub3JnCiAgYWxsb3dlZF9kb21haW5zOgogICAgLSBnbWFpbC5jb20KLSBmcm9tOiB3ZWlyZGx5c3NsLmNvcnAuYmV5b25kcGVyaW1ldGVyLmNvbQogIHRvOiBodHRwOi8vbmV2ZXJzc2wuY29tCiAgYWxsb3dlZF91c2VyczoKICAgIC0gYmRkQHBvbWVyaXVtLmlvCiAgYWxsb3dlZF9ncm91cHM6CiAgICAtIGFkbWlucwogICAgLSBkZXZlbG9wZXJzCi0gZnJvbTogaGVsbG8uY29ycC5iZXlvbmRwZXJpbWV0ZXIuY29tCiAgdG86IGh0dHA6Ly9oZWxsbzo4MDgwCiAgYWxsb3dlZF9ncm91cHM6CiAgICAtIGFkbWlucw==") + os.Setenv("COOKIE_SECRET", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=") + defer os.Unsetenv("AUTHENTICATE_SERVICE_URL") + defer os.Unsetenv("AUTHORIZE_SERVICE_URL") + defer os.Unsetenv("SHARED_SECRET") + defer os.Unsetenv("COOKIE_SECRET") + os.Setenv(tt.envKey, tt.envValue) + defer os.Unsetenv(tt.envKey) + _, err := newProxyService(tt.s, mux) + if (err != nil) != tt.wantErr { + t.Errorf("newProxyService() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} + +func Test_parseOptions(t *testing.T) { + tests := []struct { + name string + envKey string + envValue string + + want *Options + wantErr bool + }{ + {"no shared secret", "", "", nil, true}, + {"good", "SHARED_SECRET", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=", &Options{Services: "all", SharedKey: "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=", LogLevel: "debug"}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + os.Setenv(tt.envKey, tt.envValue) + defer os.Unsetenv(tt.envKey) + + got, err := parseOptions() + if (err != nil) != tt.wantErr { + t.Errorf("parseOptions() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("parseOptions()\n") + t.Errorf("got: %+v\n", got) + t.Errorf("want: %+v\n", tt.want) + + } + }) + } +} diff --git a/cmd/pomerium/options.go b/cmd/pomerium/options.go index 4abf7334e..4654287cc 100644 --- a/cmd/pomerium/options.go +++ b/cmd/pomerium/options.go @@ -3,6 +3,7 @@ package main // import "github.com/pomerium/pomerium/cmd/pomerium" import ( "errors" "fmt" + "time" "github.com/pomerium/envconfig" ) @@ -43,6 +44,12 @@ type Options struct { // to HTTPS redirect server on. For example, ":http" would start a server // on port 80. If empty, no redirect server is started. HTTPRedirectAddr string `envconfig:"HTTP_REDIRECT_ADDR"` + + // Timeout settings : https://github.com/pomerium/pomerium/issues/40 + ReadTimeout time.Duration `envconfig:"TIMEOUT_READ"` + WriteTimeout time.Duration `envconfig:"TIMEOUT_WRITE"` + ReadHeaderTimeout time.Duration `envconfig:"TIMEOUT_READ_HEADER"` + IdleTimeout time.Duration `envconfig:"TIMEOUT_IDLE"` } var defaultOptions = &Options{ @@ -68,8 +75,8 @@ func optionsFromEnvConfig() (*Options, error) { } // isValidService checks to see if a service is a valid service mode -func isValidService(service string) bool { - switch service { +func isValidService(s string) bool { + switch s { case "all", "proxy", @@ -79,3 +86,33 @@ func isValidService(service string) bool { } return false } + +func isAuthenticate(s string) bool { + switch s { + case + "all", + "authenticate": + return true + } + return false +} + +func isAuthorize(s string) bool { + switch s { + case + "all", + "authorize": + return true + } + return false +} + +func isProxy(s string) bool { + switch s { + case + "all", + "proxy": + return true + } + return false +} diff --git a/cmd/pomerium/options_test.go b/cmd/pomerium/options_test.go index acd594744..df7408b30 100644 --- a/cmd/pomerium/options_test.go +++ b/cmd/pomerium/options_test.go @@ -66,3 +66,26 @@ func Test_isValidService(t *testing.T) { }) } } + +func Test_isAuthenticate(t *testing.T) { + tests := []struct { + name string + service string + want bool + }{ + {"proxy", "proxy", false}, + {"all", "all", true}, + {"authenticate", "authenticate", true}, + {"authenticate bad case", "AuThenticate", false}, + {"authorize implemented", "authorize", false}, + {"jiberish", "xd23", false}, + } + for _, tt := range tests { + + t.Run(tt.name, func(t *testing.T) { + if got := isAuthenticate(tt.service); got != tt.want { + t.Errorf("isAuthenticate() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/docs/.vuepress/public/logo-long-white.svg b/docs/.vuepress/public/logo-long-white.svg new file mode 100644 index 000000000..e76e287b9 --- /dev/null +++ b/docs/.vuepress/public/logo-long-white.svg @@ -0,0 +1 @@ +logo-long-white \ No newline at end of file diff --git a/docs/.vuepress/public/logo-long.svg b/docs/.vuepress/public/logo-long.svg new file mode 100644 index 000000000..95e947038 --- /dev/null +++ b/docs/.vuepress/public/logo-long.svg @@ -0,0 +1 @@ +logo-long \ No newline at end of file diff --git a/docs/.vuepress/public/logo-no-text.svg b/docs/.vuepress/public/logo-no-text.svg new file mode 100644 index 000000000..528a33564 --- /dev/null +++ b/docs/.vuepress/public/logo-no-text.svg @@ -0,0 +1 @@ +logo-no-text \ No newline at end of file diff --git a/docs/.vuepress/public/logo-only.png b/docs/.vuepress/public/logo-only.png new file mode 100644 index 000000000..f6965796c Binary files /dev/null and b/docs/.vuepress/public/logo-only.png differ diff --git a/docs/.vuepress/public/logo-stacked.svg b/docs/.vuepress/public/logo-stacked.svg new file mode 100644 index 000000000..41d399be9 --- /dev/null +++ b/docs/.vuepress/public/logo-stacked.svg @@ -0,0 +1 @@ +logo-stacked \ No newline at end of file diff --git a/docs/.vuepress/public/logo.svg b/docs/.vuepress/public/logo.svg index e9e3d3a70..41d399be9 100644 --- a/docs/.vuepress/public/logo.svg +++ b/docs/.vuepress/public/logo.svg @@ -1 +1 @@ -Bilevel ViaductCreated with Sketch. \ No newline at end of file +logo-stacked \ No newline at end of file diff --git a/docs/readme.md b/docs/readme.md index 445fc11da..02d01868f 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -1,8 +1,8 @@ --- home: true heroImage: logo.svg -heroText: Pomerium -tagline: Identity-aware access proxy. +heroText: " " +tagline: Pomerium is a context and identity aware access proxy. actionText: Read the docs actionLink: /docs/ --- diff --git a/docs/reference/readme.md b/docs/reference/readme.md index 1d6859d9f..eef5bbd38 100644 --- a/docs/reference/readme.md +++ b/docs/reference/readme.md @@ -61,10 +61,75 @@ head -c32 /dev/urandom | base64 - Filetype: `json` or `yaml` - Required -Policy contains the routes, and their access policies. For example, +Policy contains route specific settings, and access control details. For example, <<< @/policy.example.yaml +A list of policy configuration variables follows. + +#### From + +- `yaml`/`json` setting: `from` +- Type: `string` domain +- Required +- Example: `httpbin.corp.example.com` + +`From` is externally accessible source of the proxied request. + +#### To + +- `yaml`/`json` setting: `to` +- Type: `string` domain +- Required +- Example: `httpbin` , `192.1.20.12:20`, `http://neverssl.com` + +`To` is the destination of a proxied request. It can be an internal resource, or an external resource. + +#### Allowed Users + +- `yaml`/`json` setting: `allowed_users` +- Type: collection of `strings` +- Required +- Example: `alice@pomerium.io` , `bob@contractor.co` + +Allowed users is a collection of whitelisted users to authorize for a given route. + +#### Allowed Groups + +- `yaml`/`json` setting: `allowed_groups` +- Type: collection of `strings` +- Required +- Example: `admins` , `support@company.com` + +Allowed groups is a collection of whitelisted groups to authorize for a given route. + +#### Allowed Domains + +- `yaml`/`json` setting: `allowed_domains` +- Type: collection of `strings` +- Required +- Example: `pomerium.io` , `gmail.com` + +Allowed domains is a collection of whitelisted domains to authorize for a given route. + +#### CORS Preflight + +- `yaml`/`json` setting: `cors_allow_preflight` +- Type: `bool` +- Optional +- Default: `false` + +Allow unauthenticated HTTP OPTIONS requests as [per the CORS spec](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Preflighted_requests). + +### Timeout + +- `yaml`/`json` setting: `timeout` +- Type: [Go Duration](https://golang.org/pkg/time/#Duration.String) `string` +- Optional +- Default: `30s` + +Policy timeout establishes the per-route timeout value. Cannot exceed global timeout values. + ### Debug - Environmental Variable: `POMERIUM_DEBUG` @@ -116,6 +181,19 @@ Certificate is the x509 _public-key_ used to establish secure HTTP and gRPC conn Certificate key is the x509 _private-key_ used to establish secure HTTP and gRPC connections. If unset, pomerium will attempt to find and use `./privkey.pem`. +### Timeouts + +- Environmental Variables: `TIMEOUT_READ` `TIMEOUT_WRITE` `TIMEOUT_READ_HEADER` `TIMEOUT_IDLE` +- Type: [Go Duration](https://golang.org/pkg/time/#Duration.String) `string` +- Example: `TIMEOUT_READ=30s` +- Defaults: `TIMEOUT_READ_HEADER=10s` `TIMEOUT_READ=30s` `TIMEOUT_WRITE=0` `TIMEOUT_IDLE=5m` + +Timeouts set the global server timeouts. For route-specific timeouts, see `Policy`. + +![cloudflare blog on timeouts](https://blog.cloudflare.com/content/images/2016/06/Timeouts-001.png) + +> For a deep dive on timeout values see [these](https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/) [two](https://blog.cloudflare.com/exposing-go-on-the-internet/) excellent blog posts. + ## Authenticate Service ### Authenticate Service URL @@ -241,8 +319,16 @@ Certificate Authority is set when behind-the-ingress service communication uses - Type: map of `strings` key value pairs - Example: `X-Content-Type-Options:nosniff,X-Frame-Options:SAMEORIGIN` - To disable: `disable:true` +- Default : -Headers specifies a mapping of [HTTP Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers) to be added to proxied requests. *Nota bene* Downstream application headers will be overwritten by Pomerium's headers on conflict. + ```javascript + X-Content-Type-Options : nosniff, + X-Frame-Options:SAMEORIGIN, + X-XSS-Protection:1; mode=block, + Strict-Transport-Security:max-age=31536000; includeSubDomains; preload, + ``` + + Headers specifies a mapping of [HTTP Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers) to be added to proxied requests. _Nota bene_ Downstream application headers will be overwritten by Pomerium's headers on conflict. By default, conservative [secure HTTP headers](https://www.owasp.org/index.php/OWASP_Secure_Headers_Project) are set. diff --git a/internal/https/https.go b/internal/https/https.go index 263b83e81..ff459f3f3 100644 --- a/internal/https/https.go +++ b/internal/https/https.go @@ -23,18 +23,27 @@ type Options struct { // HTTPS requests. If empty, ":https" is used. Addr string - // Cert and Key specifies the base64 encoded TLS certificates to use. - Cert string - Key string - // CertFile and KeyFile specifies the TLS certificates to use. + // TLS certificates to use. + Cert string + Key string CertFile string KeyFile string + + // Timeouts + ReadHeaderTimeout time.Duration + ReadTimeout time.Duration + WriteTimeout time.Duration + IdleTimeout time.Duration } var defaultOptions = &Options{ - Addr: ":https", - CertFile: filepath.Join(findKeyDir(), "cert.pem"), - KeyFile: filepath.Join(findKeyDir(), "privkey.pem"), + Addr: ":https", + CertFile: filepath.Join(findKeyDir(), "cert.pem"), + KeyFile: filepath.Join(findKeyDir(), "privkey.pem"), + ReadHeaderTimeout: 10 * time.Second, + ReadTimeout: 30 * time.Second, + WriteTimeout: 0, // support streaming by default + IdleTimeout: 5 * time.Minute, } func findKeyDir() string { @@ -45,15 +54,27 @@ func findKeyDir() string { return p } -func (opt *Options) applyDefaults() { - if opt.Addr == "" { - opt.Addr = defaultOptions.Addr +func (o *Options) applyDefaults() { + if o.Addr == "" { + o.Addr = defaultOptions.Addr } - if opt.Cert == "" && opt.CertFile == "" { - opt.CertFile = defaultOptions.CertFile + if o.Cert == "" && o.CertFile == "" { + o.CertFile = defaultOptions.CertFile } - if opt.Key == "" && opt.KeyFile == "" { - opt.KeyFile = defaultOptions.KeyFile + if o.Key == "" && o.KeyFile == "" { + o.KeyFile = defaultOptions.KeyFile + } + if o.ReadHeaderTimeout == 0 { + o.ReadHeaderTimeout = defaultOptions.ReadHeaderTimeout + } + if o.ReadTimeout == 0 { + o.ReadTimeout = defaultOptions.ReadTimeout + } + if o.WriteTimeout == 0 { + o.WriteTimeout = defaultOptions.WriteTimeout + } + if o.IdleTimeout == 0 { + o.IdleTimeout = defaultOptions.IdleTimeout } } @@ -96,14 +117,13 @@ func ListenAndServeTLS(opt *Options, httpHandler http.Handler, grpcHandler *grpc // Set up the main server. server := &http.Server{ - ReadHeaderTimeout: 10 * time.Second, - ReadTimeout: 30 * time.Second, - // WriteTimeout is set to 0 for streaming replies - WriteTimeout: 0, - IdleTimeout: 5 * time.Minute, - TLSConfig: config, - Handler: h, - ErrorLog: stdlog.New(&log.StdLogWrapper{Logger: &sublogger}, "", 0), + ReadHeaderTimeout: opt.ReadHeaderTimeout, + ReadTimeout: opt.ReadTimeout, + WriteTimeout: opt.WriteTimeout, + IdleTimeout: opt.IdleTimeout, + TLSConfig: config, + Handler: h, + ErrorLog: stdlog.New(&log.StdLogWrapper{Logger: &sublogger}, "", 0), } return server.Serve(ln) diff --git a/internal/policy/policy.go b/internal/policy/policy.go index 52672d123..2fc92cb2a 100644 --- a/internal/policy/policy.go +++ b/internal/policy/policy.go @@ -14,11 +14,9 @@ import ( // Policy contains authorization policy information. // todo(bdd) : add upstream timeout and configuration settings type Policy struct { - // proxy related + // From string `yaml:"from"` To string `yaml:"to"` - // upstream transport settings - UpstreamTimeout time.Duration `yaml:"timeout"` // Identity related policy AllowedEmails []string `yaml:"allowed_users"` AllowedGroups []string `yaml:"allowed_groups"` @@ -30,6 +28,10 @@ type Policy struct { // Allow unauthenticated HTTP OPTIONS requests as per the CORS spec // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Preflighted_requests CORSAllowPreflight bool `yaml:"cors_allow_preflight"` + + // UpstreamTimeout is the route specific timeout. Must be less than the global + // timeout. If unset, route will fallback to the proxy's DefaultUpstreamTimeout. + UpstreamTimeout time.Duration `yaml:"timeout"` } func (p *Policy) validate() (err error) { diff --git a/policy.example.yaml b/policy.example.yaml index 110e2ed6c..918b6b699 100644 --- a/policy.example.yaml +++ b/policy.example.yaml @@ -1,24 +1,21 @@ - from: httpbin.corp.beyondperimeter.com to: http://httpbin allowed_domains: - - pomerium.io + - pomerium.io + cors_allow_preflight: true + timeout: 30s - from: external-httpbin.corp.beyondperimeter.com to: httpbin.org allowed_domains: - - gmail.com + - gmail.com - from: weirdlyssl.corp.beyondperimeter.com to: http://neverssl.com allowed_users: - - bdd@pomerium.io + - bdd@pomerium.io allowed_groups: - - admins - - developers + - admins + - developers - from: hello.corp.beyondperimeter.com to: http://hello:8080 allowed_groups: - - admins -- from: cross-origin.corp.beyondperimeter.com - to: httpbin.org - allowed_domains: - - gmail.com - cors_allow_preflight: true + - admins \ No newline at end of file diff --git a/proxy/proxy.go b/proxy/proxy.go index fe05813e0..7f84235bd 100755 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -109,13 +109,19 @@ func OptionsFromEnvConfig() (*Options, error) { // Validate checks that proper configuration settings are set to create // a proper Proxy instance func (o *Options) Validate() error { + decoded, err := base64.StdEncoding.DecodeString(o.SharedKey) + if err != nil { + return fmt.Errorf("authorize: `SHARED_SECRET` setting is invalid base64: %v", err) + } + if len(decoded) != 32 { + return fmt.Errorf("authorize: `SHARED_SECRET` want 32 but got %d bytes", len(decoded)) + } if len(o.Routes) != 0 { return errors.New("routes setting is deprecated, use policy instead") } if o.Policy == "" && o.PolicyFile == "" { return errors.New("proxy: either `POLICY` or `POLICY_FILE` must be non-nil") } - var err error if o.Policy != "" { confBytes, err := base64.StdEncoding.DecodeString(o.Policy) if err != nil { @@ -148,9 +154,6 @@ func (o *Options) Validate() error { if o.CookieSecret == "" { return errors.New("missing setting: cookie-secret") } - if o.SharedKey == "" { - return errors.New("missing setting: client-secret") - } decodedCookieSecret, err := base64.StdEncoding.DecodeString(o.CookieSecret) if err != nil { return fmt.Errorf("cookie secret is invalid base64: %v", err)