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
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"