Using HashiCorp Vault’s PKI Secret Engine
Vault’s PKI Secret Engine generates dynamic X.509 certificates. It allows services to get certificates without manually generating a private key and CSR, submitting to a CA, and waiting for signed certificate. The PKI secret engine allows dynamically generating certificates, which has the following advantages over classic CA scenarios:
-
Generating certificates with short TTLs reduces the need for and/or size of CRLs.
-
Allows for ephemeral certificates that are generated upon application startup, stored in memory and discarded on shutdown.
In this guide we cover the following:
-
setup: configuring the engine to generate certificates
-
generation: generating certificates using roles
-
revocation: revoking a previously generated certificate
Prerequisites
To complete this guide, you need:
-
to complete the "Starting Vault" section of the Vault guide
-
roughly 15 minutes
-
an IDE
-
JDK 11+ installed with
JAVA_HOME
configured appropriately -
Apache Maven 3.8.1+
-
Docker installed
Setup
We assume there is a Vault running from the Vault guide, and the root token is known. The first step consists of activating the PKI Secret Engine, and configuring a CA certificate and private key:
docker exec -it dev-vault sh
export VAULT_TOKEN=s.5VUS8pte13RqekCB2fmMT3u2
vault secrets enable pki
# ==> Success! Enabled the pki secrets engine at: pki/
vault secrets tune -max-lease-ttl=8760h pki
# ==> Success! Tuned the secrets engine at: pki/
vault write pki/root/generate/internal \
common_name=my-website.com \
ttl=8760h
# ==> Success! Configured CA with self-signed root
# ==> Key Value
# ==> --- -----
# ==> certificate -----BEGIN CERTIFICATE-----...
# ==> expiration 1536807433
# ==> issuing_ca -----BEGIN CERTIFICATE-----...
# ==> serial_number 7c:f1:fb:2c:6e:4d:99:0e:82:1b:08:0a:81:ed:61:3e:1d:fa:f5:29
This example configures the CA with an internal self-signed root certificate and associated key pair that is managed by Vault. Alternatively, you can configure the CA with an existing key pair.
CA configuration can be done programmatically using VaultPKISecretEngine.generateRoot(GenerateRootOptions)
|
With the CA configured we now require a role to be defined that determines what parameters are allowed when generating certificates.
Here we create a role example-dot-com
that allows certificates with the common name
allowed to be any subdomain of my-website.com
.
vault write pki/roles/example-dot-com \
allowed_domains=my-website.com \
allow_subdomains=true \
max_ttl=72h
# ==> Success! Data written to: pki/roles/example-dot-com
Role configuration can be done programmatically using
VaultPKISecretEngine.updateRole(String role, RoleOptions options)
|
Generating Certificates
First, let’s create a simple Quarkus application with Vault and Jackson extensions:
mvn io.quarkus.platform:quarkus-maven-plugin:3.12.0:create \
-DprojectGroupId=org.acme \
-DprojectArtifactId=vault-pki-quickstart \
-DclassName="org.acme.quickstart.GreetingResource" \
-Dpath="/hello" \
-Dextensions="resteasy,vault,resteasy-jackson"
cd vault-pki-quickstart
Now, configure access to Vault from the application.properties
:
# vault url
quarkus.vault.url=http://localhost:8200
# vault authentication
quarkus.vault.authentication.userpass.username=bob
quarkus.vault.authentication.userpass.password=sinclair
We can then add a new endpoint that will allow us to generate a certificate using the configured CA & role:
@Path("/pki")
@Produces(TEXT_PLAIN)
@Consumes(TEXT_PLAIN)
public class PKIResource {
@Inject
public VaultPKISecretEngine pkiSecretEngine;
@POST
@Path("/generate")
public String generate(String subdomain) {
GenerateCertificateOptions options = new GenerateCertificateOptions()
.setSubjectCommonName(subdomain + ".my-website.com");
GeneratedCertificate generated = pkiSecretEngine.generateCertificate("example-dot-com", options);
return generated.certificate.getData();
}
}
After compiling and starting the Quarkus application, let’s generate a new certificate with a generated key pair:
curl -X POST --data 'a-subdomain' --header "Content-Type: text/plain" http://localhost:8080/pki/generate
# ==> -----BEGIN CERTIFICATE-----
# ==> ...
# ==> -----END CERTIFICATE-----
Alternatively we can generate a key pair and CSR locally and generate a certificate by having vault sign our CSR.
Let’s add a new method that accepts a CSR:
@POST
@Path("/sign")
public String sign(String csr) {
GenerateCertificateOptions options = new GenerateCertificateOptions();
SignedCertificate signed = pkiSecretEngine.signRequest("example-dot-com", csr, options);
return signed.certificate.getData();
}
Now we can generate a CSR (e.g. using OpenSSL) and pass it to our /sign
endpoint to sign and generate a
certificate from the CSR:
openssl req -newkey rsa:2048 -keyout example.key -out example.csr
curl -X POST --data @example.csr --header "Content-Type: text/plain" http://localhost:8080/pki/sign
# ==> -----BEGIN CERTIFICATE-----
# ==> ...
# ==> -----END CERTIFICATE-----
Revoking Certificates
Let’s add another new method to our PKIResource
:
@POST
@Path("/revoke")
public void revoke(String serialNumber) {
pkiSecretEngine.revokeCertificate(serialNumber);
}
And revoke a previously generated certificate:
curl -X POST --data '1d:2e:c6:06:45:18:60:0e:23:d6:c5:17:43:c0:fe:46:ed:d1:50:be' --header "Content-Type: text/plain" http://localhost:8080/pki/revoke
# ==> No Data
Dynamically Mounting PKI Engines
Quarkus’s Vault PKI support includes that ability to mount & unmount PKI engines dynamically using
the VaultPKISecretEngineFactory
& VaultSystemBackendEngine
interfaces.
To enable, or mount, a new PKI engine at specific mount path you can use the
VaultSystemBackendEngine.enable
method:
// Obtain interfaces via injection or other standard CDI method.
VaultSystemBackendEngine systemBackendEngine = ...;
VaultPKISecretEngineFactory pkiSecretEngineFactory = ...;
// Mount a PKI engine at a specified path.
EnableEngineOptions options = new EnableEngineOptions()
.setMaxLeaseTimeToLive("8760h");
systemBackendEngine.enable(VaultSecretEngine.PKI, "pki-dyn", "A dynamic PKI engine", options);
// Obtain an engine manager for the newly mounted PKI engine.
VaultPKISecretEngine dynPkiSecretEngine = pkiSecretEngineFactory.engine("pki-dyn");
// Use dynamically created engine as you please.
dynPkiSecretEngine.generateRoot(new GenerateRootOptions());
To disable (aka unmount) a PKI engine at a specific path you simply use the VaultSystemBackendEngine.disable
method:
systemBackendEngine.disable("pki-dyn");
If you want to test if a specific mount path is already in use you can use
VaultSystemBackendEngine.isEngineMounted(String) .
|
Conclusion
The PKI Secret Engine is a great tool for managing CAs and their provisioned certificates. We have seen the most obvious functions of the interface but all of the methods and modes of Vault’s PKI secret engine are supported, including:
-
Provisioning roles used to generate certificates.
-
Storing the root CA externally and issuing certificates from intermediate CAs.
-
Reading current CRLs for each provisioned engine instance.
Feel free to look at the VaultPKISecretEngine
& VaultPKISecretEngineFactory
interfaces.