HashiCorp Vault — Project: PKI Infrastructure
Build a complete PKI infrastructure with Vault — root CA, intermediate CA, certificate issuance, mTLS between services, and automated rotation.
Project Overview
Root CA (offline, long-lived)
└── Intermediate CA (Vault-managed, 1 year)
├── Web Server Certificates (90 days)
├── Client Certificates for mTLS (30 days)
└── Service Mesh Certificates (24 hours)
Step 1 — Create Root CA
# Enable PKI for root CA
vault secrets enable -path=pki pki
# Set max TTL to 10 years
vault secrets tune -max-lease-ttl=87600h pki
# Generate root certificate
vault write -field=certificate pki/root/generate/internal \
common_name="Example Corp Root CA" \
issuer_name="root-2024" \
ttl=87600h > root-ca.pem
# Configure CA and CRL URLs
vault write pki/config/urls \
issuing_certificates="https://vault.example.com:8200/v1/pki/ca" \
crl_distribution_points="https://vault.example.com:8200/v1/pki/crl"
Step 2 — Create Intermediate CA
# Enable PKI for intermediate CA
vault secrets enable -path=pki_int pki
# Set max TTL to 5 years
vault secrets tune -max-lease-ttl=43800h pki_int
# Generate CSR for intermediate
vault write -field=csr pki_int/intermediate/generate/internal \
common_name="Example Corp Intermediate CA" \
issuer_name="intermediate-2024" > pki_intermediate.csr
# Sign the intermediate CSR with the root CA
vault write -field=certificate pki/root/sign-intermediate \
csr=@pki_intermediate.csr \
format=pem_bundle \
ttl=43800h > intermediate-signed.pem
# Import the signed intermediate certificate
vault write pki_int/intermediate/set-signed \
certificate=@intermediate-signed.pem
# Configure URLs for intermediate
vault write pki_int/config/urls \
issuing_certificates="https://vault.example.com:8200/v1/pki_int/ca" \
crl_distribution_points="https://vault.example.com:8200/v1/pki_int/crl"
Step 3 — Create Certificate Roles
# Web server certificates (90 days)
vault write pki_int/roles/web-server \
allowed_domains="example.com" \
allow_subdomains=true \
max_ttl=2160h \
key_bits=2048 \
key_type=rsa \
require_cn=true
# Client certificates for mTLS (30 days)
vault write pki_int/roles/mtls-client \
allowed_domains="example.com" \
allow_subdomains=true \
max_ttl=720h \
client_flag=true \
server_flag=false
# Short-lived service mesh certificates (24 hours)
vault write pki_int/roles/service-mesh \
allowed_domains="svc.cluster.local" \
allow_subdomains=true \
max_ttl=24h \
key_type=ec \
key_bits=256
Step 4 — Issue Certificates
# Issue a web server certificate
vault write pki_int/issue/web-server \
common_name="app.example.com" \
alt_names="www.example.com,api.example.com" \
ttl=2160h
# Issue a client certificate for mTLS
vault write pki_int/issue/mtls-client \
common_name="service-a.example.com" \
ttl=720h
# Issue a short-lived service mesh certificate
vault write pki_int/issue/service-mesh \
common_name="payment.svc.cluster.local" \
ttl=24h
Step 5 — Set Up mTLS Between Services
Nginx mTLS Configuration
# Service A (server with mTLS)
server {
listen 443 ssl;
server_name service-a.example.com;
# Server certificate (from Vault)
ssl_certificate /etc/nginx/tls/server.pem;
ssl_certificate_key /etc/nginx/tls/server-key.pem;
# Require client certificate
ssl_client_certificate /etc/nginx/tls/ca-chain.pem;
ssl_verify_client on;
ssl_verify_depth 2;
location / {
proxy_pass http://127.0.0.1:8080;
}
}
Test mTLS
# This fails (no client cert)
curl https://service-a.example.com
# Error: SSL peer certificate or SSH remote key was not OK
# This works (with client cert)
curl --cert client.pem --key client-key.pem \
--cacert ca-chain.pem \
https://service-a.example.com
Step 6 — Automate Certificate Rotation
# vault-agent-pki.hcl
auto_auth {
method "approle" {
mount_path = "auth/approle"
config = {
role_id_file_path = "/etc/vault-agent/role-id"
secret_id_file_path = "/etc/vault-agent/secret-id"
}
}
}
template {
contents = <<-EOT
{{- with secret "pki_int/issue/web-server" "common_name=app.example.com" "ttl=2160h" -}}
{{ .Data.certificate }}
{{ .Data.issuing_ca }}
{{- end -}}
EOT
destination = "/etc/nginx/tls/server.pem"
command = "systemctl reload nginx"
}
template {
contents = <<-EOT
{{- with secret "pki_int/issue/web-server" "common_name=app.example.com" "ttl=2160h" -}}
{{ .Data.private_key }}
{{- end -}}
EOT
destination = "/etc/nginx/tls/server-key.pem"
perms = "0600"
command = "systemctl reload nginx"
}
Policies
# Web server certificate issuance
path "pki_int/issue/web-server" {
capabilities = ["update"]
}
# mTLS client certificates
path "pki_int/issue/mtls-client" {
capabilities = ["update"]
}
# CA chain (for trust store)
path "pki_int/ca_chain" {
capabilities = ["read"]
}
Verification
# Verify certificate chain
openssl verify -CAfile root-ca.pem -untrusted intermediate-signed.pem server.pem
# Check certificate details
openssl x509 -in server.pem -noout -text
# Check expiry
openssl x509 -in server.pem -noout -enddate