- Context
- Step 1: Create the Docker Compose file
- Step 2: Create the Vault configuration
- Step 3: Dev vs production mode
- Step 4: Start Vault
- Step 5: Initialize Vault
- Step 6: Back up the unseal keys OUTSIDE the VPS
- Step 7: Unseal Vault
- Step 8: Log in with the root token
- Step 9: Enable the KV v2 secrets engine
- Step 10: Store the first secrets
- Step 11: Read a secret (verification)
- Step 12: Create an application token (not the root token)
- Step 13: Unseal after restart
- Common errors
- Verification
- Estimated time
status: complete audience: both chapter: 02 last_updated: 2026-04 contributors: [alexwill87, claude-cockpit] lang: en
2.7 -- HashiCorp Vault
Context
This is the most important section of the entire chapter. Vault centralizes ALL secrets: API keys, database passwords, Telegram tokens, etc.
Why Vault and not .env files?
| Criteria | .env files | Vault |
|---|---|---|
| Versioning | Risk of accidental commit | Secrets never in git |
| Rotation | Manual, forgotten | Programmable, automated |
| Audit | None | Log of every access |
| Granular access | All or nothing | Fine-grained policies by path |
| Encryption at rest | No | Yes (AES-256-GCM) |
| Multi-service | Copy .env everywhere | Centralized API |
In summary: .env files are technical debt. Vault is an investment.
Step 1: Create the Docker Compose file
$ mkdir -p ~/docker/vault/config
$ mkdir -p ~/docker/vault/data
Create the file ~/docker/vault/docker-compose.yml:
version: "3.8"
services:
vault:
image: hashicorp/vault:1.17
container_name: vault
restart: unless-stopped
ports:
- "127.0.0.1:8200:8200"
environment:
VAULT_ADDR: "http://127.0.0.1:8200"
cap_add:
- IPC_LOCK
volumes:
- ./config:/vault/config:ro
- ./data:/vault/data
command: server -config=/vault/config/vault.hcl
networks:
- vault-net
networks:
vault-net:
driver: bridge
IMPORTANT: The port is bound to 127.0.0.1, not 0.0.0.0. Vault is only accessible locally (and via Tailscale if configured).
Step 2: Create the Vault configuration
Create the file ~/docker/vault/config/vault.hcl:
storage "file" {
path = "/vault/data"
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_disable = 1
}
api_addr = "http://127.0.0.1:8200"
ui = true
disable_mlock = true
Notes:
- tls_disable = 1: Acceptable because Vault is behind Tailscale (WireGuard encryption). In public production, enable TLS.
- disable_mlock = true: Necessary in Docker without additional privileges.
- ui = true: Web interface accessible at http://127.0.0.1:8200/ui.
Step 3: Dev vs production mode
| Mode | Usage | Persistence | Security |
|---|---|---|---|
Dev (vault server -dev) |
Local tests | No (memory) | Root token known |
| Production (what we do here) | VPS | Yes (file/consul) | Secure initialization |
This playbook uses production mode. Do not use dev mode on a VPS.
Step 4: Start Vault
$ cd ~/docker/vault
$ docker compose up -d
Fix the permissions on the data volume (the Vault container runs as the vault user, not root):
$ docker exec vault chown -R vault:vault /vault/data
Why? Without this command, Vault can crash on startup with a permissions error on
/vault/data. The volume mounted from the host belongs to root by default, but the Vault process inside the container runs as thevaultuser.
Verify that the container is running:
$ docker ps | grep vault
Step 5: Initialize Vault
WARNING -- VAULT_ADDR and TLS: Vault CLI uses HTTPS by default. If TLS is disabled (as in this configuration with
tls_disable = 1), you must always passVAULT_ADDR=http://...in eachdocker exec vaultcommand. Without this, you will get errors like "http: server gave HTTP response to HTTPS client". All commands below already include-e VAULT_ADDR=http://127.0.0.1:8200for this reason.
Initialization happens only ONCE. It generates the decryption keys (unseal keys) and the root token.
$ docker exec -e VAULT_ADDR=http://127.0.0.1:8200 vault vault operator init -key-shares=5 -key-threshold=3
Result: 5 unseal keys and 1 root token. Example output:
Unseal Key 1: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Unseal Key 2: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Unseal Key 3: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Unseal Key 4: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Unseal Key 5: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Initial Root Token: hvs.XXXXXXXXXXXXXXXXXXXXXXXX
SAVE THIS INFORMATION IMMEDIATELY.
Step 6: Back up the unseal keys OUTSIDE the VPS
The unseal keys must be stored outside the VPS. If someone compromises the VPS and has the unseal keys, they can decrypt all secrets.
Recommended method: 1. Copy each key into a password manager (1Password, Bitwarden, KeePass) 2. Distribute the keys: 2 for you, 2 for a co-admin, 1 in a physical safe 3. The threshold is 3: you need 3 keys out of 5 to decrypt. No single person can open everything alone.
DO NOT STORE the unseal keys in a file on the VPS.
Step 7: Unseal Vault
After each restart, Vault starts in "sealed" (encrypted) state. You need 3 keys to open it:
$ docker exec -e VAULT_ADDR=http://127.0.0.1:8200 vault vault operator unseal KEY_1_HERE
$ docker exec -e VAULT_ADDR=http://127.0.0.1:8200 vault vault operator unseal KEY_2_HERE
$ docker exec -e VAULT_ADDR=http://127.0.0.1:8200 vault vault operator unseal KEY_3_HERE
Check the status:
$ docker exec -e VAULT_ADDR=http://127.0.0.1:8200 vault vault status
Expected result: Sealed: false
Step 8: Log in with the root token
$ docker exec -e VAULT_ADDR=http://127.0.0.1:8200 vault vault login ROOT_TOKEN_HERE
Or via environment variable (more convenient for scripts):
$ export VAULT_TOKEN='ROOT_TOKEN_HERE'
Step 9: Enable the KV v2 secrets engine
KV (Key-Value) v2 enables secret versioning:
$ docker exec -e VAULT_ADDR=http://127.0.0.1:8200 vault vault secrets enable -path=secret kv-v2
Verify:
$ docker exec -e VAULT_ADDR=http://127.0.0.1:8200 vault vault secrets list
You should see secret/ in the list.
Step 10: Store the first secrets
Store your secrets by category. Each command creates or updates a secret:
OpenRouter API key:
$ docker exec -e VAULT_ADDR=http://127.0.0.1:8200 vault vault kv put secret/openrouter api_key="sk-or-YOUR_KEY_HERE"
Telegram token:
$ docker exec -e VAULT_ADDR=http://127.0.0.1:8200 vault vault kv put secret/telegram bot_token="123456:ABC-DEF..." chat_id="YOUR_CHAT_ID"
GitHub token:
$ docker exec -e VAULT_ADDR=http://127.0.0.1:8200 vault vault kv put secret/github token="ghp_YOUR_TOKEN_HERE"
Database credentials:
$ docker exec -e VAULT_ADDR=http://127.0.0.1:8200 vault vault kv put secret/database \
host="127.0.0.1" \
port="5432" \
name="oa_system" \
user="oa_admin" \
password="STRONG_PASSWORD_HERE"
Cockpit secret:
$ docker exec -e VAULT_ADDR=http://127.0.0.1:8200 vault vault kv put secret/cockpit \
jwt_secret="GENERATE_WITH_openssl_rand_hex_32" \
admin_password="ADMIN_PASSWORD"
Step 11: Read a secret (verification)
$ docker exec -e VAULT_ADDR=http://127.0.0.1:8200 vault vault kv get secret/openrouter
Expected result: the api_key field with the stored value.
Step 12: Create an application token (not the root token)
DO NOT use the root token in your applications. Create a dedicated token:
$ docker exec -e VAULT_ADDR=http://127.0.0.1:8200 vault vault token create \
-display-name="openclaw-app" \
-ttl=720h \
-renewable=true \
-policy=default
Note the generated token. This is the one you will use in the OpenClaw configuration.
Step 13: Unseal after restart
After each VPS restart, Vault starts in sealed mode. You must unseal it manually:
$ docker exec -e VAULT_ADDR=http://127.0.0.1:8200 vault vault operator unseal
# Enter key 1 when prompted
$ docker exec -e VAULT_ADDR=http://127.0.0.1:8200 vault vault operator unseal
# Enter key 2
$ docker exec -e VAULT_ADDR=http://127.0.0.1:8200 vault vault operator unseal
# Enter key 3
SECURITY: Never store the unseal keys in a file on the server. No
vault-unseal.shscript with keys in plain text. The unseal keys must be kept outside the VPS (password manager, physical safe, or cloud KMS). An AI agent that has access to the unseal keys can compromise the entire system.
For production environments, consider Vault auto-unseal with a cloud KMS (AWS KMS, GCP Cloud KMS). It's more complex to set up but eliminates the need for manual unsealing.
IMPORTANT: This script contains unseal keys in plain text. It's a compromise between security and practicality. In a critical environment, prefer manual unsealing or Vault auto-unseal with a cloud KMS.
Common errors
- "server is not yet initialized": You didn't do step 5 (init). Initialization happens only once.
- "Vault is sealed": After a container or VPS restart, Vault is always sealed. You must unseal it with 3 keys.
- Losing the unseal keys: If you lose 3+ keys, your secrets are unrecoverable. Back them up.
- Using the root token in production: The root token is all-powerful. Create dedicated tokens with restricted policies.
- Vault crashing on startup: Check permissions on
~/docker/vault/data/. The folder must belong to your user.
Verification
$ docker exec -e VAULT_ADDR=http://127.0.0.1:8200 vault vault status
$ docker exec -e VAULT_ADDR=http://127.0.0.1:8200 vault vault kv list secret/
$ docker exec -e VAULT_ADDR=http://127.0.0.1:8200 vault vault kv get secret/openrouter
Expected results: - Status: Sealed = false, Initialized = true - Secret list: openrouter, telegram, github, database, cockpit - Reading a secret: value displayed correctly
Estimated time
30 minutes (plus time to generate and back up the secrets).
Proposer une modification sur GitHub