From 5725b15ea768aa4fa198b1d72101aff0c182969c Mon Sep 17 00:00:00 2001 From: viniciusdc Date: Fri, 5 Jun 2026 21:50:50 -0300 Subject: [PATCH 1/3] feat: routing.filters pass-through, host-scoped WebOrigins, unified route builders Unify the protected and public HTTPRoute builders onto a shared path, then build the additive changes on top so they land in one place for both routes. Refactor (routing/httproute.go): - Extract resolveListenerSection (TLS listener/section resolution), buildRouteMatches (path-match construction, parameterized by the default pathType so the main route stays PathPrefix and public routes stay Exact), and buildRouteRule (single rule with backend refs + filters). The main and public builders were ~95% duplicated; they now share these helpers. routing.filters pass-through: - Add RoutingConfig.Filters []gatewayv1.HTTPRouteFilter, appended in order to the rule of both the protected and public HTTPRoute via buildRouteRule. The operator does not interpret, reorder, or strip them -- same escape-hatch pattern as the existing Annotations field. Lets deployers fix gateway-side header issues (e.g. an upstream's invalid CORS header combo) without forking the operator or rebuilding the backend image. Host-scoped Keycloak WebOrigins: - Replace the wildcard WebOrigins ["*"] with the NebariApp hostname (both schemes) on all four client paths (main client create/update, SPA client create/update), via a buildWebOrigins helper mirroring buildRedirectURLs. Keeps the OIDC client to least privilege for CORS to the token endpoint. publicRoutes vs enforceAtGateway docs: - Cross-reference the two fields in the CRD comments and routing.md, calling out that publicRoutes carves out a subset of paths and is not the way to disable auth for the whole app (use enforceAtGateway: false). The reconciler/admission guard for the overlap case is intentionally left out of this change -- Gateway API precedence makes most overlaps intended, so the guard needs a precedence-aware design pass. Regenerated zz_generated.deepcopy.go, the CRD, and docs/api-reference.md. New unit tests cover filter propagation+ordering on both routes and the host-scoped WebOrigins. Closes #137 Closes #37 --- api/v1/nebariapp_types.go | 26 + api/v1/zz_generated.deepcopy.go | 8 + .../reconcilers.nebari.dev_nebariapps.yaml | 1341 +++++++++++++++++ docs/api-reference.md | 5 +- docs/reconcilers/routing.md | 40 +- .../reconcilers/auth/providers/keycloak.go | 20 +- .../auth/providers/keycloak_test.go | 30 + .../reconcilers/routing/httproute.go | 121 +- .../reconcilers/routing/httproute_test.go | 81 + 9 files changed, 1609 insertions(+), 63 deletions(-) diff --git a/api/v1/nebariapp_types.go b/api/v1/nebariapp_types.go index bb25dbe..2bef487 100644 --- a/api/v1/nebariapp_types.go +++ b/api/v1/nebariapp_types.go @@ -18,6 +18,7 @@ package v1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" ) // NebariAppSpec defines the desired state of NebariApp @@ -104,6 +105,15 @@ type RoutingConfig struct { // Each entry uses the same RouteMatch format as routes, supporting both // PathPrefix (default) and Exact matching via the pathType field. // Example: [{pathPrefix: "/api/v1/health", pathType: "Exact"}] + // + // PublicRoutes is for carving out a *subset* of paths that should bypass + // auth while the rest stays protected. It is not a way to disable auth for + // the whole app: if a public path also matches the main route (including the + // default "/" match when routes is empty) while auth.enforceAtGateway is + // true, both a protected and an unprotected HTTPRoute claim that path and + // the effective auth posture is undefined. To turn gateway auth off entirely, + // set auth.enforceAtGateway: false (keep provisioning the Keycloak client) or + // auth.enabled: false (no client at all) instead of listing every path here. // +optional PublicRoutes []RouteMatch `json:"publicRoutes,omitempty"` @@ -120,6 +130,18 @@ type RoutingConfig struct { // annotations always take precedence to avoid breaking internal behaviour. // +optional Annotations map[string]string `json:"annotations,omitempty"` + + // Filters are appended, in order, to every HTTPRouteRule the operator + // generates for this NebariApp (both the protected route and the public + // route built from publicRoutes). This is a pass-through to the upstream + // gateway.networking.k8s.io/v1 HTTPRouteFilter spec, mirroring the + // Annotations escape hatch: the operator does not interpret, reorder, or + // strip the filters, it only forwards them. Use it to adjust request or + // response headers at the gateway (for example, stripping an overpermissive + // CORS header an upstream sets, or adding security headers) without forking + // the operator or rebuilding the backend image. + // +optional + Filters []gatewayv1.HTTPRouteFilter `json:"filters,omitempty"` } // RouteMatch defines a path-based routing rule. @@ -230,6 +252,10 @@ type AuthConfig struct { // When false, the operator provisions the OIDC client and stores credentials // in a Secret, but does NOT create a SecurityPolicy - the application is // expected to handle OAuth natively (e.g., Grafana's built-in generic_oauth). + // This is the correct switch for "no gateway-level auth"; do not try to + // achieve that by listing every path under routing.publicRoutes, which + // instead produces overlapping protected and unprotected routes for the same + // path (see PublicRoutes). // +kubebuilder:default=true // +optional EnforceAtGateway *bool `json:"enforceAtGateway,omitempty"` diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index ab03041..30fcdf1 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -23,6 +23,7 @@ package v1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" + apisv1 "sigs.k8s.io/gateway-api/apis/v1" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -448,6 +449,13 @@ func (in *RoutingConfig) DeepCopyInto(out *RoutingConfig) { (*out)[key] = val } } + if in.Filters != nil { + in, out := &in.Filters, &out.Filters + *out = make([]apisv1.HTTPRouteFilter, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoutingConfig. diff --git a/config/crd/bases/reconcilers.nebari.dev_nebariapps.yaml b/config/crd/bases/reconcilers.nebari.dev_nebariapps.yaml index a93fd38..670b772 100644 --- a/config/crd/bases/reconcilers.nebari.dev_nebariapps.yaml +++ b/config/crd/bases/reconcilers.nebari.dev_nebariapps.yaml @@ -132,6 +132,10 @@ spec: When false, the operator provisions the OIDC client and stores credentials in a Secret, but does NOT create a SecurityPolicy - the application is expected to handle OAuth natively (e.g., Grafana's built-in generic_oauth). + This is the correct switch for "no gateway-level auth"; do not try to + achieve that by listing every path under routing.publicRoutes, which + instead produces overlapping protected and unprotected routes for the same + path (see PublicRoutes). type: boolean forwardAccessToken: description: |- @@ -442,6 +446,1334 @@ spec: These annotations are merged with any operator-managed annotations; operator annotations always take precedence to avoid breaking internal behaviour. type: object + filters: + description: |- + Filters are appended, in order, to every HTTPRouteRule the operator + generates for this NebariApp (both the protected route and the public + route built from publicRoutes). This is a pass-through to the upstream + gateway.networking.k8s.io/v1 HTTPRouteFilter spec, mirroring the + Annotations escape hatch: the operator does not interpret, reorder, or + strip the filters, it only forwards them. Use it to adjust request or + response headers at the gateway (for example, stripping an overpermissive + CORS header an upstream sets, or adding security headers) without forking + the operator or rebuilding the backend image. + items: + description: |- + HTTPRouteFilter defines processing steps that must be completed during the + request or response lifecycle. HTTPRouteFilters are meant as an extension + point to express processing that may be done in Gateway implementations. Some + examples include request or response modification, implementing + authentication strategies, rate-limiting, and traffic shaping. API + guarantee/conformance is defined based on the type of the filter. + + + + + + properties: + cors: + description: |- + CORS defines a schema for a filter that responds to the + cross-origin request based on HTTP response header. + + Support: Extended + + + properties: + allowCredentials: + description: |- + AllowCredentials indicates whether the actual cross-origin request allows + to include credentials. + + When set to true, the gateway will include the `Access-Control-Allow-Credentials` + response header with value true (case-sensitive). + + When set to false or omitted the gateway will omit the header + `Access-Control-Allow-Credentials` entirely (this is the standard CORS + behavior). + + Support: Extended + type: boolean + allowHeaders: + description: |- + AllowHeaders indicates which HTTP request headers are supported for + accessing the requested resource. + + Header names are not case sensitive. + + Multiple header names in the value of the `Access-Control-Allow-Headers` + response header are separated by a comma (","). + + When the `AllowHeaders` field is configured with one or more headers, the + gateway must return the `Access-Control-Allow-Headers` response header + which value is present in the `AllowHeaders` field. + + If any header name in the `Access-Control-Request-Headers` request header + is not included in the list of header names specified by the response + header `Access-Control-Allow-Headers`, it will present an error on the + client side. + + If any header name in the `Access-Control-Allow-Headers` response header + does not recognize by the client, it will also occur an error on the + client side. + + A wildcard indicates that the requests with all HTTP headers are allowed. + The `Access-Control-Allow-Headers` response header can only use `*` + wildcard as value when the `AllowCredentials` field is false or omitted. + + When the `AllowCredentials` field is true and `AllowHeaders` field + specified with the `*` wildcard, the gateway must specify one or more + HTTP headers in the value of the `Access-Control-Allow-Headers` response + header. The value of the header `Access-Control-Allow-Headers` is same as + the `Access-Control-Request-Headers` header provided by the client. If + the header `Access-Control-Request-Headers` is not included in the + request, the gateway will omit the `Access-Control-Allow-Headers` + response header, instead of specifying the `*` wildcard. A Gateway + implementation may choose to add implementation-specific default headers. + + Support: Extended + items: + description: |- + HTTPHeaderName is the name of an HTTP header. + + Valid values include: + + * "Authorization" + * "Set-Cookie" + + Invalid values include: + + - ":method" - ":" is an invalid character. This means that HTTP/2 pseudo + headers are not currently supported by this type. + - "/invalid" - "/ " is an invalid character + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + allowMethods: + description: |- + AllowMethods indicates which HTTP methods are supported for accessing the + requested resource. + + Valid values are any method defined by RFC9110, along with the special + value `*`, which represents all HTTP methods are allowed. + + Method names are case sensitive, so these values are also case-sensitive. + (See https://www.rfc-editor.org/rfc/rfc2616#section-5.1.1) + + Multiple method names in the value of the `Access-Control-Allow-Methods` + response header are separated by a comma (","). + + A CORS-safelisted method is a method that is `GET`, `HEAD`, or `POST`. + (See https://fetch.spec.whatwg.org/#cors-safelisted-method) The + CORS-safelisted methods are always allowed, regardless of whether they + are specified in the `AllowMethods` field. + + When the `AllowMethods` field is configured with one or more methods, the + gateway must return the `Access-Control-Allow-Methods` response header + which value is present in the `AllowMethods` field. + + If the HTTP method of the `Access-Control-Request-Method` request header + is not included in the list of methods specified by the response header + `Access-Control-Allow-Methods`, it will present an error on the client + side. + + The `Access-Control-Allow-Methods` response header can only use `*` + wildcard as value when the `AllowCredentials` field is false or omitted. + + When the `AllowCredentials` field is true and `AllowMethods` field + specified with the `*` wildcard, the gateway must specify one HTTP method + in the value of the Access-Control-Allow-Methods response header. The + value of the header `Access-Control-Allow-Methods` is same as the + `Access-Control-Request-Method` header provided by the client. If the + header `Access-Control-Request-Method` is not included in the request, + the gateway will omit the `Access-Control-Allow-Methods` response header, + instead of specifying the `*` wildcard. A Gateway implementation may + choose to add implementation-specific default methods. + + Support: Extended + items: + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + - '*' + type: string + maxItems: 9 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowMethods cannot contain '*' alongside + other methods + rule: '!(''*'' in self && self.size() > 1)' + allowOrigins: + description: |- + AllowOrigins indicates whether the response can be shared with requested + resource from the given `Origin`. + + The `Origin` consists of a scheme and a host, with an optional port, and + takes the form `://(:)`. + + Valid values for scheme are: `http` and `https`. + + Valid values for port are any integer between 1 and 65535 (the list of + available TCP/UDP ports). Note that, if not included, port `80` is + assumed for `http` scheme origins, and port `443` is assumed for `https` + origins. This may affect origin matching. + + The host part of the origin may contain the wildcard character `*`. These + wildcard characters behave as follows: + + * `*` is a greedy match to the _left_, including any number of + DNS labels to the left of its position. This also means that + `*` will include any number of period `.` characters to the + left of its position. + * A wildcard by itself matches all hosts. + + An origin value that includes _only_ the `*` character indicates requests + from all `Origin`s are allowed. + + When the `AllowOrigins` field is configured with multiple origins, it + means the server supports clients from multiple origins. If the request + `Origin` matches the configured allowed origins, the gateway must return + the given `Origin` and sets value of the header + `Access-Control-Allow-Origin` same as the `Origin` header provided by the + client. + + The status code of a successful response to a "preflight" request is + always an OK status (i.e., 204 or 200). + + If the request `Origin` does not match the configured allowed origins, + the gateway returns 204/200 response but doesn't set the relevant + cross-origin response headers. Alternatively, the gateway responds with + 403 status to the "preflight" request is denied, coupled with omitting + the CORS headers. The cross-origin request fails on the client side. + Therefore, the client doesn't attempt the actual cross-origin request. + + The `Access-Control-Allow-Origin` response header can only use `*` + wildcard as value when the `AllowCredentials` field is false or omitted. + + When the `AllowCredentials` field is true and `AllowOrigins` field + specified with the `*` wildcard, the gateway must return a single origin + in the value of the `Access-Control-Allow-Origin` response header, + instead of specifying the `*` wildcard. The value of the header + `Access-Control-Allow-Origin` is same as the `Origin` header provided by + the client. + + Support: Extended + items: + description: |- + The CORSOrigin MUST NOT be a relative URI, and it MUST follow the URI syntax and + encoding rules specified in RFC3986. The CORSOrigin MUST include both a + scheme (e.g., "http" or "spiffe") and a scheme-specific-part, or it should be a single '*' character. + URIs that include an authority MUST include a fully qualified domain name or + IP address as the host. + The below regex was generated to simplify the assertion of scheme://host: being port optional + maxLength: 253 + minLength: 1 + pattern: (^\*$)|(^([a-zA-Z][a-zA-Z0-9+\-.]+):\/\/([^:/?#]+)(:([0-9]{1,5}))?$) + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowOrigins cannot contain '*' alongside + other origins + rule: '!(''*'' in self && self.size() > 1)' + exposeHeaders: + description: |- + ExposeHeaders indicates which HTTP response headers can be exposed + to client-side scripts in response to a cross-origin request. + + A CORS-safelisted response header is an HTTP header in a CORS response + that it is considered safe to expose to the client scripts. + The CORS-safelisted response headers include the following headers: + `Cache-Control` + `Content-Language` + `Content-Length` + `Content-Type` + `Expires` + `Last-Modified` + `Pragma` + (See https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name) + The CORS-safelisted response headers are exposed to client by default. + + When an HTTP header name is specified using the `ExposeHeaders` field, + this additional header will be exposed as part of the response to the + client. + + Header names are not case sensitive. + + Multiple header names in the value of the `Access-Control-Expose-Headers` + response header are separated by a comma (","). + + A wildcard indicates that the responses with all HTTP headers are exposed + to clients. The `Access-Control-Expose-Headers` response header can only + use `*` wildcard as value when the `AllowCredentials` field is false or omitted. + + Support: Extended + items: + description: |- + HTTPHeaderName is the name of an HTTP header. + + Valid values include: + + * "Authorization" + * "Set-Cookie" + + Invalid values include: + + - ":method" - ":" is an invalid character. This means that HTTP/2 pseudo + headers are not currently supported by this type. + - "/invalid" - "/ " is an invalid character + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + maxAge: + default: 5 + description: |- + MaxAge indicates the duration (in seconds) for the client to cache the + results of a "preflight" request. + + The information provided by the `Access-Control-Allow-Methods` and + `Access-Control-Allow-Headers` response headers can be cached by the + client until the time specified by `Access-Control-Max-Age` elapses. + + The default value of `Access-Control-Max-Age` response header is 5 + (seconds). + format: int32 + minimum: 1 + type: integer + type: object + extensionRef: + description: |- + ExtensionRef is an optional, implementation-specific extension to the + "filter" behavior. For example, resource "myroutefilter" in group + "networking.example.net"). ExtensionRef MUST NOT be used for core and + extended filters. + + This filter can be used multiple times within the same rule. + + Support: Implementation-specific + properties: + group: + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For example + "HTTPRoute" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + externalAuth: + description: |- + ExternalAuth configures settings related to sending request details + to an external auth service. The external service MUST authenticate + the request, and MAY authorize the request as well. + + If there is any problem communicating with the external service, + this filter MUST fail closed. + + Support: Extended + + + properties: + backendRef: + description: |- + BackendRef is a reference to a backend to send authorization + requests to. + + The backend must speak the selected protocol (GRPC or HTTP) on the + referenced port. + + If the backend service requires TLS, use BackendTLSPolicy to tell the + implementation to supply the TLS details to be used to connect to that + backend. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + forwardBody: + description: |- + ForwardBody controls if requests to the authorization server should include + the body of the client request; and if so, how big that body is allowed + to be. + + It is expected that implementations will buffer the request body up to + `forwardBody.maxSize` bytes. Bodies over that size must be rejected with a + 4xx series error (413 or 403 are common examples), and fail processing + of the filter. + + If unset, or `forwardBody.maxSize` is set to `0`, then the body will not + be forwarded. + + Feature Name: HTTPRouteExternalAuthForwardBody + properties: + maxSize: + description: |- + MaxSize specifies how large in bytes the largest body that will be buffered + and sent to the authorization server. If the body size is larger than + `maxSize`, then the body sent to the authorization server must be + truncated to `maxSize` bytes. + + Experimental note: This behavior needs to be checked against + various dataplanes; it may need to be changed. + See https://github.com/kubernetes-sigs/gateway-api/pull/4001#discussion_r2291405746 + for more. + + If 0, the body will not be sent to the authorization server. + type: integer + type: object + grpc: + description: |- + GRPCAuthConfig contains configuration for communication with ext_authz + protocol-speaking backends. + + If unset, implementations must assume the default behavior for each + included field is intended. + properties: + allowedHeaders: + description: |- + AllowedRequestHeaders specifies what headers from the client request + will be sent to the authorization server. + + If this list is empty, then all headers must be sent. + + If the list has entries, only those entries must be sent. + items: + type: string + type: array + x-kubernetes-list-type: set + type: object + http: + description: |- + HTTPAuthConfig contains configuration for communication with HTTP-speaking + backends. + + If unset, implementations must assume the default behavior for each + included field is intended. + properties: + allowedHeaders: + description: |- + AllowedRequestHeaders specifies what additional headers from the client request + will be sent to the authorization server. + + The following headers must always be sent to the authorization server, + regardless of this setting: + + * `Host` + * `Method` + * `Path` + * `Content-Length` + * `Authorization` + + If this list is empty, then only those headers must be sent. + + Note that `Content-Length` has a special behavior, in that the length + sent must be correct for the actual request to the external authorization + server - that is, it must reflect the actual number of bytes sent in the + body of the request to the authorization server. + + So if the `forwardBody` stanza is unset, or `forwardBody.maxSize` is set + to `0`, then `Content-Length` must be `0`. If `forwardBody.maxSize` is set + to anything other than `0`, then the `Content-Length` of the authorization + request must be set to the actual number of bytes forwarded. + items: + type: string + type: array + x-kubernetes-list-type: set + allowedResponseHeaders: + description: |- + AllowedResponseHeaders specifies what headers from the authorization response + will be copied into the request to the backend. + + If this list is empty, then all headers from the authorization server + except Authority or Host must be copied. + items: + type: string + type: array + x-kubernetes-list-type: set + path: + description: |- + Path sets the prefix that paths from the client request will have added + when forwarded to the authorization server. + + When empty or unspecified, no prefix is added. + + Valid values are the same as the "value" regex for path values in the `match` + stanza, and the validation regex will screen out invalid paths in the same way. + Even with the validation, implementations MUST sanitize this input before using it + directly. + maxLength: 1024 + pattern: ^(?:[-A-Za-z0-9/._~!$&'()*+,;=:@]|[%][0-9a-fA-F]{2})+$ + type: string + type: object + protocol: + description: |- + ExternalAuthProtocol describes which protocol to use when communicating with an + ext_authz authorization server. + + When this is set to GRPC, each backend must use the Envoy ext_authz protocol + on the port specified in `backendRefs`. Requests and responses are defined + in the protobufs explained at: + https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto + + When this is set to HTTP, each backend must respond with a `200` status + code in on a successful authorization. Any other code is considered + an authorization failure. + + Feature Names: + GRPC Support - HTTPRouteExternalAuthGRPC + HTTP Support - HTTPRouteExternalAuthHTTP + enum: + - HTTP + - GRPC + type: string + required: + - backendRef + - protocol + type: object + x-kubernetes-validations: + - message: grpc must be specified when protocol is set to + 'GRPC' + rule: 'self.protocol == ''GRPC'' ? has(self.grpc) : true' + - message: protocol must be 'GRPC' when grpc is set + rule: 'has(self.grpc) ? self.protocol == ''GRPC'' : true' + - message: http must be specified when protocol is set to + 'HTTP' + rule: 'self.protocol == ''HTTP'' ? has(self.http) : true' + - message: protocol must be 'HTTP' when http is set + rule: 'has(self.http) ? self.protocol == ''HTTP'' : true' + requestHeaderModifier: + description: |- + RequestHeaderModifier defines a schema for a filter that modifies request + headers. + + Support: Core + properties: + add: + description: |- + Add adds the given header(s) (name, value) to the request + before the action. It appends to any existing values associated + with the header name. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + add: + - name: "my-header" + value: "bar,baz" + + Output: + GET /foo HTTP/1.1 + my-header: foo,bar,baz + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: |- + Remove the given header(s) from the HTTP request before the action. The + value of Remove is a list of HTTP header names. Note that the header + names are case-insensitive (see + https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + + Input: + GET /foo HTTP/1.1 + my-header1: foo + my-header2: bar + my-header3: baz + + Config: + remove: ["my-header1", "my-header3"] + + Output: + GET /foo HTTP/1.1 + my-header2: bar + items: + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: set + set: + description: |- + Set overwrites the request with the given header (name, value) + before the action. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + set: + - name: "my-header" + value: "bar" + + Output: + GET /foo HTTP/1.1 + my-header: bar + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestMirror: + description: |- + RequestMirror defines a schema for a filter that mirrors requests. + Requests are sent to the specified destination, but responses from + that destination are ignored. + + This filter can be used multiple times within the same rule. Note that + not all implementations will be able to support mirroring to multiple + backends. + + Support: Extended + properties: + backendRef: + description: |- + BackendRef references a resource where mirrored requests are sent. + + Mirrored requests must be sent only to a single destination endpoint + within this BackendRef, irrespective of how many endpoints are present + within this BackendRef. + + If the referent cannot be found, this BackendRef is invalid and must be + dropped from the Gateway. The controller must ensure the "ResolvedRefs" + condition on the Route status is set to `status: False` and not configure + this backend in the underlying implementation. + + If there is a cross-namespace reference to an *existing* object + that is not allowed by a ReferenceGrant, the controller must ensure the + "ResolvedRefs" condition on the Route is set to `status: False`, + with the "RefNotPermitted" reason and not configure this backend in the + underlying implementation. + + In either error case, the Message of the `ResolvedRefs` Condition + should be used to provide more detail about the problem. + + Support: Extended for Kubernetes Service + + Support: Implementation-specific for any other resource + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + fraction: + description: |- + Fraction represents the fraction of requests that should be + mirrored to BackendRef. + + Only one of Fraction or Percent may be specified. If neither field + is specified, 100% of requests will be mirrored. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal to denominator + rule: self.numerator <= self.denominator + percent: + description: |- + Percent represents the percentage of requests that should be + mirrored to BackendRef. Its minimum value is 0 (indicating 0% of + requests) and its maximum value is 100 (indicating 100% of requests). + + Only one of Fraction or Percent may be specified. If neither field + is specified, 100% of requests will be mirrored. + format: int32 + maximum: 100 + minimum: 0 + type: integer + required: + - backendRef + type: object + x-kubernetes-validations: + - message: Only one of percent or fraction may be specified + in HTTPRequestMirrorFilter + rule: '!(has(self.percent) && has(self.fraction))' + requestRedirect: + description: |- + RequestRedirect defines a schema for a filter that responds to the + request with an HTTP redirection. + + Support: Core + properties: + hostname: + description: |- + Hostname is the hostname to be used in the value of the `Location` + header in the response. + When empty, the hostname in the `Host` header of the request is used. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: |- + Path defines parameters used to modify the path of the incoming request. + The modified path is then used to construct the `Location` header. When + empty, the request path is used as-is. + + Support: Extended + properties: + replaceFullPath: + description: |- + ReplaceFullPath specifies the value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: |- + ReplacePrefixMatch specifies the value with which to replace the prefix + match of a request during a rewrite or redirect. For example, a request + to "/foo/bar" with a prefix match of "/foo" and a ReplacePrefixMatch + of "/xyz" would be modified to "/xyz/bar". + + Note that this matches the behavior of the PathPrefix match type. This + matches full path elements. A path element refers to the list of labels + in the path split by the `/` separator. When specified, a trailing `/` is + ignored. For example, the paths `/abc`, `/abc/`, and `/abc/def` would all + match the prefix `/abc`, but the path `/abcd` would not. + + ReplacePrefixMatch is only compatible with a `PathPrefix` HTTPRouteMatch. + Using any other HTTPRouteMatch type on the same HTTPRouteRule will result in + the implementation setting the Accepted Condition for the Route to `status: False`. + + Request Path | Prefix Match | Replace Prefix | Modified Path + maxLength: 1024 + type: string + type: + description: |- + Type defines the type of path modifier. Additional types may be + added in a future release of the API. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: replaceFullPath must be specified when type + is set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? has(self.replaceFullPath) + : true' + - message: type must be 'ReplaceFullPath' when replaceFullPath + is set + rule: 'has(self.replaceFullPath) ? self.type == ''ReplaceFullPath'' + : true' + - message: replacePrefixMatch must be specified when + type is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' ? has(self.replacePrefixMatch) + : true' + - message: type must be 'ReplacePrefixMatch' when replacePrefixMatch + is set + rule: 'has(self.replacePrefixMatch) ? self.type == + ''ReplacePrefixMatch'' : true' + port: + description: |- + Port is the port to be used in the value of the `Location` + header in the response. + + If no port is specified, the redirect port MUST be derived using the + following rules: + + * If redirect scheme is not-empty, the redirect port MUST be the well-known + port associated with the redirect scheme. Specifically "http" to port 80 + and "https" to port 443. If the redirect scheme does not have a + well-known port, the listener port of the Gateway SHOULD be used. + * If redirect scheme is empty, the redirect port MUST be the Gateway + Listener port. + + Implementations SHOULD NOT add the port number in the 'Location' + header in the following cases: + + * A Location header that will use HTTP (whether that is determined via + the Listener protocol or the Scheme field) _and_ use port 80. + * A Location header that will use HTTPS (whether that is determined via + the Listener protocol or the Scheme field) _and_ use port 443. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: |- + Scheme is the scheme to be used in the value of the `Location` header in + the response. When empty, the scheme of the request is used. + + Scheme redirects can affect the port of the redirect, for more information, + refer to the documentation for the port field of this filter. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + + Support: Extended + enum: + - http + - https + type: string + statusCode: + default: 302 + description: |- + StatusCode is the HTTP status code to be used in response. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + + Support: Core + enum: + - 301 + - 302 + type: integer + type: object + responseHeaderModifier: + description: |- + ResponseHeaderModifier defines a schema for a filter that modifies response + headers. + + Support: Extended + properties: + add: + description: |- + Add adds the given header(s) (name, value) to the request + before the action. It appends to any existing values associated + with the header name. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + add: + - name: "my-header" + value: "bar,baz" + + Output: + GET /foo HTTP/1.1 + my-header: foo,bar,baz + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: |- + Remove the given header(s) from the HTTP request before the action. The + value of Remove is a list of HTTP header names. Note that the header + names are case-insensitive (see + https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + + Input: + GET /foo HTTP/1.1 + my-header1: foo + my-header2: bar + my-header3: baz + + Config: + remove: ["my-header1", "my-header3"] + + Output: + GET /foo HTTP/1.1 + my-header2: bar + items: + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: set + set: + description: |- + Set overwrites the request with the given header (name, value) + before the action. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + set: + - name: "my-header" + value: "bar" + + Output: + GET /foo HTTP/1.1 + my-header: bar + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: + description: |- + Type identifies the type of filter to apply. As with other API fields, + types are classified into three conformance levels: + + - Core: Filter types and their corresponding configuration defined by + "Support: Core" in this package, e.g. "RequestHeaderModifier". All + implementations must support core filters. + + - Extended: Filter types and their corresponding configuration defined by + "Support: Extended" in this package, e.g. "RequestMirror". Implementers + are encouraged to support extended filters. + + - Implementation-specific: Filters that are defined and supported by + specific vendors. + In the future, filters showing convergence in behavior across multiple + implementations will be considered for inclusion in extended or core + conformance levels. Filter-specific configuration for such filters + is specified using the ExtensionRef field. `Type` should be set to + "ExtensionRef" for custom filters. + + Implementers are encouraged to define custom implementation types to + extend the core API with implementation-specific behavior. + + If a reference to a custom filter type cannot be resolved, the filter + MUST NOT be skipped. Instead, requests that would have been processed by + that filter MUST receive a HTTP error response. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + + + enum: + - RequestHeaderModifier + - ResponseHeaderModifier + - RequestMirror + - RequestRedirect + - URLRewrite + - ExtensionRef + type: string + urlRewrite: + description: |- + URLRewrite defines a schema for a filter that modifies a request during forwarding. + + Support: Extended + properties: + hostname: + description: |- + Hostname is the value to be used to replace the Host header value during + forwarding. + + Support: Extended + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: |- + Path defines a path rewrite. + + Support: Extended + properties: + replaceFullPath: + description: |- + ReplaceFullPath specifies the value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: |- + ReplacePrefixMatch specifies the value with which to replace the prefix + match of a request during a rewrite or redirect. For example, a request + to "/foo/bar" with a prefix match of "/foo" and a ReplacePrefixMatch + of "/xyz" would be modified to "/xyz/bar". + + Note that this matches the behavior of the PathPrefix match type. This + matches full path elements. A path element refers to the list of labels + in the path split by the `/` separator. When specified, a trailing `/` is + ignored. For example, the paths `/abc`, `/abc/`, and `/abc/def` would all + match the prefix `/abc`, but the path `/abcd` would not. + + ReplacePrefixMatch is only compatible with a `PathPrefix` HTTPRouteMatch. + Using any other HTTPRouteMatch type on the same HTTPRouteRule will result in + the implementation setting the Accepted Condition for the Route to `status: False`. + + Request Path | Prefix Match | Replace Prefix | Modified Path + maxLength: 1024 + type: string + type: + description: |- + Type defines the type of path modifier. Additional types may be + added in a future release of the API. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: replaceFullPath must be specified when type + is set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? has(self.replaceFullPath) + : true' + - message: type must be 'ReplaceFullPath' when replaceFullPath + is set + rule: 'has(self.replaceFullPath) ? self.type == ''ReplaceFullPath'' + : true' + - message: replacePrefixMatch must be specified when + type is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' ? has(self.replacePrefixMatch) + : true' + - message: type must be 'ReplacePrefixMatch' when replacePrefixMatch + is set + rule: 'has(self.replacePrefixMatch) ? self.type == + ''ReplacePrefixMatch'' : true' + type: object + required: + - type + type: object + x-kubernetes-validations: + - message: filter.requestHeaderModifier must be nil if the filter.type + is not RequestHeaderModifier + rule: '!(has(self.requestHeaderModifier) && self.type != ''RequestHeaderModifier'')' + - message: filter.requestHeaderModifier must be specified for + RequestHeaderModifier filter.type + rule: '!(!has(self.requestHeaderModifier) && self.type == + ''RequestHeaderModifier'')' + - message: filter.responseHeaderModifier must be nil if the + filter.type is not ResponseHeaderModifier + rule: '!(has(self.responseHeaderModifier) && self.type != + ''ResponseHeaderModifier'')' + - message: filter.responseHeaderModifier must be specified for + ResponseHeaderModifier filter.type + rule: '!(!has(self.responseHeaderModifier) && self.type == + ''ResponseHeaderModifier'')' + - message: filter.requestMirror must be nil if the filter.type + is not RequestMirror + rule: '!(has(self.requestMirror) && self.type != ''RequestMirror'')' + - message: filter.requestMirror must be specified for RequestMirror + filter.type + rule: '!(!has(self.requestMirror) && self.type == ''RequestMirror'')' + - message: filter.requestRedirect must be nil if the filter.type + is not RequestRedirect + rule: '!(has(self.requestRedirect) && self.type != ''RequestRedirect'')' + - message: filter.requestRedirect must be specified for RequestRedirect + filter.type + rule: '!(!has(self.requestRedirect) && self.type == ''RequestRedirect'')' + - message: filter.urlRewrite must be nil if the filter.type + is not URLRewrite + rule: '!(has(self.urlRewrite) && self.type != ''URLRewrite'')' + - message: filter.urlRewrite must be specified for URLRewrite + filter.type + rule: '!(!has(self.urlRewrite) && self.type == ''URLRewrite'')' + - message: filter.extensionRef must be nil if the filter.type + is not ExtensionRef + rule: '!(has(self.extensionRef) && self.type != ''ExtensionRef'')' + - message: filter.extensionRef must be specified for ExtensionRef + filter.type + rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')' + type: array publicRoutes: description: |- PublicRoutes specifies paths that should bypass OIDC authentication. @@ -450,6 +1782,15 @@ spec: Each entry uses the same RouteMatch format as routes, supporting both PathPrefix (default) and Exact matching via the pathType field. Example: [{pathPrefix: "/api/v1/health", pathType: "Exact"}] + + PublicRoutes is for carving out a *subset* of paths that should bypass + auth while the rest stays protected. It is not a way to disable auth for + the whole app: if a public path also matches the main route (including the + default "/" match when routes is empty) while auth.enforceAtGateway is + true, both a protected and an unprotected HTTPRoute claim that path and + the effective auth posture is undefined. To turn gateway auth off entirely, + set auth.enforceAtGateway: false (keep provisioning the Keycloak client) or + auth.enabled: false (no client at all) instead of listing every path here. items: description: RouteMatch defines a path-based routing rule. properties: diff --git a/docs/api-reference.md b/docs/api-reference.md index c8ca244..07b49bf 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -41,7 +41,7 @@ _Appears in:_ | `scopes` _string array_ | Scopes defines the OIDC scopes to request during authentication.
Common scopes: openid, profile, email, roles, groups
If not specified, defaults to: ["openid", "profile", "email"] | | Optional: \{\}
| | `groups` _string array_ | Groups specifies the list of groups that should have access to this application.
When specified, only users belonging to these groups will be authorized.
Group matching is case-sensitive and depends on the OIDC provider's group claim. | | Optional: \{\}
| | `provisionClient` _boolean_ | ProvisionClient determines whether the operator should automatically provision
an OIDC client in the provider. When true, the operator will create a client
(e.g., in Keycloak) and store the credentials in a Secret.
Only supported for provider="keycloak".
Defaults to true if not specified. | true | Optional: \{\}
| -| `enforceAtGateway` _boolean_ | EnforceAtGateway determines whether the operator should create an Envoy Gateway
SecurityPolicy to enforce authentication at the gateway level.
When true (default), the operator creates a SecurityPolicy that handles
the OIDC flow at the gateway before requests reach the application.
When false, the operator provisions the OIDC client and stores credentials
in a Secret, but does NOT create a SecurityPolicy - the application is
expected to handle OAuth natively (e.g., Grafana's built-in generic_oauth). | true | Optional: \{\}
| +| `enforceAtGateway` _boolean_ | EnforceAtGateway determines whether the operator should create an Envoy Gateway
SecurityPolicy to enforce authentication at the gateway level.
When true (default), the operator creates a SecurityPolicy that handles
the OIDC flow at the gateway before requests reach the application.
When false, the operator provisions the OIDC client and stores credentials
in a Secret, but does NOT create a SecurityPolicy - the application is
expected to handle OAuth natively (e.g., Grafana's built-in generic_oauth).
This is the correct switch for "no gateway-level auth"; do not try to
achieve that by listing every path under routing.publicRoutes, which
instead produces overlapping protected and unprotected routes for the same
path (see PublicRoutes). | true | Optional: \{\}
| | `forwardAccessToken` _boolean_ | ForwardAccessToken instructs the gateway-enforced OIDC filter to forward
the user's OAuth2 access token to the upstream service via the
`Authorization: Bearer ` header. Use this when the application
needs to read the JWT itself - for example to extract the user's groups
claim and apply per-user authorization decisions on top of the gateway's
authentication. By default the gateway only stores the token in an
encrypted session cookie that backends cannot decode.
Only applies when enforceAtGateway is true. | | Optional: \{\}
| | `denyRedirect` _[DenyRedirectHeader](#denyredirectheader) array_ | DenyRedirect configures headers that, when matched, prevent the OIDC filter
from redirecting to the identity provider. Instead, matching requests receive
a 401 response. This prevents PKCE race conditions when SPAs fire multiple
requests on page load (e.g., the main page and AJAX calls simultaneously),
each of which would otherwise start a separate OAuth flow and overwrite
each other's state cookies.
Only applies when enforceAtGateway is true. | | Optional: \{\}
| | `issuerURL` _string_ | IssuerURL specifies the OIDC issuer URL for generic-oidc provider.
Required when provider="generic-oidc", ignored for other providers.
Example: https://accounts.google.com, https://login.microsoftonline.com//v2.0 | | Optional: \{\}
| @@ -309,9 +309,10 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | | `routes` _[RouteMatch](#routematch) array_ | Routes defines path-based routing rules for the application.
If not specified, all traffic to the hostname will be routed to the service.
When specified, only traffic matching these path prefixes will be routed.
Example: ["/app-1", "/api/v1"] | | Optional: \{\}
| -| `publicRoutes` _[RouteMatch](#routematch) array_ | PublicRoutes specifies paths that should bypass OIDC authentication.
When auth is enabled and these are specified, these paths will be routed
via a separate HTTPRoute that is not protected by the SecurityPolicy.
Each entry uses the same RouteMatch format as routes, supporting both
PathPrefix (default) and Exact matching via the pathType field.
Example: [\{pathPrefix: "/api/v1/health", pathType: "Exact"\}] | | Optional: \{\}
| +| `publicRoutes` _[RouteMatch](#routematch) array_ | PublicRoutes specifies paths that should bypass OIDC authentication.
When auth is enabled and these are specified, these paths will be routed
via a separate HTTPRoute that is not protected by the SecurityPolicy.
Each entry uses the same RouteMatch format as routes, supporting both
PathPrefix (default) and Exact matching via the pathType field.
Example: [\{pathPrefix: "/api/v1/health", pathType: "Exact"\}]
PublicRoutes is for carving out a *subset* of paths that should bypass
auth while the rest stays protected. It is not a way to disable auth for
the whole app: if a public path also matches the main route (including the
default "/" match when routes is empty) while auth.enforceAtGateway is
true, both a protected and an unprotected HTTPRoute claim that path and
the effective auth posture is undefined. To turn gateway auth off entirely,
set auth.enforceAtGateway: false (keep provisioning the Keycloak client) or
auth.enabled: false (no client at all) instead of listing every path here. | | Optional: \{\}
| | `tls` _[RoutingTLSConfig](#routingtlsconfig)_ | TLS configures TLS certificate management and termination behavior.
When TLS is enabled (the default), the operator creates a cert-manager Certificate
for the application's hostname and adds a per-app HTTPS listener to the shared Gateway. | | Optional: \{\}
| | `annotations` _object (keys:string, values:string)_ | Annotations defines additional annotations to merge onto the generated HTTPRoute.
Useful for tools like ArgoCD that track resources via annotations
(e.g. argocd.argoproj.io/tracking-id).
These annotations are merged with any operator-managed annotations; operator
annotations always take precedence to avoid breaking internal behaviour. | | Optional: \{\}
| +| `filters` _HTTPRouteFilter array_ | Filters are appended, in order, to every HTTPRouteRule the operator
generates for this NebariApp (both the protected route and the public
route built from publicRoutes). This is a pass-through to the upstream
gateway.networking.k8s.io/v1 HTTPRouteFilter spec, mirroring the
Annotations escape hatch: the operator does not interpret, reorder, or
strip the filters, it only forwards them. Use it to adjust request or
response headers at the gateway (for example, stripping an overpermissive
CORS header an upstream sets, or adding security headers) without forking
the operator or rebuilding the backend image. | | Optional: \{\}
| --- diff --git a/docs/reconcilers/routing.md b/docs/reconcilers/routing.md index 5e83b0a..0c1bc35 100644 --- a/docs/reconcilers/routing.md +++ b/docs/reconcilers/routing.md @@ -330,12 +330,50 @@ spec: ``` **Key details:** -- Public routes always use `PathPrefix` matching +- Public routes default to `Exact` matching (safer for auth bypass); set `pathType: PathPrefix` per entry to widen - The public HTTPRoute has the label `nebari.dev/route-type: public` - Both routes point to the same backend service - If auth is disabled, no public route is created (all routes are already public) - Cleanup removes both HTTPRoutes when the NebariApp is deleted +> **`publicRoutes` is for a subset of paths, not "disable auth".** It carves +> specific paths out of the authenticated app. If a public path also matches the +> main route (including the default `/` match when `routes` is empty) while +> `auth.enforceAtGateway` is `true`, both a protected and an unprotected HTTPRoute +> claim that path and the effective auth posture is left to Gateway API conflict +> resolution. To turn gateway auth off for the whole app, set +> `auth.enforceAtGateway: false` (keep provisioning the Keycloak client) or +> `auth.enabled: false` (no client at all) — do not list every path under +> `publicRoutes`. + +### Route Filters + +`spec.routing.filters` is a pass-through to the upstream Gateway API +[`HTTPRouteFilter`](https://gateway-api.sigs.k8s.io/api-types/httproute/#filters) +list. The operator appends the filters you supply, in order, to the rule of +**both** the protected and the public HTTPRoute; it does not interpret, reorder, +or strip them. This mirrors the `annotations` escape hatch and is the supported +way to adjust request/response headers at the gateway without forking the +operator or rebuilding the backend image — for example, stripping an +overpermissive CORS header an upstream sets, or adding security headers. + +```yaml +spec: + routing: + filters: + - type: ResponseHeaderModifier + responseHeaderModifier: + remove: + - Access-Control-Allow-Credentials + set: + - name: Strict-Transport-Security + value: "max-age=63072000; includeSubDomains" +``` + +Because the operator owns the generated HTTPRoute (`ownerReferences.controller: +true`), a manual `kubectl patch` of these fields is reverted on the next +reconcile — `routing.filters` is the durable way to express them. + ### Backend References The operator creates backend references using the service details from the NebariApp spec: diff --git a/internal/controller/reconcilers/auth/providers/keycloak.go b/internal/controller/reconcilers/auth/providers/keycloak.go index 71fba4e..140ef02 100644 --- a/internal/controller/reconcilers/auth/providers/keycloak.go +++ b/internal/controller/reconcilers/auth/providers/keycloak.go @@ -614,7 +614,7 @@ func (p *KeycloakProvider) updateExistingClient(ctx context.Context, kcClient *g // Update client configuration redirectURIs := p.buildRedirectURLs(nebariApp) existingClient.RedirectURIs = &redirectURIs - existingClient.WebOrigins = &[]string{"*"} + existingClient.WebOrigins = ptr.To(p.buildWebOrigins(nebariApp)) existingClient.StandardFlowEnabled = gocloak.BoolP(true) // Ensure post-logout redirect URIs are set (preserving any existing attributes) @@ -653,7 +653,7 @@ func (p *KeycloakProvider) createNewClient(ctx context.Context, kcClient *gocloa Name: gocloak.StringP(fmt.Sprintf("%s OIDC Client", nebariApp.Name)), Secret: gocloak.StringP(clientSecret), RedirectURIs: &redirectURIs, - WebOrigins: &[]string{"*"}, + WebOrigins: ptr.To(p.buildWebOrigins(nebariApp)), Attributes: &map[string]string{"post.logout.redirect.uris": p.buildPostLogoutRedirectURIs(nebariApp)}, PublicClient: gocloak.BoolP(false), StandardFlowEnabled: gocloak.BoolP(true), @@ -683,6 +683,18 @@ func (p *KeycloakProvider) buildRedirectURLs(nebariApp *appsv1.NebariApp) []stri } } +// buildWebOrigins constructs the CORS WebOrigins for the client, scoped to the +// NebariApp's hostname (both schemes) rather than a "*" wildcard. This mirrors +// the host-scoping already applied to RedirectURIs and keeps the client to the +// principle of least privilege: only the app's own origin may make CORS requests +// to Keycloak's token endpoint for this client. +func (p *KeycloakProvider) buildWebOrigins(nebariApp *appsv1.NebariApp) []string { + return []string{ + fmt.Sprintf("https://%s", nebariApp.Spec.Hostname), + fmt.Sprintf("http://%s", nebariApp.Spec.Hostname), + } +} + // buildPostLogoutRedirectURIs constructs the Keycloak post.logout.redirect.uris attribute value. // Keycloak stores multiple URIs as "##"-delimited strings in this client attribute. // Envoy Gateway sends the app's base URL as post_logout_redirect_uri when hitting /logout. @@ -1107,7 +1119,7 @@ func (p *KeycloakProvider) provisionSPAClient(ctx context.Context, kcClient *goc if existingSPAClient != nil { // Update existing SPA client existingSPAClient.RedirectURIs = &redirectURIs - existingSPAClient.WebOrigins = &[]string{"*"} + existingSPAClient.WebOrigins = ptr.To(p.buildWebOrigins(nebariApp)) existingSPAClient.PublicClient = gocloak.BoolP(true) existingSPAClient.StandardFlowEnabled = gocloak.BoolP(true) @@ -1128,7 +1140,7 @@ func (p *KeycloakProvider) provisionSPAClient(ctx context.Context, kcClient *goc ClientID: gocloak.StringP(spaClientID), Name: gocloak.StringP(fmt.Sprintf("%s SPA Client", nebariApp.Name)), RedirectURIs: &redirectURIs, - WebOrigins: &[]string{"*"}, + WebOrigins: ptr.To(p.buildWebOrigins(nebariApp)), PublicClient: gocloak.BoolP(true), StandardFlowEnabled: gocloak.BoolP(true), DirectAccessGrantsEnabled: gocloak.BoolP(false), diff --git a/internal/controller/reconcilers/auth/providers/keycloak_test.go b/internal/controller/reconcilers/auth/providers/keycloak_test.go index 51f6bdd..625d3e7 100644 --- a/internal/controller/reconcilers/auth/providers/keycloak_test.go +++ b/internal/controller/reconcilers/auth/providers/keycloak_test.go @@ -344,6 +344,36 @@ func TestKeycloakProvider_BuildRedirectURLs(t *testing.T) { } } +// TestKeycloakProvider_BuildWebOrigins asserts that the client's CORS WebOrigins +// are scoped to the NebariApp hostname (both schemes) rather than a "*" wildcard +// (see #37). +func TestKeycloakProvider_BuildWebOrigins(t *testing.T) { + provider := &KeycloakProvider{Config: config.KeycloakConfig{}} + + nebariApp := &appsv1.NebariApp{ + ObjectMeta: metav1.ObjectMeta{Name: "test-app", Namespace: "default"}, + Spec: appsv1.NebariAppSpec{ + Hostname: "test.example.com", + Auth: &appsv1.AuthConfig{Enabled: true}, + }, + } + + got := provider.buildWebOrigins(nebariApp) + want := []string{"https://test.example.com", "http://test.example.com"} + + if len(got) != len(want) { + t.Fatalf("expected %d origins, got %d: %v", len(want), len(got), got) + } + for i := range want { + if got[i] != want[i] { + t.Errorf("origin[%d]: expected %s, got %s", i, want[i], got[i]) + } + if got[i] == "*" { + t.Errorf("origin[%d] is the wildcard '*'; WebOrigins must be host-scoped", i) + } + } +} + func TestKeycloakProvider_StoreClientSecret(t *testing.T) { scheme := runtime.NewScheme() _ = corev1.AddToScheme(scheme) diff --git a/internal/controller/reconcilers/routing/httproute.go b/internal/controller/reconcilers/routing/httproute.go index 14bd12e..9e1d6a7 100644 --- a/internal/controller/reconcilers/routing/httproute.go +++ b/internal/controller/reconcilers/routing/httproute.go @@ -163,17 +163,8 @@ func (r *RoutingReconciler) buildHTTPRoute(nebariApp *appsv1.NebariApp, gatewayN routeName := naming.HTTPRouteName(nebariApp) namespace := gatewayv1.Namespace(constants.GatewayNamespace) - // Determine which Gateway listener to use - // Priority: tlsListenerName (from TLS reconciler) > TLS enabled ("https") > TLS disabled ("http") - sectionName := gatewayv1.SectionName("https") - tlsEnabled := true - if nebariApp.Spec.Routing != nil && nebariApp.Spec.Routing.TLS != nil && nebariApp.Spec.Routing.TLS.Enabled != nil && !*nebariApp.Spec.Routing.TLS.Enabled { - sectionName = gatewayv1.SectionName("http") - tlsEnabled = false - } - if tlsListenerName != "" && tlsEnabled { - sectionName = gatewayv1.SectionName(tlsListenerName) - } + // Determine which Gateway listener to use. + sectionName, tlsEnabled := resolveListenerSection(nebariApp, tlsListenerName) // Build HTTPRoute annotations: start with user-supplied annotations from the // routing spec, then apply operator-managed ones so they always take precedence. @@ -221,41 +212,82 @@ func (r *RoutingReconciler) buildHTTPRoute(nebariApp *appsv1.NebariApp, gatewayN return route, nil } -// buildHTTPRouteRules generates HTTPRoute rules based on NebariApp routes +// buildHTTPRouteRules generates the HTTPRoute rule for the protected (main) +// route. Matches come from routing.routes and default to PathPrefix; when none +// are specified the matches slice is empty and Gateway API applies its own +// default "/" (PathPrefix) match. func (r *RoutingReconciler) buildHTTPRouteRules(nebariApp *appsv1.NebariApp) []gatewayv1.HTTPRouteRule { - // Get routes from routing config if specified var routes []appsv1.RouteMatch if nebariApp.Spec.Routing != nil { routes = nebariApp.Spec.Routing.Routes } + return []gatewayv1.HTTPRouteRule{ + r.buildRouteRule(nebariApp, buildRouteMatches(routes, gatewayv1.PathMatchPathPrefix)), + } +} + +// buildRouteRule assembles a single HTTPRouteRule from the given matches, +// attaching the shared backend refs and any user-supplied routing.filters. +// Both the protected route and the public route flow through here, so filters +// apply to each consistently and in the order the user listed them. +func (r *RoutingReconciler) buildRouteRule(nebariApp *appsv1.NebariApp, matches []gatewayv1.HTTPRouteMatch) gatewayv1.HTTPRouteRule { + return gatewayv1.HTTPRouteRule{ + Matches: matches, + BackendRefs: r.buildBackendRefs(nebariApp), + Filters: userFilters(nebariApp), + } +} - // Build a single rule with multiple matches (one per route) - // All matches route to the same backend, so we use one rule - // If no routes specified, we create an empty matches array. Gateway API will automatically - // add a default path match of "/" (PathPrefix) when matches is empty or null. +// buildRouteMatches converts NebariApp RouteMatch entries into Gateway API path +// matches. defaultPathType is applied when an entry omits pathType, letting the +// main route default to PathPrefix and public routes to Exact. An empty input +// yields an empty slice so callers can rely on Gateway API's default "/" match. +func buildRouteMatches(routes []appsv1.RouteMatch, defaultPathType gatewayv1.PathMatchType) []gatewayv1.HTTPRouteMatch { matches := make([]gatewayv1.HTTPRouteMatch, 0, len(routes)) for _, route := range routes { - pathType := gatewayv1.PathMatchPathPrefix - if route.PathType == "Exact" { + pathType := defaultPathType + switch route.PathType { + case "Exact": pathType = gatewayv1.PathMatchExact + case "PathPrefix": + pathType = gatewayv1.PathMatchPathPrefix } - pathValue := route.PathPrefix - match := gatewayv1.HTTPRouteMatch{ + matches = append(matches, gatewayv1.HTTPRouteMatch{ Path: &gatewayv1.HTTPPathMatch{ Type: &pathType, Value: &pathValue, }, - } - matches = append(matches, match) + }) } + return matches +} - return []gatewayv1.HTTPRouteRule{ - { - Matches: matches, - BackendRefs: r.buildBackendRefs(nebariApp), - }, +// userFilters returns the user-supplied routing.filters for this NebariApp, or +// nil when none are configured. Pass-through to the Gateway API HTTPRouteFilter +// spec: the operator does not interpret, reorder, or strip them. +func userFilters(nebariApp *appsv1.NebariApp) []gatewayv1.HTTPRouteFilter { + if nebariApp.Spec.Routing == nil || len(nebariApp.Spec.Routing.Filters) == 0 { + return nil } + return nebariApp.Spec.Routing.Filters +} + +// resolveListenerSection determines which Gateway listener section the route +// attaches to, and whether TLS is enabled. Priority: the per-app TLS listener +// from the TLS reconciler (when TLS is on) > "https" (TLS on) > "http" (TLS +// explicitly disabled). +func resolveListenerSection(nebariApp *appsv1.NebariApp, tlsListenerName string) (gatewayv1.SectionName, bool) { + sectionName := gatewayv1.SectionName("https") + tlsEnabled := true + if nebariApp.Spec.Routing != nil && nebariApp.Spec.Routing.TLS != nil && nebariApp.Spec.Routing.TLS.Enabled != nil && !*nebariApp.Spec.Routing.TLS.Enabled { + sectionName = gatewayv1.SectionName("http") + tlsEnabled = false + } + if tlsListenerName != "" && tlsEnabled { + sectionName = gatewayv1.SectionName(tlsListenerName) + } + return sectionName, tlsEnabled } // buildBackendRefs generates backend references for the HTTPRoute @@ -393,31 +425,11 @@ func (r *RoutingReconciler) buildPublicHTTPRoute(nebariApp *appsv1.NebariApp, ga routeName := naming.PublicHTTPRouteName(nebariApp) namespace := gatewayv1.Namespace(constants.GatewayNamespace) - sectionName := gatewayv1.SectionName("https") - tlsEnabled := true - if nebariApp.Spec.Routing != nil && nebariApp.Spec.Routing.TLS != nil && nebariApp.Spec.Routing.TLS.Enabled != nil && !*nebariApp.Spec.Routing.TLS.Enabled { - sectionName = gatewayv1.SectionName("http") - tlsEnabled = false - } - if tlsListenerName != "" && tlsEnabled { - sectionName = gatewayv1.SectionName(tlsListenerName) - } + sectionName, tlsEnabled := resolveListenerSection(nebariApp, tlsListenerName) - // Build matches for each public route (default to Exact for safer auth bypass) - matches := make([]gatewayv1.HTTPRouteMatch, 0, len(nebariApp.Spec.Routing.PublicRoutes)) - for _, route := range nebariApp.Spec.Routing.PublicRoutes { - pathType := gatewayv1.PathMatchExact - if route.PathType == "PathPrefix" { - pathType = gatewayv1.PathMatchPathPrefix - } - pathValue := route.PathPrefix - matches = append(matches, gatewayv1.HTTPRouteMatch{ - Path: &gatewayv1.HTTPPathMatch{ - Type: &pathType, - Value: &pathValue, - }, - }) - } + // Public routes default to Exact matching (safer for auth bypass than a broad + // prefix). User-supplied routing.filters apply here too, via buildRouteRule. + matches := buildRouteMatches(nebariApp.Spec.Routing.PublicRoutes, gatewayv1.PathMatchExact) route := &gatewayv1.HTTPRoute{ ObjectMeta: metav1.ObjectMeta{ @@ -447,10 +459,7 @@ func (r *RoutingReconciler) buildPublicHTTPRoute(nebariApp *appsv1.NebariApp, ga gatewayv1.Hostname(nebariApp.Spec.Hostname), }, Rules: []gatewayv1.HTTPRouteRule{ - { - Matches: matches, - BackendRefs: r.buildBackendRefs(nebariApp), - }, + r.buildRouteRule(nebariApp, matches), }, }, } diff --git a/internal/controller/reconcilers/routing/httproute_test.go b/internal/controller/reconcilers/routing/httproute_test.go index a828318..4f73795 100644 --- a/internal/controller/reconcilers/routing/httproute_test.go +++ b/internal/controller/reconcilers/routing/httproute_test.go @@ -18,6 +18,7 @@ package routing import ( "context" + "reflect" "testing" corev1 "k8s.io/api/core/v1" @@ -406,6 +407,86 @@ func TestBuildHTTPRouteRules(t *testing.T) { } } +// TestUserFiltersPropagateToRoutes asserts that user-supplied routing.filters are +// appended, in order, to both the protected and the public HTTPRoute (see #137). +func TestUserFiltersPropagateToRoutes(t *testing.T) { + scheme := runtime.NewScheme() + _ = appsv1.AddToScheme(scheme) + _ = gatewayv1.Install(scheme) + + reconciler := &RoutingReconciler{Scheme: scheme} + + filters := []gatewayv1.HTTPRouteFilter{ + { + Type: gatewayv1.HTTPRouteFilterResponseHeaderModifier, + ResponseHeaderModifier: &gatewayv1.HTTPHeaderFilter{ + Remove: []string{"Access-Control-Allow-Credentials"}, + }, + }, + { + Type: gatewayv1.HTTPRouteFilterRequestHeaderModifier, + RequestHeaderModifier: &gatewayv1.HTTPHeaderFilter{ + Add: []gatewayv1.HTTPHeader{{Name: "X-Forwarded-Tenant", Value: "acme"}}, + }, + }, + } + + nebariApp := &appsv1.NebariApp{ + ObjectMeta: metav1.ObjectMeta{Name: "test-app", Namespace: "default"}, + Spec: appsv1.NebariAppSpec{ + Hostname: "test.example.com", + Service: appsv1.ServiceReference{Name: "test-service", Port: 8080}, + Routing: &appsv1.RoutingConfig{ + Filters: filters, + PublicRoutes: []appsv1.RouteMatch{{PathPrefix: "/healthz", PathType: "Exact"}}, + }, + }, + } + + mainRoute, err := reconciler.buildHTTPRoute(nebariApp, "nebari-gateway", "") + if err != nil { + t.Fatalf("buildHTTPRoute returned error: %v", err) + } + publicRoute, err := reconciler.buildPublicHTTPRoute(nebariApp, "nebari-gateway", "") + if err != nil { + t.Fatalf("buildPublicHTTPRoute returned error: %v", err) + } + + for _, tc := range []struct { + name string + route *gatewayv1.HTTPRoute + }{ + {"main", mainRoute}, + {"public", publicRoute}, + } { + if len(tc.route.Spec.Rules) != 1 { + t.Fatalf("%s route: expected 1 rule, got %d", tc.name, len(tc.route.Spec.Rules)) + } + got := tc.route.Spec.Rules[0].Filters + if !reflect.DeepEqual(got, filters) { + t.Errorf("%s route: filters not propagated in order.\n got: %#v\nwant: %#v", tc.name, got, filters) + } + } +} + +// TestUserFiltersAbsentWhenUnset asserts the rule carries no filters when the +// user configures none. +func TestUserFiltersAbsentWhenUnset(t *testing.T) { + reconciler := &RoutingReconciler{Scheme: runtime.NewScheme()} + nebariApp := &appsv1.NebariApp{ + Spec: appsv1.NebariAppSpec{ + Service: appsv1.ServiceReference{Name: "svc", Port: 80}, + }, + } + rules := reconciler.buildHTTPRouteRules(nebariApp) + if len(rules) != 1 { + t.Fatalf("expected 1 rule, got %d", len(rules)) + } + if len(rules[0].Filters) != 0 { + t.Errorf("expected no filters, got %d", len(rules[0].Filters)) + } +} + func TestReconcileRouting(t *testing.T) { scheme := runtime.NewScheme() _ = appsv1.AddToScheme(scheme) From ab93026cfeefa1a9f33eb36607976dff7139f03f Mon Sep 17 00:00:00 2001 From: viniciusdc Date: Mon, 15 Jun 2026 13:40:25 -0300 Subject: [PATCH 2/3] fix: make routing.filters schemaless to stay within the CRD CEL cost budget Inlining the full gateway.networking.k8s.io/v1 HTTPRouteFilter OpenAPI schema copied its nested x-kubernetes-validations (CEL) rules -- notably the cors filter's allowOrigins/allowMethods and the externalAuth/requestMirror backendRef rules -- into the NebariApp CRD. That pushed the per-CRD CEL cost total past the Kubernetes budget (~1.22x over, with cors.allowOrigins alone 7.2x over its per-rule limit), so the CRD failed to install and the E2E, Helm, and install checks all failed. Mark Filters as Schemaless + PreserveUnknownFields (Type=array) so controller-gen emits a permissive array schema with no nested CEL. The Go type stays []gatewayv1.HTTPRouteFilter, so each entry is still decoded into the typed struct and applied to the generated HTTPRoute, where Gateway API validates it -- the pass-through contract is unchanged. Shrinks the generated CRD by ~1300 lines. --- api/v1/nebariapp_types.go | 12 +- .../reconcilers.nebari.dev_nebariapps.yaml | 1322 +---------------- docs/api-reference.md | 2 +- 3 files changed, 19 insertions(+), 1317 deletions(-) diff --git a/api/v1/nebariapp_types.go b/api/v1/nebariapp_types.go index 2bef487..882070b 100644 --- a/api/v1/nebariapp_types.go +++ b/api/v1/nebariapp_types.go @@ -140,7 +140,17 @@ type RoutingConfig struct { // response headers at the gateway (for example, stripping an overpermissive // CORS header an upstream sets, or adding security headers) without forking // the operator or rebuilding the backend image. - // +optional + // + // The field is schemaless in the CRD: inlining the full HTTPRouteFilter + // OpenAPI schema would copy its nested x-kubernetes-validations (CEL) rules + // into the NebariApp CRD and blow the per-CRD CEL cost budget. As a + // pass-through, the filters are instead validated by Gateway API when the + // operator applies them to the generated HTTPRoute. Each entry must still be + // a valid HTTPRouteFilter object (it is decoded into the typed Go struct). + // +optional + // +kubebuilder:validation:Schemaless + // +kubebuilder:validation:Type=array + // +kubebuilder:pruning:PreserveUnknownFields Filters []gatewayv1.HTTPRouteFilter `json:"filters,omitempty"` } diff --git a/config/crd/bases/reconcilers.nebari.dev_nebariapps.yaml b/config/crd/bases/reconcilers.nebari.dev_nebariapps.yaml index 670b772..6643490 100644 --- a/config/crd/bases/reconcilers.nebari.dev_nebariapps.yaml +++ b/config/crd/bases/reconcilers.nebari.dev_nebariapps.yaml @@ -457,1323 +457,15 @@ spec: response headers at the gateway (for example, stripping an overpermissive CORS header an upstream sets, or adding security headers) without forking the operator or rebuilding the backend image. - items: - description: |- - HTTPRouteFilter defines processing steps that must be completed during the - request or response lifecycle. HTTPRouteFilters are meant as an extension - point to express processing that may be done in Gateway implementations. Some - examples include request or response modification, implementing - authentication strategies, rate-limiting, and traffic shaping. API - guarantee/conformance is defined based on the type of the filter. - - - - - - properties: - cors: - description: |- - CORS defines a schema for a filter that responds to the - cross-origin request based on HTTP response header. - - Support: Extended - - - properties: - allowCredentials: - description: |- - AllowCredentials indicates whether the actual cross-origin request allows - to include credentials. - - When set to true, the gateway will include the `Access-Control-Allow-Credentials` - response header with value true (case-sensitive). - - When set to false or omitted the gateway will omit the header - `Access-Control-Allow-Credentials` entirely (this is the standard CORS - behavior). - - Support: Extended - type: boolean - allowHeaders: - description: |- - AllowHeaders indicates which HTTP request headers are supported for - accessing the requested resource. - - Header names are not case sensitive. - - Multiple header names in the value of the `Access-Control-Allow-Headers` - response header are separated by a comma (","). - - When the `AllowHeaders` field is configured with one or more headers, the - gateway must return the `Access-Control-Allow-Headers` response header - which value is present in the `AllowHeaders` field. - - If any header name in the `Access-Control-Request-Headers` request header - is not included in the list of header names specified by the response - header `Access-Control-Allow-Headers`, it will present an error on the - client side. - - If any header name in the `Access-Control-Allow-Headers` response header - does not recognize by the client, it will also occur an error on the - client side. - - A wildcard indicates that the requests with all HTTP headers are allowed. - The `Access-Control-Allow-Headers` response header can only use `*` - wildcard as value when the `AllowCredentials` field is false or omitted. - - When the `AllowCredentials` field is true and `AllowHeaders` field - specified with the `*` wildcard, the gateway must specify one or more - HTTP headers in the value of the `Access-Control-Allow-Headers` response - header. The value of the header `Access-Control-Allow-Headers` is same as - the `Access-Control-Request-Headers` header provided by the client. If - the header `Access-Control-Request-Headers` is not included in the - request, the gateway will omit the `Access-Control-Allow-Headers` - response header, instead of specifying the `*` wildcard. A Gateway - implementation may choose to add implementation-specific default headers. - - Support: Extended - items: - description: |- - HTTPHeaderName is the name of an HTTP header. - - Valid values include: - - * "Authorization" - * "Set-Cookie" - - Invalid values include: - - - ":method" - ":" is an invalid character. This means that HTTP/2 pseudo - headers are not currently supported by this type. - - "/invalid" - "/ " is an invalid character - maxLength: 256 - minLength: 1 - pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ - type: string - maxItems: 64 - type: array - x-kubernetes-list-type: set - allowMethods: - description: |- - AllowMethods indicates which HTTP methods are supported for accessing the - requested resource. - - Valid values are any method defined by RFC9110, along with the special - value `*`, which represents all HTTP methods are allowed. - - Method names are case sensitive, so these values are also case-sensitive. - (See https://www.rfc-editor.org/rfc/rfc2616#section-5.1.1) - - Multiple method names in the value of the `Access-Control-Allow-Methods` - response header are separated by a comma (","). - - A CORS-safelisted method is a method that is `GET`, `HEAD`, or `POST`. - (See https://fetch.spec.whatwg.org/#cors-safelisted-method) The - CORS-safelisted methods are always allowed, regardless of whether they - are specified in the `AllowMethods` field. - - When the `AllowMethods` field is configured with one or more methods, the - gateway must return the `Access-Control-Allow-Methods` response header - which value is present in the `AllowMethods` field. - - If the HTTP method of the `Access-Control-Request-Method` request header - is not included in the list of methods specified by the response header - `Access-Control-Allow-Methods`, it will present an error on the client - side. - - The `Access-Control-Allow-Methods` response header can only use `*` - wildcard as value when the `AllowCredentials` field is false or omitted. - - When the `AllowCredentials` field is true and `AllowMethods` field - specified with the `*` wildcard, the gateway must specify one HTTP method - in the value of the Access-Control-Allow-Methods response header. The - value of the header `Access-Control-Allow-Methods` is same as the - `Access-Control-Request-Method` header provided by the client. If the - header `Access-Control-Request-Method` is not included in the request, - the gateway will omit the `Access-Control-Allow-Methods` response header, - instead of specifying the `*` wildcard. A Gateway implementation may - choose to add implementation-specific default methods. - - Support: Extended - items: - enum: - - GET - - HEAD - - POST - - PUT - - DELETE - - CONNECT - - OPTIONS - - TRACE - - PATCH - - '*' - type: string - maxItems: 9 - type: array - x-kubernetes-list-type: set - x-kubernetes-validations: - - message: AllowMethods cannot contain '*' alongside - other methods - rule: '!(''*'' in self && self.size() > 1)' - allowOrigins: - description: |- - AllowOrigins indicates whether the response can be shared with requested - resource from the given `Origin`. - - The `Origin` consists of a scheme and a host, with an optional port, and - takes the form `://(:)`. - - Valid values for scheme are: `http` and `https`. - - Valid values for port are any integer between 1 and 65535 (the list of - available TCP/UDP ports). Note that, if not included, port `80` is - assumed for `http` scheme origins, and port `443` is assumed for `https` - origins. This may affect origin matching. - - The host part of the origin may contain the wildcard character `*`. These - wildcard characters behave as follows: - - * `*` is a greedy match to the _left_, including any number of - DNS labels to the left of its position. This also means that - `*` will include any number of period `.` characters to the - left of its position. - * A wildcard by itself matches all hosts. - - An origin value that includes _only_ the `*` character indicates requests - from all `Origin`s are allowed. - - When the `AllowOrigins` field is configured with multiple origins, it - means the server supports clients from multiple origins. If the request - `Origin` matches the configured allowed origins, the gateway must return - the given `Origin` and sets value of the header - `Access-Control-Allow-Origin` same as the `Origin` header provided by the - client. - - The status code of a successful response to a "preflight" request is - always an OK status (i.e., 204 or 200). - - If the request `Origin` does not match the configured allowed origins, - the gateway returns 204/200 response but doesn't set the relevant - cross-origin response headers. Alternatively, the gateway responds with - 403 status to the "preflight" request is denied, coupled with omitting - the CORS headers. The cross-origin request fails on the client side. - Therefore, the client doesn't attempt the actual cross-origin request. - - The `Access-Control-Allow-Origin` response header can only use `*` - wildcard as value when the `AllowCredentials` field is false or omitted. - - When the `AllowCredentials` field is true and `AllowOrigins` field - specified with the `*` wildcard, the gateway must return a single origin - in the value of the `Access-Control-Allow-Origin` response header, - instead of specifying the `*` wildcard. The value of the header - `Access-Control-Allow-Origin` is same as the `Origin` header provided by - the client. - - Support: Extended - items: - description: |- - The CORSOrigin MUST NOT be a relative URI, and it MUST follow the URI syntax and - encoding rules specified in RFC3986. The CORSOrigin MUST include both a - scheme (e.g., "http" or "spiffe") and a scheme-specific-part, or it should be a single '*' character. - URIs that include an authority MUST include a fully qualified domain name or - IP address as the host. - The below regex was generated to simplify the assertion of scheme://host: being port optional - maxLength: 253 - minLength: 1 - pattern: (^\*$)|(^([a-zA-Z][a-zA-Z0-9+\-.]+):\/\/([^:/?#]+)(:([0-9]{1,5}))?$) - type: string - maxItems: 64 - type: array - x-kubernetes-list-type: set - x-kubernetes-validations: - - message: AllowOrigins cannot contain '*' alongside - other origins - rule: '!(''*'' in self && self.size() > 1)' - exposeHeaders: - description: |- - ExposeHeaders indicates which HTTP response headers can be exposed - to client-side scripts in response to a cross-origin request. - - A CORS-safelisted response header is an HTTP header in a CORS response - that it is considered safe to expose to the client scripts. - The CORS-safelisted response headers include the following headers: - `Cache-Control` - `Content-Language` - `Content-Length` - `Content-Type` - `Expires` - `Last-Modified` - `Pragma` - (See https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name) - The CORS-safelisted response headers are exposed to client by default. - - When an HTTP header name is specified using the `ExposeHeaders` field, - this additional header will be exposed as part of the response to the - client. - - Header names are not case sensitive. - - Multiple header names in the value of the `Access-Control-Expose-Headers` - response header are separated by a comma (","). - - A wildcard indicates that the responses with all HTTP headers are exposed - to clients. The `Access-Control-Expose-Headers` response header can only - use `*` wildcard as value when the `AllowCredentials` field is false or omitted. - - Support: Extended - items: - description: |- - HTTPHeaderName is the name of an HTTP header. - - Valid values include: - - * "Authorization" - * "Set-Cookie" - - Invalid values include: - - - ":method" - ":" is an invalid character. This means that HTTP/2 pseudo - headers are not currently supported by this type. - - "/invalid" - "/ " is an invalid character - maxLength: 256 - minLength: 1 - pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ - type: string - maxItems: 64 - type: array - x-kubernetes-list-type: set - maxAge: - default: 5 - description: |- - MaxAge indicates the duration (in seconds) for the client to cache the - results of a "preflight" request. - - The information provided by the `Access-Control-Allow-Methods` and - `Access-Control-Allow-Headers` response headers can be cached by the - client until the time specified by `Access-Control-Max-Age` elapses. - - The default value of `Access-Control-Max-Age` response header is 5 - (seconds). - format: int32 - minimum: 1 - type: integer - type: object - extensionRef: - description: |- - ExtensionRef is an optional, implementation-specific extension to the - "filter" behavior. For example, resource "myroutefilter" in group - "networking.example.net"). ExtensionRef MUST NOT be used for core and - extended filters. - - This filter can be used multiple times within the same rule. - - Support: Implementation-specific - properties: - group: - description: |- - Group is the group of the referent. For example, "gateway.networking.k8s.io". - When unspecified or empty string, core API group is inferred. - maxLength: 253 - pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - kind: - description: Kind is kind of the referent. For example - "HTTPRoute" or "Service". - maxLength: 63 - minLength: 1 - pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ - type: string - name: - description: Name is the name of the referent. - maxLength: 253 - minLength: 1 - type: string - required: - - group - - kind - - name - type: object - externalAuth: - description: |- - ExternalAuth configures settings related to sending request details - to an external auth service. The external service MUST authenticate - the request, and MAY authorize the request as well. - - If there is any problem communicating with the external service, - this filter MUST fail closed. - - Support: Extended - - - properties: - backendRef: - description: |- - BackendRef is a reference to a backend to send authorization - requests to. - - The backend must speak the selected protocol (GRPC or HTTP) on the - referenced port. - - If the backend service requires TLS, use BackendTLSPolicy to tell the - implementation to supply the TLS details to be used to connect to that - backend. - properties: - group: - default: "" - description: |- - Group is the group of the referent. For example, "gateway.networking.k8s.io". - When unspecified or empty string, core API group is inferred. - maxLength: 253 - pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - kind: - default: Service - description: |- - Kind is the Kubernetes resource kind of the referent. For example - "Service". - - Defaults to "Service" when not specified. - - ExternalName services can refer to CNAME DNS records that may live - outside of the cluster and as such are difficult to reason about in - terms of conformance. They also may not be safe to forward to (see - CVE-2021-25740 for more information). Implementations SHOULD NOT - support ExternalName Services. - - Support: Core (Services with a type other than ExternalName) - - Support: Implementation-specific (Services with type ExternalName) - maxLength: 63 - minLength: 1 - pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ - type: string - name: - description: Name is the name of the referent. - maxLength: 253 - minLength: 1 - type: string - namespace: - description: |- - Namespace is the namespace of the backend. When unspecified, the local - namespace is inferred. - - Note that when a namespace different than the local namespace is specified, - a ReferenceGrant object is required in the referent namespace to allow that - namespace's owner to accept the reference. See the ReferenceGrant - documentation for details. - - Support: Core - maxLength: 63 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - type: string - port: - description: |- - Port specifies the destination port number to use for this resource. - Port is required when the referent is a Kubernetes Service. In this - case, the port number is the service port number, not the target port. - For other resources, destination port might be derived from the referent - resource or this field. - format: int32 - maximum: 65535 - minimum: 1 - type: integer - required: - - name - type: object - x-kubernetes-validations: - - message: Must have port for Service reference - rule: '(size(self.group) == 0 && self.kind == ''Service'') - ? has(self.port) : true' - forwardBody: - description: |- - ForwardBody controls if requests to the authorization server should include - the body of the client request; and if so, how big that body is allowed - to be. - - It is expected that implementations will buffer the request body up to - `forwardBody.maxSize` bytes. Bodies over that size must be rejected with a - 4xx series error (413 or 403 are common examples), and fail processing - of the filter. - - If unset, or `forwardBody.maxSize` is set to `0`, then the body will not - be forwarded. - - Feature Name: HTTPRouteExternalAuthForwardBody - properties: - maxSize: - description: |- - MaxSize specifies how large in bytes the largest body that will be buffered - and sent to the authorization server. If the body size is larger than - `maxSize`, then the body sent to the authorization server must be - truncated to `maxSize` bytes. - - Experimental note: This behavior needs to be checked against - various dataplanes; it may need to be changed. - See https://github.com/kubernetes-sigs/gateway-api/pull/4001#discussion_r2291405746 - for more. - - If 0, the body will not be sent to the authorization server. - type: integer - type: object - grpc: - description: |- - GRPCAuthConfig contains configuration for communication with ext_authz - protocol-speaking backends. - - If unset, implementations must assume the default behavior for each - included field is intended. - properties: - allowedHeaders: - description: |- - AllowedRequestHeaders specifies what headers from the client request - will be sent to the authorization server. - - If this list is empty, then all headers must be sent. - - If the list has entries, only those entries must be sent. - items: - type: string - type: array - x-kubernetes-list-type: set - type: object - http: - description: |- - HTTPAuthConfig contains configuration for communication with HTTP-speaking - backends. - - If unset, implementations must assume the default behavior for each - included field is intended. - properties: - allowedHeaders: - description: |- - AllowedRequestHeaders specifies what additional headers from the client request - will be sent to the authorization server. - - The following headers must always be sent to the authorization server, - regardless of this setting: - - * `Host` - * `Method` - * `Path` - * `Content-Length` - * `Authorization` - - If this list is empty, then only those headers must be sent. - - Note that `Content-Length` has a special behavior, in that the length - sent must be correct for the actual request to the external authorization - server - that is, it must reflect the actual number of bytes sent in the - body of the request to the authorization server. - - So if the `forwardBody` stanza is unset, or `forwardBody.maxSize` is set - to `0`, then `Content-Length` must be `0`. If `forwardBody.maxSize` is set - to anything other than `0`, then the `Content-Length` of the authorization - request must be set to the actual number of bytes forwarded. - items: - type: string - type: array - x-kubernetes-list-type: set - allowedResponseHeaders: - description: |- - AllowedResponseHeaders specifies what headers from the authorization response - will be copied into the request to the backend. - - If this list is empty, then all headers from the authorization server - except Authority or Host must be copied. - items: - type: string - type: array - x-kubernetes-list-type: set - path: - description: |- - Path sets the prefix that paths from the client request will have added - when forwarded to the authorization server. - - When empty or unspecified, no prefix is added. - - Valid values are the same as the "value" regex for path values in the `match` - stanza, and the validation regex will screen out invalid paths in the same way. - Even with the validation, implementations MUST sanitize this input before using it - directly. - maxLength: 1024 - pattern: ^(?:[-A-Za-z0-9/._~!$&'()*+,;=:@]|[%][0-9a-fA-F]{2})+$ - type: string - type: object - protocol: - description: |- - ExternalAuthProtocol describes which protocol to use when communicating with an - ext_authz authorization server. - - When this is set to GRPC, each backend must use the Envoy ext_authz protocol - on the port specified in `backendRefs`. Requests and responses are defined - in the protobufs explained at: - https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto - - When this is set to HTTP, each backend must respond with a `200` status - code in on a successful authorization. Any other code is considered - an authorization failure. - - Feature Names: - GRPC Support - HTTPRouteExternalAuthGRPC - HTTP Support - HTTPRouteExternalAuthHTTP - enum: - - HTTP - - GRPC - type: string - required: - - backendRef - - protocol - type: object - x-kubernetes-validations: - - message: grpc must be specified when protocol is set to - 'GRPC' - rule: 'self.protocol == ''GRPC'' ? has(self.grpc) : true' - - message: protocol must be 'GRPC' when grpc is set - rule: 'has(self.grpc) ? self.protocol == ''GRPC'' : true' - - message: http must be specified when protocol is set to - 'HTTP' - rule: 'self.protocol == ''HTTP'' ? has(self.http) : true' - - message: protocol must be 'HTTP' when http is set - rule: 'has(self.http) ? self.protocol == ''HTTP'' : true' - requestHeaderModifier: - description: |- - RequestHeaderModifier defines a schema for a filter that modifies request - headers. - Support: Core - properties: - add: - description: |- - Add adds the given header(s) (name, value) to the request - before the action. It appends to any existing values associated - with the header name. - - Input: - GET /foo HTTP/1.1 - my-header: foo - - Config: - add: - - name: "my-header" - value: "bar,baz" - - Output: - GET /foo HTTP/1.1 - my-header: foo,bar,baz - items: - description: HTTPHeader represents an HTTP Header - name and value as defined by RFC 7230. - properties: - name: - description: |- - Name is the name of the HTTP Header to be matched. Name matching MUST be - case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). - - If multiple entries specify equivalent header names, the first entry with - an equivalent name MUST be considered for a match. Subsequent entries - with an equivalent header name MUST be ignored. Due to the - case-insensitivity of header names, "foo" and "Foo" are considered - equivalent. - maxLength: 256 - minLength: 1 - pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ - type: string - value: - description: Value is the value of HTTP Header - to be matched. - maxLength: 4096 - minLength: 1 - type: string - required: - - name - - value - type: object - maxItems: 16 - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - remove: - description: |- - Remove the given header(s) from the HTTP request before the action. The - value of Remove is a list of HTTP header names. Note that the header - names are case-insensitive (see - https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). - - Input: - GET /foo HTTP/1.1 - my-header1: foo - my-header2: bar - my-header3: baz - - Config: - remove: ["my-header1", "my-header3"] - - Output: - GET /foo HTTP/1.1 - my-header2: bar - items: - type: string - maxItems: 16 - type: array - x-kubernetes-list-type: set - set: - description: |- - Set overwrites the request with the given header (name, value) - before the action. - - Input: - GET /foo HTTP/1.1 - my-header: foo - - Config: - set: - - name: "my-header" - value: "bar" - - Output: - GET /foo HTTP/1.1 - my-header: bar - items: - description: HTTPHeader represents an HTTP Header - name and value as defined by RFC 7230. - properties: - name: - description: |- - Name is the name of the HTTP Header to be matched. Name matching MUST be - case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). - - If multiple entries specify equivalent header names, the first entry with - an equivalent name MUST be considered for a match. Subsequent entries - with an equivalent header name MUST be ignored. Due to the - case-insensitivity of header names, "foo" and "Foo" are considered - equivalent. - maxLength: 256 - minLength: 1 - pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ - type: string - value: - description: Value is the value of HTTP Header - to be matched. - maxLength: 4096 - minLength: 1 - type: string - required: - - name - - value - type: object - maxItems: 16 - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - type: object - requestMirror: - description: |- - RequestMirror defines a schema for a filter that mirrors requests. - Requests are sent to the specified destination, but responses from - that destination are ignored. - - This filter can be used multiple times within the same rule. Note that - not all implementations will be able to support mirroring to multiple - backends. - - Support: Extended - properties: - backendRef: - description: |- - BackendRef references a resource where mirrored requests are sent. - - Mirrored requests must be sent only to a single destination endpoint - within this BackendRef, irrespective of how many endpoints are present - within this BackendRef. - - If the referent cannot be found, this BackendRef is invalid and must be - dropped from the Gateway. The controller must ensure the "ResolvedRefs" - condition on the Route status is set to `status: False` and not configure - this backend in the underlying implementation. - - If there is a cross-namespace reference to an *existing* object - that is not allowed by a ReferenceGrant, the controller must ensure the - "ResolvedRefs" condition on the Route is set to `status: False`, - with the "RefNotPermitted" reason and not configure this backend in the - underlying implementation. - - In either error case, the Message of the `ResolvedRefs` Condition - should be used to provide more detail about the problem. - - Support: Extended for Kubernetes Service - - Support: Implementation-specific for any other resource - properties: - group: - default: "" - description: |- - Group is the group of the referent. For example, "gateway.networking.k8s.io". - When unspecified or empty string, core API group is inferred. - maxLength: 253 - pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - kind: - default: Service - description: |- - Kind is the Kubernetes resource kind of the referent. For example - "Service". - - Defaults to "Service" when not specified. - - ExternalName services can refer to CNAME DNS records that may live - outside of the cluster and as such are difficult to reason about in - terms of conformance. They also may not be safe to forward to (see - CVE-2021-25740 for more information). Implementations SHOULD NOT - support ExternalName Services. - - Support: Core (Services with a type other than ExternalName) - - Support: Implementation-specific (Services with type ExternalName) - maxLength: 63 - minLength: 1 - pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ - type: string - name: - description: Name is the name of the referent. - maxLength: 253 - minLength: 1 - type: string - namespace: - description: |- - Namespace is the namespace of the backend. When unspecified, the local - namespace is inferred. - - Note that when a namespace different than the local namespace is specified, - a ReferenceGrant object is required in the referent namespace to allow that - namespace's owner to accept the reference. See the ReferenceGrant - documentation for details. - - Support: Core - maxLength: 63 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - type: string - port: - description: |- - Port specifies the destination port number to use for this resource. - Port is required when the referent is a Kubernetes Service. In this - case, the port number is the service port number, not the target port. - For other resources, destination port might be derived from the referent - resource or this field. - format: int32 - maximum: 65535 - minimum: 1 - type: integer - required: - - name - type: object - x-kubernetes-validations: - - message: Must have port for Service reference - rule: '(size(self.group) == 0 && self.kind == ''Service'') - ? has(self.port) : true' - fraction: - description: |- - Fraction represents the fraction of requests that should be - mirrored to BackendRef. - - Only one of Fraction or Percent may be specified. If neither field - is specified, 100% of requests will be mirrored. - properties: - denominator: - default: 100 - format: int32 - minimum: 1 - type: integer - numerator: - format: int32 - minimum: 0 - type: integer - required: - - numerator - type: object - x-kubernetes-validations: - - message: numerator must be less than or equal to denominator - rule: self.numerator <= self.denominator - percent: - description: |- - Percent represents the percentage of requests that should be - mirrored to BackendRef. Its minimum value is 0 (indicating 0% of - requests) and its maximum value is 100 (indicating 100% of requests). - - Only one of Fraction or Percent may be specified. If neither field - is specified, 100% of requests will be mirrored. - format: int32 - maximum: 100 - minimum: 0 - type: integer - required: - - backendRef - type: object - x-kubernetes-validations: - - message: Only one of percent or fraction may be specified - in HTTPRequestMirrorFilter - rule: '!(has(self.percent) && has(self.fraction))' - requestRedirect: - description: |- - RequestRedirect defines a schema for a filter that responds to the - request with an HTTP redirection. - - Support: Core - properties: - hostname: - description: |- - Hostname is the hostname to be used in the value of the `Location` - header in the response. - When empty, the hostname in the `Host` header of the request is used. - - Support: Core - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - path: - description: |- - Path defines parameters used to modify the path of the incoming request. - The modified path is then used to construct the `Location` header. When - empty, the request path is used as-is. - - Support: Extended - properties: - replaceFullPath: - description: |- - ReplaceFullPath specifies the value with which to replace the full path - of a request during a rewrite or redirect. - maxLength: 1024 - type: string - replacePrefixMatch: - description: |- - ReplacePrefixMatch specifies the value with which to replace the prefix - match of a request during a rewrite or redirect. For example, a request - to "/foo/bar" with a prefix match of "/foo" and a ReplacePrefixMatch - of "/xyz" would be modified to "/xyz/bar". - - Note that this matches the behavior of the PathPrefix match type. This - matches full path elements. A path element refers to the list of labels - in the path split by the `/` separator. When specified, a trailing `/` is - ignored. For example, the paths `/abc`, `/abc/`, and `/abc/def` would all - match the prefix `/abc`, but the path `/abcd` would not. - - ReplacePrefixMatch is only compatible with a `PathPrefix` HTTPRouteMatch. - Using any other HTTPRouteMatch type on the same HTTPRouteRule will result in - the implementation setting the Accepted Condition for the Route to `status: False`. - - Request Path | Prefix Match | Replace Prefix | Modified Path - maxLength: 1024 - type: string - type: - description: |- - Type defines the type of path modifier. Additional types may be - added in a future release of the API. - - Note that values may be added to this enum, implementations - must ensure that unknown values will not cause a crash. - - Unknown values here must result in the implementation setting the - Accepted Condition for the Route to `status: False`, with a - Reason of `UnsupportedValue`. - enum: - - ReplaceFullPath - - ReplacePrefixMatch - type: string - required: - - type - type: object - x-kubernetes-validations: - - message: replaceFullPath must be specified when type - is set to 'ReplaceFullPath' - rule: 'self.type == ''ReplaceFullPath'' ? has(self.replaceFullPath) - : true' - - message: type must be 'ReplaceFullPath' when replaceFullPath - is set - rule: 'has(self.replaceFullPath) ? self.type == ''ReplaceFullPath'' - : true' - - message: replacePrefixMatch must be specified when - type is set to 'ReplacePrefixMatch' - rule: 'self.type == ''ReplacePrefixMatch'' ? has(self.replacePrefixMatch) - : true' - - message: type must be 'ReplacePrefixMatch' when replacePrefixMatch - is set - rule: 'has(self.replacePrefixMatch) ? self.type == - ''ReplacePrefixMatch'' : true' - port: - description: |- - Port is the port to be used in the value of the `Location` - header in the response. - - If no port is specified, the redirect port MUST be derived using the - following rules: - - * If redirect scheme is not-empty, the redirect port MUST be the well-known - port associated with the redirect scheme. Specifically "http" to port 80 - and "https" to port 443. If the redirect scheme does not have a - well-known port, the listener port of the Gateway SHOULD be used. - * If redirect scheme is empty, the redirect port MUST be the Gateway - Listener port. - - Implementations SHOULD NOT add the port number in the 'Location' - header in the following cases: - - * A Location header that will use HTTP (whether that is determined via - the Listener protocol or the Scheme field) _and_ use port 80. - * A Location header that will use HTTPS (whether that is determined via - the Listener protocol or the Scheme field) _and_ use port 443. - - Support: Extended - format: int32 - maximum: 65535 - minimum: 1 - type: integer - scheme: - description: |- - Scheme is the scheme to be used in the value of the `Location` header in - the response. When empty, the scheme of the request is used. - - Scheme redirects can affect the port of the redirect, for more information, - refer to the documentation for the port field of this filter. - - Note that values may be added to this enum, implementations - must ensure that unknown values will not cause a crash. - - Unknown values here must result in the implementation setting the - Accepted Condition for the Route to `status: False`, with a - Reason of `UnsupportedValue`. - - Support: Extended - enum: - - http - - https - type: string - statusCode: - default: 302 - description: |- - StatusCode is the HTTP status code to be used in response. - - Note that values may be added to this enum, implementations - must ensure that unknown values will not cause a crash. - - Unknown values here must result in the implementation setting the - Accepted Condition for the Route to `status: False`, with a - Reason of `UnsupportedValue`. - - Support: Core - enum: - - 301 - - 302 - type: integer - type: object - responseHeaderModifier: - description: |- - ResponseHeaderModifier defines a schema for a filter that modifies response - headers. - - Support: Extended - properties: - add: - description: |- - Add adds the given header(s) (name, value) to the request - before the action. It appends to any existing values associated - with the header name. - - Input: - GET /foo HTTP/1.1 - my-header: foo - - Config: - add: - - name: "my-header" - value: "bar,baz" - - Output: - GET /foo HTTP/1.1 - my-header: foo,bar,baz - items: - description: HTTPHeader represents an HTTP Header - name and value as defined by RFC 7230. - properties: - name: - description: |- - Name is the name of the HTTP Header to be matched. Name matching MUST be - case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). - - If multiple entries specify equivalent header names, the first entry with - an equivalent name MUST be considered for a match. Subsequent entries - with an equivalent header name MUST be ignored. Due to the - case-insensitivity of header names, "foo" and "Foo" are considered - equivalent. - maxLength: 256 - minLength: 1 - pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ - type: string - value: - description: Value is the value of HTTP Header - to be matched. - maxLength: 4096 - minLength: 1 - type: string - required: - - name - - value - type: object - maxItems: 16 - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - remove: - description: |- - Remove the given header(s) from the HTTP request before the action. The - value of Remove is a list of HTTP header names. Note that the header - names are case-insensitive (see - https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). - - Input: - GET /foo HTTP/1.1 - my-header1: foo - my-header2: bar - my-header3: baz - - Config: - remove: ["my-header1", "my-header3"] - - Output: - GET /foo HTTP/1.1 - my-header2: bar - items: - type: string - maxItems: 16 - type: array - x-kubernetes-list-type: set - set: - description: |- - Set overwrites the request with the given header (name, value) - before the action. - - Input: - GET /foo HTTP/1.1 - my-header: foo - - Config: - set: - - name: "my-header" - value: "bar" - - Output: - GET /foo HTTP/1.1 - my-header: bar - items: - description: HTTPHeader represents an HTTP Header - name and value as defined by RFC 7230. - properties: - name: - description: |- - Name is the name of the HTTP Header to be matched. Name matching MUST be - case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). - - If multiple entries specify equivalent header names, the first entry with - an equivalent name MUST be considered for a match. Subsequent entries - with an equivalent header name MUST be ignored. Due to the - case-insensitivity of header names, "foo" and "Foo" are considered - equivalent. - maxLength: 256 - minLength: 1 - pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ - type: string - value: - description: Value is the value of HTTP Header - to be matched. - maxLength: 4096 - minLength: 1 - type: string - required: - - name - - value - type: object - maxItems: 16 - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - type: object - type: - description: |- - Type identifies the type of filter to apply. As with other API fields, - types are classified into three conformance levels: - - - Core: Filter types and their corresponding configuration defined by - "Support: Core" in this package, e.g. "RequestHeaderModifier". All - implementations must support core filters. - - - Extended: Filter types and their corresponding configuration defined by - "Support: Extended" in this package, e.g. "RequestMirror". Implementers - are encouraged to support extended filters. - - - Implementation-specific: Filters that are defined and supported by - specific vendors. - In the future, filters showing convergence in behavior across multiple - implementations will be considered for inclusion in extended or core - conformance levels. Filter-specific configuration for such filters - is specified using the ExtensionRef field. `Type` should be set to - "ExtensionRef" for custom filters. - - Implementers are encouraged to define custom implementation types to - extend the core API with implementation-specific behavior. - - If a reference to a custom filter type cannot be resolved, the filter - MUST NOT be skipped. Instead, requests that would have been processed by - that filter MUST receive a HTTP error response. - - Note that values may be added to this enum, implementations - must ensure that unknown values will not cause a crash. - - Unknown values here must result in the implementation setting the - Accepted Condition for the Route to `status: False`, with a - Reason of `UnsupportedValue`. - - - enum: - - RequestHeaderModifier - - ResponseHeaderModifier - - RequestMirror - - RequestRedirect - - URLRewrite - - ExtensionRef - type: string - urlRewrite: - description: |- - URLRewrite defines a schema for a filter that modifies a request during forwarding. - - Support: Extended - properties: - hostname: - description: |- - Hostname is the value to be used to replace the Host header value during - forwarding. - - Support: Extended - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - path: - description: |- - Path defines a path rewrite. - - Support: Extended - properties: - replaceFullPath: - description: |- - ReplaceFullPath specifies the value with which to replace the full path - of a request during a rewrite or redirect. - maxLength: 1024 - type: string - replacePrefixMatch: - description: |- - ReplacePrefixMatch specifies the value with which to replace the prefix - match of a request during a rewrite or redirect. For example, a request - to "/foo/bar" with a prefix match of "/foo" and a ReplacePrefixMatch - of "/xyz" would be modified to "/xyz/bar". - - Note that this matches the behavior of the PathPrefix match type. This - matches full path elements. A path element refers to the list of labels - in the path split by the `/` separator. When specified, a trailing `/` is - ignored. For example, the paths `/abc`, `/abc/`, and `/abc/def` would all - match the prefix `/abc`, but the path `/abcd` would not. - - ReplacePrefixMatch is only compatible with a `PathPrefix` HTTPRouteMatch. - Using any other HTTPRouteMatch type on the same HTTPRouteRule will result in - the implementation setting the Accepted Condition for the Route to `status: False`. - - Request Path | Prefix Match | Replace Prefix | Modified Path - maxLength: 1024 - type: string - type: - description: |- - Type defines the type of path modifier. Additional types may be - added in a future release of the API. - - Note that values may be added to this enum, implementations - must ensure that unknown values will not cause a crash. - - Unknown values here must result in the implementation setting the - Accepted Condition for the Route to `status: False`, with a - Reason of `UnsupportedValue`. - enum: - - ReplaceFullPath - - ReplacePrefixMatch - type: string - required: - - type - type: object - x-kubernetes-validations: - - message: replaceFullPath must be specified when type - is set to 'ReplaceFullPath' - rule: 'self.type == ''ReplaceFullPath'' ? has(self.replaceFullPath) - : true' - - message: type must be 'ReplaceFullPath' when replaceFullPath - is set - rule: 'has(self.replaceFullPath) ? self.type == ''ReplaceFullPath'' - : true' - - message: replacePrefixMatch must be specified when - type is set to 'ReplacePrefixMatch' - rule: 'self.type == ''ReplacePrefixMatch'' ? has(self.replacePrefixMatch) - : true' - - message: type must be 'ReplacePrefixMatch' when replacePrefixMatch - is set - rule: 'has(self.replacePrefixMatch) ? self.type == - ''ReplacePrefixMatch'' : true' - type: object - required: - - type - type: object - x-kubernetes-validations: - - message: filter.requestHeaderModifier must be nil if the filter.type - is not RequestHeaderModifier - rule: '!(has(self.requestHeaderModifier) && self.type != ''RequestHeaderModifier'')' - - message: filter.requestHeaderModifier must be specified for - RequestHeaderModifier filter.type - rule: '!(!has(self.requestHeaderModifier) && self.type == - ''RequestHeaderModifier'')' - - message: filter.responseHeaderModifier must be nil if the - filter.type is not ResponseHeaderModifier - rule: '!(has(self.responseHeaderModifier) && self.type != - ''ResponseHeaderModifier'')' - - message: filter.responseHeaderModifier must be specified for - ResponseHeaderModifier filter.type - rule: '!(!has(self.responseHeaderModifier) && self.type == - ''ResponseHeaderModifier'')' - - message: filter.requestMirror must be nil if the filter.type - is not RequestMirror - rule: '!(has(self.requestMirror) && self.type != ''RequestMirror'')' - - message: filter.requestMirror must be specified for RequestMirror - filter.type - rule: '!(!has(self.requestMirror) && self.type == ''RequestMirror'')' - - message: filter.requestRedirect must be nil if the filter.type - is not RequestRedirect - rule: '!(has(self.requestRedirect) && self.type != ''RequestRedirect'')' - - message: filter.requestRedirect must be specified for RequestRedirect - filter.type - rule: '!(!has(self.requestRedirect) && self.type == ''RequestRedirect'')' - - message: filter.urlRewrite must be nil if the filter.type - is not URLRewrite - rule: '!(has(self.urlRewrite) && self.type != ''URLRewrite'')' - - message: filter.urlRewrite must be specified for URLRewrite - filter.type - rule: '!(!has(self.urlRewrite) && self.type == ''URLRewrite'')' - - message: filter.extensionRef must be nil if the filter.type - is not ExtensionRef - rule: '!(has(self.extensionRef) && self.type != ''ExtensionRef'')' - - message: filter.extensionRef must be specified for ExtensionRef - filter.type - rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')' + The field is schemaless in the CRD: inlining the full HTTPRouteFilter + OpenAPI schema would copy its nested x-kubernetes-validations (CEL) rules + into the NebariApp CRD and blow the per-CRD CEL cost budget. As a + pass-through, the filters are instead validated by Gateway API when the + operator applies them to the generated HTTPRoute. Each entry must still be + a valid HTTPRouteFilter object (it is decoded into the typed Go struct). type: array + x-kubernetes-preserve-unknown-fields: true publicRoutes: description: |- PublicRoutes specifies paths that should bypass OIDC authentication. diff --git a/docs/api-reference.md b/docs/api-reference.md index 07b49bf..1c0d7a6 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -312,7 +312,7 @@ _Appears in:_ | `publicRoutes` _[RouteMatch](#routematch) array_ | PublicRoutes specifies paths that should bypass OIDC authentication.
When auth is enabled and these are specified, these paths will be routed
via a separate HTTPRoute that is not protected by the SecurityPolicy.
Each entry uses the same RouteMatch format as routes, supporting both
PathPrefix (default) and Exact matching via the pathType field.
Example: [\{pathPrefix: "/api/v1/health", pathType: "Exact"\}]
PublicRoutes is for carving out a *subset* of paths that should bypass
auth while the rest stays protected. It is not a way to disable auth for
the whole app: if a public path also matches the main route (including the
default "/" match when routes is empty) while auth.enforceAtGateway is
true, both a protected and an unprotected HTTPRoute claim that path and
the effective auth posture is undefined. To turn gateway auth off entirely,
set auth.enforceAtGateway: false (keep provisioning the Keycloak client) or
auth.enabled: false (no client at all) instead of listing every path here. | | Optional: \{\}
| | `tls` _[RoutingTLSConfig](#routingtlsconfig)_ | TLS configures TLS certificate management and termination behavior.
When TLS is enabled (the default), the operator creates a cert-manager Certificate
for the application's hostname and adds a per-app HTTPS listener to the shared Gateway. | | Optional: \{\}
| | `annotations` _object (keys:string, values:string)_ | Annotations defines additional annotations to merge onto the generated HTTPRoute.
Useful for tools like ArgoCD that track resources via annotations
(e.g. argocd.argoproj.io/tracking-id).
These annotations are merged with any operator-managed annotations; operator
annotations always take precedence to avoid breaking internal behaviour. | | Optional: \{\}
| -| `filters` _HTTPRouteFilter array_ | Filters are appended, in order, to every HTTPRouteRule the operator
generates for this NebariApp (both the protected route and the public
route built from publicRoutes). This is a pass-through to the upstream
gateway.networking.k8s.io/v1 HTTPRouteFilter spec, mirroring the
Annotations escape hatch: the operator does not interpret, reorder, or
strip the filters, it only forwards them. Use it to adjust request or
response headers at the gateway (for example, stripping an overpermissive
CORS header an upstream sets, or adding security headers) without forking
the operator or rebuilding the backend image. | | Optional: \{\}
| +| `filters` _HTTPRouteFilter array_ | Filters are appended, in order, to every HTTPRouteRule the operator
generates for this NebariApp (both the protected route and the public
route built from publicRoutes). This is a pass-through to the upstream
gateway.networking.k8s.io/v1 HTTPRouteFilter spec, mirroring the
Annotations escape hatch: the operator does not interpret, reorder, or
strip the filters, it only forwards them. Use it to adjust request or
response headers at the gateway (for example, stripping an overpermissive
CORS header an upstream sets, or adding security headers) without forking
the operator or rebuilding the backend image.
The field is schemaless in the CRD: inlining the full HTTPRouteFilter
OpenAPI schema would copy its nested x-kubernetes-validations (CEL) rules
into the NebariApp CRD and blow the per-CRD CEL cost budget. As a
pass-through, the filters are instead validated by Gateway API when the
operator applies them to the generated HTTPRoute. Each entry must still be
a valid HTTPRouteFilter object (it is decoded into the typed Go struct). | | Schemaless: \{\}
Type: array
Optional: \{\}
| --- From c0d870fb56caf3b469eb4e8de8e29200a2351deb Mon Sep 17 00:00:00 2001 From: viniciusdc Date: Mon, 15 Jun 2026 13:56:17 -0300 Subject: [PATCH 3/3] fix: emit a valid permissive schema for routing.filters The previous attempt set Type=array together with Schemaless, which produced a `type: array` node with no `items` -- a structural schema requires `items` for arrays, so the CRD still failed to install ("filters.items: Required value: must be specified"). Drop the explicit Type marker and keep Schemaless + PreserveUnknownFields, so controller-gen emits the field as `{x-kubernetes-preserve-unknown-fields: true}` (the canonical "arbitrary content" schema) with no type constraint and no nested CEL. Verified by installing the regenerated CRD into a real apiserver via envtest: CRD installation now succeeds. --- api/v1/nebariapp_types.go | 1 - config/crd/bases/reconcilers.nebari.dev_nebariapps.yaml | 1 - docs/api-reference.md | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/api/v1/nebariapp_types.go b/api/v1/nebariapp_types.go index 882070b..2dd29b3 100644 --- a/api/v1/nebariapp_types.go +++ b/api/v1/nebariapp_types.go @@ -149,7 +149,6 @@ type RoutingConfig struct { // a valid HTTPRouteFilter object (it is decoded into the typed Go struct). // +optional // +kubebuilder:validation:Schemaless - // +kubebuilder:validation:Type=array // +kubebuilder:pruning:PreserveUnknownFields Filters []gatewayv1.HTTPRouteFilter `json:"filters,omitempty"` } diff --git a/config/crd/bases/reconcilers.nebari.dev_nebariapps.yaml b/config/crd/bases/reconcilers.nebari.dev_nebariapps.yaml index 6643490..7e8174d 100644 --- a/config/crd/bases/reconcilers.nebari.dev_nebariapps.yaml +++ b/config/crd/bases/reconcilers.nebari.dev_nebariapps.yaml @@ -464,7 +464,6 @@ spec: pass-through, the filters are instead validated by Gateway API when the operator applies them to the generated HTTPRoute. Each entry must still be a valid HTTPRouteFilter object (it is decoded into the typed Go struct). - type: array x-kubernetes-preserve-unknown-fields: true publicRoutes: description: |- diff --git a/docs/api-reference.md b/docs/api-reference.md index 1c0d7a6..e5a97e9 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -312,7 +312,7 @@ _Appears in:_ | `publicRoutes` _[RouteMatch](#routematch) array_ | PublicRoutes specifies paths that should bypass OIDC authentication.
When auth is enabled and these are specified, these paths will be routed
via a separate HTTPRoute that is not protected by the SecurityPolicy.
Each entry uses the same RouteMatch format as routes, supporting both
PathPrefix (default) and Exact matching via the pathType field.
Example: [\{pathPrefix: "/api/v1/health", pathType: "Exact"\}]
PublicRoutes is for carving out a *subset* of paths that should bypass
auth while the rest stays protected. It is not a way to disable auth for
the whole app: if a public path also matches the main route (including the
default "/" match when routes is empty) while auth.enforceAtGateway is
true, both a protected and an unprotected HTTPRoute claim that path and
the effective auth posture is undefined. To turn gateway auth off entirely,
set auth.enforceAtGateway: false (keep provisioning the Keycloak client) or
auth.enabled: false (no client at all) instead of listing every path here. | | Optional: \{\}
| | `tls` _[RoutingTLSConfig](#routingtlsconfig)_ | TLS configures TLS certificate management and termination behavior.
When TLS is enabled (the default), the operator creates a cert-manager Certificate
for the application's hostname and adds a per-app HTTPS listener to the shared Gateway. | | Optional: \{\}
| | `annotations` _object (keys:string, values:string)_ | Annotations defines additional annotations to merge onto the generated HTTPRoute.
Useful for tools like ArgoCD that track resources via annotations
(e.g. argocd.argoproj.io/tracking-id).
These annotations are merged with any operator-managed annotations; operator
annotations always take precedence to avoid breaking internal behaviour. | | Optional: \{\}
| -| `filters` _HTTPRouteFilter array_ | Filters are appended, in order, to every HTTPRouteRule the operator
generates for this NebariApp (both the protected route and the public
route built from publicRoutes). This is a pass-through to the upstream
gateway.networking.k8s.io/v1 HTTPRouteFilter spec, mirroring the
Annotations escape hatch: the operator does not interpret, reorder, or
strip the filters, it only forwards them. Use it to adjust request or
response headers at the gateway (for example, stripping an overpermissive
CORS header an upstream sets, or adding security headers) without forking
the operator or rebuilding the backend image.
The field is schemaless in the CRD: inlining the full HTTPRouteFilter
OpenAPI schema would copy its nested x-kubernetes-validations (CEL) rules
into the NebariApp CRD and blow the per-CRD CEL cost budget. As a
pass-through, the filters are instead validated by Gateway API when the
operator applies them to the generated HTTPRoute. Each entry must still be
a valid HTTPRouteFilter object (it is decoded into the typed Go struct). | | Schemaless: \{\}
Type: array
Optional: \{\}
| +| `filters` _HTTPRouteFilter array_ | Filters are appended, in order, to every HTTPRouteRule the operator
generates for this NebariApp (both the protected route and the public
route built from publicRoutes). This is a pass-through to the upstream
gateway.networking.k8s.io/v1 HTTPRouteFilter spec, mirroring the
Annotations escape hatch: the operator does not interpret, reorder, or
strip the filters, it only forwards them. Use it to adjust request or
response headers at the gateway (for example, stripping an overpermissive
CORS header an upstream sets, or adding security headers) without forking
the operator or rebuilding the backend image.
The field is schemaless in the CRD: inlining the full HTTPRouteFilter
OpenAPI schema would copy its nested x-kubernetes-validations (CEL) rules
into the NebariApp CRD and blow the per-CRD CEL cost budget. As a
pass-through, the filters are instead validated by Gateway API when the
operator applies them to the generated HTTPRoute. Each entry must still be
a valid HTTPRouteFilter object (it is decoded into the typed Go struct). | | Schemaless: \{\}
Optional: \{\}
| ---