A two-tier setup protects the root CA: it signs only intermediate CAs and is unavailable most of the time. Certificates for services are issued by the intermediate CA in Stronghold.
Goal
Deploy a root CA, an intermediate CA in Stronghold, a role for issuing server certificates, CRL and OCSP publishing, and connect consumers: cert-manager and ACME clients.
Prerequisites
Before you start, prepare the following:
- A Stronghold token with permissions to enable secrets engines and write to
pki*/. - The
jqandopensslutilities on your workstation. - An external Stronghold address reachable by clients:
https://stronghold.example.comin the examples. - A domain for certificates:
example.comin the examples.
Step 1. Prepare the root CA
Choose one of the options.
Option A. Offline root CA
The root CA is kept outside Stronghold (for example, on an isolated machine or in an HSM) and is used only to sign intermediate CAs. In this option, there is no pki_root mount in Stronghold, and step 3 is performed with the offline CA tools (openssl ca, openssl x509 -req, or your HSM software). Save the root CA certificate to the root-ca.pem file.
In this option, the root CA private key is not available through the Stronghold API: a compromise of Stronghold or of a Stronghold token does not expose it.
Option B. Root CA in a separate mount
If you do not use an offline CA, keep the root CA in a separate mount that only PKI administrators can access.
Enable the secrets engine and set the maximum lifetime:
d8 stronghold secrets enable -path=pki_root pki d8 stronghold secrets tune -max-lease-ttl=87600h pki_rootGenerate the root CA. The private key (
internal) never leaves Stronghold:d8 stronghold write -field=certificate pki_root/root/generate/internal common_name="Example Root CA" issuer_name="root-2026" ttl=87600h > root-ca.pemPublish the issuing certificate and CRL addresses of the root CA:
d8 stronghold write pki_root/config/urls issuing_certificates="https://stronghold.example.com/v1/pki_root/ca" crl_distribution_points="https://stronghold.example.com/v1/pki_root/crl"
For a GOST CA, set the key_type parameter, for example key_type=gost3410-256-paramset-a. The list of allowed values is given in the API reference.
Step 2. Create an intermediate CA
Create a mount for the intermediate CA and a certificate signing request.
Enable a separate mount for the intermediate CA:
d8 stronghold secrets enable -path=pki_int pki d8 stronghold secrets tune -max-lease-ttl=43800h pki_intGenerate a key and a certificate signing request (CSR):
d8 stronghold write -format=json pki_int/intermediate/generate/internal common_name="Example Intermediate CA" | jq -r '.data.csr' > pki_int.csr
Step 3. Sign the intermediate CA with the root
For option B, run:
d8 stronghold write -format=json pki_root/root/sign-intermediate csr=@pki_int.csr format=pem_bundle ttl=43800h | jq -r '.data.certificate' > pki_int.pemFor option A, sign pki_int.csr on the offline CA and build a pki_int.pem file that contains the intermediate CA certificate and the root CA certificate.
Step 4. Import the signed certificate
Import the signed intermediate CA certificate into the pki_int mount:
d8 stronghold write pki_int/intermediate/set-signed certificate=@pki_int.pemSet the CRL, OCSP, and issuing certificate addresses that will be written into issued certificates:
d8 stronghold write pki_int/config/urls issuing_certificates="https://stronghold.example.com/v1/pki_int/ca" crl_distribution_points="https://stronghold.example.com/v1/pki_int/crl" ocsp_servers="https://stronghold.example.com/v1/pki_int/ocsp"Step 5. Create a role
The role restricts domains and certificate lifetime:
d8 stronghold write pki_int/roles/example-com allowed_domains="example.com" allow_subdomains=true max_ttl=720hKeep max_ttl short: the shorter the lifetime, the less often revocation is needed and the smaller the CRL.
Step 6. Issue a certificate
Issue a certificate for api.example.com with the example-com role:
d8 stronghold write pki_int/issue/example-com common_name="api.example.com" ttl=72hThe response contains certificate, issuing_ca, ca_chain, private_key, and serial_number. If the private key is generated on the client side, send a CSR to the pki_int/sign/example-com endpoint.
Step 7. Configure CRL and OCSP
Configure CRL rebuilding and cleanup of the certificate store.
Enable automatic CRL rebuilding:
d8 stronghold write pki_int/config/crl auto_rebuild=true expiry=72hEnable automatic cleanup of expired and revoked certificates:
d8 stronghold write pki_int/config/auto-tidy enabled=true tidy_cert_store=true tidy_revoked_certs=true safety_buffer=72hRevoke a certificate by its serial number:
d8 stronghold write pki_int/revoke serial_number=<SERIAL_NUMBER><SERIAL_NUMBER>is theserial_numbervalue from the issue response.
Step 8. Connect cert-manager
To issue certificates for Ingress and Kubernetes pods, use cert-manager with a vault Issuer pointing to pki_int/sign/example-com. Setting up the Stronghold role and cert-manager resources is described in cert-manager integration.
Step 9. Enable ACME (optional)
The PKI engine supports the ACME protocol, so certificates can be obtained with standard ACME clients.
Set the external mount address used in ACME links:
d8 stronghold write pki_int/config/cluster path="https://stronghold.example.com/v1/pki_int" aia_path="https://stronghold.example.com/v1/pki_int"Allow the headers required by the ACME protocol:
d8 stronghold 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_intEnable ACME and restrict roles:
d8 stronghold write pki_int/config/acme enabled=true allowed_roles="example-com" max_ttl=720hIn the ACME client, use the role directory
https://stronghold.example.com/v1/pki_int/roles/example-com/acme/directory. By default, account registration requires external account binding (EAB, theeab_policyparameter).
Verification
Check the result:
Check the chain of an issued certificate:
d8 stronghold write -format=json pki_int/issue/example-com common_name=test.example.com ttl=1h > test.json jq -r '.data.certificate' test.json > test.pem jq -r '.data.issuing_ca' test.json > int.pem openssl verify -CAfile root-ca.pem -untrusted int.pem test.pemCheck that the CRL is available:
curl -s https://stronghold.example.com/v1/pki_int/crl/pem | openssl crl -noout -text | headCheck the certificate status via OCSP:
openssl ocsp -issuer int.pem -VAfile int.pem -cert test.pem -url https://stronghold.example.com/v1/pki_int/ocsp -text
Cleanup
Delete the objects created in this guide.
- Revoke test certificates:
d8 stronghold write pki_int/revoke serial_number=<SERIAL_NUMBER>, where<SERIAL_NUMBER>is the certificate serial number. - Delete the local files
test.json,test.pem, andint.pem. - If the setup was for testing, disable the mounts:
d8 stronghold secrets disable pki_intandd8 stronghold secrets disable pki_root. Disabling a mount deletes the CA keys irreversibly.