Build an Internal PKI with Vault

Stéphane Este-Gracias
10 min readSep 28, 2022

Automate your Certificates Lifecycle with Vault — Part 1

Photo by GuerrillaBuzz Crypto PR on Unsplash

This article is a part of a series about how to
Automate your Certificates Lifecycle with Vault.

  1. Build an Internal PKI with Vault (this article)
  2. Issue, Deploy and Renew your Private Certificates with Vault and Consul-Template
  3. Rotate your CA seamlessly using a Vault PKI
  4. Securely store Public Certificates in Vault, generated by acme.sh
  5. Codify Vault Internal PKI using Terraform

Introduction

This article contains a walkthrough to building a branch of a three-tier PKI CA with Vault using an offline Root CA generated with certstrap. Then, a dedicated role is created to issue a leaf certificate.

A summary of commands is given at the end of the article to quickly create the three-tier PKI CA.

Three-Tier PKI CA Hierarchy

In order to respect the IT security principle of Least Privilege And Separation Of Duties and to obtain flexibility of use, the Vault PKI built in this article is a branch of a three-tier PKI Certification Authority hierarchy.

Three-Tier PKI Certification Authority Hierarchy

Usually, the root of trust is anchored within an existing company Root CA outside of Vault. So, the Root CA is stored offline, namely outside of Vault.

Then, the Intermediate CA and Issuing CA are stored inside of Vault using a dedicated PKI secret engine for each. Moreover, a dedicated Role is required for issuing Leaf Certificates.

PKI CA Hierarchy with Vault

Regarding the cryptology algorithms, each CAs uses the Elliptic Curve Digital Signature Algorithm (ECDSA) using 256-bit keys and SHA-256 as the hash algorithm.

Pre-Requisites

  • Install vault 1.11.0 or higher
  • Install certstrap 1.3.0 or higher
  • Install jq 1.6 or higher

Vault Server in Dev mode

First, launch Vault Server in Dev mode using root as a root token id. The root token is used on purpose to focus only on the PKI secret engine features.

Disclaimer: On production
- Don’t use the root token
- Create auth methods, entities, groups and policies to grant or forbid access to PKI features

$ vault server -dev -dev-root-token-id=root

In a second terminal, export Vault variables and check its status.

$ export VAULT_ADDR=http://127.0.0.1:8200
$ export VAULT_TOKEN=root
$ vault status
Key Value
--- -----
Seal Type shamir
Initialized true
Sealed false
Total Shares 1
Threshold 1
Version 1.11.3
Build Date 2022-08-26T10:27:10Z
Storage Type inmem
Cluster Name vault-cluster-80f46c53
Cluster ID 075e1954-dd54-6614-f8a0-09351c840504
HA Enabled false

So, the Vault server is running now.

Root CA

Let’s start with the Root CA.

The offline Root CA is simulated using certstrap open source tool from Square.

$ certstrap --depot-path root init \
--organization "Example" \
--common-name "Example Labs Root CA v1" \
--expires "10 years" \
--curve P-256 \
--path-length 2 \
--passphrase "secret"

Created root/Example_Labs_Root_CA_v1.key (encrypted by passphrase)
Created root/Example_Labs_Root_CA_v1.crt
Created root/Example_Labs_Root_CA_v1.crl

The private key and certificate are stored in root folder.

Intermediate CA

Then, let’s create the Intermediate CA on the pki_int path.

  • Enable and tune PKI secrets engine
$ vault secrets enable -path=pki_int pki
Success! Enabled the pki secrets engine at: pki_int/
$ vault secrets tune -max-lease-ttl=43800h pki_int
Success! Tuned the secrets engine at: pki_int/
  • Configure CA and CRL URLs
$ vault write pki_int/config/urls \
issuing_certificates="http://127.0.0.1:8200/v1/pki_int/ca" \
crl_distribution_points="http://127.0.0.1:8200/v1/pki_int/crl"

Success! Data written to: pki_int/config/urls
$ vault write -format=json \
pki_int/intermediate/generate/internal \
organization="Example" \
common_name="Example Labs Intermediate CA v1.1" \
key_type=ec \
key_bits=256 \
> pki_int_v1.1.csr.json
$ cat pki_int_v1.1.csr.json

{
"request_id": "938dab2e-6691-190d-b2d2-9f5bb7b13296",
"lease_id": "",
"lease_duration": 0,
"renewable": false,
"data": {
"csr": "-----BEGIN CERTIFICATE REQUEST-----\nMIH9MIGlAgEAMEMxFTATBgNVBAoTDEV4YW1wbGUgTGFiczEqMCgGA1UEAxMhRXhh\nbXBsZSBMYWJzIEludGVybWVkaWF0ZSBDQSB2MS4xMFkwEwYHKoZIzj0CAQYIKoZI\nzj0DAQcDQgAEGh1SLApRANMRLvXwkCWlRy5N1oCMCGYRKgXU9gU2CWACHvp0N3Jr\neWIMyy+yCLQPa7PDuywOTLGC3Ko2JY0QxaAAMAoGCCqGSM49BAMCA0cAMEQCIFQa\nllfzyF0m+U3JbLPNJNteiRHNS9q529CLnYFRlev+AiBWcT+wGEC5HPe62sUsapcD\ng8x12Qwbc9/U9VoDzm1vig==\n-----END CERTIFICATE REQUEST-----",
"key_id": "26617b65-c536-9249-c3ba-9108c7662c2a"
},
"warnings": null
}
  • Extract the CSR from the response and verify it
$ cat pki_int_v1.1.csr.json | jq -r '.data.csr' > pki_int_v1.1.csr
$ openssl req -text -noout -verify -in pki_int_v1.1.csr
verify OK
Certificate Request:
Data:
Version: 1 (0x0)
Subject: O = Example, CN = Example Labs Intermediate CA v1.1
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
04:1a:1d:52:2c:0a:51:00:d3:11:2e:f5:f0:90:25:
a5:47:2e:4d:d6:80:8c:08:66:11:2a:05:d4:f6:05:
36:09:60:02:1e:fa:74:37:72:6b:79:62:0c:cb:2f:
b2:08:b4:0f:6b:b3:c3:bb:2c:0e:4c:b1:82:dc:aa:
36:25:8d:10:c5
ASN1 OID: prime256v1
NIST CURVE: P-256
Attributes:
a0:00
Signature Algorithm: ecdsa-with-SHA256
30:44:02:20:54:1a:96:57:f3:c8:5d:26:f9:4d:c9:6c:b3:cd:
24:db:5e:89:11:cd:4b:da:b9:db:d0:8b:9d:81:51:95:eb:fe:
02:20:56:71:3f:b0:18:40:b9:1c:f7:ba:da:c5:2c:6a:97:03:
83:cc:75:d9:0c:1b:73:df:d4:f5:5a:03:ce:6d:6f:8a
  • Sign and generate a certificate using the CSR with Root CA
$ certstrap --depot-path root sign \
--CA "Example Labs Root CA v1" \
--passphrase "secret" \
--intermediate \
--csr pki_int_v1.1.csr \
--expires "5 years" \
--path-length 1 \
--cert pki_int_v1.1.crt \
"Example Labs Intermediate CA v1.1"

Building intermediate
$ openssl x509 -in pki_int_v1.1.crt -text -noout
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
48:ce:f6:1d:3d:94:d6:74:c8:e7:db:c6:f4:36:7a:4c
Signature Algorithm: ecdsa-with-SHA256
Issuer: O = Example, CN = Example Labs Root CA v1
Validity
Not Before: Sep 19 11:09:07 2022 GMT
Not After : Sep 19 11:19:07 2027 GMT
Subject: O = Example, CN = Example Labs Intermediate CA v1.1
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
04:1a:1d:52:2c:0a:51:00:d3:11:2e:f5:f0:90:25:
a5:47:2e:4d:d6:80:8c:08:66:11:2a:05:d4:f6:05:
36:09:60:02:1e:fa:74:37:72:6b:79:62:0c:cb:2f:
b2:08:b4:0f:6b:b3:c3:bb:2c:0e:4c:b1:82:dc:aa:
36:25:8d:10:c5
ASN1 OID: prime256v1
NIST CURVE: P-256
X509v3 extensions:
X509v3 Key Usage: critical
Certificate Sign, CRL Sign
X509v3 Basic Constraints: critical
CA:TRUE, pathlen:1
X509v3 Subject Key Identifier:
A5:CD:22:67:C9:EA:DF:72:A9:6A:B8:62:A8:64:5D:EA:...
X509v3 Authority Key Identifier:
keyid:B0:14:CF:35:5A:2F:89:AA:FE:06:90:E4:04:06:...
Signature Algorithm: ecdsa-with-SHA256
30:45:02:21:00:92:6f:89:12:17:a9:73:11:64:b9:f3:48:fe:
d5:c2:8e:70:3d:d2:47:26:2f:8c:e8:c3:14:3c:5f:c8:ed:f9:
2e:02:20:57:d8:9c:ee:9a:7b:48:46:e6:75:1e:10:f8:4b:21:
6a:0b:fe:93:76:ee:e7:45:c5:34:e9:7f:c9:f0:1b:57:2f
$ vault write -format=json \
pki_int/intermediate/set-signed \
certificate=@pki_int_v1.1.crt \
> pki_int_v1.1.set-signed.json
$ cat pki_int_v1.1.set-signed.json

{
"request_id": "75530a92-af31-e65a-9b49-140c58759ebd",
"lease_id": "",
"lease_duration": 0,
"renewable": false,
"data": {
"imported_issuers": [
"9287d791-412b-29a5-220d-64ecd4431ed2"
],
"imported_keys": null,
"mapping": {
"9287d791-412b-29a5-220d-64ecd4431ed2": "26617b65-c536-9249-c3ba-9108c7662c2a"
}
},
"warnings": null
}

Issuing CA

Next, let’s create the Issuing CA on the pki_intpath. The steps are similar to the previous ones, except for the “Sign and generate certificate” step.

  • Enable and tune PKI secrets engine for issuing CA
$ vault secrets enable -path=pki_iss pki
Success! Enabled the pki secrets engine at: pki_iss/
$ vault secrets tune -max-lease-ttl=8760h pki_iss
Success! Tuned the secrets engine at: pki_iss/
  • Configure CA and CRL URLs
$ vault write pki_iss/config/urls \
issuing_certificates="http://127.0.0.1:8200/v1/pki_iss/ca" \
crl_distribution_points="http://127.0.0.1:8200/v1/pki_iss/crl"
Success! Data written to: pki_iss/config/urls
$ vault write -format=json \
pki_iss/intermediate/generate/internal \
organization="Example" \
common_name="Example Labs Issuing CA v1.1.1" \
key_type=ec \
key_bits=256 \
> pki_iss_v1.1.1.csr.json

$ cat pki_iss_v1.1.1.csr.json
{
"request_id": "e6d63846-5b30-bbe9-3605-4757056f64c5",
"lease_id": "",
"lease_duration": 0,
"renewable": false,
"data": {
"csr": "-----BEGIN CERTIFICATE REQUEST-----\nMIH7MIGiAgEAMEAxFTATBgNVBAoTDEV4YW1wbGUgTGFiczEnMCUGA1UEAxMeRXhh\nbXBsZSBMYWJzIElzc3VpbmcgQ0EgdjEuMS4xMFkwEwYHKoZIzj0CAQYIKoZIzj0D\nAQcDQgAEm26d0gE96prsTtEjGMmvmMr8tlDPWB14+VhRfCCHeKgOv9F+9dhjvEf2\nx3QsiymJi5DlTWKwzqjLn0n43urJbKAAMAoGCCqGSM49BAMCA0gAMEUCIQDEAxSk\ntIb7hNx6igTeVrj5KURZzS2bjw1avWaKf8mQTAIgSbnA3LkAfuoIdMLU/n1QJDrm\nfQYmYlM0unAe5FmXd9o=\n-----END CERTIFICATE REQUEST-----",
"key_id": "da214457-a9c4-40db-964a-019183e42a67"
},
"warnings": null
}
  • Extract the CSR from the response and verify it
$ cat pki_iss_v1.1.1.csr.json | jq -r '.data.csr' \
> pki_iss_v1.1.1.csr
$ openssl req -text -noout -verify -in pki_iss_v1.1.1.csr

verify OK
Certificate Request:
Data:
Version: 1 (0x0)
Subject: O = Example, CN = Example Labs Issuing CA v1.1.1
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
04:9b:6e:9d:d2:01:3d:ea:9a:ec:4e:d1:23:18:c9:
af:98:ca:fc:b6:50:cf:58:1d:78:f9:58:51:7c:20:
87:78:a8:0e:bf:d1:7e:f5:d8:63:bc:47:f6:c7:74:
2c:8b:29:89:8b:90:e5:4d:62:b0:ce:a8:cb:9f:49:
f8:de:ea:c9:6c
ASN1 OID: prime256v1
NIST CURVE: P-256
Attributes:
a0:00
Signature Algorithm: ecdsa-with-SHA256
30:45:02:21:00:c4:03:14:a4:b4:86:fb:84:dc:7a:8a:04:de:
56:b8:f9:29:44:59:cd:2d:9b:8f:0d:5a:bd:66:8a:7f:c9:90:
4c:02:20:49:b9:c0:dc:b9:00:7e:ea:08:74:c2:d4:fe:7d:50:
24:3a:e6:7d:06:26:62:53:34:ba:70:1e:e4:59:97:77:da
$ vault write -format=json \
pki_int/root/sign-intermediate \
organization="Example" \
csr=@pki_iss_v1.1.1.csr \
ttl=8760h \
format=pem \
> pki_iss_v1.1.1.crt.json
$ cat pki_iss_v1.1.1.crt.json
{
"request_id": "86aa268b-b1cd-0d22-56aa-5340e9d14965",
"lease_id": "",
"lease_duration": 0,
"renewable": false,
"data": {
"ca_chain": [
"-----BEGIN CERTIFICATE-----\nMIICZjCCAgugAwIBAgIUb/TNrUqmLxColUUIBJqiMrUDYmcwCgYIKoZIzj0EAwIw\nQzEVMBMGA1UEChMMRXhhbXBsZSBMYWJzMSowKAYDVQQDEyFFeGFtcGxlIExhYnMg\nSW50ZXJtZWRpYXRlIENBIHYxLjEwHhcNMjIwOTE5MTEzNzQ2WhcNMjMwOTE5MTEz\nODE2WjBAMRUwEwYDVQQKEwxFeGFtcGxlIExhYnMxJzAlBgNVBAMTHkV4YW1wbGUg\nTGFicyBJc3N1aW5nIENBIHYxLjEuMTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA\nBJtundIBPeqa7E7RIxjJr5jK/LZQz1gdePlYUXwgh3ioDr/RfvXYY7xH9sd0LIsp\niYuQ5U1isM6oy59J+N7qyWyjgd8wgdwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB\n/wQIMAYBAf8CAQAwHQYDVR0OBBYEFEMj0q23fAXh0YqfZ+4alw5sIwv9MB8GA1Ud\nIwQYMBaAFKXNImfJ6t9yqWq4YqhkXer93+3rMD8GCCsGAQUFBwEBBDMwMTAvBggr\nBgEFBQcwAoYjaHR0cDovLzEyNy4wLjAuMTo4MjAwL3YxL3BraV9pbnQvY2EwNQYD\nVR0fBC4wLDAqoCigJoYkaHR0cDovLzEyNy4wLjAuMTo4MjAwL3YxL3BraV9pbnQv\nY3JsMAoGCCqGSM49BAMCA0kAMEYCIQDxERcoTQP3/1o74ZQzIEZGyggEJRwBSmTq\n/M4aZl0v/AIhAM3cJkSZo3DgQ7bFPtktxg1gtT5RaPjzpq+XM3eK50A+\n-----END CERTIFICATE-----",
"-----BEGIN CERTIFICATE-----\nMIIB4DCCAYagAwIBAgIQSM72HT2U1nTI59vG9DZ6TDAKBggqhkjOPQQDAjA5MRUw\nEwYDVQQKEwxFeGFtcGxlIExhYnMxIDAeBgNVBAMTF0V4YW1wbGUgTGFicyBSb290\nIENBIHYxMB4XDTIyMDkxOTExMDkwN1oXDTI3MDkxOTExMTkwN1owQzEVMBMGA1UE\nChMMRXhhbXBsZSBMYWJzMSowKAYDVQQDEyFFeGFtcGxlIExhYnMgSW50ZXJtZWRp\nYXRlIENBIHYxLjEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQaHVIsClEA0xEu\n9fCQJaVHLk3WgIwIZhEqBdT2BTYJYAIe+nQ3cmt5YgzLL7IItA9rs8O7LA5MsYLc\nqjYljRDFo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBATAd\nBgNVHQ4EFgQUpc0iZ8nq33KparhiqGRd6v3f7eswHwYDVR0jBBgwFoAUsBTPNVov\niar+BpDkBAZDn2FOpBkwCgYIKoZIzj0EAwIDSAAwRQIhAJJviRIXqXMRZLnzSP7V\nwo5wPdJHJi+M6MMUPF/I7fkuAiBX2JzumntIRuZ1HhD4SyFqC/6Tdu7nRcU06X/J\n8BtXLw==\n-----END CERTIFICATE-----"
],
"certificate": "-----BEGIN CERTIFICATE-----\nMIICZjCCAgugAwIBAgIUb/TNrUqmLxColUUIBJqiMrUDYmcwCgYIKoZIzj0EAwIw\nQzEVMBMGA1UEChMMRXhhbXBsZSBMYWJzMSowKAYDVQQDEyFFeGFtcGxlIExhYnMg\nSW50ZXJtZWRpYXRlIENBIHYxLjEwHhcNMjIwOTE5MTEzNzQ2WhcNMjMwOTE5MTEz\nODE2WjBAMRUwEwYDVQQKEwxFeGFtcGxlIExhYnMxJzAlBgNVBAMTHkV4YW1wbGUg\nTGFicyBJc3N1aW5nIENBIHYxLjEuMTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA\nBJtundIBPeqa7E7RIxjJr5jK/LZQz1gdePlYUXwgh3ioDr/RfvXYY7xH9sd0LIsp\niYuQ5U1isM6oy59J+N7qyWyjgd8wgdwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB\n/wQIMAYBAf8CAQAwHQYDVR0OBBYEFEMj0q23fAXh0YqfZ+4alw5sIwv9MB8GA1Ud\nIwQYMBaAFKXNImfJ6t9yqWq4YqhkXer93+3rMD8GCCsGAQUFBwEBBDMwMTAvBggr\nBgEFBQcwAoYjaHR0cDovLzEyNy4wLjAuMTo4MjAwL3YxL3BraV9pbnQvY2EwNQYD\nVR0fBC4wLDAqoCigJoYkaHR0cDovLzEyNy4wLjAuMTo4MjAwL3YxL3BraV9pbnQv\nY3JsMAoGCCqGSM49BAMCA0kAMEYCIQDxERcoTQP3/1o74ZQzIEZGyggEJRwBSmTq\n/M4aZl0v/AIhAM3cJkSZo3DgQ7bFPtktxg1gtT5RaPjzpq+XM3eK50A+\n-----END CERTIFICATE-----",
"expiration": 1695123496,
"issuing_ca": "-----BEGIN CERTIFICATE-----\nMIIB4DCCAYagAwIBAgIQSM72HT2U1nTI59vG9DZ6TDAKBggqhkjOPQQDAjA5MRUw\nEwYDVQQKEwxFeGFtcGxlIExhYnMxIDAeBgNVBAMTF0V4YW1wbGUgTGFicyBSb290\nIENBIHYxMB4XDTIyMDkxOTExMDkwN1oXDTI3MDkxOTExMTkwN1owQzEVMBMGA1UE\nChMMRXhhbXBsZSBMYWJzMSowKAYDVQQDEyFFeGFtcGxlIExhYnMgSW50ZXJtZWRp\nYXRlIENBIHYxLjEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQaHVIsClEA0xEu\n9fCQJaVHLk3WgIwIZhEqBdT2BTYJYAIe+nQ3cmt5YgzLL7IItA9rs8O7LA5MsYLc\nqjYljRDFo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBATAd\nBgNVHQ4EFgQUpc0iZ8nq33KparhiqGRd6v3f7eswHwYDVR0jBBgwFoAUsBTPNVov\niar+BpDkBAZDn2FOpBkwCgYIKoZIzj0EAwIDSAAwRQIhAJJviRIXqXMRZLnzSP7V\nwo5wPdJHJi+M6MMUPF/I7fkuAiBX2JzumntIRuZ1HhD4SyFqC/6Tdu7nRcU06X/J\n8BtXLw==\n-----END CERTIFICATE-----",
"serial_number": "6f:f4:cd:ad:4a:a6:2f:10:a8:95:45:08:04:9a:a2:32:b5:03:62:67"
},
"warnings": [
"Max path length of the signed certificate is zero. This certificate cannot be used to issue intermediate CA certificates."
]
}
$ cat pki_iss_v1.1.1.crt.json | jq -r '.data.certificate' \
> pki_iss_v1.1.1.crt
$ openssl x509 -in pki_iss_v1.1.1.crt -text -noout

Certificate:
Data:
Version: 3 (0x2)
Serial Number:
6f:f4:cd:ad:4a:a6:2f:10:a8:95:45:08:04:9a:a2:32:...
Signature Algorithm: ecdsa-with-SHA256
Issuer: O = Example, CN = Example Labs Intermediate CA v1.1
Validity
Not Before: Sep 19 11:37:46 2022 GMT
Not After : Sep 19 11:38:16 2023 GMT
Subject: O = Example, CN = Example Labs Issuing CA v1.1.1
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
04:9b:6e:9d:d2:01:3d:ea:9a:ec:4e:d1:23:18:c9:
af:98:ca:fc:b6:50:cf:58:1d:78:f9:58:51:7c:20:
87:78:a8:0e:bf:d1:7e:f5:d8:63:bc:47:f6:c7:74:
2c:8b:29:89:8b:90:e5:4d:62:b0:ce:a8:cb:9f:49:
f8:de:ea:c9:6c
ASN1 OID: prime256v1
NIST CURVE: P-256
X509v3 extensions:
X509v3 Key Usage: critical
Certificate Sign, CRL Sign
X509v3 Basic Constraints: critical
CA:TRUE, pathlen:0
X509v3 Subject Key Identifier:
43:23:D2:AD:B7:7C:05:E1:D1:8A:9F:67:EE:1A:97:0E:...
X509v3 Authority Key Identifier:
keyid:A5:CD:22:67:C9:EA:DF:72:A9:6A:B8:62:A8:64:...
Authority Information Access:
CA Issuers - URI:http://127.0.0.1:8200/v1/pki_int/ca
X509v3 CRL Distribution Points: Full Name:
URI:http://127.0.0.1:8200/v1/pki_int/crl
Signature Algorithm: ecdsa-with-SHA256
30:46:02:21:00:f1:11:17:28:4d:03:f7:ff:5a:3b:e1:94:33:
20:46:46:ca:08:04:25:1c:01:4a:64:ea:fc:ce:1a:66:5d:2f:
fc:02:21:00:cd:dc:26:44:99:a3:70:e0:43:b6:c5:3e:d9:2d:
c6:0d:60:b5:3e:51:68:f8:f3:a6:af:97:33:77:8a:e7:40:3e
$ cat pki_iss_v1.1.1.crt pki_int_v1.1.crt > pki_iss_v1.1.1.chain.crt
$ vault write -format=json \
pki_iss/intermediate/set-signed \
certificate=@pki_iss_v1.1.1.chain.crt \
> pki_iss_v1.1.1.set-signed.json
$ cat pki_iss_v1.1.1.set-signed.json
{
"request_id": "58a6a706-f041-afb8-2d9e-539eefdea001",
"lease_id": "",
"lease_duration": 0,
"renewable": false,
"data": {
"imported_issuers": [
"6d72b3c8-ccf4-ab81-eb87-3c656daaa497",
"a17129a6-fcf2-6ada-4dcd-c57bce0d2317"
],
"imported_keys": null,
"mapping": {
"6d72b3c8-ccf4-ab81-eb87-3c656daaa497": "da214457-a9c4-40db-964a-019183e42a67",
"a17129a6-fcf2-6ada-4dcd-c57bce0d2317": ""
}
},
"warnings": null
}

Create Role

Finally, let’s create a role for Issuing CA secret engine to issue leaf certificates on the example.com domain.

  • Create an issuing role
$ vault write pki_iss/roles/example \
organization="Example" \
allowed_domains="example.com" \
allow_subdomains=true \
allow_wildcard_certificates=false \
key_type=ec \
key_bits=256 \
generate_lease=true \
max_ttl=2160h

Success! Data written to: pki_iss/roles/example
  • Issue a leaf certificate
$ vault write -format=json \
pki_iss/issue/example \
common_name="sample.example.com" \

> pki_iss_v1.1.1.sample.crt.json
$ cat pki_iss_v1.1.1.sample.crt.json
{
"request_id": "7967516f-6d6b-fce1-4328-0cd23970c1a1",
"lease_id": "pki_iss/issue/example/TN1U6h8CQIVC4Y38XgETBsUr",
"lease_duration": 2764800,
"renewable": false,
"data": {
"ca_chain": [
"-----BEGIN CERTIFICATE-----\nMIICZjCCAgugAwIBAgIUb/TNrUqmLxColUUIBJqiMrUDYmcwCgYIKoZIzj0EAwIw\nQzEVMBMGA1UEChMMRXhhbXBsZSBMYWJzMSowKAYDVQQDEyFFeGFtcGxlIExhYnMg\nSW50ZXJtZWRpYXRlIENBIHYxLjEwHhcNMjIwOTE5MTEzNzQ2WhcNMjMwOTE5MTEz\nODE2WjBAMRUwEwYDVQQKEwxFeGFtcGxlIExhYnMxJzAlBgNVBAMTHkV4YW1wbGUg\nTGFicyBJc3N1aW5nIENBIHYxLjEuMTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA\nBJtundIBPeqa7E7RIxjJr5jK/LZQz1gdePlYUXwgh3ioDr/RfvXYY7xH9sd0LIsp\niYuQ5U1isM6oy59J+N7qyWyjgd8wgdwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB\n/wQIMAYBAf8CAQAwHQYDVR0OBBYEFEMj0q23fAXh0YqfZ+4alw5sIwv9MB8GA1Ud\nIwQYMBaAFKXNImfJ6t9yqWq4YqhkXer93+3rMD8GCCsGAQUFBwEBBDMwMTAvBggr\nBgEFBQcwAoYjaHR0cDovLzEyNy4wLjAuMTo4MjAwL3YxL3BraV9pbnQvY2EwNQYD\nVR0fBC4wLDAqoCigJoYkaHR0cDovLzEyNy4wLjAuMTo4MjAwL3YxL3BraV9pbnQv\nY3JsMAoGCCqGSM49BAMCA0kAMEYCIQDxERcoTQP3/1o74ZQzIEZGyggEJRwBSmTq\n/M4aZl0v/AIhAM3cJkSZo3DgQ7bFPtktxg1gtT5RaPjzpq+XM3eK50A+\n-----END CERTIFICATE-----",
"-----BEGIN CERTIFICATE-----\nMIIB4DCCAYagAwIBAgIQSM72HT2U1nTI59vG9DZ6TDAKBggqhkjOPQQDAjA5MRUw\nEwYDVQQKEwxFeGFtcGxlIExhYnMxIDAeBgNVBAMTF0V4YW1wbGUgTGFicyBSb290\nIENBIHYxMB4XDTIyMDkxOTExMDkwN1oXDTI3MDkxOTExMTkwN1owQzEVMBMGA1UE\nChMMRXhhbXBsZSBMYWJzMSowKAYDVQQDEyFFeGFtcGxlIExhYnMgSW50ZXJtZWRp\nYXRlIENBIHYxLjEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQaHVIsClEA0xEu\n9fCQJaVHLk3WgIwIZhEqBdT2BTYJYAIe+nQ3cmt5YgzLL7IItA9rs8O7LA5MsYLc\nqjYljRDFo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBATAd\nBgNVHQ4EFgQUpc0iZ8nq33KparhiqGRd6v3f7eswHwYDVR0jBBgwFoAUsBTPNVov\niar+BpDkBAZDn2FOpBkwCgYIKoZIzj0EAwIDSAAwRQIhAJJviRIXqXMRZLnzSP7V\nwo5wPdJHJi+M6MMUPF/I7fkuAiBX2JzumntIRuZ1HhD4SyFqC/6Tdu7nRcU06X/J\n8BtXLw==\n-----END CERTIFICATE-----"
],
"certificate": "-----BEGIN CERTIFICATE-----\nMIICgTCCAiigAwIBAgIUXvZikxkJM0hkxEBMbM6fUZWWCAgwCgYIKoZIzj0EAwIw\nQDEVMBMGA1UEChMMRXhhbXBsZSBMYWJzMScwJQYDVQQDEx5FeGFtcGxlIExhYnMg\nSXNzdWluZyBDQSB2MS4xLjEwHhcNMjIwOTE5MTE1MDU3WhcNMjIxMDIxMTE1MTI3\nWjA0MRUwEwYDVQQKEwxFeGFtcGxlIExhYnMxGzAZBgNVBAMTEnNhbXBsZS5leGFt\ncGxlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJjgzrukSR8oYWALhaL1\n+sYoddtjyXJ4308iwqnoXh99MFNta+SGMJHK1uIzAnNzSbwo0lWLf0KRVFMI4ppk\nUcujggEKMIIBBjAOBgNVHQ8BAf8EBAMCA6gwHQYDVR0lBBYwFAYIKwYBBQUHAwEG\nCCsGAQUFBwMCMB0GA1UdDgQWBBTARFqr/RtctV5Ye13yM5e8tqA18zAfBgNVHSME\nGDAWgBRDI9Ktt3wF4dGKn2fuGpcObCML/TA/BggrBgEFBQcBAQQzMDEwLwYIKwYB\nBQUHMAKGI2h0dHA6Ly8xMjcuMC4wLjE6ODIwMC92MS9wa2lfaXNzL2NhMB0GA1Ud\nEQQWMBSCEnNhbXBsZS5leGFtcGxlLmNvbTA1BgNVHR8ELjAsMCqgKKAmhiRodHRw\nOi8vMTI3LjAuMC4xOjgyMDAvdjEvcGtpX2lzcy9jcmwwCgYIKoZIzj0EAwIDRwAw\nRAIgFe0xNmg2r3XaDI9+tgrOyFBGEaxsHA96W2TutAVrgigCIEtZ9i0qmMJTb6HA\nUbA/fIZTlIgE0/c+Q2vsZWh0dwux\n-----END CERTIFICATE-----",
"expiration": 1666353087,
"issuing_ca": "-----BEGIN CERTIFICATE-----\nMIICZjCCAgugAwIBAgIUb/TNrUqmLxColUUIBJqiMrUDYmcwCgYIKoZIzj0EAwIw\nQzEVMBMGA1UEChMMRXhhbXBsZSBMYWJzMSowKAYDVQQDEyFFeGFtcGxlIExhYnMg\nSW50ZXJtZWRpYXRlIENBIHYxLjEwHhcNMjIwOTE5MTEzNzQ2WhcNMjMwOTE5MTEz\nODE2WjBAMRUwEwYDVQQKEwxFeGFtcGxlIExhYnMxJzAlBgNVBAMTHkV4YW1wbGUg\nTGFicyBJc3N1aW5nIENBIHYxLjEuMTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA\nBJtundIBPeqa7E7RIxjJr5jK/LZQz1gdePlYUXwgh3ioDr/RfvXYY7xH9sd0LIsp\niYuQ5U1isM6oy59J+N7qyWyjgd8wgdwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB\n/wQIMAYBAf8CAQAwHQYDVR0OBBYEFEMj0q23fAXh0YqfZ+4alw5sIwv9MB8GA1Ud\nIwQYMBaAFKXNImfJ6t9yqWq4YqhkXer93+3rMD8GCCsGAQUFBwEBBDMwMTAvBggr\nBgEFBQcwAoYjaHR0cDovLzEyNy4wLjAuMTo4MjAwL3YxL3BraV9pbnQvY2EwNQYD\nVR0fBC4wLDAqoCigJoYkaHR0cDovLzEyNy4wLjAuMTo4MjAwL3YxL3BraV9pbnQv\nY3JsMAoGCCqGSM49BAMCA0kAMEYCIQDxERcoTQP3/1o74ZQzIEZGyggEJRwBSmTq\n/M4aZl0v/AIhAM3cJkSZo3DgQ7bFPtktxg1gtT5RaPjzpq+XM3eK50A+\n-----END CERTIFICATE-----",
"private_key": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIIMX1kUxgTuEEzO9TBK+uOkEGAvIKtUfSdvFLg01hrI5oAoGCCqGSM49\nAwEHoUQDQgAEmODOu6RJHyhhYAuFovX6xih122PJcnjfTyLCqeheH30wU21r5IYw\nkcrW4jMCc3NJvCjSVYt/QpFUUwjimmRRyw==\n-----END EC PRIVATE KEY-----",
"private_key_type": "ec",
"serial_number": "5e:f6:62:93:19:09:33:48:64:c4:40:4c:6c:ce:9f:51:95:96:08:08"
},
"warnings": null
}
$ cat pki_iss_v1.1.1.sample.crt.json | jq -r .data.certificate \
| openssl x509 -text -noout
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
5e:f6:62:93:19:09:33:48:64:c4:40:4c:6c:ce:9f:51:95:...
Signature Algorithm: ecdsa-with-SHA256
Issuer: O = Example, CN = Example Labs Issuing CA v1.1.1
Validity
Not Before: Sep 19 11:50:57 2022 GMT
Not After : Oct 21 11:51:27 2022 GMT
Subject: O = Example Labs, CN = sample.example.com
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
04:98:e0:ce:bb:a4:49:1f:28:61:60:0b:85:a2:f5:
fa:c6:28:75:db:63:c9:72:78:df:4f:22:c2:a9:e8:
5e:1f:7d:30:53:6d:6b:e4:86:30:91:ca:d6:e2:33:
02:73:73:49:bc:28:d2:55:8b:7f:42:91:54:53:08:
e2:9a:64:51:cb
ASN1 OID: prime256v1
NIST CURVE: P-256
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature, Key Encipherment, Key Agreement
X509v3 Extended Key Usage:
TLS Web Server Authentication, TLS Web Client Auth
X509v3 Subject Key Identifier:
C0:44:5A:AB:FD:1B:5C:B5:5E:58:7B:5D:F2:33:97:BC:...
X509v3 Authority Key Identifier:
keyid:43:23:D2:AD:B7:7C:05:E1:D1:8A:9F:67:EE:1A:...
Authority Information Access:
CA Issuers - URI:http://127.0.0.1:8200/v1/pki_iss/ca
X509v3 Subject Alternative Name:
DNS:sample.example.com
X509v3 CRL Distribution Points:
Full Name:
URI:http://127.0.0.1:8200/v1/pki_iss/crl
Signature Algorithm: ecdsa-with-SHA256
30:44:02:20:15:ed:31:36:68:36:af:75:da:0c:8f:7e:b6:0a:
ce:c8:50:46:11:ac:6c:1c:0f:7a:5b:64:ee:b4:05:6b:82:28:
02:20:4b:59:f6:2d:2a:98:c2:53:6f:a1:c0:51:b0:3f:7c:86:
53:94:88:04:d3:f7:3e:43:6b:ec:65:68:74:77:0b:b1

Conclusion

This article shows a walkthrough to building a three-tier PKI CA with Vault using an offline Root CA generated with certstrap is presented. Then, a dedicated role has been created to issue a leaf certificate.

This configuration can be codified using the Terraform Vault provider to do this, read Codify Vault Internal PKI using Terraform article.

Next Steps

The next article (Part 2) explains how to Issue, Deploy and Renew your Private Certificates with Vault and Consul-Template using this Vault PKI.

References

Summary of commands

First, install vault 1.11.0 or higher, certstrap 1.3.0 or higher, and jq 1.6 or higher.

Next, to quickly create or recreate the three-tier PKI CA, here is the summary of the commands to be launched.

  • On the first terminal, launch the following command.
vault server -dev -dev-root-token-id=root
  • On a second terminal, launch the following commands.
export VAULT_ADDR=http://127.0.0.1:8200
export VAULT_TOKEN=root
certstrap --depot-path root init \
--organization "Example" \
--common-name "Example Labs Root CA v1" \
--expires "10 years" \
--curve P-256 \
--path-length 2 \
--passphrase "secret"
vault secrets enable -path=pki_int pki
vault secrets tune -max-lease-ttl=43800h pki_int
vault write pki_int/config/urls \
issuing_certificates="http://127.0.0.1:8200/v1/pki_int/ca" \
crl_distribution_points="http://127.0.0.1:8200/v1/pki_int/crl"
vault write -format=json \
pki_int/intermediate/generate/internal \
organization="Example" \
common_name="Example Labs Intermediate CA v1.1" \
key_type=ec \
key_bits=256 \
| jq -r '.data.csr' > pki_int_v1.1.csr
certstrap --depot-path root sign \
--CA "Example Labs Root CA v1" \
--passphrase "secret" \
--intermediate \
--csr pki_int_v1.1.csr \
--expires "5 years" \
--path-length 1 \
--cert pki_int_v1.1.crt \
"Example Labs Intermediate CA v1.1"
vault write -format=json \
pki_int/intermediate/set-signed \
certificate=@pki_int_v1.1.crt
vault secrets enable -path=pki_iss pki
vault secrets tune -max-lease-ttl=8760h pki_iss
vault write pki_iss/config/urls \
issuing_certificates="http://127.0.0.1:8200/v1/pki_iss/ca" \
crl_distribution_points="http://127.0.0.1:8200/v1/pki_iss/crl"
vault write -format=json \
pki_iss/intermediate/generate/internal \
organization="Example" \
common_name="Example Labs Issuing CA v1.1.1" \
key_type=ec \
key_bits=256 \
| jq -r '.data.csr' > pki_iss_v1.1.1.csr
vault write -format=json \
pki_int/root/sign-intermediate \
organization="Example" \
csr=@pki_iss_v1.1.1.csr \
ttl=8760h \
format=pem \
| jq -r '.data.certificate' > pki_iss_v1.1.1.crt
cat pki_iss_v1.1.1.crt pki_int_v1.1.crt > pki_iss_v1.1.1.chain.crt
vault write -format=json \
pki_iss/intermediate/set-signed \
certificate=@pki_iss_v1.1.1.chain.crt
vault write pki_iss/roles/example \
organization="Example" \
allowed_domains="example.com" \
allow_subdomains=true \
allow_wildcard_certificates=false \
key_type=ec \
key_bits=256 \
generate_lease=true \
max_ttl=2160h
  • Check the generated three-tier PKI CA with the following commands:
tree root
vault list -detailed pki_int/issuers
vault list -detailed pki_iss/issuers
vault read pki_iss/roles/example

--

--