CoursesVault from dev to productionPKI and short-lived certificates

PKI and short-lived certificates

An internal CA with 24-hour leaf certs.

In short. Vault PKI and short-lived certificates: run an internal CA that issues 24-hour leaf certs.

Advanced30 min · lesson 4 of 5

Think of an internal CA as the badge office in a secure building. It doesn't hand out permanent keys to the front door; it prints day passes that expire at midnight and mean nothing tomorrow. Vault's PKI secrets engine works the same way: instead of your team wiring long-lived certificates into config and praying they get rotated before expiry, Vault becomes an on-demand issuer that signs a fresh leaf certificate the moment a workload asks, with a lifetime measured in hours. The security win is the same as the dynamic database credentials in the previous lesson — a stolen 24-hour cert is a problem that solves itself by tomorrow morning. The operational win is that nobody is ever paged at 3am because a wildcard cert nobody remembered quietly expired.

Build a two-tier CA: offline root, online intermediate

Never let Vault sign leaf certificates directly from a root CA. The root is the thing every client trusts; you want it created once, its private key never leaving a controlled mount, and then parked. All day-to-day issuance runs through an intermediate CA that the root signs. If the intermediate is ever compromised, you revoke and reissue one intermediate instead of re-bootstrapping trust across your entire fleet. Mount two PKI engines, generate the root, then have the intermediate produce a CSR that the root signs and hands back. Tune each mount's max_lease_ttl first — it's a hard ceiling that silently caps everything issued beneath it.

stand up root and intermediate
# Root CA — long-lived, then leave it alone
vault secrets enable -path=pki_root pki
vault secrets tune -max-lease-ttl=87600h pki_root # 10y ceiling
vault write -field=certificate pki_root/root/generate/internal \
common_name="Example Corp Root CA" issuer_name="root-2026" ttl=87600h > root_ca.crt
# Intermediate CA — the workhorse Vault signs leaves from
vault secrets enable -path=pki_int pki
vault secrets tune -max-lease-ttl=43800h pki_int # 5y ceiling
# 1. intermediate generates a key + CSR (private key stays in Vault; no issuer yet)
vault write -format=json pki_int/intermediate/generate/internal \
common_name="Example Corp Intermediate CA" \
| jq -r '.data.csr' > int.csr
# 2. root signs the CSR — issuer_ref picks which root issuer does the signing
vault write -format=json pki_root/root/sign-intermediate \
issuer_ref="root-2026" [email protected] format=pem_bundle ttl=43800h \
| jq -r '.data.certificate' > int.crt
# 3. hand the signed cert back — this is what creates the intermediate issuer
vault write pki_int/intermediate/set-signed [email protected]
# 4. name the freshly-imported issuer so roles and URLs can reference it
vault write pki_int/issuer/default issuer_name="int-2026"

Define a role and issue a 24-hour leaf

A role is the policy that turns "Vault can sign anything" into "Vault will sign a cert for api.svc.example.com, RSA-2048, valid for exactly 24 hours, and nothing else." Roles constrain the allowed domains, whether subdomains and bare domains are permitted, the key type and size, and — critically — the ttl and max_ttl. Set both to 24h so callers can't request a longer-lived cert by passing their own ttl. Also point config/urls at your Vault address so issued certs carry working AIA and CRL locations; clients that fetch the issuer chain or check revocation need those embedded, and they cannot be added after the fact.

role plus issuance
# Publish issuer + CRL locations (embedded into every leaf)
vault write pki_int/config/urls \
issuing_certificates="https://vault.example.com/v1/pki_int/ca" \
crl_distribution_points="https://vault.example.com/v1/pki_int/crl"
# The role: caps domains, key type, and lifetime
vault write pki_int/roles/web-24h \
allowed_domains="svc.example.com" \
allow_subdomains=true \
key_type=rsa key_bits=2048 \
ttl=24h max_ttl=24h
# A workload requests a leaf — cert + key + issuing CA come back inline
vault write pki_int/issue/web-24h common_name="api.svc.example.com" ttl=24h
# .data.certificate .data.private_key .data.issuing_ca .data.ca_chain
How a 24-hour leaf gets minted
1Root CA
signs once, then parked
2Intermediate CA
online in pki_int mount
3Role web-24h
caps domain + 24h TTL
4Leaf cert
issued on request, expires at hour 24
Trust flows down the chain; issuance flows up from the app. The role is the choke point that makes every leaf short-lived and scoped.

The last piece is renewal. Short-lived certs are only viable if something re-issues them automatically before they lapse. In practice you don't script this by hand — Vault Agent runs as a sidecar or systemd unit, authenticates with the workload's own identity (the Kubernetes or OIDC methods from earlier in this course), and templates a fresh cert to disk on a schedule, sending SIGHUP to the server when it rotates. That closes the loop: the app never sees a bearer token, never holds a cert older than a day, and never needs a human in the renewal path.

Every issued cert lands in storage — until you tell it not to
By default Vault stores a copy of every certificate it issues so it can revoke them and build a CRL. That's fine at low volume, but a role churning out 24-hour leaves for a busy fleet writes thousands of entries a day straight into your Raft storage, and they linger until well past expiry. The backend bloats, snapshots balloon, and CRL generation crawls. The fix is no_store=true on high-volume, short-lived roles — Vault issues the cert but keeps no record of it. The trade-off is real and worth stating plainly: an unstored cert cannot be revoked through Vault, because Vault no longer knows it exists. For 24-hour certs that's usually the right call — you're already relying on the short TTL as your revocation mechanism, not the CRL. Reserve stored, revocable certs for long-lived or high-blast-radius identities, and let the ephemeral ones expire their way out of existence. Don't discover this distinction after your CRL has grown to tens of megabytes and clients start timing out fetching it.

Related