Skip to main content

Edge OIDC through the gateway

DFE has two ways to authenticate a person. Local auth is the default and needs nothing: dfe-engine holds the accounts and signs its own tokens. The other is edge OIDC, where Envoy Gateway takes the login, verifies the token, and hands dfe-engine an already-authenticated identity in X-Oidc-* headers.

This page covers the second one, and what Envoy Gateway can't do today. Read it before turning jwtAuthn.enabled on. edge.md says which routes take an edge policy at all.

The values that turn OIDC on​

There's no single on/off switch, and deliberately no auth.oidcEnabled. The engine reads whatever provider YAML is under <config.mountPath>/auth/oidc-providers.

chartvaluewhat it decides
dfe-engineauthConfig.providersConfigMapwhich ConfigMap of provider definitions is seeded into the engine's auth directory
dfe-engineoidc.enabled + oidc.providerswhich client credentials are mounted as env from which Secrets
dfe-engineauth.trustProxyHeaderswhether the engine believes X-Oidc-* on an inbound request
dfe-engineauth.proxyProviderthe provider a gateway login is stamped with. Its X-Oidc-Groups link only to groups linked to that provider, so group files naming dex need dex here
envoy-gateway-configoidc.enabled + oidc.providers + oidc.targetRouteswhich routes get an OIDC SecurityPolicy, and against which IdP

oidc.targetRoutes names dfe-engine, and the engine's PUBLIC route follows it. So an edge policy on a cloud deploy also fronts GET /api/v1/auth/oidc/{provider}/callback, which is where it collides with the engine's own external-IdP login (edge.md).

A private-CA IdP is not discoverable​

Most deployments host their own IdP, because the gateway chart issues the whole wildcard from dfe-internal-ca. Point a SecurityPolicy at one with provider.issuer alone and admission rejects it:

OIDC: Get "https://auth.example.com/.well-known/openid-configuration":
tls: failed to verify certificate: x509: certificate signed by unknown authority

Envoy Gateway's CONTROL PLANE does that fetch, against the system trust store. The SecurityPolicy CRD has no field anywhere that takes a CA bundle. provider.backendSettings is cluster settings only (timeouts, retries, health checks, load balancing) with no TLS block in it.

So skip discovery. Name both endpoints, and send the token exchange to the IdP's Service over the pod network:

oidc:
enabled: true
providers:
- name: dex
issuerUrl: https://auth.example.com # must equal the token `iss`
clientId: dfe
authorizationEndpoint: https://auth.example.com/auth
tokenEndpoint: https://auth.example.com/token
backendRefs:
- name: dex
namespace: dfe-local
port: 5556

The authorization endpoint stays the public URL, because the browser goes there, not Envoy. Envoy calls the token endpoint, which is why that one needs the backendRefs alongside it. A backendRefs entry outside the target route's namespace needs a ReferenceGrant in the IdP's namespace.

If the IdP Service itself speaks HTTPS with a private CA, the CA goes on a BackendTLSPolicy targeting that Service, not on the SecurityPolicy.

X-Oidc-Groups can't be minted​

SecurityPolicy.spec.jwt.providers[].claimToHeaders states its own limit: The claim must be of type; string, int, double, bool. Array type claims are not supported.

groups is an array in every OIDC provider worth testing against, and no Envoy Gateway feature flattens one into a header. A patch policy doesn't change what the filter accepts.

So the engine receives X-Oidc-Subject with no X-Oidc-Groups. _resolve_group_grants has no groups to map, no linked group grants a role, and the engine refuses the request with 401 and creates no account:

{"code": "no_role_granted", "message": "OIDC login refused: no DFE group with a role is linked to your identity provider groups. Ask a DFE administrator to link one."}

The way out is for the engine to read the ID token itself, instead of trusting a header the gateway can't produce. Until that exists, edge OIDC admits only an account an administrator has added by hand to a group that carries a role. That account then holds that group's roles, the same as a token sign-in would.

The Envoy Gateway version pinned in versions.yaml (envoy-gateway) has two more limitations in the same path:

  • The EnvoyPatchPolicy's jwt_authn filter takes the token from an Authorization: Bearer header. An interactive OIDC login leaves the browser holding a cookie, not a bearer header.
  • auth.trustProxyHeaders is a per-ENGINE flag, while the header stripping that makes X-Oidc-* trustworthy is per-gateway-LISTENER. One engine deployment serving both a policy-fronted route and a plain one can't trust the headers on one and not the other. Set the flag and the headers are spoofable everywhere that engine is reachable.

Argo CD's own login​

Argo CD signs in through the provider the gateway already fronts it with. bootstrap reads the gateway's dfe-oidc-argocd-admin SecurityPolicy, and bootstrap/argocd_login.py turns it into Argo's oidc.config: the same issuer and client id, and clientSecret: $<secret>:client-secret, a reference to the Secret that policy reads. The value never leaves that Secret. Dex is turned off.

argocd-rbac-cm maps dfe-admins and dfe-infra to role:admin and dfe-infra-viewers to role:readonly, the engine's own names (dfe-engine docs/control-plane/rbac-vocabulary.md). dfe-infra is Argo's admin because its engine role, infra_admin, already carries argo:*. Any other group signs in and sees nothing. The admin pair is adminGroups in argocd/values/common.yaml, and scripts/tests/test_argocd_login.py fails when the two differ. If a deployment changes adminGroups in its own overlay, Argo's admins don't change.

  • No policy, no SSO. With no provider, or exposure.groups.admin.enabled: false, the gateway renders no policy for the route and Argo keeps its local admin. The access summary says which one a deployment got.
  • It's read at bootstrap time. On a first bootstrap the gateway syncs after Argo is installed, so the login is read again once the readiness gate passes. A provider added later reaches Argo on the next bootstrap run.
  • The IdP client allows https://argocd.<domain>/auth/callback, beside the gateway's own /oauth2/callback.
  • The edge still decides who reaches Argo. A dfe-infra-viewers user needs that group in adminGroups as well, or the gateway refuses them first. That list is also Kafbat's admin role, so a deployment admitting viewers at the edge names Kafbat's admins itself, in rbac.roles.admin.subjects of its deploy repo's infra/kafbat.yaml.
  • A private-CA IdP on the deployment's own domain is verified against the CA that signed the gateway's certificate, set as Argo's rootCA. A public IdP keeps the system roots. The issuer hostname must resolve inside the cluster, as below.
  • An adopted Argo is never reconfigured.

Reaching your own gateway from inside the cluster​

A pod that has to use the deployment's public hostname rather than a Service name (the engine fetching an IdP's discovery document, for one) needs two things that are easy to miss.

Egress on the listener port. network-policies ships allow-gateway-egress for this. The baseline policy permits 443 to anywhere, which reads as "pods may reach the gateway", and they can't. The Envoy Service maps 443 to container port 10443, and NetworkPolicy is evaluated after DNAT, so the packet is judged against 10443 and dropped. Refused-vs-timeout is the tell: 443 straight at the proxy pod is refused by the kernel, 10443 never arrives.

A name that resolves, to an address that hairpins. The deployment owns this one, not DFE. Its own hostnames have to resolve in-cluster, to an address a pod can actually connect to:

  • With a cloud DNS provider configured, external-dns publishes the records and in-cluster resolution follows the public ones.
  • On a private domain with no cloud DNS, the cluster needs a split-horizon answer for the deployment's domain. That's a cluster-DNS change (a CoreDNS zone or drop-in, whose mechanism differs per distribution), so it belongs in the deployment's own infrastructure config, not in a DFE chart.
  • Answer with the gateway Service's ClusterIP, NOT its LoadBalancer VIP. Many on-prem load balancers don't hairpin, so a pod that resolves the VIP connects and then times out. Set envoyGateway.service deliberately if the deployment needs a stable name for that record.

SNI and Host stay the public name either way, so Envoy routes on the HTTPRoute hostname and serves the wildcard certificate as normal.

  • README.md - the deploy layers and the values cascade
  • helm/edge/gateway/values.yaml - the oidc and jwtAuthn keys
  • helm/charts/dfe-extras/values.yaml - the engine's auth and authConfig defaults, under extras.defaults.dfe-engine