Often when developing or testing some code, I need (or want) to use SSL, and one of the easiest ways to do that is to use Vault. However, it gets pretty annoying having to generate a new CA for each project, and import the CA cert into windows (less painful in Linux, but still annoying), especially as I forget which cert is in use, and accidentally clean up the wrong ones.
My solution has been to generate a single CA certificate and PrivateKey, import this into my Trusted Root Certificate Store, and then whenever I need a Vault instance, I just setup Vault to use the existing certificate and private key. The documentation for how to do this seems somewhat lacking, so here’s how I do it.
Things you’ll need:
- Docker
- Vault cli
- JQ
Generating the Root Certificate
First we need to create a Certificate, which we will do using the Vault docker container, and our local Vault CLI. We start the docker container in the background, and mark it for deletion when it stops (--rm
):
container=$(docker run -d --rm --cap-add=IPC_LOCK -p 8200:8200 -e "VAULT_DEV_ROOT_TOKEN_ID=vault" vault:latest)
export VAULT_ADDR="http://localhost:8200"
export VAULT_TOKEN="vault"
certs_dir="./ca"
max_ttl="87600h" # 10 years why not
mkdir -p $certs_dir
rm -rf $certs_dir/*.*
vault secrets enable pki
vault secrets tune -max-lease-ttl=$max_ttl pki
Finally, we generate a certificate by writing to the pki/root/generate/exported
path. If the path ends with exported
the Private Key is returned too. If you specify /internal
then the Private Key is stored internally to Vault, and never accessible.
result=$(vault write -format "json" \
pki/root/generate/exported \
common_name="Local Dev CA" \
alt_names="localhost,mshome.net" \
ttl=$max_ttl)
echo "$result" > $certs_dir/response.json
echo "$result" | jq -r .data.certificate > $certs_dir/ca.crt
echo "$result" | jq -r .data.private_key > $certs_dir/private.key
docker stop $container
We put the entire response into a json
file just incase there is something interesting we want out of it later, and store the certificate and private key into the same directory too. Note for the certificate’s alt_names
I have specified both localhost
and mshome.net
, which is the domain that Hyper-V machines use.
Lastly, we can now import the root CA into our machine/user’s Trusted Root Certification Authorities store, meaning our later uses of this certificate will be trusted by our local machine.
Creating a Vault CA
As before, we use a Docker container to run the Vault instance, except this time we import the existing CA certificate into the PKI backend. The first half of the script (run_ca.sh
) is pretty much the same as before, except we don’t delete the contents of the ./ca
directory, and our certificate max_ttl
is much lower:
docker run -d --rm --cap-add=IPC_LOCK -p 8200:8200 -e "VAULT_DEV_ROOT_TOKEN_ID=vault" vault:latest
export VAULT_ADDR="http://localhost:8200"
export VAULT_TOKEN="vault"
certs_dir="./ca"
max_ttl="72h"
vault secrets enable pki
vault secrets tune -max-lease-ttl=$max_ttl pki
The last part is to read in the certificate and private key, bundle them together, and configure the pki
backend to use them, and add a single role to use for issuing certificates:
pem=$(cat $certs_dir/ca.crt $certs_dir/private.key)
vault write pki/config/ca pem_bundle="$pem"
vault write pki/roles/cert \
allowed_domains=localhost,mshome.net \
allow_subdomains=true \
max_ttl=$max_ttl
Also note how we don’t stop the docker container either. Wouldn’t be much of a CA if it stopped the second it was configured…
Creating a Vault Intermediate CA
Sometimes, I want to test that a piece of software works when I have issued certificates from an Intermediate CA, rather than directly from the root. We can configure Vault to do this too, with a modified script which this time we start two PKI secret backends, one to act as the root, and onc as the intermediate:
#!/bin/bash
set -e
docker run -d --rm --cap-add=IPC_LOCK -p 8200:8200 -e "VAULT_DEV_ROOT_TOKEN_ID=vault" vault:latest
export VAULT_ADDR="http://localhost:8200"
export VAULT_TOKEN="vault"
# create root ca
certs_dir="./ca"
pem=$(cat $certs_dir/ca.crt $certs_dir/private.key)
vault secrets enable -path=pki_root pki
vault secrets tune -max-lease-ttl=87600h pki_root
vault write pki_root/config/ca pem_bundle="$pem"
# create the intermediate
vault secrets enable pki
vault secrets tune -max-lease-ttl=43800h pki
csr=$(vault write pki/intermediate/generate/internal \
-format=json common_name="Spectre Dev Intermdiate CA" \
| jq -r .data.csr)
intermediate=$(vault write pki_root/root/sign-intermediate \
-format=json csr="$csr" format=pem_bundle ttl=43800h \
| jq -r .data.certificate)
chained=$(echo -e "$intermediate\n$(cat $certs_dir/ca.crt)")
vault write pki/intermediate/set-signed certificate="$chained"
echo "$intermediate" > intermediate.crt
vault write pki/roles/cert \
allowed_domains=localhost,mshome.net \
allow_subdomains=true \
max_ttl=43800h
# destroy the temp root
vault secrets disable pki_root
We use the pki_root
backend to sign a CSR from the pki
(intermediate) backend, and once the signed response is stored in pki
, we delete the pki_root
backend, as it is no longer needed for our Development Intermediate CA.
Issuing Certificates
We can now use the cert
role to issue certificates for our applications, which I have in a script called issue.sh
:
#!/bin/bash
export VAULT_ADDR="http://localhost:8200"
export VAULT_TOKEN="vault"
vault write \
-format=json \
pki/issue/cert \
common_name=$1.mshome.net
This script I usually use with jq
to do something useful with:
response=$(./issue.sh consul)
cert=$(echo "$response" | jq -r .data.certificate)
key=$(echo "$response" | jq -r .data.private_key)
Cleaning Up
When I have finished with an application or demo, I can just stop the Vault container, and run the run_ca.sh
script again if I need Vault for another project.