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