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 is the second one, and what Envoy Gateway cannot 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 is no single on/off switch and deliberately no auth.oidcEnabled: the
engine reads whatever provider YAML is under
<config.mountPath>/auth/oidc-providers. Five values say five separate things.
| chart | value | what it decides |
|---|---|---|
| dfe-engine | authConfig.providersConfigMap | which ConfigMap of provider definitions is seeded into the engine's auth directory |
| dfe-engine | oidc.enabled + oidc.providers | which client credentials are mounted as env from which Secrets |
| dfe-engine | auth.trustProxyHeaders | whether the engine believes X-Oidc-* on an inbound request |
| dfe-engine | auth.proxyProvider | the 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-config | oidc.enabled + oidc.providers + oidc.targetRoutes | which 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
An IdP the deployment hosts for itself is the common shape, because the gateway
chart issues the whole wildcard from dfe-internal-ca. Point a SecurityPolicy
at one with provider.issuer alone and it is rejected at admission:
OIDC: Get "https://auth.example.com/.well-known/openid-configuration":
tls: failed to verify certificate: x509: certificate signed by unknown authority
That fetch is done by Envoy Gateway's CONTROL PLANE, 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 -- the browser goes there, not
Envoy. The token endpoint is called by Envoy, which is why it 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 cannot 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. There is no
Envoy Gateway feature that flattens one, and inventing a workaround out of the
patch policy does not change what the filter accepts.
What that means in practice: the engine receives X-Oidc-Subject with no X-Oidc-Groups, so no linked group grants a role, and it 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."}
_resolve_group_grants maps the groups header to roles, and the header never arrives.
The way out is the engine reading the ID token itself rather than trusting a header the gateway cannot produce. Until that exists, edge OIDC admits only an account an administrator has added by hand to a group that carries a role, and that account then holds that group's roles, the same as a token sign-in would.
Two more things in the same path that do not meet at the Envoy Gateway this
repo pins (versions.yaml, envoy-gateway):
- The
EnvoyPatchPolicy'sjwt_authnfilter takes the token from anAuthorization: Bearerheader. An interactive OIDC login leaves the browser holding a cookie, not a bearer header. auth.trustProxyHeadersis a per-ENGINE flag, while the header stripping that makesX-Oidc-*trustworthy is per-gateway-LISTENER. One engine deployment serving both a policy-fronted route and a plain one cannot trust the headers on one and not the other, so setting the flag makes them spoofable everywhere that engine is reachable.
Argo CD's own login
The Argo CD bootstrap installs 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. A deployment that changes adminGroups in its own overlay does not change Argo's admins.
- 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 localadmin. The access summary says which one a deployment got. - 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-viewersuser needs that group inadminGroupsas 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, inrbac.roles.admin.subjectsof its deploy repo'sinfra/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 cannot: the Envoy Service
maps 443 to container port 10443, 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. This one the deployment owns, not DFE. The deployment's own hostnames have to resolve in-cluster, and they have to resolve 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 is 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 do not hairpin, so a pod that resolves the VIP connects
and then times out. Set
envoyGateway.servicedeliberately 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.
Related
- index.md - the deploy layers and the values cascade
helm/edge/gateway/values.yaml- theoidcandjwtAuthnkeyshelm/charts/dfe-extras/values.yaml- the engine'sauthandauthConfigdefaults, underextras.defaults.dfe-engine