SCS Lab 05 — Certificate-based workload identity, without a human in the loop
Scaffold: 4/5. The CA scripts, the S3 exchange, and the AWS trust
configuration are all here. What is missing is the host roster: hosts.allow
starts with only the CA itself, and you decide what goes in it.
Ninety minutes. Stand up a two-tier CA on one machine, enrol a workload, watch it renew itself, then revoke it and measure how long access actually survives.
The full operating story, the SD-card ceremony, the safe protocol, Windows and macOS enrolment, and what to do when each piece is lost, lives in the HOWTO. This lab is the part you can prove in an afternoon.
What you build
Root CA (offline in practice; a directory here)
│ signs, once
▼
Issuing CA ──── polls ────▶ S3: csr-inbox/ crt-outbox/ crl/
pathlen:0 ▲ │
name-constrained │ ▼
workload ── renews itself on the
credentials it already has
Both CA tiers run on your laptop or one Pi for the lab. In production the root lives on an SD card in a safe; nothing else about the design changes, which is the point worth taking away.
Steps
1. Build the two tiers (five minutes).
export PKI_ORG=Ironworks PKI_DNS_SUFFIX=.factory.internal \
PKI_CRL_BASE=https://<bucket>.s3.<region>.amazonaws.com/crl
./issuing/bootstrap.sh # issuing key + CSR
./root/ceremony.sh /pki/issuing.csr # root, then signs the issuing CA
./issuing/install-cert.sh /pki/root/certs/issuing.crt /pki/root/certs/root.crt
install-cert.sh refuses a chain whose issuing CA has no name constraints.
That refusal is the lab’s first lesson: the constraint is what makes a stolen
issuing key survivable.
2. Deploy the AWS side (ten minutes). The only step that runs on your own credentials.
ROOT_CRT=/pki/root/certs/root.crt ALARM_EMAIL=you@example.com ./scripts/deploy.sh
Bucket, trust anchor, two profiles, two roles, and the staleness alarm.
3. Enrol a workload (ten minutes).
./leaf/linux/enrol.sh docs-worker-01.factory.internal
echo docs-worker-01.factory.internal >> /pki/issuing/hosts.allow
./issuing/signer.sh # signs what is in the inbox
Point the AWS SDK at the certificate (leaf/linux/aws-config) and make a call.
Short-lived credentials, no access key anywhere.
4. Watch it renew itself (ten minutes). Force the window:
PKI_RENEW_BEFORE_DAYS=99 ./leaf/linux/renew.sh
The host generates a new key, uploads the CSR using the credentials its current certificate provides, and collects the answer. That is the whole trick: renewal rides the authority it is about to replace, so only bootstrap needs a human.
5. Break it on purpose (thirty minutes, the part worth doing slowly).
cd /pki/issuing && ./revoke.sh docs-worker-01.factory.internal
Now watch two clocks. The worker’s current session keeps working: those
credentials were already minted. What fails is its next CreateSession.
Time that gap. It is one session duration long, and knowing the number for
your own fleet is worth more than any diagram.
Then close it, because you can:
PKI_WORKLOAD_ROLE=pki-workload ./revoke.sh docs-worker-01.factory.internal \
keyCompromise --cut-sessions
That adds a deny keyed on aws:TokenIssueTime, scoped by the certificate
subject in the session tags, so sessions already held by that host stop on
their next API call and the rest of the fleet carries on. The CRL push happens
first on purpose: cut sessions before revoking and the host just authenticates
again.
Then break it the other way. Delete the CRL import in Roles Anywhere and watch the revoked certificate start working again. Roles Anywhere consults only the CRLs you import: a revocation you never pushed is a revocation that never happened. That single experiment is the argument for the staleness alarm.
Verify
./scripts/test.sh
Checks the chain, the constraints, a CSR round-trip, CRL freshness, and, most
importantly, that a CSR for a host not in hosts.allow is refused. A CA
that issues correctly is half the test; one that refuses correctly is the rest.
Tear down
./scripts/teardown.sh