Lesson  in  Workload Identity from Scratch: Re-Inventing SPIFFE with OpenSSL, MariaDB, SoftHSM2, and a TPM

Short-Lived Certificates: Renewal Is the Engine, Not the Tax

A certificate's lifetime is a security control. Issue a cert that lives for 30 seconds, use it, watch a connection reject it the moment it expires, then renew. See why short TTLs make a stolen cert worthless and revocation almost vestigial, and why the industry walked away from OCSP.

Issue It, Then Watch It Die

Most outages and most breaches involving certificates share one root cause: certificates that live too long. A one-year cert is a one-year window for a stolen key to be useful, and a one-year fuse for the day everyone forgot it would expire. This lab treats lifetime as the security control it is: you issue a certificate that lives for 30 seconds, watch it work, watch it die, and renew it. The point is to feel why "renew constantly" is not overhead, it is the mechanism that makes the whole system safe.

Where trust bottoms out

Short TTLs do not remove the trust root, they concentrate it. The chain of "who do I believe" terminates at one well-defended fact (the CA's signing key) and everything above it is short-lived and re-derivable. A stolen leaf cert is then time-boxed to its TTL: useless once it expires, and the thief cannot mint a fresh one without reaching the CA. Renewal is not the tax you pay for security; renewal is the security.

Step 1: Issue a 30-second certificate

Build a CA and the small issuing database openssl ca needs. The unique_subject = no line matters: renewal re-issues the same identity over and over, and without it the CA refuses the second one.

mkdir -p ~/pki && cd ~/pki
: > index.txt && echo 1000 > serial && echo "unique_subject = no" > index.txt.attr

cat > openssl.cnf <<'CNF'
[ca]
default_ca = CA_default
[CA_default]
dir             = .
database        = $dir/index.txt
new_certs_dir   = $dir
serial          = $dir/serial
default_md      = sha256
policy          = pol_any
copy_extensions = none
[pol_any]
commonName = supplied
[client_ext]
extendedKeyUsage = clientAuth
CNF

openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \
  -keyout ca-key.pem -out ca.pem -subj "/CN=Demo Root CA" -days 3650

Issue a leaf that lives for exactly half a minute. -startdate and -enddate set the validity window to the second:

openssl req -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \
  -keyout billing-short-key.pem -out billing.csr -subj "/CN=billing.internal"

openssl ca -batch -notext -config openssl.cnf -cert ca.pem -keyfile ca-key.pem \
  -in billing.csr -out billing-short.pem -extensions client_ext \
  -startdate "$(date -u +%y%m%d%H%M%SZ)" -enddate "$(date -u -d '+30 sec' +%y%m%d%H%M%SZ)"
Important

Do not reach for cfssl here. cfssl backdates notBefore by five minutes and rounds it to the nearest minute, so a profile with "expiry": "30s" mints a certificate that is already expired the instant it is created, and no backdate setting rescues a sub-minute window. Anything shorter than a few minutes needs openssl ca with an explicit -startdate/-enddate, which is what you just used.

Check it. Read the validity window. notBefore and notAfter should be about 30 seconds apart:

openssl x509 -in billing-short.pem -noout -dates

Step 2: Watch it expire

The check a TLS peer runs on every handshake is exactly openssl verify: chain plus validity window. Run it immediately and it passes:

openssl verify -CAfile ca.pem billing-short.pem      # expect: billing-short.pem: OK

Wait for the fuse to burn, then run the identical command:

sleep 31
openssl verify -CAfile ca.pem billing-short.pem      # expect: error 10 at 0 depth lookup: certificate has expired

Check it. Same cert, same CA, same command, opposite result. Nothing was revoked and nothing was contacted; the cert simply aged out. A server presented this cert one second too late would reject the handshake for this reason.

Is `openssl verify` really what a TLS peer does?

Yes, for the part that matters here. Chain construction plus the validity-window check is the same code path a TLS implementation runs on the certificate its peer presents; openssl verify just runs it on demand instead of mid-handshake.

Resist the urge to "prove" it with openssl s_server -Verify and s_client in one terminal. Under TLS 1.3 the client certificate is sent after the server finishes its side, so the server's rejection arrives as a post-handshake alert that s_client often never surfaces. It prints Verify return code: 0 (ok) (which is the client's opinion of the server's cert) and looks like a success. You would be reading the wrong line.

You will watch a real peer refuse an expired certificate in the capstone, where MariaDB drops the connection outright. Here, openssl verify is the honest tool.

Renewal Is the Engine

Step 3: Renewal is the engine

A workload whose cert expired does not panic, it asks the CA for a new one. Re-issue the exact same identity with a fresh window:

openssl ca -batch -notext -config openssl.cnf -cert ca.pem -keyfile ca-key.pem \
  -in billing.csr -out billing-short.pem -extensions client_ext \
  -startdate "$(date -u +%y%m%d%H%M%SZ)" -enddate "$(date -u -d '+30 sec' +%y%m%d%H%M%SZ)"
openssl verify -CAfile ca.pem billing-short.pem      # OK again

Check it. The renewed cert verifies again. Now sit with what that means for an attacker: a leaf key copied off a box is worthless after 30 seconds, and the thief cannot renew it, because renewal happens at the CA, which the thief cannot reach (and, in Lab 4, would re-demand hardware proof before signing). Theft is time-boxed to the TTL.

Note

Thirty seconds is for the demo. Real systems pick TTLs from minutes (service-mesh identities like SPIFFE) to hours or a few days, traded against renewal load. The principle holds at every scale: the shorter the life, the smaller the blast radius of a leak and the less you ever need to revoke.

Step 4: Why revocation faded

Before short-lived certs, the answer to "this cert is compromised but not yet expired" was revocation: publish that the serial is dead and make clients check. Two mechanisms competed, and the industry has now largely moved past both.

  • OCSP had clients ask the CA about each cert in real time. It was a privacy leak (the CA saw every site you visited) and a reliability and scale problem. Let's Encrypt shut its OCSP responders off in 2025 and dropped OCSP URLs from new certificates.
  • CRLs (signed lists of revoked serials) became mandatory again at the CA/Browser Forum, but a list of every revoked serial scales badly.
  • Short-lived certificates sidestep the whole question: if a cert lives for an hour, an hour-long revocation window is the cert just expiring on its own. This is the direction the ecosystem chose, and it is exactly what you built in Step 1.

You can still see where revocation metadata would live in a cert that carries it:

out=$(openssl x509 -in billing-short.pem -noout -ext crlDistributionPoints,authorityInfoAccess 2>/dev/null)
[ -n "$out" ] && echo "$out" \
  || echo "No CRL/OCSP pointers in this cert (short-lived certs often omit them on purpose)."

Check it. Our short-lived cert carries no revocation pointers, and that is the point: its lifetime is its revocation.

Recap

You made a certificate's lifetime do real work:

  • A 30-second cert verified, then failed the identical check once it aged out. Expiry is enforced at every handshake, for free, with no network call.
  • Renewal re-derived the identity from the CA, which is why a stolen short-lived cert is worthless and why renewal is the engine rather than a chore.
  • Revocation faded because short TTLs make it nearly unnecessary, which is why OCSP was retired and short-lived certs won.

The one thing still hand-waved: renewal is only safe if the CA can refuse to renew for a thief. That requires proving, at renewal time, that the requester is genuinely the right machine. Lab 4 puts the keys in hardware and makes the CA demand that proof.