#!/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 "$@"