From cdd601d0f92da908f3c07cf955706b1304e54c22 Mon Sep 17 00:00:00 2001 From: ssttehrani Date: Sat, 27 Jan 2024 16:56:11 +0330 Subject: [PATCH 1/4] feat: add http support to authz filter --- apis/projectcontour/v1/httpproxy.go | 21 ++++++++++++++++++++ internal/dag/dag.go | 10 ++++++++++ internal/dag/httpproxy_processor.go | 15 +++++++++++++++ internal/envoy/v3/listener.go | 30 +++++++++++++++++++++++------ 4 files changed, 70 insertions(+), 6 deletions(-) diff --git a/apis/projectcontour/v1/httpproxy.go b/apis/projectcontour/v1/httpproxy.go index 7d1ffac3c73..db39e826709 100644 --- a/apis/projectcontour/v1/httpproxy.go +++ b/apis/projectcontour/v1/httpproxy.go @@ -228,6 +228,15 @@ type ExtensionServiceReference struct { Name string `json:"name,omitempty" protobuf:"bytes,3,opt,name=name"` } +// AuthorizationServiceType is an alias to enforce validation +// +kubebuilder:validation:Enum=grpc;http +type AuthorizationServiceAPIType string + +const ( + AuthorizationGRPCService AuthorizationServiceAPIType = "grpc" + AuthorizationHTTPService AuthorizationServiceAPIType = "http" +) + // AuthorizationServer configures an external server to authenticate // client requests. The external server must implement the v3 Envoy // external authorization GRPC protocol (https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto). @@ -237,6 +246,18 @@ type AuthorizationServer struct { // +optional ExtensionServiceRef ExtensionServiceReference `json:"extensionRef,omitempty"` + // ServiceAPIType defines the external authorization service API type. + // It indicates the protocol implemented by the external server, specifying whether it's a raw HTTP authorization server + // or a gRPC authorization server. + // + // +optional + ServiceAPIType AuthorizationServiceAPIType `json:"serviceAPIType,omitempty"` + + // ServerURI sets the URI of the external HTTP authorization server to which authorization requests must be sent. + // Only required for http services. + // +optional + ServerURI string `json:"serverURI,omitempty"` + // AuthPolicy sets a default authorization policy for client requests. // This policy will be used unless overridden by individual routes. // diff --git a/internal/dag/dag.go b/internal/dag/dag.go index e8f9368f344..1da73f2dd50 100644 --- a/internal/dag/dag.go +++ b/internal/dag/dag.go @@ -27,6 +27,7 @@ import ( v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" + contour_api_v1 "github.com/projectcontour/contour/apis/projectcontour/v1" "github.com/projectcontour/contour/internal/status" "github.com/projectcontour/contour/internal/timeout" ) @@ -853,6 +854,15 @@ type ExternalAuthorization struct { // authorization is enabled for this host. AuthorizationService *ExtensionCluster + // ServiceAPIType defines the external authorization service API type. + // It indicates the protocol implemented by the external server, specifying whether it's a raw HTTP authorization server + // or a gRPC authorization server. + ServiceAPIType contour_api_v1.AuthorizationServiceAPIType + + // ServerURI sets the URI of the external HTTP authorization server to which authorization requests must be sent. + // Only required for http services. + ServerURI string + // AuthorizationResponseTimeout sets how long the proxy should wait // for authorization server responses. AuthorizationResponseTimeout timeout.Setting diff --git a/internal/dag/httpproxy_processor.go b/internal/dag/httpproxy_processor.go index 9f56ad9c7b2..51a96ab82a0 100644 --- a/internal/dag/httpproxy_processor.go +++ b/internal/dag/httpproxy_processor.go @@ -1375,12 +1375,27 @@ func (p *HTTPProxyProcessor) computeVirtualHostAuthorization(auth *contour_api_v return nil } + if auth.ServiceAPIType == contour_api_v1.AuthorizationHTTPService && auth.ServerURI == "" { + validCond.AddErrorf(contour_api_v1.ConditionTypeAuthError, "AuthBadServerURI", + "Spec.Virtualhost.Authorization.ServerURI is not set and it is required for http type") + + return nil + } + globalExternalAuthorization := &ExternalAuthorization{ AuthorizationService: ext, AuthorizationFailOpen: auth.FailOpen, AuthorizationResponseTimeout: *respTimeout, } + switch auth.ServiceAPIType { + case contour_api_v1.AuthorizationGRPCService: + globalExternalAuthorization.ServiceAPIType = contour_api_v1.AuthorizationGRPCService + case contour_api_v1.AuthorizationHTTPService: + globalExternalAuthorization.ServiceAPIType = contour_api_v1.AuthorizationHTTPService + globalExternalAuthorization.ServerURI = auth.ServerURI + } + if auth.WithRequestBody != nil { var maxRequestBytes = defaultMaxRequestBytes if auth.WithRequestBody.MaxRequestBytes != 0 { diff --git a/internal/envoy/v3/listener.go b/internal/envoy/v3/listener.go index efd18cef12e..865bb22f5ca 100644 --- a/internal/envoy/v3/listener.go +++ b/internal/envoy/v3/listener.go @@ -41,6 +41,7 @@ import ( envoy_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/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" contour_api_v1alpha1 "github.com/projectcontour/contour/apis/projectcontour/v1alpha1" "github.com/projectcontour/contour/internal/dag" "github.com/projectcontour/contour/internal/envoy" @@ -780,9 +781,6 @@ end // requested parameters. 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(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? @@ -791,11 +789,31 @@ func FilterExternalAuthz(externalAuthorization *dag.ExternalAuthorization) *http StatusOnError: &envoy_type.HttpStatus{ Code: envoy_type.StatusCode_Forbidden, }, - MetadataContextNamespaces: []string{}, - IncludePeerCertificate: true, // TODO(jpeach): When we move to the Envoy v4 API, propagate the // `transport_api_version` from ExtensionServiceSpec ProtocolVersion. - TransportApiVersion: envoy_core_v3.ApiVersion_V3, + TransportApiVersion: envoy_core_v3.ApiVersion_V3, + IncludePeerCertificate: true, + } + + switch externalAuthorization.ServiceAPIType { + case contour_api_v1.AuthorizationGRPCService: + authConfig.Services = &envoy_config_filter_http_ext_authz_v3.ExtAuthz_GrpcService{ + GrpcService: GrpcService(externalAuthorization.AuthorizationService.Name, externalAuthorization.AuthorizationService.SNI, externalAuthorization.AuthorizationResponseTimeout), + } + authConfig.MetadataContextNamespaces = []string{} + + case contour_api_v1.AuthorizationHTTPService: + authConfig.Services = &envoy_config_filter_http_ext_authz_v3.ExtAuthz_HttpService{ + HttpService: &envoy_config_filter_http_ext_authz_v3.HttpService{ + ServerUri: &envoy_core_v3.HttpUri{ + Uri: externalAuthorization.ServerURI, + HttpUpstreamType: &envoy_core_v3.HttpUri_Cluster{ + Cluster: externalAuthorization.AuthorizationService.Name, + }, + Timeout: envoy.Timeout(externalAuthorization.AuthorizationResponseTimeout), + }, + }, + } } if externalAuthorization.AuthorizationServerWithRequestBody != nil { From 8af27353cea8a91350d4ba1bfa4f2534f2ea3573 Mon Sep 17 00:00:00 2001 From: ssttehrani Date: Sun, 28 Jan 2024 10:59:14 +0330 Subject: [PATCH 2/4] feat: add http support to authz filter --- apis/projectcontour/v1/httpproxy.go | 3 ++- apis/projectcontour/v1alpha1/extensionservice.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apis/projectcontour/v1/httpproxy.go b/apis/projectcontour/v1/httpproxy.go index db39e826709..4de15d607b3 100644 --- a/apis/projectcontour/v1/httpproxy.go +++ b/apis/projectcontour/v1/httpproxy.go @@ -229,7 +229,6 @@ type ExtensionServiceReference struct { } // AuthorizationServiceType is an alias to enforce validation -// +kubebuilder:validation:Enum=grpc;http type AuthorizationServiceAPIType string const ( @@ -251,6 +250,8 @@ type AuthorizationServer struct { // or a gRPC authorization server. // // +optional + // +kubebuilder:validation:Enum=http;grpc + // +kubebuilder:default=grpc ServiceAPIType AuthorizationServiceAPIType `json:"serviceAPIType,omitempty"` // ServerURI sets the URI of the external HTTP authorization server to which authorization requests must be sent. diff --git a/apis/projectcontour/v1alpha1/extensionservice.go b/apis/projectcontour/v1alpha1/extensionservice.go index 62b891603d2..5fc4a3f85b4 100644 --- a/apis/projectcontour/v1alpha1/extensionservice.go +++ b/apis/projectcontour/v1alpha1/extensionservice.go @@ -80,7 +80,7 @@ type ExtensionServiceSpec struct { // Values may be h2 or h2c. If omitted, protocol-selection falls back on Service annotations. // // +optional - // +kubebuilder:validation:Enum=h2;h2c + // +kubebuilder:validation:Enum=h1,h2;h2c Protocol *string `json:"protocol,omitempty"` // The policy for load balancing GRPC service requests. Note that the From a40d9c900542c6deb1c43697539c1e8dc8385f53 Mon Sep 17 00:00:00 2001 From: ssttehrani Date: Sun, 28 Jan 2024 11:19:49 +0330 Subject: [PATCH 3/4] feat: add http support to authz filter --- .../v1alpha1/extensionservice.go | 2 +- examples/contour/01-crds.yaml | 46 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/apis/projectcontour/v1alpha1/extensionservice.go b/apis/projectcontour/v1alpha1/extensionservice.go index 5fc4a3f85b4..f2b041e83e1 100644 --- a/apis/projectcontour/v1alpha1/extensionservice.go +++ b/apis/projectcontour/v1alpha1/extensionservice.go @@ -80,7 +80,7 @@ type ExtensionServiceSpec struct { // Values may be h2 or h2c. If omitted, protocol-selection falls back on Service annotations. // // +optional - // +kubebuilder:validation:Enum=h1,h2;h2c + // +kubebuilder:validation:Enum=h1;h2;h2c Protocol *string `json:"protocol,omitempty"` // The policy for load balancing GRPC service requests. Note that the diff --git a/examples/contour/01-crds.yaml b/examples/contour/01-crds.yaml index 5db4f122a02..7c301091870 100644 --- a/examples/contour/01-crds.yaml +++ b/examples/contour/01-crds.yaml @@ -640,6 +640,21 @@ spec: 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 + serverURI: + description: ServerURI sets the URI of the external HTTP authorization + server to which authorization requests must be sent. Only required + for http services. + type: string + serviceAPIType: + default: grpc + description: ServiceAPIType defines the external authorization + service API type. It indicates the protocol implemented by the + external server, specifying whether it's a raw HTTP authorization + server or a gRPC authorization server. + enum: + - http + - grpc + type: string withRequestBody: description: WithRequestBody specifies configuration for sending the client request's body to authorization server. @@ -4300,6 +4315,21 @@ spec: 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 + serverURI: + description: ServerURI sets the URI of the external HTTP authorization + server to which authorization requests must be sent. Only + required for http services. + type: string + serviceAPIType: + default: grpc + description: ServiceAPIType defines the external authorization + service API type. It indicates the protocol implemented + by the external server, specifying whether it's a raw HTTP + authorization server or a gRPC authorization server. + enum: + - http + - grpc + type: string withRequestBody: description: WithRequestBody specifies configuration for sending the client request's body to authorization server. @@ -4961,6 +4991,7 @@ spec: used to reach this Service. Values may be h2 or h2c. If omitted, protocol-selection falls back on Service annotations. enum: + - h1 - h2 - h2c type: string @@ -7224,6 +7255,21 @@ spec: 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 + serverURI: + description: ServerURI sets the URI of the external HTTP authorization + server to which authorization requests must be sent. Only + required for http services. + type: string + serviceAPIType: + default: grpc + description: ServiceAPIType defines the external authorization + service API type. It indicates the protocol implemented + by the external server, specifying whether it's a raw HTTP + authorization server or a gRPC authorization server. + enum: + - http + - grpc + type: string withRequestBody: description: WithRequestBody specifies configuration for sending the client request's body to authorization server. From 8aeb83f62a53b0b1397afdf7f1bea22bc4c0f19c Mon Sep 17 00:00:00 2001 From: ssttehrani Date: Thu, 8 Feb 2024 00:17:23 +0330 Subject: [PATCH 4/4] feat: add http support to authz filter --- Makefile | 1 + apis/projectcontour/v1/httpproxy.go | 75 +++++++- cmd/contour/serve.go | 39 ++++ examples/contour/01-crds.yaml | 276 ++++++++++++++++++++++++++-- internal/dag/conditions.go | 50 +++++ internal/dag/dag.go | 18 +- internal/dag/httpproxy_processor.go | 40 +++- internal/envoy/v3/listener.go | 79 +++++++- internal/xdscache/v3/listener.go | 16 +- 9 files changed, 564 insertions(+), 30 deletions(-) diff --git a/Makefile b/Makefile index 81255feef62..6ba45be3ad0 100644 --- a/Makefile +++ b/Makefile @@ -156,6 +156,7 @@ container: ## Build the Contour container image --build-arg "BUILD_GOEXPERIMENT=$(BUILD_GOEXPERIMENT)" \ $(DOCKER_BUILD_LABELS) \ $(shell pwd) \ + --platform linux/amd64 \ --tag $(IMAGE):$(VERSION) push: ## Push the Contour container image to the Docker registry diff --git a/apis/projectcontour/v1/httpproxy.go b/apis/projectcontour/v1/httpproxy.go index 4de15d607b3..464728463ca 100644 --- a/apis/projectcontour/v1/httpproxy.go +++ b/apis/projectcontour/v1/httpproxy.go @@ -254,10 +254,10 @@ type AuthorizationServer struct { // +kubebuilder:default=grpc ServiceAPIType AuthorizationServiceAPIType `json:"serviceAPIType,omitempty"` - // ServerURI sets the URI of the external HTTP authorization server to which authorization requests must be sent. - // Only required for http services. + // HttpAuthorizationServerSettings defines configurations for interacting with an external HTTP authorization server. + // // +optional - ServerURI string `json:"serverURI,omitempty"` + HttpServerSettings *HttpAuthorizationServerSettings `json:"httpSettings,omitempty"` // AuthPolicy sets a default authorization policy for client requests. // This policy will be used unless overridden by individual routes. @@ -287,6 +287,75 @@ type AuthorizationServer struct { WithRequestBody *AuthorizationServerBufferSettings `json:"withRequestBody,omitempty"` } +// HttpAuthorizationServerSettings defines configurations for interacting with an external HTTP authorization server. +type HttpAuthorizationServerSettings struct { + // PathPrefix Sets a prefix to the value of authorization request header Path. + // + // +optional + PathPrefix string `json:"pathPrefix,omitempty"` + + // Note: This field is not used by Envoy + // https://github.com/envoyproxy/envoy/issues/5357 + // + // ServerURI sets the URI of the external HTTP authorization server to which authorization requests must be sent. + // + // // +required + // // +kubebuilder:validation:Pattern=`^https?://[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*(\/[^\s]*)?$` + // ServerURI string `json:"serverURI"` + + // AllowedAuthorizationHeaders specifies client request headers that will be sent to the authorization server. + // Note that in addition to the the user’s supplied matchers, Host, Method, Path, Content-Length, and Authorization are additionally included in the list. + // + // +optional + AllowedAuthorizationHeaders []HttpAuthorizationServerAllowedHeaders `json:"allowedAuthorizationHeaders,omitempty"` + + // AllowedUpstreamHeaders specifies authorization response headers that will be added to the original client request. + // Note that coexistent headers will be overridden. + // + // +optional + AllowedUpstreamHeaders []HttpAuthorizationServerAllowedHeaders `json:"allowedUpstreamHeaders,omitempty"` +} + +// HttpAuthorizationServerAllowedHeaders specifies how to conditionally match against allowed headers +// in the context of HTTP authorization. It includes options such as Exact, Prefix, Suffix, +// Contains, and IgnoreCase to customize header matching criteria. However, regex support +// is intentionally excluded to simplify the user experience and prevent potential issues. +// One of Prefix, Exact, Suffix or Contains must be provided. +type HttpAuthorizationServerAllowedHeaders struct { + // Exact specifies a string that the header name must be equal to. + // + // +optional + Exact string `json:"exact,omitempty"` + + // Prefix defines a prefix match for the header name. + // + // +optional + Prefix string `json:"prefix,omitempty"` + + // Suffix defines a suffix match for a header name. + // + // +optional + Suffix string `json:"suffix,omitempty"` + + // To streamline user experience and mitigate potential issues, we do not support regex. + // Additionally, it's essential to ensure that any regex patterns adhere to the configured runtime key, re2.max_program_size.error_level + // by verifying that the program size is smaller than the specified value. + // This necessitates thorough validation of user input. + // + // Regex string `json:"regex,omitempty"` + + // Contains specifies a substring that must be present in the header name. + // + // +optional + Contains string `json:"contains,omitempty"` + + // IgnoreCase specifies that string matching should be case insensitive. + // Note that this has no effect on the Regex parameter. + // + // +optional + IgnoreCase bool `json:"ignoreCase,omitempty"` +} + // AuthorizationServerBufferSettings enables ExtAuthz filter to buffer client request data and send it as part of authorization request type AuthorizationServerBufferSettings struct { // MaxRequestBytes sets the maximum size of message body ExtAuthz filter will hold in-memory. diff --git a/cmd/contour/serve.go b/cmd/contour/serve.go index cf434c86af8..97b07b9654d 100644 --- a/cmd/contour/serve.go +++ b/cmd/contour/serve.go @@ -861,12 +861,51 @@ func (s *Server) setupGlobalExternalAuthentication(contourConfiguration contour_ context = contourConfiguration.GlobalExternalAuthorization.AuthPolicy.Context } + if contourConfiguration.GlobalExternalAuthorization.ServiceAPIType == contour_api_v1.AuthorizationHTTPService && contourConfiguration.GlobalExternalAuthorization.HttpServerSettings == nil { + return nil, fmt.Errorf("Spec.globalExtAuth.HttpServerSettings is not set and it is required for http type") + } + + // Not required due to Kubernetes API server validation. + // + // if auth.ServiceAPIType == contour_api_v1.AuthorizationHTTPService && auth.HttpServerSettings.ServerURI == "" { + // validCond.AddErrorf(contour_api_v1.ConditionTypeAuthError, "AuthBadServerURI", + // "Spec.Virtualhost.Authorization.HttpServerSettings.ServerURI is not set or it's empty and it is required for http type") + + // return nil + // } + globalExternalAuthConfig := &xdscache_v3.GlobalExternalAuthConfig{ ExtensionServiceConfig: extensionSvcConfig, FailOpen: contourConfiguration.GlobalExternalAuthorization.FailOpen, Context: context, } + switch contourConfiguration.GlobalExternalAuthorization.ServiceAPIType { + case contour_api_v1.AuthorizationGRPCService: + globalExternalAuthConfig.ServiceAPIType = contour_api_v1.AuthorizationGRPCService + case contour_api_v1.AuthorizationHTTPService: + globalExternalAuthConfig.ServiceAPIType = contour_api_v1.AuthorizationHTTPService + globalExternalAuthConfig.HttpPathPrefix = contourConfiguration.GlobalExternalAuthorization.HttpServerSettings.PathPrefix + // globalExternalAuthConfig.HttpServerURI = contourConfiguration.GlobalExternalAuthorization.HttpServerSettings.ServerURI + + if contourConfiguration.GlobalExternalAuthorization.HttpServerSettings.AllowedAuthorizationHeaders != nil { + if err := dag.ExternalAuthAllowedHeadersValid(contourConfiguration.GlobalExternalAuthorization.HttpServerSettings.AllowedAuthorizationHeaders); err != nil { + return nil, err + } + + globalExternalAuthConfig.HttpAllowedAuthorizationHeaders = contourConfiguration.GlobalExternalAuthorization.HttpServerSettings.AllowedAuthorizationHeaders + } + + if contourConfiguration.GlobalExternalAuthorization.HttpServerSettings.AllowedUpstreamHeaders != nil { + if err := dag.ExternalAuthAllowedHeadersValid(contourConfiguration.GlobalExternalAuthorization.HttpServerSettings.AllowedUpstreamHeaders); err != nil { + + return nil, err + } + + globalExternalAuthConfig.HttpAllowedUpstreamHeaders = contourConfiguration.GlobalExternalAuthorization.HttpServerSettings.AllowedUpstreamHeaders + } + } + if contourConfiguration.GlobalExternalAuthorization.WithRequestBody != nil { globalExternalAuthConfig.WithRequestBody = &dag.AuthorizationServerBufferSettings{ PackAsBytes: contourConfiguration.GlobalExternalAuthorization.WithRequestBody.PackAsBytes, diff --git a/examples/contour/01-crds.yaml b/examples/contour/01-crds.yaml index 7c301091870..876bef985e1 100644 --- a/examples/contour/01-crds.yaml +++ b/examples/contour/01-crds.yaml @@ -631,6 +631,91 @@ spec: intended for use only while migrating applications from internal authorization to Contour external authorization. type: boolean + httpSettings: + description: HttpAuthorizationServerSettings defines configurations + for interacting with an external HTTP authorization server. + properties: + allowedAuthorizationHeaders: + description: AllowedAuthorizationHeaders specifies client + request headers that will be sent to the authorization server. + Note that in addition to the the user’s supplied matchers, + Host, Method, Path, Content-Length, and Authorization are + additionally included in the list. + items: + description: HttpAuthorizationServerAllowedHeaders specifies + how to conditionally match against allowed headers in + the context of HTTP authorization. It includes options + such as Exact, Prefix, Suffix, Contains, and IgnoreCase + to customize header matching criteria. However, regex + support is intentionally excluded to simplify the user + experience and prevent potential issues. One of Prefix, + Exact, Suffix or Contains must be provided. + properties: + contains: + description: Contains specifies a substring that must + be present in the header name. + type: string + exact: + description: Exact specifies a string that the header + name must be equal to. + type: string + ignoreCase: + description: IgnoreCase specifies that string matching + should be case insensitive. Note that this has no + effect on the Regex parameter. + type: boolean + prefix: + description: Prefix defines a prefix match for the header + name. + type: string + suffix: + description: Suffix defines a suffix match for a header + name. + type: string + type: object + type: array + allowedUpstreamHeaders: + description: AllowedUpstreamHeaders specifies authorization + response headers that will be added to the original client + request. Note that coexistent headers will be overridden. + items: + description: HttpAuthorizationServerAllowedHeaders specifies + how to conditionally match against allowed headers in + the context of HTTP authorization. It includes options + such as Exact, Prefix, Suffix, Contains, and IgnoreCase + to customize header matching criteria. However, regex + support is intentionally excluded to simplify the user + experience and prevent potential issues. One of Prefix, + Exact, Suffix or Contains must be provided. + properties: + contains: + description: Contains specifies a substring that must + be present in the header name. + type: string + exact: + description: Exact specifies a string that the header + name must be equal to. + type: string + ignoreCase: + description: IgnoreCase specifies that string matching + should be case insensitive. Note that this has no + effect on the Regex parameter. + type: boolean + prefix: + description: Prefix defines a prefix match for the header + name. + type: string + suffix: + description: Suffix defines a suffix match for a header + name. + type: string + type: object + type: array + pathPrefix: + description: PathPrefix Sets a prefix to the value of authorization + request header Path. + type: string + type: object responseTimeout: description: ResponseTimeout configures maximum time to wait for a check response from the authorization server. Timeout durations @@ -640,11 +725,6 @@ spec: 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 - serverURI: - description: ServerURI sets the URI of the external HTTP authorization - server to which authorization requests must be sent. Only required - for http services. - type: string serviceAPIType: default: grpc description: ServiceAPIType defines the external authorization @@ -4306,6 +4386,94 @@ spec: It is intended for use only while migrating applications from internal authorization to Contour external authorization. type: boolean + httpSettings: + description: HttpAuthorizationServerSettings defines configurations + for interacting with an external HTTP authorization server. + properties: + allowedAuthorizationHeaders: + description: AllowedAuthorizationHeaders specifies client + request headers that will be sent to the authorization + server. Note that in addition to the the user’s supplied + matchers, Host, Method, Path, Content-Length, and Authorization + are additionally included in the list. + items: + description: HttpAuthorizationServerAllowedHeaders specifies + how to conditionally match against allowed headers + in the context of HTTP authorization. It includes + options such as Exact, Prefix, Suffix, Contains, and + IgnoreCase to customize header matching criteria. + However, regex support is intentionally excluded to + simplify the user experience and prevent potential + issues. One of Prefix, Exact, Suffix or Contains must + be provided. + properties: + contains: + description: Contains specifies a substring that + must be present in the header name. + type: string + exact: + description: Exact specifies a string that the header + name must be equal to. + type: string + ignoreCase: + description: IgnoreCase specifies that string matching + should be case insensitive. Note that this has + no effect on the Regex parameter. + type: boolean + prefix: + description: Prefix defines a prefix match for the + header name. + type: string + suffix: + description: Suffix defines a suffix match for a + header name. + type: string + type: object + type: array + allowedUpstreamHeaders: + description: AllowedUpstreamHeaders specifies authorization + response headers that will be added to the original + client request. Note that coexistent headers will be + overridden. + items: + description: HttpAuthorizationServerAllowedHeaders specifies + how to conditionally match against allowed headers + in the context of HTTP authorization. It includes + options such as Exact, Prefix, Suffix, Contains, and + IgnoreCase to customize header matching criteria. + However, regex support is intentionally excluded to + simplify the user experience and prevent potential + issues. One of Prefix, Exact, Suffix or Contains must + be provided. + properties: + contains: + description: Contains specifies a substring that + must be present in the header name. + type: string + exact: + description: Exact specifies a string that the header + name must be equal to. + type: string + ignoreCase: + description: IgnoreCase specifies that string matching + should be case insensitive. Note that this has + no effect on the Regex parameter. + type: boolean + prefix: + description: Prefix defines a prefix match for the + header name. + type: string + suffix: + description: Suffix defines a suffix match for a + header name. + type: string + type: object + type: array + pathPrefix: + description: PathPrefix Sets a prefix to the value of + authorization request header Path. + type: string + type: object responseTimeout: description: ResponseTimeout configures maximum time to wait for a check response from the authorization server. Timeout @@ -4315,11 +4483,6 @@ spec: 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 - serverURI: - description: ServerURI sets the URI of the external HTTP authorization - server to which authorization requests must be sent. Only - required for http services. - type: string serviceAPIType: default: grpc description: ServiceAPIType defines the external authorization @@ -7246,6 +7409,94 @@ spec: It is intended for use only while migrating applications from internal authorization to Contour external authorization. type: boolean + httpSettings: + description: HttpAuthorizationServerSettings defines configurations + for interacting with an external HTTP authorization server. + properties: + allowedAuthorizationHeaders: + description: AllowedAuthorizationHeaders specifies client + request headers that will be sent to the authorization + server. Note that in addition to the the user’s supplied + matchers, Host, Method, Path, Content-Length, and Authorization + are additionally included in the list. + items: + description: HttpAuthorizationServerAllowedHeaders specifies + how to conditionally match against allowed headers + in the context of HTTP authorization. It includes + options such as Exact, Prefix, Suffix, Contains, and + IgnoreCase to customize header matching criteria. + However, regex support is intentionally excluded to + simplify the user experience and prevent potential + issues. One of Prefix, Exact, Suffix or Contains must + be provided. + properties: + contains: + description: Contains specifies a substring that + must be present in the header name. + type: string + exact: + description: Exact specifies a string that the header + name must be equal to. + type: string + ignoreCase: + description: IgnoreCase specifies that string matching + should be case insensitive. Note that this has + no effect on the Regex parameter. + type: boolean + prefix: + description: Prefix defines a prefix match for the + header name. + type: string + suffix: + description: Suffix defines a suffix match for a + header name. + type: string + type: object + type: array + allowedUpstreamHeaders: + description: AllowedUpstreamHeaders specifies authorization + response headers that will be added to the original + client request. Note that coexistent headers will be + overridden. + items: + description: HttpAuthorizationServerAllowedHeaders specifies + how to conditionally match against allowed headers + in the context of HTTP authorization. It includes + options such as Exact, Prefix, Suffix, Contains, and + IgnoreCase to customize header matching criteria. + However, regex support is intentionally excluded to + simplify the user experience and prevent potential + issues. One of Prefix, Exact, Suffix or Contains must + be provided. + properties: + contains: + description: Contains specifies a substring that + must be present in the header name. + type: string + exact: + description: Exact specifies a string that the header + name must be equal to. + type: string + ignoreCase: + description: IgnoreCase specifies that string matching + should be case insensitive. Note that this has + no effect on the Regex parameter. + type: boolean + prefix: + description: Prefix defines a prefix match for the + header name. + type: string + suffix: + description: Suffix defines a suffix match for a + header name. + type: string + type: object + type: array + pathPrefix: + description: PathPrefix Sets a prefix to the value of + authorization request header Path. + type: string + type: object responseTimeout: description: ResponseTimeout configures maximum time to wait for a check response from the authorization server. Timeout @@ -7255,11 +7506,6 @@ spec: 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 - serverURI: - description: ServerURI sets the URI of the external HTTP authorization - server to which authorization requests must be sent. Only - required for http services. - type: string serviceAPIType: default: grpc description: ServiceAPIType defines the external authorization diff --git a/internal/dag/conditions.go b/internal/dag/conditions.go index 19f5c62d718..fce6c942987 100644 --- a/internal/dag/conditions.go +++ b/internal/dag/conditions.go @@ -407,6 +407,56 @@ func queryParameterMatchConditionsValid(conditions []contour_api_v1.MatchConditi return nil } +// ExternalAuthAllowedHeadersValid validates that the allowed header conditions within a +// slice of HttpAuthorizationServerAllowedHeaders are valid. Specifically, it returns an error for +// any of the following scenarios: +// - no conditions are set +// - more than one condition is set in the same allowed header condition branch +// - invalid regular expression is specified for the Regex condition +func ExternalAuthAllowedHeadersValid(allowedHeaders []contour_api_v1.HttpAuthorizationServerAllowedHeaders) error { + for _, allowedHeader := range allowedHeaders { + sum := 0 + + // To streamline user experience and mitigate potential issues, we do not support regex. + // Additionally, it's essential to ensure that any regex patterns adhere to the configured runtime key, re2.max_program_size.error_level + // by verifying that the program size is smaller than the specified value. + // This necessitates thorough validation of user input. + // + // if allowedHeader.Regex != "" { + // if err := ValidateRegex(allowedHeader.Regex); err != nil { + // return errors.New("the RE2 regex syntax is invalid") + // } + // sum++ + // } + + if allowedHeader.Exact != "" { + sum++ + } + + if allowedHeader.Prefix != "" { + sum++ + } + + if allowedHeader.Suffix != "" { + sum++ + } + + if allowedHeader.Contains != "" { + sum++ + } + + if sum == 0 { + return errors.New("one of prefix, suffix, exact or contains is required for each allowedHeader") + } + + if sum > 1 { + return errors.New("more than one prefix, suffix, exact or contains is not allowed in an allowedHeader") + } + } + + return nil +} + // ValidateRegex returns an error if the supplied // RE2 regex syntax is invalid. func ValidateRegex(regex string) error { diff --git a/internal/dag/dag.go b/internal/dag/dag.go index 1da73f2dd50..f8bc1052089 100644 --- a/internal/dag/dag.go +++ b/internal/dag/dag.go @@ -859,9 +859,23 @@ type ExternalAuthorization struct { // or a gRPC authorization server. ServiceAPIType contour_api_v1.AuthorizationServiceAPIType - // ServerURI sets the URI of the external HTTP authorization server to which authorization requests must be sent. + // HttpAllowedAuthorizationHeaders specifies client request headers that will be sent to the authorization server. + // Note that in addition to the the user’s supplied matchers, Host, Method, Path, Content-Length, and Authorization are additionally included in the list. + HttpAllowedAuthorizationHeaders []contour_api_v1.HttpAuthorizationServerAllowedHeaders + + // HttpAllowedUpstreamHeaders specifies authorization response headers that will be added to the original client request. + // Note that coexistent headers will be overridden. + HttpAllowedUpstreamHeaders []contour_api_v1.HttpAuthorizationServerAllowedHeaders + + // HttpPathPrefix Sets a prefix to the value of authorization request header Path. + HttpPathPrefix string + + // Note: This field is not used by Envoy + // https://github.com/envoyproxy/envoy/issues/5357 + // + // HttpServerURI sets the URI of the external HTTP authorization server to which authorization requests must be sent. // Only required for http services. - ServerURI string + // HttpServerURI string // AuthorizationResponseTimeout sets how long the proxy should wait // for authorization server responses. diff --git a/internal/dag/httpproxy_processor.go b/internal/dag/httpproxy_processor.go index 51a96ab82a0..4ed9e8e5188 100644 --- a/internal/dag/httpproxy_processor.go +++ b/internal/dag/httpproxy_processor.go @@ -1375,13 +1375,22 @@ func (p *HTTPProxyProcessor) computeVirtualHostAuthorization(auth *contour_api_v return nil } - if auth.ServiceAPIType == contour_api_v1.AuthorizationHTTPService && auth.ServerURI == "" { - validCond.AddErrorf(contour_api_v1.ConditionTypeAuthError, "AuthBadServerURI", - "Spec.Virtualhost.Authorization.ServerURI is not set and it is required for http type") + if auth.ServiceAPIType == contour_api_v1.AuthorizationHTTPService && auth.HttpServerSettings == nil { + validCond.AddErrorf(contour_api_v1.ConditionTypeAuthError, "AuthNilHttpSettings", + "Spec.Virtualhost.Authorization.HttpServerSettings is not set and it is required for http type") return nil } + // Not required due to Kubernetes API server validation. + // + // if auth.ServiceAPIType == contour_api_v1.AuthorizationHTTPService && auth.HttpServerSettings.ServerURI == "" { + // validCond.AddErrorf(contour_api_v1.ConditionTypeAuthError, "AuthBadServerURI", + // "Spec.Virtualhost.Authorization.HttpServerSettings.ServerURI is not set or it's empty and it is required for http type") + + // return nil + // } + globalExternalAuthorization := &ExternalAuthorization{ AuthorizationService: ext, AuthorizationFailOpen: auth.FailOpen, @@ -1393,7 +1402,30 @@ func (p *HTTPProxyProcessor) computeVirtualHostAuthorization(auth *contour_api_v globalExternalAuthorization.ServiceAPIType = contour_api_v1.AuthorizationGRPCService case contour_api_v1.AuthorizationHTTPService: globalExternalAuthorization.ServiceAPIType = contour_api_v1.AuthorizationHTTPService - globalExternalAuthorization.ServerURI = auth.ServerURI + globalExternalAuthorization.HttpPathPrefix = auth.HttpServerSettings.PathPrefix + // globalExternalAuthorization.HttpServerURI = auth.HttpServerSettings.ServerURI + + if auth.HttpServerSettings.AllowedAuthorizationHeaders != nil { + if err := ExternalAuthAllowedHeadersValid(auth.HttpServerSettings.AllowedAuthorizationHeaders); err != nil { + validCond.AddErrorf(contour_api_v1.ConditionTypeAuthError, "AuthBadAllowedHeader", + err.Error()) + + return nil + } + + globalExternalAuthorization.HttpAllowedAuthorizationHeaders = auth.HttpServerSettings.AllowedAuthorizationHeaders + } + + if auth.HttpServerSettings.AllowedUpstreamHeaders != nil { + if err := ExternalAuthAllowedHeadersValid(auth.HttpServerSettings.AllowedUpstreamHeaders); err != nil { + validCond.AddErrorf(contour_api_v1.ConditionTypeAuthError, "AuthBadAllowedHeader", + err.Error()) + + return nil + } + + globalExternalAuthorization.HttpAllowedUpstreamHeaders = auth.HttpServerSettings.AllowedUpstreamHeaders + } } if auth.WithRequestBody != nil { diff --git a/internal/envoy/v3/listener.go b/internal/envoy/v3/listener.go index 865bb22f5ca..66258803f9e 100644 --- a/internal/envoy/v3/listener.go +++ b/internal/envoy/v3/listener.go @@ -39,6 +39,7 @@ import ( http "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" tcp "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3" envoy_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" + envoy_matcher "github.com/envoyproxy/go-control-plane/envoy/type/matcher/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" @@ -61,7 +62,7 @@ const ( HTTPVersion3 HTTPVersionType = http.HttpConnectionManager_HTTP3 ) -// ProtoNamesForVersions returns the slice of ALPN protocol names for the give HTTP versions. +// ProtoNamesForVersions returns the slice of ALPN protocol names for the given HTTP versions. func ProtoNamesForVersions(versions ...HTTPVersionType) []string { protocols := map[HTTPVersionType]string{ HTTPVersion1: "http/1.1", @@ -777,6 +778,57 @@ end } } +// ExternalAuthzAllowedHeaders returns the slice of StringMatcher for a given slice of HttpAuthorizationServerAllowedHeaders. +func ExternalAuthzAllowedHeaders(allowedHeaders []contour_api_v1.HttpAuthorizationServerAllowedHeaders) []*envoy_matcher.StringMatcher { + var allowedHeaderPatterns []*envoy_matcher.StringMatcher + + for _, allowedHeader := range allowedHeaders { + switch { + case allowedHeader.Exact != "": + allowedHeaderPatterns = append(allowedHeaderPatterns, &envoy_matcher.StringMatcher{ + MatchPattern: &envoy_matcher.StringMatcher_Exact{ + Exact: allowedHeader.Exact, + }, + IgnoreCase: allowedHeader.IgnoreCase, + }) + case allowedHeader.Prefix != "": + allowedHeaderPatterns = append(allowedHeaderPatterns, &envoy_matcher.StringMatcher{ + MatchPattern: &envoy_matcher.StringMatcher_Prefix{ + Prefix: allowedHeader.Prefix, + }, + IgnoreCase: allowedHeader.IgnoreCase, + }) + case allowedHeader.Suffix != "": + allowedHeaderPatterns = append(allowedHeaderPatterns, &envoy_matcher.StringMatcher{ + MatchPattern: &envoy_matcher.StringMatcher_Suffix{ + Suffix: allowedHeader.Suffix, + }, + IgnoreCase: allowedHeader.IgnoreCase, + }) + // To streamline user experience and mitigate potential issues, we do not support regex. + // Additionally, it's essential to ensure that any regex patterns adhere to the configured runtime key, re2.max_program_size.error_level + // by verifying that the program size is smaller than the specified value. + // This necessitates thorough validation of user input. + // + // case allowedHeader.Regex != "": + // allowedHeaderPatterns = append(allowedHeaderPatterns, &envoy_matcher.StringMatcher{ + // MatchPattern: &envoy_matcher.StringMatcher_SafeRegex{ + // SafeRegex: SafeRegexMatch(allowedHeader.Regex), + // }, + // }) + case allowedHeader.Contains != "": + allowedHeaderPatterns = append(allowedHeaderPatterns, &envoy_matcher.StringMatcher{ + MatchPattern: &envoy_matcher.StringMatcher_Contains{ + Contains: allowedHeader.Contains, + }, + IgnoreCase: allowedHeader.IgnoreCase, + }) + } + } + + return allowedHeaderPatterns +} + // FilterExternalAuthz returns an `ext_authz` filter configured with the // requested parameters. func FilterExternalAuthz(externalAuthorization *dag.ExternalAuthorization) *http.HttpFilter { @@ -803,10 +855,11 @@ func FilterExternalAuthz(externalAuthorization *dag.ExternalAuthorization) *http authConfig.MetadataContextNamespaces = []string{} case contour_api_v1.AuthorizationHTTPService: - authConfig.Services = &envoy_config_filter_http_ext_authz_v3.ExtAuthz_HttpService{ + extAuthzService := &envoy_config_filter_http_ext_authz_v3.ExtAuthz_HttpService{ HttpService: &envoy_config_filter_http_ext_authz_v3.HttpService{ ServerUri: &envoy_core_v3.HttpUri{ - Uri: externalAuthorization.ServerURI, + // Uri: externalAuthorization.HttpServerURI, + Uri: "http://dummy/", HttpUpstreamType: &envoy_core_v3.HttpUri_Cluster{ Cluster: externalAuthorization.AuthorizationService.Name, }, @@ -814,6 +867,26 @@ func FilterExternalAuthz(externalAuthorization *dag.ExternalAuthorization) *http }, }, } + + if pathPrefix := externalAuthorization.HttpPathPrefix; pathPrefix != "" { + extAuthzService.HttpService.PathPrefix = pathPrefix + } + + if len(externalAuthorization.HttpAllowedAuthorizationHeaders) > 0 { + authConfig.AllowedHeaders = &envoy_matcher.ListStringMatcher{ + Patterns: ExternalAuthzAllowedHeaders(externalAuthorization.HttpAllowedAuthorizationHeaders), + } + } + + if len(externalAuthorization.HttpAllowedUpstreamHeaders) > 0 { + extAuthzService.HttpService.AuthorizationResponse = &envoy_config_filter_http_ext_authz_v3.AuthorizationResponse{ + AllowedUpstreamHeaders: &envoy_matcher.ListStringMatcher{ + Patterns: ExternalAuthzAllowedHeaders(externalAuthorization.HttpAllowedUpstreamHeaders), + }, + } + } + + authConfig.Services = extAuthzService } if externalAuthorization.AuthorizationServerWithRequestBody != nil { diff --git a/internal/xdscache/v3/listener.go b/internal/xdscache/v3/listener.go index 6bfee900859..6060256dc4c 100644 --- a/internal/xdscache/v3/listener.go +++ b/internal/xdscache/v3/listener.go @@ -22,6 +22,7 @@ import ( http "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" envoy_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" resource "github.com/envoyproxy/go-control-plane/pkg/resource/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/contour" "github.com/projectcontour/contour/internal/contourconfig" @@ -195,9 +196,14 @@ type RateLimitConfig struct { type GlobalExternalAuthConfig struct { ExtensionServiceConfig - FailOpen bool - Context map[string]string - WithRequestBody *dag.AuthorizationServerBufferSettings + FailOpen bool + Context map[string]string + ServiceAPIType contour_api_v1.AuthorizationServiceAPIType + HttpAllowedAuthorizationHeaders []contour_api_v1.HttpAuthorizationServerAllowedHeaders + HttpAllowedUpstreamHeaders []contour_api_v1.HttpAuthorizationServerAllowedHeaders + HttpPathPrefix string + WithRequestBody *dag.AuthorizationServerBufferSettings + // HttpServerURI string } // httpAccessLog returns the access log for the HTTP (non TLS) @@ -626,6 +632,10 @@ func httpGlobalExternalAuthConfig(config *GlobalExternalAuthConfig) *http.HttpFi Name: dag.ExtensionClusterName(config.ExtensionServiceConfig.ExtensionService), SNI: config.ExtensionServiceConfig.SNI, }, + ServiceAPIType: config.ServiceAPIType, + HttpAllowedAuthorizationHeaders: config.HttpAllowedAuthorizationHeaders, + HttpAllowedUpstreamHeaders: config.HttpAllowedUpstreamHeaders, + HttpPathPrefix: config.HttpPathPrefix, AuthorizationFailOpen: config.FailOpen, AuthorizationResponseTimeout: config.ExtensionServiceConfig.Timeout, AuthorizationServerWithRequestBody: config.WithRequestBody,