{"id":5070,"date":"2025-07-10T08:39:47","date_gmt":"2025-07-10T12:39:47","guid":{"rendered":"https:\/\/dft.wiki\/?p=5070"},"modified":"2026-06-08T09:24:14","modified_gmt":"2026-06-08T13:24:14","slug":"deploy-a-key-vault-to-store-secrets","status":"publish","type":"post","link":"https:\/\/dft.wiki\/?p=5070","title":{"rendered":"Deploy a Key Vault to store Secrets"},"content":{"rendered":"<p><strong>Hashicorp Vault<\/strong> is an open-source tool for storing and controlling access to secrets, such as tokens, passwords, certificates, encryption keys, and other sensitive data. It can be accessed manually through a UI, programmatically using the CLI, or via the HTTP API [<a href=\"https:\/\/github.com\/hashicorp\/vault\">Link<\/a>].<\/p>\n<p>With <strong>Vault<\/strong>, specific actions (e.g., read, create, update, delete) can be granted on specific secrets, enabling full separation of duties for a defined duration (auto-expire) or a designated call source.<\/p>\n<hr \/>\n<p><strong>INSTALLING VAULT<\/strong><\/p>\n<p>Using Snap on Ubuntu\/Debian:<\/p>\n<pre>sudo apt install snapd -y\r\nsudo snap install vault<\/pre>\n<p>Alternatively, add the official repository to the system&#8217;s package manager.<\/p>\n<pre>wget -O - https:\/\/apt.releases.hashicorp.com\/gpg | sudo gpg --dearmor -o \/usr\/share\/keyrings\/hashicorp-archive-keyring.gpg\r\necho \"deb [arch=$(dpkg --print-architecture) signed-by=\/usr\/share\/keyrings\/hashicorp-archive-keyring.gpg] https:\/\/apt.releases.hashicorp.com $(grep -oP '(?&lt;=UBUNTU_CODENAME=).*' \/etc\/os-release || lsb_release -cs) main\" | sudo tee \/etc\/apt\/sources.list.d\/hashicorp.list\r\nsudo apt update &amp;&amp; sudo apt install vault -y<\/pre>\n<p>See more installation methods at [<a href=\"https:\/\/developer.hashicorp.com\/vault\/install#linux\">Link<\/a>].<\/p>\n<p>Edit the configuration file:<\/p>\n<pre>sudo nano \/etc\/vault.d\/vault.hcl<\/pre>\n<pre>ui = true\r\ndisable_mlock = true\r\ndefault_lease_ttl =\"87600\"\r\nmax_lease_ttl = \"87600\"\r\nstorage \"file\" {\r\n  path = \"\/opt\/vault\/data\"\r\n}\r\n\r\n# HTTP listener\r\nlistener \"tcp\" {\r\n  address = \"127.0.0.1:8200\"\r\n  tls_disable = 1\r\n}<\/pre>\n<pre>systemctl enable vault --now\r\nexport VAULT_ADDR=http:\/\/127.0.0.1:8200\r\nvault status<\/pre>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-5073\" src=\"https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-09_20-54-55.png\" alt=\"\" width=\"357\" height=\"236\" srcset=\"https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-09_20-54-55.png 357w, https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-09_20-54-55-300x198.png 300w\" sizes=\"auto, (max-width: 357px) 100vw, 357px\" \/><\/p>\n<p><strong>Note:<\/strong> Vault is not yet initialized and is currently sealed.<\/p>\n<p>Navigate to <strong>http:\/\/127.0.0.1:8200<\/strong>, or create an SSH tunnel as follows.<\/p>\n<hr \/>\n<p><strong>INITIALIZE AND UNSEAL THE VAULT<\/strong><\/p>\n<pre>ssh root@<strong>192.168.1.200<\/strong> -L 8200:localhost:8200<\/pre>\n<p>Vault can be initialized via the <strong>Web UI<\/strong>:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-5072\" src=\"https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-09_20-53-07.png\" alt=\"\" width=\"575\" height=\"532\" srcset=\"https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-09_20-53-07.png 575w, https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-09_20-53-07-300x278.png 300w\" sizes=\"auto, (max-width: 575px) 100vw, 575px\" \/><\/p>\n<p>Or via the <strong>CLI<\/strong>:<\/p>\n<pre>export VAULT_ADDR=http:\/\/127.0.0.1:8200\r\nvault operator init<\/pre>\n<p><strong>OR<\/strong><\/p>\n<pre>vault operator init -key-shares=5 -key-threshold=3 &gt; keys.json<\/pre>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-5074\" src=\"https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-09_21-06-47.png\" alt=\"\" width=\"702\" height=\"324\" srcset=\"https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-09_21-06-47.png 702w, https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-09_21-06-47-300x138.png 300w\" sizes=\"auto, (max-width: 702px) 100vw, 702px\" \/><\/p>\n<p>Now unseal the Vault:<\/p>\n<pre>vault operator unseal<\/pre>\n<p>Provide the <strong>Unseal Keys<\/strong> from the previous step.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-5075\" src=\"https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-09_21-10-02.png\" alt=\"\" width=\"478\" height=\"273\" srcset=\"https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-09_21-10-02.png 478w, https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-09_21-10-02-300x171.png 300w\" sizes=\"auto, (max-width: 478px) 100vw, 478px\" \/><\/p>\n<p>Vault is now ready to be accessed with the <strong>Initial Root Token<\/strong>.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-5076\" src=\"https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-09_21-11-25.png\" alt=\"\" width=\"574\" height=\"506\" srcset=\"https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-09_21-11-25.png 574w, https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-09_21-11-25-300x264.png 300w\" sizes=\"auto, (max-width: 574px) 100vw, 574px\" \/><\/p>\n<p>Log in with the <strong>Initial Root Token<\/strong>:<\/p>\n<pre>vault login<\/pre>\n<p><strong>Development (Unsafe) Deployment<\/strong><\/p>\n<p>In a development environment where reduced security is acceptable:<\/p>\n<pre>export VAULT_ADDR=http:\/\/127.0.0.1:8200\r\nvault operator init -key-shares=1 -key-threshold=1 -format=json &gt; init.json\r\nvault operator unseal $(jq -r '.unseal_keys_b64[0]' init.json)\r\nvault login $(jq -r '.root_token' init.json)<\/pre>\n<hr \/>\n<p><strong>MANAGING POLICIES AND TOKENS<\/strong><\/p>\n<p>Using the <strong>Initial Root Token<\/strong>, enable a KV secrets engine:<\/p>\n<pre>export VAULT_ADDR=http:\/\/127.0.0.1:8200\r\nvault login\r\nvault secrets enable -path=kv kv<\/pre>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-5078\" src=\"https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-09_21-48-42.png\" alt=\"\" width=\"681\" height=\"307\" srcset=\"https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-09_21-48-42.png 681w, https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-09_21-48-42-300x135.png 300w\" sizes=\"auto, (max-width: 681px) 100vw, 681px\" \/><\/p>\n<p>Create <strong>Policies<\/strong>:<\/p>\n<pre>cat &lt;&lt;EOF &gt; readonly-policy.hcl\r\npath \"kv\/*\" {\r\ncapabilities = [\"read\", \"list\"]\r\n}\r\nEOF\r\nvault policy write readonly readonly-policy.hcl<\/pre>\n<pre>cat &lt;&lt;EOF &gt; readwrite-policy.hcl\r\npath \"kv\/*\" {\r\ncapabilities = [\"create\", \"read\", \"update\", \"delete\", \"list\"]\r\n}\r\nEOF\r\nvault policy write readwrite readwrite-policy.hcl<\/pre>\n<p><strong>Note:<\/strong> These policies are overly permissive and should be used for reference only.<\/p>\n<pre>vault policy list<\/pre>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-5082\" src=\"https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-10_08-02-13.png\" alt=\"\" width=\"620\" height=\"223\" srcset=\"https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-10_08-02-13.png 620w, https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-10_08-02-13-300x108.png 300w\" sizes=\"auto, (max-width: 620px) 100vw, 620px\" \/><\/p>\n<p>Create <strong>Tokens<\/strong> linked to policies:<\/p>\n<pre>vault token create -policy=<strong>readonly<\/strong> -orphan -no-default-policy -ttl=0\r\nvault token create -policy=<strong>readwrite<\/strong> -orphan -no-default-policy -ttl=0<\/pre>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-5083\" src=\"https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-10_08-26-21.png\" alt=\"\" width=\"1056\" height=\"363\" srcset=\"https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-10_08-26-21.png 1056w, https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-10_08-26-21-300x103.png 300w, https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-10_08-26-21-1024x352.png 1024w, https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-10_08-26-21-768x264.png 768w\" sizes=\"auto, (max-width: 1056px) 100vw, 1056px\" \/><\/p>\n<p><strong>Note:<\/strong> These tokens are insecure and should be used for reference only. If needed, set the TTL to 87600 for a 10-year expiry.<\/p>\n<p>To check a token&#8217;s permissions and status:<\/p>\n<pre>vault token lookup &lt;TOKEN&gt;<\/pre>\n<hr \/>\n<p><strong>PUSHING AND PULLING SECRETS<\/strong><\/p>\n<p>Set the environment variables:<\/p>\n<pre>export VAULT_ADDR='http:\/\/127.0.0.1:8200\/'\r\nexport VAULT_TOKEN=<strong>hvs.CAESIAH6vrn9oT0ITzzaCTSc4m9auVNuOULnpB8NqUsLdmv0Gh4KHGh2cy5wQVFpNzJodm1DcE5pRWNENFlBb21KWk4<\/strong><\/pre>\n<p>Test writing and reading a secret from another host:<\/p>\n<pre>vault kv put kv\/example.com cert=test key=test root_ca=test\r\nvault kv get kv\/example.com<\/pre>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-5084\" src=\"https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-10_08-33-29.png\" alt=\"\" width=\"613\" height=\"160\" srcset=\"https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-10_08-33-29.png 613w, https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-10_08-33-29-300x78.png 300w\" sizes=\"auto, (max-width: 613px) 100vw, 613px\" \/><\/p>\n<p>Write (create or update) a secret using Read+Write access:<\/p>\n<pre>vault kv put kv\/<strong>example.com<\/strong> cert=@\/etc\/letsencrypt\/live\/<strong>example.com<\/strong>\/cert.pem key=@\/etc\/letsencrypt\/live\/<strong>example.com<\/strong>\/cert.pem root_ca=@\/etc\/letsencrypt\/live\/<strong>example.com<\/strong>\/fullchain.pem<\/pre>\n<p><strong>Note:<\/strong> The <code>@<\/code> symbol loads the file&#8217;s contents, not the path string.<\/p>\n<p>From another host, read the secret using Read-Only access:<\/p>\n<pre>export VAULT_ADDR='http:\/\/127.0.0.1:8200\/'\r\nexport VAULT_TOKEN=<strong>hvs.CAESIDRbO6D1-LpgJrZK-bnM-J67C5SrUl7LR7hsmJhCdyNCGh4KHGh2cy50RU1oWmtub3VBNTUxZ3RkSWpOb2RhZWk\r\n<\/strong>vault kv get kv\/example.com\r\n<\/pre>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-5085\" src=\"https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-10_08-39-11.png\" alt=\"\" width=\"1055\" height=\"162\" srcset=\"https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-10_08-39-11.png 1055w, https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-10_08-39-11-300x46.png 300w, https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-10_08-39-11-1024x157.png 1024w, https:\/\/dft.wiki\/wp-content\/uploads\/sites\/15\/2025\/07\/Screenshot_2025-07-10_08-39-11-768x118.png 768w\" sizes=\"auto, (max-width: 1055px) 100vw, 1055px\" \/><\/p>\n<hr \/>\n<p><strong>ISSUING AN SSL\/TLS CERTIFICATE FOR VAULT<\/strong><\/p>\n<p>Before exposing Vault outside of localhost, secure it with a TLS certificate.<\/p>\n<pre>sudo apt update\r\nsudo apt install certbot python3-certbot-nginx -y\r\nsudo nano \/etc\/nginx\/sites-available\/default<\/pre>\n<p>Set the server name to your public domain. It must be reachable on port 80 from the internet with no restrictions, as Let&#8217;s Encrypt will send HTTP requests from multiple random sources for the challenge.<\/p>\n<pre>    server_name <strong>vault.example.com<\/strong>;<\/pre>\n<p>Test and apply the configuration:<\/p>\n<pre>sudo nginx -t &amp;&amp; sudo nginx -s reload<\/pre>\n<p>Run <code>certbot<\/code> and follow the prompts to enter your email, accept the terms, and so on.<\/p>\n<pre>echo 127.0.0.1 <strong>vault.example.com<\/strong> | sudo tee -a \/etc\/hosts\r\nsudo certbot --nginx<\/pre>\n<p>Confirm that automatic renewal is configured in systemd:<\/p>\n<pre>sudo systemctl list-timers | grep certbot<\/pre>\n<p>If needed, place scripts in the following directories to run at specific points during renewal:<\/p>\n<ul>\n<li><strong>\/etc\/letsencrypt\/renewal-hooks\/pre\/<\/strong>\n<ul>\n<li>Run before renewal<\/li>\n<\/ul>\n<\/li>\n<li><strong>\/etc\/letsencrypt\/renewal-hooks\/post\/<\/strong>\n<ul>\n<li>Run after successful renewal<\/li>\n<\/ul>\n<\/li>\n<li><strong>\/etc\/letsencrypt\/renewal-hooks\/deploy\/<\/strong>\n<ul>\n<li>Run after certificates are deployed<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>Update the Vault configuration to use the new certificates:<\/p>\n<pre>sudo nano \/etc\/vault.d\/vault.hcl<\/pre>\n<pre># HTTP listener (disabled)\r\n#listener \"tcp\" {\r\n#  address = \"127.0.0.1:8200\"\r\n#  tls_disable = 1\r\n#}\r\n\r\n# HTTPS listener\r\nlistener \"tcp\" {\r\n  address       = \"0.0.0.0:8200\"\r\n  tls_cert_file = \"\/etc\/letsencrypt\/live\/<strong>example.com<\/strong>\/cert.pem\"\r\n  tls_key_file  = \"\/etc\/letsencrypt\/live\/<strong>example.com<\/strong>\/privatekey.pem\"\r\n}<\/pre>\n<p>Grant Vault permission to read the certificate files:<\/p>\n<pre>sudo groupadd pki\r\nsudo chgrp -R pki \/etc\/letsencrypt\/archive\r\nsudo chgrp -R pki \/etc\/letsencrypt\/live\r\nsudo chmod -R g+rx \/etc\/letsencrypt\/archive\r\nsudo chmod -R g+rx \/etc\/letsencrypt\/live\r\nsudo usermod -a -G pki vault<\/pre>\n<p>Restart Vault:<\/p>\n<pre>sudo systemctl restart vault.service<\/pre>\n<p>API calls can now use the following HTTPS address:<\/p>\n<pre>export VAULT_ADDR=http<span style=\"text-decoration: underline;\"><strong>s<\/strong><\/span>:\/\/<strong>vault.example.com<\/strong>:8200<\/pre>\n<hr \/>\n<p><strong>BONUS<\/strong><\/p>\n<p>To issue SSL\/TLS certificates using a DNS challenge (required for wildcards, and avoids exposing port 80 for the HTTP challenge), then store them in Vault.<\/p>\n<p>Install ACME Shell [<a href=\"https:\/\/github.com\/acmesh-official\/acme.sh\">Link<\/a>]:<\/p>\n<pre>curl https:\/\/get.acme.sh | sh -s email=devops@example.com\r\nacme.sh -h<\/pre>\n<p>Set the cPanel environment variables and issue the certificate:<\/p>\n<pre>export cPanel_Username=\"devops\"\r\nexport cPanel_Apitoken=\"apitoken\"\r\nexport cPanel_Hostname=\"https:\/\/cpanel.example.com:2083\"\r\nacme.sh --insecure --issue --keylength ec-256 --dns <strong>dns_cpanel<\/strong> -d example.com -d '*.example.com' --force --dnssleep <strong>120<\/strong><\/pre>\n<p>For Cloudflare:<\/p>\n<pre>export CF_Token=\"your_cloudflare_api_token\"\r\nexport CF_Account_ID=\"your_cloudflare_account_id\"\r\nacme.sh --issue --keylength ec-256 --dns <strong>dns_cf<\/strong> -d example.com -d '*.example.com'<\/pre>\n<p>Upload the certificate to Vault:<\/p>\n<pre>cd ~\/.acme.sh\/example.com\/\r\nvault kv put kv\/example.com cert=@example.com.cer key=@example.com.key root_ca=@ca.cer full_chain=@fullchain.cer<\/pre>\n<p>A notable alternative to Vault is <strong>Sealed Secrets by Bitnami<\/strong> [<a href=\"https:\/\/github.com\/bitnami-labs\/sealed-secrets\">Link<\/a>], designed for Kubernetes to prevent base64-encoded secrets from being stored in plaintext in manifests.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Hashicorp Vault is an open-source tool for storing and controlling access to secrets, such as [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[],"class_list":["post-5070","post","type-post","status-publish","format-standard","hentry","category-general"],"_links":{"self":[{"href":"https:\/\/dft.wiki\/index.php?rest_route=\/wp\/v2\/posts\/5070","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/dft.wiki\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/dft.wiki\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/dft.wiki\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/dft.wiki\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=5070"}],"version-history":[{"count":11,"href":"https:\/\/dft.wiki\/index.php?rest_route=\/wp\/v2\/posts\/5070\/revisions"}],"predecessor-version":[{"id":5597,"href":"https:\/\/dft.wiki\/index.php?rest_route=\/wp\/v2\/posts\/5070\/revisions\/5597"}],"wp:attachment":[{"href":"https:\/\/dft.wiki\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=5070"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/dft.wiki\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=5070"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/dft.wiki\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=5070"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}