Identity — OIDC, JWKS, mTLS, SPIFFE, API keys
Configure inbound identity. The gateway verifies JWT bearers natively; richer or chained identity loads as plugins.
Identity is the first decision MCPG makes on every request. There are two surfaces:
- Built-in inbound verification under
governance.access— verifies JWT bearer tokens against an IdP's JWKS (live discovery or static keys). No plugin required. - Identity plugins loaded under
plugins[]withclass: identity_provider— for mTLS, SPIFFE workloads, API keys, basic auth, and chained resolution.
The identity plugins
| Plugin id | When to use |
|---|---|
dev.mcpg.identity.api-key | Internal services, simple bootstrap, no IdP |
dev.mcpg.identity.basic | Legacy systems migrating from htpasswd |
dev.mcpg.identity.mtls | Service-to-service with X.509 certs from upstream TLS termination |
dev.mcpg.identity.oidc | Human users with Google / Okta / Auth0 / Entra |
dev.mcpg.identity.workload | Kubernetes / SPIFFE workloads with X.509-SVID + JWT-SVID |
dev.mcpg.identity.ldap | Enterprise directory auth — Basic credentials verified against LDAP / Active Directory, groups → roles |
dev.mcpg.identity.kerberos | Windows / enterprise SSO — Authorization: Negotiate (Kerberos / SPNEGO) verified against a service keytab |
dev.mcpg.identity.saml | Enterprise SSO — a SAML 2.0 assertion whose IdP signature is verified (XSW-resistant) |
Identity plugins resolve in chain order. The first plugin to resolve an identity wins; the rest short-circuit.
Built-in OIDC verification (Google, Okta, Auth0, Entra)
The simplest path needs no plugin — point governance.access.oidc_oauth at your IdP:
governance:
access:
oidc_oauth:
providers:
- issuer: "https://accounts.google.com"
audiences: ["mcpg-prod"]
verification:
kind: oidc_jwks
allowed_algs: ["RS256"]
The verifier pulls JWKS via discovery on first use, refreshes on an interval, and
validates JWT bearer tokens on every request. SSRF guards block private/loopback issuer
URLs unless allow_private_issuer: true (dev only). Add more providers[] entries to
accept tokens from multiple issuers.
Static JWKS (air-gapped)
When the gateway can't reach the IdP's discovery endpoint, pre-stage the JWK Set:
governance:
access:
jwks:
keys_json: '{"keys":[ ... your IdP JWK Set ... ]}'
issuer: "https://idp.internal"
audience: "mcpg-prod"
No discovery call is made — the gateway validates against the supplied keys directly.
mTLS
When MCPG sits behind a TLS-terminating proxy that injects client cert info as headers, load the mTLS identity plugin:
plugins:
- id: dev.mcpg.identity.mtls
class: identity_provider
source:
oci: "ghcr.io/mcpg-dev/source-code/plugins/identity-mtls:1.0.0"
config:
sources:
- kind: forwarded_header
header_name: x-forwarded-client-cert
extraction:
mode: subject_cn
resolution:
trust_level: verified
auth_provider_label: mtls-front-door
For direct mTLS (no proxy), enable client-cert acceptance on the gateway listener itself
(gateway.server.tls.client_cert_required: mandatory); the plugin reads from the negotiated
session.
SPIFFE workload identity
For Kubernetes / SPIRE deployments, load the workload identity plugin:
plugins:
- id: dev.mcpg.identity.workload
class: identity_provider
source:
oci: "ghcr.io/mcpg-dev/source-code/plugins/identity-workload:1.0.0"
config:
trustDomain: example.org
bundle:
kind: workload_api
socketPath: unix:/run/spire/agent.sock
sources:
- kind: x509_svid
- kind: jwt_svid_bearer
audiences:
- mcpg-prod
The plugin streams from the SPIRE Workload API, hot-reloads trust bundles, and stamps each
request with the resolved SPIFFE ID + selectors. Policy plugins downstream can then
authorize on spiffe://example.org/ns/prod/sa/payments.
API key
Simplest option for internal services:
plugins:
- id: dev.mcpg.identity.api-key
class: identity_provider
source:
oci: "ghcr.io/mcpg-dev/source-code/plugins/identity-api-key:1.0.0"
config:
token_sources:
- kind: header
header_name: x-api-key
keys:
- id: alice
secret: "${env.ALICE_API_KEY}"
attributes:
team: platform
resolution:
trust_level: verified
auth_provider_label: api-key
Secrets are resolved through the gateway's secret-resolver pre-walk (here from an env var)
and compared in constant time. Pull the literal secret out of YAML with a secret-provider
plugin or ${env.X} rather than committing it.
LDAP / Active Directory
Verify Authorization: Basic credentials against a directory by binding as the
caller — the directory checks the password — and map the caller's groups to roles.
The Active Directory case uses search-then-bind (look the user up by sAMAccountName,
then re-bind as their DN):
plugins:
- id: dev.mcpg.identity.ldap
class: identity_provider
source:
oci: "ghcr.io/mcpg-dev/source-code/plugins/identity-ldap:1.0.0"
config:
url: "ldaps://dc1.corp.example.com:636"
bind:
mode: search
bind_dn: "cn=svc-mcpg,ou=svc,dc=corp,dc=example,dc=com"
bind_password: "${env.LDAP_SVC_PASSWORD}"
base_dn: "ou=people,dc=corp,dc=example,dc=com"
user_filter: "(sAMAccountName={username})" # {username} is RFC 4515 filter-escaped
subject_attribute: sAMAccountName
group_attribute: memberOf # group DNs → groups, their CNs → roles
attributes: [mail, displayName]
For a directory with predictable DNs, skip the service account with mode: direct:
bind:
mode: direct
user_dn_template: "uid={username},ou=people,dc=example,dc=org" # {username} is RFC 4514 DN-escaped
The caller's password is used only for the bind and is never logged; the service-account
password resolves through the gateway secret-resolver. A successful bind is verified
trust. A wrong password and an unknown user both return the same generic rejection (no user
enumeration), and an unreachable directory fails closed (rejected, never passed through).
This is the directory-backed counterpart to dev.mcpg.identity.basic (which checks Basic
credentials against a static hash registry) and pairs with the dev.mcpg.backend.ldap
backend (which searches the directory with a service account).
Kerberos / SPNEGO (Negotiate)
Windows-integrated / enterprise SSO. The caller (browser or service) presents
an Authorization: Negotiate GSSAPI token; the plugin verifies it against the
gateway's service keytab and resolves the Kerberos principal. Verification
is local (the keytab decrypts the service ticket) — no call to the KDC.
plugins:
- id: dev.mcpg.identity.kerberos
class: identity_provider
source:
oci: "ghcr.io/mcpg-dev/source-code/plugins/identity-kerberos:1.0.0"
config:
keytab: "${env.MCPG_KEYTAB}" # path to the service keytab
# service_name: "HTTP@gateway.corp.example.com" # optional
strip_realm: true # alice@CORP → subject "alice"
Builds against the system GSSAPI (
libkrb5); the plugin image ships it. Active Directory group membership (the ticket PAC) is a follow-on — pair with an LDAP lookup for groups in the meantime.
SAML 2.0
Enterprise SSO via signed SAML assertions. A SAML SP front-end (or proxy) terminates the Web SSO POST and passes the assertion to mcpg in a header; the plugin verifies the IdP's XML signature against the configured certificate, enforces signature-wrapping defenses, validates conditions + audience, and maps the subject + attributes:
plugins:
- id: dev.mcpg.identity.saml
class: identity_provider
source:
oci: "ghcr.io/mcpg-dev/source-code/plugins/identity-saml:1.0.0"
config:
idp_certificate: "${file:///etc/mcpg/idp.crt}" # PEM — the trust anchor
idp_entity_id: "https://idp.corp.example.com/saml2"
audience: "mcpg-gateway"
role_attribute: "http://schemas.example.com/role" # → roles
group_attribute: "memberOf" # → groups
# assertion_header: X-SAML-Assertion # default
The trust anchor is your configured IdP certificate — never the one embedded in the message. The plugin is XSW-resistant (exactly one assertion, a direct-child signature that covers that assertion's id) and rejects anything outside the standard signing profile (exclusive-C14N, enveloped, RSA-SHA256). Verification uses libxml2 for canonicalization + pure-Rust RSA — no OpenSSL.
Chaining
When you have multiple paths to identity, list the plugins in priority order — the first to resolve wins:
plugins:
- id: dev.mcpg.identity.workload # try SPIFFE first (service-to-service)
class: identity_provider
source:
oci: "ghcr.io/mcpg-dev/source-code/plugins/identity-workload:1.0.0"
config: { trustDomain: example.org }
- id: dev.mcpg.identity.api-key # fall back to API key (CI bots)
class: identity_provider
source:
oci: "ghcr.io/mcpg-dev/source-code/plugins/identity-api-key:1.0.0"
config:
token_sources: [{ kind: header, header_name: x-api-key }]
keys: [{ id: ci-bot, secret: "${env.CI_BOT_KEY}" }]
Order matters: the first plugin to return a resolved identity wins. Built-in
governance.access verification runs on the bearer-token path independently of this chain.
After identity: policy
Identity resolution produces a caller with subject_id, attributes, roles, and
groups. Policy consumes this. See the policy guide next.