HashiCorp Vault — Project: PKI Infrastructure

2 minute read

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

Next: Kubernetes Secrets Project