HashiCorp Vault PKI Commands

Enterprise secrets management and PKI with dynamic X.509 certificate generation, automated rotation, and policy-driven issuance.

PKI Engine Setup

Enable PKI secrets engine

Mount the PKI secrets engine at the default path:

vault secrets enable pki

Set maximum TTL for root CA

Tune the PKI engine to allow certificates with up to 10-year validity:

vault secrets tune -max-lease-ttl=87600h pki

Enable intermediate CA engine

Mount a separate PKI engine for the intermediate CA at a custom path:

vault secrets enable -path=pki_int pki

Tune intermediate CA TTL

Set the maximum lease TTL for the intermediate CA (5 years):

vault secrets tune -max-lease-ttl=43800h pki_int

Enable PKI at custom path

Mount the PKI engine at a custom path for multi-tenant or environment isolation:

vault secrets enable -path=pki_prod pki

Root CA Management

Generate internal root CA

Generate a self-signed root certificate with the private key stored internally in Vault:

vault write -field=certificate pki/root/generate/internal \
  common_name="Example Root CA" \
  issuer_name="root-2024" \
  ttl=87600h

Generate exported root CA

Generate a root CA and export the private key (use for offline root CA scenarios):

vault write pki/root/generate/exported \
  common_name="Example Root CA" \
  ttl=87600h \
  key_type=rsa \
  key_bits=4096

Import existing root CA

Import an existing CA certificate and key bundle into Vault:

vault write pki/config/ca [email protected]

Configure CA and CRL URLs

Set the issuing certificate URL and CRL distribution points for the CA:

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"

Read current CA certificate

Retrieve the current CA certificate in PEM format:

vault read -field=certificate pki/cert/ca

Delete root CA

Remove the root CA and all issued certificates. This operation is irreversible:

vault delete pki/root

Intermediate CA

Generate intermediate CSR

Generate a certificate signing request for the intermediate CA:

vault write -format=json pki_int/intermediate/generate/internal \
  common_name="Example Intermediate CA" \
  issuer_name="intermediate-2024" \
  | jq -r '.data.csr' > pki_int.csr

Sign intermediate with root CA

Use the root CA to sign the intermediate CSR:

vault write -format=json pki/root/sign-intermediate \
  issuer_ref="root-2024" \
  csr=@pki_int.csr \
  format=pem_bundle \
  ttl=43800h \
  | jq -r '.data.certificate' > intermediate.cert.pem

Set signed intermediate certificate

Import the signed intermediate certificate back into the intermediate CA engine:

vault write pki_int/intermediate/set-signed [email protected]

Configure intermediate CA URLs

Set the issuing certificate and CRL URLs for the intermediate CA:

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"

Cross-sign an intermediate CA

Cross-sign an existing intermediate CA with a different root for trust chain migration:

vault write pki/root/sign-intermediate \
  csr=@existing_intermediate.csr \
  common_name="Cross-Signed Intermediate CA" \
  ttl=43800h \
  use_csr_values=true

List issuers

List all configured issuers (root and intermediate CAs) in a PKI mount:

vault list pki/issuers

Roles

Create a role for server certificates

Define a role that allows issuing certificates for a specific domain:

vault write pki_int/roles/example-dot-com \
  allowed_domains="example.com" \
  allow_subdomains=true \
  max_ttl=72h \
  key_type=rsa \
  key_bits=2048

Create a role with ECDSA keys

Define a role that issues ECDSA certificates for modern TLS:

vault write pki_int/roles/ecdsa-role \
  allowed_domains="example.com" \
  allow_subdomains=true \
  max_ttl=72h \
  key_type=ec \
  key_bits=256

Create a wildcard certificate role

Allow issuing wildcard certificates for a domain:

vault write pki_int/roles/wildcard-role \
  allowed_domains="example.com" \
  allow_subdomains=true \
  allow_glob_domains=true \
  allow_wildcard_certificates=true \
  max_ttl=72h

Create a client certificate role

Define a role for mTLS client authentication certificates:

vault write pki_int/roles/client-cert \
  allowed_domains="example.com" \
  allow_subdomains=true \
  client_flag=true \
  server_flag=false \
  max_ttl=24h \
  key_type=ec \
  key_bits=256

List all roles

View all configured roles for a PKI mount:

vault list pki_int/roles

Read role configuration

View the full configuration of an existing role:

vault read pki_int/roles/example-dot-com

Delete a role

Remove a role definition. Existing certificates issued by this role remain valid:

vault delete pki_int/roles/example-dot-com

Certificate Issuance

Issue a certificate

Request a new certificate from the intermediate CA using a defined role:

vault write pki_int/issue/example-dot-com \
  common_name="app.example.com" \
  ttl=24h

Issue with Subject Alternative Names

Include additional DNS names in the certificate:

vault write pki_int/issue/example-dot-com \
  common_name="app.example.com" \
  alt_names="api.example.com,www.example.com" \
  ttl=24h

Issue with IP SANs

Include IP addresses as subject alternative names:

vault write pki_int/issue/example-dot-com \
  common_name="app.example.com" \
  ip_sans="10.0.0.1,192.168.1.100" \
  ttl=24h

Issue with URI SANs

Include URI subject alternative names (common for SPIFFE identities):

vault write pki_int/issue/example-dot-com \
  common_name="app.example.com" \
  uri_sans="spiffe://example.com/service/app" \
  ttl=24h

Issue and save to files

Issue a certificate and extract the certificate, key, and CA chain to separate files:

vault write -format=json pki_int/issue/example-dot-com \
  common_name="app.example.com" \
  ttl=24h \
  | tee \
  >(jq -r '.data.certificate' > app.crt) \
  >(jq -r '.data.private_key' > app.key) \
  >(jq -r '.data.ca_chain[]' > ca-chain.crt)

Sign an external CSR

Sign a certificate signing request generated outside of Vault:

vault write pki_int/sign/example-dot-com \
  [email protected] \
  common_name="app.example.com" \
  ttl=24h

Issue a wildcard certificate

Request a wildcard certificate (requires a role with wildcard permissions):

vault write pki_int/issue/wildcard-role \
  common_name="*.example.com" \
  ttl=72h

Certificate Management

List all certificates

List serial numbers of all certificates issued by the PKI engine:

vault list pki_int/certs

Read a specific certificate

Retrieve a certificate by its serial number:

vault read pki_int/cert/3a-cb-16-be-04-e1-60-bb-02-4e-f1-0a-fa-68-e0-76-ff-6e-85-67

Revoke a certificate

Revoke a certificate using its serial number:

vault write pki_int/revoke \
  serial_number="3a-cb-16-be-04-e1-60-bb-02-4e-f1-0a-fa-68-e0-76-ff-6e-85-67"

Tidy certificate storage

Clean up expired certificates and revocation entries from storage:

vault write pki_int/tidy \
  tidy_cert_store=true \
  tidy_revoked_certs=true \
  safety_buffer=72h

Configure auto-tidy

Enable automatic periodic tidying of the certificate store:

vault write pki_int/config/auto-tidy \
  enabled=true \
  interval_duration=12h \
  tidy_cert_store=true \
  tidy_revoked_certs=true \
  safety_buffer=72h

Rotate CRL

Force rotation of the certificate revocation list:

vault read -field=success pki_int/crl/rotate

CRL & OCSP

Read the current CRL

Retrieve the certificate revocation list in PEM format:

vault read pki_int/cert/crl

Fetch CRL in PEM format (unauthenticated)

Retrieve the CRL via the unauthenticated HTTP endpoint:

curl -s https://vault.example.com:8200/v1/pki_int/crl/pem

Configure CRL settings

Set CRL expiry, auto-rebuild, and delta CRL options:

vault write pki_int/config/crl \
  expiry=72h \
  auto_rebuild=true \
  auto_rebuild_grace_period=12h \
  enable_delta=true \
  delta_rebuild_interval=15m

Enable OCSP responder (Vault 1.12+)

Configure the built-in OCSP responder for real-time revocation checking:

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"

Query OCSP status

Check the revocation status of a certificate using the Vault OCSP endpoint:

openssl ocsp \
  -issuer intermediate-ca.pem \
  -cert app.crt \
  -url https://vault.example.com:8200/v1/pki_int/ocsp \
  -resp_text

Read unified CRL and OCSP configuration

View the current CRL configuration including delta CRL and auto-rebuild settings:

vault read pki_int/config/crl

ACME Support (Vault 1.14+)

Configure cluster path for ACME

Set the cluster path so Vault can construct ACME directory URLs:

vault write pki/config/cluster \
  path="https://vault.example.com:8200/v1/pki" \
  aia_path="https://vault.example.com:8200/v1/pki"

Enable ACME on a PKI mount

Configure the ACME settings and enable the ACME directory:

vault write pki/config/acme \
  enabled=true

ACME directory URL

The ACME directory is available at the following paths (role-specific or default):

# Default ACME directory (uses default issuer)
https://vault.example.com:8200/v1/pki/acme/directory

# Role-specific ACME directory
https://vault.example.com:8200/v1/pki/roles/example-dot-com/acme/directory

# Issuer-specific ACME directory
https://vault.example.com:8200/v1/pki/issuer/default/acme/directory

Use with certbot

Obtain a certificate from Vault using certbot as the ACME client:

certbot certonly --standalone \
  --server https://vault.example.com:8200/v1/pki/acme/directory \
  -d app.example.com \
  --agree-tos \
  --no-eff-email

Use with acme.sh

Obtain a certificate from Vault using acme.sh:

acme.sh --issue --standalone \
  -d app.example.com \
  --server https://vault.example.com:8200/v1/pki/acme/directory

Configure external account binding

Require external account binding (EAB) for ACME clients:

vault write pki/config/acme \
  enabled=true \
  eab_policy=always-required

Read ACME configuration

View the current ACME settings:

vault read pki/config/acme

Certificate Templates & Policies

ACL policy for certificate issuance

Create a Vault ACL policy that allows issuing certificates from a specific role:

# Save as pki-issue-policy.hcl
path "pki_int/issue/example-dot-com" {
  capabilities = ["create", "update"]
}

path "pki_int/sign/example-dot-com" {
  capabilities = ["create", "update"]
}

# Allow reading the CA cert and CRL
path "pki_int/cert/ca" {
  capabilities = ["read"]
}

path "pki_int/crl" {
  capabilities = ["read"]
}

Write policy to Vault

Apply the ACL policy to Vault:

vault policy write pki-issue pki-issue-policy.hcl

Role with restricted SANs

Create a role that restricts which domains and SANs can appear in issued certificates:

vault write pki_int/roles/restricted-role \
  allowed_domains="app.example.com,api.example.com" \
  allow_bare_domains=true \
  allow_subdomains=false \
  allow_ip_sans=false \
  enforce_hostnames=true \
  require_cn=true \
  max_ttl=24h

Role with allowed URI SANs

Restrict URI SANs to a specific SPIFFE trust domain:

vault write pki_int/roles/spiffe-role \
  allowed_uri_sans="spiffe://example.com/*" \
  allow_any_name=true \
  require_cn=false \
  key_type=ec \
  key_bits=256 \
  max_ttl=1h

Read-only PKI policy

Create a policy for operators who only need to view certificates and CRLs:

# Save as pki-read-policy.hcl
path "pki_int/cert/*" {
  capabilities = ["read", "list"]
}

path "pki_int/certs" {
  capabilities = ["list"]
}

path "pki_int/roles" {
  capabilities = ["list"]
}

path "pki_int/roles/*" {
  capabilities = ["read"]
}

Integration & Automation

Vault Agent auto-renewal template

Configure Vault Agent to automatically issue and renew certificates using a template:

# vault-agent-config.hcl
auto_auth {
  method "approle" {
    config = {
      role_id_file_path   = "/etc/vault/role-id"
      secret_id_file_path = "/etc/vault/secret-id"
    }
  }
}

template {
  source      = "/etc/vault/templates/cert.tpl"
  destination = "/etc/ssl/certs/app.crt"
  perms       = 0644
  command     = "systemctl reload nginx"
}

template {
  source      = "/etc/vault/templates/key.tpl"
  destination = "/etc/ssl/private/app.key"
  perms       = 0600
  command     = "systemctl reload nginx"
}

Vault Agent certificate template

Create the Consul Template file that Vault Agent uses to render the certificate:

{{- /* cert.tpl */}}
{{- with pkiCert "pki_int/issue/example-dot-com"
    "common_name=app.example.com" "ttl=24h" -}}
{{ .Cert }}
{{ .CA }}
{{- .Key -}}
{{- end -}}

Use separate template files for the certificate and key to apply different file permissions.

Kubernetes cert-manager with Vault issuer

Configure cert-manager to use Vault as a certificate issuer in Kubernetes:

# vault-issuer.yaml
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: vault-issuer
  namespace: default
spec:
  vault:
    server: https://vault.example.com:8200
    path: pki_int/sign/example-dot-com
    auth:
      kubernetes:
        role: cert-manager
        mountPath: /v1/auth/kubernetes

Request a certificate with cert-manager

Create a cert-manager Certificate resource to request a certificate from Vault:

# certificate.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: app-tls
  namespace: default
spec:
  secretName: app-tls-secret
  issuerRef:
    name: vault-issuer
    kind: Issuer
  commonName: app.example.com
  dnsNames:
    - app.example.com
    - api.example.com
  duration: 24h
  renewBefore: 8h

Issue certificate via API with curl

Use the Vault HTTP API directly to issue a certificate:

curl --silent \
  --header "X-Vault-Token: $VAULT_TOKEN" \
  --request POST \
  --data '{
    "common_name": "app.example.com",
    "alt_names": "api.example.com",
    "ttl": "24h"
  }' \
  https://vault.example.com:8200/v1/pki_int/issue/example-dot-com

Consul Connect integration

Configure Consul to use Vault as the Connect CA provider for service mesh certificates:

# consul-server-config.hcl
connect {
  ca_provider = "vault"
  ca_config {
    address               = "https://vault.example.com:8200"
    token                 = "s.VAULT_TOKEN"
    root_pki_path         = "connect-root"
    intermediate_pki_path = "connect-intermediate"
  }
}

Important Notes

Installation:

Install via: brew install vault (macOS), apt install vault (Debian/Ubuntu), or download from releases.hashicorp.com/vault

Root CA Strategy:

In production, use an offline root CA or import an externally managed root. Generate an intermediate CA within Vault for day-to-day issuance. Never issue end-entity certificates directly from the root CA.

Short-Lived Certificates:

Vault excels at issuing short-lived certificates (hours to days). Combine with Vault Agent or cert-manager for automated renewal rather than long-validity certificates.

Multiple Issuers (Vault 1.11+):

Vault supports multiple issuers per mount, enabling CA rotation without downtime. Use the issuer_ref parameter to select specific issuers.

Performance:

For high-volume certificate issuance, consider enabling performance replication or running multiple Vault clusters. Each PKI mount maintains its own CRL.

ACME Considerations:

ACME support (Vault 1.14+) enables Vault to serve as a drop-in replacement for public CAs in internal environments. Use EAB policies to control access.

Secrets Engine vs. Transit:

The PKI secrets engine generates and manages certificates. For signing operations without certificate management, consider the Transit secrets engine instead.

Documentation:

Official docs: developer.hashicorp.com/vault/docs/secrets/pki - comprehensive guides for all PKI operations.

See Also