tofu: encrypt state with a key from 1Password
The existing state was re-encrypted by one apply with a temporary
unencrypted fallback method.

Assisted-by: Claude Opus 5.5 via Claude Code
change npnoqlwyotmyttmlqwqtnqklrsysvpxm
commit 72d6f784b7f33759e085afa242eb7a3ce3cab479
author Alpha Chen <alpha@kejadlen.dev>
date
parent lmtutnpo
diff --git a/bin/tofu-state-key b/bin/tofu-state-key
new file mode 100755
index 0000000..f81f63a
--- /dev/null
+++ b/bin/tofu-state-key
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+
+# OpenTofu external key provider that serves the base64 AES-256 state key from
+# 1Password. Protocol: https://opentofu.org/docs/language/state/encryption/
+
+set -euo pipefail
+if [[ "${TRACE-0}" == "1" ]]; then
+    set -o xtrace
+fi
+
+main() {
+    local key
+    key=$(op read --no-newline 'op://Private/2oylajxj2xmsj6ijn7ccvxd5zi/password')
+    if [[ $(printf '%s' "$key" | base64 --decode | wc -c) -ne 32 ]]; then
+        echo "tofu-state-key: 1Password item must hold 32 base64-encoded bytes; generate one with 'openssl rand -base64 32'" >&2
+        exit 1
+    fi
+
+    echo '{"magic":"OpenTofu-External-Key-Provider","version":1}'
+
+    # Always send the decryption key: without stored metadata, OpenTofu sends
+    # null on decrypt too, so the input can't tell the two cases apart.
+    cat >/dev/null
+    printf '{"keys":{"encryption_key":"%s","decryption_key":"%s"}}\n' "$key" "$key"
+}
+
+main "$@"
diff --git a/docs/secrets.md b/docs/secrets.md
index 7eea715..ebbef70 100644
--- a/docs/secrets.md
+++ b/docs/secrets.md
@@ -62,6 +62,16 @@ sops host_vars/lotus-land-story.sops.yml
 The same command creates a new `host_vars/<host>.sops.yml`, encrypted
 through the creation rule.
 
+## OpenTofu state
+
+OpenTofu keeps state in the gitignored `terraform.tfstate` and encrypts it
+with AES-GCM. The key is 32 random bytes, base64-encoded, in the
+`tofu-state-key` item in the Private vault. `bin/tofu-state-key` is an
+[external key provider][external] that reads it with `op read`, so each
+`tofu` command that touches state asks 1Password for it.
+
+[external]: https://opentofu.org/docs/language/state/encryption/
+
 ## Losing the key
 
 `.sops.yaml` lists one recipient. If the 1Password item is lost, every
diff --git a/main.tf b/main.tf
index ae62cd0..5c01fe3 100644
--- a/main.tf
+++ b/main.tf
@@ -1,4 +1,24 @@
 terraform {
+  encryption {
+    key_provider "external" "op" {
+      command = ["./bin/tofu-state-key"]
+    }
+
+    method "aes_gcm" "op" {
+      keys = key_provider.external.op
+    }
+
+    state {
+      method   = method.aes_gcm.op
+      enforced = true
+    }
+
+    plan {
+      method   = method.aes_gcm.op
+      enforced = true
+    }
+  }
+
   required_providers {
     dnsimple = {
       source  = "dnsimple/dnsimple"