Replace the gitignored plaintext infra/tofu/.r2.env and backend.hcl with a single committed, SOPS/age-encrypted dotenv (secrets.sops.env) plus a thin wrapper (tf.sh), so tofu runs with no manual secret entry and secrets live encrypted in git like the repo's other secrets. - .sops.yaml: creation rule encrypting infra/tofu/secrets.sops.env (all values) to the existing age recipient. - secrets.sops.env: encrypted, PLACEHOLDER values only. Holds R2 S3 keys, the state-encryption passphrase (TF_STATE_PASSPHRASE scalar), and the Proxmox API token/endpoint. Operator fills real values via `sops`. - tf.sh: sops exec-env decrypts the dotenv, exports PROXMOX_VE_*/AWS_*, builds the multi-line TF_ENCRYPTION HCL from TF_STATE_PASSPHRASE, and runs tofu (injecting -backend-config=backend.r2.hcl on init). Proxmox token stays env-only, never a tofu variable/state. - backend.r2.hcl: committed non-secret R2 bucket/key/endpoint (account id is not a credential), passed to `tofu init`. - Retire .r2.env/backend.hcl(.example): drop the committed templates; keep the gitignore lines as a safety net for leftover local plaintext. - Update backend.tf/README/REMOTE-STATE for the new day-2 flow and DR story (repo + age key is sufficient). Verified: placeholder file round-trips sops -e/-d with the repo age key; tf.sh exports the expected VAR NAMES and builds valid TF_ENCRYPTION HCL (stub tofu, no real plan/secrets). Co-Authored-By: Claude Opus 4.8 <[email protected]>
76 lines
3.3 KiB
Bash
Executable File
76 lines
3.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# infra/tofu/tf.sh — run OpenTofu with EVERY secret sourced from the SOPS/age-encrypted
|
|
# secrets.sops.env. No plaintext .r2.env, no manual secret entry, secrets live encrypted
|
|
# in git like the repo's other secrets.
|
|
#
|
|
# MUST run inside the dev container (it has sops + tofu, and the age PRIVATE key mounted
|
|
# from ~/.config/sops/age/keys.txt):
|
|
#
|
|
# ./infra/devcontainer/dev.sh run ./infra/tofu/tf.sh init
|
|
# ./infra/devcontainer/dev.sh run ./infra/tofu/tf.sh plan
|
|
# ./infra/devcontainer/dev.sh run ./infra/tofu/tf.sh apply
|
|
#
|
|
# What it does:
|
|
# 1. `sops exec-env` decrypts secrets.sops.env and puts the dotenv keys in the ENV
|
|
# (exec-env parses the dotenv itself, so values with shell-special chars — ! ( ) ; —
|
|
# are set safely, unlike `source`). It then re-execs THIS script (phase 2).
|
|
# 2. Phase 2 exports PROXMOX_VE_* / AWS_* (already in env) + TF_VAR_proxmox_endpoint,
|
|
# builds the multi-line TF_ENCRYPTION HCL from the single TF_STATE_PASSPHRASE scalar
|
|
# (same pbkdf2 → aes_gcm → state/plan structure the old .r2.env hardcoded), and runs
|
|
# `tofu`. For `init` it appends `-backend-config=backend.r2.hcl` (the non-secret R2
|
|
# bucket/key/endpoint); other subcommands are passed through unchanged.
|
|
#
|
|
# The Proxmox API token is injected ONLY as PROXMOX_VE_API_TOKEN (provider reads it from
|
|
# the env) — it is NEVER a tofu variable, so it never enters state, vars, or plan output.
|
|
set -euo pipefail
|
|
|
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
SECRETS="${HERE}/secrets.sops.env"
|
|
BACKEND="${HERE}/backend.r2.hcl"
|
|
|
|
# ── Phase 1: decrypt secrets into the environment, then re-exec self ──────────────────
|
|
if [[ "${_TF_SH_SECRETS_LOADED:-}" != "1" ]]; then
|
|
command -v sops >/dev/null 2>&1 || {
|
|
echo "tf.sh: 'sops' not found — run me inside the dev container:" >&2
|
|
echo " ./infra/devcontainer/dev.sh run ./infra/tofu/tf.sh $*" >&2
|
|
exit 127
|
|
}
|
|
[[ -f "$SECRETS" ]] || { echo "tf.sh: missing $SECRETS" >&2; exit 1; }
|
|
export _TF_SH_SECRETS_LOADED=1
|
|
# Re-exec THIS script (with its args) under sops exec-env so the dotenv values are in
|
|
# the process environment. printf %q keeps args intact through exec-env's shell.
|
|
exec sops exec-env "$SECRETS" "$(printf '%q ' "${BASH_SOURCE[0]}" "$@")"
|
|
fi
|
|
|
|
# ── Phase 2: secrets are now in the environment ───────────────────────────────────────
|
|
: "${TF_STATE_PASSPHRASE:?tf.sh: TF_STATE_PASSPHRASE not set (fill in secrets.sops.env)}"
|
|
: "${PROXMOX_VE_ENDPOINT:?tf.sh: PROXMOX_VE_ENDPOINT not set (fill in secrets.sops.env)}"
|
|
|
|
# Enable versions.tf endpoint scheme-normalization (harmless if already has https://).
|
|
export TF_VAR_proxmox_endpoint="${PROXMOX_VE_ENDPOINT}"
|
|
|
|
# Build the state-at-rest encryption blob from the single passphrase scalar.
|
|
export TF_ENCRYPTION="
|
|
key_provider \"pbkdf2\" \"state\" {
|
|
passphrase = \"${TF_STATE_PASSPHRASE}\"
|
|
}
|
|
method \"aes_gcm\" \"state\" {
|
|
keys = key_provider.pbkdf2.state
|
|
}
|
|
state {
|
|
method = method.aes_gcm.state
|
|
}
|
|
plan {
|
|
method = method.aes_gcm.state
|
|
}
|
|
"
|
|
|
|
cd "$HERE"
|
|
|
|
# `-backend-config` is only valid for `init`; inject it there, pass everything else through.
|
|
if [[ "${1:-}" == "init" ]]; then
|
|
shift
|
|
exec tofu init -backend-config="$BACKEND" "$@"
|
|
fi
|
|
exec tofu "$@"
|