Scope note (split): this issue originally proposed two surfaces — a
HTTPRouteFilter pass-through and a curated CORS block. They close on different
timelines, so the curated CORS block was split out to #138. This issue now tracks
only the filters pass-through.
Problem
NebariAppSpec.Routing has no way to attach Gateway API HTTPRouteFilter entries to the routes the operator generates. The operator owns the generated HTTPRoute (ownerReferences.controller: true), so any manual filter added with kubectl patch gets reverted on the next reconcile. There is no Argo CD ignoreDifferences workaround because the operator — not Argo — manages the HTTPRoute.
This blocks deployer-side mitigation of common upstream-side HTTP issues that would otherwise be one-liner fixes at the gateway, and forces every deployer who needs to adjust headers to either fork the operator or rebuild the backend image.
Motivating example: invalid CORS headers from an upstream backend
A NebariApp backend's HTTP layer emits both:
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: *
These are mutually exclusive per the CORS spec — a credentialed response must specify an explicit origin, not the wildcard. Browsers reject any response that combines them.
The backend's index.html ships a <script type="module" crossorigin src="/assets/index-*.js"> (standard Vite output). The crossorigin attribute forces a CORS check even for same-origin module loads. With the invalid header combo above, the browser silently refuses to load the bundle, the SPA never initializes, and the user lands in an infinite /login reload loop with no surface-level diagnostic unless they open DevTools.
The deployer-side fix is trivial if the operator allowed filter customization:
spec:
routing:
filters:
- type: ResponseHeaderModifier
responseHeaderModifier:
remove:
- Access-Control-Allow-Credentials
(Note: this specific backend bug has since been fixed at the source in
https://redirect.github.com/nebari-dev/nebi/pull/385. It remains the clearest
motivating example for why a gateway-side escape hatch is needed for the next
misbehaving upstream.)
Other use cases for routing.filters
HTTPRouteFilter pass-through unlocks several common deployment needs that the operator currently has no answer for:
- Security headers — add
Strict-Transport-Security, Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, Referrer-Policy
- CORS hygiene — strip overpermissive headers an upstream sets; restrict
Allow-Origin to a specific origin
- Cache control — override
Cache-Control: no-store on static asset paths that should be cacheable
- Cookie scope rewrites — when a backend sets a cookie with the wrong
Path or Domain
- Trace ID stripping — remove internal
X-Request-ID / X-B3-* headers from external responses
- Request header injection — pass deployer context (
X-Forwarded-Tenant, etc.) to the backend
All standard Gateway API features the operator currently swallows.
Proposed change
Add a pass-through Filters field to RoutingConfig:
type RoutingConfig struct {
Routes []RouteMatch `json:"routes,omitempty"`
PublicRoutes []RouteMatch `json:"publicRoutes,omitempty"`
TLS *RoutingTLSConfig `json:"tls,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`
// Filters are appended to every HTTPRouteRule generated from this
// NebariApp (both protected and public routes). Pass-through to the
// upstream gateway.networking.k8s.io/v1 HTTPRouteFilter spec.
// +optional
Filters []gatewayv1.HTTPRouteFilter `json:"filters,omitempty"`
}
Apply at both HTTPRouteRule construction sites in internal/controller/reconcilers/routing/httproute.go (buildHTTPRouteRules for the protected route, and the parallel public-route construction):
return []gatewayv1.HTTPRouteRule{
{
Matches: matches,
BackendRefs: r.buildBackendRefs(nebariApp),
Filters: userFilters(nebariApp), // ← new
},
}
This is the same pattern as the existing Annotations field — pass-through to a standard Gateway API surface, deployer-controlled, no opinionated wrapper.
Definition of Done
Related
Problem
NebariAppSpec.Routinghas no way to attach Gateway APIHTTPRouteFilterentries to the routes the operator generates. The operator owns the generatedHTTPRoute(ownerReferences.controller: true), so any manual filter added withkubectl patchgets reverted on the next reconcile. There is no Argo CDignoreDifferencesworkaround because the operator — not Argo — manages theHTTPRoute.This blocks deployer-side mitigation of common upstream-side HTTP issues that would otherwise be one-liner fixes at the gateway, and forces every deployer who needs to adjust headers to either fork the operator or rebuild the backend image.
Motivating example: invalid CORS headers from an upstream backend
A NebariApp backend's HTTP layer emits both:
These are mutually exclusive per the CORS spec — a credentialed response must specify an explicit origin, not the wildcard. Browsers reject any response that combines them.
The backend's
index.htmlships a<script type="module" crossorigin src="/assets/index-*.js">(standard Vite output). Thecrossoriginattribute forces a CORS check even for same-origin module loads. With the invalid header combo above, the browser silently refuses to load the bundle, the SPA never initializes, and the user lands in an infinite/loginreload loop with no surface-level diagnostic unless they open DevTools.The deployer-side fix is trivial if the operator allowed filter customization:
(Note: this specific backend bug has since been fixed at the source in
https://redirect.github.com/nebari-dev/nebi/pull/385. It remains the clearest
motivating example for why a gateway-side escape hatch is needed for the next
misbehaving upstream.)
Other use cases for
routing.filtersHTTPRouteFilterpass-through unlocks several common deployment needs that the operator currently has no answer for:Strict-Transport-Security,Content-Security-Policy,X-Frame-Options,X-Content-Type-Options,Referrer-PolicyAllow-Originto a specific originCache-Control: no-storeon static asset paths that should be cacheablePathorDomainX-Request-ID/X-B3-*headers from external responsesX-Forwarded-Tenant, etc.) to the backendAll standard Gateway API features the operator currently swallows.
Proposed change
Add a pass-through
Filtersfield toRoutingConfig:Apply at both
HTTPRouteRuleconstruction sites ininternal/controller/reconcilers/routing/httproute.go(buildHTTPRouteRulesfor the protected route, and the parallel public-route construction):This is the same pattern as the existing
Annotationsfield — pass-through to a standard Gateway API surface, deployer-controlled, no opinionated wrapper.Definition of Done
RoutingConfig.Filters []gatewayv1.HTTPRouteFilterfield addedbuildHTTPRouteRules(primary HTTPRoute)publicRoutespaths get the same treatmentResponseHeaderModifierfilter exampleRelated
WebOriginson Keycloak clients; same theme on the IdP side