TL;DR: OpenTofu’s remote state is the most sensitive file in an infrastructure repo: it holds every resource ID, and sometimes secrets, in plain JSON. The S3 bucket that stores it has to defend itself on three fronts. It must not be deletable by accident, it must reject anything not encrypted with the right key, and thanks to a recent OpenTofu feature it no longer needs a DynamoDB table alongside it to handle locking.
The most sensitive file in the repo
OpenTofu, like Terraform, keeps a state file: a JSON document recording every resource the stack manages, its real-world ID, and its attributes. It is how the tool knows what already exists. It is also, quietly, the most sensitive file in the whole repo. It can hold resource identifiers an attacker would value, and depending on the providers in play it can hold secret values in clear text.
Three bad things can happen to it. It can be deleted, and now the tool has forgotten everything it manages. It can be read by someone who should not. It can be corrupted by two runs writing at once. The bucket that holds remote state has to defend against all three, and terraform-aws-bootstrap’s state-backend module is built around doing exactly that.
The DynamoDB lock table is gone
Start with the corruption problem, because the answer changed recently.
The long-standing pattern for remote state on AWS was an S3 bucket plus a DynamoDB table. S3 held the state; the DynamoDB table held a lock, so two apply runs could not write at once. Everyone who has done Terraform on AWS has provisioned that table.
OpenTofu 1.10 made it unnecessary. The S3 backend gained use_lockfile, which does the locking with a small lock object in the same bucket, using S3’s conditional-write support. No separate table. The state backend is now genuinely one bucket and one key, with the lock living beside the state. It is one fewer resource to create, one fewer thing to pay for, and one fewer moving part to reason about. The module takes the new path, and the DynamoDB table simply is not there.
A bucket you can’t delete by accident
Deletion is guarded with lifecycle { prevent_destroy = true } on the bucket. With that set, OpenTofu refuses to produce a plan that would destroy the bucket. A stray tofu destroy, a refactor that drops the resource, an accidental rename: all of them fail loudly instead of taking the state bucket with them.
This is also why the state-backend module is hand-rolled from raw aws_s3_bucket resources rather than wrapping a community module like terraform-aws-modules/s3-bucket. prevent_destroy has to sit on the actual resource, and a lifecycle block is not something you can pass into a wrapper module as an input. Hand-rolling the bucket keeps prevent_destroy somewhere you can put it and, just as importantly, somewhere the next reader can see it.
Reject anything encrypted wrong
Confidentiality is the subtle one, because the obvious control is not enough.
The bucket has a default encryption configuration: server-side encryption with the customer-managed KMS key. But default encryption is a default. A client making a PutObject call can override it per request, asking for plain AES256 or a different KMS key, and S3 will honour the override.
So the module does not rely on the default. The bucket policy explicitly denies the upload it does not want. It denies any request not over TLS. It denies any PutObject that is not using SSE-KMS. And it denies any PutObject that names the wrong KMS key. The default encryption config says “this is what you get if you do not ask”; the bucket policy says “and you are not allowed to ask for anything else.” State can only ever land encrypted, in transit and at rest, under the one key the module controls.
One small companion setting: bucket_key_enabled. With per-object SSE-KMS, every object operation is also a KMS API call, which costs money and can throttle. An S3 Bucket Key collapses those into far fewer KMS calls, cutting per-object KMS traffic by well over ninety per cent. It is a one-line setting the module turns on and most people forget exists.
In short
Remote state is the most sensitive file an infrastructure repo has, and the bucket that holds it has to defend against deletion, disclosure and corruption.
terraform-aws-bootstrap’s state backend handles corruption with OpenTofu 1.10’s use_lockfile, dropping the old DynamoDB lock table entirely. It guards deletion with prevent_destroy, which is also why the bucket is hand-rolled rather than wrapped. And it guards confidentiality with a bucket policy that denies non-TLS traffic and denies any upload not encrypted with the right KMS key, because default encryption is only a default and a client can override it. The state bucket is not just a place to put state. It is built to refuse every wrong thing that could happen to it.