From 9b80efd8e89db59e635ef53c8204eb542a7e8e16 Mon Sep 17 00:00:00 2001 From: claytonig Date: Tue, 14 Feb 2023 17:13:43 +0100 Subject: [PATCH 1/3] feat: Add support for Global External Authorization Closes #4954. Signed-off-by: claytonig Address review comments Signed-off-by: claytonig --- apis/projectcontour/v1/helpers.go | 2 +- apis/projectcontour/v1/httpproxy.go | 4 +- apis/projectcontour/v1alpha1/contourconfig.go | 5 + .../v1alpha1/zz_generated.deepcopy.go | 5 + .../4994-clayton-gonsalves-minor.md | 17 + cmd/contour/serve.go | 124 ++- cmd/contour/servecontext.go | 38 +- cmd/contour/servecontext_test.go | 46 +- examples/contour/01-crds.yaml | 164 +++- .../global-external-auth/01-authserver.yaml | 53 ++ .../02-globalextauth-extsvc.yaml | 12 + .../03-contour-config.yaml | 15 + examples/render/contour-deployment.yaml | 164 +++- .../render/contour-gateway-provisioner.yaml | 164 +++- examples/render/contour-gateway.yaml | 164 +++- examples/render/contour.yaml | 164 +++- internal/dag/dag.go | 44 +- internal/dag/httpproxy_processor.go | 173 +++-- internal/dag/httpproxy_processor_test.go | 175 +++++ internal/envoy/v3/route.go | 29 +- .../v3/global_authorization_test.go | 710 ++++++++++++++++++ internal/xdscache/v3/listener.go | 33 +- internal/xdscache/v3/route.go | 6 +- internal/xdscache/v3/route_test.go | 242 ++++++ internal/xdscache/v3/secret_test.go | 39 + pkg/config/parameters.go | 64 ++ .../docs/main/config/api-reference.html | 36 +- .../main/guides/external-authorization.md | 159 +++- test/e2e/deployment.go | 64 +- .../httpproxy/global_external_auth_test.go | 397 ++++++++++ test/e2e/httpproxy/httpproxy_test.go | 50 ++ 31 files changed, 3211 insertions(+), 151 deletions(-) create mode 100644 changelogs/unreleased/4994-clayton-gonsalves-minor.md create mode 100644 examples/global-external-auth/01-authserver.yaml create mode 100644 examples/global-external-auth/02-globalextauth-extsvc.yaml create mode 100644 examples/global-external-auth/03-contour-config.yaml create mode 100644 internal/featuretests/v3/global_authorization_test.go create mode 100644 test/e2e/httpproxy/global_external_auth_test.go diff --git a/apis/projectcontour/v1/helpers.go b/apis/projectcontour/v1/helpers.go index 947bdcdde70..531331173b4 100644 --- a/apis/projectcontour/v1/helpers.go +++ b/apis/projectcontour/v1/helpers.go @@ -20,7 +20,7 @@ import ( // AuthorizationConfigured returns whether authorization is // configured on this virtual host. func (v *VirtualHost) AuthorizationConfigured() bool { - return v.TLS != nil && v.Authorization != nil + return v.Authorization != nil } // DisableAuthorization returns true if this virtual host disables diff --git a/apis/projectcontour/v1/httpproxy.go b/apis/projectcontour/v1/httpproxy.go index 511684a4f75..66dbf01aced 100644 --- a/apis/projectcontour/v1/httpproxy.go +++ b/apis/projectcontour/v1/httpproxy.go @@ -194,8 +194,8 @@ type ExtensionServiceReference struct { type AuthorizationServer struct { // ExtensionServiceRef specifies the extension resource that will authorize client requests. // - // +required - ExtensionServiceRef ExtensionServiceReference `json:"extensionRef"` + // +optional + ExtensionServiceRef ExtensionServiceReference `json:"extensionRef,omitempty"` // AuthPolicy sets a default authorization policy for client requests. // This policy will be used unless overridden by individual routes. diff --git a/apis/projectcontour/v1alpha1/contourconfig.go b/apis/projectcontour/v1alpha1/contourconfig.go index 5b6eb1ad8bd..f810a0cfbd0 100644 --- a/apis/projectcontour/v1alpha1/contourconfig.go +++ b/apis/projectcontour/v1alpha1/contourconfig.go @@ -62,6 +62,11 @@ type ContourConfigurationSpec struct { // +optional EnableExternalNameService *bool `json:"enableExternalNameService,omitempty"` + // GlobalExternalAuthorization allows envoys external authorization filter + // to be enabled for all virtual hosts. + // +optional + GlobalExternalAuthorization *contour_api_v1.AuthorizationServer `json:"globalExtAuth,omitempty"` + // RateLimitService optionally holds properties of the Rate Limit Service // to be used for global rate limiting. // +optional diff --git a/apis/projectcontour/v1alpha1/zz_generated.deepcopy.go b/apis/projectcontour/v1alpha1/zz_generated.deepcopy.go index 7ee5df65a55..5cc64d74f5f 100644 --- a/apis/projectcontour/v1alpha1/zz_generated.deepcopy.go +++ b/apis/projectcontour/v1alpha1/zz_generated.deepcopy.go @@ -165,6 +165,11 @@ func (in *ContourConfigurationSpec) DeepCopyInto(out *ContourConfigurationSpec) *out = new(bool) **out = **in } + if in.GlobalExternalAuthorization != nil { + in, out := &in.GlobalExternalAuthorization, &out.GlobalExternalAuthorization + *out = new(v1.AuthorizationServer) + (*in).DeepCopyInto(*out) + } if in.RateLimitService != nil { in, out := &in.RateLimitService, &out.RateLimitService *out = new(RateLimitServiceConfig) diff --git a/changelogs/unreleased/4994-clayton-gonsalves-minor.md b/changelogs/unreleased/4994-clayton-gonsalves-minor.md new file mode 100644 index 00000000000..6667d17e28b --- /dev/null +++ b/changelogs/unreleased/4994-clayton-gonsalves-minor.md @@ -0,0 +1,17 @@ +## Add support for Global External Authorization for HTTPProxy. + +Contour now supports external authorization for all hosts by setting the config as part of the `contourConfig` like so: + +```yaml +globalExtAuth: + extensionService: projectcontour-auth/htpasswd + failOpen: false + authPolicy: + context: + header1: value1 + header2: value2 + responseTimeout: 1s +``` + +Individual hosts can also override or opt out of this global configuration. +You can read more about this feature in detail in the [guide](https://projectcontour.io/docs/v1.25.0/guides/external-authorization/#global-external-authorization). \ No newline at end of file diff --git a/cmd/contour/serve.go b/cmd/contour/serve.go index 4993425ca67..cd63acbf31d 100644 --- a/cmd/contour/serve.go +++ b/cmd/contour/serve.go @@ -372,6 +372,10 @@ func (s *Server) doServe() error { return err } + if listenerConfig.GlobalExternalAuthConfig, err = s.setupGlobalExternalAuthentication(contourConfiguration); err != nil { + return err + } + contourMetrics := metrics.NewMetrics(s.registry) // Endpoints updates are handled directly by the EndpointsTranslator @@ -435,19 +439,20 @@ func (s *Server) doServe() error { } builder := s.getDAGBuilder(dagBuilderConfig{ - ingressClassNames: ingressClassNames, - rootNamespaces: contourConfiguration.HTTPProxy.RootNamespaces, - gatewayControllerName: gatewayControllerName, - gatewayRef: gatewayRef, - disablePermitInsecure: *contourConfiguration.HTTPProxy.DisablePermitInsecure, - enableExternalNameService: *contourConfiguration.EnableExternalNameService, - dnsLookupFamily: contourConfiguration.Envoy.Cluster.DNSLookupFamily, - headersPolicy: contourConfiguration.Policy, - clientCert: clientCert, - fallbackCert: fallbackCert, - connectTimeout: timeouts.ConnectTimeout, - client: s.mgr.GetClient(), - metrics: contourMetrics, + ingressClassNames: ingressClassNames, + rootNamespaces: contourConfiguration.HTTPProxy.RootNamespaces, + gatewayControllerName: gatewayControllerName, + gatewayRef: gatewayRef, + disablePermitInsecure: *contourConfiguration.HTTPProxy.DisablePermitInsecure, + enableExternalNameService: *contourConfiguration.EnableExternalNameService, + dnsLookupFamily: contourConfiguration.Envoy.Cluster.DNSLookupFamily, + headersPolicy: contourConfiguration.Policy, + clientCert: clientCert, + fallbackCert: fallbackCert, + connectTimeout: timeouts.ConnectTimeout, + client: s.mgr.GetClient(), + metrics: contourMetrics, + globalExternalAuthorizationService: contourConfiguration.GlobalExternalAuthorization, }) // Build the core Kubernetes event handler. @@ -644,6 +649,55 @@ func (s *Server) setupRateLimitService(contourConfiguration contour_api_v1alpha1 }, nil } +func (s *Server) setupGlobalExternalAuthentication(contourConfiguration contour_api_v1alpha1.ContourConfigurationSpec) (*xdscache_v3.GlobalExternalAuthConfig, error) { + if contourConfiguration.GlobalExternalAuthorization == nil { + return nil, nil + } + + // ensure the specified ExtensionService exists + extensionSvc := &contour_api_v1alpha1.ExtensionService{} + + key := client.ObjectKey{ + Namespace: contourConfiguration.GlobalExternalAuthorization.ExtensionServiceRef.Namespace, + Name: contourConfiguration.GlobalExternalAuthorization.ExtensionServiceRef.Name, + } + + // Using GetAPIReader() here because the manager's caches won't be started yet, + // so reads from the manager's client (which uses the caches for reads) will fail. + if err := s.mgr.GetAPIReader().Get(context.Background(), key, extensionSvc); err != nil { + return nil, fmt.Errorf("error getting global external authorization extension service %s: %v", key, err) + } + + // get the response timeout from the ExtensionService + var responseTimeout timeout.Setting + var err error + + if tp := extensionSvc.Spec.TimeoutPolicy; tp != nil { + responseTimeout, err = timeout.Parse(tp.Response) + if err != nil { + return nil, fmt.Errorf("error parsing global http ext auth extension service %s response timeout: %v", key, err) + } + } + + var sni string + if extensionSvc.Spec.UpstreamValidation != nil { + sni = extensionSvc.Spec.UpstreamValidation.SubjectName + } + + var context map[string]string + if contourConfiguration.GlobalExternalAuthorization.AuthPolicy.Context != nil { + context = contourConfiguration.GlobalExternalAuthorization.AuthPolicy.Context + } + + return &xdscache_v3.GlobalExternalAuthConfig{ + ExtensionService: key, + SNI: sni, + Timeout: responseTimeout, + FailOpen: contourConfiguration.GlobalExternalAuthorization.FailOpen, + Context: context, + }, nil +} + func (s *Server) setupDebugService(debugConfig contour_api_v1alpha1.DebugConfig, builder *dag.Builder) error { debugsvc := &debug.Service{ Service: httpsvc.Service{ @@ -849,19 +903,20 @@ func (s *Server) setupGatewayAPI(contourConfiguration contour_api_v1alpha1.Conto } type dagBuilderConfig struct { - ingressClassNames []string - rootNamespaces []string - gatewayControllerName string - gatewayRef *types.NamespacedName - disablePermitInsecure bool - enableExternalNameService bool - dnsLookupFamily contour_api_v1alpha1.ClusterDNSFamilyType - headersPolicy *contour_api_v1alpha1.PolicyConfig - clientCert *types.NamespacedName - fallbackCert *types.NamespacedName - connectTimeout time.Duration - client client.Client - metrics *metrics.Metrics + ingressClassNames []string + rootNamespaces []string + gatewayControllerName string + gatewayRef *types.NamespacedName + disablePermitInsecure bool + enableExternalNameService bool + dnsLookupFamily contour_api_v1alpha1.ClusterDNSFamilyType + headersPolicy *contour_api_v1alpha1.PolicyConfig + clientCert *types.NamespacedName + fallbackCert *types.NamespacedName + connectTimeout time.Duration + client client.Client + metrics *metrics.Metrics + globalExternalAuthorizationService *contour_api_v1.AuthorizationServer } func (s *Server) getDAGBuilder(dbc dagBuilderConfig) *dag.Builder { @@ -930,14 +985,15 @@ func (s *Server) getDAGBuilder(dbc dagBuilderConfig) *dag.Builder { ConnectTimeout: dbc.connectTimeout, }, &dag.HTTPProxyProcessor{ - EnableExternalNameService: dbc.enableExternalNameService, - DisablePermitInsecure: dbc.disablePermitInsecure, - FallbackCertificate: dbc.fallbackCert, - DNSLookupFamily: dbc.dnsLookupFamily, - ClientCertificate: dbc.clientCert, - RequestHeadersPolicy: &requestHeadersPolicy, - ResponseHeadersPolicy: &responseHeadersPolicy, - ConnectTimeout: dbc.connectTimeout, + EnableExternalNameService: dbc.enableExternalNameService, + DisablePermitInsecure: dbc.disablePermitInsecure, + FallbackCertificate: dbc.fallbackCert, + DNSLookupFamily: dbc.dnsLookupFamily, + ClientCertificate: dbc.clientCert, + RequestHeadersPolicy: &requestHeadersPolicy, + ResponseHeadersPolicy: &responseHeadersPolicy, + ConnectTimeout: dbc.connectTimeout, + GlobalExternalAuthorization: dbc.globalExternalAuthorizationService, }, } diff --git a/cmd/contour/servecontext.go b/cmd/contour/servecontext.go index d563f7df2ec..00b07e7900c 100644 --- a/cmd/contour/servecontext.go +++ b/cmd/contour/servecontext.go @@ -23,6 +23,7 @@ import ( "strings" "time" + contour_api_v1 "github.com/projectcontour/contour/apis/projectcontour/v1" contour_api_v1alpha1 "github.com/projectcontour/contour/apis/projectcontour/v1alpha1" envoy_v3 "github.com/projectcontour/contour/internal/envoy/v3" "github.com/projectcontour/contour/internal/k8s" @@ -391,6 +392,34 @@ func (ctx *serveContext) convertToContourConfigurationSpec() contour_api_v1alpha serverHeaderTransformation = contour_api_v1alpha1.PassThroughServerHeader } + var globalExtAuth *contour_api_v1.AuthorizationServer + if ctx.Config.GlobalExternalAuthorization.ExtensionService != "" { + nsedName := k8s.NamespacedNameFrom(ctx.Config.GlobalExternalAuthorization.ExtensionService) + globalExtAuth = &contour_api_v1.AuthorizationServer{ + ExtensionServiceRef: contour_api_v1.ExtensionServiceReference{ + Name: nsedName.Name, + Namespace: nsedName.Namespace, + }, + ResponseTimeout: ctx.Config.GlobalExternalAuthorization.ResponseTimeout, + FailOpen: ctx.Config.GlobalExternalAuthorization.FailOpen, + } + + if ctx.Config.GlobalExternalAuthorization.AuthPolicy != nil { + globalExtAuth.AuthPolicy = &contour_api_v1.AuthorizationPolicy{ + Disabled: ctx.Config.GlobalExternalAuthorization.AuthPolicy.Disabled, + Context: ctx.Config.GlobalExternalAuthorization.AuthPolicy.Context, + } + } + + if ctx.Config.GlobalExternalAuthorization.WithRequestBody != nil { + globalExtAuth.WithRequestBody = &contour_api_v1.AuthorizationServerBufferSettings{ + MaxRequestBytes: ctx.Config.GlobalExternalAuthorization.WithRequestBody.MaxRequestBytes, + AllowPartialMessage: ctx.Config.GlobalExternalAuthorization.WithRequestBody.AllowPartialMessage, + PackAsBytes: ctx.Config.GlobalExternalAuthorization.WithRequestBody.PackAsBytes, + } + } + } + policy := &contour_api_v1alpha1.PolicyConfig{ RequestHeadersPolicy: &contour_api_v1alpha1.HeadersPolicy{ Set: ctx.Config.Policy.RequestHeadersPolicy.Set, @@ -504,10 +533,11 @@ func (ctx *serveContext) convertToContourConfigurationSpec() contour_api_v1alpha RootNamespaces: ctx.proxyRootNamespaces(), FallbackCertificate: fallbackCertificate, }, - EnableExternalNameService: &ctx.Config.EnableExternalNameService, - RateLimitService: rateLimitService, - Policy: policy, - Metrics: &contourMetrics, + EnableExternalNameService: &ctx.Config.EnableExternalNameService, + GlobalExternalAuthorization: globalExtAuth, + RateLimitService: rateLimitService, + Policy: policy, + Metrics: &contourMetrics, } xdsServerType := contour_api_v1alpha1.ContourServerType diff --git a/cmd/contour/servecontext_test.go b/cmd/contour/servecontext_test.go index 4d5583caa2b..cc537a78715 100644 --- a/cmd/contour/servecontext_test.go +++ b/cmd/contour/servecontext_test.go @@ -27,6 +27,7 @@ import ( "github.com/projectcontour/contour/pkg/config" "github.com/tsaarni/certyaml" + contour_api_v1 "github.com/projectcontour/contour/apis/projectcontour/v1" contour_api_v1alpha1 "github.com/projectcontour/contour/apis/projectcontour/v1alpha1" "github.com/projectcontour/contour/internal/contourconfig" envoy_v3 "github.com/projectcontour/contour/internal/envoy/v3" @@ -481,8 +482,9 @@ func TestConvertServeContext(t *testing.T) { DisablePermitInsecure: ref.To(false), FallbackCertificate: nil, }, - EnableExternalNameService: ref.To(false), - RateLimitService: nil, + EnableExternalNameService: ref.To(false), + RateLimitService: nil, + GlobalExternalAuthorization: nil, Policy: &contour_api_v1alpha1.PolicyConfig{ RequestHeadersPolicy: &contour_api_v1alpha1.HeadersPolicy{}, ResponseHeadersPolicy: &contour_api_v1alpha1.HeadersPolicy{}, @@ -699,6 +701,46 @@ func TestConvertServeContext(t *testing.T) { return cfg }, }, + "global external authorization": { + getServeContext: func(ctx *serveContext) *serveContext { + ctx.Config.GlobalExternalAuthorization = config.GlobalExternalAuthorization{ + ExtensionService: "extauthns/extauthtext", + FailOpen: true, + AuthPolicy: &config.GlobalAuthorizationPolicy{ + Context: map[string]string{ + "foo": "bar", + }, + }, + WithRequestBody: &config.GlobalAuthorizationServerBufferSettings{ + MaxRequestBytes: 512, + PackAsBytes: true, + AllowPartialMessage: true, + }, + } + return ctx + }, + getContourConfiguration: func(cfg contour_api_v1alpha1.ContourConfigurationSpec) contour_api_v1alpha1.ContourConfigurationSpec { + cfg.GlobalExternalAuthorization = &contour_api_v1.AuthorizationServer{ + ExtensionServiceRef: contour_api_v1.ExtensionServiceReference{ + Name: "extauthtext", + Namespace: "extauthns", + }, + FailOpen: true, + AuthPolicy: &contour_api_v1.AuthorizationPolicy{ + Context: map[string]string{ + "foo": "bar", + }, + Disabled: false, + }, + WithRequestBody: &contour_api_v1.AuthorizationServerBufferSettings{ + MaxRequestBytes: 512, + PackAsBytes: true, + AllowPartialMessage: true, + }, + } + return cfg + }, + }, } for name, tc := range cases { diff --git a/examples/contour/01-crds.yaml b/examples/contour/01-crds.yaml index 304047235e5..6ebf0b9d722 100644 --- a/examples/contour/01-crds.yaml +++ b/examples/contour/01-crds.yaml @@ -411,6 +411,87 @@ spec: - namespace type: object type: object + globalExtAuth: + description: GlobalExternalAuthorization allows envoys external authorization + filter to be enabled for all virtual hosts. + properties: + authPolicy: + description: AuthPolicy sets a default authorization policy for + client requests. This policy will be used unless overridden + by individual routes. + properties: + context: + additionalProperties: + type: string + description: Context is a set of key/value pairs that are + sent to the authentication server in the check request. + If a context is provided at an enclosing scope, the entries + are merged such that the inner scope overrides matching + keys from the outer scope. + type: object + disabled: + description: When true, this field disables client request + authentication for the scope of the policy. + type: boolean + type: object + extensionRef: + description: ExtensionServiceRef specifies the extension resource + that will authorize client requests. + properties: + apiVersion: + description: API version of the referent. If this field is + not specified, the default "projectcontour.io/v1alpha1" + will be used + minLength: 1 + type: string + name: + description: "Name of the referent. \n More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names" + minLength: 1 + type: string + namespace: + description: "Namespace of the referent. If this field is + not specifies, the namespace of the resource that targets + the referent will be used. \n More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/" + minLength: 1 + type: string + type: object + failOpen: + description: If FailOpen is true, the client request is forwarded + to the upstream service even if the authorization server fails + to respond. This field should not be set in most cases. It is + intended for use only while migrating applications from internal + authorization to Contour external authorization. + type: boolean + responseTimeout: + description: ResponseTimeout configures maximum time to wait for + a check response from the authorization server. Timeout durations + are expressed in the Go [Duration format](https://godoc.org/time#ParseDuration). + Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". + The string "infinity" is also a valid input and specifies no + timeout. + pattern: ^(((\d*(\.\d*)?h)|(\d*(\.\d*)?m)|(\d*(\.\d*)?s)|(\d*(\.\d*)?ms)|(\d*(\.\d*)?us)|(\d*(\.\d*)?µs)|(\d*(\.\d*)?ns))+|infinity|infinite)$ + type: string + withRequestBody: + description: WithRequestBody specifies configuration for sending + the client request's body to authorization server. + properties: + allowPartialMessage: + description: If AllowPartialMessage is true, then Envoy will + buffer the body until MaxRequestBytes are reached. + type: boolean + maxRequestBytes: + default: 1024 + description: MaxRequestBytes sets the maximum size of message + body ExtAuthz filter will hold in-memory. + format: int32 + minimum: 1 + type: integer + packAsBytes: + description: If PackAsBytes is true, the body sent to Authorization + Server is in raw bytes. + type: boolean + type: object + type: object health: description: "Health defines the endpoints Contour uses to serve health checks. \n Contour's default is { address: \"0.0.0.0\", port: 8000 @@ -3433,6 +3514,87 @@ spec: - namespace type: object type: object + globalExtAuth: + description: GlobalExternalAuthorization allows envoys external + authorization filter to be enabled for all virtual hosts. + properties: + authPolicy: + description: AuthPolicy sets a default authorization policy + for client requests. This policy will be used unless overridden + by individual routes. + properties: + context: + additionalProperties: + type: string + description: Context is a set of key/value pairs that + are sent to the authentication server in the check request. + If a context is provided at an enclosing scope, the + entries are merged such that the inner scope overrides + matching keys from the outer scope. + type: object + disabled: + description: When true, this field disables client request + authentication for the scope of the policy. + type: boolean + type: object + extensionRef: + description: ExtensionServiceRef specifies the extension resource + that will authorize client requests. + properties: + apiVersion: + description: API version of the referent. If this field + is not specified, the default "projectcontour.io/v1alpha1" + will be used + minLength: 1 + type: string + name: + description: "Name of the referent. \n More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names" + minLength: 1 + type: string + namespace: + description: "Namespace of the referent. If this field + is not specifies, the namespace of the resource that + targets the referent will be used. \n More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/" + minLength: 1 + type: string + type: object + failOpen: + description: If FailOpen is true, the client request is forwarded + to the upstream service even if the authorization server + fails to respond. This field should not be set in most cases. + It is intended for use only while migrating applications + from internal authorization to Contour external authorization. + type: boolean + responseTimeout: + description: ResponseTimeout configures maximum time to wait + for a check response from the authorization server. Timeout + durations are expressed in the Go [Duration format](https://godoc.org/time#ParseDuration). + Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", + "h". The string "infinity" is also a valid input and specifies + no timeout. + pattern: ^(((\d*(\.\d*)?h)|(\d*(\.\d*)?m)|(\d*(\.\d*)?s)|(\d*(\.\d*)?ms)|(\d*(\.\d*)?us)|(\d*(\.\d*)?µs)|(\d*(\.\d*)?ns))+|infinity|infinite)$ + type: string + withRequestBody: + description: WithRequestBody specifies configuration for sending + the client request's body to authorization server. + properties: + allowPartialMessage: + description: If AllowPartialMessage is true, then Envoy + will buffer the body until MaxRequestBytes are reached. + type: boolean + maxRequestBytes: + default: 1024 + description: MaxRequestBytes sets the maximum size of + message body ExtAuthz filter will hold in-memory. + format: int32 + minimum: 1 + type: integer + packAsBytes: + description: If PackAsBytes is true, the body sent to + Authorization Server is in raw bytes. + type: boolean + type: object + type: object health: description: "Health defines the endpoints Contour uses to serve health checks. \n Contour's default is { address: \"0.0.0.0\", @@ -5793,8 +5955,6 @@ spec: Authorization Server is in raw bytes. type: boolean type: object - required: - - extensionRef type: object corsPolicy: description: Specifies the cross-origin policy to apply to the diff --git a/examples/global-external-auth/01-authserver.yaml b/examples/global-external-auth/01-authserver.yaml new file mode 100644 index 00000000000..8a88869732d --- /dev/null +++ b/examples/global-external-auth/01-authserver.yaml @@ -0,0 +1,53 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: testserver + namespace: projectcontour + labels: + app.kubernetes.io/name: testserver +spec: + selector: + matchLabels: + app.kubernetes.io/name: testserver + replicas: 1 + template: + metadata: + labels: + app.kubernetes.io/name: testserver + spec: + containers: + - name: testserver + image: docker.io/projectcontour/contour-authserver:v2 + imagePullPolicy: IfNotPresent + command: + - /contour-authserver + args: + - testserver + - --address=:9443 + ports: + - name: auth + containerPort: 9443 + protocol: TCP + resources: + limits: + cpu: 100m + memory: 30Mi + +--- + +apiVersion: v1 +kind: Service +metadata: + name: testserver + namespace: projectcontour + labels: + app.kubernetes.io/name: testserver +spec: + ports: + - name: auth + protocol: TCP + port: 9443 + targetPort: 9443 + selector: + app.kubernetes.io/name: testserver + type: ClusterIP \ No newline at end of file diff --git a/examples/global-external-auth/02-globalextauth-extsvc.yaml b/examples/global-external-auth/02-globalextauth-extsvc.yaml new file mode 100644 index 00000000000..5beb309b4ee --- /dev/null +++ b/examples/global-external-auth/02-globalextauth-extsvc.yaml @@ -0,0 +1,12 @@ +apiVersion: projectcontour.io/v1alpha1 +kind: ExtensionService +metadata: + namespace: projectcontour + name: testserver +spec: + protocol: h2c + services: + - name: testserver + port: 9443 + timeoutPolicy: + response: 100ms diff --git a/examples/global-external-auth/03-contour-config.yaml b/examples/global-external-auth/03-contour-config.yaml new file mode 100644 index 00000000000..a1bf13a02ee --- /dev/null +++ b/examples/global-external-auth/03-contour-config.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: contour + namespace: projectcontour +data: + contour.yaml: | + globalExtAuth: + extensionService: projectcontour/testserver + failOpen: false + authPolicy: + context: + header1: value1 + header2: value2 + responseTimeout: 1s diff --git a/examples/render/contour-deployment.yaml b/examples/render/contour-deployment.yaml index 02f898d4de5..686e07109e4 100644 --- a/examples/render/contour-deployment.yaml +++ b/examples/render/contour-deployment.yaml @@ -624,6 +624,87 @@ spec: - namespace type: object type: object + globalExtAuth: + description: GlobalExternalAuthorization allows envoys external authorization + filter to be enabled for all virtual hosts. + properties: + authPolicy: + description: AuthPolicy sets a default authorization policy for + client requests. This policy will be used unless overridden + by individual routes. + properties: + context: + additionalProperties: + type: string + description: Context is a set of key/value pairs that are + sent to the authentication server in the check request. + If a context is provided at an enclosing scope, the entries + are merged such that the inner scope overrides matching + keys from the outer scope. + type: object + disabled: + description: When true, this field disables client request + authentication for the scope of the policy. + type: boolean + type: object + extensionRef: + description: ExtensionServiceRef specifies the extension resource + that will authorize client requests. + properties: + apiVersion: + description: API version of the referent. If this field is + not specified, the default "projectcontour.io/v1alpha1" + will be used + minLength: 1 + type: string + name: + description: "Name of the referent. \n More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names" + minLength: 1 + type: string + namespace: + description: "Namespace of the referent. If this field is + not specifies, the namespace of the resource that targets + the referent will be used. \n More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/" + minLength: 1 + type: string + type: object + failOpen: + description: If FailOpen is true, the client request is forwarded + to the upstream service even if the authorization server fails + to respond. This field should not be set in most cases. It is + intended for use only while migrating applications from internal + authorization to Contour external authorization. + type: boolean + responseTimeout: + description: ResponseTimeout configures maximum time to wait for + a check response from the authorization server. Timeout durations + are expressed in the Go [Duration format](https://godoc.org/time#ParseDuration). + Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". + The string "infinity" is also a valid input and specifies no + timeout. + pattern: ^(((\d*(\.\d*)?h)|(\d*(\.\d*)?m)|(\d*(\.\d*)?s)|(\d*(\.\d*)?ms)|(\d*(\.\d*)?us)|(\d*(\.\d*)?µs)|(\d*(\.\d*)?ns))+|infinity|infinite)$ + type: string + withRequestBody: + description: WithRequestBody specifies configuration for sending + the client request's body to authorization server. + properties: + allowPartialMessage: + description: If AllowPartialMessage is true, then Envoy will + buffer the body until MaxRequestBytes are reached. + type: boolean + maxRequestBytes: + default: 1024 + description: MaxRequestBytes sets the maximum size of message + body ExtAuthz filter will hold in-memory. + format: int32 + minimum: 1 + type: integer + packAsBytes: + description: If PackAsBytes is true, the body sent to Authorization + Server is in raw bytes. + type: boolean + type: object + type: object health: description: "Health defines the endpoints Contour uses to serve health checks. \n Contour's default is { address: \"0.0.0.0\", port: 8000 @@ -3646,6 +3727,87 @@ spec: - namespace type: object type: object + globalExtAuth: + description: GlobalExternalAuthorization allows envoys external + authorization filter to be enabled for all virtual hosts. + properties: + authPolicy: + description: AuthPolicy sets a default authorization policy + for client requests. This policy will be used unless overridden + by individual routes. + properties: + context: + additionalProperties: + type: string + description: Context is a set of key/value pairs that + are sent to the authentication server in the check request. + If a context is provided at an enclosing scope, the + entries are merged such that the inner scope overrides + matching keys from the outer scope. + type: object + disabled: + description: When true, this field disables client request + authentication for the scope of the policy. + type: boolean + type: object + extensionRef: + description: ExtensionServiceRef specifies the extension resource + that will authorize client requests. + properties: + apiVersion: + description: API version of the referent. If this field + is not specified, the default "projectcontour.io/v1alpha1" + will be used + minLength: 1 + type: string + name: + description: "Name of the referent. \n More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names" + minLength: 1 + type: string + namespace: + description: "Namespace of the referent. If this field + is not specifies, the namespace of the resource that + targets the referent will be used. \n More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/" + minLength: 1 + type: string + type: object + failOpen: + description: If FailOpen is true, the client request is forwarded + to the upstream service even if the authorization server + fails to respond. This field should not be set in most cases. + It is intended for use only while migrating applications + from internal authorization to Contour external authorization. + type: boolean + responseTimeout: + description: ResponseTimeout configures maximum time to wait + for a check response from the authorization server. Timeout + durations are expressed in the Go [Duration format](https://godoc.org/time#ParseDuration). + Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", + "h". The string "infinity" is also a valid input and specifies + no timeout. + pattern: ^(((\d*(\.\d*)?h)|(\d*(\.\d*)?m)|(\d*(\.\d*)?s)|(\d*(\.\d*)?ms)|(\d*(\.\d*)?us)|(\d*(\.\d*)?µs)|(\d*(\.\d*)?ns))+|infinity|infinite)$ + type: string + withRequestBody: + description: WithRequestBody specifies configuration for sending + the client request's body to authorization server. + properties: + allowPartialMessage: + description: If AllowPartialMessage is true, then Envoy + will buffer the body until MaxRequestBytes are reached. + type: boolean + maxRequestBytes: + default: 1024 + description: MaxRequestBytes sets the maximum size of + message body ExtAuthz filter will hold in-memory. + format: int32 + minimum: 1 + type: integer + packAsBytes: + description: If PackAsBytes is true, the body sent to + Authorization Server is in raw bytes. + type: boolean + type: object + type: object health: description: "Health defines the endpoints Contour uses to serve health checks. \n Contour's default is { address: \"0.0.0.0\", @@ -6006,8 +6168,6 @@ spec: Authorization Server is in raw bytes. type: boolean type: object - required: - - extensionRef type: object corsPolicy: description: Specifies the cross-origin policy to apply to the diff --git a/examples/render/contour-gateway-provisioner.yaml b/examples/render/contour-gateway-provisioner.yaml index aec6a80a778..4e090f4c592 100644 --- a/examples/render/contour-gateway-provisioner.yaml +++ b/examples/render/contour-gateway-provisioner.yaml @@ -425,6 +425,87 @@ spec: - namespace type: object type: object + globalExtAuth: + description: GlobalExternalAuthorization allows envoys external authorization + filter to be enabled for all virtual hosts. + properties: + authPolicy: + description: AuthPolicy sets a default authorization policy for + client requests. This policy will be used unless overridden + by individual routes. + properties: + context: + additionalProperties: + type: string + description: Context is a set of key/value pairs that are + sent to the authentication server in the check request. + If a context is provided at an enclosing scope, the entries + are merged such that the inner scope overrides matching + keys from the outer scope. + type: object + disabled: + description: When true, this field disables client request + authentication for the scope of the policy. + type: boolean + type: object + extensionRef: + description: ExtensionServiceRef specifies the extension resource + that will authorize client requests. + properties: + apiVersion: + description: API version of the referent. If this field is + not specified, the default "projectcontour.io/v1alpha1" + will be used + minLength: 1 + type: string + name: + description: "Name of the referent. \n More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names" + minLength: 1 + type: string + namespace: + description: "Namespace of the referent. If this field is + not specifies, the namespace of the resource that targets + the referent will be used. \n More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/" + minLength: 1 + type: string + type: object + failOpen: + description: If FailOpen is true, the client request is forwarded + to the upstream service even if the authorization server fails + to respond. This field should not be set in most cases. It is + intended for use only while migrating applications from internal + authorization to Contour external authorization. + type: boolean + responseTimeout: + description: ResponseTimeout configures maximum time to wait for + a check response from the authorization server. Timeout durations + are expressed in the Go [Duration format](https://godoc.org/time#ParseDuration). + Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". + The string "infinity" is also a valid input and specifies no + timeout. + pattern: ^(((\d*(\.\d*)?h)|(\d*(\.\d*)?m)|(\d*(\.\d*)?s)|(\d*(\.\d*)?ms)|(\d*(\.\d*)?us)|(\d*(\.\d*)?µs)|(\d*(\.\d*)?ns))+|infinity|infinite)$ + type: string + withRequestBody: + description: WithRequestBody specifies configuration for sending + the client request's body to authorization server. + properties: + allowPartialMessage: + description: If AllowPartialMessage is true, then Envoy will + buffer the body until MaxRequestBytes are reached. + type: boolean + maxRequestBytes: + default: 1024 + description: MaxRequestBytes sets the maximum size of message + body ExtAuthz filter will hold in-memory. + format: int32 + minimum: 1 + type: integer + packAsBytes: + description: If PackAsBytes is true, the body sent to Authorization + Server is in raw bytes. + type: boolean + type: object + type: object health: description: "Health defines the endpoints Contour uses to serve health checks. \n Contour's default is { address: \"0.0.0.0\", port: 8000 @@ -3447,6 +3528,87 @@ spec: - namespace type: object type: object + globalExtAuth: + description: GlobalExternalAuthorization allows envoys external + authorization filter to be enabled for all virtual hosts. + properties: + authPolicy: + description: AuthPolicy sets a default authorization policy + for client requests. This policy will be used unless overridden + by individual routes. + properties: + context: + additionalProperties: + type: string + description: Context is a set of key/value pairs that + are sent to the authentication server in the check request. + If a context is provided at an enclosing scope, the + entries are merged such that the inner scope overrides + matching keys from the outer scope. + type: object + disabled: + description: When true, this field disables client request + authentication for the scope of the policy. + type: boolean + type: object + extensionRef: + description: ExtensionServiceRef specifies the extension resource + that will authorize client requests. + properties: + apiVersion: + description: API version of the referent. If this field + is not specified, the default "projectcontour.io/v1alpha1" + will be used + minLength: 1 + type: string + name: + description: "Name of the referent. \n More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names" + minLength: 1 + type: string + namespace: + description: "Namespace of the referent. If this field + is not specifies, the namespace of the resource that + targets the referent will be used. \n More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/" + minLength: 1 + type: string + type: object + failOpen: + description: If FailOpen is true, the client request is forwarded + to the upstream service even if the authorization server + fails to respond. This field should not be set in most cases. + It is intended for use only while migrating applications + from internal authorization to Contour external authorization. + type: boolean + responseTimeout: + description: ResponseTimeout configures maximum time to wait + for a check response from the authorization server. Timeout + durations are expressed in the Go [Duration format](https://godoc.org/time#ParseDuration). + Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", + "h". The string "infinity" is also a valid input and specifies + no timeout. + pattern: ^(((\d*(\.\d*)?h)|(\d*(\.\d*)?m)|(\d*(\.\d*)?s)|(\d*(\.\d*)?ms)|(\d*(\.\d*)?us)|(\d*(\.\d*)?µs)|(\d*(\.\d*)?ns))+|infinity|infinite)$ + type: string + withRequestBody: + description: WithRequestBody specifies configuration for sending + the client request's body to authorization server. + properties: + allowPartialMessage: + description: If AllowPartialMessage is true, then Envoy + will buffer the body until MaxRequestBytes are reached. + type: boolean + maxRequestBytes: + default: 1024 + description: MaxRequestBytes sets the maximum size of + message body ExtAuthz filter will hold in-memory. + format: int32 + minimum: 1 + type: integer + packAsBytes: + description: If PackAsBytes is true, the body sent to + Authorization Server is in raw bytes. + type: boolean + type: object + type: object health: description: "Health defines the endpoints Contour uses to serve health checks. \n Contour's default is { address: \"0.0.0.0\", @@ -5807,8 +5969,6 @@ spec: Authorization Server is in raw bytes. type: boolean type: object - required: - - extensionRef type: object corsPolicy: description: Specifies the cross-origin policy to apply to the diff --git a/examples/render/contour-gateway.yaml b/examples/render/contour-gateway.yaml index 06db853a6ef..971cf3f9f04 100644 --- a/examples/render/contour-gateway.yaml +++ b/examples/render/contour-gateway.yaml @@ -630,6 +630,87 @@ spec: - namespace type: object type: object + globalExtAuth: + description: GlobalExternalAuthorization allows envoys external authorization + filter to be enabled for all virtual hosts. + properties: + authPolicy: + description: AuthPolicy sets a default authorization policy for + client requests. This policy will be used unless overridden + by individual routes. + properties: + context: + additionalProperties: + type: string + description: Context is a set of key/value pairs that are + sent to the authentication server in the check request. + If a context is provided at an enclosing scope, the entries + are merged such that the inner scope overrides matching + keys from the outer scope. + type: object + disabled: + description: When true, this field disables client request + authentication for the scope of the policy. + type: boolean + type: object + extensionRef: + description: ExtensionServiceRef specifies the extension resource + that will authorize client requests. + properties: + apiVersion: + description: API version of the referent. If this field is + not specified, the default "projectcontour.io/v1alpha1" + will be used + minLength: 1 + type: string + name: + description: "Name of the referent. \n More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names" + minLength: 1 + type: string + namespace: + description: "Namespace of the referent. If this field is + not specifies, the namespace of the resource that targets + the referent will be used. \n More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/" + minLength: 1 + type: string + type: object + failOpen: + description: If FailOpen is true, the client request is forwarded + to the upstream service even if the authorization server fails + to respond. This field should not be set in most cases. It is + intended for use only while migrating applications from internal + authorization to Contour external authorization. + type: boolean + responseTimeout: + description: ResponseTimeout configures maximum time to wait for + a check response from the authorization server. Timeout durations + are expressed in the Go [Duration format](https://godoc.org/time#ParseDuration). + Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". + The string "infinity" is also a valid input and specifies no + timeout. + pattern: ^(((\d*(\.\d*)?h)|(\d*(\.\d*)?m)|(\d*(\.\d*)?s)|(\d*(\.\d*)?ms)|(\d*(\.\d*)?us)|(\d*(\.\d*)?µs)|(\d*(\.\d*)?ns))+|infinity|infinite)$ + type: string + withRequestBody: + description: WithRequestBody specifies configuration for sending + the client request's body to authorization server. + properties: + allowPartialMessage: + description: If AllowPartialMessage is true, then Envoy will + buffer the body until MaxRequestBytes are reached. + type: boolean + maxRequestBytes: + default: 1024 + description: MaxRequestBytes sets the maximum size of message + body ExtAuthz filter will hold in-memory. + format: int32 + minimum: 1 + type: integer + packAsBytes: + description: If PackAsBytes is true, the body sent to Authorization + Server is in raw bytes. + type: boolean + type: object + type: object health: description: "Health defines the endpoints Contour uses to serve health checks. \n Contour's default is { address: \"0.0.0.0\", port: 8000 @@ -3652,6 +3733,87 @@ spec: - namespace type: object type: object + globalExtAuth: + description: GlobalExternalAuthorization allows envoys external + authorization filter to be enabled for all virtual hosts. + properties: + authPolicy: + description: AuthPolicy sets a default authorization policy + for client requests. This policy will be used unless overridden + by individual routes. + properties: + context: + additionalProperties: + type: string + description: Context is a set of key/value pairs that + are sent to the authentication server in the check request. + If a context is provided at an enclosing scope, the + entries are merged such that the inner scope overrides + matching keys from the outer scope. + type: object + disabled: + description: When true, this field disables client request + authentication for the scope of the policy. + type: boolean + type: object + extensionRef: + description: ExtensionServiceRef specifies the extension resource + that will authorize client requests. + properties: + apiVersion: + description: API version of the referent. If this field + is not specified, the default "projectcontour.io/v1alpha1" + will be used + minLength: 1 + type: string + name: + description: "Name of the referent. \n More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names" + minLength: 1 + type: string + namespace: + description: "Namespace of the referent. If this field + is not specifies, the namespace of the resource that + targets the referent will be used. \n More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/" + minLength: 1 + type: string + type: object + failOpen: + description: If FailOpen is true, the client request is forwarded + to the upstream service even if the authorization server + fails to respond. This field should not be set in most cases. + It is intended for use only while migrating applications + from internal authorization to Contour external authorization. + type: boolean + responseTimeout: + description: ResponseTimeout configures maximum time to wait + for a check response from the authorization server. Timeout + durations are expressed in the Go [Duration format](https://godoc.org/time#ParseDuration). + Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", + "h". The string "infinity" is also a valid input and specifies + no timeout. + pattern: ^(((\d*(\.\d*)?h)|(\d*(\.\d*)?m)|(\d*(\.\d*)?s)|(\d*(\.\d*)?ms)|(\d*(\.\d*)?us)|(\d*(\.\d*)?µs)|(\d*(\.\d*)?ns))+|infinity|infinite)$ + type: string + withRequestBody: + description: WithRequestBody specifies configuration for sending + the client request's body to authorization server. + properties: + allowPartialMessage: + description: If AllowPartialMessage is true, then Envoy + will buffer the body until MaxRequestBytes are reached. + type: boolean + maxRequestBytes: + default: 1024 + description: MaxRequestBytes sets the maximum size of + message body ExtAuthz filter will hold in-memory. + format: int32 + minimum: 1 + type: integer + packAsBytes: + description: If PackAsBytes is true, the body sent to + Authorization Server is in raw bytes. + type: boolean + type: object + type: object health: description: "Health defines the endpoints Contour uses to serve health checks. \n Contour's default is { address: \"0.0.0.0\", @@ -6012,8 +6174,6 @@ spec: Authorization Server is in raw bytes. type: boolean type: object - required: - - extensionRef type: object corsPolicy: description: Specifies the cross-origin policy to apply to the diff --git a/examples/render/contour.yaml b/examples/render/contour.yaml index 1d7d6677cda..89a307be396 100644 --- a/examples/render/contour.yaml +++ b/examples/render/contour.yaml @@ -624,6 +624,87 @@ spec: - namespace type: object type: object + globalExtAuth: + description: GlobalExternalAuthorization allows envoys external authorization + filter to be enabled for all virtual hosts. + properties: + authPolicy: + description: AuthPolicy sets a default authorization policy for + client requests. This policy will be used unless overridden + by individual routes. + properties: + context: + additionalProperties: + type: string + description: Context is a set of key/value pairs that are + sent to the authentication server in the check request. + If a context is provided at an enclosing scope, the entries + are merged such that the inner scope overrides matching + keys from the outer scope. + type: object + disabled: + description: When true, this field disables client request + authentication for the scope of the policy. + type: boolean + type: object + extensionRef: + description: ExtensionServiceRef specifies the extension resource + that will authorize client requests. + properties: + apiVersion: + description: API version of the referent. If this field is + not specified, the default "projectcontour.io/v1alpha1" + will be used + minLength: 1 + type: string + name: + description: "Name of the referent. \n More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names" + minLength: 1 + type: string + namespace: + description: "Namespace of the referent. If this field is + not specifies, the namespace of the resource that targets + the referent will be used. \n More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/" + minLength: 1 + type: string + type: object + failOpen: + description: If FailOpen is true, the client request is forwarded + to the upstream service even if the authorization server fails + to respond. This field should not be set in most cases. It is + intended for use only while migrating applications from internal + authorization to Contour external authorization. + type: boolean + responseTimeout: + description: ResponseTimeout configures maximum time to wait for + a check response from the authorization server. Timeout durations + are expressed in the Go [Duration format](https://godoc.org/time#ParseDuration). + Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". + The string "infinity" is also a valid input and specifies no + timeout. + pattern: ^(((\d*(\.\d*)?h)|(\d*(\.\d*)?m)|(\d*(\.\d*)?s)|(\d*(\.\d*)?ms)|(\d*(\.\d*)?us)|(\d*(\.\d*)?µs)|(\d*(\.\d*)?ns))+|infinity|infinite)$ + type: string + withRequestBody: + description: WithRequestBody specifies configuration for sending + the client request's body to authorization server. + properties: + allowPartialMessage: + description: If AllowPartialMessage is true, then Envoy will + buffer the body until MaxRequestBytes are reached. + type: boolean + maxRequestBytes: + default: 1024 + description: MaxRequestBytes sets the maximum size of message + body ExtAuthz filter will hold in-memory. + format: int32 + minimum: 1 + type: integer + packAsBytes: + description: If PackAsBytes is true, the body sent to Authorization + Server is in raw bytes. + type: boolean + type: object + type: object health: description: "Health defines the endpoints Contour uses to serve health checks. \n Contour's default is { address: \"0.0.0.0\", port: 8000 @@ -3646,6 +3727,87 @@ spec: - namespace type: object type: object + globalExtAuth: + description: GlobalExternalAuthorization allows envoys external + authorization filter to be enabled for all virtual hosts. + properties: + authPolicy: + description: AuthPolicy sets a default authorization policy + for client requests. This policy will be used unless overridden + by individual routes. + properties: + context: + additionalProperties: + type: string + description: Context is a set of key/value pairs that + are sent to the authentication server in the check request. + If a context is provided at an enclosing scope, the + entries are merged such that the inner scope overrides + matching keys from the outer scope. + type: object + disabled: + description: When true, this field disables client request + authentication for the scope of the policy. + type: boolean + type: object + extensionRef: + description: ExtensionServiceRef specifies the extension resource + that will authorize client requests. + properties: + apiVersion: + description: API version of the referent. If this field + is not specified, the default "projectcontour.io/v1alpha1" + will be used + minLength: 1 + type: string + name: + description: "Name of the referent. \n More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names" + minLength: 1 + type: string + namespace: + description: "Namespace of the referent. If this field + is not specifies, the namespace of the resource that + targets the referent will be used. \n More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/" + minLength: 1 + type: string + type: object + failOpen: + description: If FailOpen is true, the client request is forwarded + to the upstream service even if the authorization server + fails to respond. This field should not be set in most cases. + It is intended for use only while migrating applications + from internal authorization to Contour external authorization. + type: boolean + responseTimeout: + description: ResponseTimeout configures maximum time to wait + for a check response from the authorization server. Timeout + durations are expressed in the Go [Duration format](https://godoc.org/time#ParseDuration). + Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", + "h". The string "infinity" is also a valid input and specifies + no timeout. + pattern: ^(((\d*(\.\d*)?h)|(\d*(\.\d*)?m)|(\d*(\.\d*)?s)|(\d*(\.\d*)?ms)|(\d*(\.\d*)?us)|(\d*(\.\d*)?µs)|(\d*(\.\d*)?ns))+|infinity|infinite)$ + type: string + withRequestBody: + description: WithRequestBody specifies configuration for sending + the client request's body to authorization server. + properties: + allowPartialMessage: + description: If AllowPartialMessage is true, then Envoy + will buffer the body until MaxRequestBytes are reached. + type: boolean + maxRequestBytes: + default: 1024 + description: MaxRequestBytes sets the maximum size of + message body ExtAuthz filter will hold in-memory. + format: int32 + minimum: 1 + type: integer + packAsBytes: + description: If PackAsBytes is true, the body sent to + Authorization Server is in raw bytes. + type: boolean + type: object + type: object health: description: "Health defines the endpoints Contour uses to serve health checks. \n Contour's default is { address: \"0.0.0.0\", @@ -6006,8 +6168,6 @@ spec: Authorization Server is in raw bytes. type: boolean type: object - required: - - extensionRef type: object corsPolicy: description: Specifies the cross-origin policy to apply to the diff --git a/internal/dag/dag.go b/internal/dag/dag.go index 19fc14d6705..cfbed6dc5c1 100644 --- a/internal/dag/dag.go +++ b/internal/dag/dag.go @@ -680,24 +680,9 @@ type SecureVirtualHost struct { // DownstreamValidation defines how to verify the client's certificate. DownstreamValidation *PeerValidationContext - // AuthorizationService points to the extension that client - // requests are forwarded to for authorization. If nil, no - // authorization is enabled for this host. - AuthorizationService *ExtensionCluster - - // AuthorizationResponseTimeout sets how long the proxy should wait - // for authorization server responses. - AuthorizationResponseTimeout timeout.Setting - - // AuthorizationFailOpen sets whether authorization server - // failures should cause the client request to also fail. The - // only reason to set this to `true` is when you are migrating - // from internal to external authorization. - AuthorizationFailOpen bool - - // AuthorizationServerWithRequestBody specifies configuration - // for buffering request data sent to AuthorizationServer - AuthorizationServerWithRequestBody *AuthorizationServerBufferSettings + // ExternalAuthorization contains the configuration for enabling + // the ExtAuthz filter. + ExternalAuthorization *ExternalAuthorization // JWTProviders specify how to verify JWTs. JWTProviders []JWTProvider @@ -734,6 +719,29 @@ type JWTRule struct { ProviderName string } +// ExternalAuthorization contains the configuration for enabling +// the ExtAuthz filter. +type ExternalAuthorization struct { + // AuthorizationService points to the extension that client + // requests are forwarded to for authorization. If nil, no + // authorization is enabled for this host. + AuthorizationService *ExtensionCluster + + // AuthorizationResponseTimeout sets how long the proxy should wait + // for authorization server responses. + AuthorizationResponseTimeout timeout.Setting + + // AuthorizationFailOpen sets whether authorization server + // failures should cause the client request to also fail. The + // only reason to set this to `true` is when you are migrating + // from internal to external authorization. + AuthorizationFailOpen bool + + // AuthorizationServerWithRequestBody specifies configuration + // for buffering request data sent to AuthorizationServer + AuthorizationServerWithRequestBody *AuthorizationServerBufferSettings +} + // AuthorizationServerBufferSettings enables ExtAuthz filter to buffer client // request data and send it as part of authorization request type AuthorizationServerBufferSettings struct { diff --git a/internal/dag/httpproxy_processor.go b/internal/dag/httpproxy_processor.go index 364a468c727..ea8aeb62fe9 100644 --- a/internal/dag/httpproxy_processor.go +++ b/internal/dag/httpproxy_processor.go @@ -92,6 +92,9 @@ type HTTPProxyProcessor struct { // Response headers that will be set on all routes (optional). ResponseHeadersPolicy *HeadersPolicy + // GlobalExternalAuthorization defines how requests will be authorized. + GlobalExternalAuthorization *contour_api_v1.AuthorizationServer + // ConnectTimeout defines how long the proxy should wait when establishing connection to upstream service. ConnectTimeout time.Duration } @@ -304,56 +307,8 @@ func (p *HTTPProxyProcessor) computeHTTPProxy(proxy *contour_api_v1.HTTPProxy) { svhost.DownstreamValidation = dv } - if proxy.Spec.VirtualHost.AuthorizationConfigured() { - auth := proxy.Spec.VirtualHost.Authorization - ref := defaultExtensionRef(auth.ExtensionServiceRef) - - if ref.APIVersion != contour_api_v1alpha1.GroupVersion.String() { - validCond.AddErrorf(contour_api_v1.ConditionTypeAuthError, "AuthBadResourceVersion", - "Spec.Virtualhost.Authorization.extensionRef specifies an unsupported resource version %q", auth.ExtensionServiceRef.APIVersion) - return - } - - // Lookup the extension service reference. - extensionName := types.NamespacedName{ - Name: ref.Name, - Namespace: stringOrDefault(ref.Namespace, proxy.Namespace), - } - - ext := p.dag.GetExtensionCluster(ExtensionClusterName(extensionName)) - if ext == nil { - validCond.AddErrorf(contour_api_v1.ConditionTypeAuthError, "ExtensionServiceNotFound", - "Spec.Virtualhost.Authorization.ServiceRef extension service %q not found", extensionName) - return - } - - svhost.AuthorizationService = ext - svhost.AuthorizationFailOpen = auth.FailOpen - - timeout, err := timeout.Parse(auth.ResponseTimeout) - if err != nil { - validCond.AddErrorf(contour_api_v1.ConditionTypeAuthError, "AuthResponseTimeoutInvalid", - "Spec.Virtualhost.Authorization.ResponseTimeout is invalid: %s", err) - return - } - - if timeout.UseDefault() { - svhost.AuthorizationResponseTimeout = ext.RouteTimeoutPolicy.ResponseTimeout - } else { - svhost.AuthorizationResponseTimeout = timeout - } - - if auth.WithRequestBody != nil { - var maxRequestBytes = defaultMaxRequestBytes - if auth.WithRequestBody.MaxRequestBytes != 0 { - maxRequestBytes = auth.WithRequestBody.MaxRequestBytes - } - svhost.AuthorizationServerWithRequestBody = &AuthorizationServerBufferSettings{ - MaxRequestBytes: maxRequestBytes, - AllowPartialMessage: auth.WithRequestBody.AllowPartialMessage, - PackAsBytes: auth.WithRequestBody.PackAsBytes, - } - } + if !p.computeSecureVirtualHostAuthorization(validCond, proxy, svhost) { + return } providerNames := sets.NewString() @@ -526,6 +481,10 @@ func (p *HTTPProxyProcessor) computeHTTPProxy(proxy *contour_api_v1.HTTPProxy) { } insecure.RateLimitPolicy = rlp + if p.GlobalExternalAuthorization != nil && !proxy.Spec.VirtualHost.DisableAuthorization() { + p.computeVirtualHostAuthorization(p.GlobalExternalAuthorization, validCond, proxy) + } + addRoutes(insecure, routes) // if TLS is enabled for this virtual host and there is no tcp proxy defined, @@ -781,7 +740,7 @@ func (p *HTTPProxyProcessor) computeRoutes( // If the enclosing root proxy enabled authorization, // enable it on the route and propagate defaults // downwards. - if rootProxy.Spec.VirtualHost.AuthorizationConfigured() { + if rootProxy.Spec.VirtualHost.AuthorizationConfigured() || p.GlobalExternalAuthorization != nil { // When the ext_authz filter is added to a // vhost, it is in enabled state, but we can // disable it per route. We emulate disabling @@ -797,7 +756,12 @@ func (p *HTTPProxyProcessor) computeRoutes( } r.AuthDisabled = disabled - r.AuthContext = route.AuthorizationContext(rootProxy.Spec.VirtualHost.AuthorizationContext()) + + if rootProxy.Spec.VirtualHost.AuthorizationConfigured() { + r.AuthContext = route.AuthorizationContext(rootProxy.Spec.VirtualHost.AuthorizationContext()) + } else if p.GlobalExternalAuthorization != nil { + r.AuthContext = route.AuthorizationContext(p.GlobalAuthorizationContext()) + } } if len(route.GetPrefixReplacements()) > 0 { @@ -1191,6 +1155,111 @@ func (p *HTTPProxyProcessor) rootAllowed(namespace string) bool { return false } +func (p *HTTPProxyProcessor) computeVirtualHostAuthorization(auth *contour_api_v1.AuthorizationServer, validCond *contour_api_v1.DetailedCondition, httpproxy *contour_api_v1.HTTPProxy) *ExternalAuthorization { + ok, ext := validateExternalAuthExtensionService(defaultExtensionRef(auth.ExtensionServiceRef), + validCond, + httpproxy, + p.dag.GetExtensionCluster, + ) + if !ok { + return nil + } + + ok, respTimeout := determineExternalAuthTimeout(auth.ResponseTimeout, validCond, ext) + if !ok { + return nil + } + + globalExternalAuthorization := &ExternalAuthorization{ + AuthorizationService: ext, + AuthorizationFailOpen: auth.FailOpen, + AuthorizationResponseTimeout: *respTimeout, + } + + if auth.WithRequestBody != nil { + var maxRequestBytes = defaultMaxRequestBytes + if auth.WithRequestBody.MaxRequestBytes != 0 { + maxRequestBytes = auth.WithRequestBody.MaxRequestBytes + } + globalExternalAuthorization.AuthorizationServerWithRequestBody = &AuthorizationServerBufferSettings{ + MaxRequestBytes: maxRequestBytes, + AllowPartialMessage: auth.WithRequestBody.AllowPartialMessage, + PackAsBytes: auth.WithRequestBody.PackAsBytes, + } + } + return globalExternalAuthorization +} + +func validateExternalAuthExtensionService(ref contour_api_v1.ExtensionServiceReference, validCond *contour_api_v1.DetailedCondition, httpproxy *contour_api_v1.HTTPProxy, getExtensionCluster func(name string) *ExtensionCluster) (bool, *ExtensionCluster) { + if ref.APIVersion != contour_api_v1alpha1.GroupVersion.String() { + validCond.AddErrorf(contour_api_v1.ConditionTypeAuthError, "AuthBadResourceVersion", + "Spec.Virtualhost.Authorization.extensionRef specifies an unsupported resource version %q", ref.APIVersion) + return false, nil + } + + // Lookup the extension service reference. + extensionName := types.NamespacedName{ + Name: ref.Name, + Namespace: stringOrDefault(ref.Namespace, httpproxy.Namespace), + } + + ext := getExtensionCluster(ExtensionClusterName(extensionName)) + if ext == nil { + validCond.AddErrorf(contour_api_v1.ConditionTypeAuthError, "ExtensionServiceNotFound", + "Spec.Virtualhost.Authorization.ServiceRef extension service %q not found", extensionName) + return false, ext + } + + return true, ext +} + +func determineExternalAuthTimeout(responseTimeout string, validCond *contour_api_v1.DetailedCondition, ext *ExtensionCluster) (bool, *timeout.Setting) { + timeout, err := timeout.Parse(responseTimeout) + if err != nil { + validCond.AddErrorf(contour_api_v1.ConditionTypeAuthError, "AuthResponseTimeoutInvalid", + "Spec.Virtualhost.Authorization.ResponseTimeout is invalid: %s", err) + return false, nil + } + + if timeout.UseDefault() { + return true, &ext.RouteTimeoutPolicy.ResponseTimeout + } + + return true, &timeout +} + +func (p *HTTPProxyProcessor) computeSecureVirtualHostAuthorization(validCond *contour_api_v1.DetailedCondition, httpproxy *contour_api_v1.HTTPProxy, svhost *SecureVirtualHost) bool { + if httpproxy.Spec.VirtualHost.AuthorizationConfigured() && !httpproxy.Spec.VirtualHost.DisableAuthorization() { + authorization := p.computeVirtualHostAuthorization(httpproxy.Spec.VirtualHost.Authorization, validCond, httpproxy) + if authorization == nil { + return false + } + + svhost.ExternalAuthorization = authorization + } else if p.GlobalExternalAuthorization != nil && !httpproxy.Spec.VirtualHost.DisableAuthorization() { + globalAuthorization := p.computeVirtualHostAuthorization(p.GlobalExternalAuthorization, validCond, httpproxy) + if globalAuthorization == nil { + return false + } + + svhost.ExternalAuthorization = globalAuthorization + } + + return true +} + +func (p *HTTPProxyProcessor) GlobalAuthorizationConfigured() bool { + return p.GlobalExternalAuthorization != nil +} + +// AuthorizationContext returns the authorization policy context (if present). +func (p *HTTPProxyProcessor) GlobalAuthorizationContext() map[string]string { + if p.GlobalAuthorizationConfigured() && p.GlobalExternalAuthorization.AuthPolicy != nil { + return p.GlobalExternalAuthorization.AuthPolicy.Context + } + return nil +} + // expandPrefixMatches adds new Routes to account for the difference // between prefix replacement when matching on '/foo' and '/foo/'. // diff --git a/internal/dag/httpproxy_processor_test.go b/internal/dag/httpproxy_processor_test.go index 3d63262f30c..93958ba9b81 100644 --- a/internal/dag/httpproxy_processor_test.go +++ b/internal/dag/httpproxy_processor_test.go @@ -18,9 +18,11 @@ import ( "time" contour_api_v1 "github.com/projectcontour/contour/apis/projectcontour/v1" + "github.com/projectcontour/contour/internal/ref" "github.com/projectcontour/contour/internal/timeout" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestDetermineSNI(t *testing.T) { @@ -696,3 +698,176 @@ func TestIncludeMatchConditionsIdentical(t *testing.T) { }) } } + +func TestValidateExternalAuthExtensionService(t *testing.T) { + tests := map[string]struct { + ref contour_api_v1.ExtensionServiceReference + wantValidCond *contour_api_v1.DetailedCondition + httpproxy *contour_api_v1.HTTPProxy + getExtensionCluster func(name string) *ExtensionCluster + want *ExtensionCluster + wantBool bool + }{ + "Unsupported API version": { + ref: contour_api_v1.ExtensionServiceReference{ + APIVersion: "wrong version", + Namespace: "ns", + Name: "test", + }, + wantValidCond: &contour_api_v1.DetailedCondition{ + Condition: v1.Condition{ + Status: contour_api_v1.ConditionTrue, + Reason: "ErrorPresent", + Message: "At least one error present, see Errors for details", + }, + Errors: []contour_api_v1.SubCondition{ + { + Type: "AuthError", + Reason: "AuthBadResourceVersion", + Message: "Spec.Virtualhost.Authorization.extensionRef specifies an unsupported resource version \"wrong version\"", + Status: contour_api_v1.ConditionTrue, + }, + }, + }, + httpproxy: &contour_api_v1.HTTPProxy{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "ns", + }, + }, + want: nil, + getExtensionCluster: func(name string) *ExtensionCluster { + return &ExtensionCluster{ + Name: "test", + } + }, + wantBool: false, + }, + "ExtensionService does not exist": { + ref: contour_api_v1.ExtensionServiceReference{ + APIVersion: "projectcontour.io/v1alpha1", + Namespace: "ns", + Name: "test", + }, + wantValidCond: &contour_api_v1.DetailedCondition{ + Condition: v1.Condition{ + Status: contour_api_v1.ConditionTrue, + Reason: "ErrorPresent", + Message: "At least one error present, see Errors for details", + }, + Errors: []contour_api_v1.SubCondition{ + { + Type: "AuthError", + Reason: "ExtensionServiceNotFound", + Message: "Spec.Virtualhost.Authorization.ServiceRef extension service \"ns/test\" not found", + Status: contour_api_v1.ConditionTrue, + }, + }, + }, + httpproxy: &contour_api_v1.HTTPProxy{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "ns", + }, + }, + getExtensionCluster: func(name string) *ExtensionCluster { + return nil + }, + want: nil, + wantBool: false, + }, + "Validation successful": { + ref: contour_api_v1.ExtensionServiceReference{ + APIVersion: "projectcontour.io/v1alpha1", + Namespace: "ns", + Name: "test", + }, + wantValidCond: &contour_api_v1.DetailedCondition{}, + httpproxy: &contour_api_v1.HTTPProxy{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "ns", + }, + }, + getExtensionCluster: func(name string) *ExtensionCluster { + return &ExtensionCluster{ + Name: "test", + } + }, + want: &ExtensionCluster{ + Name: "test", + }, + wantBool: true, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + validCond := &contour_api_v1.DetailedCondition{} + gotBool, got := validateExternalAuthExtensionService(tc.ref, validCond, tc.httpproxy, tc.getExtensionCluster) + require.Equal(t, tc.want, got) + require.Equal(t, tc.wantBool, gotBool) + require.Equal(t, tc.wantValidCond, validCond) + }) + } +} + +func TestDetermineExternalAuthTimeout(t *testing.T) { + tests := map[string]struct { + responseTimeout string + wantValidCond *contour_api_v1.DetailedCondition + ext *ExtensionCluster + want *timeout.Setting + wantBool bool + }{ + "invalid timeout": { + responseTimeout: "foo", + wantValidCond: &contour_api_v1.DetailedCondition{ + Condition: v1.Condition{ + Status: contour_api_v1.ConditionTrue, + Reason: "ErrorPresent", + Message: "At least one error present, see Errors for details", + }, + Errors: []contour_api_v1.SubCondition{ + { + Type: "AuthError", + Reason: "AuthResponseTimeoutInvalid", + Message: "Spec.Virtualhost.Authorization.ResponseTimeout is invalid: unable to parse timeout string \"foo\": time: invalid duration \"foo\"", + Status: contour_api_v1.ConditionTrue, + }, + }, + }, + }, + "default timeout": { + responseTimeout: "", + wantValidCond: &contour_api_v1.DetailedCondition{}, + ext: &ExtensionCluster{ + Name: "test", + RouteTimeoutPolicy: RouteTimeoutPolicy{ + ResponseTimeout: timeout.DurationSetting(time.Second * 10), + }, + }, + want: ref.To(timeout.DurationSetting(time.Second * 10)), + wantBool: true, + }, + "success": { + responseTimeout: "20s", + wantValidCond: &contour_api_v1.DetailedCondition{}, + ext: &ExtensionCluster{ + Name: "test", + RouteTimeoutPolicy: RouteTimeoutPolicy{ + ResponseTimeout: timeout.DurationSetting(time.Second * 10), + }, + }, + want: ref.To(timeout.DurationSetting(time.Second * 20)), + wantBool: true, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + validCond := &contour_api_v1.DetailedCondition{} + gotBool, got := determineExternalAuthTimeout(tc.responseTimeout, validCond, tc.ext) + require.Equal(t, tc.want, got) + require.Equal(t, tc.wantBool, gotBool) + require.Equal(t, tc.wantValidCond, validCond) + }) + } +} diff --git a/internal/envoy/v3/route.go b/internal/envoy/v3/route.go index 079de7dcd21..7bd378310b5 100644 --- a/internal/envoy/v3/route.go +++ b/internal/envoy/v3/route.go @@ -39,10 +39,10 @@ import ( ) // VirtualHostAndRoutes converts a DAG virtual host and routes to an Envoy virtual host. -func VirtualHostAndRoutes(vh *dag.VirtualHost, dagRoutes []*dag.Route, secure bool, authService *dag.ExtensionCluster) *envoy_route_v3.VirtualHost { +func VirtualHostAndRoutes(vh *dag.VirtualHost, dagRoutes []*dag.Route, secure bool) *envoy_route_v3.VirtualHost { var envoyRoutes []*envoy_route_v3.Route for _, route := range dagRoutes { - envoyRoutes = append(envoyRoutes, buildRoute(route, vh.Name, secure, authService)) + envoyRoutes = append(envoyRoutes, buildRoute(route, vh.Name, secure)) } evh := VirtualHost(vh.Name, envoyRoutes...) @@ -68,7 +68,7 @@ func VirtualHostAndRoutes(vh *dag.VirtualHost, dagRoutes []*dag.Route, secure bo } // buildRoute converts a DAG route to an Envoy route. -func buildRoute(dagRoute *dag.Route, vhostName string, secure bool, authService *dag.ExtensionCluster) *envoy_route_v3.Route { +func buildRoute(dagRoute *dag.Route, vhostName string, secure bool) *envoy_route_v3.Route { switch { case dagRoute.HTTPSUpgrade && !secure: // TODO(dfc) if we ensure the builder never returns a dag.Route connected @@ -111,20 +111,17 @@ func buildRoute(dagRoute *dag.Route, vhostName string, secure bool, authService rt.TypedPerFilterConfig["envoy.filters.http.local_ratelimit"] = LocalRateLimitConfig(dagRoute.RateLimitPolicy.Local, "vhost."+vhostName) } - // If authorization is enabled on this host, we may need to set per-route filter overrides. - if authService != nil { - // Apply per-route authorization policy modifications. - if dagRoute.AuthDisabled { - if rt.TypedPerFilterConfig == nil { - rt.TypedPerFilterConfig = map[string]*anypb.Any{} - } - rt.TypedPerFilterConfig["envoy.filters.http.ext_authz"] = routeAuthzDisabled() - } else if len(dagRoute.AuthContext) > 0 { - if rt.TypedPerFilterConfig == nil { - rt.TypedPerFilterConfig = map[string]*anypb.Any{} - } - rt.TypedPerFilterConfig["envoy.filters.http.ext_authz"] = routeAuthzContext(dagRoute.AuthContext) + // Apply per-route authorization policy modifications. + if dagRoute.AuthDisabled { + if rt.TypedPerFilterConfig == nil { + rt.TypedPerFilterConfig = map[string]*anypb.Any{} + } + rt.TypedPerFilterConfig["envoy.filters.http.ext_authz"] = routeAuthzDisabled() + } else if len(dagRoute.AuthContext) > 0 { + if rt.TypedPerFilterConfig == nil { + rt.TypedPerFilterConfig = map[string]*anypb.Any{} } + rt.TypedPerFilterConfig["envoy.filters.http.ext_authz"] = routeAuthzContext(dagRoute.AuthContext) } // If JWT verification is enabled, add per-route filter diff --git a/internal/featuretests/v3/global_authorization_test.go b/internal/featuretests/v3/global_authorization_test.go new file mode 100644 index 00000000000..23d2df13dc3 --- /dev/null +++ b/internal/featuretests/v3/global_authorization_test.go @@ -0,0 +1,710 @@ +// Copyright Project Contour Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v3 + +import ( + "testing" + + envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" + envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + envoy_config_filter_http_ext_authz_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ext_authz/v3" + http "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + envoy_discovery_v3 "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" + envoy_type "github.com/envoyproxy/go-control-plane/envoy/type/v3" + "github.com/envoyproxy/go-control-plane/pkg/wellknown" + contour_api_v1 "github.com/projectcontour/contour/apis/projectcontour/v1" + "github.com/projectcontour/contour/apis/projectcontour/v1alpha1" + contour_api_v1alpha1 "github.com/projectcontour/contour/apis/projectcontour/v1alpha1" + "github.com/projectcontour/contour/internal/dag" + envoy_v3 "github.com/projectcontour/contour/internal/envoy/v3" + "github.com/projectcontour/contour/internal/featuretests" + "github.com/projectcontour/contour/internal/fixture" + "github.com/projectcontour/contour/internal/k8s" + "github.com/projectcontour/contour/internal/protobuf" + "github.com/projectcontour/contour/internal/timeout" + xdscache_v3 "github.com/projectcontour/contour/internal/xdscache/v3" + "google.golang.org/protobuf/types/known/anypb" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/cache" +) + +func globalExternalAuthorizationFilterExists(t *testing.T, rh cache.ResourceEventHandler, c *Contour) { + p := &contour_api_v1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "proxy1", + }, + Spec: contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{ + Fqdn: "foo.com", + }, + Routes: []contour_api_v1.Route{ + { + Services: []contour_api_v1.Service{ + { + Name: "s1", + Port: 80, + }, + }, + }, + }, + }, + } + rh.OnAdd(p) + + var httpListener = defaultHTTPListener() + + // replace the default filter chains with an HCM that includes the global + // extAuthz filter. + var hcm = envoy_v3.HTTPConnectionManagerBuilder(). + RouteConfigName("ingress_http"). + MetricsPrefix("ingress_http"). + AccessLoggers(envoy_v3.FileAccessLogEnvoy("/dev/stdout", "", nil, contour_api_v1alpha1.LogLevelInfo)). + DefaultFilters(). + AddFilter(&http.HttpFilter{ + Name: wellknown.HTTPExternalAuthorization, + ConfigType: &http.HttpFilter_TypedConfig{ + TypedConfig: protobuf.MustMarshalAny(&envoy_config_filter_http_ext_authz_v3.ExtAuthz{ + Services: grpcCluster("extension/auth/extension"), + ClearRouteCache: true, + IncludePeerCertificate: true, + StatusOnError: &envoy_type.HttpStatus{ + Code: envoy_type.StatusCode_Forbidden, + }, + TransportApiVersion: envoy_core_v3.ApiVersion_V3, + }), + }, + }). + Get() + + httpListener.FilterChains = envoy_v3.FilterChains(hcm) + + c.Request(listenerType).Equals(&envoy_discovery_v3.DiscoveryResponse{ + TypeUrl: listenerType, + Resources: resources(t, + httpListener, + statsListener()), + }).Status(p).IsValid() +} + +func globalExternalAuthorizationFilterExistsTLS(t *testing.T, rh cache.ResourceEventHandler, c *Contour) { + p := fixture.NewProxy("TLSProxy"). + WithFQDN("foo.com"). + WithSpec(contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{ + Fqdn: "foo.com", + TLS: &contour_api_v1.TLS{ + SecretName: "certificate", + }, + }, + Routes: []contour_api_v1.Route{ + { + Services: []contour_api_v1.Service{ + { + Name: "s1", + Port: 80, + }, + }, + }, + }, + }) + + rh.OnAdd(p) + + var httpListener = defaultHTTPListener() + + // replace the default filter chains with an HCM that includes the global + // extAuthz filter. + var hcm = envoy_v3.HTTPConnectionManagerBuilder(). + RouteConfigName("ingress_http"). + MetricsPrefix("ingress_http"). + AccessLoggers(envoy_v3.FileAccessLogEnvoy("/dev/stdout", "", nil, contour_api_v1alpha1.LogLevelInfo)). + DefaultFilters(). + AddFilter(&http.HttpFilter{ + Name: wellknown.HTTPExternalAuthorization, + ConfigType: &http.HttpFilter_TypedConfig{ + TypedConfig: protobuf.MustMarshalAny(&envoy_config_filter_http_ext_authz_v3.ExtAuthz{ + Services: grpcCluster("extension/auth/extension"), + ClearRouteCache: true, + IncludePeerCertificate: true, + StatusOnError: &envoy_type.HttpStatus{ + Code: envoy_type.StatusCode_Forbidden, + }, + TransportApiVersion: envoy_core_v3.ApiVersion_V3, + }), + }, + }). + Get() + + httpListener.FilterChains = envoy_v3.FilterChains(hcm) + + httpsListener := &envoy_listener_v3.Listener{ + Name: "ingress_https", + Address: envoy_v3.SocketAddress("0.0.0.0", 8443), + ListenerFilters: envoy_v3.ListenerFilters( + envoy_v3.TLSInspector(), + ), + FilterChains: []*envoy_listener_v3.FilterChain{ + filterchaintls("foo.com", + &corev1.Secret{ + ObjectMeta: fixture.ObjectMeta("certificate"), + Type: "kubernetes.io/tls", + Data: featuretests.Secretdata(featuretests.CERTIFICATE, featuretests.RSA_PRIVATE_KEY), + }, + authzFilterFor( + "foo.com", + &envoy_config_filter_http_ext_authz_v3.ExtAuthz{ + Services: grpcCluster("extension/auth/extension"), + ClearRouteCache: true, + IncludePeerCertificate: true, + StatusOnError: &envoy_type.HttpStatus{ + Code: envoy_type.StatusCode_Forbidden, + }, + TransportApiVersion: envoy_core_v3.ApiVersion_V3, + }, + ), + nil, "h2", "http/1.1"), + }, + SocketOptions: envoy_v3.TCPKeepaliveSocketOptions(), + } + + c.Request(listenerType).Equals(&envoy_discovery_v3.DiscoveryResponse{ + TypeUrl: listenerType, + Resources: resources(t, + httpListener, + httpsListener, + statsListener()), + }).Status(p).IsValid() +} + +func globalExternalAuthorizationWithTLSGlobalAuthDisabled(t *testing.T, rh cache.ResourceEventHandler, c *Contour) { + p := fixture.NewProxy("TLSProxy"). + WithFQDN("foo.com"). + WithSpec(contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{ + Fqdn: "foo.com", + TLS: &contour_api_v1.TLS{ + SecretName: "certificate", + }, + Authorization: &contour_api_v1.AuthorizationServer{ + AuthPolicy: &contour_api_v1.AuthorizationPolicy{ + Disabled: true, + }, + }, + }, + Routes: []contour_api_v1.Route{ + { + Services: []contour_api_v1.Service{ + { + Name: "s1", + Port: 80, + }, + }, + }, + }, + }) + + rh.OnAdd(p) + + var httpListener = defaultHTTPListener() + + // replace the default filter chains with an HCM that includes the global + // extAuthz filter. + var hcm = envoy_v3.HTTPConnectionManagerBuilder(). + RouteConfigName("ingress_http"). + MetricsPrefix("ingress_http"). + AccessLoggers(envoy_v3.FileAccessLogEnvoy("/dev/stdout", "", nil, contour_api_v1alpha1.LogLevelInfo)). + DefaultFilters(). + AddFilter(&http.HttpFilter{ + Name: wellknown.HTTPExternalAuthorization, + ConfigType: &http.HttpFilter_TypedConfig{ + TypedConfig: protobuf.MustMarshalAny(&envoy_config_filter_http_ext_authz_v3.ExtAuthz{ + Services: grpcCluster("extension/auth/extension"), + ClearRouteCache: true, + IncludePeerCertificate: true, + StatusOnError: &envoy_type.HttpStatus{ + Code: envoy_type.StatusCode_Forbidden, + }, + TransportApiVersion: envoy_core_v3.ApiVersion_V3, + }), + }, + }). + Get() + + httpListener.FilterChains = envoy_v3.FilterChains(hcm) + + httpsListener := &envoy_listener_v3.Listener{ + Name: "ingress_https", + Address: envoy_v3.SocketAddress("0.0.0.0", 8443), + ListenerFilters: envoy_v3.ListenerFilters( + envoy_v3.TLSInspector(), + ), + FilterChains: []*envoy_listener_v3.FilterChain{ + filterchaintls("foo.com", + &corev1.Secret{ + ObjectMeta: fixture.ObjectMeta("certificate"), + Type: "kubernetes.io/tls", + Data: featuretests.Secretdata(featuretests.CERTIFICATE, featuretests.RSA_PRIVATE_KEY), + }, + httpsFilterFor("foo.com"), + nil, "h2", "http/1.1"), + }, + SocketOptions: envoy_v3.TCPKeepaliveSocketOptions(), + } + + c.Request(listenerType).Equals(&envoy_discovery_v3.DiscoveryResponse{ + TypeUrl: listenerType, + Resources: resources(t, + httpListener, + httpsListener, + statsListener()), + }).Status(p).IsValid() +} + +func globalExternalAuthorizationWithMergedAuthPolicy(t *testing.T, rh cache.ResourceEventHandler, c *Contour) { + p := &contour_api_v1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "proxy1", + }, + Spec: contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{ + Fqdn: "foo.com", + }, + Routes: []contour_api_v1.Route{ + { + Services: []contour_api_v1.Service{ + { + Name: "s1", + Port: 80, + }, + }, + AuthPolicy: &contour_api_v1.AuthorizationPolicy{ + Context: map[string]string{ + "header_type": "proxy_config", + "header_2": "message_2", + }, + }, + }, + }, + }, + } + rh.OnAdd(p) + + var httpListener = defaultHTTPListener() + + // replace the default filter chains with an HCM that includes the global + // extAuthz filter. + var hcm = envoy_v3.HTTPConnectionManagerBuilder(). + RouteConfigName("ingress_http"). + MetricsPrefix("ingress_http"). + AccessLoggers(envoy_v3.FileAccessLogEnvoy("/dev/stdout", "", nil, contour_api_v1alpha1.LogLevelInfo)). + DefaultFilters(). + AddFilter(&http.HttpFilter{ + Name: wellknown.HTTPExternalAuthorization, + ConfigType: &http.HttpFilter_TypedConfig{ + TypedConfig: protobuf.MustMarshalAny(&envoy_config_filter_http_ext_authz_v3.ExtAuthz{ + Services: grpcCluster("extension/auth/extension"), + ClearRouteCache: true, + IncludePeerCertificate: true, + StatusOnError: &envoy_type.HttpStatus{ + Code: envoy_type.StatusCode_Forbidden, + }, + TransportApiVersion: envoy_core_v3.ApiVersion_V3, + }), + }, + }). + Get() + + httpListener.FilterChains = envoy_v3.FilterChains(hcm) + + c.Request(listenerType).Equals(&envoy_discovery_v3.DiscoveryResponse{ + TypeUrl: listenerType, + Resources: resources(t, + httpListener, + statsListener()), + }).Status(p).IsValid() + + c.Request(routeType).Equals(&envoy_discovery_v3.DiscoveryResponse{ + TypeUrl: routeType, + Resources: resources(t, + envoy_v3.RouteConfiguration( + "ingress_http", + envoy_v3.VirtualHost("foo.com", + &envoy_route_v3.Route{ + Match: routePrefix("/"), + Action: routeCluster("default/s1/80/da39a3ee5e"), + TypedPerFilterConfig: map[string]*anypb.Any{ + "envoy.filters.http.ext_authz": protobuf.MustMarshalAny( + &envoy_config_filter_http_ext_authz_v3.ExtAuthzPerRoute{ + Override: &envoy_config_filter_http_ext_authz_v3.ExtAuthzPerRoute_CheckSettings{ + CheckSettings: &envoy_config_filter_http_ext_authz_v3.CheckSettings{ + ContextExtensions: map[string]string{ + "header_type": "proxy_config", + "header_1": "message_1", + "header_2": "message_2", + }, + }, + }, + }, + ), + }, + }, + ), + ), + ), + }) +} + +func globalExternalAuthorizationWithMergedAuthPolicyTLS(t *testing.T, rh cache.ResourceEventHandler, c *Contour) { + p := fixture.NewProxy("TLSProxy"). + WithFQDN("foo.com"). + WithSpec(contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{ + Fqdn: "foo.com", + TLS: &contour_api_v1.TLS{ + SecretName: "certificate", + }, + }, + Routes: []contour_api_v1.Route{ + { + Services: []contour_api_v1.Service{ + { + Name: "s1", + Port: 80, + }, + }, + AuthPolicy: &contour_api_v1.AuthorizationPolicy{ + Context: map[string]string{ + "header_type": "proxy_config", + "header_2": "message_2", + }, + }, + }, + }, + }) + + rh.OnAdd(p) + + var httpListener = defaultHTTPListener() + + // replace the default filter chains with an HCM that includes the global + // extAuthz filter. + var hcm = envoy_v3.HTTPConnectionManagerBuilder(). + RouteConfigName("ingress_http"). + MetricsPrefix("ingress_http"). + AccessLoggers(envoy_v3.FileAccessLogEnvoy("/dev/stdout", "", nil, contour_api_v1alpha1.LogLevelInfo)). + DefaultFilters(). + AddFilter(&http.HttpFilter{ + Name: wellknown.HTTPExternalAuthorization, + ConfigType: &http.HttpFilter_TypedConfig{ + TypedConfig: protobuf.MustMarshalAny(&envoy_config_filter_http_ext_authz_v3.ExtAuthz{ + Services: grpcCluster("extension/auth/extension"), + ClearRouteCache: true, + IncludePeerCertificate: true, + StatusOnError: &envoy_type.HttpStatus{ + Code: envoy_type.StatusCode_Forbidden, + }, + TransportApiVersion: envoy_core_v3.ApiVersion_V3, + }), + }, + }). + Get() + + httpListener.FilterChains = envoy_v3.FilterChains(hcm) + + httpsListener := &envoy_listener_v3.Listener{ + Name: "ingress_https", + Address: envoy_v3.SocketAddress("0.0.0.0", 8443), + ListenerFilters: envoy_v3.ListenerFilters( + envoy_v3.TLSInspector(), + ), + FilterChains: []*envoy_listener_v3.FilterChain{ + filterchaintls("foo.com", + &corev1.Secret{ + ObjectMeta: fixture.ObjectMeta("certificate"), + Type: "kubernetes.io/tls", + Data: featuretests.Secretdata(featuretests.CERTIFICATE, featuretests.RSA_PRIVATE_KEY), + }, + authzFilterFor( + "foo.com", + &envoy_config_filter_http_ext_authz_v3.ExtAuthz{ + Services: grpcCluster("extension/auth/extension"), + ClearRouteCache: true, + IncludePeerCertificate: true, + StatusOnError: &envoy_type.HttpStatus{ + Code: envoy_type.StatusCode_Forbidden, + }, + TransportApiVersion: envoy_core_v3.ApiVersion_V3, + }, + ), + nil, "h2", "http/1.1"), + }, + SocketOptions: envoy_v3.TCPKeepaliveSocketOptions(), + } + + c.Request(listenerType).Equals(&envoy_discovery_v3.DiscoveryResponse{ + TypeUrl: listenerType, + Resources: resources(t, + httpListener, + httpsListener, + statsListener()), + }).Status(p).IsValid() + + c.Request(routeType).Equals(&envoy_discovery_v3.DiscoveryResponse{ + TypeUrl: routeType, + Resources: resources(t, + envoy_v3.RouteConfiguration( + "https/foo.com", + envoy_v3.VirtualHost("foo.com", + &envoy_route_v3.Route{ + Match: routePrefix("/"), + Action: routeCluster("default/s1/80/da39a3ee5e"), + TypedPerFilterConfig: map[string]*anypb.Any{ + "envoy.filters.http.ext_authz": protobuf.MustMarshalAny( + &envoy_config_filter_http_ext_authz_v3.ExtAuthzPerRoute{ + Override: &envoy_config_filter_http_ext_authz_v3.ExtAuthzPerRoute_CheckSettings{ + CheckSettings: &envoy_config_filter_http_ext_authz_v3.CheckSettings{ + ContextExtensions: map[string]string{ + "header_type": "proxy_config", + "header_1": "message_1", + "header_2": "message_2", + }, + }, + }, + }, + ), + }, + }, + ), + ), + envoy_v3.RouteConfiguration( + "ingress_http", + envoy_v3.VirtualHost("foo.com", + &envoy_route_v3.Route{ + Match: routePrefix("/"), + Action: withRedirect(), + }, + ), + ), + ), + }) +} + +func globalExternalAuthorizationWithTLSAuthOverride(t *testing.T, rh cache.ResourceEventHandler, c *Contour) { + p := fixture.NewProxy("TLSProxy"). + WithFQDN("foo.com"). + WithSpec(contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{ + Fqdn: "foo.com", + TLS: &contour_api_v1.TLS{ + SecretName: "certificate", + }, + Authorization: &contour_api_v1.AuthorizationServer{ + ExtensionServiceRef: contour_api_v1.ExtensionServiceReference{ + Namespace: "auth", + Name: "extension", + }, + ResponseTimeout: defaultResponseTimeout.String(), + FailOpen: true, + WithRequestBody: &contour_api_v1.AuthorizationServerBufferSettings{ + MaxRequestBytes: 512, + PackAsBytes: true, + AllowPartialMessage: true, + }, + }, + }, + Routes: []contour_api_v1.Route{ + { + Services: []contour_api_v1.Service{ + { + Name: "s1", + Port: 80, + }, + }, + }, + }, + }) + + rh.OnAdd(p) + + var httpListener = defaultHTTPListener() + + // replace the default filter chains with an HCM that includes the global + // extAuthz filter. + var hcm = envoy_v3.HTTPConnectionManagerBuilder(). + RouteConfigName("ingress_http"). + MetricsPrefix("ingress_http"). + AccessLoggers(envoy_v3.FileAccessLogEnvoy("/dev/stdout", "", nil, contour_api_v1alpha1.LogLevelInfo)). + DefaultFilters(). + AddFilter(&http.HttpFilter{ + Name: wellknown.HTTPExternalAuthorization, + ConfigType: &http.HttpFilter_TypedConfig{ + TypedConfig: protobuf.MustMarshalAny(&envoy_config_filter_http_ext_authz_v3.ExtAuthz{ + Services: grpcCluster("extension/auth/extension"), + ClearRouteCache: true, + IncludePeerCertificate: true, + StatusOnError: &envoy_type.HttpStatus{ + Code: envoy_type.StatusCode_Forbidden, + }, + TransportApiVersion: envoy_core_v3.ApiVersion_V3, + }), + }, + }). + Get() + + httpListener.FilterChains = envoy_v3.FilterChains(hcm) + + httpsListener := &envoy_listener_v3.Listener{ + Name: "ingress_https", + Address: envoy_v3.SocketAddress("0.0.0.0", 8443), + ListenerFilters: envoy_v3.ListenerFilters( + envoy_v3.TLSInspector(), + ), + FilterChains: []*envoy_listener_v3.FilterChain{ + filterchaintls("foo.com", + &corev1.Secret{ + ObjectMeta: fixture.ObjectMeta("certificate"), + Type: "kubernetes.io/tls", + Data: featuretests.Secretdata(featuretests.CERTIFICATE, featuretests.RSA_PRIVATE_KEY), + }, + authzFilterFor( + "foo.com", + &envoy_config_filter_http_ext_authz_v3.ExtAuthz{ + Services: grpcCluster("extension/auth/extension"), + ClearRouteCache: true, + IncludePeerCertificate: true, + FailureModeAllow: true, + StatusOnError: &envoy_type.HttpStatus{ + Code: envoy_type.StatusCode_Forbidden, + }, + WithRequestBody: &envoy_config_filter_http_ext_authz_v3.BufferSettings{ + MaxRequestBytes: 512, + PackAsBytes: true, + AllowPartialMessage: true, + }, + TransportApiVersion: envoy_core_v3.ApiVersion_V3, + }, + ), + nil, "h2", "http/1.1"), + }, + SocketOptions: envoy_v3.TCPKeepaliveSocketOptions(), + } + + c.Request(listenerType).Equals(&envoy_discovery_v3.DiscoveryResponse{ + TypeUrl: listenerType, + Resources: resources(t, + httpListener, + httpsListener, + statsListener()), + }).Status(p).IsValid() +} + +func TestGlobalAuthorization(t *testing.T) { + subtests := map[string]func(*testing.T, cache.ResourceEventHandler, *Contour){ + // Default extAuthz on non TLS host. + "GlobalExternalAuthorizationFilterExists": globalExternalAuthorizationFilterExists, + // Default extAuthz on non TLS and TLS hosts. + "GlobalExternalAuthorizationFilterExistsTLS": globalExternalAuthorizationFilterExistsTLS, + // extAuthz disabled on TLS host. + "GlobalExternalAuthorizationWithTLSGlobalAuthDisabled": globalExternalAuthorizationWithTLSGlobalAuthDisabled, + // extAuthz override on TLS host. + "GlobalExternalAuthorizationWithTLSAuthOverride": globalExternalAuthorizationWithTLSAuthOverride, + // extAuthz authpolicy merge for non TLS hosts. + "GlobalExternalAuthorizationWithMergedAuthPolicy": globalExternalAuthorizationWithMergedAuthPolicy, + // extAuthz authpolicy merge for TLS hosts. + "GlobalExternalAuthorizationWithMergedAuthPolicyTLS": globalExternalAuthorizationWithMergedAuthPolicyTLS, + } + + for n, f := range subtests { + f := f + t.Run(n, func(t *testing.T) { + rh, c, done := setup(t, + func(cfg *xdscache_v3.ListenerConfig) { + cfg.GlobalExternalAuthConfig = &xdscache_v3.GlobalExternalAuthConfig{ + ExtensionService: k8s.NamespacedNameFrom("auth/extension"), + FailOpen: false, + Context: map[string]string{ + "header_type": "root_config", + "header_1": "message_1", + }, + Timeout: timeout.DurationSetting(defaultResponseTimeout), + } + }, + func(b *dag.Builder) { + b.Processors = []dag.Processor{ + &dag.ExtensionServiceProcessor{}, + &dag.HTTPProxyProcessor{ + GlobalExternalAuthorization: &contour_api_v1.AuthorizationServer{ + ExtensionServiceRef: contour_api_v1.ExtensionServiceReference{ + Name: "extension", + Namespace: "auth", + }, + FailOpen: false, + ResponseTimeout: defaultResponseTimeout.String(), + AuthPolicy: &contour_api_v1.AuthorizationPolicy{ + Context: map[string]string{ + "header_type": "root_config", + "header_1": "message_1", + }, + }, + }, + }, + &dag.ListenerProcessor{}, + } + }) + defer done() + + // Add common test fixtures. + rh.OnAdd(fixture.NewService("s1").WithPorts(corev1.ServicePort{Port: 80})) + rh.OnAdd(fixture.NewService("auth/oidc-server"). + WithPorts(corev1.ServicePort{Port: 8081})) + + rh.OnAdd(featuretests.Endpoints("auth", "oidc-server", corev1.EndpointSubset{ + Addresses: featuretests.Addresses("192.168.183.21"), + Ports: featuretests.Ports(featuretests.Port("", 8081)), + })) + + rh.OnAdd(&v1alpha1.ExtensionService{ + ObjectMeta: fixture.ObjectMeta("auth/extension"), + Spec: v1alpha1.ExtensionServiceSpec{ + Services: []v1alpha1.ExtensionServiceTarget{ + {Name: "oidc-server", Port: 8081}, + }, + TimeoutPolicy: &contour_api_v1.TimeoutPolicy{ + Response: defaultResponseTimeout.String(), + }, + }, + }) + + rh.OnAdd(fixture.NewService("app-server"). + WithPorts(corev1.ServicePort{Port: 80})) + + rh.OnAdd(featuretests.Endpoints("auth", "app-server", corev1.EndpointSubset{ + Addresses: featuretests.Addresses("192.168.183.21"), + Ports: featuretests.Ports(featuretests.Port("", 80)), + })) + + rh.OnAdd(&corev1.Secret{ + ObjectMeta: fixture.ObjectMeta("certificate"), + Type: "kubernetes.io/tls", + Data: featuretests.Secretdata(featuretests.CERTIFICATE, featuretests.RSA_PRIVATE_KEY), + }) + + f(t, rh, c) + }) + } +} diff --git a/internal/xdscache/v3/listener.go b/internal/xdscache/v3/listener.go index ddb096edb14..f1069bb452d 100644 --- a/internal/xdscache/v3/listener.go +++ b/internal/xdscache/v3/listener.go @@ -140,6 +140,10 @@ type ListenerConfig struct { // RateLimitConfig optionally configures the global Rate Limit Service to be // used. RateLimitConfig *RateLimitConfig + + // GlobalExternalAuthConfig optionally configures the global external authorization Service to be + // used. + GlobalExternalAuthConfig *GlobalExternalAuthConfig } type RateLimitConfig struct { @@ -152,6 +156,14 @@ type RateLimitConfig struct { EnableResourceExhaustedCode bool } +type GlobalExternalAuthConfig struct { + ExtensionService types.NamespacedName + FailOpen bool + SNI string + Timeout timeout.Setting + Context map[string]string +} + // DefaultListeners returns the configured Listeners or a single // Insecure (http) & single Secure (https) default listeners // if not provided. @@ -395,6 +407,7 @@ func (c *ListenerCache) OnChange(root *dag.DAG) { ServerHeaderTransformation(cfg.ServerHeaderTransformation). NumTrustedHops(cfg.XffNumTrustedHops). AddFilter(envoy_v3.GlobalRateLimitFilter(envoyGlobalRateLimitConfig(cfg.RateLimitConfig))). + AddFilter(httpGlobalExternalAuthConfig(cfg.GlobalExternalAuthConfig)). Get() listeners[httpListener.Name] = envoy_v3.Listener( @@ -419,13 +432,13 @@ func (c *ListenerCache) OnChange(root *dag.DAG) { if vh.TCPProxy == nil { var authFilter *http.HttpFilter - if vh.AuthorizationService != nil { + if vh.ExternalAuthorization != nil { authFilter = envoy_v3.FilterExternalAuthz( - vh.AuthorizationService.Name, - vh.AuthorizationService.SNI, - vh.AuthorizationFailOpen, - vh.AuthorizationResponseTimeout, - vh.AuthorizationServerWithRequestBody, + vh.ExternalAuthorization.AuthorizationService.Name, + vh.ExternalAuthorization.AuthorizationService.SNI, + vh.ExternalAuthorization.AuthorizationFailOpen, + vh.ExternalAuthorization.AuthorizationResponseTimeout, + vh.ExternalAuthorization.AuthorizationServerWithRequestBody, ) } @@ -559,6 +572,14 @@ func (c *ListenerCache) OnChange(root *dag.DAG) { c.Update(listeners) } +func httpGlobalExternalAuthConfig(config *GlobalExternalAuthConfig) *http.HttpFilter { + if config == nil { + return nil + } + + return envoy_v3.FilterExternalAuthz(dag.ExtensionClusterName(config.ExtensionService), config.SNI, config.FailOpen, config.Timeout, nil) +} + func envoyGlobalRateLimitConfig(config *RateLimitConfig) *envoy_v3.GlobalRateLimitConfig { if config == nil { return nil diff --git a/internal/xdscache/v3/route.go b/internal/xdscache/v3/route.go index b1eea18223f..e437056ee74 100644 --- a/internal/xdscache/v3/route.go +++ b/internal/xdscache/v3/route.go @@ -98,7 +98,7 @@ func (c *RouteCache) OnChange(root *dag.DAG) { for vhost, routes := range root.GetVirtualHostRoutes() { sortRoutes(routes) routeConfigs[ENVOY_HTTP_LISTENER].VirtualHosts = append(routeConfigs[ENVOY_HTTP_LISTENER].VirtualHosts, - envoy_v3.VirtualHostAndRoutes(vhost, routes, false, nil)) + envoy_v3.VirtualHostAndRoutes(vhost, routes, false)) } for vhost, routes := range root.GetSecureVirtualHostRoutes() { @@ -110,7 +110,7 @@ func (c *RouteCache) OnChange(root *dag.DAG) { sortRoutes(routes) routeConfigs[name].VirtualHosts = append(routeConfigs[name].VirtualHosts, - envoy_v3.VirtualHostAndRoutes(&vhost.VirtualHost, routes, true, vhost.AuthorizationService)) + envoy_v3.VirtualHostAndRoutes(&vhost.VirtualHost, routes, true)) // A fallback route configuration contains routes for all the vhosts that have the fallback certificate enabled. // When a request is received, the default TLS filterchain will accept the connection, @@ -122,7 +122,7 @@ func (c *RouteCache) OnChange(root *dag.DAG) { } routeConfigs[ENVOY_FALLBACK_ROUTECONFIG].VirtualHosts = append(routeConfigs[ENVOY_FALLBACK_ROUTECONFIG].VirtualHosts, - envoy_v3.VirtualHostAndRoutes(&vhost.VirtualHost, routes, true, vhost.AuthorizationService)) + envoy_v3.VirtualHostAndRoutes(&vhost.VirtualHost, routes, true)) } } diff --git a/internal/xdscache/v3/route_test.go b/internal/xdscache/v3/route_test.go index 23765ee6fcd..a668d034547 100644 --- a/internal/xdscache/v3/route_test.go +++ b/internal/xdscache/v3/route_test.go @@ -21,14 +21,17 @@ import ( envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" envoy_cors_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/cors/v3" + envoy_config_filter_http_ext_authz_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ext_authz/v3" matcher "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" contour_api_v1 "github.com/projectcontour/contour/apis/projectcontour/v1" + contour_api_v1alpha1 "github.com/projectcontour/contour/apis/projectcontour/v1alpha1" "github.com/projectcontour/contour/internal/dag" envoy_v3 "github.com/projectcontour/contour/internal/envoy/v3" "github.com/projectcontour/contour/internal/protobuf" "github.com/projectcontour/contour/internal/ref" "github.com/stretchr/testify/assert" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/wrapperspb" v1 "k8s.io/api/core/v1" @@ -3394,6 +3397,245 @@ func TestRouteVisit(t *testing.T) { } } +func TestRouteVisit_GlobalExternalAuthorization(t *testing.T) { + tests := map[string]struct { + objs []interface{} + fallbackCertificate *types.NamespacedName + want map[string]*envoy_route_v3.RouteConfiguration + }{ + "HTTP virtual host, authcontext override": { + objs: []interface{}{ + &contour_api_v1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "simple", + Namespace: "default", + }, + Spec: contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{ + Fqdn: "www.example.com", + }, + Routes: []contour_api_v1.Route{{ + Services: []contour_api_v1.Service{{ + Name: "backend", + Port: 80, + }}, + AuthPolicy: &contour_api_v1.AuthorizationPolicy{ + Context: map[string]string{ + "header_2": "new_message_2", + "header_3": "message_3", + }, + }, + }}, + }, + }, + &contour_api_v1alpha1.ExtensionService{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "ns", + }, + }, + &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "backend", + Namespace: "default", + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{{ + Protocol: "TCP", + Port: 80, + TargetPort: intstr.FromInt(8080), + }}, + }, + }, + }, + want: routeConfigurations( + envoy_v3.RouteConfiguration("ingress_http", + envoy_v3.VirtualHost("www.example.com", + &envoy_route_v3.Route{ + Match: routePrefix("/"), + Action: routecluster("default/backend/80/da39a3ee5e"), + TypedPerFilterConfig: map[string]*anypb.Any{ + "envoy.filters.http.ext_authz": protobuf.MustMarshalAny(&envoy_config_filter_http_ext_authz_v3.ExtAuthzPerRoute{ + Override: &envoy_config_filter_http_ext_authz_v3.ExtAuthzPerRoute_CheckSettings{ + CheckSettings: &envoy_config_filter_http_ext_authz_v3.CheckSettings{ + ContextExtensions: map[string]string{ + "header_1": "message_1", + "header_2": "new_message_2", + "header_3": "message_3", + }, + }, + }, + }), + }, + }, + ), + ), + ), + }, + "HTTP virtual host, auth disabled for a route": { + objs: []interface{}{ + &contour_api_v1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "simple", + Namespace: "default", + }, + Spec: contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{ + Fqdn: "www.example.com", + }, + Routes: []contour_api_v1.Route{{ + Services: []contour_api_v1.Service{{ + Name: "backend", + Port: 80, + }}, + AuthPolicy: &contour_api_v1.AuthorizationPolicy{ + Disabled: true, + }, + }}, + }, + }, + &contour_api_v1alpha1.ExtensionService{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "ns", + }, + }, + &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "backend", + Namespace: "default", + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{{ + Protocol: "TCP", + Port: 80, + TargetPort: intstr.FromInt(8080), + }}, + }, + }, + }, + want: routeConfigurations( + envoy_v3.RouteConfiguration("ingress_http", + envoy_v3.VirtualHost("www.example.com", + &envoy_route_v3.Route{ + Match: routePrefix("/"), + Action: routecluster("default/backend/80/da39a3ee5e"), + TypedPerFilterConfig: map[string]*anypb.Any{ + "envoy.filters.http.ext_authz": protobuf.MustMarshalAny(&envoy_config_filter_http_ext_authz_v3.ExtAuthzPerRoute{ + Override: &envoy_config_filter_http_ext_authz_v3.ExtAuthzPerRoute_Disabled{ + Disabled: true, + }, + }), + }, + }, + ), + ), + ), + }, + "HTTPs virtual host, authcontext override": { + objs: []interface{}{ + &contour_api_v1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "simple", + Namespace: "default", + }, + Spec: contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{ + Fqdn: "www.example.com", + TLS: &contour_api_v1.TLS{ + SecretName: "secret", + }, + }, + Routes: []contour_api_v1.Route{{ + Services: []contour_api_v1.Service{{ + Name: "backend", + Port: 80, + }}, + AuthPolicy: &contour_api_v1.AuthorizationPolicy{ + Context: map[string]string{ + "header_2": "new_message_2", + "header_3": "message_3", + }, + }, + }}, + }, + }, + &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "secret", + Namespace: "default", + }, + Type: "kubernetes.io/tls", + Data: secretdata(CERTIFICATE, RSA_PRIVATE_KEY), + }, + &contour_api_v1alpha1.ExtensionService{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "ns", + }, + }, + &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "backend", + Namespace: "default", + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{{ + Protocol: "TCP", + Port: 80, + TargetPort: intstr.FromInt(8080), + }}, + }, + }, + }, + want: routeConfigurations( + envoy_v3.RouteConfiguration("ingress_http", + envoy_v3.VirtualHost("www.example.com", + &envoy_route_v3.Route{ + Match: routePrefix("/"), + Action: &envoy_route_v3.Route_Redirect{ + Redirect: &envoy_route_v3.RedirectAction{ + SchemeRewriteSpecifier: &envoy_route_v3.RedirectAction_HttpsRedirect{ + HttpsRedirect: true, + }, + }, + }, + }, + ), + ), + envoy_v3.RouteConfiguration("https/www.example.com", + envoy_v3.VirtualHost("www.example.com", + &envoy_route_v3.Route{ + Match: routePrefix("/"), + Action: routecluster("default/backend/80/da39a3ee5e"), + TypedPerFilterConfig: map[string]*anypb.Any{ + "envoy.filters.http.ext_authz": protobuf.MustMarshalAny(&envoy_config_filter_http_ext_authz_v3.ExtAuthzPerRoute{ + Override: &envoy_config_filter_http_ext_authz_v3.ExtAuthzPerRoute_CheckSettings{ + CheckSettings: &envoy_config_filter_http_ext_authz_v3.CheckSettings{ + ContextExtensions: map[string]string{ + "header_1": "message_1", + "header_2": "new_message_2", + "header_3": "message_3", + }, + }, + }, + }), + }, + }, + ), + )), + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + var rc RouteCache + rc.OnChange(buildDAGGlobalExtAuth(t, tc.fallbackCertificate, tc.objs...)) + protobuf.ExpectEqual(t, tc.want, rc.values) + }) + } +} + func TestSortLongestRouteFirst(t *testing.T) { tests := map[string]struct { routes []*dag.Route diff --git a/internal/xdscache/v3/secret_test.go b/internal/xdscache/v3/secret_test.go index 26c6a063bd9..d13660d51e9 100644 --- a/internal/xdscache/v3/secret_test.go +++ b/internal/xdscache/v3/secret_test.go @@ -519,6 +519,45 @@ func buildDAGFallback(t *testing.T, fallbackCertificate *types.NamespacedName, o return builder.Build() } +// buildDAGGlobalExtAuth produces a dag.DAG from the supplied objects with global external authorization configured. +func buildDAGGlobalExtAuth(t *testing.T, fallbackCertificate *types.NamespacedName, objs ...interface{}) *dag.DAG { + builder := dag.Builder{ + Source: dag.KubernetesCache{ + FieldLogger: fixture.NewTestLogger(t), + }, + Processors: []dag.Processor{ + &dag.ExtensionServiceProcessor{}, + &dag.IngressProcessor{ + FieldLogger: fixture.NewTestLogger(t), + }, + &dag.HTTPProxyProcessor{ + FallbackCertificate: fallbackCertificate, + GlobalExternalAuthorization: &contour_api_v1.AuthorizationServer{ + ExtensionServiceRef: contour_api_v1.ExtensionServiceReference{ + Name: "test", + Namespace: "ns", + }, + FailOpen: false, + AuthPolicy: &contour_api_v1.AuthorizationPolicy{ + Context: map[string]string{ + "header_1": "message_1", + "header_2": "message_2", + }, + }, + ResponseTimeout: "10s", + }, + }, + &dag.ListenerProcessor{}, + }, + } + + for _, o := range objs { + builder.Source.Insert(o) + } + + return builder.Build() +} + func secretmap(secrets ...*envoy_tls_v3.Secret) map[string]*envoy_tls_v3.Secret { m := make(map[string]*envoy_tls_v3.Secret) for _, s := range secrets { diff --git a/pkg/config/parameters.go b/pkg/config/parameters.go index ed9026803dd..d3235b002e9 100644 --- a/pkg/config/parameters.go +++ b/pkg/config/parameters.go @@ -555,10 +555,74 @@ type Parameters struct { // to be used for global rate limiting. RateLimitService RateLimitService `yaml:"rateLimitService,omitempty"` + // GlobalExternalAuthorization optionally holds properties of the global external authorization configuration. + GlobalExternalAuthorization GlobalExternalAuthorization `yaml:"globalExtAuth,omitempty"` + // MetricsParameters holds configurable parameters for Contour and Envoy metrics. Metrics MetricsParameters `yaml:"metrics,omitempty"` } +// GlobalExternalAuthorizationConfig defines properties of global external authorization. +type GlobalExternalAuthorization struct { + // ExtensionService identifies the extension service defining the RLS, + // formatted as /. + ExtensionService string `yaml:"extensionService,omitempty"` + // AuthPolicy sets a default authorization policy for client requests. + // This policy will be used unless overridden by individual routes. + // + // +optional + AuthPolicy *GlobalAuthorizationPolicy `yaml:"authPolicy,omitempty"` + // ResponseTimeout configures maximum time to wait for a check response from the authorization server. + // Timeout durations are expressed in the Go [Duration format](https://godoc.org/time#ParseDuration). + // Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". + // The string "infinity" is also a valid input and specifies no timeout. + // + // +optional + ResponseTimeout string `yaml:"responseTimeout,omitempty"` + // If FailOpen is true, the client request is forwarded to the upstream service + // even if the authorization server fails to respond. This field should not be + // set in most cases. It is intended for use only while migrating applications + // from internal authorization to Contour external authorization. + // + // +optional + FailOpen bool `yaml:"failOpen,omitempty"` + // WithRequestBody specifies configuration for sending the client request's body to authorization server. + // +optional + WithRequestBody *GlobalAuthorizationServerBufferSettings `yaml:"withRequestBody,omitempty"` +} + +// GlobalAuthorizationServerBufferSettings enables ExtAuthz filter to buffer client request data and send it as part of authorization request +type GlobalAuthorizationServerBufferSettings struct { + // MaxRequestBytes sets the maximum size of message body ExtAuthz filter will hold in-memory. + // +optional + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:default=1024 + MaxRequestBytes uint32 `yaml:"maxRequestBytes,omitempty"` + // If AllowPartialMessage is true, then Envoy will buffer the body until MaxRequestBytes are reached. + // +optional + AllowPartialMessage bool `yaml:"allowPartialMessage,omitempty"` + // If PackAsBytes is true, the body sent to Authorization Server is in raw bytes. + // +optional + PackAsBytes bool `yaml:"packAsBytes,omitempty"` +} + +// GlobalAuthorizationPolicy modifies how client requests are authenticated. +type GlobalAuthorizationPolicy struct { + // When true, this field disables client request authentication + // for the scope of the policy. + // + // +optional + Disabled bool `yaml:"disabled,omitempty"` + // Context is a set of key/value pairs that are sent to the + // authentication server in the check request. If a context + // is provided at an enclosing scope, the entries are merged + // such that the inner scope overrides matching keys from the + // outer scope. + // + // +optional + Context map[string]string `yaml:"context,omitempty"` +} + // RateLimitService defines properties of a global Rate Limit Service. type RateLimitService struct { // ExtensionService identifies the extension service defining the RLS, diff --git a/site/content/docs/main/config/api-reference.html b/site/content/docs/main/config/api-reference.html index ef1e730cee8..a2143434f86 100644 --- a/site/content/docs/main/config/api-reference.html +++ b/site/content/docs/main/config/api-reference.html @@ -328,7 +328,8 @@

AuthorizationServer

(Appears on: -VirtualHost) +VirtualHost, +ContourConfigurationSpec)

AuthorizationServer configures an external server to authenticate @@ -354,6 +355,7 @@

AuthorizationServer +(Optional)

ExtensionServiceRef specifies the extension resource that will authorize client requests.

@@ -4625,6 +4627,22 @@

ContourConfiguration +globalExtAuth +
+ + +AuthorizationServer + + + + +(Optional) +

GlobalExternalAuthorization allows envoys external authorization filter +to be enabled for all virtual hosts.

+ + + + rateLimitService
@@ -5303,6 +5321,22 @@

ContourConfiguratio +globalExtAuth +
+ + +AuthorizationServer + + + + +(Optional) +

GlobalExternalAuthorization allows envoys external authorization filter +to be enabled for all virtual hosts.

+ + + + rateLimitService
diff --git a/site/content/docs/main/guides/external-authorization.md b/site/content/docs/main/guides/external-authorization.md index d513bf7a805..9524a28fb0d 100644 --- a/site/content/docs/main/guides/external-authorization.md +++ b/site/content/docs/main/guides/external-authorization.md @@ -360,13 +360,170 @@ $ curl -k --user user1:password1 https://local.projectcontour.io/test/$((RANDOM) {"TestId":"","Path":"/test/27132","Host":"local.projectcontour.io","Method":"GET","Proto":"HTTP/1.1","Headers":{"Accept":["*/*"],"Auth-Handler":["htpasswd"],"Auth-Realm":["default"],"Auth-Username":["user1"],"Authorization":["Basic dXNlcjE6cGFzc3dvcmQx"],"Content-Length":["0"],"User-Agent":["curl/7.64.1"],"X-Envoy-Expected-Rq-Timeout-Ms":["15000"],"X-Envoy-Internal":["true"],"X-Forwarded-For":["172.18.0.1"],"X-Forwarded-Proto":["https"],"X-Request-Id":["2c0ae102-4cf6-400e-a38f-5f0b844364cc"],"X-Request-Start":["t=1601601826.102"]}} ``` +## Global External Authorization + +Starting from version 1.25, Contour supports global external authorization. This allows you to setup a single external authorization configuration for all your virtual hosts (HTTP and HTTPS). + +To get started, ensure you have `contour-authserver` and the `ExtensionService` deployed as described above. + +### Global Configuration + +Define the global external authorization configuration in your contour config. + +```yaml +globalExtAuth: + extensionService: projectcontour-auth/htpasswd + failOpen: false + authPolicy: + context: + header1: value1 + header2: value2 + responseTimeout: 1s +``` + +Setup a HTTPProxy without TLS +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: echo +spec: + virtualhost: + fqdn: local.projectcontour.io + routes: + - services: + - name: ingress-conformance-echo + port: 80 +``` + +``` +$ kubectl apply -f echo-proxy.yaml +httpproxy.projectcontour.io/echo created +``` + +When we make a HTTP request without authentication details, we can see that the endpoint is secured and returns a 401. + +``` +$ curl -k -I http://local.projectcontour.io/test/$((RANDOM)) +HTTP/1.1 401 Unauthorized +www-authenticate: Basic realm="default", charset="UTF-8" +vary: Accept-Encoding +date: Mon, 20 Feb 2023 13:45:31 GMT +``` + +If you add the username and password to the same request you can verify that the request succeeds. +``` +$ curl -k --user user1:password1 http://local.projectcontour.io/test/$((RANDOM)) +{"TestId":"","Path":"/test/27748","Host":"local.projectcontour.io","Method":"GET","Proto":"HTTP/1.1","Headers":{"Accept":["*/*"],"Auth-Context-Header1":["value1"],"Auth-Context-Header2":["value2"],"Auth-Context-Routq":["global"],"Auth-Handler":["htpasswd"],"Auth-Realm":["default"],"Auth-Username":["user1"],"Authorization":["Basic dXNlcjE6cGFzc3dvcmQx"],"User-Agent":["curl/7.86.0"],"X-Envoy-Expected-Rq-Timeout-Ms":["15000"],"X-Envoy-Internal":["true"],"X-Forwarded-For":["172.18.0.1"],"X-Forwarded-Proto":["http"],"X-Request-Id":["b6bb7036-8408-4b03-9ce5-7011d89799b4"],"X-Request-Start":["t=1676900780.118"]}} +``` + +Global external authorization can also be configured with TLS virtual hosts. Update your HTTPProxy by adding `tls` and `secretName` to it. + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: echo +spec: + virtualhost: + fqdn: local.projectcontour.io + tls: + secretName: ingress-conformance-echo + routes: + - services: + - name: ingress-conformance-echo + port: 80 +``` + +``` +$ kubectl apply -f echo-proxy.yaml +httpproxy.projectcontour.io/echo configured +``` + +you can verify the HTTPS requests succeeds +``` +$ curl -k --user user1:password1 https://local.projectcontour.io/test/$((RANDOM)) +{"TestId":"","Path":"/test/13499","Host":"local.projectcontour.io","Method":"GET","Proto":"HTTP/1.1","Headers":{"Accept":["*/*"],"Auth-Context-Header1":["value1"],"Auth-Context-Header2":["value2"],"Auth-Context-Routq":["global"],"Auth-Handler":["htpasswd"],"Auth-Realm":["default"],"Auth-Username":["user1"],"Authorization":["Basic dXNlcjE6cGFzc3dvcmQx"],"User-Agent":["curl/7.86.0"],"X-Envoy-Expected-Rq-Timeout-Ms":["15000"],"X-Envoy-Internal":["true"],"X-Forwarded-For":["172.18.0.1"],"X-Forwarded-Proto":["https"],"X-Request-Id":["2b3edbed-3c68-44ef-a659-2e1245d7fe13"],"X-Request-Start":["t=1676901557.918"]}} +``` + +### Excluding a virtual host from global external authorization + +You can exclude a virtual host from the global external authorization policy by setting the `disabled` flag to true under `authPolicy`. + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: echo +spec: + virtualhost: + fqdn: local.projectcontour.io + tls: + secretName: ingress-conformance-echo + authorization: + authPolicy: + disabled: true + routes: + - services: + - name: ingress-conformance-echo + port: 80 +``` + +``` +$ kubectl apply -f echo-proxy.yaml +httpproxy.projectcontour.io/echo configured +``` + +You can verify that an insecure request succeeds without being authorized. + +``` +$ curl -k https://local.projectcontour.io/test/$((RANDOM)) +{"TestId":"","Path":"/test/51","Host":"local.projectcontour.io","Method":"GET","Proto":"HTTP/1.1","Headers":{"Accept":["*/*"],"User-Agent":["curl/7.86.0"],"X-Envoy-Expected-Rq-Timeout-Ms":["15000"],"X-Envoy-Internal":["true"],"X-Forwarded-For":["172.18.0.1"],"X-Forwarded-Proto":["https"],"X-Request-Id":["18716e12-dcce-45ba-a3bb-bc26af3775d2"],"X-Request-Start":["t=1676901847.802"]}} +``` + +### Overriding global external authorization for a virtual host + +Sometimes you may want a different configuration than what is defined globally. To override the global external authorization, add the `authorization` block to your HTTPProxy as shown below + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: echo +spec: + virtualhost: + fqdn: local.projectcontour.io + tls: + secretName: ingress-conformance-echo + authorization: + extensionRef: + name: htpasswd + namespace: projectcontour-auth + routes: + - services: + - name: ingress-conformance-echo + port: 80 +``` + +``` +$ kubectl apply -f echo-proxy.yaml +httpproxy.projectcontour.io/echo configured +``` + +You can verify that the endpoint has applied the overridden external authorization configuration. + +``` +$ curl -k --user user1:password1 https://local.projectcontour.io/test/$((RANDOM)) +{"TestId":"","Path":"/test/4514","Host":"local.projectcontour.io","Method":"GET","Proto":"HTTP/1.1","Headers":{"Accept":["*/*"],"Auth-Context-Overriden_message":["overriden_value"],"Auth-Handler":["htpasswd"],"Auth-Realm":["default"],"Auth-Username":["user1"],"Authorization":["Basic dXNlcjE6cGFzc3dvcmQx"],"User-Agent":["curl/7.86.0"],"X-Envoy-Expected-Rq-Timeout-Ms":["15000"],"X-Envoy-Internal":["true"],"X-Forwarded-For":["172.18.0.1"],"X-Forwarded-Proto":["https"],"X-Request-Id":["8a02d6ce-8be0-4e87-8ed8-cca7e239e986"],"X-Request-Start":["t=1676902237.111"]}} +``` + ## Caveats There are a few caveats to consider when deploying external authorization: 1. Only one external authorization server can be configured on a virtual host -1. Only HTTPS virtual hosts are supported +1. HTTP hosts are only supported with global external authorization. 1. External authorization cannot be used with the TLS fallback certificate (i.e. client SNI support is required) [1]: https://github.com/projectcontour/contour-authserver diff --git a/test/e2e/deployment.go b/test/e2e/deployment.go index 5a1a1ece296..8e4a1f189c4 100644 --- a/test/e2e/deployment.go +++ b/test/e2e/deployment.go @@ -107,6 +107,11 @@ type Deployment struct { RateLimitDeployment *apps_v1.Deployment RateLimitService *v1.Service RateLimitExtensionService *contour_api_v1alpha1.ExtensionService + + // // Global External Authorization deployment. + GlobalExtAuthDeployment *apps_v1.Deployment + GlobalExtAuthService *v1.Service + GlobalExtAuthExtensionService *contour_api_v1alpha1.ExtensionService } // UnmarshalResources unmarshals resources from rendered Contour manifest in @@ -196,6 +201,7 @@ func (d *Deployment) UnmarshalResources() error { } } + // ratelimit rateLimitExamplePath := filepath.Join(filepath.Dir(thisFile), "..", "..", "examples", "ratelimit") rateLimitDeploymentFile := filepath.Join(rateLimitExamplePath, "02-ratelimit.yaml") rateLimitExtSvcFile := filepath.Join(rateLimitExamplePath, "03-ratelimit-extsvc.yaml") @@ -223,7 +229,39 @@ func (d *Deployment) UnmarshalResources() error { decoder = apimachinery_util_yaml.NewYAMLToJSONDecoder(rLESFile) d.RateLimitExtensionService = new(contour_api_v1alpha1.ExtensionService) - return decoder.Decode(d.RateLimitExtensionService) + if err := decoder.Decode(d.RateLimitExtensionService); err != nil { + return err + } + + // // Global external auth + globalExtAuthExamplePath := filepath.Join(filepath.Dir(thisFile), "..", "..", "examples", "global-external-auth") + globalExtAuthServerDeploymentFile := filepath.Join(globalExtAuthExamplePath, "01-authserver.yaml") + globalExtAuthExtSvcFile := filepath.Join(globalExtAuthExamplePath, "02-globalextauth-extsvc.yaml") + + rGlobalExtAuthDeploymentFile, err := os.Open(globalExtAuthServerDeploymentFile) + if err != nil { + return err + } + defer rGlobalExtAuthDeploymentFile.Close() + decoder = apimachinery_util_yaml.NewYAMLToJSONDecoder(rGlobalExtAuthDeploymentFile) + d.GlobalExtAuthDeployment = new(apps_v1.Deployment) + if err := decoder.Decode(d.GlobalExtAuthDeployment); err != nil { + return err + } + d.GlobalExtAuthService = new(v1.Service) + if err := decoder.Decode(d.GlobalExtAuthService); err != nil { + return err + } + + rGlobalExtAuthExtSvcFile, err := os.Open(globalExtAuthExtSvcFile) + if err != nil { + return err + } + defer rGlobalExtAuthExtSvcFile.Close() + decoder = apimachinery_util_yaml.NewYAMLToJSONDecoder(rGlobalExtAuthExtSvcFile) + d.GlobalExtAuthExtensionService = new(contour_api_v1alpha1.ExtensionService) + + return decoder.Decode(d.GlobalExtAuthExtensionService) } // Common case of updating object if exists, create otherwise. @@ -466,6 +504,30 @@ func (d *Deployment) EnsureRateLimitResources(namespace string, configContents s return d.ensureResource(extSvc, new(contour_api_v1alpha1.ExtensionService)) } +func (d *Deployment) EnsureGlobalExternalAuthResources(namespace string) error { + setNamespace := d.Namespace.Name + if len(namespace) > 0 { + setNamespace = namespace + } + + deployment := d.GlobalExtAuthDeployment.DeepCopy() + deployment.Namespace = setNamespace + if err := d.ensureResource(deployment, new(apps_v1.Deployment)); err != nil { + return err + } + + service := d.GlobalExtAuthService.DeepCopy() + service.Namespace = setNamespace + if err := d.ensureResource(service, new(v1.Service)); err != nil { + return err + } + + extSvc := d.GlobalExtAuthExtensionService.DeepCopy() + extSvc.Namespace = setNamespace + + return d.ensureResource(extSvc, new(contour_api_v1alpha1.ExtensionService)) +} + // Convenience method for deploying the pieces of the deployment needed for // testing Contour running locally, out of cluster. // Includes: diff --git a/test/e2e/httpproxy/global_external_auth_test.go b/test/e2e/httpproxy/global_external_auth_test.go new file mode 100644 index 00000000000..666e820e479 --- /dev/null +++ b/test/e2e/httpproxy/global_external_auth_test.go @@ -0,0 +1,397 @@ +// Copyright Project Contour Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build e2e +// +build e2e + +package httpproxy + +import ( + . "github.com/onsi/ginkgo/v2" + contourv1 "github.com/projectcontour/contour/apis/projectcontour/v1" + "github.com/projectcontour/contour/test/e2e" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func testGlobalExternalAuthVirtualHostNonTLS(namespace string) { + Specify("global external auth can be configured on a non TLS HTTPProxy", func() { + t := f.T() + + f.Fixtures.Echo.Deploy(namespace, "echo") + + p := &contourv1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "external-auth", + }, + Spec: contourv1.HTTPProxySpec{ + VirtualHost: &contourv1.VirtualHost{ + Fqdn: "http.globalexternalauth.projectcontour.io", + }, + Routes: []contourv1.Route{ + { + Conditions: []contourv1.MatchCondition{ + { + Prefix: "/first", + }, + }, + Services: []contourv1.Service{ + { + Name: "echo", + Port: 80, + }, + }, + }, + { + Conditions: []contourv1.MatchCondition{ + { + Prefix: "/second", + }, + }, + AuthPolicy: &contourv1.AuthorizationPolicy{ + Disabled: true, + }, + Services: []contourv1.Service{ + { + Name: "echo", + Port: 80, + }, + }, + }, + { + AuthPolicy: &contourv1.AuthorizationPolicy{ + Context: map[string]string{ + "target": "default", + }, + }, + Services: []contourv1.Service{ + { + Name: "echo", + Port: 80, + }, + }, + }, + }, + }, + } + f.CreateHTTPProxyAndWaitFor(p, e2e.HTTPProxyValid) + + // By default requests to /first should not be authorized. + res, ok := f.HTTP.RequestUntil(&e2e.HTTPRequestOpts{ + Host: p.Spec.VirtualHost.Fqdn, + Path: "/first", + Condition: e2e.HasStatusCode(401), + }) + + require.NotNil(t, res, "request never succeeded") + require.Truef(t, ok, "expected 401 response code, got %d", res.StatusCode) + + // THe /second route disables authorization so this request should succeed. + res, ok = f.HTTP.RequestUntil(&e2e.HTTPRequestOpts{ + Host: p.Spec.VirtualHost.Fqdn, + Path: "/second", + Condition: e2e.HasStatusCode(200), + }) + + require.NotNil(t, res, "request never succeeded") + require.Truef(t, ok, "expected 200 response code, got %d", res.StatusCode) + + // The default route should not authorize by default. + res, ok = f.HTTP.RequestUntil(&e2e.HTTPRequestOpts{ + Host: p.Spec.VirtualHost.Fqdn, + Path: "/matches-default-route", + Condition: e2e.HasStatusCode(401), + }) + require.NotNil(t, res, "request never succeeded") + require.Truef(t, ok, "expected 401 response code, got %d", res.StatusCode) + }) +} + +func testGlobalExternalAuthTLS(namespace string) { + Specify("global external auth can be configured on a TLS HTTPProxy", func() { + t := f.T() + + f.Fixtures.Echo.Deploy(namespace, "echo") + f.Certs.CreateSelfSignedCert(namespace, "echo", "echo", "https.globalexternalauth.projectcontour.io") + + p := &contourv1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "external-auth", + }, + Spec: contourv1.HTTPProxySpec{ + VirtualHost: &contourv1.VirtualHost{ + Fqdn: "https.globalexternalauth.projectcontour.io", + TLS: &contourv1.TLS{ + SecretName: "echo", + }, + }, + Routes: []contourv1.Route{ + { + Conditions: []contourv1.MatchCondition{ + { + Prefix: "/first", + }, + }, + Services: []contourv1.Service{ + { + Name: "echo", + Port: 80, + }, + }, + }, + { + Conditions: []contourv1.MatchCondition{ + { + Prefix: "/second", + }, + }, + AuthPolicy: &contourv1.AuthorizationPolicy{ + Disabled: true, + }, + Services: []contourv1.Service{ + { + Name: "echo", + Port: 80, + }, + }, + }, + { + AuthPolicy: &contourv1.AuthorizationPolicy{ + Context: map[string]string{ + "target": "default", + }, + }, + Services: []contourv1.Service{ + { + Name: "echo", + Port: 80, + }, + }, + }, + }, + }, + } + f.CreateHTTPProxyAndWaitFor(p, e2e.HTTPProxyValid) + + // By default requests to /first should not be authorized. + res, ok := f.HTTP.SecureRequestUntil(&e2e.HTTPSRequestOpts{ + Host: p.Spec.VirtualHost.Fqdn, + Path: "/first", + Condition: e2e.HasStatusCode(401), + }) + + require.NotNil(t, res, "request never succeeded") + require.Truef(t, ok, "expected 401 response code, got %d", res.StatusCode) + + // THe /second route disables authorization so this request should succeed. + res, ok = f.HTTP.SecureRequestUntil(&e2e.HTTPSRequestOpts{ + Host: p.Spec.VirtualHost.Fqdn, + Path: "/second", + Condition: e2e.HasStatusCode(200), + }) + + require.NotNil(t, res, "request never succeeded") + require.Truef(t, ok, "expected 200 response code, got %d", res.StatusCode) + + // The default route should not authorize by default. + res, ok = f.HTTP.SecureRequestUntil(&e2e.HTTPSRequestOpts{ + Host: p.Spec.VirtualHost.Fqdn, + Path: "/matches-default-route", + Condition: e2e.HasStatusCode(401), + }) + require.NotNil(t, res, "request never succeeded") + require.Truef(t, ok, "expected 401 response code, got %d", res.StatusCode) + }) +} + +func testGlobalExternalAuthNonTLSAuthDisabled(namespace string) { + Specify("global external auth can be disabled on a non TLS HTTPProxy", func() { + t := f.T() + + f.Fixtures.Echo.Deploy(namespace, "echo") + + p := &contourv1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "external-auth", + }, + Spec: contourv1.HTTPProxySpec{ + VirtualHost: &contourv1.VirtualHost{ + Fqdn: "disabled.http.globalexternalauth.projectcontour.io", + Authorization: &contourv1.AuthorizationServer{ + AuthPolicy: &contourv1.AuthorizationPolicy{ + Disabled: true, + }, + }, + }, + Routes: []contourv1.Route{ + { + Conditions: []contourv1.MatchCondition{ + { + Prefix: "/first", + }, + }, + Services: []contourv1.Service{ + { + Name: "echo", + Port: 80, + }, + }, + }, + { + Conditions: []contourv1.MatchCondition{ + { + Prefix: "/second", + }, + }, + Services: []contourv1.Service{ + { + Name: "echo", + Port: 80, + }, + }, + }, + { + Services: []contourv1.Service{ + { + Name: "echo", + Port: 80, + }, + }, + }, + }, + }, + } + f.CreateHTTPProxyAndWaitFor(p, e2e.HTTPProxyValid) + + res, ok := f.HTTP.RequestUntil(&e2e.HTTPRequestOpts{ + Host: p.Spec.VirtualHost.Fqdn, + Path: "/first", + Condition: e2e.HasStatusCode(200), + }) + + require.NotNil(t, res, "request never succeeded") + require.Truef(t, ok, "expected 200 response code, got %d", res.StatusCode) + + res, ok = f.HTTP.RequestUntil(&e2e.HTTPRequestOpts{ + Host: p.Spec.VirtualHost.Fqdn, + Path: "/second", + Condition: e2e.HasStatusCode(200), + }) + + require.NotNil(t, res, "request never succeeded") + require.Truef(t, ok, "expected 200 response code, got %d", res.StatusCode) + + res, ok = f.HTTP.RequestUntil(&e2e.HTTPRequestOpts{ + Host: p.Spec.VirtualHost.Fqdn, + Path: "/matches-default-route", + Condition: e2e.HasStatusCode(200), + }) + require.NotNil(t, res, "request never succeeded") + require.Truef(t, ok, "expected 200 response code, got %d", res.StatusCode) + }) +} + +func testGlobalExternalAuthTLSAuthDisabled(namespace string) { + Specify("global external auth can be disabled on a TLS HTTPProxy", func() { + t := f.T() + + f.Fixtures.Echo.Deploy(namespace, "echo") + f.Certs.CreateSelfSignedCert(namespace, "echo", "echo", "disabled.https.globalexternalauth.projectcontour.io") + + p := &contourv1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "external-auth", + }, + Spec: contourv1.HTTPProxySpec{ + VirtualHost: &contourv1.VirtualHost{ + Fqdn: "disabled.https.globalexternalauth.projectcontour.io", + TLS: &contourv1.TLS{ + SecretName: "echo", + }, + Authorization: &contourv1.AuthorizationServer{ + AuthPolicy: &contourv1.AuthorizationPolicy{ + Disabled: true, + }, + }, + }, + Routes: []contourv1.Route{ + { + Conditions: []contourv1.MatchCondition{ + { + Prefix: "/first", + }, + }, + Services: []contourv1.Service{ + { + Name: "echo", + Port: 80, + }, + }, + }, + { + Conditions: []contourv1.MatchCondition{ + { + Prefix: "/second", + }, + }, + Services: []contourv1.Service{ + { + Name: "echo", + Port: 80, + }, + }, + }, + { + Services: []contourv1.Service{ + { + Name: "echo", + Port: 80, + }, + }, + }, + }, + }, + } + f.CreateHTTPProxyAndWaitFor(p, e2e.HTTPProxyValid) + + res, ok := f.HTTP.SecureRequestUntil(&e2e.HTTPSRequestOpts{ + Host: p.Spec.VirtualHost.Fqdn, + Path: "/first", + Condition: e2e.HasStatusCode(200), + }) + + require.NotNil(t, res, "request never succeeded") + require.Truef(t, ok, "expected 200 response code, got %d", res.StatusCode) + + res, ok = f.HTTP.SecureRequestUntil(&e2e.HTTPSRequestOpts{ + Host: p.Spec.VirtualHost.Fqdn, + Path: "/second", + Condition: e2e.HasStatusCode(200), + }) + + require.NotNil(t, res, "request never succeeded") + require.Truef(t, ok, "expected 200 response code, got %d", res.StatusCode) + + res, ok = f.HTTP.SecureRequestUntil(&e2e.HTTPSRequestOpts{ + Host: p.Spec.VirtualHost.Fqdn, + Path: "/matches-default-route", + Condition: e2e.HasStatusCode(200), + }) + require.NotNil(t, res, "request never succeeded") + require.Truef(t, ok, "expected 200 response code, got %d", res.StatusCode) + }) +} diff --git a/test/e2e/httpproxy/httpproxy_test.go b/test/e2e/httpproxy/httpproxy_test.go index 6734656caca..37a22a98c6a 100644 --- a/test/e2e/httpproxy/httpproxy_test.go +++ b/test/e2e/httpproxy/httpproxy_test.go @@ -26,6 +26,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/onsi/gomega/gexec" + contour_api_v1 "github.com/projectcontour/contour/apis/projectcontour/v1" contour_api_v1alpha1 "github.com/projectcontour/contour/apis/projectcontour/v1alpha1" "github.com/projectcontour/contour/internal/ref" "github.com/projectcontour/contour/pkg/config" @@ -483,4 +484,53 @@ descriptors: f.NamespacedTest("grpc-web", testGRPCWeb) }) + + Context("global external auth", func() { + withGlobalExtAuth := func(body e2e.NamespacedTestBody) e2e.NamespacedTestBody { + return func(namespace string) { + Context("with global external auth service", func() { + BeforeEach(func() { + contourConfig.GlobalExternalAuthorization = config.GlobalExternalAuthorization{ + ExtensionService: fmt.Sprintf("%s/%s", namespace, "testserver"), + FailOpen: false, + AuthPolicy: &config.GlobalAuthorizationPolicy{ + Context: map[string]string{ + "location": "global_config", + "header_2": "message_2", + }, + }, + ResponseTimeout: "10s", + } + contourConfiguration.Spec.GlobalExternalAuthorization = &contour_api_v1.AuthorizationServer{ + ExtensionServiceRef: contour_api_v1.ExtensionServiceReference{ + Namespace: namespace, + Name: "testserver", + }, + FailOpen: false, + AuthPolicy: &contour_api_v1.AuthorizationPolicy{ + Disabled: false, + Context: map[string]string{ + "location": "global_config", + "header_2": "message_2", + }, + }, + ResponseTimeout: "10s", + } + require.NoError(f.T(), + f.Deployment.EnsureGlobalExternalAuthResources(namespace)) + }) + body(namespace) + }) + } + } + + f.NamespacedTest("httpproxy-global-ext-auth-non-tls", withGlobalExtAuth(testGlobalExternalAuthVirtualHostNonTLS)) + + f.NamespacedTest("httpproxy-global-ext-auth-tls", withGlobalExtAuth(testGlobalExternalAuthTLS)) + + f.NamespacedTest("httpproxy-global-ext-auth-non-tls-disabled", withGlobalExtAuth(testGlobalExternalAuthNonTLSAuthDisabled)) + + f.NamespacedTest("httpproxy-global-ext-auth-tls-disabled", withGlobalExtAuth(testGlobalExternalAuthTLSAuthDisabled)) + }) + }) From 672dd937c55ae009d5938ec990255d0b58c3ac0c Mon Sep 17 00:00:00 2001 From: claytonig Date: Fri, 10 Mar 2023 15:05:17 +0100 Subject: [PATCH 2/3] address review comments Signed-off-by: claytonig --- cmd/contour/serve.go | 14 +- internal/envoy/v3/listener.go | 14 +- internal/envoy/v3/listener_test.go | 33 +++- .../v3/global_authorization_test.go | 162 ++++-------------- internal/xdscache/v3/listener.go | 20 ++- internal/xdscache/v3/route_test.go | 40 +++++ internal/xdscache/v3/secret_test.go | 39 ----- .../main/guides/external-authorization.md | 6 +- test/e2e/deployment.go | 4 +- 9 files changed, 135 insertions(+), 197 deletions(-) diff --git a/cmd/contour/serve.go b/cmd/contour/serve.go index cd63acbf31d..02a2a8b3574 100644 --- a/cmd/contour/serve.go +++ b/cmd/contour/serve.go @@ -689,13 +689,23 @@ func (s *Server) setupGlobalExternalAuthentication(contourConfiguration contour_ context = contourConfiguration.GlobalExternalAuthorization.AuthPolicy.Context } - return &xdscache_v3.GlobalExternalAuthConfig{ + globalExternalAuthConfig := &xdscache_v3.GlobalExternalAuthConfig{ ExtensionService: key, SNI: sni, Timeout: responseTimeout, FailOpen: contourConfiguration.GlobalExternalAuthorization.FailOpen, Context: context, - }, nil + WithRequestBody: &dag.AuthorizationServerBufferSettings{}, + } + + if contourConfiguration.GlobalExternalAuthorization.WithRequestBody != nil { + globalExternalAuthConfig.WithRequestBody = &dag.AuthorizationServerBufferSettings{ + PackAsBytes: contourConfiguration.GlobalExternalAuthorization.WithRequestBody.PackAsBytes, + AllowPartialMessage: contourConfiguration.GlobalExternalAuthorization.WithRequestBody.AllowPartialMessage, + MaxRequestBytes: contourConfiguration.GlobalExternalAuthorization.WithRequestBody.MaxRequestBytes, + } + } + return globalExternalAuthConfig, nil } func (s *Server) setupDebugService(debugConfig contour_api_v1alpha1.DebugConfig, builder *dag.Builder) error { diff --git a/internal/envoy/v3/listener.go b/internal/envoy/v3/listener.go index 0088140e2cd..f4a2caef4ae 100644 --- a/internal/envoy/v3/listener.go +++ b/internal/envoy/v3/listener.go @@ -720,16 +720,16 @@ end // FilterExternalAuthz returns an `ext_authz` filter configured with the // requested parameters. -func FilterExternalAuthz(authzClusterName, sni string, failOpen bool, timeout timeout.Setting, bufferSettings *dag.AuthorizationServerBufferSettings) *http.HttpFilter { +func FilterExternalAuthz(externalAuthorization *dag.ExternalAuthorization) *http.HttpFilter { authConfig := envoy_config_filter_http_ext_authz_v3.ExtAuthz{ Services: &envoy_config_filter_http_ext_authz_v3.ExtAuthz_GrpcService{ - GrpcService: GrpcService(authzClusterName, sni, timeout), + GrpcService: GrpcService(externalAuthorization.AuthorizationService.Name, externalAuthorization.AuthorizationService.SNI, externalAuthorization.AuthorizationResponseTimeout), }, // Pretty sure we always want this. Why have an // external auth service if it is not going to affect // routing decisions? ClearRouteCache: true, - FailureModeAllow: failOpen, + FailureModeAllow: externalAuthorization.AuthorizationFailOpen, StatusOnError: &envoy_type.HttpStatus{ Code: envoy_type.StatusCode_Forbidden, }, @@ -740,11 +740,11 @@ func FilterExternalAuthz(authzClusterName, sni string, failOpen bool, timeout ti TransportApiVersion: envoy_core_v3.ApiVersion_V3, } - if bufferSettings != nil { + if externalAuthorization.AuthorizationServerWithRequestBody != nil { authConfig.WithRequestBody = &envoy_config_filter_http_ext_authz_v3.BufferSettings{ - MaxRequestBytes: bufferSettings.MaxRequestBytes, - AllowPartialMessage: bufferSettings.AllowPartialMessage, - PackAsBytes: bufferSettings.PackAsBytes, + MaxRequestBytes: externalAuthorization.AuthorizationServerWithRequestBody.MaxRequestBytes, + AllowPartialMessage: externalAuthorization.AuthorizationServerWithRequestBody.AllowPartialMessage, + PackAsBytes: externalAuthorization.AuthorizationServerWithRequestBody.PackAsBytes, } } diff --git a/internal/envoy/v3/listener_test.go b/internal/envoy/v3/listener_test.go index 441e3427d20..e6e4ac25c08 100644 --- a/internal/envoy/v3/listener_test.go +++ b/internal/envoy/v3/listener_test.go @@ -1710,7 +1710,15 @@ func TestAddFilter(t *testing.T) { }, "Add to the default filters": { builder: HTTPConnectionManagerBuilder().DefaultFilters(), - add: FilterExternalAuthz("test", "", false, timeout.Setting{}, nil), + add: FilterExternalAuthz(&dag.ExternalAuthorization{ + AuthorizationService: &dag.ExtensionCluster{ + Name: "test", + SNI: "", + }, + AuthorizationFailOpen: false, + AuthorizationResponseTimeout: timeout.Setting{}, + AuthorizationServerWithRequestBody: nil, + }), want: []*http.HttpFilter{ { Name: "compressor", @@ -1775,7 +1783,15 @@ func TestAddFilter(t *testing.T) { }), }, }, - FilterExternalAuthz("test", "", false, timeout.Setting{}, nil), + FilterExternalAuthz(&dag.ExternalAuthorization{ + AuthorizationService: &dag.ExtensionCluster{ + Name: "test", + SNI: "", + }, + AuthorizationFailOpen: false, + AuthorizationResponseTimeout: timeout.Setting{}, + AuthorizationServerWithRequestBody: nil, + }), { Name: "router", ConfigType: &http.HttpFilter_TypedConfig{ @@ -1786,12 +1802,19 @@ func TestAddFilter(t *testing.T) { }, "Add to the default filters with AuthorizationServerBufferSettings": { builder: HTTPConnectionManagerBuilder().DefaultFilters(), - add: FilterExternalAuthz( - "test", "ext-auth-server.com", false, timeout.Setting{}, &dag.AuthorizationServerBufferSettings{ + add: FilterExternalAuthz(&dag.ExternalAuthorization{ + AuthorizationService: &dag.ExtensionCluster{ + Name: "test", + SNI: "ext-auth-server.com", + }, + AuthorizationFailOpen: false, + AuthorizationResponseTimeout: timeout.Setting{}, + AuthorizationServerWithRequestBody: &dag.AuthorizationServerBufferSettings{ MaxRequestBytes: 10, AllowPartialMessage: true, PackAsBytes: true, - }), + }, + }), want: []*http.HttpFilter{ { Name: "compressor", diff --git a/internal/featuretests/v3/global_authorization_test.go b/internal/featuretests/v3/global_authorization_test.go index 23d2df13dc3..938dfbb9cdd 100644 --- a/internal/featuretests/v3/global_authorization_test.go +++ b/internal/featuretests/v3/global_authorization_test.go @@ -69,28 +69,7 @@ func globalExternalAuthorizationFilterExists(t *testing.T, rh cache.ResourceEven // replace the default filter chains with an HCM that includes the global // extAuthz filter. - var hcm = envoy_v3.HTTPConnectionManagerBuilder(). - RouteConfigName("ingress_http"). - MetricsPrefix("ingress_http"). - AccessLoggers(envoy_v3.FileAccessLogEnvoy("/dev/stdout", "", nil, contour_api_v1alpha1.LogLevelInfo)). - DefaultFilters(). - AddFilter(&http.HttpFilter{ - Name: wellknown.HTTPExternalAuthorization, - ConfigType: &http.HttpFilter_TypedConfig{ - TypedConfig: protobuf.MustMarshalAny(&envoy_config_filter_http_ext_authz_v3.ExtAuthz{ - Services: grpcCluster("extension/auth/extension"), - ClearRouteCache: true, - IncludePeerCertificate: true, - StatusOnError: &envoy_type.HttpStatus{ - Code: envoy_type.StatusCode_Forbidden, - }, - TransportApiVersion: envoy_core_v3.ApiVersion_V3, - }), - }, - }). - Get() - - httpListener.FilterChains = envoy_v3.FilterChains(hcm) + httpListener.FilterChains = envoy_v3.FilterChains(getGlobalExtAuthHCM()) c.Request(listenerType).Equals(&envoy_discovery_v3.DiscoveryResponse{ TypeUrl: listenerType, @@ -128,28 +107,7 @@ func globalExternalAuthorizationFilterExistsTLS(t *testing.T, rh cache.ResourceE // replace the default filter chains with an HCM that includes the global // extAuthz filter. - var hcm = envoy_v3.HTTPConnectionManagerBuilder(). - RouteConfigName("ingress_http"). - MetricsPrefix("ingress_http"). - AccessLoggers(envoy_v3.FileAccessLogEnvoy("/dev/stdout", "", nil, contour_api_v1alpha1.LogLevelInfo)). - DefaultFilters(). - AddFilter(&http.HttpFilter{ - Name: wellknown.HTTPExternalAuthorization, - ConfigType: &http.HttpFilter_TypedConfig{ - TypedConfig: protobuf.MustMarshalAny(&envoy_config_filter_http_ext_authz_v3.ExtAuthz{ - Services: grpcCluster("extension/auth/extension"), - ClearRouteCache: true, - IncludePeerCertificate: true, - StatusOnError: &envoy_type.HttpStatus{ - Code: envoy_type.StatusCode_Forbidden, - }, - TransportApiVersion: envoy_core_v3.ApiVersion_V3, - }), - }, - }). - Get() - - httpListener.FilterChains = envoy_v3.FilterChains(hcm) + httpListener.FilterChains = envoy_v3.FilterChains(getGlobalExtAuthHCM()) httpsListener := &envoy_listener_v3.Listener{ Name: "ingress_https", @@ -223,28 +181,7 @@ func globalExternalAuthorizationWithTLSGlobalAuthDisabled(t *testing.T, rh cache // replace the default filter chains with an HCM that includes the global // extAuthz filter. - var hcm = envoy_v3.HTTPConnectionManagerBuilder(). - RouteConfigName("ingress_http"). - MetricsPrefix("ingress_http"). - AccessLoggers(envoy_v3.FileAccessLogEnvoy("/dev/stdout", "", nil, contour_api_v1alpha1.LogLevelInfo)). - DefaultFilters(). - AddFilter(&http.HttpFilter{ - Name: wellknown.HTTPExternalAuthorization, - ConfigType: &http.HttpFilter_TypedConfig{ - TypedConfig: protobuf.MustMarshalAny(&envoy_config_filter_http_ext_authz_v3.ExtAuthz{ - Services: grpcCluster("extension/auth/extension"), - ClearRouteCache: true, - IncludePeerCertificate: true, - StatusOnError: &envoy_type.HttpStatus{ - Code: envoy_type.StatusCode_Forbidden, - }, - TransportApiVersion: envoy_core_v3.ApiVersion_V3, - }), - }, - }). - Get() - - httpListener.FilterChains = envoy_v3.FilterChains(hcm) + httpListener.FilterChains = envoy_v3.FilterChains(getGlobalExtAuthHCM()) httpsListener := &envoy_listener_v3.Listener{ Name: "ingress_https", @@ -308,28 +245,7 @@ func globalExternalAuthorizationWithMergedAuthPolicy(t *testing.T, rh cache.Reso // replace the default filter chains with an HCM that includes the global // extAuthz filter. - var hcm = envoy_v3.HTTPConnectionManagerBuilder(). - RouteConfigName("ingress_http"). - MetricsPrefix("ingress_http"). - AccessLoggers(envoy_v3.FileAccessLogEnvoy("/dev/stdout", "", nil, contour_api_v1alpha1.LogLevelInfo)). - DefaultFilters(). - AddFilter(&http.HttpFilter{ - Name: wellknown.HTTPExternalAuthorization, - ConfigType: &http.HttpFilter_TypedConfig{ - TypedConfig: protobuf.MustMarshalAny(&envoy_config_filter_http_ext_authz_v3.ExtAuthz{ - Services: grpcCluster("extension/auth/extension"), - ClearRouteCache: true, - IncludePeerCertificate: true, - StatusOnError: &envoy_type.HttpStatus{ - Code: envoy_type.StatusCode_Forbidden, - }, - TransportApiVersion: envoy_core_v3.ApiVersion_V3, - }), - }, - }). - Get() - - httpListener.FilterChains = envoy_v3.FilterChains(hcm) + httpListener.FilterChains = envoy_v3.FilterChains(getGlobalExtAuthHCM()) c.Request(listenerType).Equals(&envoy_discovery_v3.DiscoveryResponse{ TypeUrl: listenerType, @@ -403,28 +319,7 @@ func globalExternalAuthorizationWithMergedAuthPolicyTLS(t *testing.T, rh cache.R // replace the default filter chains with an HCM that includes the global // extAuthz filter. - var hcm = envoy_v3.HTTPConnectionManagerBuilder(). - RouteConfigName("ingress_http"). - MetricsPrefix("ingress_http"). - AccessLoggers(envoy_v3.FileAccessLogEnvoy("/dev/stdout", "", nil, contour_api_v1alpha1.LogLevelInfo)). - DefaultFilters(). - AddFilter(&http.HttpFilter{ - Name: wellknown.HTTPExternalAuthorization, - ConfigType: &http.HttpFilter_TypedConfig{ - TypedConfig: protobuf.MustMarshalAny(&envoy_config_filter_http_ext_authz_v3.ExtAuthz{ - Services: grpcCluster("extension/auth/extension"), - ClearRouteCache: true, - IncludePeerCertificate: true, - StatusOnError: &envoy_type.HttpStatus{ - Code: envoy_type.StatusCode_Forbidden, - }, - TransportApiVersion: envoy_core_v3.ApiVersion_V3, - }), - }, - }). - Get() - - httpListener.FilterChains = envoy_v3.FilterChains(hcm) + httpListener.FilterChains = envoy_v3.FilterChains(getGlobalExtAuthHCM()) httpsListener := &envoy_listener_v3.Listener{ Name: "ingress_https", @@ -545,28 +440,7 @@ func globalExternalAuthorizationWithTLSAuthOverride(t *testing.T, rh cache.Resou // replace the default filter chains with an HCM that includes the global // extAuthz filter. - var hcm = envoy_v3.HTTPConnectionManagerBuilder(). - RouteConfigName("ingress_http"). - MetricsPrefix("ingress_http"). - AccessLoggers(envoy_v3.FileAccessLogEnvoy("/dev/stdout", "", nil, contour_api_v1alpha1.LogLevelInfo)). - DefaultFilters(). - AddFilter(&http.HttpFilter{ - Name: wellknown.HTTPExternalAuthorization, - ConfigType: &http.HttpFilter_TypedConfig{ - TypedConfig: protobuf.MustMarshalAny(&envoy_config_filter_http_ext_authz_v3.ExtAuthz{ - Services: grpcCluster("extension/auth/extension"), - ClearRouteCache: true, - IncludePeerCertificate: true, - StatusOnError: &envoy_type.HttpStatus{ - Code: envoy_type.StatusCode_Forbidden, - }, - TransportApiVersion: envoy_core_v3.ApiVersion_V3, - }), - }, - }). - Get() - - httpListener.FilterChains = envoy_v3.FilterChains(hcm) + httpListener.FilterChains = envoy_v3.FilterChains(getGlobalExtAuthHCM()) httpsListener := &envoy_listener_v3.Listener{ Name: "ingress_https", @@ -708,3 +582,27 @@ func TestGlobalAuthorization(t *testing.T) { }) } } + +// getGlobalExtAuthHCM returns a HTTP Connection Manager with Global External Authorization configured. +func getGlobalExtAuthHCM() *envoy_listener_v3.Filter { + return envoy_v3.HTTPConnectionManagerBuilder(). + RouteConfigName("ingress_http"). + MetricsPrefix("ingress_http"). + AccessLoggers(envoy_v3.FileAccessLogEnvoy("/dev/stdout", "", nil, contour_api_v1alpha1.LogLevelInfo)). + DefaultFilters(). + AddFilter(&http.HttpFilter{ + Name: wellknown.HTTPExternalAuthorization, + ConfigType: &http.HttpFilter_TypedConfig{ + TypedConfig: protobuf.MustMarshalAny(&envoy_config_filter_http_ext_authz_v3.ExtAuthz{ + Services: grpcCluster("extension/auth/extension"), + ClearRouteCache: true, + IncludePeerCertificate: true, + StatusOnError: &envoy_type.HttpStatus{ + Code: envoy_type.StatusCode_Forbidden, + }, + TransportApiVersion: envoy_core_v3.ApiVersion_V3, + }), + }, + }). + Get() +} diff --git a/internal/xdscache/v3/listener.go b/internal/xdscache/v3/listener.go index f1069bb452d..efe1ab5e016 100644 --- a/internal/xdscache/v3/listener.go +++ b/internal/xdscache/v3/listener.go @@ -162,6 +162,7 @@ type GlobalExternalAuthConfig struct { SNI string Timeout timeout.Setting Context map[string]string + WithRequestBody *dag.AuthorizationServerBufferSettings } // DefaultListeners returns the configured Listeners or a single @@ -433,13 +434,7 @@ func (c *ListenerCache) OnChange(root *dag.DAG) { var authFilter *http.HttpFilter if vh.ExternalAuthorization != nil { - authFilter = envoy_v3.FilterExternalAuthz( - vh.ExternalAuthorization.AuthorizationService.Name, - vh.ExternalAuthorization.AuthorizationService.SNI, - vh.ExternalAuthorization.AuthorizationFailOpen, - vh.ExternalAuthorization.AuthorizationResponseTimeout, - vh.ExternalAuthorization.AuthorizationServerWithRequestBody, - ) + authFilter = envoy_v3.FilterExternalAuthz(vh.ExternalAuthorization) } // Create a uniquely named HTTP connection manager for @@ -577,7 +572,16 @@ func httpGlobalExternalAuthConfig(config *GlobalExternalAuthConfig) *http.HttpFi return nil } - return envoy_v3.FilterExternalAuthz(dag.ExtensionClusterName(config.ExtensionService), config.SNI, config.FailOpen, config.Timeout, nil) + return envoy_v3.FilterExternalAuthz(&dag.ExternalAuthorization{ + AuthorizationService: &dag.ExtensionCluster{ + Name: dag.ExtensionClusterName(config.ExtensionService), + SNI: config.SNI, + }, + AuthorizationFailOpen: config.FailOpen, + AuthorizationResponseTimeout: config.Timeout, + AuthorizationServerWithRequestBody: config.WithRequestBody, + }) + } func envoyGlobalRateLimitConfig(config *RateLimitConfig) *envoy_v3.GlobalRateLimitConfig { diff --git a/internal/xdscache/v3/route_test.go b/internal/xdscache/v3/route_test.go index a668d034547..7fc16b96ec1 100644 --- a/internal/xdscache/v3/route_test.go +++ b/internal/xdscache/v3/route_test.go @@ -27,6 +27,7 @@ import ( contour_api_v1alpha1 "github.com/projectcontour/contour/apis/projectcontour/v1alpha1" "github.com/projectcontour/contour/internal/dag" envoy_v3 "github.com/projectcontour/contour/internal/envoy/v3" + "github.com/projectcontour/contour/internal/fixture" "github.com/projectcontour/contour/internal/protobuf" "github.com/projectcontour/contour/internal/ref" "github.com/stretchr/testify/assert" @@ -4028,3 +4029,42 @@ func withMirrorPolicy(route *envoy_route_v3.Route_Route, mirror string) *envoy_r }} return route } + +// buildDAGGlobalExtAuth produces a dag.DAG from the supplied objects with global external authorization configured. +func buildDAGGlobalExtAuth(t *testing.T, fallbackCertificate *types.NamespacedName, objs ...interface{}) *dag.DAG { + builder := dag.Builder{ + Source: dag.KubernetesCache{ + FieldLogger: fixture.NewTestLogger(t), + }, + Processors: []dag.Processor{ + &dag.ExtensionServiceProcessor{}, + &dag.IngressProcessor{ + FieldLogger: fixture.NewTestLogger(t), + }, + &dag.HTTPProxyProcessor{ + FallbackCertificate: fallbackCertificate, + GlobalExternalAuthorization: &contour_api_v1.AuthorizationServer{ + ExtensionServiceRef: contour_api_v1.ExtensionServiceReference{ + Name: "test", + Namespace: "ns", + }, + FailOpen: false, + AuthPolicy: &contour_api_v1.AuthorizationPolicy{ + Context: map[string]string{ + "header_1": "message_1", + "header_2": "message_2", + }, + }, + ResponseTimeout: "10s", + }, + }, + &dag.ListenerProcessor{}, + }, + } + + for _, o := range objs { + builder.Source.Insert(o) + } + + return builder.Build() +} diff --git a/internal/xdscache/v3/secret_test.go b/internal/xdscache/v3/secret_test.go index d13660d51e9..26c6a063bd9 100644 --- a/internal/xdscache/v3/secret_test.go +++ b/internal/xdscache/v3/secret_test.go @@ -519,45 +519,6 @@ func buildDAGFallback(t *testing.T, fallbackCertificate *types.NamespacedName, o return builder.Build() } -// buildDAGGlobalExtAuth produces a dag.DAG from the supplied objects with global external authorization configured. -func buildDAGGlobalExtAuth(t *testing.T, fallbackCertificate *types.NamespacedName, objs ...interface{}) *dag.DAG { - builder := dag.Builder{ - Source: dag.KubernetesCache{ - FieldLogger: fixture.NewTestLogger(t), - }, - Processors: []dag.Processor{ - &dag.ExtensionServiceProcessor{}, - &dag.IngressProcessor{ - FieldLogger: fixture.NewTestLogger(t), - }, - &dag.HTTPProxyProcessor{ - FallbackCertificate: fallbackCertificate, - GlobalExternalAuthorization: &contour_api_v1.AuthorizationServer{ - ExtensionServiceRef: contour_api_v1.ExtensionServiceReference{ - Name: "test", - Namespace: "ns", - }, - FailOpen: false, - AuthPolicy: &contour_api_v1.AuthorizationPolicy{ - Context: map[string]string{ - "header_1": "message_1", - "header_2": "message_2", - }, - }, - ResponseTimeout: "10s", - }, - }, - &dag.ListenerProcessor{}, - }, - } - - for _, o := range objs { - builder.Source.Insert(o) - } - - return builder.Build() -} - func secretmap(secrets ...*envoy_tls_v3.Secret) map[string]*envoy_tls_v3.Secret { m := make(map[string]*envoy_tls_v3.Secret) for _, s := range secrets { diff --git a/site/content/docs/main/guides/external-authorization.md b/site/content/docs/main/guides/external-authorization.md index 9524a28fb0d..048f3f64520 100644 --- a/site/content/docs/main/guides/external-authorization.md +++ b/site/content/docs/main/guides/external-authorization.md @@ -481,9 +481,9 @@ $ curl -k https://local.projectcontour.io/test/$((RANDOM)) {"TestId":"","Path":"/test/51","Host":"local.projectcontour.io","Method":"GET","Proto":"HTTP/1.1","Headers":{"Accept":["*/*"],"User-Agent":["curl/7.86.0"],"X-Envoy-Expected-Rq-Timeout-Ms":["15000"],"X-Envoy-Internal":["true"],"X-Forwarded-For":["172.18.0.1"],"X-Forwarded-Proto":["https"],"X-Request-Id":["18716e12-dcce-45ba-a3bb-bc26af3775d2"],"X-Request-Start":["t=1676901847.802"]}} ``` -### Overriding global external authorization for a virtual host +### Overriding global external authorization for a HTTPS virtual host -Sometimes you may want a different configuration than what is defined globally. To override the global external authorization, add the `authorization` block to your HTTPProxy as shown below +You may want a different configuration than what is defined globally. To override the global external authorization, add the `authorization` block to your TLS enabled HTTPProxy as shown below ```yaml apiVersion: projectcontour.io/v1 @@ -517,6 +517,8 @@ $ curl -k --user user1:password1 https://local.projectcontour.io/test/$((RANDOM) {"TestId":"","Path":"/test/4514","Host":"local.projectcontour.io","Method":"GET","Proto":"HTTP/1.1","Headers":{"Accept":["*/*"],"Auth-Context-Overriden_message":["overriden_value"],"Auth-Handler":["htpasswd"],"Auth-Realm":["default"],"Auth-Username":["user1"],"Authorization":["Basic dXNlcjE6cGFzc3dvcmQx"],"User-Agent":["curl/7.86.0"],"X-Envoy-Expected-Rq-Timeout-Ms":["15000"],"X-Envoy-Internal":["true"],"X-Forwarded-For":["172.18.0.1"],"X-Forwarded-Proto":["https"],"X-Request-Id":["8a02d6ce-8be0-4e87-8ed8-cca7e239e986"],"X-Request-Start":["t=1676902237.111"]}} ``` +NOTE: You can only override the global external configuration on a HTTPS virtual host. + ## Caveats There are a few caveats to consider when deploying external diff --git a/test/e2e/deployment.go b/test/e2e/deployment.go index 8e4a1f189c4..5316726f974 100644 --- a/test/e2e/deployment.go +++ b/test/e2e/deployment.go @@ -108,7 +108,7 @@ type Deployment struct { RateLimitService *v1.Service RateLimitExtensionService *contour_api_v1alpha1.ExtensionService - // // Global External Authorization deployment. + // Global External Authorization deployment. GlobalExtAuthDeployment *apps_v1.Deployment GlobalExtAuthService *v1.Service GlobalExtAuthExtensionService *contour_api_v1alpha1.ExtensionService @@ -233,7 +233,7 @@ func (d *Deployment) UnmarshalResources() error { return err } - // // Global external auth + // Global external auth globalExtAuthExamplePath := filepath.Join(filepath.Dir(thisFile), "..", "..", "examples", "global-external-auth") globalExtAuthServerDeploymentFile := filepath.Join(globalExtAuthExamplePath, "01-authserver.yaml") globalExtAuthExtSvcFile := filepath.Join(globalExtAuthExamplePath, "02-globalextauth-extsvc.yaml") From dd6a8b519c3b0241b347ab4a31af42403160e0d4 Mon Sep 17 00:00:00 2001 From: claytonig Date: Fri, 10 Mar 2023 16:48:15 +0100 Subject: [PATCH 3/3] fix e2e tests Signed-off-by: claytonig --- cmd/contour/serve.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/contour/serve.go b/cmd/contour/serve.go index 02a2a8b3574..93afd39757c 100644 --- a/cmd/contour/serve.go +++ b/cmd/contour/serve.go @@ -695,7 +695,6 @@ func (s *Server) setupGlobalExternalAuthentication(contourConfiguration contour_ Timeout: responseTimeout, FailOpen: contourConfiguration.GlobalExternalAuthorization.FailOpen, Context: context, - WithRequestBody: &dag.AuthorizationServerBufferSettings{}, } if contourConfiguration.GlobalExternalAuthorization.WithRequestBody != nil {