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)
issclaim matches the issuer'sissuer_urlaudclaim matches the issuer'saudience(if configured)expclaim 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:
| Profile | org | service | env | action | branch | actor | groups |
|---|---|---|---|---|---|---|---|
github-actions | repository_owner | repository | environment | workflow_ref | ref | actor | -- |
kubernetes | namespace | service_account | -- | -- | -- | -- | groups |
gcp | project_id | email | -- | -- | -- | email | -- |
aws | account | role_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:
- CEL execution: The
ruleexpression 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, withref.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
- Identity fields (verified from JWT):
- Effect: If the rule returns
true, the policy'seffect(allow/deny) applies. Iffalse, evaluation continues to the next policy. - 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 responseshort_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.