HashiCorp Vault PKI Deployment
Overview
The ideal workflow for issuing certificates is a “issue-only” identity that can be embedded with various processes to automate certificate issuance, but minimized risk within those embedded processes. That workflow is defined below, with each section broken out for comprehension. These command could all be combined into a single PKI-generating shell script. Just note, elevated permissions are needed to create the initial PKI bits; then the issuing process uses a more stringent, issue-only credential.
- Only one CA certificate is allowed per secrets engine, so must define
-path
in order to establish a chain -
Can provide CSRs via external means using
data=@path_to_file.json
(in JSON format) - Reference links:
- https://www.hashicorp.com/blog/certificate-management-with-vault/
- https://www.vaultproject.io/docs/secrets/pki
Example Workflow
- Create Root CA
- Create Intermediate/Issuing CA
- Create Certificate roles (web server, smart card, code signing, etc.)
- Create Issuing policy for issue only
- Create Issuing role [system role, not engine role]
- Capture Secret ID to leverage #3
- Each use of this role will query for access token
- Use #6 logic within automation roles
Configure Engine and Root CA
# Variables
VAULT_URL="vault.kenterwebs.net"
PKI_ROOTCA="pki_kenterwebs-rootCA"
CERT_CN="kenterwebs.net"
# Enable PKI
vault secrets enable -path=${PKI_ROOTCA} pki
# Tune for Root CA
vault secrets tune -max-lease-ttl=87600h ${PKI_ROOTCA}
# Generate root certifcate and save file
vault write -field=certificate ${PKI_ROOTCA}/root/generate/internal \
common_name="${CERT_CN} Root CA" \
ttl=87600h > Root_CA_${CERT_CN}.crt
# CA and CRL URLs
vault write ${PKI_ROOTCA}/config/urls \
issuing_certificates="http://${VAULT_URL}/v1/${PKI_ROOTCA}/ca" \
crl_distribution_points="http://${VAULT_URL}/v1/${PKI_ROOTCA}/crl"
Configure Engine and Issuing/Intermediate CA
# Variables
VAULT_URL="vault.kenterwebs.net"
PKI_ROOTCA="pki_kenterwebs-rootCA"
PKI_INTCA="pki_kenterwebs-issuingCA"
CERT_CN="kenterwebs.net"
# Enable PKI
vault secrets enable -path=${PKI_INTCA} pki
# Tune for Root CA
vault secrets tune -max-lease-ttl=43800h ${PKI_INTCA}
# Generate issuing certifcate and save file
vault write -format=json ${PKI_INTCA}/intermediate/generate/internal \
common_name="${CERT_CN} Issuing CA" \
| jq -r '.data.csr' > Issuing_CA_${CERT_CN}.csr
# Sign issuing certificate with root certificate
vault write -format=json ${PKI_ROOTCA}/root/sign-intermediate csr=@Issuing_CA_${CERT_CN}.csr \
format=pem_bundle ttl="43800h" \
| jq -r '.data.certificate' > Issuing_CA_${CERT_CN}.pem
# Import signed certifcate into Vault
vault write ${PKI_INTCA}/intermediate/set-signed certificate=@Issuing_CA_${CERT_CN}.pem
# Configure Issuing CA CRLs
vault write ${PKI_INTCA}/config/urls issuing_certificates="http://${VAULT_URL}/v1/${PKI_INTCA}/ca" crl_distribution_points="http://${VAULT_URL}/v1/${PKI_INTCA}/crl"
Configure certificate issuing roles
VAULT_URL="vault.kenterwebs.net"
PKI_ROOTCA="pki_kenterwebs-rootCA"
PKI_INTCA="pki_kenterwebs-issuingCA"
CERT_CN="kenterwebs.net"
PKI_ROLE_NAME="webservers-v1"
# TTL set to 90 days
vault write ${PKI_INTCA}/roles/${PKI_ROLE_NAME} \
allowed_domains=${CERT_CN} \
allow_subdomains=true \
max_ttl="2160h"
Configure Issuing Service Identity Permissions
POLICY_NAME="certificate-issuers"
VAULT_URL="vault.kenterwebs.net"
VAULT_TOKEN="insert_priv_token_here"
PKI_ROLE_NAME="webservers-v1"
# Create PKI Issuing Policy
curl -k -X PUT -H "X-Vault-Token:$VAULT_TOKEN" \
--data "{\"policy\": \"path \\\"${PKI_INTCA}/issue/${PKI_ROLE_NAME}*\\\" {capabilities = [\\\"create\\\", \\\"read\\\", \\\"update\\\", \\\"list\\\"]}\"}" https://${VAULT_URL}/v1/sys/policy/${POLICY_NAME}
# Create PKI Issuing Role
curl -X PUT -H "X-Vault-Token:$VAULT_TOKEN" \
--data "{\"policies\":\"${POLICY_NAME}\"}" \
https://${VAULT_URL}/v1/auth/approle/role/${POLICY_NAME}
# Validation [Should return some data :)]
curl -X GET -H "X-Vault-Token:$VAULT_TOKEN" https://${VAULT_URL}/v1/sys/policy/${POLICY_NAME} | jq
curl -X GET -H "X-Vault-Token:$VAULT_TOKEN" https://${VAULT_URL}/v1/auth/approle/role/${POLICY_NAME} |jq
Request Runtime Token Access
RUNTIME_ROLE_ID=$(curl -s -X GET -H "X-Vault-Token:$VAULT_TOKEN" https://${VAULT_URL}/v1/auth/approle/role/${POLICY_NAME}/role-id | jq -r '.data | .role_id')
RUNTIME_ROLE_SECRET=$(curl -s -X POST -H "X-Vault-Token:$VAULT_TOKEN" https://${VAULT_URL}/v1/auth/approle/role/${POLICY_NAME}/secret-id | jq -r '.data | .secret_id')
RUNTIME_TOKEN=$(curl -s -X POST -d "{\"role_id\":\"${RUNTIME_ROLE_ID}\",\"secret_id\":\"${RUNTIME_ROLE_SECRET}\"}" https://${VAULT_URL}/v1/auth/approle/login | jq -r '.auth | .client_token')
export APP_TOKEN=${RUNTIME_TOKEN}
Issue Certificate
- Examples using Vault CLI or API
Vault CLI Issuance
VAULT_URL="vault.kenterwebs.net"
PKI_ROOTCA="pki_kenterwebs-rootRA"
PKI_INTCA="pki_kenterwebs-issuingCA"
PKI_ROLE_NAME="webservers-v1"
CERT_CN="kenterwebs.net"
CERT_SANS="www.kenterwebs.net"
CERT_IP_SANS="10.0.1.145"
CERT_TTL="2160h" #90 days
vault write ${PKI_INTCA}/issue/${PKI_ROLE_NAME} common_name=${CERT_CN}
# Sample with SAN and IPs included
vault write ${PKI_INTCA}/issue/${PKI_ROLE_NAME} common_name=${CERT_CN} alt_names=${CERT_SANS} ip_sans=${CERT_IP_SANS} ttl=${CERT_TTL}
Vault API Issuance
VAULT_URL="vault.kenterwebs.net"
VAULT_TOKEN="${APP_TOKEN}"
PKI_INTCA="pki_kenterwebs-issuingCA"
PKI_ROLE_NAME="webservers-v1"
CERT_CN="test.kenterwebs.net"
CERT_TTL="2160h" #90 days
curl --header "X-Vault-Token: ${VAULT_TOKEN}" \
--request POST \
--data "{\"common_name\": \"${CERT_CN}\", \"ttl\": \"${CERT_TTL}\"}" \
https://${VAULT_URL}/v1/${PKI_INTCA}/issue/${PKI_ROLE_NAME} >> ${CERT_CN}.return
echo "$(cat ${CERT_CN}.return | jq -r '.data |.certificate'| grep -v null )" > ${CERT_CN}.pem
echo "$(cat ${CERT_CN}.return | jq -r '.data |.private_key'| grep -v null )" > ${CERT_CN}.key
echo "$(cat ${CERT_CN}.return | jq -r '.data |.ca_chain[0]' | grep -v null )" > ${CERT_CN}-chain.pem
cat ${CERT_CN}.pem ${CERT_CN}.key > ${CERT_CN}.combo