From 4ee1b66e2368d33748ed1e77d3d3975c25e1a9b3 Mon Sep 17 00:00:00 2001 From: sjat Date: Sat, 30 May 2026 18:16:35 +0200 Subject: [PATCH] Source vault password from Vaultwarden via rbw; nest vault structure Master vault password is fetched from Vaultwarden via the rbw agent (scripts/vault-pass-client.sh, wired as vault_password_file) instead of a plaintext .vault_pass. Vault secrets use a nested vault.. map. Encrypted vault.yml files are excluded from lint. Includes the host rename in Makefile and STATUS.md. Co-Authored-By: Claude Opus 4.8 (1M context) --- .ansible-lint | 1 + .yamllint | 1 + CLAUDE.md | 5 +- Makefile | 9 +- STATUS.md | 5 +- ansible.cfg | 2 +- docs/decisions/002-security.md | 11 +- docs/decisions/003-toolchain.md | 8 +- docs/runbooks/rotate-secrets.md | 106 ++++++++++-------- .../production/group_vars/all/vault.yml | 25 +++-- scripts/vault-pass-client.sh | 35 ++++++ 11 files changed, 142 insertions(+), 66 deletions(-) create mode 100755 scripts/vault-pass-client.sh diff --git a/.ansible-lint b/.ansible-lint index 1a298da..147ab8c 100644 --- a/.ansible-lint +++ b/.ansible-lint @@ -6,6 +6,7 @@ exclude_paths: - .venv/ - .collections/ - .scaffold/ + - "**/vault.yml" # ansible-vault encrypted — not lintable YAML # Warn only (don't fail) on these rules during initial setup # Remove entries as the codebase matures diff --git a/.yamllint b/.yamllint index 1ee0c62..b47f9ca 100644 --- a/.yamllint +++ b/.yamllint @@ -24,3 +24,4 @@ ignore: | .venv/ .collections/ .scaffold/ + **/vault.yml diff --git a/CLAUDE.md b/CLAUDE.md index d7125a3..ba498a7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -61,7 +61,10 @@ Full design rationale: `docs/decisions/` - Encrypted files are always named `vault.yml`, sitting alongside `vars.yml` - Never put plaintext secrets in any file not named `vault.yml` -- Vault password file: `.vault_pass` (gitignored — obtain via secure channel) +- Structure secrets as a nested map `vault..` (e.g. + `vault.grafana.admin_password`); reference as `{{ vault.grafana.admin_password }}` +- Vault password comes from Vaultwarden via `rbw` (`scripts/vault-pass-client.sh`, + wired as `vault_password_file`). Unlock once per session: `rbw unlock` - To edit a vault file: `make decrypt FILE=`, edit, `make encrypt FILE=` --- diff --git a/Makefile b/Makefile index 37ede35..36a7617 100644 --- a/Makefile +++ b/Makefile @@ -9,12 +9,13 @@ PLAYBOOK := $(VENV)/bin/ansible-playbook GALAXY := $(VENV)/bin/ansible-galaxy LINT := $(VENV)/bin/ansible-lint MOLECULE := $(VENV)/bin/molecule -VAULT_ARGS := --vault-password-file .vault_pass +# Vault password is resolved via ansible.cfg (vault_password_file); no flag needed. +VAULT_ARGS := INVENTORY := -i inventories/production/hosts.yml TF := terraform TF_ENV ?= staging -MOLECULE_IMAGE := git.baobab.band///molecule-debian13:latest +MOLECULE_IMAGE := forgejo.nyumbani.baobab.band///molecule-debian13:latest MOLECULE_DOCKERFILE := .docker/molecule-debian13/Dockerfile .DEFAULT_GOAL := help @@ -101,13 +102,13 @@ encrypt: ifndef FILE $(error FILE is required: make encrypt FILE=) endif - $(ANSIBLE)-vault encrypt --vault-password-file .vault_pass $(FILE) + $(ANSIBLE)-vault encrypt $(FILE) decrypt: ifndef FILE $(error FILE is required: make decrypt FILE=) endif - $(ANSIBLE)-vault decrypt --vault-password-file .vault_pass $(FILE) + $(ANSIBLE)-vault decrypt $(FILE) # ── Molecule test image ─────────────────────────────────────────────────────── diff --git a/STATUS.md b/STATUS.md index 3bff46d..a5d43b3 100644 --- a/STATUS.md +++ b/STATUS.md @@ -16,8 +16,9 @@ _Last reviewed: 2026-05-30._ | `.docker/molecule-debian13/Dockerfile` | Present — custom Molecule test image (ADR-008) | | `docs/decisions/*`, `docs/runbooks/*` | Current and mutually reconciled | | `Makefile`, lint config (`.ansible-lint`, `.yamllint`), `.gitignore` | Present and used | -| `git` (local) | Initialized — trunk-based on `main`. Off-machine remote (Forgejo) being set up separately. | +| `git` | Initialized, trunk-based on `main`, pushed to `origin` (`forgejo.nyumbani.baobab.band:7577`). | | Pre-commit hooks | Configured: lint, gitleaks, vault-encryption guard. Activate with `pre-commit install` after `make setup`. | +| Vault password client | `scripts/vault-pass-client.sh` fetches the master password from Vaultwarden via `rbw` (wired as `vault_password_file`). Requires `rbw` installed + `rbw unlock`. | | Terraform HCL (`terraform/`) | Written (proxmox VM module + envs) — but never run; see below | ## Scaffolded but empty — NOT implemented @@ -41,7 +42,7 @@ calls are empty. | CI (Forgejo Actions) | ADR-003 / ADR-008 | Pipeline described; not implemented | | Level 2 / 3 testing (staging, `askari` smoke) | ADR-008 | Depends on real VMs / `askari`, which don't exist yet | | Per-service roles | ADR-004 | Model defined; no service roles built | -| Forgejo remote + CI | ADR-003 / ADR-008 | Local git is live; pushing to `git.baobab.band` and Actions CI are being set up | +| Forgejo Actions CI | ADR-003 / ADR-008 | Remote is live (pushed); Actions/`act_runner` pipeline not yet built | ## Keeping this honest diff --git a/ansible.cfg b/ansible.cfg index 0e1cd75..1fc913b 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -2,7 +2,7 @@ inventory = inventories/production/hosts.yml roles_path = roles collections_path = .collections -vault_password_file = .vault_pass +vault_password_file = scripts/vault-pass-client.sh interpreter_python = auto_silent stdout_callback = yaml callbacks_enabled = timer, profile_tasks diff --git a/docs/decisions/002-security.md b/docs/decisions/002-security.md index 84bfaf4..d9af3ee 100644 --- a/docs/decisions/002-security.md +++ b/docs/decisions/002-security.md @@ -54,10 +54,13 @@ some public-facing services — not a compliance exercise. ## Secrets management -- Ansible Vault for all secrets (API keys, passwords, certificates) -- Vault password stored outside the repo (`.vault_pass` gitignored) -- New collaborators receive vault password via a separate secure channel -- See `docs/runbooks/rotate-secrets.md` for rotation procedure +- Ansible Vault for all secrets (API keys, passwords, certificates), structured as a + nested `vault..` map (ADR-003) +- The master vault password lives in **Vaultwarden** and is fetched on demand by + `scripts/vault-pass-client.sh` (wired as `vault_password_file`) through the `rbw` + agent — never written to a plaintext file on disk. Unlock once per session with + `rbw unlock`; nothing decryptable sits at rest in the repo or working tree +- See `docs/runbooks/rotate-secrets.md` for `rbw` setup and rotation ## What this baseline does not include diff --git a/docs/decisions/003-toolchain.md b/docs/decisions/003-toolchain.md index ef260c3..19967dc 100644 --- a/docs/decisions/003-toolchain.md +++ b/docs/decisions/003-toolchain.md @@ -33,8 +33,10 @@ reproducible, and has no extra dependencies. - HashiCorp Vault: powerful, but significant operational overhead for this scale **Rationale**: Vault is built-in, requires no extra services, and works well at this -scale. The main limitation (whole-file encryption makes diffs unreadable) is mitigated -by keeping `vault.yml` files small and purposeful — only actual secrets, no structure. +scale. Whole-file encryption makes diffs unreadable regardless of layout, so rather +than flattening we organise secrets for human lookup and clean extraction: a nested +`vault..` map inside each `vault.yml`, scoped to actual secrets (see +CLAUDE.md → Secrets). --- @@ -71,7 +73,7 @@ Config files: `.ansible-lint`, `.yamllint` in repo root. ## CI/CD -**Choice**: Forgejo Actions (self-hosted at git.baobab.band) + `act_runner` +**Choice**: Forgejo Actions (self-hosted at forgejo.nyumbani.baobab.band) + `act_runner` **Not chosen**: GitHub Actions (external), Jenkins (heavy) diff --git a/docs/runbooks/rotate-secrets.md b/docs/runbooks/rotate-secrets.md index 26cc096..05dfd59 100644 --- a/docs/runbooks/rotate-secrets.md +++ b/docs/runbooks/rotate-secrets.md @@ -1,71 +1,89 @@ -# Runbook — Rotating vault secrets +# Runbook — Vault secrets (setup & rotation) + +The master vault password lives in **Vaultwarden** (item `boma-ansible-vault`) and is +fetched on demand by `scripts/vault-pass-client.sh` through the `rbw` agent. It is +never stored in a plaintext file on disk. See ADR-002. + +--- + +## One-time — `rbw` setup on a new machine + +```bash +# Install rbw (Debian 13: try apt first, else cargo) +sudo apt install rbw || cargo install rbw + +# Point it at the homelab Vaultwarden and log in +rbw config set base_url https://vaultwarden.baobab.band +rbw config set email +rbw login # prompts for your Vaultwarden master password (+ 2FA if enabled) + +# Unlock the agent (do this once per terminal session before vault operations) +rbw unlock + +# Sanity check — should print the master vault password: +rbw get boma-ansible-vault +``` + +Once unlocked, `make encrypt/decrypt/check/deploy` and the pre-commit ansible-lint +hook all obtain the password automatically. If the agent is locked you'll see a +clear "run: rbw unlock" error rather than a hang. + +--- ## Rotating a single secret value -1. Decrypt the relevant vault file: +1. Ensure the agent is unlocked: `rbw unlock` +2. Decrypt the relevant vault file: ```bash make decrypt FILE=inventories/production/group_vars/all/vault.yml ``` - -2. Edit the file and update the secret value. - -3. Re-encrypt: +3. Edit the value (keep the nested `vault..` structure). +4. Re-encrypt: ```bash make encrypt FILE=inventories/production/group_vars/all/vault.yml ``` - -4. Commit the updated vault file: +5. Commit and deploy: ```bash git add inventories/production/group_vars/all/vault.yml - git commit -m "Rotate " - ``` - -5. Deploy to apply the new secret to hosts: - ```bash + git commit -m "Rotate " make check PLAYBOOK=site # verify what will change make deploy PLAYBOOK=site ``` --- -## Rotating the vault password +## Rotating the master vault password -This affects all encrypted files in the repo. Do this only when: -- A person with vault access leaves the project -- The password is suspected to be compromised +This re-keys every encrypted file from the old password to a new one. **Order +matters** — re-key the files *before* updating Vaultwarden, or the fetch script will +hand back the new password while the files are still encrypted with the old one. -Steps: - -1. Ensure you have the current vault password in `.vault_pass`. - -2. Re-key all vault files: +1. Unlock the agent: `rbw unlock` (so the script can read the current/old password). +2. Generate a new password and write it to a throwaway file *outside* the repo: ```bash - find . -name "vault.yml" | xargs ansible-vault rekey \ - --vault-password-file .vault_pass \ - --new-vault-password-file /path/to/new_password_file + umask 077; rbw generate --length 40 > /tmp/new_vault_pass ``` - -3. Replace `.vault_pass` with the new password file. - -4. Distribute the new password to all collaborators via a secure channel. - -5. Commit all rekeyed vault files: +3. Re-key all vault files (old via the fetch script, new from the temp file): ```bash - git add -A - git commit -m "Rekey all vault files" + find . -path ./.venv -prune -o -name vault.yml -print \ + | xargs ansible-vault rekey \ + --vault-password-file scripts/vault-pass-client.sh \ + --new-vault-password-file /tmp/new_vault_pass ``` +4. Update the `boma-ansible-vault` item in Vaultwarden to the new password + (paste the contents of `/tmp/new_vault_pass`), then re-sync: `rbw sync`. +5. Shred the temp file: `shred -u /tmp/new_vault_pass` (or `rm -f`). +6. Verify: `make decrypt FILE=` should still work, now via the new + password. Commit the re-keyed files. + +Rotate the master password when someone with access leaves, or if it is suspected +compromised. --- -## Adding a new collaborator +## Access for additional people / machines -1. Share the vault password via a secure channel (password manager, etc.) -2. The collaborator creates `.vault_pass` locally (gitignored) -3. They can now decrypt/encrypt vault files normally - -## Removing a collaborator's access - -Rotate the vault password as described above. There is no per-user access -control in Ansible Vault — access is binary (has the password or not). - -If per-user access control becomes necessary, evaluate SOPS + age at that point. +Access is granted in Vaultwarden — share the `boma-ansible-vault` item (or place it +in a shared collection). Each person/machine then runs the one-time `rbw` setup +above. To revoke access, remove the share in Vaultwarden and rotate the master +password (above), since anyone who had it could have copied it. diff --git a/inventories/production/group_vars/all/vault.yml b/inventories/production/group_vars/all/vault.yml index 32d9d45..9eb12fd 100644 --- a/inventories/production/group_vars/all/vault.yml +++ b/inventories/production/group_vars/all/vault.yml @@ -1,7 +1,18 @@ ---- -# This file must be encrypted with Ansible Vault before committing. -# Run: make encrypt FILE=inventories/production/group_vars/all/vault.yml -# -# Example secrets (plaintext shown here for structure reference only): -# -# vault__ansible_become_password: "changeme" +$ANSIBLE_VAULT;1.1;AES256 +62313835613730303334653334393033646661323865636534353061333765326239333835643139 +6631393939363263313861656461303134383162336662380a333564343131323036343137383736 +64666265653233636631373266396132623561363766326266353638643538303936613435333530 +3864356133633663650a376362313861326263633036303664336439663030613438636339613765 +63383431376636646236346435363035333036373466613066643761646237323133633866366230 +38396333313238393630336263373063363538343865366432353138643663653638356438303738 +66313832343436616634313734343433363362613437383963383263363666613431346663376263 +62633162633962376537306262353736336435343339333266643661373538643236636631666662 +39643664303137356562313061306439623239656534323065306132643833383738623261393232 +63643434396165343631633063616161616430373130663830623936306339393933653437393931 +37633532363264636537343165316231363130613964646635666665363136623637326561323336 +30386235366261353661656231396362366263316338663135663333306434306563363464363336 +35376233303939393039646261633833656337666335636333343030343435656664306433363530 +36306636303530336262396664646331663834336235663236656636353833396437303636373133 +64313639346164656438613066636661353736613334383734633232376335323761396634363031 +65333631636337323630353165356539306531393434633163373637373739366131363734353934 +34373762303661316235353162636132623736646630663438366433376639613964 diff --git a/scripts/vault-pass-client.sh b/scripts/vault-pass-client.sh new file mode 100755 index 0000000..edf8404 --- /dev/null +++ b/scripts/vault-pass-client.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# +# ansible-vault password client. +# +# Prints the boma master vault password to stdout by fetching it from Vaultwarden +# via the `rbw` agent. Wired in as `vault_password_file` (ansible.cfg) and used by +# the Makefile vault targets, so every ansible-vault / ansible-playbook / lint run +# obtains the password the same way. +# +# The password lives only in Vaultwarden (encrypted at rest) and in the rbw agent's +# memory while unlocked — never in a plaintext file on disk. +# +# Unlock once per terminal session before running any vault operation: +# rbw unlock +# +# Override the Vaultwarden item name via BOMA_VAULT_ITEM if it ever changes. +# +set -euo pipefail + +item="${BOMA_VAULT_ITEM:-boma-ansible-vault}" + +if ! command -v rbw >/dev/null 2>&1; then + echo "vault-pass-client: 'rbw' is not installed — see docs/runbooks/rotate-secrets.md." >&2 + exit 1 +fi + +# Only the password reaches stdout; all diagnostics go to stderr so they can never +# be mistaken for the password by ansible-vault. +if ! pw="$(rbw get "$item" 2>/dev/null)"; then + echo "vault-pass-client: could not read '$item' from Vaultwarden via rbw." >&2 + echo " The agent is probably locked. Run: rbw unlock" >&2 + exit 1 +fi + +printf '%s\n' "$pw"