Security
Security6 min

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:

  1. Built-in inbound verification under governance.access — verifies JWT bearer tokens against an IdP's JWKS (live discovery or static keys). No plugin required.
  2. Identity plugins loaded under plugins[] with class: identity_provider — for mTLS, SPIFFE workloads, API keys, basic auth, and chained resolution.

The identity plugins

Plugin idWhen to use
dev.mcpg.identity.api-keyInternal services, simple bootstrap, no IdP
dev.mcpg.identity.basicLegacy systems migrating from htpasswd
dev.mcpg.identity.mtlsService-to-service with X.509 certs from upstream TLS termination
dev.mcpg.identity.oidcHuman users with Google / Okta / Auth0 / Entra
dev.mcpg.identity.workloadKubernetes / SPIFFE workloads with X.509-SVID + JWT-SVID
dev.mcpg.identity.ldapEnterprise directory auth — Basic credentials verified against LDAP / Active Directory, groups → roles
dev.mcpg.identity.kerberosWindows / enterprise SSO — Authorization: Negotiate (Kerberos / SPNEGO) verified against a service keytab
dev.mcpg.identity.samlEnterprise 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:

yaml
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:

yaml
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:

yaml
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:

yaml
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:

yaml
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):

yaml
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:

yaml
      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.

yaml
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:

yaml
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:

yaml
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.