Workload Lab
PreviewThe labs for identities that belong to machines. There is no human in any of these workflows by design — the consumer is a pipeline, a broker or a cluster, not a person. A certificate, a SPIFFE identity, a Kubernetes token and a short-lived cloud credential are four answers to the same question, which is why they share a page — and which one an operator can actually use depends on what the consumer is and who runs the thing it is reaching. The Agent tab is the fifth, and it is not a fifth answer: it is a consumer holding one, so the others stop being demonstrations of a mechanism and become a principal you can watch be stopped. Which one you want.
Password Safe as a certificate registrar and broker — it never becomes a certificate authority. The managed account holds the PKCS#12 passphrase, Secrets Safe holds the bundle it opens, and both halves are governed. Build a private CA here, onboard an identity onto it, then rotate from BeyondInsight. The plugin ships as two platforms: Certificate for an end-entity certificate, and Subordinate CA for an issuing authority another system mints beneath — a delegation of issuing authority rather than one more credential, which is why its access control is separate. Feature guide.
| Name | Pool | Location | Signs | Status | Identity | Expires | |
|---|---|---|---|---|---|---|---|
| leaves + sub-CA leaves only | — no functional account |
Build a certificate authority
A CAS pool, a self-signed root CA, and the enrollment service account the plugin authenticates as. Destroying it removes all three in one pass — the module sets the three flags CAS otherwise uses to refuse deletion.
A private CA, its self-signed root certificate, and the IAM user the plugin authenticates as. ~$400/month standing, twenty times the GCP pool, billed whether or not it issues anything — so it is refused outright unless auto-delete would stamp a timer on it.
The project id, not its display name or number. Defaults to the project this dashboard's GCP credential is scoped to; the pool lands wherever this says, so a different project needs the CA Service roles granted there too.
The pool, its CAs and any certificate template are all location-scoped and must agree.
A free-text field rather than a list: a Private CA is regional, and it is available in far more regions than CAS is.
Fixed when the root is created and not widenable afterwards. A root built to issue leaves has a path length of 0 and will refuse to sign a subordinate CA — and it is the certificate authority that refuses, with a policy error naming neither this choice nor the flag behind it. Pick can also sign a subordinate CA to demonstrate the Subordinate CA platform; it still issues end-entity certificates exactly as before.
One managed account per identity. The account name becomes the subject CN by default, and the whole profile below becomes the managed system's Network Address on the Certificate platform.
The managed credential here is an issuer, not a certificate. Password Safe obtains a subordinate CA from this root, something else — PRA Vault, typically — holds it and mints its own short-lived leaves beneath it, and Password Safe rotates the subordinate on a schedule. It lands on the Subordinate CA platform, whose access control is separate for that reason. Install the root chain into every target's trust store once, before anything else: that is what makes rotation safe, and nothing here automates it.
Rotation does not revoke. A target validates a leaf by walking
its chain to the root and neither knows nor cares which subordinate was current
when the leaf was minted — so this, not the rotation interval, is the
exposure window if the signing key leaks. Issue it just longer than the interval
Password Safe rotates it on: 8d against a 7-day schedule leaves two
concurrently valid authorities briefly, where 1y would leave ~52.
A pipeline identity is not a host, so usually leave this empty — and note that
under eku=Auto a DNS SAN drags serverAuth into the EKU set.
A vaulted CA key is not a credential to one system — it is the authority to mint an identity for anything the CA may assert. Constraints turn a compromise from mint anything into mint within a bounded namespace, and the extension is marked critical, so a relying party that cannot interpret it refuses the certificate rather than ignoring the boundary. Leaving this empty is allowed and warned about. Prefer the parent CA pool's own issuance policy where you can: the subordinate then inherits a boundary it cannot widen, and it costs nothing against the 255-character address.
A subtree is a network: 10.0.0.0/8, never
10.1.2.3/8. Host bits set is refused here, because no later
action can detect a subordinate constrained to the wrong thing.
How many further CAs may appear beneath the subordinate. 0 is almost always right, and is all this lab's roots permit.
The Subordinate CA package already defaults to PemBundle, which is
what a consumer of a CA key needs — PRA Vault's X.509 Parent Certificate
Authority account takes a PEM key, a passphrase and a PEM certificate as
three separate fields, and its key box says "Only PEM encoding is valid".
Leaving this alone is both correct and 17 characters of address budget back.
No isca= and no bundle=: the Subordinate CA package
already defaults both, and restating them would spend 27 characters of the budget
the permitted-DNS list is competing for. No eku= either — the plugin
asserts none on a CA certificate, because an EKU there constrains everything
issued beneath it and implementations disagree about how.
CA chain
Hand this to the mTLS endpoint playbook as ca_chain_pem. It is the
ssl_client_certificate nginx verifies presented certificates against.
A SPIRE trust domain on a VM this dashboard already deployed, so the Password Safe SPIFFE SVID plugin has workload identities to govern. The lab is four Ansible playbooks against an ordinary Linux host — there is no Terraform module, because a SPIRE server is a Go binary and a sqlite file. Feature guide · Standup runbook.
tcp/8081 and leaves the VM alone.
| Trust domain | Host | Status | Stages | Reachable from | SVID expires | |
|---|---|---|---|---|---|---|
|
no cloud rule — in-network only
|
Build a trust domain
A bare DNS-style name, not a spiffe:// URI. It is baked into
the server config, every SPIFFE ID and the Managed System, and the plugin asserts
it on every connect — so it cannot be changed afterwards.
One mode per host. Every mode publishes an OIDC Discovery Provider on tcp/8443 and registers the trust domain with this dashboard by JWKS URL. Not staged: .
No VM available. Deploy one from its cloud page first — the lab attaches to an existing host so it inherits that VM's SSH key, its auto-delete timer and its Destroy. Ubuntu 22.04 on the smallest shape that runs a Go binary is plenty. It must be on a subnet the Resource Broker can reach.
How the runs log in (optional)
Pick a host first. No managed accounts for this host — Password Safe may be disabled, or the VM is not onboarded there.
An account and a key are two answers to one question, so picking one disables the other. A managed account also supplies its own login user. An SSH-key secret works on every runner; a managed account works on the local and Azure (ACI) runners, and on ECS / Cloud Run via a short-lived RBAC-locked store copy. on ECS / Cloud Run only if ‘Ephemeral cloud secrets’ is enabled in Settings.
The functional account's name is this SPIFFE ID, not a username.
It goes into the server's admin_ids — which is the line people
get wrong: an entry-level -admin grants nothing to a minted SVID, and
a missing admin_ids surfaces much later as
PERMISSION_DENIED on Verify Functional Account.
What this will do, in order:
Connecting as (Password Safe, checked out just‑in‑time, sudo included) , using the SSH-key secret you picked , using the keypair this VM was deployed with
- Open
tcp/andtcp/8443on the cloud ACL to — nothing, because no source CIDRs are configured - Install SPIRE under systemd
- Install Docker, then run SPIRE from the official images under Compose
- Install k3s, then the SPIRE Helm charts
- Open the host firewall on the same sources
- Seed registration entries ( should be discovered)
- Mint the administrative credential into Secrets Safe
- Publish the OIDC Discovery Provider and register the trust domain's JWKS URL here
Kubernetes access
Never live-validatedAttest a second VM — a small k3s node — into , and make its API server accept this trust domain's JWT-SVIDs. The workload then reaches the cluster with a token that lives s and is written to no disk and no vault. What this proves.
Self-managed clusters only. This sets
--authentication-config on the API server, which EKS, AKS and GKE do
not let you pass — which is why the lab brings its own k3s node. For a
managed cluster, the
brokers a bound ServiceAccount token through Password Safe instead — audited
and rotated, though the vault authenticates whoever can retrieve rather than the
workload itself. That is the trade, and it is the one axis this page wins on.
- Issuer
- Audience — must match in three places
- Workload identity
- RBAC subject — the prefix is mandatory and looks wrong
- Bound to
Prove it, on the k3s node
Then break it — this MUST be refused
The audience is the only thing stopping a token minted for another relying party being replayed here. Stopping the agent is the other one worth showing: access lapses within the TTL, with no revocation step anywhere.
The last attempt failed
The trust domain itself is unaffected — it is still available and still governs everything it did before.
A plain VM; k3s is installed here. Not the SPIRE host — an agent attesting over loopback proves the mechanism but not that it works across a network, which is the half worth showing. 2 vCPU / 4 GB is plenty. No other VM available. Deploy one from its cloud page first — it keeps its own SSH key, timer and Destroy.
How the k3s runs log in (optional)
Pick the k3s host first. No managed accounts for that host — Password Safe may be disabled, or the VM is not onboarded there.
Separate from the SPIRE host's on purpose — the two VMs were deployed independently and do not share a key. Leaving all three blank is the normal case: each host's keypair comes from its own deploy job.
Must already exist in the cluster.
Read this before the first run
The last stage edits the API server's authentication configuration, and a cluster that will not restart has no API left to repair itself through. The way back is deleting one drop-in file and restarting k3s — the play says so at the end. Structured authentication is GA in Kubernetes 1.34, and the play refuses anything older rather than writing a config it will reject.
Five playbooks, alternating hosts. Upload them on the Storage page first.
- ✓
Onboard into BeyondInsight
Govern creates the managed system for you. These are the values it uses, and the two below it cannot; §5 of the runbook is the order.
Still needs a human
- —
The attribute probe — does the gateway hand attributes to a plugin action?
is on the
managed system (read back).
Then run Verify Functional Account on the managed system and read one line in its activity record:
Attributes received: system=[...] account=[...]
Recorded:
by ,
UTC
Next:
Press Govern first: the attribute goes on the managed system.
After that run Discovery Accounts and check the count: , not “succeeded”.
Trust bundle
Set SpiffeTrustBundlePem on the managed system to this. A SPIRE server
presents only its leaf certificate, so there is nothing in the handshake to chain
against and fingerprint pinning cannot substitute — without it every connect
fails SPIRE Server certificate could not be validated. Public by
construction: it is what every consumer of the trust domain has to trust.
A bound ServiceAccount token for a machine outside the
cluster — a CI build, a fleet scan — minted by the API server, rotated and
audited by Password Safe, retrieved per run. This is the path that works on a
managed cluster: the SPIFFE route needs
--authentication-config, which EKS, AKS and GKE do not let
you pass.
Feature guide.
For a self-managed cluster with no credential at rest at all, use
.
Not configured yet
No workload identity is onboarded.
Onboard a Deployer to show a CI build deploying into one namespace and being refused in another, or a Reader to show a cluster-wide scan that cannot read Secrets.
Onboard a workload identity
The profile is the decision here, not the token. It selects the
RBAC, and the RBAC is what makes a vaulted token worth demonstrating —
neither profile is cluster-admin.
Not selectable (no stored kubeconfig, so nothing here can reach the API server):
Created if it does not exist. For deployer this is the only namespace the token may write in.
needs the cloud's own name for this cluster. The dashboard only stores the label you registered it under, and the Password Safe plugin looks it up by its real name — a mismatch fails at the first rotation, inside Password Safe.
A zonal cluster's location is its zone, a regional one's is its region. Mixing them is the documented cause of a GKE 404.
bound mints through the TokenRequest API and creates no Secret at all — strictly less privilege for the rotator.
600 is the TokenRequest API's own floor; anything lower is clamped.
A forgotten workload token keeps authenticating, and this row is the only page it appears on.
What the consumer needs
No credential is shown here, and none ever will be. The consumer retrieves with its own Password Safe client id — that is what records which build read the token. A value proxied through this page would record only that the dashboard did.
Not ready: the managed account either does not exist yet or has never rotated, so a retrieval would return the placeholder it was created with rather than a token.
A short-lived AWS or Azure credential for a workload, minted on demand by Workload Credentials against a dynamic secret and handed out as a lease. This is the tab that replaces the access key in a CI secret store — the one with no expiry, no revocation and no record of who read it. Feature guide.
Not configured yet
Before you choose a cloud
— a lease can be released early, so there is a real kill switch. a lease cannot be revoked. STS will not withdraw a credential it has already signed, so the TTL is the only control — which makes a short one matter more here, not less.
No workload identity is registered.
Register one against a Workload Credentials dynamic secret, then press Issue to mint. Registering alone costs nothing — issuance is metered.
·
·
issuances
Scope is set by the dynamic secret in Workload Credentials, not here. A mint
returns
to the consumer that asked for it.
Register a workload identity
Records that this workload draws from a dynamic secret. Mints nothing — issuance is metered, so the first credential appears when you press Issue.
Not available (no Workload Credentials configuration):
This decides the scope — which role is assumed and what it may do. Defined in Workload Credentials; this page cannot change it, which is why there is no default.
The dashboard uses
for its own cloud calls. A lab identity is usually a
different secret with a narrower scope.
A request, not a guarantee — AWS caps a role-chained credential at one hour and the provider may clamp.
Retiring the identity stops it minting another credential. An outstanding one lives out its TTL regardless.
The current lease
Read live from Workload Credentials, not from this dashboard's record — the two diverge exactly when it matters, if the lease was released elsewhere. No credential is shown here and none ever will be: the values went to the consumer that asked for them, which is what puts that consumer in the audit log rather than this page.
The provider's own view
The consumer. A worker on a VM this dashboard already deployed, which attests itself to the 's trust domain and reads your estate through this dashboard's MCP server — authorized by a token that expires and can be revoked while somebody watches. Make it answerable for a lease, a token or a identity and the other tabs stop being four separate demos. Feature guide.
Not ready yet
config_mgmt:write — what the cell
ultimately does is run playbooks against a host. You can see the agents below.
Identity, and authorization
Its identity is a SPIFFE SVID it re-attests on every loop, holding
nothing at rest. Its authorization is bounded by the RBAC of the
user you mint it against. Mint it against a service account and the
SVID is the authorization's root: the worker exchanges a fresh JWT-SVID at
/api/oauth/token for an access token that lives minutes, and holds no
secret at all. Mint it against a person-shaped user and it gets a Personal Access
Token, and the two stay separate.
Either way the SVID does not authenticate to /mcp directly
— it is exchanged for a bearer token, and that is what /mcp sees.
The other bridge, the SVID minting a credential inside Password Safe through the
SPIFFE SVID plugin, is still unresolved.
The demo is the same in both: revoke the agent and the worker is still who it was, and may no longer do anything.
No agent has been minted.
Minting issues a bounded token and records what was issued. It deploys nothing — the worker attaches to a VM you already have, and the two playbooks that put it there are runs you make.
· on
·
Every MCP tool applies the token user's own permissions, so that user is this agent's blast radius. Revoking the token from Settings → API Tokens stops the worker; it does not uninstall it.
Answerable for —
Accountability only. That tab's credential is returned to nobody by design, so nothing here can spend it.
A capability.
Cluster access —
The dashboard records that a request is open. The waiting, the approval and the
probes happen on the host, and the worker cannot report them back — its
token belongs to a non-admin user by construction.
journalctl -u mcp-agent -f is where the episode happens.
Certificate use — a run you make on the host, not a button here
The worker asks for both halves itself — the PKCS#12 passphrase through
a recorded Password Safe request, and the bundle from Secrets Safe on the same
session — then presents the certificate to your mTLS endpoint. Run
mcp_agent.py --cert-episode with the bundle title and CN from the
link; journalctl -u mcp-agent -f is the record.
Revoking this certificate will not stop the agent. Nothing on that path consults a CRL or OCSP — it stops when the certificate expires, not when somebody takes it away. That is the third shape, and it is what makes a revocable token and an approval gate worth having.
Cloud credential — a run you make on the host, not a button here
The worker mints it itself: it presents this machine's own identity token to
Workload Credentials and generates against the dynamic secret, so the
issuance is recorded against the workload and nothing passes through this
dashboard. Run mcp_agent.py --cloud-episode with the dynamic
secret from the link; journalctl -u mcp-agent -f is the record.
Every run is one metered issuance. Workload Credentials bills per mint, so this is the only link in this cell that costs money to spend — and a worker looping on it is a cost problem before it is an audit one.
Its ending is not a revoke. An AWS lease cannot be withdrawn at all, so the episode waits out the real expiry and proves the same call is then refused. Azure can release early, which ends the ability to get another token — not the one already issued.
Mint an agent
Issues a bounded token and records what was issued. Deploys nothing — the worker attaches to a VM you already have, and the install is two playbooks you run yourself.
The worker will attest as
, one ID per cell.
The dashboard creates the entry itself and hands you a one-use join token; the
worker's host runs its own SPIRE agent, so there is no lab and no second VM. The
host must reach
on tcp/8081.
The worker will attest as
.
That path is fixed, so the registration entry the playbook creates and the ID
the worker expects cannot drift.
None yet. Start the dashboard's own SPIRE server
(docker-compose.spire.yml, or the examples/cloud-spire
sidecar on a managed runtime), or stand a lab up on
first — without a trust domain the worker logs unattested and
the identity half of the demo becomes an assertion.
1 to . It always expires — a non-human principal whose authorization never ends is what this cell argues against.
Re-derived against this dashboard's own deploy records, never trusted as given. Pick the VM that is already a SPIRE agent node — the lab's host is prefilled above when you choose one.
Administrators are not listed, and the cell refuses one. Every MCP tool applies this user's own permissions, so the token user is the agent's blast radius — picking an admin would quietly make the demo say the opposite of what it means to.
Everyone in this workstream can see and manage the agent — including revoking its token. Leave it blank and only you and administrators can.
is recorded
The row keeps the SPIFFE ID, the token's name and its expiry. It never keeps the token. This is the only time it is shown — carry it into the install playbook now, because nothing can recover it afterwards.
.
One use, fifteen minutes, for the SPIRE agent on the worker's host. Mint the cell again if it lapses.
Save it as bootstrap.crt where you run the playbook.
Put the worker on the host
Two playbooks, in this order, from examples/playbooks/agent/. The
first creates the SPIRE registration entry on the SPIRE server so
the worker can attest; the second installs the unit on the worker's host.
The dashboard does not run these and does not watch them —
which is why there is no progress bar here and why the journal is the record.
1 — a SPIRE agent on the worker's host
The dashboard already created the registration entry. This attests the host with the join token from the mint, and proves a JWT-SVID can be fetched as the worker.
1 — the registration entry (on the SPIRE server)
The worker's host must run a SPIRE agent. In this lab the host that does is the SPIRE row's Kubernetes-linked k3s node — link the lab first, and put the worker there.
This agent's host is not the lab's k3s node (),
the one host that runs a SPIRE agent — the worker there would log
unattested.
2 — the worker
agent_token is the token from the mint response. It was shown once;
if it is gone, revoke this agent and mint another rather than reusing a token
whose owner you are no longer sure of.
No token: the worker exchanges a fresh JWT-SVID from the host's SPIRE agent at
/api/oauth/token for each short-lived access token.
3 — watch it
Every line names both halves — the SPIFFE ID it proved and the token it spent — so the gap between them stays visible while the room watches.
What is it answerable for?
One credential at a time, deliberately. An agent answerable for a cloud lease and a cluster token and a certificate would be the most over-credentialed principal in the estate — which is the shape this cell argues against, so widening is a decision rather than an accumulation.
That tab has no rows to link. Create one there first.
The subscription the credential must be able to read — what
--cloud-scope takes. Azure only: on AWS the allowed call is
sts:GetCallerIdentity, which needs no scope and cannot be
denied.
The refusal is what proves anything. That the credential
works shows only that it exists; that it is refused something is
the scope. This dashboard cannot see the dynamic secret's role, so this is
your assertion about it — and none makes the episode say
out loud that the run proves authentication and not scope.
Neither is stored here. The row records the mechanism and the identity's id; these two shape the command the link hands back.
The identity you onboarded onto this CA. Its credential is the PKCS#12 passphrase — one half, and useless without the other.
The bundle's Secrets Safe reference — the other half, and what
--cert-bundle-title takes. One containing / is
looked up as a folder path, one without it as a bare title. It is a
name, not a credential.
What --cert-cn asserts. An endpoint that answers 200 without
having seen the certificate proves nothing, so the episode checks that the
subject comes back.
Neither half is stored here. The row records the mechanism and
the CA; these three compose the notes you carry to
mcp_agent.py --cert-episode. And this link is the one whose
removal changes nothing — nothing on that path checks revocation.
Request cluster access
Opens one bounded request against the linked token. It hands nothing over — Password Safe decides whether to release, and where the access policy requires approval it holds the request for a person. That is the beat: an agent that cannot authorise its own access.
Clamped to 5–, because this is how long the account's concurrent slot stays held if nobody approves. An abandoned request makes the next attempt fail on the cap, reporting the cap rather than the approval it was waiting on.