Workload identity — give software a verifiable identity, no secrets to steal
What it is
A workload is a running piece of software — a service, a container, a CI job, an AI agent — that proves what it is to other services without a long-lived password or API key planted inside it, by combining attestation (cryptographic proof of what and where a workload is) with short-lived credentials issued only to workloads that pass it.
The mental model: instead of a permanent badge every employee might lose, you install a fingerprint scanner at each door. The workload carries no secret — it proves what it is at the moment it needs access and gets a pass that expires in minutes. This page covers the SPIFFE standard, trstctl's attestation chain, ephemeral issuance, the non-human identity lifecycle, and a purpose-built AI-agent broker.
Why it exists
The classic way to give a service access — bake an API key or certificate into it — is also the classic way to get breached: those secrets get copied into logs, images, git history, and laptops, and rarely expire. Attestation-based, short-lived identity removes the thing attackers steal: nothing long-lived to leak, and a captured credential is useless within minutes. This is the foundation of "zero-trust" service-to-service security, and it matters even more for AI agents, which spin up fast, act with real privileges, and need tight, revocable scopes.
How it works
The attestation chain (F30) — proof before trust
Everything here rests on attestation: before issuing anything, trstctl demands and
verifies proof of the workload's identity. The framework is pluggable — an Attestor
verifies one kind of proof — and trstctl ships six:
- TPM 2.0 quote — verifies a hardware TPM's endorsement chain back to the manufacturer root, plus a signed quote bound to a fresh nonce.
- AWS IMDSv2 — verifies the PKCS#7-signed EC2 instance identity document against the AWS root.
- GCP metadata — verifies the Google-signed instance-identity JWT against Google's JWKS.
- Azure metadata — verifies the PKCS#7-signed IMDS attested document against a trusted Azure root.
- Kubernetes projected SAT — verifies a pod's projected service-account token against the cluster's JWKS.
- GitHub OIDC + Fulcio — verifies a GitHub Actions OIDC token and can produce a Sigstore/Fulcio binding for keyless code signing.
The verifier dispatches by method, computes a stable attestation ID through the single
crypto path, adds an attestation node to the credential graph, and
emits an immutable attestation.verified event — or, on failure, attestation.rejected
and nothing else (fail-closed). Every attester must pass a conformance harness proving
it accepts a genuine proof and rejects a forgery.
Served at /api/v1/workloads/attester-trust-sources and
POST /api/v1/workloads/attested-issuance: workload owners with certs:issue manage
(create/replace/rotate/revoke/delete) tenant trust sources for tpm, aws_iid,
gcp_iit, azure_imds, k8s_sat, and github_oidc, and the binary builds the
verifier from those records plus any configured process defaults. It verifies the
proof, signs an X.509-SVID through the isolated signer, records the certificate as
certificate.recorded, and binds the attestation with attestation.bound — or fails
closed if no enabled trust source matches the method.
The SPIFFE Workload API (F24) — the standard interface
SPIFFE is the open standard for workload identity; its document is the
SVID, delivered as an X.509 certificate or a JWT. trstctl implements a
SPIRE-compatible Workload API server: a workload presents selectors (e.g.
k8s:ns:default, k8s:sa:web), the server matches them against registration entries
by set-subset (every required selector must be present), and issues the SVID. Signing
goes through the single crypto path to keys in the separate, isolated signing
service — private-key operations never run in the API process. A NeedsRotation
helper flags an SVID for renewal once half-expired (SPIRE's policy); issuance runs in
its own bounded lane, each step recorded as an immutable event.
Served as a gRPC service on a Unix domain socket (protocols.spiffe.enabled, default
off): a spiffe-helper/go-spiffe/Envoy-SDS workload dials the socket and can call
FetchX509SVID, FetchX509Bundles, FetchJWTSVID, FetchJWTBundles, and
ValidateJWTSVID. X.509-SVIDs are signed through the isolated signing service;
JWT-SVIDs use the signer-backed JWT handle and validate against the served JWT bundle.
The Workload-API gRPC/protobuf contract is vendored verbatim from go-spiffe, so the
wire format is byte-identical.
SPIRE upstream authority — keep SPIRE, anchor it in trstctl
If you already run SPIRE, trstctl can sit above it as the upstream
private CA: the trstctl-spire-upstream-authority plugin implements SPIRE's
UpstreamAuthority interface, so SPIRE keeps its local CA private key, sends only a CSR
to trstctl, and gets a signed intermediate CA chain back. SPIRE keeps minting locally
while trstctl becomes the governed root of trust — tenant-scoped API auth,
idempotency, audit, and signer-backed CA custody.
The plugin calls the served route POST /api/v1/ca/authorities/{id}/intermediates/csr
with csr_pem and a CA profile (common_name, ttl_seconds, max_path_len, optional
DNS constraints), reading its token from a mounted file rather than a command-line
argument. A stable Idempotency-Key on the CSR stops SPIRE retries from minting
duplicate intermediates; the response is the SPIRE intermediate plus the trstctl
upstream root.
UpstreamAuthority "trstctl" {
plugin_cmd = "/opt/spire/plugins/trstctl-spire-upstream-authority"
plugin_data {
endpoint = "https://trstctl.example.com:8443"
ca_authority_id = "11111111-1111-1111-1111-111111111111"
token_file = "/run/secrets/trstctl-spire-token"
common_name = "SPIRE Server CA"
ttl_seconds = 3600
max_path_len = 0
}
}
This is container-proven end to end: CI runs a real SPIRE server, loads the plugin, mints an X.509-SVID, and verifies the chain as workload leaf -> SPIRE intermediate -> trstctl root. SPIRE's optional JWT upstream method isn't claimed by this plugin — X.509-SVID trust anchoring only.
Ephemeral issuance (F25) — attestation in, short-lived cert out
The ephemeral issuer ties it together: it verifies an attestation (refusing to sign on
failure), mints a short-lived certificate (default TTL 15 minutes, clamped to a
per-method maximum), and binds the attestation to the credential in the graph and
audit trail. Every request takes an Idempotency-Key, so a retry never mints a second
credential — it returns the original.
The direct X.509-SVID flavor is served when attested issuance is configured, at
POST /api/v1/workloads/attested-issuance; the approval-gated JIT flavor is served
when ephemeral issuance is configured, at POST /api/v1/ephemeral. The first call
verifies the proof, opens a dual-control approval, and enqueues the notification
intent in the same tenant transaction; after a distinct approver calls
POST /api/v1/ephemeral/{request_id}/approvals, a fresh Idempotency-Key on
POST /api/v1/ephemeral mints the short-TTL credential. The response carries
certificate_pem, credential_id, certificate_id, subject, not_after, approval
counts, and verified attestation metadata.
Non-human identity lifecycle (F59)
Beyond a single credential, the identity itself has a lifecycle: requested, issued,
deployed, renewing, revoked, retired (terminal). trstctl models this as a guarded
state machine — every transition goes through one served path enforcing the legal
moves, updating PostgreSQL-backed identity rows and the credential graph projection,
and emitting immutable lifecycle events (identity.created, identity.issued,
identity.deployed, identity.revoked, identity.renewed, identity.retired).
The served REST routes POST /api/v1/identities and
POST /api/v1/identities/{id}/transitions (both take an Idempotency-Key, so a retry
never double-creates or double-applies) are the canonical identity lifecycle surface:
there's no parallel in-memory NHI manager — the PostgreSQL-backed identity rows,
orchestrator events, audit trail, graph projection, and OpenAPI/CLI paths are the
product path operators run.
The AI-agent identity broker (F61)
AI agents are a sharp case: they appear fast, act with real privileges, and chain
tools together, so an over-scoped or un-revocable credential is dangerous. The
AI-agent identity broker is a dedicated issuance surface that (1) evaluates a
policy decision before issuing — a deny records
agent.identity.refused and signs nothing; (2) issues an attested, short-lived
credential via the ephemeral issuer; (3) records the agent and its credential in the
graph so you can ask blast radius ("everything this agent can reach") before
trusting it. A tenant-wide broker history and one-call revocation console remain a
roadmap residual, not part of the served GA claim.
Served when the agent broker is configured, at POST /api/v1/broker/agent-identities:
the operator supplies the trust domain, attestors, Rego policy module, and
signer-backed issuing CA. A request carries the agent id, attestation method, proof
payload, public key, requested scopes, and optional TTL; trstctl verifies the proof,
evaluates policy before signing, mints a short-lived X.509-SVID through the isolated
signer, records certificate.recorded, and projects the agent-to-credential edge into
the graph. Denies emit agent.identity.refused and return no credential.
In the console
The console adds a governance lens over non-human identities: a unified NHI inventory
by kind (GET /api/v1/nhi/inventory), a risk-posture summary, orphan-governance for
credentials whose custodian is gone or inactive, and a blast-radius explorer at
/graph. The identity grid at /identities carries issue / deploy / revoke actions
behind the same confirm and dual-control guards as the API. See
The web console.
Use it
Create and transition a managed identity:
trstctl-cli identities create -f service-account.json
trstctl-cli identities transition <id> \
-f '{"to":"revoked","reason":"cessationOfOperation"}'
Both map to POST /api/v1/identities and POST /api/v1/identities/{id}/transitions
(mutations require an Idempotency-Key). Automated decommissioning from owner
departure, vendor termination, or inactivity signals is governance, not an
identity-lifecycle primitive: POST /api/v1/nhi/decommission
(trstctl-cli nhi decommission) resolves those signals against managed NHIs and
revokes or retires them via these same transitions. Canonical home:
Policy & governance.
Deploying against an existing SPIRE cluster is just installing the plugin binary (or
mounting it read-only) and adding the UpstreamAuthority "trstctl" block above with a
token_file scoped to certs:issue on the owning tenant.
Attested X.509-SVID issuance needs an enabled trust source first
(POST /api/v1/workloads/attester-trust-sources; name, method, issuer, audience,
JWKS), then:
curl -sS -X POST https://localhost:8443/api/v1/workloads/attested-issuance \
-H "Authorization: Bearer $TRSTCTL_TOKEN" \
-H "Idempotency-Key: k8s-web-1" -H "Content-Type: application/json" \
-d '{"method":"k8s_sat","payload_base64":"...","public_key_pem":"...",
"ttl_seconds":600}'
The response is the certificate the workload should load, plus the verified subject
that became the SPIFFE path (e.g. spiffe://example.org/ns/default/sa/web). Trust
material rotates, revokes, and offboards via .../rotate, .../revoke, and
DELETE .../{id}, each idempotent and recorded as an immutable event.
Approval-gated ephemeral/JIT issuance needs EphemeralIssuanceConfig (attestors,
trust domain, signer-backed issuing CA, approval TTL, approval threshold). The
requester opens the approval, a distinct approver records it (never themselves), then
the requester mints with a fresh idempotency key:
trstctl-cli --idempotency-key jit-1-request ephemeral issue -f jit-request.json
trstctl-cli --idempotency-key jit-1-approve ephemeral approve jit-1 -f approval.json
trstctl-cli --idempotency-key jit-1-issue ephemeral issue -f jit-request.json
The first call returns state: "awaiting_approval" and no certificate; the approved
call returns state: "issued" with a certificate whose not_after is clamped by the
TTL policy. Replaying either key returns the same response without opening another
approval or minting again.
The AI-agent broker works the same way once configured:
curl -sS -X POST https://localhost:8443/api/v1/broker/agent-identities \
-H "Authorization: Bearer $TRSTCTL_TOKEN" \
-H "Idempotency-Key: agent-7-issue" -H "Content-Type: application/json" \
-d '{"agent_id":"agent-7","method":"k8s_sat","payload_base64":"...",
"public_key_pem":"...","scopes":["mcp:graph.read","tool:inventory.read"],
"ttl_seconds":600}'
The response includes the issued certificate, credential_id, certificate_id,
verified attestation metadata, expiry, and the graph node_id for the agent workload.
Replaying the same key returns the same response without minting twice.
Pitfalls & limits
| Capability | Status today |
|---|---|
| NHI lifecycle routes (F59) | Served — /api/v1/identities, /transitions |
| SPIFFE Workload API (F24) | Served — gRPC over a UDS (protocols.spiffe.enabled); FetchX509SVID, FetchJWTSVID, bundle fetches, and ValidateJWTSVID wired to the signer-backed path |
| SPIRE upstream authority | Served and container-proven for X.509 — SPIRE loads trstctl-spire-upstream-authority, trstctl signs its intermediate CA CSR via /api/v1/ca/authorities/{id}/intermediates/csr, and the e2e verifies a minted SVID chain to the trstctl root |
| Ephemeral issuance (F25) | Served — direct attested X.509-SVID mint at POST /api/v1/workloads/attested-issuance once a tenant trust source is enabled; approval-gated JIT mint at POST /api/v1/ephemeral plus /api/v1/ephemeral/{request_id}/approvals |
| Attestation chain (F30) | Served — tenant trust-source lifecycle at /api/v1/workloads/attester-trust-sources; the six-attester verifier gates POST /api/v1/workloads/attested-issuance; conformance covers each attester |
| AI-agent broker (F61) | Served when configured — POST /api/v1/broker/agent-identities verifies proof, gates policy, mints a short-lived credential, and projects the graph grant |
Operationally: each attestation method needs public trust material configured first (cloud roots, cluster JWKS, TPM manufacturer roots), and short TTLs mean frequent renewal for workloads and agents — the point, but plan for it.
Reference
- Served routes:
POST /api/v1/identities,POST /api/v1/identities/{id}/transitions,GET /api/v1/workloads/attester-trust-sources,POST /api/v1/workloads/attester-trust-sources,PUT /api/v1/workloads/attester-trust-sources/{id},POST /api/v1/workloads/attester-trust-sources/{id}/rotate,POST /api/v1/workloads/attester-trust-sources/{id}/revoke,DELETE /api/v1/workloads/attester-trust-sources/{id},POST /api/v1/workloads/attested-issuance,POST /api/v1/ephemeral,POST /api/v1/ephemeral/{request_id}/approvals,POST /api/v1/broker/agent-identities,POST /api/v1/ca/authorities/{id}/intermediates/csr. - Attestation methods:
tpm,aws_iid,gcp_iit,azure_imds,k8s_sat,github_oidc. - SPIFFE:
FetchX509SVID,FetchX509Bundles,FetchJWTSVID,FetchJWTBundles,ValidateJWTSVID; selector match is set-subset. - Events:
attestation.verified/rejected/bound,ephemeral.approval.requested,ephemeral.approval.granted,ephemeral.issued,spiffe.svid.issued,certificate.recorded,identity.created,identity.{issued,deployed,revoked,renewed,retired},agent.identity.{issued,refused,revoked}.
See also
SSH (attestation-gated SSH certs use the same chain) · Issuance & certificate authorities · Graph, query & AI (blast radius) · Policy & governance (the broker's policy gate and NHI decommissioning) · glossary: workload, attestation, SPIFFE/SVID
Covers: F24, F25, F30, F59, F61