Terraform remote state backend on AWS, using discovery-resistant naming patterns and ephemeral local states.
- Single file ignition.
- Globally addressable resource names (S3) are fully randomized.
- State store and lock table reside in home region, and are encrypted with customer managed multi-region KMS keys.
- State, lock, and keys are replicated to secondary region.
Keys are additionally replicated to a "keystore region". - Resulting identifiers are stored in KMS encrypted SSM parameters.
- State and replica have distinct log buckets in their respective regions.
Logs are encrypted and replicated cross-region. - State version history and log history are maintained through lifecycle management.
Note
All resources are locked down through resource-based policies, as applicable. For S3 buckets:
- Require state requests to originate from MFA-authenticated sessions, not older than 1 hour.
- Require all requests to use TLS v1.3 (or better).
- Require all objects to be encrypted exclusively with our key.
- Prevent all PUT and DELETE requests on replication targets.
Out of scope (for now):
- Custom key store
- Custom key material
- Multiple state replication regions
- Cross-account replication
- History object lock (in planning)
Create an AWS CLI SSO profile for your account, or whatever you have to do to commandeer the account
Caution
Storing 4 customer managed keys in 3 regions incurs charges. Even entirely idle deployments, will add 144Â USD to your yearly cloud spend.
tofu init
tofu apply
tofu output seed
# Optional: Display a backend configuration.
./display-backend.tf.sh
Important
Take note of the seed
output. This is crucial to restore the state later. Keep it safe.
To generate the seed outside of the first infrastructure plan generation, -target
the resource.
tofu init
tofu apply -refresh=false -target=random_id.seed
tofu apply
Restore the state by providing the seed that was used to create it.
cd import
tofu init
# Ensure AWS_PROFILE and AWS_REGION are set appropriately.
# Prefix command with space to prevent history entry.
tofu import random_id.seed oCxD1aYEn4eSQXIObCAQZd6KpN_5-82G8_7PGYvXvmo
tofu apply
Expect success
# Note seed.
tofu output -json seed | jq --raw-output '.id'
# Delete state.
rm *.tfstate*
tofu import random_id.seed oCxD1aYEn4eSQXIObCAQZd6KpN_5-82G8_7PGYvXvmo
tofu apply
Expect success
# Write flag to state bucket
echo "$(date) $(whoami)@$(hostname):$PWD" | aws s3 cp - s3://$(tofu output -json s3 | jq --raw-output '.state.id')/flag.txt --sse=aws:kms --sse-kms-key-id=$(tofu output -json kms | jq --raw-output '.state.id')
# Verify
aws s3 cp s3://$(tofu output -json s3 | jq --raw-output '.state.id')/flag.txt -
Expect success
# Write flag to state bucket
echo "$(date) $(whoami)@$(hostname):$PWD" | aws s3 cp - s3://$(tofu output -json s3 | jq --raw-output '.state.id')/flag.txt --sse=aws:kms --sse-kms-key-id=$(tofu output -json kms | jq --raw-output '.state.id')
# Verify on replica
aws s3api wait object-exists --bucket=$(tofu output -json s3 | jq --raw-output '.replica.id') --key=flag.txt
aws s3 cp s3://$(tofu output -json s3 | jq --raw-output '.replica.id')/flag.txt -
Expect failure
# Write flag to replica bucket
echo "$(date) $(whoami)@$(hostname):$PWD" | aws s3 cp - s3://$(tofu output -json s3 | jq --raw-output '.replica.id')/flag.txt --sse=aws:kms
# Replace flag with own
echo "$(date) CAPTURE" | aws s3 cp - s3://$(tofu output -json s3 | jq --raw-output '.replica.id')/flag.txt --sse=aws:kms
# Delete flag
aws s3 rm s3://$(tofu output -json s3 | jq --raw-output '.replica.id')/flag.txt --sse=aws:kms
Name | Version |
---|---|
terraform | >=1.5.9 |
aws | 5.86.0 |
random | 3.6.3 |
Name | Version |
---|---|
aws | 5.86.0 |
aws.keystore | 5.86.0 |
aws.replica | 5.86.0 |
random | 3.6.3 |
No modules.
Name | Description | Type | Default | Required |
---|---|---|---|---|
force_kms_key_deletion_window_in_days | It should usually be worth it to keep the deleted keys for 30 days as a precaution. But, especially during testing, it can be excessive to store the temporary resources this long. Thus, the value is lowered to 7 during tests. |
number |
30 |
no |
force_namespace | We expect that only a single IaC state is used per AWS account, as anything else just raises potential for conflicts. Thus, account-local resources are created with a fixed name to make them easier to discover. If you absolutely require multiple IaC states in one account, you can set this variable to true to have all resources namespaced. |
bool |
false |
no |
Name | Description |
---|---|
dynamodb | Details about the created DynamoDB state lock table. |
iam | ARN of the IAM policy that allows management access to the state resources. |
kms | Details about Server-Side-Encryption keys for created resources. |
s3 | Details about all created S3 buckets. |
seed | Cryptographic seed of this backend deployment. You need this to recover the state at a later point in time. |
ssm | Details about SSM parameters in the account, which hold the names of the created resources. |