How to Install HashiCorp Vault on Ubuntu 24.04 VPS: Self-Hosted Secrets Management
Secrets sprawl is a silent liability. Database passwords end up in .env files, API keys get pasted into Slack, TLS private keys sit in git repos, and nobody can answer "who accessed this secret last week?". HashiCorp Vault fixes that by giving you a single, audited, policy-controlled source of truth for every secret, certificate, and encryption key in your stack. This guide walks you through installing Vault on an Ubuntu 24.04 VPS from apt install to a production-grade deployment with integrated Raft storage, TLS, KV v2, PKI, Transit, policies, audit logs, and encrypted snapshot backups.
Want a proven VPS for Vault? The CloudCore Professional plan gives you 6 vCPU, 12 GB RAM, and 100 GB NVMe — enough headroom for Vault plus Consul or Nomad on the same server.
Table of Contents
What is HashiCorp Vault?
HashiCorp Vault is a secrets management platform that centralizes how an organization stores, distributes, rotates, and audits sensitive data. At its core, Vault is an encrypted key-value store, but the value comes from the abstractions layered on top: pluggable authentication backends, policy-based access control, dynamic secrets (credentials generated on demand and revoked automatically), an encryption-as-a-service engine, a full certificate authority, tokens with fine-grained TTLs, and tamper-evident audit logs.
Rather than hand-crafting access control for every database and service, you mount a secrets engine in Vault, write an HCL policy, attach it to an identity (a human, a machine, or a CI job), and let Vault broker every request. Applications never see long-lived credentials — they authenticate to Vault using something they already have (a Kubernetes service account, an AWS IAM role, a JWT from your identity provider) and receive a short-lived token scoped to exactly what they need.
Vault is the centerpiece of HashiCorp's production stack. It pairs naturally with Consul for service discovery and Nomad for orchestration, and sits comfortably behind an Nginx reverse proxy when you need extra ingress controls.
Why Self-Host Vault?
Managed secret stores (AWS Secrets Manager, Google Secret Manager, Azure Key Vault, 1Password Secrets Automation) are convenient, but running Vault on your own VPS has concrete benefits:
- Vendor neutrality — A self-hosted Vault works identically whether your workloads run on AWS, GCP, Azure, Hetzner, OVH, Contabo, or on-prem. No rewriting integrations when you change providers.
- Predictable pricing — Managed secret services charge per-secret, per-access, or both. Self-hosted Vault is flat-rate: one VPS, unlimited secrets, unlimited reads.
- Advanced engines without enterprise fees — The open-source Vault (and OpenBao) include PKI, Transit encryption, KV v2, Database dynamic secrets, SSH CA, and Kubernetes auth at no extra cost.
- Full audit trail under your control — Audit logs live on your disk. Ship them to your own SIEM without per-event fees.
- Data sovereignty — For GDPR, HIPAA, SOC 2, or PCI DSS, keeping root-of-trust keys on infrastructure you physically control simplifies compliance scoping.
- Offline and air-gapped deployments — Once the binary is installed, Vault needs no internet to serve secrets.
- Low latency — A local Vault node responds in single-digit milliseconds, versus 40-150ms for cross-region managed APIs.
Licensing: BSL and OpenBao
A quick note before you install. In August 2023, HashiCorp changed the license for Vault (and most of their products) from the permissive Mozilla Public License 2.0 to the Business Source License 1.1 (BSL). In practice this means:
- You can still freely use, modify, and self-host Vault for your own internal use — including commercial production use inside your company.
- You cannot offer Vault as a competing managed/hosted service without a commercial agreement.
- Four years after each release, the BSL code converts to MPL 2.0.
vault CLI, the HTTP API, policies, and configuration files work almost unchanged. Most of the steps in this guide apply to OpenBao verbatim; replace the apt package name with bao and the binary with bao.For this tutorial we install the official HashiCorp Vault from the HashiCorp apt repository.
Prerequisites
Before you begin, you will need:
- A VPS running Ubuntu 24.04 LTS with root or sudo access
- SSH access to the server
- A public DNS record (e.g.
vault.example.com) pointing to your VPS IP — required for the Let's Encrypt TLS certificate - Ports 80, 443, 8200, and 8201 reachable as appropriate (see Step 4)
- At least 2 GB RAM and 2 vCPU (Vault itself is small; Raft storage is I/O heavy)
- At least 20 GB of SSD storage — NVMe strongly recommended for Raft write latency
Recommended Plan: CloudCore Professional>
For a production Vault node that will also serve as a Raft participant (and leave room for Consul or Nomad on the same box), we recommend the CloudCore Professional plan:>
- 6 vCPU cores
- 12 GB RAM
- 100 GB NVMe SSD
- Unmetered bandwidth
Connect to your server:
ssh root@your-server-ipStep 1: Update System Packages
sudo apt update && sudo apt upgrade -yInstall prerequisite packages used later in this guide:
sudo apt install -y gnupg software-properties-common curl jq unzip ufwSet a descriptive hostname for the Vault node — this will show up in audit logs and cluster output:
sudo hostnamectl set-hostname vault-01Step 2: Add the HashiCorp apt Repository
HashiCorp publishes signed apt packages for Ubuntu. Install the GPG key:
wget -O- https://apt.releases.hashicorp.com/gpg | \
sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpgVerify the key fingerprint:
gpg --no-default-keyring \
--keyring /usr/share/keyrings/hashicorp-archive-keyring.gpg \
--fingerprintExpected fingerprint: 798A EC65 4E5C 1542 8C8E 42EE AA16 FCBC A621 E701.
Add the repository:
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.listRefresh the package index:
sudo apt updateStep 3: Install Vault
sudo apt install -y vaultVerify the installation:
vault --versionExpected output:
Vault v1.17.6 (...)The package creates:
/usr/bin/vault— the binary- A
vaultsystem user and group /etc/vault.d/vault.hcl— default config file (we will replace it)/opt/vault/— data and TLS directories/usr/lib/systemd/system/vault.service— systemd unit- Linux capability
IPC_LOCKallowing Vault to mlock memory so secrets never hit swap
sudo systemctl stop vault 2>/dev/null || true
sudo systemctl disable vault 2>/dev/null || trueStep 4: Obtain a TLS Certificate from Let's Encrypt
Vault's TLS listener needs a certificate. You have three good options: a public Let's Encrypt cert (easiest when the node is internet-reachable), a cert from Vault's own PKI engine (chicken-and-egg on day one), or a cert from an internal CA. We'll use Let's Encrypt with standalone mode so you don't need Nginx running yet.
Open port 80 for the HTTP-01 challenge:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 22/tcp
sudo ufw allow 8200/tcp comment "Vault API"
sudo ufw allow 8201/tcp comment "Vault Raft"
sudo ufw --force enableInstall Certbot and issue the cert:
sudo apt install -y certbot
sudo certbot certonly --standalone \
-d vault.example.com \
--non-interactive --agree-tos -m [email protected]Certbot drops the cert at /etc/letsencrypt/live/vault.example.com/. Vault runs as the vault user and needs to read these files. Instead of opening up /etc/letsencrypt, copy the cert into a directory Vault already owns and set up a renewal hook.
sudo mkdir -p /opt/vault/tls
sudo cp /etc/letsencrypt/live/vault.example.com/fullchain.pem /opt/vault/tls/vault.crt
sudo cp /etc/letsencrypt/live/vault.example.com/privkey.pem /opt/vault/tls/vault.key
sudo chown -R vault:vault /opt/vault/tls
sudo chmod 640 /opt/vault/tls/vault.keyCreate a renewal hook so the cert stays fresh:
sudo tee /etc/letsencrypt/renewal-hooks/deploy/vault.sh > /dev/null <<'EOF'
#!/bin/bash
set -e
cp /etc/letsencrypt/live/vault.example.com/fullchain.pem /opt/vault/tls/vault.crt
cp /etc/letsencrypt/live/vault.example.com/privkey.pem /opt/vault/tls/vault.key
chown vault:vault /opt/vault/tls/vault.crt /opt/vault/tls/vault.key
chmod 640 /opt/vault/tls/vault.key
systemctl reload vault || true
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/vault.shStep 5: Configure Vault with Integrated Raft Storage and TLS
Integrated Raft is Vault's built-in, HashiCorp-recommended storage backend. It stores everything on the local disk, supports clustering out of the box, and removes the dependency on a separate Consul cluster for storage. For a single-node install (or the first node of a future cluster), Raft is the right choice.
Replace the default config:
sudo tee /etc/vault.d/vault.hcl > /dev/null <<'EOF'
ui = true
cluster_name = "vault-prod"
disable_mlock = falsestorage "raft" {
path = "/opt/vault/data"
node_id = "vault-01"
}
listener "tcp" {
address = "0.0.0.0:8200"
cluster_address = "0.0.0.0:8201"
tls_cert_file = "/opt/vault/tls/vault.crt"
tls_key_file = "/opt/vault/tls/vault.key"
tls_min_version = "tls12"
tls_disable = false
}
api_addr = "https://vault.example.com:8200"
cluster_addr = "https://vault.example.com:8201"
Uncomment after Step 14 to enable auto-unseal with AWS KMS
seal "awskms" {
region = "eu-central-1"
kms_key_id = "alias/vault-unseal"
}
EOFSet tight permissions on the data directory and config:
sudo mkdir -p /opt/vault/data
sudo chown -R vault:vault /opt/vault
sudo chmod 700 /opt/vault/data
sudo chown root:vault /etc/vault.d/vault.hcl
sudo chmod 640 /etc/vault.d/vault.hclStep 6: Start Vault with systemd
The apt package ships a hardened systemd unit. Inspect it:
systemctl cat vaultKey hardening flags already in place: ProtectSystem=full, PrivateTmp=yes, NoNewPrivileges=yes, Capabilities=CAP_IPC_LOCK+ep, AmbientCapabilities=CAP_IPC_LOCK, LimitMEMLOCK=infinity.
Enable and start Vault:
sudo systemctl enable vault
sudo systemctl start vault
sudo systemctl status vaultExpected output:
● vault.service - "HashiCorp Vault - A tool for managing secrets"
Loaded: loaded (/usr/lib/systemd/system/vault.service; enabled; preset: enabled)
Active: active (running) since ...
Main PID: 4321 (vault)
Tasks: 9
Memory: 110.0MPoint the CLI at Vault over TLS:
export VAULT_ADDR="https://vault.example.com:8200"
echo "export VAULT_ADDR=\"https://vault.example.com:8200\"" | sudo tee /etc/profile.d/vault.shCheck status — at this point Vault is running but uninitialized and sealed:
vault statusExpected output (exit code 2 is normal for a sealed vault):
Key Value
--- -----
Seal Type shamir
Initialized false
Sealed true
Total Shares 0
Threshold 0
Version 1.17.6
Storage Type raft
HA Enabled trueStep 7: Initialize and Unseal Vault
Initialization generates the master key, splits it into 5 unseal key shares using Shamir's Secret Sharing, and requires any 3 of 5 to unseal. It also prints the initial root token — the only token with unlimited permissions — which you will use once to bootstrap the rest of Vault, then revoke.
vault operator init -key-shares=5 -key-threshold=3Expected output:
Unseal Key 1: abcd1234... Unseal Key 2: efgh5678... Unseal Key 3: ijkl9012... Unseal Key 4: mnop3456... Unseal Key 5: qrst7890...Initial Root Token: hvs.XXXXXXXXXXXXXXXXXX
Vault initialized with 5 key shares and a key threshold of 3.
CRITICAL: Save each unseal key in a different place (a different team member, a different password manager, an offline hardware token). If you lose 3 of 5 keys, your data is unrecoverable. Never store unseal keys in the same location as the encrypted Vault data, and never commit them to git.
Unseal Vault by providing 3 of the 5 keys:
vault operator unsealpaste Unseal Key 1
vault operator unseal
paste Unseal Key 2
vault operator unseal
paste Unseal Key 3
After the third key, Vault transitions to unsealed and vault status shows Sealed: false. Log in with the root token:
vault login hvs.XXXXXXXXXXXXXXXXXXStep 8: Enable Auth Methods (userpass, AppRole, OIDC)
The root token is too powerful for everyday use. Before going further, enable proper auth methods so humans and machines log in with their own identities.
userpass — Humans with Username/Password
vault auth enable userpass
vault write auth/userpass/users/alice \ password='change-me-first-login' \ token_policies="admin"
Alice can now log in:
vault login -method=userpass username=aliceIn production, put MFA in front with Vault's built-in TOTP or point userpass at your SSO via OIDC instead.
AppRole — Machines, CI Jobs, and Applications
AppRole is the standard way for a non-human client (a Jenkins job, a web app, a deployment script) to authenticate. Each app gets a RoleID (semi-public) and a SecretID (secret), exchanges them for a short-lived token, and uses that token to fetch secrets.
vault auth enable approle
vault write auth/approle/role/web-app \ token_policies="web-app-policy" \ token_ttl=1h \ token_max_ttl=4h \ secret_id_ttl=24h \ secret_id_num_uses=1
Retrieve the RoleID (bake this into your app config):
vault read auth/approle/role/web-app/role-idGenerate a SecretID (inject this at deploy time via a trusted delivery mechanism — never commit it):
vault write -f auth/approle/role/web-app/secret-idExchange them for a token:
vault write auth/approle/login \
role_id="ROLE_ID_HERE" \
secret_id="SECRET_ID_HERE"OIDC — Single Sign-On with Google, Okta, Azure AD, or Keycloak
For teams, federate Vault login through your existing identity provider.
vault auth enable oidcvault write auth/oidc/config \ oidc_discovery_url="https://accounts.google.com" \ oidc_client_id="YOUR_CLIENT_ID.apps.googleusercontent.com" \ oidc_client_secret="YOUR_CLIENT_SECRET" \ default_role="engineer"
vault write auth/oidc/role/engineer \ bound_audiences="YOUR_CLIENT_ID.apps.googleusercontent.com" \ allowed_redirect_uris="https://vault.example.com:8200/ui/vault/auth/oidc/oidc/callback" \ allowed_redirect_uris="http://localhost:8250/oidc/callback" \ user_claim="email" \ token_policies="engineer"
Engineers now click "Sign in with OIDC" in the Vault UI and land in an authenticated session scoped by the engineer policy.
Step 9: Enable the KV v2 Secrets Engine
Key-Value v2 is the general-purpose secrets store, with versioning, metadata, and soft delete.
vault secrets enable -path=secret -version=2 kvWrite and read a secret:
vault kv put secret/apps/billing/db \ username="billing_svc" \ password="s3cretP@ss" \ host="db.internal" \ port="5432"
vault kv get secret/apps/billing/db
Read a specific version:
vault kv get -version=1 secret/apps/billing/dbSoft-delete and undelete:
vault kv delete secret/apps/billing/db
vault kv undelete -versions=1 secret/apps/billing/dbPermanently destroy a version (audit-logged, irreversible):
vault kv destroy -versions=1 secret/apps/billing/dbFetch a secret over the HTTP API with a token:
curl -s \
-H "X-Vault-Token: $VAULT_TOKEN" \
https://vault.example.com:8200/v1/secret/data/apps/billing/db | jq .Step 10: Write Policies in HCL
Policies define what a token can do. They are written in HashiCorp Configuration Language (HCL) and attached to tokens at creation.
Create a least-privilege policy for the billing app:
sudo tee /tmp/web-app-policy.hcl > /dev/null <<'EOF'Read and list billing secrets
path "secret/data/apps/billing/*" { capabilities = ["read", "list"] }path "secret/metadata/apps/billing/*" { capabilities = ["list", "read"] }
Use the transit engine to encrypt/decrypt data
path "transit/encrypt/billing" { capabilities = ["update"] } path "transit/decrypt/billing" { capabilities = ["update"] }Renew own token
path "auth/token/renew-self" { capabilities = ["update"] } EOF
vault policy write web-app-policy /tmp/web-app-policy.hcl rm /tmp/web-app-policy.hcl
A broader admin policy for human operators:
sudo tee /tmp/admin.hcl > /dev/null <<'EOF' path "sys/mounts/*" { capabilities = ["create", "read", "update", "delete", "sudo"] } path "sys/mounts" { capabilities = ["read"] } path "sys/policies/acl/*" { capabilities = ["create", "read", "update", "delete", "list"] } path "sys/policies/acl" { capabilities = ["list"] } path "secret/*" { capabilities = ["create", "read", "update", "delete", "list"] } path "auth/*" { capabilities = ["create", "read", "update", "delete", "list", "sudo"] } path "sys/auth" { capabilities = ["read"] } path "sys/health" { capabilities = ["read", "sudo"] } EOF
vault policy write admin /tmp/admin.hcl rm /tmp/admin.hcl
List policies:
vault policy listAssign the admin policy to Alice (already done in Step 8) and revoke the root token now that you have alternatives:
vault token revoke hvs.XXXXXXXXXXXXXXXXXXYou can always regenerate a root token later with unseal key quorum:
vault operator generate-root -initStep 11: Configure PKI for Internal Certificates
Vault's PKI engine turns your Vault into a full-fledged internal CA that issues short-lived, automatically-rotated certificates for internal services.
Enable and tune the root CA mount:
vault secrets enable -path=pki pki
vault secrets tune -max-lease-ttl=87600h pkiGenerate the root certificate (10-year lifespan for the root, short-lived for leaves):
vault write -field=certificate pki/root/generate/internal \
common_name="example.com Internal Root CA" \
issuer_name="example-root-2026" \
ttl=87600h > /opt/vault/tls/ca.crtConfigure CRL and issuing URLs:
vault write pki/config/urls \
issuing_certificates="https://vault.example.com:8200/v1/pki/ca" \
crl_distribution_points="https://vault.example.com:8200/v1/pki/crl"Create an intermediate CA (best practice — never issue directly from the root):
vault secrets enable -path=pki_int pki vault secrets tune -max-lease-ttl=43800h pki_intvault write -format=json pki_int/intermediate/generate/internal \ common_name="example.com Intermediate CA" \ issuer_name="example-intermediate-2026" \ | jq -r '.data.csr' > /tmp/pki_intermediate.csr
vault write -format=json pki/root/sign-intermediate \ csr=@/tmp/pki_intermediate.csr \ format=pem_bundle \ ttl="43800h" \ | jq -r '.data.certificate' > /tmp/intermediate.cert.pem
vault write pki_int/intermediate/set-signed \ certificate=@/tmp/intermediate.cert.pem
Create a role that issues certs for *.internal.example.com:
vault write pki_int/roles/internal-dot-example-dot-com \
issuer_ref="$(vault read -field=default pki_int/config/issuers)" \
allowed_domains="internal.example.com" \
allow_subdomains=true \
max_ttl="720h"Issue a certificate for a service:
vault write pki_int/issue/internal-dot-example-dot-com \
common_name="api.internal.example.com" \
ttl="168h"The response contains certificate, private_key, ca_chain, and serial_number. Integrate this with your deploy tooling or use consul-template / vault-agent to write certs to disk and reload services.
Step 12: Enable the Transit Engine
The Transit engine is encryption-as-a-service. Your apps send plaintext to Vault, get ciphertext back, and store the ciphertext in their own databases. Keys never leave Vault, and rotation is centralized.
vault secrets enable transit
vault write -f transit/keys/billing
Encrypt a payload (plaintext must be base64-encoded):
vault write transit/encrypt/billing \
plaintext=$(echo -n "4242 4242 4242 4242" | base64)Expected output:
Key Value
--- -----
ciphertext vault:v1:...
key_version 1Decrypt:
vault write transit/decrypt/billing \
ciphertext="vault:v1:..." \
| grep plaintext \
| awk '{print $2}' | base64 -dRotate the key (all new encryptions use v2; existing ciphertext stays decryptable):
vault write -f transit/keys/billing/rotateRewrap existing ciphertext to the latest key version without revealing plaintext:
vault write transit/rewrap/billing ciphertext="vault:v1:..."This pattern eliminates the "where do we keep the encryption key" question entirely — the key is in Vault, and your policy decides who can use it.
Step 13: Enable Audit Logging
Audit devices produce a tamper-evident log of every request and response Vault processes. In production, enable at least two so a single disk failure doesn't stop auditing (Vault refuses to serve requests if no audit device can write).
Enable a file audit device:
sudo mkdir -p /var/log/vault sudo chown vault:vault /var/log/vault
vault audit enable file file_path=/var/log/vault/audit.log
Enable a second (syslog) device for redundancy:
vault audit enable -path=syslog syslog tag=vault facility=AUTHRotate the file log with logrotate:
sudo tee /etc/logrotate.d/vault > /dev/null <<'EOF'
/var/log/vault/audit.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
create 0640 vault vault
postrotate
kill -HUP $(pidof vault) 2>/dev/null || true
endscript
}
EOFInspect audit entries — request/response hashes are HMAC-salted, so the actual secret values are not logged in plaintext:
sudo tail -n 20 /var/log/vault/audit.log | jq .Step 14: Auto-Unseal via Cloud KMS
The default Shamir unseal means every Vault restart requires 3 humans to paste keys. For production you want auto-unseal: Vault stores its master key encrypted by an external KMS, and at startup it calls the KMS to decrypt the key and unseal itself.
Supported KMS providers: AWS KMS, GCP Cloud KMS, Azure Key Vault, AliCloud KMS, OCI KMS, HSM (PKCS#11 — Vault Enterprise), and Transit seal (another Vault cluster).
AWS KMS Example
Create a KMS key in AWS (alias/vault-unseal) and an IAM role/user with kms:Encrypt, kms:Decrypt, and kms:DescribeKey permissions. Attach the role to your EC2 instance, or set AWS credentials as environment variables in /etc/vault.d/vault.env.
Uncomment the seal stanza in /etc/vault.d/vault.hcl:
seal "awskms" {
region = "eu-central-1"
kms_key_id = "alias/vault-unseal"
}Because you already initialized with Shamir, you need to migrate the seal:
sudo systemctl restart vault vault operator unseal -migrate
provide 3 of 5 Shamir keys when prompted
After migration, the unseal keys returned by vault operator init become recovery keys — still 3 of 5, still needed for operator-level recovery actions like regenerating the root token, but no longer required to unseal at startup.
For non-cloud VPS deployments, a self-hosted Transit seal works well: run a second tiny Vault cluster whose only job is to hold the unsealing key. This is the "auto-unseal with Vault" pattern and is the recommended approach when you don't want to tie a self-hosted Vault to a cloud provider's KMS.
Step 15: Snapshot Backups
Raft storage supports online snapshots — a point-in-time copy of the entire Vault state (secrets, policies, auth methods, tokens, everything).
Take a manual snapshot:
vault operator raft snapshot save /var/backups/vault/snapshot-$(date +%F-%H%M).snapAutomate daily snapshots with a systemd timer:
sudo mkdir -p /var/backups/vault sudo chown vault:vault /var/backups/vaultsudo tee /etc/systemd/system/vault-snapshot.service > /dev/null <<'EOF' [Unit] Description=Vault Raft snapshot After=vault.service Requires=vault.service
[Service] Type=oneshot User=vault Environment="VAULT_ADDR=https://vault.example.com:8200" EnvironmentFile=/etc/vault.d/snapshot.env ExecStart=/bin/bash -c '/usr/bin/vault operator raft snapshot save /var/backups/vault/snapshot-$(date +%%F-%%H%%M).snap' ExecStartPost=/bin/bash -c 'find /var/backups/vault -name "snapshot-*.snap" -mtime +14 -delete' EOF
sudo tee /etc/systemd/system/vault-snapshot.timer > /dev/null <<'EOF' [Unit] Description=Daily Vault snapshot
[Timer] OnCalendar=daily Persistent=true
[Install] WantedBy=timers.target EOF
The snapshot service needs a token with the sys/storage/raft/snapshot capability. Create one:
vault policy write snapshot - <<'EOF' path "sys/storage/raft/snapshot" { capabilities = ["read"] } EOFvault token create -policy=snapshot -period=768h -format=json \ | jq -r '.auth.client_token' \ | sudo tee /etc/vault.d/snapshot.env > /dev/null
sudo sed -i '1s/^/VAULT_TOKEN=/' /etc/vault.d/snapshot.env sudo chown root:vault /etc/vault.d/snapshot.env sudo chmod 640 /etc/vault.d/snapshot.env
Enable the timer:
sudo systemctl daemon-reload
sudo systemctl enable --now vault-snapshot.timer
sudo systemctl list-timers | grep vaultShip snapshots off-box. Snapshots are encrypted with your Vault's master key, so a stolen snapshot is useless without the unseal/recovery keys — but still push them to encrypted object storage (S3, Backblaze B2, Wasabi) via rclone or restic for disaster recovery.
Restore a snapshot (destructive — use only on a rebuilt node):
vault operator raft snapshot restore /var/backups/vault/snapshot-2026-04-16-0200.snapTroubleshooting
| Problem | Cause | Solution |
|---|---|---|
Error checking seal status: Get ... x509: certificate signed by unknown authority | VAULT_ADDR using IP or short hostname that isn't on the cert | Use the FQDN from the cert: export VAULT_ADDR="https://vault.example.com:8200" |
Error initializing: ... server is not yet initialized then immediately sealed again after restart | Expected with Shamir | Unseal after each restart, or configure auto-unseal (Step 14) |
permission denied reading /opt/vault/tls/vault.key | Cert copied but ownership not updated | sudo chown vault:vault /opt/vault/tls/ |
failed to read cluster members: rpc error | Raft node IDs or cluster_addr misconfigured | Ensure node_id is unique per node and cluster_addr resolves between peers |
storage migration check error: ... mlock is not supported | Running Vault inside an unprivileged container | Either grant IPC_LOCK capability or set disable_mlock = true (reduces security) |
| Audit logs growing fast and filling disk | High request volume, no rotation | Configure logrotate (Step 13) and consider shipping to remote syslog |
Error making API request: 503: local node not active | Node is a Raft follower — only the leader serves writes | Point VAULT_ADDR at the leader, or use a load balancer that health-checks /v1/sys/health?standbyok=true |
Useful diagnostic commands
sudo journalctl -u vault -f
vault status
vault operator raft list-peers
vault operator raft autopilot state
vault read sys/healthFAQ
Do I need a cluster, or is a single Vault node enough?
A single Raft node is fully functional for development and low-stakes internal use. For anything production-critical, run three nodes: Raft tolerates (N-1)/2 failures, so three nodes survive one failure and five nodes survive two. Add nodes by installing Vault on new VPS instances with unique node_id values, pointing retry_join at an existing leader, and unsealing with the same keys.
How is Vault different from AWS Secrets Manager or 1Password Secrets Automation?
Managed secret stores are good at static KV. Vault's differentiator is the breadth of secrets engines: dynamic database credentials (Vault generates a new MySQL/Postgres user with a scoped role, hands over the creds, and auto-revokes them after TTL), PKI certificate issuance, SSH CA signing, AWS/Azure/GCP credential leasing, Kubernetes service account tokens, and Transit encryption-as-a-service. Plus you own the data and the audit pipeline.
How do I rotate the unseal keys?
Use vault operator rekey with a quorum of existing keys. This generates a new set of shares without changing any data; apps keep working without restart. Rotate annually and whenever a keyholder leaves the company.
What's the difference between a root token and a recovery key?
A root token is an actual Vault token that bypasses all policies — use it once during bootstrap, then revoke it. Unseal keys (Shamir) decrypt the master key at startup. Recovery keys (auto-unseal) replace unseal keys once the KMS is doing the unsealing — they're needed for operator-level actions like generating a new root token or rekeying.
Can Vault run inside Kubernetes instead of on a VPS?
Yes — HashiCorp publishes an official Helm chart, and many teams run Vault in-cluster backed by integrated Raft on persistent volumes. The VPS path in this guide is simpler, more portable across clouds, and keeps Vault decoupled from any one Kubernetes cluster's lifecycle. Either works.
What are the resource requirements for Vault?
Vault itself is light: idle memory is under 200 MB, and CPU is near zero outside of request bursts. The demanding resources are disk IOPS (Raft fsyncs every write) and network latency between Raft peers. A 2 vCPU / 4 GB RAM NVMe VPS handles thousands of secrets/sec. The CloudCore Professional plan's 6 vCPU / 12 GB / NVMe gives you room to run Vault alongside Consul or Nomad on the same node for a small-team HashiStack.
How do I migrate from Vault to OpenBao (or vice versa)?
Because OpenBao forked from Vault 1.14, a Vault Raft snapshot restores cleanly into OpenBao up to that version. For newer Vault releases, export secrets via the API into a neutral format and reimport. API compatibility is maintained for the core paths and engines covered in this guide.
Next Steps
With Vault running and hardened, build out the rest of your platform:
- Pair Vault with Consul for service discovery — Install Consul on Ubuntu and use Consul Connect intentions to gate which services can talk to Vault.
- Orchestrate workloads with Nomad — Install Nomad on Ubuntu and use its native Vault integration to inject short-lived tokens into every job.
- Front Vault with Nginx — Install Nginx on Ubuntu to add IP allowlists, rate limiting, and a cleaner URL in front of the Vault API.
- Deploy Vault Agent — Vault Agent runs alongside your apps, handles auto-auth (AppRole, JWT, Kubernetes), caches tokens, and templates secrets into config files. It eliminates most of the custom glue code for secret consumption.
- Turn on Vault's UI — The UI is already enabled in the config (
ui = true). Browse tohttps://vault.example.com:8200/uiand log in with Alice's userpass credentials or OIDC. - Explore dynamic database secrets — Enable the
databasesecrets engine to generate just-in-time Postgres or MySQL users with scoped privileges and automatic revocation. This is the single biggest security win most teams get from Vault.
Deploy Vault on a VPS built for it>
The CloudCore Professional plan gives you the 6 vCPU, 12 GB RAM, and NVMe SSD that Vault's Raft storage loves — with enough headroom to run Consul and Nomad on the same box. Unmetered bandwidth, EU-hosted options for GDPR, and root access from minute one.