DEV Community

πŸ¦„N BπŸ›‘
πŸ¦„N BπŸ›‘

Posted on • Updated on

Vault PKI Secrets Engine with Intermediate Signing Authority

Vault & Vault Enterprise offer a fully ReST enabled CA, with built in expiration and leasing. You can generate a root CA, make an intermediate and get it signed, or import your own root CA. Others on this platform have written about this in general, but I'd like to look at some of the specific details. Specifically, I want to see what happens when I generate a private key for an Intermediate CA, and create a CSR for it.

If I may paraphrase the immortal Dilbert:

Alt Text

Here's a nickel, Kid. Get yourself a better internal CA distribution API.

--Nathan Basanese, 2019

But seriously, if you've ever thought you could do a better job than DigiCert or Symantec, Vault gives you the ability to create an internal, bespoke root CA, complete with a private key that no human can ever touch.

The guide from HashiCorp, here, shows how to do that: https://learn.hashicorp.com/vault/security/sm-pki-engine

But what if you all want something more sophisticated than that? What if you have an existing PKI system deployed all over tens of thousands of different platforms?

What about creating an Intermediate in Vault, and having another CA sign that intermediate? Vault can do that.

That way, if your organization already has a CA system in place, you all can still take advantage of Vault's API for getting signed certificates.

I've made a brief guide for how to make an Intermediate signing authority in Vault, make a CSR for that Intermediate in a way that's compatible with Micro$oft CA management tools, and create certificates using that Intermediate.

I expect that this most likely works with with all versions of HashiCorp Vault Enterprise & Open Source greater than or equal to 0.8.0.

Get Ye a Vault, Laddy!

First, download and install HashiCorp Vault from here: https://vaultproject.io/downloads.html

Extract the .zip file.

Run Ye a Vault, Laddy!

Open a Terminal, navigate to the directory to which you've downloaded Vault, using cd and run the Vault binary there:

./vault server -dev -root-token-id=root

You now have your very own HashiCorp Vault. Congratulations.

encrypt all of teh things

Interact Ye with Vault, Laddy!

Open another Terminal, and run this:

export VAULT_ADDR='http://127.0.0.1:8200'
export VAULT_TOKEN='root'

Time to make a CA root.

vault write pki/root/generate/internal common_name=eff.wtf

With output:

tls$ vault write pki/root/generate/internal common_name=eff.wtf ttl=8760h
Key              Value
---              -----
certificate      -----BEGIN CERTIFICATE-----
MIIDqjCCApKgAwIBAgIUfBiOa+giZFHvf1jWQaP6UQ67djgwDQYJKoZIhvcNAQEL
BQAwFzEVMBMGA1UEAxMMYmFzYW5lc2UuY29tMB4XDTE5MTIyMDAzMzQyMFoXDTIw
MTIxOTAzMzQ1MFowFzEVMBMGA1UEAxMMYmFzYW5lc2UuY29tMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtPf4MKTfhIfe5NGkSa5HIrF2bWrvssu2WcOw
AZyls8zuAhouW2tvOm3dslUhbkI63RlrzlcxpDw0fGpouS3s9KHrUnRK39wFrQAw
HZNzDIvERshj2XsGADeFY9c+ztkbuqr0tBG5y8wceZi5aJB+EmUra4wtOraF5ACk
FVa6ni5YKU5RG7kSrn0+LUBLTBIKa+LU4lsrdSfqba8qttnMvlwe3fGZOhl+wA87
II53KTxEhLVARxt2+xCBTyBhFi66NTLLQD7RhG7Bkb/FpiLkD79OBxVTeUhvw3ll
VYJSrjDoaj3RIjmdwoP9b1Wr2QaIsxXhuQ85cDBX3K1yEOKezwIDAQABo4HtMIHq
MA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBS6/TVv
NmJ5CqGhwx9k1Knw4DsFBTAfBgNVHSMEGDAWgBS6/TVvNmJ5CqGhwx9k1Knw4DsF
BTA7BggrBgEFBQcBAQQvMC0wKwYIKwYBBQUHMAKGH2h0dHA6Ly8xMjcuMC4wLjE6
ODIwMC92MS9wa2kvY2EwFwYDVR0RBBAwDoIMYmFzYW5lc2UuY29tMDEGA1UdHwQq
MCgwJqAkoCKGIGh0dHA6Ly8xMjcuMC4wLjE6ODIwMC92MS9wa2kvY3JsMA0GCSqG
SIb3DQEBCwUAA4IBAQApG/kDT4FIPv2781IWfELZb9IZylRLIED2+LcCU4D02gSM
y8oSw3DYD1HFMTo9/Stb2K9NKby5p36MitYWXor1I/yYUwMkrW96gDytXg7zx2rT
Cu5DSVD48077urGGK73pGX7Ctlf7fgmnTGwkQAafjEQLgXeknZaILYEwWTEiqQmx
XPa/etA+8rZTUORmVO/H33ga2C21AG2KVDaTP2j2rLx16NVc4xJ6pIkfGInd8Zom
Xth53iksvAeZ/DgrPusRjMGswArYmeYk2YWr7wa1Xe0WoEXBtsiH/Hl7QMvXQq2w
/iw8nDSS16Otj0jUwtMqLv2aFOYcsp3BjkNRZWv1
-----END CERTIFICATE-----
expiration       1608348890
issuing_ca       -----BEGIN CERTIFICATE-----
MIIDqjCCApKgAwIBAgIUfBiOa+giZFHvf1jWQaP6UQ67djgwDQYJKoZIhvcNAQEL
BQAwFzEVMBMGA1UEAxMMYmFzYW5lc2UuY29tMB4XDTE5MTIyMDAzMzQyMFoXDTIw
MTIxOTAzMzQ1MFowFzEVMBMGA1UEAxMMYmFzYW5lc2UuY29tMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtPf4MKTfhIfe5NGkSa5HIrF2bWrvssu2WcOw
AZyls8zuAhouW2tvOm3dslUhbkI63RlrzlcxpDw0fGpouS3s9KHrUnRK39wFrQAw
HZNzDIvERshj2XsGADeFY9c+ztkbuqr0tBG5y8wceZi5aJB+EmUra4wtOraF5ACk
FVa6ni5YKU5RG7kSrn0+LUBLTBIKa+LU4lsrdSfqba8qttnMvlwe3fGZOhl+wA87
II53KTxEhLVARxt2+xCBTyBhFi66NTLLQD7RhG7Bkb/FpiLkD79OBxVTeUhvw3ll
VYJSrjDoaj3RIjmdwoP9b1Wr2QaIsxXhuQ85cDBX3K1yEOKezwIDAQABo4HtMIHq
MA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBS6/TVv
NmJ5CqGhwx9k1Knw4DsFBTAfBgNVHSMEGDAWgBS6/TVvNmJ5CqGhwx9k1Knw4DsF
BTA7BggrBgEFBQcBAQQvMC0wKwYIKwYBBQUHMAKGH2h0dHA6Ly8xMjcuMC4wLjE6
ODIwMC92MS9wa2kvY2EwFwYDVR0RBBAwDoIMYmFzYW5lc2UuY29tMDEGA1UdHwQq
MCgwJqAkoCKGIGh0dHA6Ly8xMjcuMC4wLjE6ODIwMC92MS9wa2kvY3JsMA0GCSqG
SIb3DQEBCwUAA4IBAQApG/kDT4FIPv2781IWfELZb9IZylRLIED2+LcCU4D02gSM
y8oSw3DYD1HFMTo9/Stb2K9NKby5p36MitYWXor1I/yYUwMkrW96gDytXg7zx2rT
Cu5DSVD48077urGGK73pGX7Ctlf7fgmnTGwkQAafjEQLgXeknZaILYEwWTEiqQmx
XPa/etA+8rZTUORmVO/H33ga2C21AG2KVDaTP2j2rLx16NVc4xJ6pIkfGInd8Zom
Xth53iksvAeZ/DgrPusRjMGswArYmeYk2YWr7wa1Xe0WoEXBtsiH/Hl7QMvXQq2w
/iw8nDSS16Otj0jUwtMqLv2aFOYcsp3BjkNRZWv1
-----END CERTIFICATE-----
serial_number    7c:18:8e:6b:e8:22:64:51:ef:7f:58:d6:41:a3:fa:51:0e:bb:76:38
tls$ 

Now, let's see what our options are for making an intermediate CA.

vault path-help pki/intermediate/generate/internal

Here's the output you should, ideally, get with Vault 1.1:

tls1$ vault path-help pki/intermediate/generate/internal 
Request:        intermediate/generate/internal
Matching Route: ^intermediate/generate/(?P<exported>\w(([\w-.]+)?\w)?)$

Generate a new CSR and private key used for signing.

## PARAMETERS

    add_basic_constraints (bool)
        Whether to add a Basic Constraints
        extension with CA: true. Only needed as a
        workaround in some compatibility scenarios
        with Active Directory Certificate Services.

    alt_names (string)
        The requested Subject Alternative Names, if any,
        in a comma-delimited list. May contain both
        DNS names and email addresses.

    common_name (string)
        The requested common name; if you want more than
        one, specify the alternative names in the alt_names
        map. If not specified when signing, the common
        name will be taken from the CSR; other names
        must still be specified in alt_names or ip_sans.

    country (slice)
        If set, Country will be set to
        this value.

    exclude_cn_from_sans (bool)
        If true, the Common Name will not be
        included in DNS or Email Subject Alternate Names.
        Defaults to false (CN is included).

    exported (string)
        Must be "internal" or "exported". If set to
        "exported", the generated private key will be
        returned. This is your *only* chance to retrieve
        the private key!

    format (string)
        Format for returned data. Can be "pem", "der",
        or "pem_bundle". If "pem_bundle" any private
        key and issuing cert will be appended to the
        certificate pem. Defaults to "pem".

    ip_sans (slice)
        The requested IP SANs, if any, in a
        comma-delimited list

    key_bits (int)
        The number of bits to use. You will almost
        certainly want to change this if you adjust
        the key_type.

    key_type (string)
        The type of key to use; defaults to RSA. "rsa"
        and "ec" are the only valid values.

    locality (slice)
        If set, Locality will be set to
        this value.

    organization (slice)
        If set, O (Organization) will be set to
        this value.

    other_sans (slice)
        Requested other SANs, in an array with the format
        <oid>;UTF8:<utf8 string value> for each entry.

    ou (slice)
        If set, OU (OrganizationalUnit) will be set to
        this value.

    postal_code (slice)
        If set, Postal Code will be set to
        this value.

    private_key_format (string)
        Format for the returned private key.
        Generally the default will be controlled by the "format"
        parameter as either base64-encoded DER or PEM-encoded DER.
        However, this can be set to "pkcs8" to have the returned
        private key contain base64-encoded pkcs8 or PEM-encoded
        pkcs8 instead. Defaults to "der".

    province (slice)
        If set, Province will be set to
        this value.

    serial_number (string)
        The requested serial number, if any. If you want
        more than one, specify alternative names in
        the alt_names map using OID 2.5.4.5.

    street_address (slice)
        If set, Street Address will be set to
        this value.

    ttl (duration (sec))
        The requested Time To Live for the certificate;
        sets the expiration date. If not specified
        the role default, backend default, or system
        default TTL is used, in that order. Cannot
        be larger than the mount max TTL. Note:
        this only has an effect when generating
        a CA cert or signing a CA cert, not when
        generating a CSR for an intermediate CA.

    uri_sans (slice)
        The requested URI SANs, if any, in a
        comma-delimited list.

## DESCRIPTION

See the API documentation for more information.
tls$

The add_basic_constraints option is in place for backwards compatibility with Micro$oft systems.

And now, we'll make an intermediate, with a basic constraint of CA: True.

vault write pki/intermediate/generate/internal add_basic_constraints=true common_name=eff.wtf ttl=8760h -output file csr_with_catrue.asc

Here's what it looks like with output:

tls$ vault write pki/intermediate/generate/internal  add_basic_constraints=true    common_name=eff.wtf     ttl=8760h
Key    Value
---    -----
csr    -----BEGIN CERTIFICATE REQUEST-----
MIICmzCCAYMCAQAwGTEXMBUGA1UEAxMObXktd2Vic2l0ZS5jb20wggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDXQGdlVA437cWAwjZjuznoBZt2KI6gcqy+
htxFIHiF7lV5zS+8MACP+ozSUtUapuPh7Bji3yMMbs/7YNgJaICWnqwjP4hKtBt5
293QLnl3SjGvb2wbKTmR0iugJ8Q96Lag1nSQ2UTOxfPvwV5ud4VgCS8sezOKJe9x
+c6KMmbgRuCeBE9MagkwhVbunMXu8rqSdO5vQPb175VnTazOYsHSQRWyfG3uHmAL
JYSOWZ0MLN4ieO75otIJmOn1CqrtYqoHsk9ukml4TL0MJdZG/W+JnlhQkDC3hDf+
hznsOAPV7wxiirX/j8H42lIj9A4Mm1F7CP3+Bs1fsO4GVIOc/drhAgMBAAGgPTA7
BgkqhkiG9w0BCQ4xLjAsMBkGA1UdEQQSMBCCDm15LXdlYnNpdGUuY29tMA8GA1Ud
EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADetBMcv8ebir2EuLDdkr8Gr
aobUDKq+/Amh/HXBE/UiqMXMTW+XGgQJHg7p0aK3tf4mmeFYpk3fmxlzujKvFPEX
2XlBR72bUbG2fJ86dU+RcPMb83kOvW6eMs3J9kPX+Z8tDS26xT4fOpjOgZxDiM4E
uPVnatFG5ajAc+Y/46zF0E9bKubynf0oIeYRsb6KDb0qiDdEQ6kcmuuI1VJqj5E8
9QmmUghoaMp6YjPYgc7BB1X5dhg2zH9ussCtJuPW39v00ez0tQdSJaW8fRTD2Fco
xOmGbGxx95jjMrKGvvtUTWym0GqQzXx25e/8u8NRu5Q61xGBjk1tPp2x3QA/DSk=
-----END CERTIFICATE REQUEST-----
tls$ 

To inspect the certificate request, copy the text starting with -----BEGIN CERT... into a file like csr_with_catrue.asc.

openssl x509 -in csr_with_catrue.asc

You should see some output like this:

Certificate Request:
    Data:
        Version: 1 (0x0)
        Subject: CN = my-website.com
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:af:5d:d1:75:f3:3b:02:ce:13:1e:2f:7a:bc:88:
                    f9:21:b6:cb:4e:e8:56:c4:23:68:37:b8:0a:64:f5:
                    11:dd:0d:db:4e:2d:0a:89:65:bf:23:90:f7:b2:7d:
                    bc:b7:9d:9e:cd:14:92:64:43:52:b9:3d:32:29:5f:
                    92:cb:96:b5:e9:29:d8:c7:fa:e1:c0:c0:af:06:c6:
                    89:a6:c3:c9:d2:99:a0:f3:ec:6b:cc:ec:62:ab:9d:
                    d1:2c:15:7d:db:1d:3f:cb:44:bd:34:52:87:15:ac:
                    3d:7a:e8:b5:f8:cf:1a:45:e6:ec:d8:e7:9b:f0:9e:
                    27:7d:04:1e:8b:c7:b3:71:2a:88:0e:3d:1c:65:2a:
                    fa:36:c1:af:55:1c:02:bf:07:5a:3d:92:2e:e4:fd:
                    d2:88:82:e9:08:1f:d9:dd:63:12:5d:e7:47:69:2e:
                    8e:62:56:71:02:fa:90:b6:b3:2a:45:fd:21:97:8a:
                    3d:f3:b5:7e:9d:1a:df:bc:58:98:7c:93:2d:2c:b9:
                    08:45:09:b0:47:90:77:dc:33:cc:d3:62:d7:e6:c7:
                    c0:65:2a:af:a1:1e:b9:66:78:e5:ef:da:9b:12:24:
                    e9:38:0e:4d:30:57:a5:41:52:81:ac:5f:b8:6a:09:
                    ed:c5:ca:10:3c:7b:3b:63:0e:c2:de:52:35:92:6b:
                    db:21
                Exponent: 65537 (0x10001)
        Attributes:
        Requested Extensions:
            X509v3 Subject Alternative Name: 
                DNS:my-website.com
            X509v3 Basic Constraints: critical
                CA:TRUE
    Signature Algorithm: sha256WithRSAEncryption
         76:43:f5:e8:c8:8b:6a:63:0d:51:d9:4b:5d:0c:b6:07:cf:b1:
         4e:a6:be:87:25:92:fe:75:99:57:63:7e:04:2f:f7:c3:75:a6:
         a2:98:d3:03:ed:c2:3d:12:4f:b3:80:d1:10:75:e5:dc:95:1c:
         a2:7a:a8:73:4e:71:5b:e8:9c:a5:b0:ed:aa:8b:67:8b:5d:5f:
         cb:7f:f5:43:c7:17:47:1e:7a:e0:6b:9b:94:cf:f6:34:00:e7:
         89:c6:95:0d:85:ed:81:8d:1f:33:12:11:1e:cb:2c:a0:0d:f4:
         83:c8:7e:9f:95:f9:10:b5:a7:80:1f:a7:98:e6:ec:0a:8a:f1:
         48:e0:92:2d:fe:44:75:96:55:9c:67:2e:13:fc:b8:3c:6b:cb:
         d8:38:6a:66:da:74:d7:2d:4e:68:02:13:9e:04:a3:da:28:0e:
         a1:da:13:d4:7b:a5:0e:2a:fc:0b:46:28:35:8a:a1:a3:b0:2e:
         4b:78:0c:29:38:dc:fa:11:42:24:0d:1c:17:75:3e:c4:a8:5e:
         39:ae:22:57:e4:29:c3:d5:72:48:18:4d:5d:7a:d2:62:46:cf:
         2a:4a:14:2f:40:05:bf:91:05:23:00:8a:c6:b4:9e:e8:e9:c0:
         af:f6:cb:74:12:e7:58:da:1c:e7:c1:2d:f8:aa:ae:04:5a:2d:
         ae:57:dc:99

Make sure the CA: True is in there, because Micro$oft.

Now you can get this signed by any other internal CA you may have, even a Micro$oft one.

Once it's signed, you can submit it using the /pki/intermediate/set-signed endpoint.

I don't have my own CA at this point, so I'll skip that step. But let me know in the comments how that works out for you.

Now let's create a role and issue a certificate against the Intermediate CA whose private key we've just used Vault to generate and store.

Create a PKI Secrets Engine Role:

vault write pki/roles/eff-dot-wtf allowed_domains=eff.wtf allowed_subdomains=true max_ttl=72h

You should see some output like this:

Success! Data written to: pki/roles/eff-dot-wtf

Create a PKI Secrets Engine Role (with Output):

tls$ vault write pki/roles/eff-dot-wtf allowed_domains=eff.wtf allowed_subdomains=true max_ttl=72h
Success! Data written to: pki/roles/eff-dot-wtf

Issue a Certificate against the PKI Secrets Engine Role:

vault write pki/issue/eff-dot-wtf common_name=www.etf.wtf

Now we'll just delete that ol' root certificate.

Because what could go wrong?

tls2$ vault delete pki/root
Success! Data deleted (if it existed) at: pki/root

Further reading:

https://www.vaultproject.io/docs/commands/index.html

https://www.vaultproject.io/docs/secrets/pki/index.html

https://www.vaultproject.io/api/secret/pki/index.html

https://dev.to/fewknow/vault-at-the-center-using-a-python-cert-manager-18a1

Venafi and Vault are best pals:

https://github.com/Venafi/vault-pki-monitor-venafi

https://github.com/Venafi/vault-pki-backend-venafi

Props to Venafi for their prompt attention to industry trends and scaling needs of their enterprise customers.

Top comments (0)