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
Lifecycle ladder
Section titled “Lifecycle ladder”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/readinessDrafts and failed sources do not consume an automated-source capacity slot; active and activating sources do.
Prerequisites
Section titled “Prerequisites”- 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 enrollwas added in v0.2.1; the current signed release is v0.2.4. The v0.1 CLI did not includeenroll— older documentation listingenrollas 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:
export AWS_PROFILE='your-aws-profile'export AWS_REGION=us-east-1export 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:
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:
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-apiWhere every entered value comes from
Section titled “Where every entered value comes from”| 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 JWKSDisplay name: Payments production workloadsIssuer: https://oidc.eks.us-east-1.amazonaws.com/id/EXAMPLEJWKS URI: https://oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE/keysAudience: trustplane-enrollmentThen walk the source up the lifecycle ladder:
- Choose the environment, fleet, and exact Auth Site that will receive the enrolled keys.
- Create the source draft with the public issuer/JWKS coordinates.
- 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.
- Confirm validation succeeded, a revision and safe fingerprint are reported, and the detected verifier scope matches the intended cluster.
- 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.
Step 3 — Create the enrollment policy
Section titled “Step 3 — Create the enrollment policy”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 authorizationDo 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.
Step 5 — Configure the workload proof
Section titled “Step 5 — Configure the workload proof”Mount a projected ServiceAccount token into the workload. The token
audience must equal the Trust Anchor Source audience.
apiVersion: v1kind: Podmetadata: name: payments-api namespace: paymentsspec: 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: tokenKubernetes 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.
Step 6 — Enroll the workload key
Section titled “Step 6 — Enroll the workload key”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.
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=5mFlag notes (verified against the v0.2.4 CLI):
--policy(alias--enrollment-policy) takes the opaque Enrollment Policy reference.--providerdefaults tooidc_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 withset_exactly_one_private_key_file_or_out. --proof-filereads the proof from the projected token path above. Alternatively,--kubernetes-token-filedefaults to the standard projected ServiceAccount token path — useful when the token is mounted at the Kubernetes default location.--wait-for-activationdefaults totrue: the command polls until the derived key is active or--timeoutelapses. Tune polling with--poll-interval.--submit-retriesbounds proof-submission retries (default 2, maximum 5).--refresh-skewsupports re-enrolling before expiry;--force-reenrollforces 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.
Derived-key lifetime
Section titled “Derived-key lifetime”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.
Step 7 — Verify runtime propagation
Section titled “Step 7 — Verify runtime propagation”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: activeKey-level route grant: activeSigning profile: activeSelected target: acknowledgedOnly 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 responseGET /api/orders → sign with the same enrolled key → expect denial if the client was granted only /api/customersThe private key stays in the workload; create and execute the signed request inside the workload boundary, and never print the generated passport to logs.
Verification checklist
Section titled “Verification checklist”| 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 |
Rollback and recovery boundary
Section titled “Rollback and recovery boundary”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.
Next steps
Section titled “Next steps”- Enrollment troubleshooting — diagnosis boundaries for stale policy references, symlinked token files, rate limiting, and propagation stalls.
- Trust Anchor source availability — which enrollment profiles are supported and which are not currently supported.
- Key lifecycle and compromise model — lifetime, rotation, and revocation semantics.
- Trust Anchors in Control — the console screen this runbook drives.
- Trust anchors (runtime bundle model) — the other trust plane: per-request route-source authorization.