diff --git a/docs/reference/readme.md b/docs/reference/readme.md index 706b80270..1d6859d9f 100644 --- a/docs/reference/readme.md +++ b/docs/reference/readme.md @@ -33,7 +33,6 @@ Service mode sets the pomerium service(s) to run. If testing, you may want to se Address specifies the host and port to serve HTTPS and gRPC requests from. If empty, `:https`/`:443` is used. - ### HTTP Redirect Address - Environmental Variable: `HTTP_REDIRECT_ADDR` @@ -41,7 +40,7 @@ Address specifies the host and port to serve HTTPS and gRPC requests from. If em - Example: `:80`, `:http`, `:8080` - Optional -If set, the HTTP Redirect Address specifies the host and port to redirect http to https traffic on. If unset, no redirect server is started. +If set, the HTTP Redirect Address specifies the host and port to redirect http to https traffic on. If unset, no redirect server is started. ### Shared Secret @@ -206,7 +205,7 @@ Authenticate Service URL is the externally accessible URL for the authenticate s - Optional - Example: `pomerium-authenticate-service.pomerium.svc.cluster.local` -Authenticate Internal Service URL is the internally routed dns name of the authenticate service. This setting is typically used with load balancers that do not gRPC, thus allowing you to specify an internally accessible name. +Authenticate Internal Service URL is the internally routed dns name of the authenticate service. This setting is typically used with load balancers that do not gRPC, thus allowing you to specify an internally accessible name. ### Authorize Service URL @@ -215,9 +214,9 @@ Authenticate Internal Service URL is the internally routed dns name of the authe - Required - Example: `https://access.corp.example.com` or `pomerium-authorize-service.pomerium.svc.cluster.local` -Authorize Service URL is the location of the internally accessible authorize service. NOTE: Unlike authenticate, authorize has no publicly accessible http handlers so this setting is purely for gRPC communication. +Authorize Service URL is the location of the internally accessible authorize service. NOTE: Unlike authenticate, authorize has no publicly accessible http handlers so this setting is purely for gRPC communication. -If your load balancer does not support gRPC pass-through you'll need to set this value to an internally routable location (`pomerium-authorize-service.pomerium.svc.cluster.local`) instead of an externally routable one (`https://access.corp.example.com`). +If your load balancer does not support gRPC pass-through you'll need to set this value to an internally routable location (`pomerium-authorize-service.pomerium.svc.cluster.local`) instead of an externally routable one (`https://access.corp.example.com`). ### Override Certificate Name @@ -236,6 +235,19 @@ When Authenticate Internal Service Address is set, secure service communication Certificate Authority is set when behind-the-ingress service communication uses self-signed certificates. Be sure to include the intermediary certificate. +### Headers + +- Environmental Variable: `HEADERS` +- Type: map of `strings` key value pairs +- Example: `X-Content-Type-Options:nosniff,X-Frame-Options:SAMEORIGIN` +- To disable: `disable:true` + +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. + +![pomerium security headers](./security-headers.png) + [base64 encoded]: https://en.wikipedia.org/wiki/Base64 [environmental variables]: https://en.wikipedia.org/wiki/Environment_variable [identity provider]: ./identity-providers.md diff --git a/docs/reference/security-headers.png b/docs/reference/security-headers.png new file mode 100644 index 000000000..d1c164789 Binary files /dev/null and b/docs/reference/security-headers.png differ diff --git a/go.mod b/go.mod index a2459c461..5ddb11bfd 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,8 @@ go 1.12 require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang/mock v1.2.0 - github.com/golang/protobuf v1.3.0 - github.com/pomerium/envconfig v1.4.0 + github.com/golang/protobuf v1.3.1 + github.com/pomerium/envconfig v1.5.0 github.com/pomerium/go-oidc v2.0.0+incompatible github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect github.com/rs/zerolog v1.12.0 @@ -14,8 +14,9 @@ require ( golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 + golang.org/x/text v0.3.2 // indirect google.golang.org/api v0.1.0 - google.golang.org/grpc v1.19.0 - gopkg.in/square/go-jose.v2 v2.3.0 + google.golang.org/grpc v1.19.1 + gopkg.in/square/go-jose.v2 v2.3.1 gopkg.in/yaml.v2 v2.2.2 ) diff --git a/go.sum b/go.sum index dbabb219c..db6be3b15 100644 --- a/go.sum +++ b/go.sum @@ -17,8 +17,8 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk= -github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -26,8 +26,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5 github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pomerium/envconfig v1.4.0 h1:o+WY/E/9M4fh0nDX7oJodU7N9p1hcHPsTnNLYjlbQA8= -github.com/pomerium/envconfig v1.4.0/go.mod h1:1Kz8Ca8PhJDtLYqgvbDZGn6GsJCvrT52SxQ3sPNJkDc= +github.com/pomerium/envconfig v1.5.0 h1:OeYS/p6AUxKFqCZHM5BG7pUb0m3MkaC1ZhRLPTHbk8g= +github.com/pomerium/envconfig v1.5.0/go.mod h1:1Kz8Ca8PhJDtLYqgvbDZGn6GsJCvrT52SxQ3sPNJkDc= github.com/pomerium/go-oidc v2.0.0+incompatible h1:gVvG/ExWsHQqatV+uceROnGmbVYF44mDNx5nayBhC0o= github.com/pomerium/go-oidc v2.0.0+incompatible/go.mod h1:DRsGVw6MOgxbfq4Y57jKOE8lbEfayxeiY0A8/4vxjBM= github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU= @@ -67,7 +67,10 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.1.0 h1:K6z2u68e86TPdSdefXdzvXgR1zEMa+459vBSfWYAZkI= @@ -83,12 +86,12 @@ google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.19.1 h1:TrBcJ1yqAl1G++wO39nD/qtgpsW9/1+QGrluyMGEYgM= +google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/square/go-jose.v2 v2.3.0 h1:nLzhkFyl5bkblqYBoiWJUt5JkWOzmiaBtCxdJAqJd3U= -gopkg.in/square/go-jose.v2 v2.3.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4= +gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/policy.example.yaml b/policy.example.yaml index ec1aa7e88..110e2ed6c 100644 --- a/policy.example.yaml +++ b/policy.example.yaml @@ -1,23 +1,23 @@ - from: httpbin.corp.beyondperimeter.com to: http://httpbin allowed_domains: - - pomerium.io + - pomerium.io - 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 + - admins +- from: cross-origin.corp.beyondperimeter.com to: httpbin.org allowed_domains: - gmail.com diff --git a/proxy/handlers.go b/proxy/handlers.go index b3fc26512..cca003bec 100644 --- a/proxy/handlers.go +++ b/proxy/handlers.go @@ -24,13 +24,6 @@ var ( ErrUserNotAuthorized = errors.New("user not authorized") ) -var securityHeaders = map[string]string{ - "X-Content-Type-Options": "nosniff", - "X-Frame-Options": "SAMEORIGIN", - "X-XSS-Protection": "1; mode=block", - "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload", // 1 year -} - // StateParameter holds the redirect id along with the session id. type StateParameter struct { SessionID string `json:"session_id"` @@ -63,7 +56,7 @@ func (p *Proxy) Handler() http.Handler { Str("pomerium-email", r.Header.Get(HeaderEmail)). Msg("proxy: request") })) - c = c.Append(middleware.SetHeaders(securityHeaders)) + c = c.Append(middleware.SetHeaders(p.headers)) c = c.Append(middleware.ForwardedAddrHandler("fwd_ip")) c = c.Append(middleware.RemoteAddrHandler("ip")) c = c.Append(middleware.UserAgentHandler("user_agent")) diff --git a/proxy/proxy.go b/proxy/proxy.go index e7cbda2a0..fe05813e0 100755 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -31,6 +31,8 @@ const ( HeaderEmail = "x-pomerium-authenticated-user-email" // HeaderGroups is the header key containing the user's groups. HeaderGroups = "x-pomerium-authenticated-user-groups" + // DisableHeaderKey is the key used to check whether to disable setting header + DisableHeaderKey = "disable" ) // Options represents the configurations available for the proxy service. @@ -70,6 +72,9 @@ type Options struct { CookieExpire time.Duration `envconfig:"COOKIE_EXPIRE"` CookieRefresh time.Duration `envconfig:"COOKIE_REFRESH"` + // Headers to set on all proxied requests. Add a 'disable' key map to turn off. + Headers map[string]string `envconfig:"HEADERS"` + // Sub-routes Routes map[string]string `envconfig:"ROUTES"` DefaultUpstreamTimeout time.Duration `envconfig:"DEFAULT_UPSTREAM_TIMEOUT"` @@ -83,6 +88,12 @@ var defaultOptions = &Options{ CookieExpire: time.Duration(14) * time.Hour, CookieRefresh: time.Duration(30) * time.Minute, DefaultUpstreamTimeout: time.Duration(30) * time.Second, + Headers: map[string]string{ + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "SAMEORIGIN", + "X-XSS-Protection": "1; mode=block", + "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload", + }, } // OptionsFromEnvConfig builds the identity provider service's configuration @@ -175,6 +186,7 @@ type Proxy struct { redirectURL *url.URL templates *template.Template routeConfigs map[string]*routeConfig + headers map[string]string } type routeConfig struct { @@ -212,6 +224,12 @@ func New(opts *Options) (*Proxy, error) { return nil, err } + // if the disable key is found in the security header map, clear the map + if _, disable := opts.Headers[DisableHeaderKey]; disable { + opts.Headers = make(map[string]string) + } + log.Debug().Interface("headers", opts.Headers).Msg("proxy: security headers") + p := &Proxy{ routeConfigs: make(map[string]*routeConfig), // services @@ -223,6 +241,7 @@ func New(opts *Options) (*Proxy, error) { SharedKey: opts.SharedKey, redirectURL: &url.URL{Path: "/.pomerium/callback"}, templates: templates.New(), + headers: opts.Headers, } var policies []policy.Policy if opts.Policy != "" { diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go index 9cd511300..839a44276 100644 --- a/proxy/proxy_test.go +++ b/proxy/proxy_test.go @@ -124,6 +124,7 @@ func testOptions() *Options { SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=", CookieSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=", CookieName: "pomerium", + Headers: defaultOptions.Headers, } } @@ -205,20 +206,23 @@ func TestNew(t *testing.T) { shortCookieLength.CookieSecret = "gN3xnvfsAwfCXxnJorGLKUG4l2wC8sS8nfLMhcStPg==" badRoutedProxy := testOptions() badRoutedProxy.SigningKey = "YmFkIGtleQo=" + disableHeaders := testOptions() + disableHeaders.Headers = map[string]string{"disable": "true"} tests := []struct { - name string - opts *Options - optFuncs []func(*Proxy) error - wantProxy bool - numRoutes int - wantErr bool + name string + opts *Options + wantProxy bool + numRoutes int + wantErr bool + numHeaders int }{ - {"good", good, nil, true, 1, false}, - {"empty options", &Options{}, nil, false, 0, true}, - {"nil options", nil, nil, false, 0, true}, - {"short secret/validate sanity check", shortCookieLength, nil, false, 0, true}, - {"invalid ec key, valid base64 though", badRoutedProxy, nil, false, 0, true}, + {"good", good, true, 1, false, len(defaultOptions.Headers)}, + {"empty options", &Options{}, false, 0, true, 0}, + {"nil options", nil, false, 0, true, 0}, + {"short secret/validate sanity check", shortCookieLength, false, 0, true, 0}, + {"invalid ec key, valid base64 though", badRoutedProxy, false, 0, true, 0}, + {"test disabled headers", disableHeaders, false, 1, false, 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -233,6 +237,10 @@ func TestNew(t *testing.T) { if got != nil && len(got.routeConfigs) != tt.numRoutes { t.Errorf("New() = num routeConfigs \n%+v, want \n%+v", got, tt.numRoutes) } + if got != nil && len(got.headers) != tt.numHeaders { + t.Errorf("New() = num Headers \n%+v, want \n%+v", got.headers, tt.numHeaders) + } + }) } }