HashiCorp Vault’s PKI secrets engine transforms Vault into a fully functional certificate authority capable of issuing, renewing, and revoking X.509 certificates programmatically. For DevOps teams already using Vault for secrets management, the PKI engine provides a natural extension for certificate automation — eliminating manual CSR workflows and enabling short-lived certificates that reduce the blast radius of key compromise.
This guide covers everything from initial setup through production deployment, including integration patterns with Kubernetes cert-manager, Consul Connect, and CI/CD pipelines.
What Is the Vault PKI Secrets Engine?
The PKI secrets engine is a built-in Vault component that acts as a certificate authority. Unlike traditional CAs that require separate infrastructure, Vault PKI runs within your existing Vault cluster and leverages Vault’s access control, audit logging, and high-availability features.
Architecture Overview
┌─────────────────────────────────────────────────────────┐
│ Vault Cluster │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────────────────────┐ │
│ │ Root CA │ │ Intermediate CA (Issuing) │ │
│ │ (pki/) │───▶│ (pki_int/) │ │
│ │ Offline use │ │ Issues end-entity certs │ │
│ └──────────────┘ └──────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Roles (Certificate Profiles) │ │
│ │ ┌─────────┐ ┌──────────┐ ┌───────────────┐ │ │
│ │ │ web-srv │ │ internal │ │ microservice │ │ │
│ │ │ 1yr TTL │ │ 30d TTL │ │ 24hr TTL │ │ │
│ │ └─────────┘ └──────────┘ └───────────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
└──────────────────────────────┼───────────────────────────┘
▼
┌──────────────────────────────────────┐
│ Consumers │
│ cert-manager │ Consul │ Applications │
└──────────────────────────────────────┘
Key Capabilities
| Feature | Description |
|---|---|
| Dynamic certificate issuance | Generate certificates on-demand via API |
| Short-lived certificates | TTLs from minutes to years |
| Automatic rotation | Consumers request new certs before expiry |
| CRL/OCSP | Built-in revocation support |
| Multiple CAs | Separate root and intermediate hierarchies |
| Role-based access | Fine-grained control over who can issue what |
| Audit logging | Every operation logged through Vault’s audit system |
| Cross-signing | Support for CA migration scenarios |
Setting Up Root and Intermediate CAs
Step 1: Enable the Root CA
# Enable the PKI engine for the root CA
vault secrets enable -path=pki pki
# Set maximum TTL for root CA (10 years)
vault secrets tune -max-lease-ttl=87600h pki
# Generate the root certificate
vault write pki/root/generate/internal \
common_name="My Organization Root CA" \
issuer_name="root-2026" \
ttl=87600h \
key_type=rsa \
key_bits=4096
# 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: Enable the Intermediate CA
# Enable a separate PKI engine for the intermediate CA
vault secrets enable -path=pki_int pki
# Set maximum TTL (5 years)
vault secrets tune -max-lease-ttl=43800h pki_int
# Generate intermediate CSR
vault write -format=json pki_int/intermediate/generate/internal \
common_name="My Organization Intermediate CA" \
issuer_name="intermediate-2026" \
key_type=rsa \
key_bits=4096 \
| jq -r '.data.csr' > intermediate.csr
Step 3: Sign the Intermediate with the Root
# Sign the intermediate CSR with the root CA
vault write -format=json pki/root/sign-intermediate \
csr=@intermediate.csr \
format=pem_bundle \
ttl=43800h \
| jq -r '.data.certificate' > intermediate.cert.pem
# Import the signed intermediate certificate
vault write pki_int/intermediate/set-signed \
certificate=@intermediate.cert.pem
# Configure URLs for the 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" \
ocsp_servers="https://vault.example.com:8200/v1/pki_int/ocsp"
Step 4: Create Roles (Certificate Profiles)
Roles define the parameters for issued certificates:
# Role for web servers (1 year, RSA)
vault write pki_int/roles/web-server \
allowed_domains="example.com" \
allow_subdomains=true \
max_ttl=8760h \
key_type=rsa \
key_bits=2048 \
require_cn=true \
allow_ip_sans=true \
server_flag=true \
client_flag=false
# Role for internal microservices (24 hours, ECDSA)
vault write pki_int/roles/microservice \
allowed_domains="internal.example.com,svc.cluster.local" \
allow_subdomains=true \
allow_bare_domains=false \
max_ttl=24h \
key_type=ec \
key_bits=256 \
require_cn=false \
allow_ip_sans=true \
server_flag=true \
client_flag=true \
enforce_hostnames=true
# Role for client authentication (30 days)
vault write pki_int/roles/client-auth \
allowed_domains="users.example.com" \
allow_subdomains=true \
max_ttl=720h \
key_type=ec \
key_bits=256 \
client_flag=true \
server_flag=false \
no_store=true
Issuing Certificates
Via CLI
# Issue a certificate for a web server
vault write -format=json pki_int/issue/web-server \
common_name="api.example.com" \
alt_names="api-v2.example.com" \
ip_sans="10.0.1.50" \
ttl=720h
# Output includes: certificate, private_key, ca_chain, serial_number
Via API
# Issue certificate via HTTP API
curl -s --header "X-Vault-Token: $VAULT_TOKEN" \
--request POST \
--data '{
"common_name": "api.example.com",
"ttl": "720h",
"alt_names": "api-v2.example.com",
"ip_sans": "10.0.1.50"
}' \
https://vault.example.com:8200/v1/pki_int/issue/web-server | jq
Via Application Code
package main
import (
"fmt"
"log"
vault "github.com/hashicorp/vault/api"
)
func issueCertificate(client *vault.Client, commonName string, ttl string) (*vault.Secret, error) {
secret, err := client.Logical().Write("pki_int/issue/microservice", map[string]interface{}{
"common_name": commonName,
"ttl": ttl,
})
if err != nil {
return nil, fmt.Errorf("failed to issue certificate: %w", err)
}
return secret, nil
}
func main() {
config := vault.DefaultConfig()
client, err := vault.NewClient(config)
if err != nil {
log.Fatal(err)
}
secret, err := issueCertificate(client, "payment-svc.internal.example.com", "24h")
if err != nil {
log.Fatal(err)
}
cert := secret.Data["certificate"].(string)
key := secret.Data["private_key"].(string)
ca := secret.Data["issuing_ca"].(string)
// Write to files or configure TLS directly
fmt.Printf("Certificate issued, serial: %s\n", secret.Data["serial_number"])
}
Short-Lived Certificates for Microservices
Short-lived certificates (hours to days) are a paradigm shift from traditional long-lived certificates. They reduce the need for revocation infrastructure because compromised certificates expire quickly.
Benefits of Short-Lived Certificates
| Aspect | Traditional (1 year) | Short-Lived (24 hours) |
|---|---|---|
| Compromise window | Up to 365 days | Up to 24 hours |
| Revocation dependency | Critical (CRL/OCSP) | Minimal |
| Rotation complexity | Manual/scheduled | Automatic |
| Storage requirements | Persistent | Ephemeral |
| Audit trail | Sparse | Comprehensive |
Implementing Auto-Rotation
# Python service with automatic certificate rotation
import threading
import time
import hvac
from datetime import datetime, timedelta
class CertificateManager:
def __init__(self, vault_url, vault_token, role, common_name):
self.client = hvac.Client(url=vault_url, token=vault_token)
self.role = role
self.common_name = common_name
self.cert = None
self.key = None
self.expiry = None
self._renewal_thread = None
def issue_certificate(self):
response = self.client.secrets.pki.generate_certificate(
name=self.role,
common_name=self.common_name,
mount_point="pki_int",
extra_params={"ttl": "24h"}
)
self.cert = response["data"]["certificate"]
self.key = response["data"]["private_key"]
# Renew at 70% of lifetime
ttl_seconds = response["data"]["expiration"] - time.time()
self.expiry = datetime.now() + timedelta(seconds=ttl_seconds * 0.7)
return self.cert, self.key
def start_auto_renewal(self):
def renew_loop():
while True:
sleep_time = (self.expiry - datetime.now()).total_seconds()
if sleep_time > 0:
time.sleep(sleep_time)
self.issue_certificate()
print(f"Certificate renewed at {datetime.now()}")
self._renewal_thread = threading.Thread(target=renew_loop, daemon=True)
self._renewal_thread.start()
Integration with cert-manager
The Vault PKI engine integrates natively with Kubernetes cert-manager through the Vault issuer:
Configure Vault Authentication for cert-manager
# Enable Kubernetes auth in Vault
vault auth enable kubernetes
# Configure Kubernetes auth
vault write auth/kubernetes/config \
kubernetes_host="https://kubernetes.default.svc" \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
# Create policy for cert-manager
vault policy write cert-manager - <<EOF
path "pki_int/sign/web-server" {
capabilities = ["create", "update"]
}
path "pki_int/issue/web-server" {
capabilities = ["create"]
}
EOF
# Bind the policy to cert-manager's service account
vault write auth/kubernetes/role/cert-manager \
bound_service_account_names=cert-manager \
bound_service_account_namespaces=cert-manager \
policies=cert-manager \
ttl=1h
Deploy cert-manager Vault Issuer
# vault-issuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: vault-issuer
spec:
vault:
path: pki_int/sign/web-server
server: https://vault.example.com:8200
caBundle: <base64-encoded-vault-ca>
auth:
kubernetes:
role: cert-manager
mountPath: /v1/auth/kubernetes
serviceAccountRef:
name: cert-manager
---
# certificate.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: api-tls
namespace: production
spec:
secretName: api-tls-secret
duration: 24h
renewBefore: 8h
issuerRef:
name: vault-issuer
kind: ClusterIssuer
commonName: api.example.com
dnsNames:
- api.example.com
- api.internal.svc.cluster.local
privateKey:
algorithm: ECDSA
size: 256
Consul Connect Integration
Vault PKI can serve as the CA for Consul’s service mesh:
# Consul server configuration
connect {
enabled = true
ca_provider = "vault"
ca_config {
address = "https://vault.example.com:8200"
token = "vault-consul-token"
root_pki_path = "pki"
intermediate_pki_path = "pki_int"
leaf_cert_ttl = "72h"
rotation_period = "2160h"
intermediate_cert_ttl = "8760h"
private_key_type = "ec"
private_key_bits = 256
}
}
CRL and OCSP Configuration
Certificate Revocation List (CRL)
# Enable auto-tidy for CRL management
vault write pki_int/config/auto-tidy \
enabled=true \
tidy_cert_store=true \
tidy_revoked_certs=true \
tidy_expired_issuers=true \
safety_buffer="72h" \
interval_duration="12h"
# Manually revoke a certificate
vault write pki_int/revoke \
serial_number="39:dd:2e:90:b7:23:1f:8d:d3:7d:31:c5:1b:da:84:c0:ac:b8:03:49"
# Fetch current CRL
curl -s https://vault.example.com:8200/v1/pki_int/crl/pem > crl.pem
openssl crl -in crl.pem -text -noout
OCSP Responder
Vault 1.12+ includes a built-in OCSP responder:
# OCSP is automatically available at:
# GET /v1/pki_int/ocsp/<base64-encoded-request>
# POST /v1/pki_int/ocsp (with DER-encoded request body)
# Test OCSP response
openssl ocsp \
-issuer intermediate-ca.pem \
-cert server.pem \
-url https://vault.example.com:8200/v1/pki_int/ocsp \
-resp_text
Policies and Access Control
Least-Privilege Policy Examples
# Policy for web team - can only issue web server certificates
path "pki_int/issue/web-server" {
capabilities = ["create", "update"]
}
path "pki_int/sign/web-server" {
capabilities = ["create", "update"]
}
# Policy for platform team - can manage roles and issue any cert
path "pki_int/roles/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "pki_int/issue/*" {
capabilities = ["create", "update"]
}
path "pki_int/sign/*" {
capabilities = ["create", "update"]
}
path "pki_int/revoke" {
capabilities = ["create", "update"]
}
# Policy for monitoring - read-only access to CRL and CA chain
path "pki_int/cert/*" {
capabilities = ["read", "list"]
}
path "pki_int/ca/pem" {
capabilities = ["read"]
}
path "pki_int/crl/pem" {
capabilities = ["read"]
}
Performance Tuning
High-Volume Certificate Issuance
For environments issuing thousands of certificates per hour:
# Disable certificate storage for high-volume roles (reduces storage I/O)
vault write pki_int/roles/ephemeral-service \
allowed_domains="svc.cluster.local" \
allow_subdomains=true \
max_ttl=1h \
no_store=true \
generate_lease=false
# Tune the mount for performance
vault secrets tune \
-max-lease-ttl=8760h \
-default-lease-ttl=24h \
-listing-visibility=unauth \
pki_int/
Performance Benchmarks
| Configuration | Issuance Rate | Notes |
|---|---|---|
| Default (RSA 2048, storage enabled) | ~200 certs/sec | Suitable for most deployments |
| no_store=true, RSA 2048 | ~500 certs/sec | No revocation tracking |
| no_store=true, ECDSA P-256 | ~800 certs/sec | Fastest option |
| With HSM backend | ~50-100 certs/sec | HSM becomes bottleneck |
Storage Backend Considerations
# Consul storage (recommended for HA)
storage "consul" {
address = "127.0.0.1:8500"
path = "vault/"
# Tune for PKI workloads
max_parallel = 128
}
# Integrated Raft storage (simpler operations)
storage "raft" {
path = "/opt/vault/data"
# Performance tuning
performance_multiplier = 1
}
Production Deployment Considerations
High Availability
# Vault HA configuration for PKI workloads
listener "tcp" {
address = "0.0.0.0:8200"
tls_cert_file = "/opt/vault/tls/vault.crt"
tls_key_file = "/opt/vault/tls/vault.key"
}
# Enable performance standby nodes for read-heavy PKI operations
# (Enterprise feature)
seal "awskms" {
region = "us-east-1"
kms_key_id = "alias/vault-unseal"
}
Monitoring and Alerting
# Prometheus metrics to monitor
- vault_secret_kv_count{mount_point="pki_int"} # Certificate count
- vault_secret_lease_creation_count # Issuance rate
- vault_core_handle_request_duration # Latency
- vault_expire_num_leases # Active leases
# Alert rules
groups:
- name: vault-pki
rules:
- alert: HighCertIssuanceLatency
expr: vault_core_handle_request_duration{mount_point="pki_int"} > 2
for: 5m
- alert: IntermediateCAExpiringSoon
expr: vault_pki_cert_expiry_seconds{issuer="intermediate"} < 2592000
for: 1h
Backup and Disaster Recovery
# Export the intermediate CA for disaster recovery
vault read -format=json pki_int/cert/ca > intermediate-ca-backup.json
# Snapshot the entire Vault (includes PKI state)
vault operator raft snapshot save vault-backup-$(date +%Y%m%d).snap
# Test restore procedure regularly
vault operator raft snapshot restore vault-backup.snap
When Vault PKI Is Enough vs. When You Need More
Vault PKI Excels At
- Internal certificate issuance for microservices
- Short-lived certificates with automatic rotation
- Integration with HashiCorp ecosystem (Consul, Nomad, Terraform)
- Teams already operating Vault for secrets management
- DevOps-driven certificate workflows
Consider a Dedicated PKI Solution When
| Requirement | Vault PKI | Dedicated PKI Platform |
|---|---|---|
| Public CA integration | Limited | Native |
| Certificate discovery | No | Yes |
| Multi-protocol (SCEP, CMP, EST) | No (API only) | Yes |
| Compliance reporting | Basic audit logs | Purpose-built reports |
| Non-technical user workflows | No UI for cert ops | Self-service portals |
| Cross-platform agent deployment | Vault agent only | Multi-platform agents |
| Certificate visibility dashboard | Minimal | Comprehensive |
| Hybrid (public + private) management | Separate tools | Unified |
For organizations that need both Vault’s secrets management and comprehensive certificate lifecycle management, platforms like QCecuring can integrate with Vault while providing the broader visibility, compliance reporting, and multi-protocol support that Vault alone doesn’t offer.
Hybrid Architecture
Many production environments use Vault PKI for internal/ephemeral certificates while using a dedicated CLM platform for longer-lived certificates, public CA integration, and compliance:
┌─────────────────────────────────────────────────────┐
│ Certificate Lifecycle Platform │
│ (Public certs, compliance, discovery, reporting) │
└─────────────────────────┬───────────────────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Public CAs │ │ Vault PKI │ │ Cloud CAs │
│ (DigiCert, │ │ (Internal, │ │ (AWS PCA, │
│ Let's │ │ ephemeral) │ │ GCP CAS) │
│ Encrypt) │ │ │ │ │
└──────────────┘ └──────────────┘ └──────────────┘
Key Takeaways
- Vault PKI is ideal for DevOps teams already using Vault — it provides certificate automation without additional infrastructure.
- Always use a two-tier CA hierarchy with an offline or tightly controlled root CA and an online intermediate for day-to-day issuance.
- Short-lived certificates reduce risk — configure TTLs of 24 hours or less for service-to-service communication and let automation handle renewal.
- cert-manager integration makes Vault PKI seamless in Kubernetes environments, handling issuance and renewal declaratively.
- Use
no_store=truefor high-volume roles where you don’t need revocation tracking — this dramatically improves performance. - Monitor your intermediate CA expiration — a forgotten intermediate CA renewal will break all certificate issuance.
- Vault PKI has limits — it lacks certificate discovery, multi-protocol support, and compliance dashboards that dedicated PKI platforms provide.
- Plan for scale — test your Vault cluster’s certificate issuance rate under load before relying on it for production workloads with thousands of services.