HashiCorp Vault — PKI Exercises

3 minute read

Practice PKI certificate management with these hands-on exercises.


Exercise 1 — Build a CA Hierarchy

Task: Set up a complete root CA → intermediate CA hierarchy.

  1. Enable PKI at pki/ (root) and pki_int/ (intermediate)
  2. Generate a root CA
  3. Generate an intermediate CSR
  4. Sign the intermediate with the root
  5. Import the signed intermediate
  6. Verify the chain with openssl verify
Show Answer
# Enable and tune
vault secrets enable pki
vault secrets tune -max-lease-ttl=87600h pki
vault secrets enable -path=pki_int pki
vault secrets tune -max-lease-ttl=26280h pki_int

# Root CA
vault write pki/root/generate/internal \
    common_name="Test Root CA" issuer_name="root" ttl=87600h

vault write pki/config/urls \
    issuing_certificates="$VAULT_ADDR/v1/pki/ca" \
    crl_distribution_points="$VAULT_ADDR/v1/pki/crl"

# Intermediate CSR
vault write -format=json pki_int/intermediate/generate/internal \
    common_name="Test Intermediate CA" issuer_name="intermediate" \
    | jq -r '.data.csr' > /tmp/int.csr

# Sign with root
vault write -format=json pki/root/sign-intermediate \
    issuer_ref="root" csr=@/tmp/int.csr format=pem_bundle ttl=26280h \
    | jq -r '.data.certificate' > /tmp/int.pem

# Import
vault write pki_int/intermediate/set-signed certificate=@/tmp/int.pem

# Verify
vault read -field=certificate pki/ca/pem > /tmp/root-ca.pem
vault read -field=certificate pki_int/ca/pem > /tmp/int-ca.pem
openssl verify -CAfile /tmp/root-ca.pem /tmp/int-ca.pem
# /tmp/int-ca.pem: OK ✅

Exercise 2 — Issue and Inspect Certificates

Task: Create a role and issue a certificate, then inspect it.

  1. Create a role web for *.example.com with 24h max TTL
  2. Issue a certificate for app.example.com
  3. Extract and inspect the certificate with openssl
  4. Verify the certificate is signed by the intermediate CA
Show Answer
# Create role
vault write pki_int/roles/web \
    allowed_domains="example.com" \
    allow_subdomains=true \
    max_ttl=24h

# Issue certificate
vault write -format=json pki_int/issue/web \
    common_name="app.example.com" ttl=24h > /tmp/cert.json

# Extract
jq -r '.data.certificate' /tmp/cert.json > /tmp/app.crt
jq -r '.data.private_key' /tmp/cert.json > /tmp/app.key

# Inspect
openssl x509 -in /tmp/app.crt -text -noout | head -30
# Subject: CN = app.example.com
# Issuer: CN = Test Intermediate CA
# Validity: 24 hours from now

# Verify against CA chain
openssl verify -CAfile /tmp/root-ca.pem -untrusted /tmp/int-ca.pem /tmp/app.crt
# /tmp/app.crt: OK ✅

Exercise 3 — Certificate Revocation

Task: Issue a certificate, revoke it, and verify it appears on the CRL.

  1. Issue a certificate and note the serial number
  2. Revoke the certificate
  3. Download the CRL and verify the serial is listed
Show Answer
# Issue
CERT_DATA=$(vault write -format=json pki_int/issue/web \
    common_name="revoke-me.example.com" ttl=24h)
SERIAL=$(echo $CERT_DATA | jq -r '.data.serial_number')
echo "Serial: $SERIAL"

# Revoke
vault write pki_int/revoke serial_number="$SERIAL"

# Check CRL
curl -s "$VAULT_ADDR/v1/pki_int/crl/pem" > /tmp/crl.pem
openssl crl -in /tmp/crl.pem -text -noout | grep -A1 "Serial"
# Should show the revoked serial number

Exercise 4 — Short-Lived Certificates

Task: Create a role for 1-hour certificates suitable for microservices.

  1. Create an internal-svc role with 1h max TTL for *.svc.internal
  2. Issue a certificate
  3. Verify the TTL is approximately 1 hour
  4. Discuss: why is this better than long-lived certs?
Show Answer
# Create short-lived role
vault write pki_int/roles/internal-svc \
    allowed_domains="svc.internal" \
    allow_subdomains=true \
    max_ttl=1h \
    key_type=ec \
    key_bits=256 \
    no_store=true

# Issue
vault write -format=json pki_int/issue/internal-svc \
    common_name="api.svc.internal" ttl=1h > /tmp/short-cert.json

# Check TTL
jq -r '.data.certificate' /tmp/short-cert.json > /tmp/short.crt
openssl x509 -in /tmp/short.crt -text -noout | grep -A2 "Validity"
# Not Before: (now)
# Not After: (1 hour from now)

Why short-lived is better:

  • No CRL/OCSP needed — cert expires before you’d detect compromise
  • Compromised cert has minimal window of abuse
  • Forces automation — no manual cert management
  • no_store=true reduces Vault storage overhead

Exercise 5 — PKI Policy

Task: Write a policy that allows a service to issue certificates but not manage the CA.

Show Answer
# pki-issue-policy.hcl

# Issue certificates from the "internal-svc" role only
path "pki_int/issue/internal-svc" {
  capabilities = ["create", "update"]
}

# Read CA chain (needed to verify peers)
path "pki_int/ca/pem" {
  capabilities = ["read"]
}
path "pki_int/ca_chain" {
  capabilities = ["read"]
}

# Deny CA management
path "pki_int/root/*" {
  capabilities = ["deny"]
}
path "pki_int/intermediate/*" {
  capabilities = ["deny"]
}
path "pki_int/roles/*" {
  capabilities = ["deny"]
}
vault policy write pki-issue pki-issue-policy.hcl
TOKEN=$(vault token create -policy=pki-issue -ttl=1h -field=token)

# Can issue
VAULT_TOKEN=$TOKEN vault write pki_int/issue/internal-svc \
    common_name="test.svc.internal" ttl=1h
# Success ✅

# Cannot manage roles
VAULT_TOKEN=$TOKEN vault write pki_int/roles/new-role allowed_domains="hack.com"
# Error: permission denied ❌

What’s Next?

With PKI covered, let’s automate secret delivery with Vault Agent — a client-side daemon that handles authentication, caching, and templating.

Next Chapter: Vault Agent