From a96aec57d58797e8fa6b5ca196e04e0186b1fa6c Mon Sep 17 00:00:00 2001 From: Bobby DeSimone Date: Fri, 4 Oct 2019 14:51:52 -0700 Subject: [PATCH] proxy: add per-route request headers setting (#346) Signed-off-by: Bobby DeSimone --- docs/docs/CHANGELOG.md | 4 +++- docs/docs/reference/reference.md | 22 +++++++++++++++++ internal/config/policy.go | 12 ++++------ internal/middleware/middleware.go | 6 ++--- proxy/middleware.go | 14 +++++++++++ proxy/middleware_test.go | 39 +++++++++++++++++++++++++++++++ proxy/proxy.go | 6 ++++- 7 files changed, 90 insertions(+), 13 deletions(-) diff --git a/docs/docs/CHANGELOG.md b/docs/docs/CHANGELOG.md index 44be758ca..72f5af580 100644 --- a/docs/docs/CHANGELOG.md +++ b/docs/docs/CHANGELOG.md @@ -4,6 +4,7 @@ ### New +- Allow setting request headers for back-end requests on per route basis in policy. [GH-308] - Add endpoint to support "forward-auth" integration with third-party ingresses and proxies. Supports [nginx]https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-subrequest-authentication/, [nginx-ingress](https://kubernetes.github.io/ingress-nginx/examples/auth/oauth-external-auth/), and [Traefik](https://docs.traefik.io/middlewares/forwardauth/). [GH-324] - Add insecure transport support. [GH-328] - Add setting to override HTTPS backend's TLS Server Name. [GH-297] @@ -13,7 +14,7 @@ ### Security - The user's original intended location before completing the authentication process is now encrypted and kept confidential from the identity provider. [GH-316] -- Under certain circumstances, where debug logging was enabled, pomerium's shared secret could be leaked to http access logs as a query param. +- Under certain circumstances, where debug logging was enabled, pomerium's shared secret could be leaked to http access logs as a query param. [GH-338] ### Fixed @@ -288,3 +289,4 @@ [gh-319]: https://github.com/pomerium/pomerium/issues/319 [gh-328]: https://github.com/pomerium/pomerium/issues/328 [gh-332]: https://github.com/pomerium/pomerium/pull/332/ +[gh-338]: https://github.com/pomerium/pomerium/issues/338 diff --git a/docs/docs/reference/reference.md b/docs/docs/reference/reference.md index b765311a5..bc247d1f3 100644 --- a/docs/docs/reference/reference.md +++ b/docs/docs/reference/reference.md @@ -491,6 +491,28 @@ Note: This setting will replace (not append) the system's trust store for a give Pomerium supports client certificates which can be used to enforce [mutually authenticated and encrypted TLS connections](https://en.wikipedia.org/wiki/Mutual_authentication) (mTLS). For more details, see our [mTLS example repository](https://github.com/pomerium/examples/tree/master/mutual-tls) and the [certificate docs](./certificates.md). +### Set Request Headers + +- Config File Key: `set_request_headers` +- Type: map of `strings` key value pairs +- Optional + +Set Request Headers allows you to set static values for given request headers. This can be useful if you want to pass along additional information to downstream applications as headers, or set authentication header to the request. For example: + +```yaml +- from: https://httpbin.corp.example.com + to: https://httpbin.org + allowed_users: + - bdd@pomerium.io + - bobbydesimone@gmail.com + - bobby@tdia.com + set_request_headers: + # works auto-magically! + # https://httpbin.corp.example.com/basic-auth/root/hunter42 + Authorization: Basic cm9vdDpodW50ZXI0Mg== + X-Your-favorite-authenticating-Proxy: "Pomerium" +``` + # Authenticate Service ## Authenticate Service URL diff --git a/internal/config/policy.go b/internal/config/policy.go index d47f6049b..50961c059 100644 --- a/internal/config/policy.go +++ b/internal/config/policy.go @@ -66,14 +66,10 @@ type Policy struct { TLSClientKeyFile string `mapstructure:"tls_client_key_file" yaml:"tls_client_key_file"` ClientCertificate *tls.Certificate - // IsForwardAuthEndpoint allows for a given route to be used as a forward-auth - // endpoint instead of a reverse proxy. Some third-party proxies that do not - // have rich access control capabilities (nginx, envoy, ambassador, traefik) - // allow you to delegate and authenticate each request to your website - // with an external server or service. Pomerium can be configured to accept - // these requests with this switch - // todo(bdd): link to docs - IsForwardAuthEndpoint bool + // SetRequestHeaders adds a collection of headers to the downstream request + // in the form of key value pairs. Note bene, this will overwrite the + // value of any existing value of a given header key. + SetRequestHeaders map[string]string `mapstructure:"set_request_headers" yaml:"set_request_headers"` } // Validate checks the validity of a policy. diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index 7c222e59a..5860e9182 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -16,13 +16,13 @@ import ( "golang.org/x/net/publicsuffix" ) -// SetHeaders ensures that every response includes some basic security headers -func SetHeaders(securityHeaders map[string]string) func(next http.Handler) http.Handler { +// SetHeaders sets a map of response headers. +func SetHeaders(headers map[string]string) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx, span := trace.StartSpan(r.Context(), "middleware.SetHeaders") defer span.End() - for key, val := range securityHeaders { + for key, val := range headers { w.Header().Set(key, val) } next.ServeHTTP(w, r.WithContext(ctx)) diff --git a/proxy/middleware.go b/proxy/middleware.go index ff985f1cf..321610fc4 100644 --- a/proxy/middleware.go +++ b/proxy/middleware.go @@ -111,3 +111,17 @@ func (p *Proxy) reqNeedsAuthentication(w http.ResponseWriter, r *http.Request) { uri := urlutil.SignedRedirectURL(p.SharedKey, p.authenticateSigninURL, urlutil.GetAbsoluteURL(r)) http.Redirect(w, r, uri.String(), http.StatusFound) } + +// SetResponseHeaders sets a map of response headers. +func SetResponseHeaders(headers map[string]string) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx, span := trace.StartSpan(r.Context(), "middleware.SetResponseHeaders") + defer span.End() + for key, val := range headers { + r.Header.Set(key, val) + } + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} diff --git a/proxy/middleware_test.go b/proxy/middleware_test.go index 96a930a2e..1b503ec9e 100644 --- a/proxy/middleware_test.go +++ b/proxy/middleware_test.go @@ -6,9 +6,11 @@ import ( "fmt" "net/http" "net/http/httptest" + "strings" "testing" "time" + "github.com/google/go-cmp/cmp" "github.com/pomerium/pomerium/internal/identity" "github.com/pomerium/pomerium/internal/sessions" "github.com/pomerium/pomerium/proxy/clients" @@ -173,3 +175,40 @@ func TestProxy_SignRequest(t *testing.T) { }) } } + +func TestProxy_SetResponseHeaders(t *testing.T) { + t.Parallel() + fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Header().Set("X-Content-Type-Options", "nosniff") + var sb strings.Builder + for k, v := range r.Header { + k = strings.ToLower(k) + for _, h := range v { + sb.WriteString(fmt.Sprintf("%v: %v\n", k, h)) + } + } + fmt.Fprint(w, sb.String()) + w.WriteHeader(http.StatusOK) + }) + tests := []struct { + name string + setHeaders map[string]string + wantHeaders string + }{ + {"good", map[string]string{"x-gonna": "give-it-to-ya"}, "x-gonna: give-it-to-ya\n"}, + {"nil", nil, ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + r := httptest.NewRequest(http.MethodGet, "/", nil) + w := httptest.NewRecorder() + got := SetResponseHeaders(tt.setHeaders)(fn) + got.ServeHTTP(w, r) + if diff := cmp.Diff(w.Body.String(), tt.wantHeaders); diff != "" { + t.Errorf("SignRequest() :\n %s", diff) + } + }) + } +} diff --git a/proxy/proxy.go b/proxy/proxy.go index 1064d07f1..0912b558c 100755 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -242,7 +242,11 @@ func (p *Proxy) reverseProxyHandler(r *mux.Router, policy *config.Policy) (*mux. } rp.Use(p.SignRequest(signer)) } - + // Optional: if additional headers are to be set for this url + if len(policy.SetRequestHeaders) != 0 { + log.Warn().Interface("headers", policy.SetRequestHeaders).Msg("proxy: set request headers") + rp.Use(SetResponseHeaders(policy.SetRequestHeaders)) + } return r, nil }