PKI
Public Key Infrastructure — certificate authority hierarchies, chain of trust, and enrollment workflows.
CA Hierarchy
A two-tier hierarchy: offline root CA signs the intermediate; intermediate signs everything else. The root key stays offline (air-gapped or in Vault).
Root CA (offline, 10-year validity)
└── Intermediate CA (online, 3-year validity)
├── Server certificates (1-year)
├── Client certificates (90-day to 1-year)
└── Code signing certificates
Root CA — Generate Offline
Generate root CA private key — 4096-bit RSA or ed25519
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out root-ca.key
chmod 600 root-ca.key
Self-sign the root CA certificate — 10-year validity
openssl req -x509 -new -key root-ca.key -sha256 -days 3650 \
-subj "/C=US/ST=California/O=Domus Digitalis/CN=Domus Root CA" \
-out root-ca.pem
Verify the root CA
openssl x509 -in root-ca.pem -noout -subject -issuer -dates -serial
Intermediate CA
Generate intermediate key and CSR
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out intermediate-ca.key
openssl req -new -key intermediate-ca.key -sha256 \
-subj "/C=US/ST=California/O=Domus Digitalis/CN=Domus Intermediate CA" \
-out intermediate-ca.csr
Create intermediate CA extensions file
cat > intermediate-ext.cnf <<'EOF'
[v3_intermediate_ca]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
EOF
pathlen:0 prevents the intermediate from creating sub-CAs. This is deliberate: two tiers is enough.
Sign the intermediate with the root CA
openssl x509 -req -in intermediate-ca.csr -CA root-ca.pem -CAkey root-ca.key \
-CAcreateserial -sha256 -days 1095 \
-extfile intermediate-ext.cnf -extensions v3_intermediate_ca \
-out intermediate-ca.pem
Build the CA chain file — servers need this
cat intermediate-ca.pem root-ca.pem > ca-chain.pem
CSR Generation
Generate key and CSR in one command — server certificate
openssl req -new -newkey rsa:2048 -nodes \
-keyout server.key \
-subj "/C=US/ST=California/O=Domus Digitalis/CN=web.inside.domusdigitalis.dev" \
-out server.csr
CSR with Subject Alternative Names — required since Chrome 58
openssl req -new -newkey rsa:2048 -nodes \
-keyout server.key \
-subj "/CN=web.inside.domusdigitalis.dev" \
-addext "subjectAltName=DNS:web.inside.domusdigitalis.dev,DNS:web,IP:10.50.1.100" \
-out server.csr
Inspect a CSR — verify before submitting to CA
openssl req -in server.csr -noout -text | grep -A3 "Subject\|Alternative"
Signing with the Intermediate CA
Create server certificate extensions
cat > server-ext.cnf <<'EOF'
[server_cert]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = DNS:web.inside.domusdigitalis.dev,DNS:web,IP:10.50.1.100
EOF
Sign the CSR with the intermediate CA
openssl x509 -req -in server.csr -CA intermediate-ca.pem -CAkey intermediate-ca.key \
-CAcreateserial -sha256 -days 365 \
-extfile server-ext.cnf -extensions server_cert \
-out server.pem
Verify the certificate chains correctly
openssl verify -CAfile ca-chain.pem server.pem
Certificate Extensions
Server authentication — web servers, API endpoints
extendedKeyUsage = serverAuth
Client authentication — EAP-TLS, mutual TLS, ISE endpoints
extendedKeyUsage = clientAuth
Both — server that also authenticates to peers
extendedKeyUsage = serverAuth, clientAuth
Code signing
extendedKeyUsage = codeSigning
OCSP signing — for your OCSP responder
extendedKeyUsage = OCSPSigning
CRL — Certificate Revocation List
Create an empty CRL
openssl ca -gencrl -keyfile intermediate-ca.key -cert intermediate-ca.pem \
-out crl.pem -config openssl.cnf
Revoke a certificate
openssl ca -revoke compromised-cert.pem -keyfile intermediate-ca.key \
-cert intermediate-ca.pem -config openssl.cnf
Regenerate CRL after revocation
openssl ca -gencrl -keyfile intermediate-ca.key -cert intermediate-ca.pem \
-out crl.pem -config openssl.cnf
Check CRL contents
openssl crl -in crl.pem -noout -text
Vault PKI Engine
Vault automates PKI: issue, renew, and revoke certificates via API. No manual openssl commands in production.
Enable the PKI engine
vault secrets enable -path=pki pki
vault secrets tune -max-lease-ttl=87600h pki
Generate internal root CA in Vault
vault write pki/root/generate/internal \
common_name="Domus Root CA" \
ttl=87600h
Enable intermediate PKI
vault secrets enable -path=pki_int pki
vault write pki_int/intermediate/generate/internal \
common_name="Domus Intermediate CA" \
ttl=43800h
Create a role for issuing server certificates
vault write pki_int/roles/domus-server \
allowed_domains="inside.domusdigitalis.dev" \
allow_subdomains=true \
max_ttl=8760h \
key_type=rsa \
key_bits=2048 \
require_cn=false
Issue a certificate via Vault
vault write pki_int/issue/domus-server \
common_name="web.inside.domusdigitalis.dev" \
alt_names="web" \
ip_sans="10.50.1.100" \
ttl=720h
Create a role for client certificates (EAP-TLS)
vault write pki_int/roles/domus-client \
allowed_domains="inside.domusdigitalis.dev" \
allow_subdomains=true \
max_ttl=2160h \
key_type=rsa \
key_bits=2048 \
client_flag=true \
server_flag=false
Certificate Lifecycle Commands
Check certificate expiry across all certs in a directory
for cert in /etc/ssl/certs/*.pem; do
expiry=$(openssl x509 -in "$cert" -noout -enddate 2>/dev/null | cut -d= -f2)
[[ -n "$expiry" ]] && printf "%-50s %s\n" "$(basename "$cert")" "$expiry"
done | sort -k2
Find certificates expiring in the next 30 days
for cert in /etc/ssl/certs/*.pem; do
openssl x509 -in "$cert" -checkend 2592000 -noout 2>/dev/null || \
echo "EXPIRING: $(basename "$cert")"
done
Chain building — assemble the full chain in correct order
# Order: server cert → intermediate → root
cat server.pem intermediate-ca.pem root-ca.pem > fullchain.pem
# Verify the assembled chain
openssl verify -CAfile root-ca.pem -untrusted intermediate-ca.pem server.pem