Skip to main content

Automate Certificates with Vault PKI

What is the Vault PKI secret engine

Vault is an open-source tool by HashiCorp that provides secrets management, encryption as a service, and advanced data protection. It helps organizations secure, store, and control access to tokens, passwords, certificates, and encryption keys. Vault offers a unified interface to access different secret engines and provides granular access control to ensure only authorized entities can access sensitive data.

The Vault PKI (Public Key Infrastructure) secret engine allows Vault to act as a certificate authority (CA), enabling it to issue, manage, and revoke digital certificates. This engine simplifies the processes involved in PKI by providing a set of APIs to interact with and automate certificate lifecycle management.

Vault PKI secret engine can be integrated with an Automated Certificate Management Environment (ACME) client to automate the issuance and renewal of TLS certificates. ACME is a protocol for automating interactions between certificate authorities (CAs) and servers.

Using Traefik Gateway ACME certificate resolvers, you can use Vault PKI secret engine for automatic issuance, validation, and renewal or TLS certificates.

Setting Up Vault PKI for ACME

warning

ACME certificate lifecycle management protocol is supported starting on Vault v1.14.

Using the Vault PKI secret engine we are going to setup two CAs on two different mount paths:

  • Root CA: The highest level of trust in a PKI hierarchy.
  • Intermediate CA: Operate under the Root CA and is responsible for issuing ACME certificates.

Having multiple CAs will allow you to tweak certificate lifespan and reduce exposure.

note

In the following steps, we assume that the Vault server is reachable on http://vault:8200.

First, let's start with the Root CA:

Root CA
# Enable the PKI secret engine on the `/pki` mount path.
vault secrets enable pki
# Tweak certificate lifespan.
vault secrets tune -max-lease-ttl=8760h pki
# Generates a new self-signed root CA certificate and save it under `./root_ca.crt`.
vault write -field=certificate pki/root/generate/internal \
common_name="example.com" \
issuer_name="root" \
ttl=87600h > root_ca.crt
# Configure the cluster path and AIA path (Required by the ACME feature).
vault write pki/config/cluster \
path=http://vault:8200/v1/pki \
aia_path=http://vault:8200/v1/pki
# Create the role that will be used for issuing certificates from the Root CA.
vault write pki/roles/servers \
allow_any_name=true \
no_store=false
# Configure the issuing certificate endpoints, CRL distribution points, and OCSP server endpoints that
# will be encoded into issued certificates
vault write pki/config/urls \
issuing_certificates={{cluster_aia_path}}/issuer/{{issuer_id}}/der \
crl_distribution_points={{cluster_aia_path}}/issuer/{{issuer_id}}/crl/der \
ocsp_servers={{cluster_path}}/ocsp \
enable_templating=true

Then, we can continue with the intermediate CA:

Intermediate CA
# Enable the PKI secret engine on the `/pki_int` mount path.
vault secrets enable -path=pki_int pki
# Tweak certificate lifespan, shorter than the root CA.
vault secrets tune -max-lease-ttl=43800h pki_int
# Generate an intermediate certificate and save the CSR under `./pki_intermediate.csr`.
vault write -format=json pki_int/intermediate/generate/internal \
common_name="example.com Vault Intermediate Authority" \
issuer_name="intermediate" \
| jq -r '.data.csr' > pki_intermediate.csr
# Sign the intermediate certificate with the root CA private key, and save the generated certificate
# under `./intermediate.cert.pem`.
vault write -format=json pki/root/sign-intermediate \
issuer_ref="root" \
csr=@pki_intermediate.csr \
format=pem_bundle ttl="43800h" \
| jq -r '.data.certificate' > intermediate.cert.pem
# Imported the signed intermediate certificate.
vault write pki_int/intermediate/set-signed certificate=@intermediate.cert.pem
# Configure the cluster path and AIA path (Required by the ACME feature).
vault write pki_int/config/cluster \
path=http://vault:8200/v1/pki_int \
aia_path=http://vault:8200/v1/pki_int
# Create the role that will be used for issuing certificates from the Intermediate CA.
vault write pki_int/roles/intermediate \
issuer_ref="$(vault read -field=default pki_int/config/issuers)" \
allow_any_name=true \
max_ttl="720h" \
no_store=false
# Configure the issuing certificate endpoints, CRL distribution points, and OCSP server endpoints that
# will be encoded into issued certificates.
vault write pki_int/config/urls \
issuing_certificates={{cluster_aia_path}}/issuer/{{issuer_id}}/der \
crl_distribution_points={{cluster_aia_path}}/issuer/{{issuer_id}}/crl/der \
ocsp_servers={{cluster_path}}/ocsp \
enable_templating=true
# Configure the pki_int engine so it handle ACME requests correctly.
vault secrets tune \
-passthrough-request-headers=If-Modified-Since \
-allowed-response-headers=Last-Modified \
-allowed-response-headers=Location \
-allowed-response-headers=Replay-Nonce \
-allowed-response-headers=Link \
pki_int

And finally, enable the ACME feature on the intermediate CA:

Enable ACME
vault write pki_int/config/acme enabled=true

Setting Up Traefik Gateway

Once Vault server is configured, it can be used as a CA in an ACME certificate resolver. You can choose to use a non-distributed or a distributed ACME certificate resolver, as explained in the Let's Encrypt page.

To keep the example short, we are going to show the configuration for a non-distributed ACME certificate resolver.

A new certificate resolver needs to be defined with the caServer option. This option defines which CA will be responsible for issuing certificates. In our case, the pki_int secret engine on the Vault server.

Static configuration
certificatesResolvers:
my-resolver:
acme:
email: "[email protected]"
storage: "/path/to/acme.json"
caServer: "http://vault:8200/v1/pki_int/acme/directory"
httpChallenge:
entryPoint: "web"

The certificate resolver can then be used on the Ingress objects that needs certificates

Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: whoami
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls.certresolver: my-resolver
spec:
rules:
- host: my-domain.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: whoami
port:
number: 80