state: per-stack Transit keys for namespace-owner access control

- Each stack gets its own Vault Transit key (transit/keys/sops-state-<stack>)
- state-sync passes per-stack Transit URI + age keys on encrypt
- Vault policies scope namespace-owners to their stacks only:
  - sops-admin: wildcard access to all transit keys
  - sops-user-<name>: access only to owned stack keys
- Anca (plotting-book) can only decrypt plotting-book state
- Admin can decrypt everything (via admin Transit policy or age fallback)
- External group sops-plotting-book maps Authentik group to Vault policy
- Updated CLAUDE.md with state sync documentation
This commit is contained in:
Viktor Barzin 2026-03-17 23:08:18 +00:00
parent 6239e07dd5
commit 77143dfd6b
96 changed files with 56972 additions and 56944 deletions

View file

@ -13,14 +13,41 @@ vault_available() {
VAULT_ADDR="$VAULT_ADDR" vault token lookup &>/dev/null 2>&1
}
# Per-stack Transit key URI
transit_uri() {
local stack_name="$1"
echo "${VAULT_ADDR}/v1/transit/keys/sops-state-${stack_name}"
}
# Extract stack name from directory path
stack_name_from_dir() {
basename "$1"
}
# Read age recipients from .sops.yaml
AGE_RECIPIENTS="$(python3 -c "
import yaml, sys
with open('$REPO_ROOT/.sops.yaml') as f: c = yaml.safe_load(f)
for r in c.get('creation_rules', []):
age = r.get('age', '')
if age:
print(age.replace('\n', '').strip())
break
" 2>/dev/null || echo "")"
encrypt_state() {
local dir="$1"
local src="$dir/terraform.tfstate"
local dst="$dir/terraform.tfstate.enc"
local name
name="$(stack_name_from_dir "$dir")"
[ -f "$src" ] || return 0
# Only re-encrypt if state is newer than encrypted version
if [ ! -f "$dst" ] || [ "$src" -nt "$dst" ]; then
sops -e --input-type json --output-type json "$src" > "$dst"
sops -e --input-type json --output-type json \
--hc-vault-transit "$(transit_uri "$name")" \
--age "$AGE_RECIPIENTS" \
"$src" > "$dst"
fi
}
@ -31,7 +58,7 @@ decrypt_state() {
[ -f "$src" ] || return 0
if vault_available; then
# Vault Transit — no local key needed
# Vault Transit — per-stack key, no local key needed
sops -d --input-type json --output-type json "$src" > "$dst"
elif [ -f "${SOPS_AGE_KEY_FILE:-$HOME/.config/sops/age/keys.txt}" ]; then
# Fallback: age key on disk (bootstrap / Vault down)
@ -74,6 +101,7 @@ case "$cmd" in
;;
help)
echo "Usage: state-sync {encrypt|decrypt|commit} [stack-name]"
echo "Encrypt uses per-stack Vault Transit key (transit/keys/sops-state-<stack>)."
echo "Decrypt uses Vault Transit if logged in, falls back to age key."
;;
esac