Playbook 06 — HashiCorp Vault Setup
This page covers the unified HashiCorp Vault lifecycle playbook that initializes, unseals, and provisions the secrets backend for the Cyber Sentinel stack in a single run.
Consolidated playbook
Previously split into 06_1_initialize_vault.yml and 06_2_provision_vault.yml, the Vault setup is now a single end-to-end playbook (06_initialize_provision_vault.yml) that detects whether Vault is fresh or already initialized and adapts its behaviour accordingly.
Related pages: Deployment overview · Config Reference · Secrets & Env · Stack & Containers · Nginx & SSL · Architecture · Components
Overview
File: ansible/06_initialize_provision_vault.yml Hosts: all_servers Privilege escalation: No (become not set)
| Property | Value |
|---|---|
| Playbook file | ansible/06_initialize_provision_vault.yml |
| Target hosts | all_servers |
| Vault URL | http://{{ ansible_host }}:8200 |
| Secrets engine | KV v2 at path secret/ |
| Auth method | Root token (auto-generated on first run, vault_root_token on re-run) |
| Idempotent | Yes — safe to re-run on a provisioned Vault |
Behaviour by mode
The playbook automatically detects the state of Vault via the /v1/sys/init endpoint and chooses one of two modes:
- Initializes Vault and captures
root_token+unseal_keys. - Auto-unseals using the freshly generated keys.
- Provisions all secrets, certificates, and credentials.
- Displays root token and unseal keys exactly once at the end — the operator must save them manually.
- Skips initialization.
- Auto-unseals from
group_vars(vault_unseal_keys) if Vault is sealed. - Provisions or updates secrets using
vault_root_tokenfromgroup_vars. - Confirms success without re-displaying any sensitive values.
Unseal keys are never stored in Vault
By design, the unseal keys are never written to Vault itself (chicken-and-egg problem). Save them to a password manager, encrypted vault, or sealed envelope — losing them means permanent loss of access to all Vault data. See the official Seal/Unseal documentation for background.
Pipeline stages
The playbook is organized into seven sequential stages, each with a clear responsibility:
| Stage | Name | Purpose |
|---|---|---|
| 0 | PRE | Pre-flight checks: detect Vault state, validate required variables, verify cert/key pairs |
| 1 | INIT | Initialize Vault (only on first run) |
| 2 | UNSEAL | Auto-unseal Vault using the appropriate key set |
| 3 | KV | Enable the KV v2 secrets engine at secret/ |
| 4 | API | Provision external API tokens |
| 5 | CERTS | Provision TLS certificate / key pairs |
| 6 | CREDS | Provision application and database credentials |
| 7 | DONE | Final message — credential display (first run) or confirmation (re-run) |
Stage 0 — Pre-flight checks
Vault state detection
Queries /v1/sys/init using the ansible.builtin.uri module and stores the result in vault_already_initialized via ansible.builtin.set_fact. This single fact drives the conditional logic for the rest of the play.
| ansible/06_initialize_provision_vault.yml | |
|---|---|
Variable validation (fail-fast)
Required variables are split into two categories and validated using ansible.builtin.assert. The always-required set must be defined for any provisioning run, while the re-run set is only enforced once Vault is initialized (because the first run generates these values itself).
| Group | When enforced | Variables |
|---|---|---|
required_vars_always | Every run | API tokens, app passwords, DB passwords, user names |
required_vars_rerun | Only when Vault is already initialized | vault_root_token, vault_unseal_keys |
Storing variables securely
All sensitive values must be encrypted with ansible-vault encrypt_string before being committed to group_vars/. See the Secrets & Env page for the standard project layout.
Cert/key pair validation
For each service in required_cert_services (pihole, n8n, grafana), the playbook verifies that both vault_<service>_cert and vault_<service>_key are defined and non-empty using the lookup('vars', ...) plugin.
The TLS material itself is generated and consumed by the Nginx & SSL playbook (05).
Stage 1 — Initialize (first run only)
Runs vault operator init inside the hashicorp_vault container with the -format=json flag, parses the JSON output, and stores it in vault_creds (with no_log: true to keep secrets out of the Ansible log).
The vault operator init command applies Shamir's Secret Sharing — by default the master key is split into 5 shares with a threshold of 3.
Unified token
To avoid branching logic in every subsequent task, the playbook computes a single effective_root_token fact that is used by all provisioning tasks regardless of mode:
| ansible/06_initialize_provision_vault.yml | |
|---|---|
The token is later passed to Vault via the X-Vault-Token HTTP header.
Stage 2 — Unseal
After init (or on re-run), the playbook re-checks Vault's health via /v1/sys/health and submits unseal keys to /v1/sys/unseal if Vault is sealed (HTTP 503). Two separate tasks handle the two key sources:
| HTTP Status | Vault state | Reference |
|---|---|---|
200 | Initialized, unsealed, active | Health endpoint |
429 | Standby node | HA mode |
501 | Not initialized | Init concept |
503 | Sealed — unseal required | Seal/Unseal |
Quorum
Vault requires a quorum of unseal keys (default: 3 of 5) to unseal. The playbook submits all available keys in the loop — Vault itself ignores any beyond the required threshold.
Stage 3 — Enable KV secrets engine
Mounts the KV v2 secrets engine at secret/ via /v1/sys/mounts. HTTP 400 is treated as success because it indicates the engine is already mounted (idempotent).
KV v1 vs KV v2
KV v2 adds versioning and soft-delete on top of KV v1. The data path becomes secret/data/<path> instead of secret/<path> — note the extra data/ segment in all subsequent stages.
Stage 4 — API tokens
All external Cyber Threat Intelligence and service API tokens are written under cyber-sentinel/api-keys/ in a single looped task using the KV v2 write endpoint.
| Vault path | Variable | Service | Provider docs |
|---|---|---|---|
cyber-sentinel/api-keys/virustotal | vault_virus_total_token | VirusTotal IP/domain scans | API v3 |
cyber-sentinel/api-keys/gemini/home-network-guardian | vault_gemini_api_key | Google Gemini AI analysis | Gemini API |
cyber-sentinel/api-keys/gemini/kali-linux | vault_kali_gemini_api_key | Gemini for Kali environment | Gemini API |
cyber-sentinel/api-keys/abuse/api-key | vault_abuse_api_key | Abuse.ch (ThreatFox + URLHaus) | Auth-Key docs |
cyber-sentinel/api-keys/grafana/api-key | vault_grafana_api_key | Grafana API | Service accounts |
cyber-sentinel/api-keys/urlscanio/api-key | vault_urlscanio_api_key | urlscan.io | API docs |
These tokens are consumed by the n8n threat enrichment workflow at runtime.
Stage 5 — TLS certificates
Certificate / key pairs for every TLS-fronted service are stored under cyber-sentinel/certs/<service> for later consumption by the Nginx reverse proxy playbook (05). The list of services iterated by this stage is driven by the required_cert_services variable.
Stage 6 — Application & database credentials
All service passwords and database credentials are stored under cyber-sentinel/credentials/ in a single looped task. Each entry stores both the user and the password, with admin as the default user when none is specified (via the default filter).
| Vault path | User | Service | Reference |
|---|---|---|---|
credentials/pihole | admin (default) | Pi-hole web UI | Pi-hole admin |
credentials/grafana | admin (default) | Grafana admin | Grafana auth |
credentials/portainer | portainer_admin_user | Portainer admin | Portainer auth |
credentials/n8n | n8n_admin_user | n8n owner account | n8n auth |
credentials/mysql/root | root | MySQL root | MySQL 8.0 |
credentials/mysql/app_manager | vault_mysql_app_user | MySQL application user | Database init (04.3) |
credentials/mongodb/admin | admin | MongoDB root | MongoDB 4.4 |
credentials/gmail | vault_n8n_user | Gmail credentials for n8n alerting | Google App Passwords |
Stage 7 — Final message
The final stage diverges by mode using the ansible.builtin.debug module.
First-init credential display
On a fresh install, the playbook prints the freshly generated root token and unseal keys exactly once. Save them immediately — they will not be displayed again.
Action required after the first run
- Copy the root token and unseal keys to a secure offline location (password manager, encrypted vault, sealed envelope).
- Add
vault_root_tokenandvault_unseal_keysto yourgroup_vars, encrypted withansible-vault encrypt_string. - These keys will never be displayed again. Lose them and you lose access to all Vault data permanently — see the Seal/Unseal recovery docs.
Re-run confirmation
On subsequent runs, the playbook prints a brief confirmation without exposing anything sensitive. The Vault Web UI is reachable at {{ vault_url }}/ui.
Security posture
The unified playbook tightens several aspects of the previous two-playbook flow:
- No persistent credential debug output. The temporary
DEBUG - Show Keystask from the old06_1is gone. The first-init display is now an intentional, gated, one-time event with explicit operator instructions. no_log: trueon every secret-bearing task. This includes the parsedvault_creds, theeffective_root_tokenfact, and alluricalls that carry tokens, passwords, or keys.- Fail-fast pre-flight. Missing variables or cert pairs abort the run before any secret is written, preventing partial / inconsistent state.
- Unseal keys never written to Vault. Storing unseal keys inside the very system they unseal is a chicken-and-egg violation. They remain entirely in operator-controlled storage. See the Seal concept for the reasoning.
- Defence in depth. Combined with the UFW firewall (02) and Nginx TLS proxy (05), Vault is reachable only over the internal network with encrypted transport.
Further reading: Vault production hardening · Project Security Policy.
Variables reference
Variables consumed by the playbook, grouped by purpose. All sensitive variables should be encrypted with ansible-vault encrypt_string before being committed to group_vars/. For the project-wide convention see the Config Reference.
Always required
| Variable | Purpose | Provider |
|---|---|---|
vault_virus_total_token | VirusTotal API token | virustotal.com |
vault_gemini_api_key | Gemini API key (Home Network Guardian) | Google AI Studio |
vault_kali_gemini_api_key | Gemini API key (Kali) | Google AI Studio |
vault_abuse_api_key | Abuse.ch API key | auth.abuse.ch |
vault_grafana_api_key | Grafana API key | Grafana service accounts |
vault_urlscanio_api_key | urlscan.io API key | urlscan.io |
vault_pihole_admin_password | Pi-hole admin password | — |
vault_grafana_password | Grafana admin password | — |
vault_portainer_password | Portainer admin password | — |
vault_n8n_password | n8n owner password | — |
vault_mysql_root_password | MySQL root password | — |
vault_mysql_password | MySQL app-user password | — |
vault_mongodb_password | MongoDB admin password | — |
vault_n8n_gmail | Gmail app password for n8n alerting | Google App Passwords |
vault_n8n_user | Gmail username for n8n alerting | — |
portainer_admin_user | Portainer admin username | — |
n8n_admin_user | n8n admin username | — |
vault_mysql_app_user | MySQL application username | — |
vault_<service>_cert / vault_<service>_key | TLS material for pihole, n8n, grafana, portainer, firefox, hashicorp_vault (consumed by playbook 05) | Nginx playbook (05) |
Required only on re-runs
| Variable | Purpose | Source |
|---|---|---|
vault_root_token | Root token from the first-init output | vault operator init |
vault_unseal_keys | List of unseal keys (base64) from the first-init output | vault operator init |