HashiCorp Vault is a secrets manager: applications fetch passwords, API keys and certificates from Vault over an authenticated API instead of reading them from configuration files, and every access is governed by policies and recorded in an audit log. In this tutorial you will install Vault on Ubuntu 24.04 with its integrated Raft storage and TLS, initialize and unseal it, store secrets in the KV version 2 engine, and let an application read only its own secrets through AppRole authentication.

Prerequisites

To follow this tutorial, you will need:

  • A server running Ubuntu 24.04 LTS with at least 2 GB of RAM, for example a CubePath VPS.
  • A non-root user with sudo privileges.
  • UFW enabled with SSH allowed.
  • A hostname for Vault, such as vault.your_domain, resolving to the server. This tutorial uses a self-signed certificate; if you already have a certificate from a public or internal CA for that name, use it instead.

Throughout the tutorial, replace vault.your_domain with your hostname.

Step 1 - Installing Vault from the HashiCorp repository

HashiCorp publishes signed packages in its own APT repository. Download the signing key into /etc/apt/keyrings:

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/hashicorp-archive-keyring.gpg

Add the repository for your Ubuntu release (noble on 24.04):

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(. /etc/os-release && echo "$VERSION_CODENAME") main" | sudo tee /etc/apt/sources.list.d/hashicorp.list

Install Vault:

sudo apt update
sudo apt install vault

Verify the installation:

vault version
Vault v1.20.4 (...), built 2025-09-23T13:22:38Z

The package creates a vault system user, a systemd unit, the configuration directory /etc/vault.d and the data directory /opt/vault/data.

Step 2 - Creating a TLS certificate

Vault sends secrets and tokens over its API, so it must always use TLS. Generate a self-signed certificate valid for your hostname and for 127.0.0.1, so the CLI on the server itself can connect too:

sudo mkdir -p /opt/vault/tls
sudo openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes \
  -keyout /opt/vault/tls/vault.key -out /opt/vault/tls/vault.crt \
  -subj "/CN=vault.your_domain" \
  -addext "subjectAltName=DNS:vault.your_domain,IP:127.0.0.1"

Allow the vault user to read the key, and nobody else:

sudo chown root:vault /opt/vault/tls/vault.key /opt/vault/tls/vault.crt
sudo chmod 640 /opt/vault/tls/vault.key
sudo chmod 644 /opt/vault/tls/vault.crt

Check the names in the certificate:

openssl x509 -in /opt/vault/tls/vault.crt -noout -ext subjectAltName
X509v3 Subject Alternative Name:
    DNS:vault.your_domain, IP Address:127.0.0.1

Step 3 - Configuring Vault

Replace the example configuration shipped with the package:

sudo nano /etc/vault.d/vault.hcl
ui            = true
disable_mlock = true

api_addr     = "https://vault.your_domain:8200"
cluster_addr = "https://vault.your_domain:8201"

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

listener "tcp" {
  address       = "0.0.0.0:8200"
  tls_cert_file = "/opt/vault/tls/vault.crt"
  tls_key_file  = "/opt/vault/tls/vault.key"
}

What the settings mean:

  • storage "raft" stores Vault's encrypted data on local disk and replicates it when you add more nodes later. No external database is required.
  • disable_mlock = true is HashiCorp's recommendation when using Raft storage, because memory locking interferes with the memory-mapped database files. Make sure swap is disabled or encrypted on the server.
  • api_addr is the address clients use; cluster_addr is used for traffic between Vault nodes.
  • ui = true enables the web interface on the same port as the API.

Restrict the file to root and the vault group, since it defines where the data lives:

sudo chown root:vault /etc/vault.d/vault.hcl
sudo chmod 640 /etc/vault.d/vault.hcl

Start the service:

sudo systemctl enable --now vault
systemctl status vault --no-pager

The status shows active (running). The journal will note that Vault is sealed, which is expected until the next step.

Step 4 - Initializing and unsealing Vault

Tell the CLI where Vault is and which certificate to trust. Add both lines to ~/.bashrc so they persist across sessions:

export VAULT_ADDR=https://127.0.0.1:8200
export VAULT_CACERT=/opt/vault/tls/vault.crt

Initialization creates the master encryption key and splits it into unseal key shares using Shamir's secret sharing. With five shares and a threshold of three, any three key holders together can unseal Vault, but no single person can:

vault operator init -key-shares=5 -key-threshold=3
Unseal Key 1: 4jYbl2CBIv6SpkKj6Hos9iD32k5RfGkLzlosrrq/JgOm
Unseal Key 2: B05G1DRtfYckFV5BbdBvXq0wkK5HFqB9g2jcDmNfTQiS
Unseal Key 3: Arig0N9rN9ezkTRo7qTB7gsIZDaonOcc53EHo83F5chA
Unseal Key 4: 0cZE0C/gEk3YHaKjIWxhyyfs8REhqkRW/CSXTnmTilv+
Unseal Key 5: fYhZOseRgzxmJCmIqUdxEm9C3jB5Q27AowER9w4FC2Ck

Initial Root Token: hvs.KkSiTZF1qQ6j9Wm4g2PZy1Yh
...

Vault starts sealed: it holds the encrypted data but cannot decrypt it. Unseal it by entering three different keys. Run the command without an argument so the key is typed at a hidden prompt instead of being saved in your shell history:

vault operator unseal
Unseal Key (will be hidden):
Key                Value
---                -----
Seal Type          shamir
Initialized        true
Sealed             true
Total Shares       5
Threshold          3
Unseal Progress    1/3
...

Repeat the command twice more with two other keys. After the third key, check the status:

vault status
Key                     Value
---                     -----
Seal Type               shamir
Initialized             true
Sealed                  false
Total Shares            5
Threshold               3
Version                 1.20.4
Storage Type            raft
HA Enabled              true
HA Mode                 active
...

Sealed false means Vault is serving requests. Every time the Vault process restarts it seals itself again, and three key holders must repeat this step.

Log in with the root token, again at a hidden prompt:

vault login
Token (will be hidden):
Success! You are now authenticated.
...

Step 5 - Enabling the audit log

Enable an audit device before storing secrets, so every request from now on is recorded. Vault hashes secret values in the log, so it can be kept and shipped safely:

sudo mkdir -p /var/log/vault
sudo chown vault:vault /var/log/vault
vault audit enable file file_path=/var/log/vault/audit.log
Success! Enabled the file audit device at: file/

Each request now appends a JSON line to /var/log/vault/audit.log. Add the file to your log rotation, and keep an eye on disk space: if Vault cannot write to its only audit device, it refuses requests.

Step 6 - Storing secrets in the KV engine

Secrets engines are mounted at paths. Enable the key/value engine, version 2, at secret/. Version 2 keeps previous versions of every secret:

vault secrets enable -path=secret kv-v2

Store the database credentials for an application called myapp:

vault kv put secret/myapp/database username=myapp password=your_strong_password
====== Secret Path ======
secret/data/myapp/database

======= Metadata =======
Key                Value
---                -----
created_time       2026-09-25T10:31:07.112233Z
deletion_time      n/a
destroyed          false
version            1

Read it back, or read a single field as a script would:

vault kv get secret/myapp/database
vault kv get -field=password secret/myapp/database

Note the Secret Path in the output: with KV version 2, the API path includes data/, which is the path you use in policies.

Step 7 - Writing a policy for the application

Policies grant capabilities on paths, and everything not granted is denied. Create a policy that lets myapp read its own secrets and nothing else:

nano ~/myapp-policy.hcl
path "secret/data/myapp/*" {
  capabilities = ["read"]
}

path "secret/metadata/myapp/*" {
  capabilities = ["list"]
}

Upload it to Vault:

vault policy write myapp ~/myapp-policy.hcl
Success! Uploaded policy: myapp

Step 8 - Authenticating the application with AppRole

AppRole is the authentication method designed for machines. The application logs in with a role_id, which works like a username, and a secret_id, which works like a password, and receives a short-lived token carrying the myapp policy. Enable it and create the role:

vault auth enable approle
vault write auth/approle/role/myapp \
  token_policies=myapp \
  token_ttl=1h \
  token_max_ttl=4h \
  secret_id_ttl=24h

Tokens issued to the app expire after one hour (renewable up to four), and each secret_id expires after 24 hours. Fetch the two credentials:

vault read -field=role_id auth/approle/role/myapp/role-id
vault write -f -field=secret_id auth/approle/role/myapp/secret-id

In production, the role_id is baked into the application's configuration, while the secret_id is delivered at deploy time by a trusted system such as your CI pipeline, so no single place holds both.

Log in as the application:

vault write auth/approle/login role_id=your_role_id secret_id=your_secret_id
Key                     Value
---                     -----
token                   hvs.CAESIJ3t...
token_accessor          V9oF1dcQpBSG1f5vXkYfG2Kq
token_duration          1h
token_renewable         true
token_policies          ["default" "myapp"]
...

Use that token to confirm what the application can and cannot do. Reading its own secret works:

VAULT_TOKEN=your_app_token vault kv get -field=username secret/myapp/database
myapp

Writing fails, because the policy only grants read:

VAULT_TOKEN=your_app_token vault kv put secret/myapp/database password=changed
Error writing data to secret/data/myapp/database: Error making API request.
...
Code: 403. Errors:

* 1 error occurred:
	* permission denied

Applications can use the same flow through the HTTP API or an official client library, or run Vault Agent next to the application to handle login and token renewal automatically.

Step 9 - Opening the firewall

Allow the API port only from the networks that need Vault, such as your application servers' private range:

sudo ufw allow from 10.0.0.0/24 to any port 8200 proto tcp

Port 8201 is only needed between Vault nodes when you build a cluster. The web UI is available at https://vault.your_domain:8200/ui from any allowed network; your browser will warn about the self-signed certificate unless you import it.

Once you have created a proper administrative login, such as a userpass or OIDC method with an admin policy, revoke the root token so it cannot be misused:

vault token revoke your_root_token

You can generate a new root token later with vault operator generate-root and a quorum of unseal keys.

Troubleshooting

x509: certificate signed by unknown authority: VAULT_CACERT is not set in the current shell, or the certificate does not include the name or IP in VAULT_ADDR. Use https://127.0.0.1:8200 on the server, as the certificate includes that IP.

Error checking seal status ... connection refused: Vault is not running. Read sudo journalctl -u vault -n 50; a common cause is the vault user being unable to read the TLS key.

Vault is sealed errors after a reboot: this is expected. Unseal it again with three keys. To avoid manual unsealing, configure auto-unseal with a cloud KMS or another Vault cluster's Transit engine.

permission denied for an operation that should be allowed: check which policies the token has with vault token lookup, and what it can do on a path with vault token capabilities secret/data/myapp/database. With KV version 2, policies must use the secret/data/ prefix, not secret/.

Conclusion

You installed Vault with Raft storage and TLS, initialized it with split unseal keys, turned on auditing, stored a secret, and gave an application least-privilege access through AppRole. Good next steps are adding two more nodes with retry_join blocks in their storage "raft" stanza for high availability, enabling the database secrets engine so applications receive short-lived database users instead of a static password, and taking regular backups with vault operator raft snapshot save.