diff --git a/config/policy.go b/config/policy.go index 44a5b9bb9..ebdc1810d 100644 --- a/config/policy.go +++ b/config/policy.go @@ -89,6 +89,14 @@ type Policy struct { // // https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_set_header PreserveHostHeader bool `mapstructure:"preserve_host_header" yaml:"preserve_host_header,omitempty"` + + // PassIdentityHeaders controls whether to add a user's identity headers to the downstream request. + // These includes: + // + // - X-Pomerium-Jwt-Assertion + // - X-Pomerium-Claim-* + // + PassIdentityHeaders bool `mapstructure:"pass_identity_headers" yaml:"pass_identity_headers,omitempty"` } // Validate checks the validity of a policy. diff --git a/docs/configuration/readme.md b/docs/configuration/readme.md index 412b66ac5..f9732b369 100644 --- a/docs/configuration/readme.md +++ b/docs/configuration/readme.md @@ -1014,6 +1014,18 @@ If set, enables proxying of websocket connections. **Use with caution:** By definition, websockets are long-lived connections, so [global timeouts](#global-timeouts) are not enforced. Allowing websocket connections to the proxy could result in abuse via [DOS attacks](https://www.cloudflare.com/learning/ddos/ddos-attack-tools/slowloris/). +### Pass Identity Headers + +- `yaml`/`json` setting: `pass_identity_headers` +- Type: `bool` +- Optional +- Default: `false` + +When enabled, this option will pass the identity headers to the downstream application. These headers include: + + - X-Pomerium-Jwt-Assertion + - X-Pomerium-Claim-* + ## Authorize Service ### Authenticate Service URL diff --git a/docs/docs/CHANGELOG.md b/docs/docs/CHANGELOG.md index b69039df7..98192bc28 100644 --- a/docs/docs/CHANGELOG.md +++ b/docs/docs/CHANGELOG.md @@ -6,6 +6,11 @@ - config: add remove_request_headers @cuonglm [GH-702] - config: change default log level to INFO @cuonglm [GH-902] +- config: add pass_identity_headers @cuonglm [GH-903] + +### Changes + +- proxy: do not set X-Pomerium-Jwt-Assertion/X-Pomerium-Claim-* headers by default [GH-903] ## v0.9.1 diff --git a/docs/docs/upgrading.md b/docs/docs/upgrading.md index 63886a931..30f9f150e 100644 --- a/docs/docs/upgrading.md +++ b/docs/docs/upgrading.md @@ -5,6 +5,15 @@ description: >- for Pomerium. Please read it carefully. --- +# Since 0.10.0 + +## Breaking + +### Identity headers + +With this release, pomerium will not insert identity headers (X-Pomerium-Jwt-Asserttion/X-Pomerium-Claim-*) by default. To get pre 0.9.0 behavior, you +can set `pass_identity_headers` to true on a per-policy basis. + # Since 0.9.0 ## Breaking @@ -29,6 +38,7 @@ In `0.9.0`: option httpchk GET /ping HTTP/1.1\r\nHost:pomerium ``` +>>>>>>> c29807c3915b2e61d1a53dd007a8871b6494c3c6 # Since 0.8.0 diff --git a/integration/manifests/lib/pomerium.libsonnet b/integration/manifests/lib/pomerium.libsonnet index dbe554b30..434f34b9d 100644 --- a/integration/manifests/lib/pomerium.libsonnet +++ b/integration/manifests/lib/pomerium.libsonnet @@ -71,12 +71,14 @@ local PomeriumPolicy = function() std.flattenArrays( prefix: '/by-domain', to: 'http://' + domain + '.default.svc.cluster.local', allowed_domains: ['dogs.test'], + pass_identity_headers: false, }, { from: 'http://' + domain + '.localhost.pomerium.io', prefix: '/by-user', to: 'http://' + domain + '.default.svc.cluster.local', allowed_users: ['bob@dogs.test'], + pass_identity_headers: true, }, { from: 'http://' + domain + '.localhost.pomerium.io', @@ -185,6 +187,7 @@ local PomeriumConfigMap = function() { CACHE_SERVICE_URL: 'https://cache.default.svc.cluster.local:5443', FORWARD_AUTH_URL: 'https://forward-authenticate.localhost.pomerium.io', HEADERS: 'X-Frame-Options:SAMEORIGIN', + JWT_CLAIMS_HEADERS: 'email', SHARED_SECRET: 'Wy+c0uSuIM0yGGXs82MBwTZwRiZ7Ki2T0LANnmzUtkI=', COOKIE_SECRET: 'eZ91a/j9fhgki9zPDU5zHdQWX4io89pJanChMVa5OoM=', diff --git a/integration/policy_test.go b/integration/policy_test.go index e635ca00f..9e336df5a 100644 --- a/integration/policy_test.go +++ b/integration/policy_test.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "encoding/json" + "fmt" "net" "net/http" "testing" @@ -433,3 +434,44 @@ func TestAttestationJWT(t *testing.T) { assert.NotEmpty(t, result.Headers["X-Pomerium-Jwt-Assertion"], "Expected JWT assertion") } + +func TestPassIdentityHeaders(t *testing.T) { + ctx := mainCtx + ctx, clearTimeout := context.WithTimeout(ctx, time.Second*30) + defer clearTimeout() + + tests := []struct { + name string + path string + wantExist bool + }{ + {"enabled", "/by-user", true}, + {"disabled", "/by-domain", false}, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + client := testcluster.NewHTTPClient() + res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io"+tc.path), + nil, flows.WithEmail("bob@dogs.test"), flows.WithGroups("user")) + if !assert.NoError(t, err, "unexpected http error") { + return + } + defer res.Body.Close() + + var result struct { + Headers map[string]string `json:"headers"` + } + err = json.NewDecoder(res.Body).Decode(&result) + if !assert.NoError(t, err) { + return + } + + for _, header := range []string{"X-Pomerium-Jwt-Assertion", "X-Pomerium-Claim-Email"} { + _, exist := result.Headers[header] + assert.True(t, exist == tc.wantExist, fmt.Sprintf("Header %s, expected: %v, got: %v", header, tc.wantExist, exist)) + } + }) + } +} diff --git a/internal/controlplane/xds_routes.go b/internal/controlplane/xds_routes.go index a5d6a07cb..8241519f5 100644 --- a/internal/controlplane/xds_routes.go +++ b/internal/controlplane/xds_routes.go @@ -13,6 +13,7 @@ import ( "google.golang.org/protobuf/types/known/structpb" "github.com/pomerium/pomerium/config" + "github.com/pomerium/pomerium/internal/httputil" ) func buildGRPCRoutes() []*envoy_config_route_v3.Route { @@ -133,6 +134,14 @@ func buildPolicyRoutes(options *config.Options, domain string) []*envoy_config_r requestHeadersToAdd = append(requestHeadersToAdd, mkEnvoyHeader(k, v)) } + requestHeadersToRemove := policy.RemoveRequestHeaders + if !policy.PassIdentityHeaders { + requestHeadersToRemove = append(requestHeadersToRemove, httputil.HeaderPomeriumJWTAssertion) + for _, claim := range options.JWTClaimsHeaders { + requestHeadersToRemove = append(requestHeadersToRemove, httputil.PomeriumJWTHeaderName(claim)) + } + } + var routeTimeout *durationpb.Duration if policy.AllowWebsockets { // disable the route timeout for websocket support @@ -182,7 +191,7 @@ func buildPolicyRoutes(options *config.Options, domain string) []*envoy_config_r }, }, RequestHeadersToAdd: requestHeadersToAdd, - RequestHeadersToRemove: policy.RemoveRequestHeaders, + RequestHeadersToRemove: requestHeadersToRemove, ResponseHeadersToAdd: responseHeadersToAdd, }) } diff --git a/internal/controlplane/xds_routes_test.go b/internal/controlplane/xds_routes_test.go index c85b95254..42685adb5 100644 --- a/internal/controlplane/xds_routes_test.go +++ b/internal/controlplane/xds_routes_test.go @@ -200,32 +200,38 @@ func Test_buildPolicyRoutes(t *testing.T) { DefaultUpstreamTimeout: time.Second * 3, Policies: []config.Policy{ { - Source: &config.StringURL{URL: mustParseURL("https://ignore.example.com")}, + Source: &config.StringURL{URL: mustParseURL("https://ignore.example.com")}, + PassIdentityHeaders: true, }, { - Source: &config.StringURL{URL: mustParseURL("https://example.com")}, + Source: &config.StringURL{URL: mustParseURL("https://example.com")}, + PassIdentityHeaders: true, }, { - Source: &config.StringURL{URL: mustParseURL("https://example.com")}, - Path: "/some/path", - AllowWebsockets: true, - PreserveHostHeader: true, + Source: &config.StringURL{URL: mustParseURL("https://example.com")}, + Path: "/some/path", + AllowWebsockets: true, + PreserveHostHeader: true, + PassIdentityHeaders: true, }, { - Source: &config.StringURL{URL: mustParseURL("https://example.com")}, - Prefix: "/some/prefix/", - SetRequestHeaders: map[string]string{"HEADER-KEY": "HEADER-VALUE"}, - UpstreamTimeout: time.Minute, + Source: &config.StringURL{URL: mustParseURL("https://example.com")}, + Prefix: "/some/prefix/", + SetRequestHeaders: map[string]string{"HEADER-KEY": "HEADER-VALUE"}, + UpstreamTimeout: time.Minute, + PassIdentityHeaders: true, }, { - Source: &config.StringURL{URL: mustParseURL("https://example.com")}, - Regex: `^/[a]+$`, + Source: &config.StringURL{URL: mustParseURL("https://example.com")}, + Regex: `^/[a]+$`, + PassIdentityHeaders: true, }, { Source: &config.StringURL{URL: mustParseURL("https://example.com")}, Prefix: "/some/prefix/", RemoveRequestHeaders: []string{"HEADER-KEY"}, UpstreamTimeout: time.Minute, + PassIdentityHeaders: true, }, }, }, "example.com") @@ -370,7 +376,8 @@ func TestAddOptionsHeadersToResponse(t *testing.T) { DefaultUpstreamTimeout: time.Second * 3, Policies: []config.Policy{ { - Source: &config.StringURL{URL: mustParseURL("https://example.com")}, + Source: &config.StringURL{URL: mustParseURL("https://example.com")}, + PassIdentityHeaders: true, }, }, Headers: map[string]string{"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload"}, diff --git a/internal/httputil/headers.go b/internal/httputil/headers.go index 87fc4f971..a3df4524d 100644 --- a/internal/httputil/headers.go +++ b/internal/httputil/headers.go @@ -66,3 +66,8 @@ var HeadersXForwarded = []string{ HeaderRealIP, HeaderSentFrom, } + +// PomeriumJWTHeaderName returns the header name set by pomerium for given JWT claim field. +func PomeriumJWTHeaderName(claim string) string { + return "x-pomerium-claim-" + claim +}