tofu-age-encryption provides an external encryption method for OpenTofu using age.
OpenTofu encrypts state with a symmetric key derived from a shared passphrase that every operator must share and rotate together. This project replaces that workflow with age's asymmetric key pairs so operators can keep private keys, specify their own recipients, and rotate access without redistributing a secret.
-
Provide the age recipient and identity file using either environment variables or CLI flags:
Environment variables:
AGE_IDENTITY_FILE
(alias:AGE_KEY_FILE
): path to your age identity fileAGE_IDENTITY
(alias:AGE_KEY
): age identity string; supportsfile:PATH
,cmd:COMMAND
, andcommand:COMMAND
AGE_IDENTITY_COMMAND
(alias:AGE_IDENTITY_CMD
): command whose output is the age identityAGE_RECIPIENT
orAGE_RECIPIENTS
: comma-separated list of age recipientsAGE_RECIPIENTS_FILE
: path to a file with newline-separated age recipients
The following
SOPS_
-prefixed variables are also supported as aliases for compatibility with tools that expect them:SOPS_AGE_KEY_FILE
: alias forAGE_IDENTITY_FILE
SOPS_AGE_KEY
: age identity stringSOPS_AGE_KEY_CMD
: alias forAGE_IDENTITY_COMMAND
SOPS_AGE_RECIPIENTS
: alias forAGE_RECIPIENT
/AGE_RECIPIENTS
(values from both variables are merged; duplicates are ignored)
CLI flags:
--identity-file
: path to your age identity file--identity
: age identity string orfile:PATH
,cmd:COMMAND
,command:COMMAND
--identity-command
: command whose output is the age identity--recipient
: may be provided multiple times or as a comma-separated list of recipients--recipients-file
: path to a file with newline-separated age recipients
- Configure OpenTofu to use the external method. For multiple recipients, build the command dynamically:
terraform {
encryption {
method "external" "age" {
encrypt_command = concat(
["tofu-age-encryption", "--encrypt"],
flatten([for recipient in local.age_recipients : ["--recipient", recipient]])
)
decrypt_command = ["tofu-age-encryption", "--decrypt", "--identity", nonsensitive(var.age_identity)]
}
state {
method = method.external.age
enforced = true
}
plan {
method = method.external.age
enforced = true
}
}
}
locals {
age_recipients = [
"KEY_A",
"KEY_B",
"KEY_C",
]
}
variable "age_identity" {
type = string
sensitive = true
}
resource "random_pet" "example" {}
output "pet" {
value = random_pet.example.id
}
- Run OpenTofu as usual. The state and plan files are encrypted with the given age recipients:
$ tofu init
$ tofu apply
If the tofu-age-encryption
binary is unavailable, you can decrypt an existing state file using common command-line tools:
jq --raw-output '.encrypted_data' terraform.tfstate | base64 --decode >state.age
age --decrypt --identity key.txt state.age >state.json
jq . state.json
This process does not require tofu-age-encryption
and can be used to recover the plain text state for backup or debugging.