HashiCorp Vault and PKI
I started playing with HashiCorp Vault about 2 years ago and I really struggled to start with. I didn't expect the simplicity. Here are some of my notes that may help you touch the ground running.
<p>I started playing with HashiCorp Vault about 2 years ago and I really struggled to start with. I didn't expect the simplicity. Here are some of my notes that may help you touch the ground running.</p><hr><p><em>Note 1: I have updated the steps for Vault version 1.3+ (tested with v1.3.2) as the syntax has changed since my first deployments.</em></p><p><em>Note 2: There is an official how-to page </em><a href="https://www.vaultproject.io/docs/secrets/pki/index.html" target="_blank"><em>https://www.vaultproject.io/docs/secrets/pki/index.html</em></a><em> which I used for this description and it's worth having a look at. I have added a description of basic concepts to help you understand the Vault more quickly and also added some details that took me a little to figure out.</em></p><p><a href="https://github.com/hashicorp/vault" target="_blank">Vault</a> is an open source key management system by <a href="https://www.hashicorp.com/" target="_blank">HashiCorp</a>. You can use it with <a href="https://www.hashicorp.com/products/consul" target="_blank">Consul</a> and other CI/CD tools to securely manage passwords, keys, and certificates - basically any sensitive data items for your software configurations. Vault is a universal tool to manage all kinds of secrets - API keys, passwords, symmetric keys, certificates. It is open source and free if you can learn and use its CLI or RESTful API, if you want a graphic interface, you can go for a paid license.</p><p>In a sense, the overall concept of the Vault is simple. There is a data storage - called Backends. On top of that are Secrets Engines. Each provides access and a logic for a particular type of secrets. On top of that are Auth Methods, which provide access control according to your access control policy. The use of the Vault is logged in JSON files and there's an "audit" command to inspect those.</p><p>I didn't mention the "Seal" concept yet. When you start a Vault server, you need to "unseal" the backend storage - it basically gives the server an encryption key to access the backend storage. The Vault will keep the secret in memory so long as it's running. It is not unusual to require 2 or more people (so called dual control) to provide their seal secret to start a new instance of the Vault server. If you don't do it, you expose yourself to an internal threat of a rogue employee. As you can run many instance of the server against any backend, it's easy to launch a new server and pull secrets from it.</p><p>If I sum up the "sealing/unsealing" - the backend is encrypted with a key that has to be reconstructed from "key shares". The server will keep this key in memory while it runs. If it fails, you need to unseal again. This is one of the main reason why you need several servers in the production environment to prevent denial of service.</p><p>Let's get started with the PKI Secret Engine using the Vault's CLI. All commands can be replicated with a RESTful API calls.</p><h2>Download & Installation</h2><p><a href="https://www.vaultproject.io/downloads.html" target="_blank" style="color: rgb(0, 82, 204);">https://www.vaultproject.io/downloads.html</a> - here you can find binaries for quick get-started, no installation needed, just one file of about 50MB that will unzip to 140MB or so. The supported platforms are OSX, Windows, Linux, FreeBSD, NetBSD, OpenBSD, and Solaris.</p><p>As the Vault provides a service, you need to run it as a server, and you need a client to run commands. The same binary file works for both if you use the CLI. <a href="https://www.postman.com/" target="_blank">Postman</a> is my preferred option for testing RESTful APIs.</p><p>Before you start the server, you need an initial configuration file that will define:</p><ul><li>a path to data; and</li><li>the address and port for the server</li></ul><p>You can use '#' to comment out lines you don't need. Here's my simple test.hcl file:</p><p><code class="ql-font-monospace" style="color: rgb(23, 43, 77);">storage "file" {</code></p><p><code style="color: rgb(23, 43, 77);" class="ql-font-monospace"> path = "/Users/dcvrcek/Downloads/vault.cfg/data"</code></p><p><code class="ql-font-monospace" style="color: rgb(23, 43, 77);">}</code></p><p><code class="ql-font-monospace" style="color: rgb(23, 43, 77);">listener "tcp" {</code></p><p><code class="ql-font-monospace" style="color: rgb(23, 43, 77);"> address = "127.0.0.1:8200" tls_disable = 1</code></p><p><code class="ql-font-monospace" style="color: rgb(23, 43, 77);">}</code></p><p><code class="ql-font-monospace" style="color: rgb(101, 84, 192);">#listener "tcp" {</code></p><p><code class="ql-font-monospace" style="color: rgb(101, 84, 192);"># address = "172.16.8.29:8200"</code></p><p><code class="ql-font-monospace" style="color: rgb(101, 84, 192);"># tls_disable = 1</code></p><p><code class="ql-font-monospace" style="color: rgb(101, 84, 192);">#}</code></p><p><em>Note: I have played with a free UI </em><a href="https://github.com/djenriquez/vault-ui" target="_blank" style="color: rgb(0, 82, 204); background-color: rgb(255, 255, 255);"><em>https://github.com/djenriquez/vault-ui</em></a><em>, which I didn't find that useful, really. However, it needs a non-localhost address to work.</em></p><p>OK, now we can start the server:</p><pre class="ql-syntax" spellcheck="false">./vault server <span class="hljs-comment">--config test.hcl</span>
</pre><p>You should see something like this:</p><p> <code> Cgo: disabled</code></p><p><code> Listener 1: tcp (addr: "127.0.0.1:8200", cluster address: "127.0.0.1:8201", max_request_duration: "1m30s", max_request_size: "33554432", tls: "disabled")</code></p><p><code> Log Level: info</code></p><p><code> Mlock: supported: false, enabled: false</code></p><p><code> Recovery Mode: false</code></p><p><code> Storage: file</code></p><p><code> Version: Vault v1.3.2</code></p><p>Besides a few technical messages, you can see that we have successfully set up a basic configuration. The only mandatory items are the listener and the backend/storage, but you can add the following items later on:</p><ul><li>listener - where we can reach the server.</li><li>storage - the selection of a Backend - in our case it's "file", once you stop testing, you can choose from a wide selection from Zookeeper to MySQL, PostgreSQL, S3, to in-memory.</li><li>seal - it's an additional protection of the Vault - you can use an HSM, one of the cloud key vaults (AliCloud, Google, AWS, Azure), and a couple more.</li><li>telemetry - i.e., performance data - here you can specify an upstream server to collect the Vault's performance data.</li><li>entropy - an additional source of entropy for secrets generation.</li></ul><h2>Vault Client</h2><p>Now we have a server running in one terminal window, we move to another terminal window and set an env. variable VAULT_ADDR that should reflect the listener configuration.</p><pre class="ql-syntax" spellcheck="false"><span class="hljs-keyword">export</span> VAULT_ADDR=http:<span class="hljs-comment">//127.0.0.1:8200</span>
</pre><p>The important bit is to define the use of "http". The client would otherwise complain about an HTTPS/HTTP mismatch. We have a server running and we have an environment set up for the client. What we need to do first is to initialize the storage - create a "seal".</p><h2>Initialize the Vault instance</h2><p>First you need to initialize the "store backend. You can skip the parameters key-shares and key-threshold. The default values are 5 and 3, i.e., you need three to enter 3 shares out of 5 to unseal the backend storage.</p><pre class="ql-syntax" spellcheck="false">./vault <span class="hljs-keyword">operator</span> init -key-shares=<span class="hljs-number">1</span> -key-threshold=<span class="hljs-number">1</span>
</pre><p>The client will print the share and "root token" into the terminal:</p><p><code>Unseal Key 1: pvoVBIL9i+mcvU3iGXaToEAuocpACuO++IVZ0nGqPqY=</code></p><p><code>Initial Root Token: s.pST0K6ecmyK0eCXbACrtohjZ</code></p><p>Now comes the really hard bit - at least for long-term use of the Vault. What to do with these values. You need to keep them somewhere. The unseal keys should be distributed to different persons AND no person should have access to more than one key (unless it's part of your security policy).</p><p>The root token is needed to load the initial security policy. You can then create tokens to manage particular parts of the policy and access particular Secrets Engines and secrets within. This initial token should be revoked as soon as practically possible.</p><p><em>Note: when you stop the server (don't forget to start it again) you can see that it created a data folder "vault.cfg" - as set in the configuration file, with an initial structure.</em></p><h2>The First Unseal</h2><p>We have created an encrypted backend storage. The next step to do the first unseal - so you need the "threshold" number of keys. It's worth mentioning that you can use different clients to send an unseal key to the server. So if you need your team cooperation, each of them can use a client on their laptop ... so long as they can access the server's API. </p><pre class="ql-syntax" spellcheck="false">./vault <span class="hljs-keyword">operator</span> unseal pvoVBIL9i+mcvU3iGXaToEAuocpACuO++IVZ0nGqPqY=
</pre><p>When enough keys are entered, the server can calculate the Master Key, which will be in memory while the server is running. The client will also print the server's config:</p><ul><li>Seal Type - shamir</li><li>Initialized - true</li><li>Sealed - false</li><li>Total Shares - 1</li><li>Threshold - 1</li><li>Version - 1.3.2</li><li>Cluster Name - vault-cluster-35aa9a47</li><li>Cluster ID - a5e043be-f857-2f8a-bd92-c048acdcdda1</li><li>HA Enabled - false</li></ul><p>The next step is to authenticate using the root token - it's basically an API key of sorts.</p><pre class="ql-syntax" spellcheck="false">./vault login token=s.pST0K6ecmyK0eCXbACrtohjZ
</pre><p>You should see a "Success!" message with some important details:</p><ul><li>token - value of the token you used;</li><li>token_accessor - it's a token handle that you can use to query properties of the token - quite useful for auditing existing tokens;</li><li>token_duration - how long till it expires, the default validity is forever</li><li>token_renewable - some tokens require renewals and they will expire if you don't do that. If you have a long running process, it can renew its token after 5 mins. If it dies the access automatically expires.</li><li>token_policies ["root"] - the scope of the token</li><li>...</li></ul><p>Alright, you should be set up and we can start playing with the PKI Secrets Engine. Just a quick recap. We have:</p><ol><li>started a server with a simple default configuration;</li><li>initialized the data storage;</li><li>unsealed the storage; and</li><li>authenticated ourselves so we now have full access to the Vault server and its backend.</li></ol><h2>PKI Initialization</h2><p>Each Secrets Engine has to be enabled/mounted before its first use. It creates an instance of the selected Secrets Engine. For the PKI engine, you need a new mount for each CA you want to use. If you need a root and 2 issuing CAs, you will need to mount the pki three times. We will start with a root </p><pre class="ql-syntax" spellcheck="false">./vault secrets <span class="hljs-built_in">enable</span> -path=rootca pki
</pre><p>and one issuing CA.</p><pre class="ql-syntax" spellcheck="false">./vault secrets <span class="hljs-built_in">enable</span> -path=ca pki
</pre><p>That's it - we have created two certification authorities! Now comes the tuning. Let's start with the validity of the root CA cert. In the Vault's language it's "max-lease-ttl". Let's try 10 years.</p><pre class="ql-syntax" spellcheck="false">./vault secrets tune -max-lease-ttl=87600h rootca
</pre><p>And the issuing the CA will have a cert valid 1 year.</p><pre class="ql-syntax" spellcheck="false">./vault secrets tune -max-lease-ttl=8760h ca
</pre><p>That's the basic setup. Let's generate the first certificate for the root CA. The default name would be something like "myvault.com" but we can change it with a "common_name" parameter, and we can, e.g., specify the validity of certs a CA will be creating.</p><pre class="ql-syntax" spellcheck="false">./vault write rootca/root/generate/internal \
common_name=myrootca.com \
ttl=87600h
</pre><p>You will get a new certificate printed on the screen. The private key is stored in the backend. </p><p>Note: notice that the path in the command contains a keyword "root". This tells the Vault that it is a root CA and it will automatically create a self signed certificate. The next command is for "intermediate" CA. As a result, you will only get a CSR that will have to be signed by another CA.</p><pre class="ql-syntax" spellcheck="false">./vault write ca/intermediate/generate/internal \
common_name=<span class="hljs-string">"Issuing CA"</span> \
ttl=8760h
</pre><p><em>Note: mind the space before "\".</em></p><p>We can now store the CSR in a file and get it signed. Let's create, <strong><em>pki_int.csr</em></strong> , e.g. with a vim or any other editor - copy&paste the PEM string of the CSR into this new file.</p><p>We can now call the rootca CA to sign the certificate request. We want the result as a PEM data with the whole chain = "pem_bundle".</p><pre class="ql-syntax" spellcheck="false">./vault <span class="hljs-keyword">write</span> rootca/root/sign-intermediate csr=@pki_int.csr <span class="hljs-keyword">format</span>=pem_bundle
</pre><p>You need to copy&paste the result into a file or redirect stdout when you call this "write" command.</p><p>Hooray - we have a certificate for the issuing CA. Let's import it. </p><p>now you can import the signed certificate to the issuing CA</p><pre class="ql-syntax" spellcheck="false">vault write ca/intermediate/<span class="hljs-built_in">set</span>-<span class="hljs-keyword">signed</span> certificate=@signed_certificate.pem
</pre><p>You should get back a "Data written" message.</p><h2>Policy/user and first certificate</h2><p>The next step is to create a role for clients to request certificates - lets' say we have one agent requesting certificates for many clients in the <a href="http://example.com/" target="_blank" style="color: rgb(0, 82, 204);">example.com</a> domain. Let's restrict the issuing CA to this domain. We can also set the certificate validity.</p><pre class="ql-syntax" spellcheck="false">./vault write ca/roles/example-dot-com \
allowed_domains=example.com \
allow_subdomains=true \
max_ttl=72h
</pre><p>This command created a policy "example-dot-com". It has also created an entry in the policy file that you can assign a new token to. Whoever shows this new token will only be able to issue certificates under this "example-dot-com" policy.</p><p>Let's create a first end-user certificate.</p><pre class="ql-syntax" spellcheck="false">./vault <span class="hljs-keyword">write</span> ca/issue/example-dot-com \ common_name=blah.example.com
</pre><p>The command will generate a private key and create a certificate for it. You get back:</p><ul><li>ca_chain - [ root CA in PEM, CA in PEM ]</li><li>certificate - PEM data</li><li>expiration - timestamp</li><li>issuing_ca - PEM data</li><li>private_key - PEM / PKCS1</li><li>private_key_type - rsa</li><li>serial_number = string</li></ul><p>You may need a bit more structured output for automation. I like JSON so I would just add "-format=json" to the command. </p><p><em>Note: the private key is printed out only once so you need to carefully capture the output so you can install it. </em></p><p><em>This tutorial loosely follows instructions from here - </em><a href="https://www.vaultproject.io/docs/secrets/pki/index.html" target="_blank">https://www.vaultproject.io/docs/secrets/pki/index.html</a> . Hopefully the text above should help you avoid some difficult bits that took me a little to get right. </p><p><br></p><blockquote>We really like vault and we work on integrating Vault with <a href="https://keychest.net" target="_blank">KeyChest</a> and with our hardware root CA service. The goal is to offer a very, very secure storage of root keys with a convenient remote access to generate keys for intermediate CAs or even end-points. If you'd be interested in this integration, drop us a line at <a href="support@keychest.net" target="_blank">support@keychest.net</a> .</blockquote><p>While the CLI is convenient for first testing, I would certainly recommend switching to the RESTful API as soon as you get to grips with the PKI secrets engine. </p><p>Here's a query to get the configuration of our Issuing CA so you can see the flexibility you have:</p><p><code>curl --header "X-Vault-Token: s.pST0K6ecmyK0eCXbACrtohjZ" http://127.0.0.1:8200/v1/ca/roles/example-dot-com</code></p><p><span style="color: rgb(114, 159, 207);">{</span></p><ul><li><span style="color: rgb(32, 74, 135);">"request_id"</span>:<span style="color: rgb(78, 154, 6);">"97dfbae4-0a68-5e88-df0c-3c6db990d70d"</span>,</li><li><span style="color: rgb(32, 74, 135);">"lease_id"</span>:<span style="color: rgb(78, 154, 6);">""</span>,</li><li><span style="color: rgb(32, 74, 135);">"renewable"</span>:<span style="color: rgb(196, 160, 0);">false</span>,</li><li><span style="color: rgb(32, 74, 135);">"lease_duration"</span>:<span style="color: rgb(173, 127, 168);">0</span>,</li><li><span style="color: rgb(32, 74, 135);">"data"</span>:<span style="color: rgb(114, 159, 207);">{</span></li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"allow_any_name"</span>:<span style="color: rgb(196, 160, 0);">false</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"allow_bare_domains"</span>:<span style="color: rgb(196, 160, 0);">false</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"allow_glob_domains"</span>:<span style="color: rgb(196, 160, 0);">false</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"allow_ip_sans"</span>:<span style="color: rgb(196, 160, 0);">true</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"allow_localhost"</span>:<span style="color: rgb(196, 160, 0);">true</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"allow_subdomains"</span>:<span style="color: rgb(196, 160, 0);">true</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"allow_token_displayname"</span>:<span style="color: rgb(196, 160, 0);">false</span>,</li><li><span style="color: rgb(32, 74, 135);">"allowed_domains"</span>:<span style="color: rgb(164, 0, 0);">[</span></li></ul><ol><li class="ql-indent-2"><span style="color: rgb(78, 154, 6);">"example.com"</span></li></ol><ul><li class="ql-indent-1"><span style="color: rgb(164, 0, 0);">]</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"allowed_other_sans"</span>:<em style="color: rgb(186, 189, 182);">null</em>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"allowed_serial_numbers"</span>:<span style="color: rgb(164, 0, 0);">[]</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"allowed_uri_sans"</span>:<span style="color: rgb(164, 0, 0);">[]</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"basic_constraints_valid_for_non_ca"</span>:<span style="color: rgb(196, 160, 0);">false</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"client_flag"</span>:<span style="color: rgb(196, 160, 0);">true</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"code_signing_flag"</span>:<span style="color: rgb(196, 160, 0);">false</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"country"</span>:<span style="color: rgb(164, 0, 0);">[]</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"email_protection_flag"</span>:<span style="color: rgb(196, 160, 0);">false</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"enforce_hostnames"</span>:<span style="color: rgb(196, 160, 0);">true</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"ext_key_usage"</span>:<span style="color: rgb(164, 0, 0);">[]</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"ext_key_usage_oids"</span>:<span style="color: rgb(164, 0, 0);">[]</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"generate_lease"</span>:<span style="color: rgb(196, 160, 0);">false</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"key_bits"</span>:<span style="color: rgb(173, 127, 168);">2048</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"key_type"</span>:<span style="color: rgb(78, 154, 6);">"rsa"</span>,</li><li><span style="color: rgb(32, 74, 135);">"key_usage"</span>:<span style="color: rgb(164, 0, 0);">[</span></li></ul><ol><li class="ql-indent-2"><span style="color: rgb(78, 154, 6);">"DigitalSignature"</span>,</li><li class="ql-indent-2"><span style="color: rgb(78, 154, 6);">"KeyAgreement"</span>,</li><li class="ql-indent-2"><span style="color: rgb(78, 154, 6);">"KeyEncipherment"</span></li></ol><ul><li class="ql-indent-1"><span style="color: rgb(164, 0, 0);">]</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"locality"</span>:<span style="color: rgb(164, 0, 0);">[]</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"max_ttl"</span>:<span style="color: rgb(173, 127, 168);">259200</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"no_store"</span>:<span style="color: rgb(196, 160, 0);">false</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"not_before_duration"</span>:<span style="color: rgb(173, 127, 168);">30</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"organization"</span>:<span style="color: rgb(164, 0, 0);">[]</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"ou"</span>:<span style="color: rgb(164, 0, 0);">[]</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"policy_identifiers"</span>:<span style="color: rgb(164, 0, 0);">[]</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"postal_code"</span>:<span style="color: rgb(164, 0, 0);">[]</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"province"</span>:<span style="color: rgb(164, 0, 0);">[]</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"require_cn"</span>:<span style="color: rgb(196, 160, 0);">true</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"server_flag"</span>:<span style="color: rgb(196, 160, 0);">true</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"street_address"</span>:<span style="color: rgb(164, 0, 0);">[]</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"ttl"</span>:<span style="color: rgb(173, 127, 168);">0</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"use_csr_common_name"</span>:<span style="color: rgb(196, 160, 0);">true</span>,</li><li class="ql-indent-1"><span style="color: rgb(32, 74, 135);">"use_csr_sans"</span>:<span style="color: rgb(196, 160, 0);">true</span></li><li><span style="color: rgb(114, 159, 207);">}</span>,</li><li><span style="color: rgb(32, 74, 135);">"wrap_info"</span>:<em style="color: rgb(186, 189, 182);">null</em>,</li><li><span style="color: rgb(32, 74, 135);">"warnings"</span>:<em style="color: rgb(186, 189, 182);">null</em>,</li><li><span style="color: rgb(32, 74, 135);">"auth"</span>:<em style="color: rgb(186, 189, 182);">null</em></li></ul><p><span style="color: rgb(114, 159, 207);">}</span></p>