Core Concepts

The three pipeline stages -- JWT verification, policy evaluation, and secret resolution

Every resolve request flows through three stages. Each stage must pass before the next runs. If no policy explicitly allows, the request is denied.

Stage 1: JWT Verification + Identity Normalization

The caller presents a JWT in the Authorization: Bearer <token> header. KeyZero validates it against configured issuers and normalizes the verified claims into standard identity fields.

issuers:
  - name: github
    type: github-actions
    issuer_url: "https://token.actions.githubusercontent.com"
    audience: "keyzero"

  - name: k8s
    type: kubernetes
    issuer_url: "https://kubernetes.default.svc"
    audience: "keyzero"

  - name: internal
    type: custom
    issuer_url: "https://auth.internal"
    audience: "keyzero"
    map:
      org: "claims.tenant_id"
      service: "claims.service_name"
      env: "claims.environment"

What gets validated:

  • Signature (via static key or JWKS)
  • iss claim matches the issuer's issuer_url
  • aud claim matches the issuer's audience (if configured)
  • exp claim is in the future
  • Algorithm matches between token header and configured key

Key matching: If the token has a kid header, only keys with a matching kid are tried. JWKS keys are fetched and cached automatically.

Multi-issuer: Issuers are tried in order. The first one whose key matches and validates the token wins.

Supported algorithms: RS256, RS384, RS512, ES256, ES384, PS256, PS384, PS512, HS256, HS384, HS512.

Built-in Issuer Profiles

The type field selects a built-in claim mapping profile that automatically normalizes JWT claims into standard identity fields:

Profileorgserviceenvactionbranchactorgroups
github-actionsrepository_ownerrepositoryenvironmentworkflow_refrefactor--
kubernetesnamespaceservice_account--------groups
gcpproject_idemail------email--
awsaccountrole_arn----------
custom(use map: overrides)

For example, with type: github-actions, the GitHub OIDC claim repository_owner is automatically available as org in CEL rules.

Custom issuers use type: custom with explicit map: overrides to define how JWT claims map to identity fields.

Stage 2: Policy Evaluation (CEL)

Policies are CEL (Common Expression Language) rules evaluated top-down, first-match-wins.

policies:
  - name: allow-ci-prod
    rule: "org == 'myorg' && action == 'deploy.yml' && ref.matches('secret/data/prod/**')"
    effect: allow

  - name: allow-dev-readonly
    rule: "env == 'dev' && ref.matches('secret/data/dev/**')"
    effect: allow

  - name: default-deny
    rule: "true"
    effect: deny

Evaluation flow:

  1. CEL execution: The rule expression is evaluated with the following variables:
    • Identity fields (verified from JWT): issuer, org, service, env, action, branch, actor, groups
    • Request fields: ref -- the requested secret path, with ref.matches('pattern/**') for glob matching
    • Raw JWT escape hatch: claims.* -- access any raw JWT claim (e.g., claims.repository)
    • Caller-declared fields (untrusted): context.* -- arbitrary key-value pairs sent by the caller
  2. Effect: If the rule returns true, the policy's effect (allow/deny) applies. If false, evaluation continues to the next policy.
  3. Implicit deny: If no policy matches, the request is denied with policy name default-deny.

Trust levels: Identity fields (org, service, env, etc.) are derived from a verified JWT and can be trusted. The context.* fields are caller-declared and untrusted -- use them for informational purposes or in combination with verified fields, never as the sole basis for an allow decision.

Glob matching: Use ref.matches('secret/data/prod/**') to match secret paths with glob patterns. This replaces the old match.resource_tags pre-filter.

See Writing Policies for practical examples.

Stage 3: Secret Resolution

If policy allows, the matching resource entry determines which backend resolves the secret.

resources:
  - ref: "secret/data/prod/**"
    resolver: production-vault
    mode: direct

  - ref: "secret/data/dev/**"
    resolver: dev-vault
    mode: direct
    ttl: 300

Resources use ref with glob patterns to match requested secret paths. Each resource entry has a single resolver (not an array).

Matching order: The most specific match wins. An exact path match takes priority over a narrow glob, which takes priority over a broad glob.

Two modes:

  • direct -- the secret value is fetched immediately and returned in the response
  • short_lived -- a short-lived JWT is signed and returned instead; the caller uses it to fetch the real secret through the credential-swapping proxy

See Resolution Modes for when to use each.