mirror of
https://github.com/pomerium/pomerium.git
synced 2025-06-06 21:04:39 +02:00
config: add PassIdentityHeaders option (#903)
Currently, user's identity headers are always inserted to downstream request. For privacy reason, it would be better to not insert these headers by default, and let user chose whether to include these headers per=policy basis. Fixes #702
This commit is contained in:
parent
4a3fb5d44b
commit
8d0deb0732
9 changed files with 115 additions and 14 deletions
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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=',
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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"},
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue