Skip to content

OIDC JWKS enrollment runbook

This runbook covers the generic OIDC JWKS enrollment profile (--provider oidc_jwks) — the only Trust Anchor enrollment profile currently supported; other provider profiles are not currently supported. The documented configuration points the source at an Amazon EKS cluster OIDC issuer and uses a projected ServiceAccount token as the workload proof. EKS is the value source in this runbook: it supplies the issuer, the JWKS endpoint, and the verified subject; the Control-side source kind is generic OIDC JWKS throughout.

The two planes involved:

flowchart LR
  subgraph Enrollment plane
    W[Workload pod] -->|projected token proof| P[Control enrollment policy]
    P -->|derived client key| K[Derived key]
  end
  subgraph Request plane
    K -->|signed request| A[Auth adapter]
    A -->|route/source authorization| U[Upstream API]
  end

Every claim in this runbook sits on one rung of the trust-anchor lifecycle. “Selectable in the UI”, “valid configuration”, “active”, “derived key issued”, and “production-ready” are different claims — the steps below say which one each success state makes.

draft → validated → active → enrollment policy → enrollment → derived key
→ runtime propagation/readiness

Drafts and failed sources do not consume an automated-source capacity slot; active and activating sources do.

  • An Auth Site with an active, acknowledged target.
  • An existing client with its protected-resource grants already configured. The enrollment policy binds to this client; derived keys inherit its authorization.
  • A Kubernetes workload running under a dedicated ServiceAccount on a cluster with a public OIDC issuer (this runbook uses Amazon EKS as the example).
  • Public network reachability from Control to the cluster OIDC issuer and JWKS endpoint.
  • TrustPlane CLI v0.2.1 or later inside the workload. trustplane enroll was added in v0.2.1; the current signed release is v0.2.4. The v0.1 CLI did not include enroll — older documentation listing enroll as excluded describes the v0.1 local boundary.
  • Control console access with permission to manage Trust Anchor Sources and enrollment policies for the selected environment, fleet, and Auth Site (see Trust Anchors in Control).

Step 1 — Gather the OIDC issuer and JWKS values

Section titled “Step 1 — Gather the OIDC issuer and JWKS values”

For Amazon EKS, obtain the cluster OIDC issuer from public cluster metadata:

Terminal window
export AWS_PROFILE='your-aws-profile'
export AWS_REGION=us-east-1
export EKS_CLUSTER='your-cluster-name'
export OIDC_ISSUER="$(
aws eks describe-cluster \
--name "$EKS_CLUSTER" \
--region "$AWS_REGION" \
--query 'cluster.identity.oidc.issuer' \
--output text
)"
print -r -- "$OIDC_ISSUER"

For EKS, the JWKS endpoint is the issuer plus /keys:

Terminal window
export JWKS_URI="${OIDC_ISSUER}/keys"
curl -fsS "$JWKS_URI" | jq '{key_count:(.keys|length), key_ids:[.keys[].kid]}'

This reads public signing keys only and prints only a key count and key IDs. Amazon EKS exposes an OIDC issuer per cluster and serves the public keys for projected ServiceAccount tokens; the signing keys rotate, so TrustPlane refreshes JWKS metadata rather than treating it as static. See the AWS EKS OIDC issuer guidance and EKS JWKS guidance.

Choose an enrollment audience and identify the workload:

Terminal window
export KUBE_CONTEXT='your-kube-context'
export KUBE_NAMESPACE='payments'
export KUBE_SERVICE_ACCOUNT='payments-api'
export OIDC_AUDIENCE='trustplane-enrollment'
kubectl --context "$KUBE_CONTEXT" -n "$KUBE_NAMESPACE" \
get serviceaccount "$KUBE_SERVICE_ACCOUNT"
export VERIFIED_SUBJECT="system:serviceaccount:${KUBE_NAMESPACE}:${KUBE_SERVICE_ACCOUNT}"
print -r -- "$VERIFIED_SUBJECT"

Kubernetes ServiceAccount tokens use the subject format system:serviceaccount:<namespace>:<serviceaccount>, so for this workload the exact verified subject is:

system:serviceaccount:payments:payments-api
Value Where it comes from Entered where
Source kind Always OIDC JWKS for this runbook Control source form
Display name Operator-defined, e.g. Payments production workloads Control source form
Issuer aws eks describe-cluster output ($OIDC_ISSUER) Control source form
JWKS URI EKS issuer + /keys ($JWKS_URI) Control source form
Audience Chosen by the operator ($OIDC_AUDIENCE); must match the projected token’s audience Control source form and pod spec
Verified subject system:serviceaccount:<namespace>:<serviceaccount> Enrollment policy
Verified issuer Exact cluster OIDC issuer ($OIDC_ISSUER) Enrollment policy
Existing client An already-configured client in the same Auth Site Enrollment policy
Maximum key lifetime Operator-set value in seconds, within server limits Enrollment policy
Opaque policy reference Copied verbatim from the Enrollment Command Generator trustplane enroll --policy
Control URL Your Control environment’s API base URL, from the Enrollment Command Generator output trustplane enroll --control-url
Proof file path The projected token mountPath + path from the pod spec trustplane enroll --proof-file

Step 2 — Register the Trust Anchor Source

Section titled “Step 2 — Register the Trust Anchor Source”

In Control → Auth Site → Trust Anchors, create the source:

Source kind: OIDC JWKS
Display name: Payments production workloads
Issuer: https://oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE
JWKS URI: https://oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE/keys
Audience: trustplane-enrollment

Then walk the source up the lifecycle ladder:

  1. Choose the environment, fleet, and exact Auth Site that will receive the enrolled keys.
  2. Create the source draft with the public issuer/JWKS coordinates.
  3. Open that exact source and click Validate. Control discovers and validates the public metadata; the detected issuer scope and safe verification fingerprint are outputs of validation, not values you enter.
  4. Confirm validation succeeded, a revision and safe fingerprint are reported, and the detected verifier scope matches the intended cluster.
  5. Click Activate only after validation succeeds.

Expected success state: the source shows active, with a reported revision and safe fingerprint. This claim is “active source” — it does not yet mean any workload can enroll.

Expected safe failure states: a draft that fails validation stays a failed draft; it issues nothing, grants nothing, and does not consume an automated-source capacity slot. Fix the issuer/JWKS/audience values and re-validate, or delete the draft. Do not base an enrollment policy on an old draft or a manually copied source revision.

A source says who is allowed to prove workload identity. It grants no API access by itself — access comes from the client the enrollment policy selects.

The enrollment policy binds one exact validated source revision to one existing client:

exact Trust Anchor source revision
+ verified workload identity
+ existing client
= derived key with that client's existing authorization

Do not configure API routes, grants, scopes, or caller-selected identity in the enrollment policy — those remain client-scoped and are inherited by each derived key.

Policy field Value
Source The active OIDC JWKS source
Source revision The exact validated active revision
Existing client e.g. Payments API client
Approval Automatic for unattended workload enrollment, or manual review via Enrollment Approvals
Maximum key lifetime Value in seconds; bounds the derived signing-key lifetime
Verified subject system:serviceaccount:payments:payments-api
Verified issuer https://oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE

The verified subject and issuer form the exact verified identity binding: only a proof whose subject and issuer match exactly can enroll under this policy.

Expected success state: the policy exists against the active source revision and the selected client, with a maximum key lifetime in seconds.

Step 4 — Generate the enrollment command

Section titled “Step 4 — Generate the enrollment command”

After creating the policy, open the Enrollment Command Generator in Control and copy the generated command, including the opaque policy reference.

Mount a projected ServiceAccount token into the workload. The token audience must equal the Trust Anchor Source audience.

apiVersion: v1
kind: Pod
metadata:
name: payments-api
namespace: payments
spec:
serviceAccountName: payments-api
containers:
- name: app
image: example/payments-api:latest
volumeMounts:
- name: trustplane-oidc
mountPath: /var/run/secrets/trustplane-oidc
readOnly: true
volumes:
- name: trustplane-oidc
projected:
sources:
- serviceAccountToken:
audience: trustplane-enrollment
expirationSeconds: 3600
path: token

Kubernetes projected tokens are short-lived, audience-bound tokens rotated by kubelet. expirationSeconds defaults to one hour, must be at least 600 seconds, and may be capped by cluster configuration. See the Kubernetes projected ServiceAccount token documentation.

Projected token files are maintained by the Kubernetes Atomic Writer and may be symlinks. A compatible CLI (v0.2.1+) opens the configured path and validates the resolved file descriptor as a regular file, rather than rejecting the symlink itself.

Use a dedicated enrollment pod for validation

Section titled “Use a dedicated enrollment pod for validation”

When validating the flow rather than enrolling a production service, run enrollment from a dedicated, short-lived pod: a dedicated namespace and ServiceAccount, the enrollment CLI, a writable /work directory, the projected token mounted read-only, no long-lived cloud credentials, and a short lifecycle. Delete the pod (and revoke its derived key) when validation is complete. The token is proof material — do not cat, log, copy, or upload it.

Required for production deployment: a production workload does not use a disposable pod — the CLI (or an equivalent enrollment step) runs inside the workload boundary itself, the private key is written to storage only the workload can read, and re-enrollment is automated before expiry.

Inside the workload, run the generated command. The CLI creates (or reuses) a local Ed25519 private key with restrictive permissions, obtains an enrollment challenge from Control, submits the projected OIDC proof, polls with backoff, and caches non-secret enrollment state.

Terminal window
export TP_CONTROL_URL='https://control.example.trustplane.example/'
export TP_POLICY_REF='copy-the-exact-opaque-reference-from-the-Command-Generator'
export TP_PRIVATE_KEY='/var/lib/payments/trustplane-enrollment.key'
export TP_PROOF_FILE='/var/run/secrets/trustplane-oidc/token'
trustplane enroll \
--control-url "$TP_CONTROL_URL" \
--policy "$TP_POLICY_REF" \
--provider oidc_jwks \
--private-key-out "$TP_PRIVATE_KEY" \
--proof-file "$TP_PROOF_FILE" \
--timeout=5m

Flag notes (verified against the v0.2.4 CLI):

  • --policy (alias --enrollment-policy) takes the opaque Enrollment Policy reference.
  • --provider defaults to oidc_jwks, so it may be omitted here; it is shown for clarity.
  • Exactly one of --private-key-file (reuse an existing key) or --private-key-out (create a new key) is required; passing both or neither fails with set_exactly_one_private_key_file_or_out.
  • --proof-file reads the proof from the projected token path above. Alternatively, --kubernetes-token-file defaults to the standard projected ServiceAccount token path — useful when the token is mounted at the Kubernetes default location.
  • --wait-for-activation defaults to true: the command polls until the derived key is active or --timeout elapses. Tune polling with --poll-interval.
  • --submit-retries bounds proof-submission retries (default 2, maximum 5).
  • --refresh-skew supports re-enrolling before expiry; --force-reenroll forces a fresh enrollment even when the cached state looks current.

Expected success state: the command exits successfully and reports non-secret operational metadata only — the derived key’s active status, key ID, fingerprint, and expiration. The private key file is written with mode 0600 inside the workload. This claim is “derived key issued” — runtime readiness is verified in Step 7.

Expected safe failure states: enrollment errors are stable strings you can match on, for example control_url_and_policy_required (missing coordinates), set_exactly_one_private_key_file_or_out (key-file misuse), invalid_enroll_timing (inconsistent timing flags), and unsupported_enrollment_provider (a --provider value the server does not accept). A failed enrollment issues no key and can be retried after fixing the input. If a submission may have succeeded (timeout mid-poll, throttled poll), do not blindly re-submit — see Enrollment troubleshooting.

The application must never log or export the private key file, the projected token, the challenge, or the signed proof. Required for production deployment: do not copy the private key off the workload — not to an operator laptop, not into a ticket. Prefer non-exportable or tightly-permissioned key storage.

The workload does not choose the derived-key lifetime with a CLI flag. The enrollment-policy maximum key lifetime (seconds) bounds the derived signing-key lifetime, and in the current flow:

effective key expiry = earlier of (policy maximum key lifetime, proof token expiry)

trustplane sign --ttl is different: it bounds one signed request/passport, never the key. A 30-day policy maximum is not useful if the projected proof token is only valid for one hour under the current capped-lifetime model — for long-running workloads, automate re-enrollment before expiry (--refresh-skew), and revoke + re-enroll on compromise. See Key lifecycle and compromise model for the full semantics.

A derived key can exist before an Auth adapter has acknowledged the newest policy/bundle. Before starting request verification, confirm in Control:

  • target acknowledgment;
  • selected target readiness;
  • an active signing profile;
  • effective routes on the derived key;
  • the exact Auth Site bundle/target version and its last pull/reload.

Expected success state:

Derived key: active
Key-level route grant: active
Signing profile: active
Selected target: acknowledged

Only then begin signed-request verification. A key that is active in Control but not yet acknowledged by the adapter is a propagation state, not a failure — see Enrollment troubleshooting if it does not converge.

Step 8 — Verify inherited grants and sign a protected request

Section titled “Step 8 — Verify inherited grants and sign a protected request”

The derived key must inherit the selected client’s protected-resource grants — no more, no less. Obtain the canonical signing profile for the enrolled key (it supplies the route ID and signing coordinates), then sign from inside the workload with the derived private key. A signed request binds at least: key ID, issuer and subject, audience, route ID, HTTP method, scheme and authority, exact path and query, nonce/replay identifier, and the request body hash for body-bearing methods.

GET /api/customers
→ obtain active GET signing profile
→ trustplane sign with the enrolled private key
→ send from the workload
→ expect allowed response
GET /api/orders
→ sign with the same enrolled key
→ expect denial if the client was granted only /api/customers

The private key stays in the workload; create and execute the signed request inside the workload boundary, and never print the generated passport to logs.

Check Expected result
Valid EKS ServiceAccount proof Derived key is created (active, with key ID, fingerprint, expiration)
Client inheritance Exactly the client’s granted routes materialize on the derived key
Granted route, e.g. GET /api/customers Allowed
Write route Allowed only when the client has write access
Ungranted route, e.g. GET /api/orders Denied
Reused signed request Replay denial
Modified signed body Body-hash mismatch denial
Revoked derived key Denied
Expired derived key Denied
Source suspended or retired No new enrollments; behavior follows lifecycle policy

What you can safely undo, and where to stop:

  • Wrong source values before activation — fix the draft and re-validate, or delete it. Failed drafts hold no capacity and grant nothing.
  • Wrong policy binding — deactivate or delete the enrollment policy; existing derived keys are governed by their own status and expiry, so also revoke any key issued under the wrong binding.
  • Compromised or mistaken derived key — revoke the key in Control; the workload re-enrolls under the correct policy. Revocation is the recovery action; expiry is the backstop.
  • Validation cleanup — delete the dedicated enrollment pod and revoke its derived key; suspend or retire the source if the validation cluster is being torn down.
  • Stop and do not improvise when a derived key exists but its signing profile or routes do not materialize, or a source is stuck publishing: do not revoke and re-enroll as a workaround, and do not edit policy state to force convergence. Inspect state and follow Enrollment troubleshooting, escalating with the observed states.

Required for production deployment: before production use, also apply the deployment hardening in network hardening (default-deny upstream ingress, adapter egress restrictions, no direct-to-upstream bypass) and short key lifetimes with automated rotation. The enrollment flow is supported with the documented configuration; your production network boundary is a separate control that you must apply and verify yourself.