From 462d2f87dfcee67df34b1b73954bb5db117c7d40 Mon Sep 17 00:00:00 2001 From: Sudarsan Reddy Date: Thu, 22 Sep 2022 14:04:47 +0100 Subject: [PATCH] TUN-6774: Validate OriginRequest.Access to add Ingress.Middleware We take advantage of the JWTValidator middleware and attach it to an ingress rule based on Access configurations. We attach the Validator directly to the ingress rules because we want to take advantage of caching and token revert/handling that comes with go-oidc. --- config/configuration.go | 8 +++---- ingress/ingress.go | 35 ++++++++++++++++++++++++++++++ ingress/middleware/jwtvalidator.go | 4 ++-- ingress/rule.go | 6 ++--- ingress/rule_test.go | 8 +++---- 5 files changed, 48 insertions(+), 13 deletions(-) diff --git a/config/configuration.go b/config/configuration.go index 94e6467d..34a35612 100644 --- a/config/configuration.go +++ b/config/configuration.go @@ -234,14 +234,14 @@ type OriginRequestConfig struct { } type AccessConfig struct { - // Enabled when set to true will fail every request that does not arrive through an access authenticated endpoint. - Enabled bool + // Required when set to true will fail every request that does not arrive through an access authenticated endpoint. + Required bool `yaml:"required" json:"required,omitempty"` // TeamName is the organization team name to get the public key certificates for. - TeamName string `yaml:"teamName" json:"teamName,omitempty"` + TeamName string `yaml:"teamName" json:"teamName"` // AudTag is the AudTag to verify access JWT against. - AudTag []string `yaml:"audTag" json:"audTag,omitempty"` + AudTag []string `yaml:"audTag" json:"audTag"` } type IngressIPRule struct { diff --git a/ingress/ingress.go b/ingress/ingress.go index b15eced3..b1d87878 100644 --- a/ingress/ingress.go +++ b/ingress/ingress.go @@ -13,6 +13,7 @@ import ( "github.com/urfave/cli/v2" "github.com/cloudflare/cloudflared/config" + "github.com/cloudflare/cloudflared/ingress/middleware" "github.com/cloudflare/cloudflared/ipaccess" ) @@ -168,6 +169,28 @@ func (ing Ingress) CatchAll() *Rule { return &ing.Rules[len(ing.Rules)-1] } +func validateAccessConfiguration(cfg *config.AccessConfig) error { + if !cfg.Required { + return nil + } + + // It is possible to set `required:true` and not have these two configured yet. + // But if one of them is configured, we'd validate for correctness. + if len(cfg.AudTag) == 0 && cfg.TeamName == "" { + return nil + } + + if len(cfg.AudTag) == 0 { + return errors.New("access audtag cannot be empty") + } + + if cfg.TeamName == "" { + return errors.New("access.TeamName cannot be blank") + } + + return nil +} + func validateIngress(ingress []config.UnvalidatedIngressRule, defaults OriginRequestConfig) (Ingress, error) { rules := make([]Rule, len(ingress)) for i, r := range ingress { @@ -237,6 +260,17 @@ func validateIngress(ingress []config.UnvalidatedIngressRule, defaults OriginReq } } + var handlers []middleware.Handler + if access := r.OriginRequest.Access; access != nil { + if err := validateAccessConfiguration(access); err != nil { + return Ingress{}, err + } + if access.Required { + verifier := middleware.NewJWTValidator(access.TeamName, "", access.AudTag) + handlers = append(handlers, verifier) + } + } + if err := validateHostname(r, i, len(ingress)); err != nil { return Ingress{}, err } @@ -255,6 +289,7 @@ func validateIngress(ingress []config.UnvalidatedIngressRule, defaults OriginReq Hostname: r.Hostname, Service: service, Path: pathRegexp, + Handlers: handlers, Config: cfg, } } diff --git a/ingress/middleware/jwtvalidator.go b/ingress/middleware/jwtvalidator.go index 8bdab7e1..667bc1ce 100644 --- a/ingress/middleware/jwtvalidator.go +++ b/ingress/middleware/jwtvalidator.go @@ -42,8 +42,8 @@ func NewJWTValidator(teamName string, certsURL string, audTags []string) *JWTVal } } -func (v *JWTValidator) Handle(ctx context.Context, headers http.Header) error { - accessJWT := headers.Get(headerKeyAccessJWTAssertion) +func (v *JWTValidator) Handle(ctx context.Context, r *http.Request) error { + accessJWT := r.Header.Get(headerKeyAccessJWTAssertion) if accessJWT == "" { return ErrNoAccessToken } diff --git a/ingress/rule.go b/ingress/rule.go index b3dd0001..c7733254 100644 --- a/ingress/rule.go +++ b/ingress/rule.go @@ -4,6 +4,8 @@ import ( "encoding/json" "regexp" "strings" + + "github.com/cloudflare/cloudflared/ingress/middleware" ) // Rule routes traffic from a hostname/path on the public internet to the @@ -21,9 +23,7 @@ type Rule struct { Service OriginService `json:"service"` // Handlers is a list of functions that acts as a middleware during ProxyHTTP - // TODO TUN-6774: Uncomment when we parse ingress to this. This serves as a demonstration on how - // we want to plug in Verifiers. - // Handlers []middleware.Handler + Handlers []middleware.Handler // Configure the request cloudflared sends to this specific origin. Config OriginRequestConfig `json:"originRequest"` diff --git a/ingress/rule_test.go b/ingress/rule_test.go index d399b1be..8ab86132 100644 --- a/ingress/rule_test.go +++ b/ingress/rule_test.go @@ -182,25 +182,25 @@ func TestMarshalJSON(t *testing.T) { { name: "Nil", path: nil, - expected: `{"hostname":"example.com","path":null,"service":"https://localhost:8000","originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"Enabled":false}}}`, + expected: `{"hostname":"example.com","path":null,"service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`, want: true, }, { name: "Nil regex", path: &Regexp{Regexp: nil}, - expected: `{"hostname":"example.com","path":null,"service":"https://localhost:8000","originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"Enabled":false}}}`, + expected: `{"hostname":"example.com","path":null,"service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`, want: true, }, { name: "Empty", path: &Regexp{Regexp: regexp.MustCompile("")}, - expected: `{"hostname":"example.com","path":"","service":"https://localhost:8000","originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"Enabled":false}}}`, + expected: `{"hostname":"example.com","path":"","service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`, want: true, }, { name: "Basic", path: &Regexp{Regexp: regexp.MustCompile("/echo")}, - expected: `{"hostname":"example.com","path":"/echo","service":"https://localhost:8000","originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"Enabled":false}}}`, + expected: `{"hostname":"example.com","path":"/echo","service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`, want: true, }, }