Skip to content

Routing: expose HTTPRouteFilter pass-through on generated HTTPRoutes #137

Description

@oren-openteams

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

  • RoutingConfig.Filters []gatewayv1.HTTPRouteFilter field added
  • Filters appended to rule in buildHTTPRouteRules (primary HTTPRoute)
  • Filters appended to rule in public HTTPRoute construction so publicRoutes paths get the same treatment
  • Operator does not strip / reorder / replace user-supplied filters during reconcile
  • Unit test asserts user-supplied filters appear on the rendered HTTPRoute (and chain in order when multiple are supplied)
  • CRD reference documents the field with a ResponseHeaderModifier filter example

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Start date

    None yet

    Target date

    None yet

    Size

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions