OIDC Authentication for Directory
Directory supports an optional oidc-gateway authentication layer for users, automation, and workloads that access the API from outside the cluster. This keeps external access standards-based while preserving SPIFFE/SPIRE as the primary trust model for in-cluster workloads and service-to-service communication.
At a high level:
- Directory is OIDC IdP agnostic for external access.
Envoyandext_authzform the authentication and authorization layer at the edge.Dexis one useful deployment pattern, not a requirement.oidc-gatewayv1.1.1 accepts OIDC JWT, SPIFFE JWT-SVID, and SPIFFE X.509-SVID identities, and can expose OIDC/JWT and X.509-SVID mTLS traffic on separate hostnames from one gateway deployment.- Internal backend trust can remain SPIFFE-based even when external callers use OIDC bearer tokens.
Why Use OIDC
The default Directory deployment model is a strong fit for in-cluster workloads that already use SPIFFE/SPIRE. OIDC becomes useful when you also need authenticated access from outside the cluster, for example:
- Developers using
dirctlfrom a laptop or workstation. - Service users and scheduled jobs running outside Kubernetes.
- CI workflows such as GitHub Actions.
- Enterprise users authenticating through an existing IdP.
This gateway layer is optional. If you only need in-cluster, SPIFFE-native access, you do not need to enable it.
What This Is Not
To avoid confusion with other documentation in this section:
- This page is about external OIDC authentication for Directory API access.
- It is not the same as SPIRE OIDC discovery used in federation-related docs.
- It does not replace SPIFFE/SPIRE for internal workload trust.
External OIDC Compared to Internal SPIFFE Trust
These two layers solve different problems and should be described separately:
- OIDC handles caller identity for remote users and automation.
- SPIFFE/SPIRE handles workload identity and service trust inside the platform.
Directory does not need to choose one or the other globally. A common deployment pattern is:
- Remote callers authenticate with OIDC
- Envoy validates tokens and calls
ext_authz - Authorized requests are forwarded to the Directory API
- Backend services continue using SPIFFE-aware trust and identity
Architecture Overview
flowchart LR
humanUser["Human user with dirctl"] -->|"OIDC JWT"| oidcEndpoint["OIDC/JWT hostname"]
serviceUser["Service user or automation"] -->|"OIDC JWT or SPIFFE JWT-SVID"| oidcEndpoint
githubActions["GitHub Actions workload"] -->|"OIDC JWT"| oidcEndpoint
spiffeWorkload["SPIFFE workload"] -->|"X.509-SVID mTLS"| mtlsEndpoint["mTLS hostname"]
oidcIdp["OIDC provider or broker"] -->|"JWKS and issuer metadata"| envoyGateway["Envoy gateway"]
oidcEndpoint --> envoyGateway
mtlsEndpoint --> envoyGateway
envoyGateway -->|"Verified identity context"| extAuthz["ext_authz policy service"]
extAuthz -->|"Allow or deny"| envoyGateway
envoyGateway -->|"Authorized request + x-auth-principal"| directoryApi["Directory API"]
At the edge:
- A client presents an OIDC JWT, SPIFFE JWT-SVID, or SPIFFE X.509-SVID.
- Envoy validates bearer JWTs with
jwt_authnon the OIDC/JWT listener, or validates downstream SPIFFE mTLS on the mTLS listener. ext_authzmaps trusted identity data to a canonical principal and role policy.- Only authorized requests reach the Directory API.
With oidc-gateway v1.1.1, the recommended production shape is a single gateway deployment with two optional downstream endpoints:
envoy.endpoints.oidcplusingress.oidcfor human users, CI, and automation that present bearer JWTs.envoy.endpoints.mtlsplusingress.mtlsfor SPIFFE X.509-SVID clients. This listener typically requires a client certificate and does not run Envoyjwt_authn;ext_authzauthorizes the SPIFFE principal derived from the TLS session. The downstream TLS listener advertises HTTP/2 (h2) with ALPN for gRPC client compatibility.
This keeps token handling and policy enforcement at the edge rather than spreading it across clients and backend services.
v1.1.1 mTLS gRPC compatibility
oidc-gateway v1.1.1 fixes downstream gRPC interoperability for the SPIFFE X.509-SVID mTLS listener by advertising HTTP/2 (h2) with ALPN in Envoy's downstream TLS configuration. If mTLS clients fail during the TLS handshake with an error similar to credentials: cannot check peer: missing selected ALPN property, upgrade the gateway to v1.1.1 or verify that the rendered Envoy listener includes alpn_protocols: ["h2"].
Supported Identity Patterns
Human Interactive Login
Human users typically authenticate with dirctl auth login, using browser-based PKCE, headless login, or device flow. This is the best fit for operators and developers who need interactive CLI access to a remote Directory.
Use the CLI Reference for command examples and token cache behavior.
Service Users and Non-Interactive Automation
Service users, bots, and external automation can pass a pre-issued bearer token by flag or environment variable. This is useful when a machine identity already receives tokens from a trusted OIDC issuer and does not need an interactive login flow.
This pattern is a good fit for:
- Cron jobs
- External controllers
- MCP agents
- Service integrations running outside the cluster
GitHub Actions Workload Identity
GitHub Actions can present short-lived OIDC workload identity tokens instead of long-lived static credentials. This lets Directory authorize specific workflows without requiring personal access tokens or other long-lived secrets.
This should be treated as workload identity, not human login federation.
IdP-Agnostic by Design
Directory does not depend on a single identity provider. The OIDC layer is designed to work with standards-compliant providers as long as you configure trusted issuers, audiences, and claim mappings appropriately.
Supported IdPs include the following:
Directory trusts OIDC tokens at the edge through configuration and policy. It does not require a Directory-specific identity system.
Where Dex Fits in the Architecture
Dex is best understood as an optional broker or convenience layer, especially when you want a lightweight way to support human login and upstream federation.
Dex can be a good choice when you want:
- GitHub-backed login for human users.
- A simple OIDC facade in front of upstream identity sources.
- A lightweight IdP component for a demo, lab, or small deployment.
Dex is not required if you:
- Already have an enterprise OIDC provider.
- Want Directory to trust an existing IdP directly.
- Mainly need machine-to-machine or workload identity from another standard issuer.
Choosing Direct IdP Integration Versus Dex
Use direct IdP integration when:
- Your organization already standardizes on an OIDC provider.
- Want fewer identity components in the deployment.
- Want Directory to consume tokens issued by your enterprise platform directly.
Use Dex when you want:
- A broker in front of upstream identity systems.
- GitHub federation for human login.
- A simpler lab or reference deployment pattern that is easy to explain and reproduce.
In both cases, the Directory-facing model is the same: the edge validates tokens, ext_authz evaluates policy, and the Directory API receives only authorized requests.
Relationship to dirctl and Deployment Configuration
The two main operator touchpoints are:
- CLI Reference for
dirctl auth login,dirctl auth status,--auth-mode=oidc, and pre-issued token usage - deployment configuration for Envoy and the
ext_authzlayer that trusts one or more issuers and maps verified identities to principals and roles
When documenting or configuring this feature, it helps to think in three layers:
- Client behavior: how
dirctlor automation gets and sends tokens. - Edge trust: how Envoy validates JWTs from trusted issuers.
- Authorization policy: how
ext_authzmaps identities and enforces access.
oidc-gateway Configuration Walkthrough
The staging example in dir-staging/applications/oidc-gateway/dev/values.yaml is a good reference for how this model is wired in practice.
The public dir chart no longer embeds an envoy-authz add-on. Starting with the standalone oidc-gateway model, deploy the gateway as its own Helm application in front of the internal Directory API service.
The configuration is split into four main areas:
- Envoy edge behavior
ext_authzprincipal extraction and RBAC- SPIFFE and backend trust
- External ingress exposure
To configure oidc-gateway:
-
Configure Envoy as the Edge
The
envoyblock defines how Envoy fronts the internal Directory API:envoy: endpoints: oidc: enabled: true port: 8080 servicePort: 8080 downstreamTls: enabled: false requireClientCertificate: false mtls: enabled: true port: 8443 servicePort: 8443 downstreamTls: enabled: true requireClientCertificate: true backend: address: "dir-apiserver.dir.svc.cluster.local" port: 8888 streamingPath: "/agntcy.dir.events.v1.EventService/Listen" listenRouteTimeout: 0s oidc: issuers: - name: dex enabled: true issuer: "https://dex.example.com" jwksUri: "https://dex.example.com/keys" jwksHost: "dex.example.com" github: enabled: true issuer: "https://token.actions.githubusercontent.com" jwksUri: "https://token.actions.githubusercontent.com/.well-known/jwks" jwksHost: "token.actions.githubusercontent.com" audiences: - "dir" spiffe: enabled: true trustDomain: example.org className: dir-spireThis block does these important things:
endpoints.oidcexposes the listener that accepts OIDC JWT, GitHub OIDC, and SPIFFE JWT-SVID bearer tokens.endpoints.mtlsexposes the listener that accepts SPIFFE X.509-SVID client certificates over downstream mTLS. In v1.1.1, downstream TLS listeners advertise HTTP/2 (h2) with ALPN for gRPC clients.backend.*points Envoy at the internal Kubernetes Service for the Directory API, not at either public ingress hostname.oidc.issuers[]creates Envoyjwt_authnproviders and JWKS clusters for bearer JWT validation.oidc.github.*enables JWT validation for GitHub Actions workload identity tokens.spiffe.*keeps Envoy-to-Directory traffic anchored in SPIFFE/SPIRE-based service trust.
If you use a different IdP such as Zitadel, Keycloak, Auth0, Okta, or Entra ID, replace the Dex issuer and JWKS values with the corresponding issuer metadata for that provider.
-
Configure
ext_authzPrincipal ExtractionAfter Envoy validates a bearer token, it forwards the verified JWT payload to the authorization server. The
authServer.oidcblock controls howext_authzinterprets that payload and turns it into a canonical principal:authServer: oidc: claims: principalClaim: "sub" emailClaimPath: "email" headers: authPrincipal: "x-auth-principal" issuers: - providerKey: "dex" provider: "https://dex.example.com" authFamily: "oidc" - providerKey: "github" provider: "https://token.actions.githubusercontent.com" authFamily: "oidc" denyList: []This is where you define the trust boundary for authorization:
claims.principalClaimselects which claim becomes the default OIDC principal value.subis the safest generic default.claims.emailClaimPathtellsext_authzwhere to read email-like identity metadata when present.headers.authPrincipalsets the upstream header that carries the canonical principal to Directory. The default isx-auth-principal.issuers[].providermust match the tokenissvalue.issuers[].providerKeyis the stable short name used in policy strings such asoidc:dex:alice.issuers[].authFamilyis usuallyoidc. Usespiffefor SPIFFE JWT-SVID issuers.denyListblocks specific canonical principals or email claim values before RBAC evaluation.
Envoy strips client-supplied values for the configured principal header before forwarding requests, so clients cannot spoof
x-auth-principal. -
Use Canonical Principal Strings in Roles
The
rolessection is where the high-level access model becomes concrete:roles: admin: allowedMethods: ["*"] principals: - "oidc:dex:alice" viewer: allowedMethods: - "/agntcy.dir.store.v1.StoreService/Pull" - "/agntcy.dir.search.v1.SearchService/SearchRecords" principals: - "oidc:dex:reader-service" ci-writer: allowedMethods: - "/agntcy.dir.store.v1.StoreService/Push" - "/agntcy.dir.search.v1.SearchService/SearchRecords" principals: - "oidc:github:repo:your-org/your-repo:workflow:import-records.yaml:ref:refs/heads/main"Supported principal forms include:
oidc:<providerKey>:<principal>oidc:github:repo:<owner>/<repo>:workflow:<workflow-file>:ref:<git-ref>spiffe:spiffe://<trust-domain>/<path>
GitHub workflow wildcard matching is intentionally strict. A wildcard may contain exactly one
*, it must be the final character, and it is only supported in the branch ref segment, for example:oidc:github:repo:your-org/your-repo:workflow:deploy.yaml:ref:refs/heads/release-*Practical guidance:
- keep roles least-privilege
- grant write methods only where needed
- list specific GitHub workflow principals rather than trusting all workflows
- prefer explicit principals over broad catch-all mappings
-
Expose the Gateway Externally
The
ingressblock controls whether each gateway endpoint is reachable from outside the cluster:ingress: oidc: enabled: true className: nginx host: "gateway.example.com" annotations: nginx.ingress.kubernetes.io/backend-protocol: "GRPC" nginx.ingress.kubernetes.io/grpc-backend: "true" tls: enabled: true secretName: "" mtls: enabled: true className: nginx-internal host: "gateway-mtls.example.com" annotations: nginx.ingress.kubernetes.io/ssl-passthrough: "true" nginx.ingress.kubernetes.io/backend-protocol: "GRPCS" tls: enabled: true secretName: ""ingress.oidc.hostis the hostname that remotedirctlusers, SDK clients, or CI workflows target when they present bearer JWTs.ingress.mtls.hostis the separate hostname for SPIFFE X.509-SVID callers; use an ingress controller/class that supports TLS passthrough so Envoy, not the ingress controller, terminates mTLS and sees the client certificate.In TLS-passthrough mode,
tls.secretNameis intentionally empty. The ingress object still advertises the TLS host for SNI routing, but the certificate is provided by Envoy via SPIFFE/SPIRE instead of by an ingress TLS secret.
Recommended Mental Model for the YAML
When reading or editing the staging values, use this sequence:
envoy.backend: where should authorized requests go?envoy.oidc.*: which bearer-token issuers can Envoy validate?envoy.endpoints.*: which downstream listener(s) and Service ports are exposed?envoy.spiffe.*: how does Envoy authenticate to Directory and, optionally, accept SPIFFE X.509-SVID clients?authServer.oidc.issuers: how doesext_authzmap verified issuers to canonical principal prefixes?authServer.oidc.roles: which canonical principals can call which methods?ingress.oidc/ingress.mtls: how do external clients reach the correct gateway listener?
That sequence mirrors the actual request path:
Client identity -> Envoy validation -> ext_authz canonical principal mapping -> role check -> Directory API.
Further Reading
- CLI Reference for command-level usage
- Quickstart and Deploy for deployment entry points
- Production Deployment for production topology and external endpoints
- Running a Federated Directory Instance for network federation guidance