--- # ============================================================================= # forward-auth-cec.yaml — INTERIM ext_authz enforcement for Cilium 1.16.5 # Serves: R-IDP-02 (no management UI unauthenticated), R-IDP-06 (group authz). # Owner: WS-NET (+ WS-SEC). Status: TEMPLATE — NOT wired into kustomization.yaml. # ============================================================================= # # WHY THIS IS A STANDALONE CEC (and not an edit to the shared Gateway): # On Cilium 1.16.5 the shared Gateway's Envoy program is a MACHINE-OWNED, # constantly-regenerated CiliumEnvoyConfig (`gateway/cilium-gateway-shared`, # ownerReferences->Gateway, regenerated on EVERY HTTPRoute change). There is # no supported hook to inject an `ext_authz` HTTP filter into that generated # listener, and native Gateway-API ExternalAuth (GEP-1494) ships in NO # released Cilium version yet (PoC only). So ext_authz forward-auth on 1.16.5 # must be a SEPARATELY-authored Envoy listener. See README.md "Enforcement". # # WHAT IT DOES (VERIFIED in-cluster against cilium-envoy v1.30.8, 2026-08-28): # An `envoy.filters.http.ext_authz` HTTP filter calls oauth2-proxy's # /oauth2/auth decision endpoint. oauth2-proxy returns 202 for a valid # Keycloak session in `platform-admins` (request passes to the upstream) or # 401/403 otherwise (`failure_mode_allow: false` => Envoy denies and returns # oauth2-proxy's sign-in/redirect to the browser — the request NEVER reaches # the upstream). Proven: unauthenticated => oauth2-proxy 403 sign-in returned, # upstream body ("UPSTREAM-OK") never served; a no-filter control route on the # same backend returned 200. # # OPT-IN MODEL (per-route, low blast radius): # A route is protected ONLY by being listed as a virtualHost on THIS listener. # Apps NOT listed here keep hitting the shared Gateway unprotected. To protect # an app (e.g. pihole below; next: Crafty): # 1. Add its Service to `spec.backendServices` and a matching EDS Cluster. # 2. Add its virtualHost to the `forward-auth` RouteConfiguration. # 3. REMOVE its HTTPRoute from the shared Gateway (else it stays reachable # unauthenticated there) and point its DNS at THIS listener's VIP. # The app's ServiceDefinition already declares `auth.mode: forward-auth` + # `groups: [platform-admins]`; the HTTPRoute carries those as annotations # (homelab.keithjackson.dev/auth-mode,/auth-groups) — the opt-in signal. # # DEPLOY (PM step — NOT automatic on merge): # * Allocate a dedicated L2 VIP (CiliumLoadBalancerIPPool / the Phase-2 pool) # and a LoadBalancer Service that this CEC binds via `spec.services` # (`oauth2-protected-ingress` below). TLS reuses the SAME wildcard cert the # shared Gateway already syncs to SDS: `cilium-secrets/gateway-wildcard-home-tls`. # * Repoint the protected hostnames' DNS (ExternalDNS/pihole) at that VIP. # * `kubectl apply -f forward-auth-cec.yaml` (or add to kustomization.yaml). # ----------------------------------------------------------------------------- apiVersion: cilium.io/v2 kind: CiliumClusterwideEnvoyConfig metadata: name: oauth2-forward-auth labels: app.kubernetes.io/name: oauth2-proxy app.kubernetes.io/component: forward-auth-enforcement spec: # Services Cilium must resolve to EDS endpoints for the clusters below. # oauth2-proxy = the ext_authz backend; the rest = protected upstreams. backendServices: - name: oauth2-proxy namespace: oauth2-proxy number: ["80"] - name: pihole # PROOF target (docs/13 row 6, auth.mode=forward-auth) namespace: pihole number: ["80"] # Bind this listener to a dedicated LoadBalancer Service (its own L2 VIP). # The Service is a PM deploy step (see header) so this CEC stays inert on merge. services: - name: oauth2-protected-ingress namespace: gateway listener: oauth2-forward-auth-listener ports: [443] resources: # --- Listener: TLS-terminated HTTPS with the ext_authz HTTP filter --------- - "@type": type.googleapis.com/envoy.config.listener.v3.Listener name: oauth2-forward-auth-listener listenerFilters: - name: envoy.filters.listener.tls_inspector typedConfig: "@type": type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector filterChains: - filterChainMatch: transportProtocol: tls serverNames: - "*.home.keithjackson.dev" transportSocket: name: envoy.transport_sockets.tls typedConfig: "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext commonTlsContext: tlsCertificateSdsSecretConfigs: # SAME wildcard cert the shared Gateway already syncs to SDS. - name: cilium-secrets/gateway-wildcard-home-tls filters: - name: envoy.filters.network.http_connection_manager typedConfig: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager statPrefix: forward-auth useRemoteAddress: true upgradeConfigs: - upgradeType: websocket rds: routeConfigName: forward-auth httpFilters: # ---- THE ENFORCEMENT FILTER (verified stanza) -------------- - name: envoy.filters.http.ext_authz typedConfig: "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz transportApiVersion: V3 # HARD FAIL CLOSED: if oauth2-proxy is down or errors, DENY # (never fail open onto an unauthenticated UI — R-IDP-02). failureModeAllow: false httpService: serverUri: uri: http://oauth2-proxy.oauth2-proxy.svc.cluster.local cluster: oauth2-proxy:oauth2-proxy:80 timeout: 5s # Prepended to the request path -> /oauth2/auth. # Per-route group differentiation: rely on oauth2-proxy's # global `--allowed-groups=platform-admins` floor # (helmrelease.yaml). A future non-admin group would use a # dedicated listener rather than a query on path_prefix. pathPrefix: /oauth2/auth authorizationRequest: allowedHeaders: patterns: - exact: cookie # the oauth2-proxy session - exact: authorization - exact: x-forwarded-uri - exact: x-forwarded-host - exact: x-forwarded-proto authorizationResponse: # On ALLOW (202): forward identity to the upstream app. allowedUpstreamHeaders: patterns: - prefix: x-auth-request- - exact: authorization # On DENY: return the sign-in redirect/cookie to browser. allowedClientHeaders: patterns: - exact: set-cookie - exact: location - exact: www-authenticate - name: envoy.filters.http.router typedConfig: "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router # --- Routes: one virtualHost per PROTECTED host (add Crafty etc. here) ------ - "@type": type.googleapis.com/envoy.config.route.v3.RouteConfiguration name: forward-auth virtualHosts: - name: pihole domains: - pihole.home.keithjackson.dev - pihole.home.keithjackson.dev:* routes: - match: prefix: / route: cluster: pihole:pihole:80 # --- Clusters --------------------------------------------------------------- - "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster name: oauth2-proxy:oauth2-proxy:80 # the ext_authz backend type: EDS connectTimeout: 5s edsClusterConfig: serviceName: oauth2-proxy/oauth2-proxy:80 - "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster name: pihole:pihole:80 # the protected upstream type: EDS connectTimeout: 5s edsClusterConfig: serviceName: pihole/pihole:80