Getting Started with HashiCorp Vault on Ubuntu

Written by: Bagus Facsi Aginsa
Published at: 28 Jul 2026


Every project ends up with the same pile of sensitive values: database passwords, API keys, TLS private keys, cloud credentials. Most teams start by dropping them into a .env file, then into a CI variable, then into three more places nobody remembers, until nobody can say with confidence where a given secret actually lives or who can read it. That is the problem HashiCorp Vault was built to solve.

Vault is a centralized secrets manager. Instead of scattering credentials across config files and environment variables, you store them in one place, behind authentication, with an audit trail of who read what and when. Applications and humans both authenticate to Vault and pull secrets at runtime, rather than having them baked into a repository or a deployment script.

This tutorial is for developers, sysadmins, and DevOps engineers who are comfortable with the Linux command line but have never run Vault before. By the end, you will have Vault installed on an Ubuntu server, initialized, unsealed, running as a proper background service, and holding a real secret that you can only read through a scoped-down access token instead of the root credentials. This guide assumes basic familiarity with the command line and sudo privileges on the server; no prior Vault experience is needed.


Conceptual Overview

Before running any commands, it helps to understand a handful of Vault-specific terms, because they show up constantly.

Storage backend. Vault needs somewhere to persist its encrypted data. In this guide we use Vault’s integrated storage, based on the Raft consensus protocol. It stores everything on local disk and needs no external database, which makes it the simplest way to run a single Vault node while still following the officially recommended storage method.

Sealed and unsealed. When Vault starts, all of its data is encrypted at rest with a root encryption key, and Vault refers to itself as sealed, meaning it cannot read or serve any of that data yet. To become unsealed, Vault needs a quorum of unseal keys, which are fragments produced when the encryption key was originally split using Shamir’s Secret Sharing algorithm. This split-key design means no single person or file holds the entire key; you need several key holders to agree before Vault will unlock its storage. Every time the Vault process restarts, it goes back to sealed and needs to be unsealed again.

Secrets engines. A secrets engine is a plugin that stores, generates, or manages secrets. The KV (key/value) engine used in this tutorial is the simplest one: you write arbitrary key/value pairs and read them back later, similar to a very secure /etc file. Vault also ships engines that generate short-lived database credentials, AWS IAM credentials, and TLS certificates on demand, but KV is the right place to start.

Policies and tokens. A policy is a set of rules written in HCL that says which paths inside Vault a caller may read, write, or list. A token is a credential that gets attached to one or more policies. The very first token Vault gives you, the root token, can do anything and should be treated like the master key it is: use it only to set things up, then switch to scoped tokens for everyday work.


Prerequisites

  • An Ubuntu server (this guide was tested on Ubuntu 22.04 and 24.04), with at least 1 CPU core, 1 GB of RAM, and a few GB of free disk.
  • sudo privileges and basic comfort with the command line.
  • curl and gpg available (both are preinstalled on most Ubuntu images).
  • No prior Vault experience required.

In this tutorial the server has the private IP 10.20.0.15 and we will run Vault on its standard port, 8200. Replace it with your own server’s address throughout.


Step 1: Install Vault on Ubuntu

HashiCorp publishes an official APT repository, which keeps Vault easy to update later. Add the GPG key first:

wget -O- https://apt.releases.hashicorp.com/gpg | \
  sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg

Then add the repository itself, pointed at your Ubuntu release codename:

echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
  https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
  sudo tee /etc/apt/sources.list.d/hashicorp.list

Update your package index and install Vault:

sudo apt update
sudo apt install vault -y

Confirm the install:

vault version
Vault v1.17.6, built 2024-10-02T18:11:37Z

Step 2: Create a Configuration File

Vault needs a config file that tells it where to store data and which network interface to listen on. Create a dedicated directory for its data first:

sudo mkdir -p /opt/vault/data
sudo chown -R vault:vault /opt/vault/data

The apt package already created a vault system user, which is why chown works here. Now create the config file:

sudo nano /etc/vault.d/vault.hcl

Replace its contents with this:

storage "raft" {
  path    = "/opt/vault/data"
  node_id = "vault-node-1"
}

listener "tcp" {
  address     = "0.0.0.0:8200"
  tls_disable = "true"
}

api_addr     = "http://10.20.0.15:8200"
cluster_addr = "http://10.20.0.15:8201"
ui           = true

A quick word on tls_disable: leaving TLS off is fine for this tutorial on a private network you control, but it is not something you want in production. We come back to this in Best Practices, where the fix is to put Vault behind a reverse proxy that terminates TLS, the same way you would with the Nginx and Let’s Encrypt setup covered in an earlier guide, or with Caddy’s automatic HTTPS.


Step 3: Start Vault as a Service

The APT package already ships a systemd unit, so you do not need to write one yourself. Reload systemd to pick up the packaged unit and start Vault:

sudo systemctl enable --now vault

Check that it is running:

sudo systemctl status vault

You should see active (running). Point the CLI at your local Vault instance so every subsequent command knows where to send requests:

export VAULT_ADDR="http://127.0.0.1:8200"

Add that line to your ~/.bashrc too, so it survives new shells.


Step 4: Initialize and Unseal Vault

A brand-new Vault server holds no encryption keys yet. Initialize it:

vault operator init

Vault prints five Unseal Key shares and one Initial Root Token. This is the only time these values are shown in plaintext, so capture them somewhere safe right now, such as a password manager. The output looks like this:

Unseal Key 1: 8vY3f1L9...
Unseal Key 2: k2Rj0mZq...
Unseal Key 3: pQ7wXe4T...
Unseal Key 4: nD5uHc1V...
Unseal Key 5: zA9tRl2K...

Initial Root Token: hvs.CAESIJ...

By default Vault needs any 3 of these 5 keys to unseal. Provide three of them, one per command:

vault operator unseal 8vY3f1L9...
vault operator unseal k2Rj0mZq...
vault operator unseal pQ7wXe4T...

After the third key, vault status should report Sealed: false:

vault status
Seal Type       shamir
Initialized     true
Sealed          false
Total Shares    5
Threshold       3

Log in with the root token so the CLI is authenticated for the next steps:

vault login hvs.CAESIJ...

Step 5: Enable the KV Secrets Engine and Store a Secret

Vault mounts secrets engines at a path you choose. Enable the version 2 KV engine at secret/:

vault secrets enable -path=secret -version=2 kv

Write your first secret, for example a database password for an application:

vault kv put secret/myapp/db username="app_user" password="Str0ngP@ss!"

Read it back:

vault kv get secret/myapp/db
====== Data ======
Key         Value
---         -----
password    Str0ngP@ss!
username    app_user

Every write to a KV v2 path is versioned, so if someone overwrites this secret later, you can inspect or roll back to an earlier version with vault kv get -version=1 secret/myapp/db.


Step 6: Create a Policy and a Scoped Token

Using the root token for daily application access defeats the point of Vault. Create a policy file that only allows reading the one secret path an application actually needs:

sudo nano /etc/vault.d/myapp-policy.hcl
path "secret/data/myapp/*" {
  capabilities = ["read", "list"]
}

Note the data/ segment: KV v2 stores actual secret data under secret/data/<path>, while metadata like version history lives under secret/metadata/<path>. This is a common first surprise, so keep it in mind.

Load the policy into Vault:

vault policy write myapp-read /etc/vault.d/myapp-policy.hcl

Now create a token restricted to that policy:

vault token create -policy="myapp-read" -ttl="24h"
token             hvs.s3cR3tTok3n...
token_ttl         24h
token_policies    ["default" "myapp-read"]

Hand this token to your application (through an environment variable injected at deploy time, not committed to a repository) instead of the root token. It expires in 24 hours and cannot do anything outside the one path you scoped it to.


Common Mistakes and Troubleshooting

“Vault is sealed” errors after a reboot or restart. This is expected, not a bug. Sealing on every restart is the whole point of the design. Run vault operator unseal three times with your saved keys to bring it back online. If you find this tedious for a production cluster, look into auto-unseal using a cloud KMS, which removes the manual step entirely.

Losing the unseal keys or root token. There is no recovery path for lost unseal keys short of restoring from a backup or re-initializing (which wipes existing data). Store them in a password manager immediately after vault operator init, split among trusted team members if you can, and never keep all five keys in the same place as the root token.

permission denied when reading a secret with a scoped token. Double-check the path in your policy matches the data/ prefix for KV v2, and confirm the token actually has that policy attached with vault token lookup.

Vault refuses to start after editing the config file. Run vault server -config=/etc/vault.d/vault.hcl directly in the foreground to see the exact parse error; HCL syntax mistakes like a missing quote are the usual culprit.

Confusing KV v1 and v2 paths. If secrets you write cannot be found where you expect, confirm which version you enabled with vault secrets list -detailed. The data/ prefix rule above only applies to v2.


Best Practices

Never disable TLS in production. The config in this tutorial uses tls_disable = true for simplicity on a private lab network. In production, terminate TLS with a certificate from Let’s Encrypt using Nginx or Caddy in front of Vault, or configure Vault’s own listener with tls_cert_file and tls_key_file directly.

Rotate the root token, then stop using it. After initial setup, revoke the default root token and generate a fresh one only when you truly need root-level access, using vault token revoke and vault operator generate-root.

Use short-lived tokens wherever possible. Long-lived static tokens defeat much of the value Vault provides. Combine short TTLs with Vault’s authentication methods (AppRole for services, LDAP or OIDC for humans) so tokens are minted on demand rather than handed out once and forgotten.

Automate unsealing carefully. Manual unsealing is fine for a lab, but a production cluster that restarts at 3 AM needs someone awake with unseal keys, unless you configure auto-unseal via a cloud KMS. Treat this as a day-one decision, not an afterthought.

Back up the storage directory. Because Raft storage lives on local disk, back up /opt/vault/data regularly, the same way you would any critical database. If you already use restic for encrypted backups, it is a natural fit here too.

Manage Vault configuration as code. If you run more than one Vault node or plan to reproduce this setup elsewhere, provisioning it through Ansible keeps the install and config file consistent across machines instead of drifting over time.


Conclusion

You now have a working Vault server on Ubuntu: installed from HashiCorp’s official repository, running as a systemd service, initialized with unseal keys you control, and holding a real secret behind a policy-scoped token instead of a root credential. You have also seen why sealing exists, how KV v2 versions your secrets, and where the sharp edges are, from the data/ path prefix to the very real risk of losing unseal keys.

From here, the natural next step is wiring an application to fetch its own secrets from Vault at startup instead of reading a .env file, and looking into an authentication method like AppRole so services never need a human-issued token at all. If you are running Vault behind a reverse proxy, revisit the Let’s Encrypt and Nginx guide to add real TLS in front of it. Centralizing secrets in one audited place is a small amount of upfront setup for a problem, scattered credentials, that only gets more painful the longer you put it off. </content>