When you configure Terraform to store its state in an S3 bucket, one of the most common backend options you will see is encrypt = true. At first glance it looks simple: you set encrypt = true and your state is encrypted. But do you really know how this interacts with other config options, bucket default encryption, and dedicated KMS keys? In this post we will unpack exactly what encrypt = true does, how it interacts with S3 default encryption, and what patterns you should use in production.

TL;DR

  • encrypt = true and no kms_key_id or sse_customer_key SSE S3 with an S3 managed key
  • encrypt = true and kms_key_id set SSE KMS with the specified KMS key
  • encrypt = true and sse_customer_key set SSE C with your customer provided key
  • encrypt omitted or set to false bucket default or S3 automatic encryption is used

Quick refresher: what are we encrypting and why

Terraform keeps a JSON state file that contains everything it knows about your infrastructure. That includes resource IDs, attributes, and often sensitive values like passwords, connection strings, and API keys. If someone gets hold of your state file, they get a pretty good map of your environment and sometimes actual secrets.

Because of that, the state file should never live unencrypted on disk in any shared location. When you use the S3 backend, Terraform stores the state file as an object in S3, and optionally also stores a lock file if you use state locking. Encrypting those objects at rest is an essential control.

Terraform already uses HTTPS to talk to S3, so data in transit is protected. The encrypt flag on the S3 backend is about encryption at rest inside S3.

What encrypt = true actually does

The S3 backend documentation describes encrypt as: “Enable server side encryption of the state and lock files.” Under the hood, this translates into different S3 headers that Terraform sends when it uploads the state file.

When Terraform does a PutObject to S3, the presence or absence of encrypt and some related arguments controls which server side encryption mode S3 uses.

Case 1: encrypt = true and no kms_key_id or sse_customer_key

If you set encrypt = true and do not set kms_key_id or sse_customer_key, Terraform asks S3 to use SSE S3, which is the standard S3 managed encryption using AES 256. It does this by adding the header:

x-amz-server-side-encryption: AES256

S3 then encrypts the object at rest using an S3 managed key.

Case 2: encrypt = true with kms_key_id

If you want S3 to use a specific KMS key, you add the kms_key_id argument:

terraform {
  backend "s3" {
    bucket     = "my-tf-state"
    key        = "env/prod/terraform.tfstate"
    region     = "eu-west-1"
    encrypt    = true
    kms_key_id = "arn:aws:kms:eu-west-1:123456789012:key/your-key-id"
  }
}
``
 
With this configuration Terraform sends headers that are equivalent to:
 
```http
x-amz-server-side-encryption: aws:kms
x-amz-server-side-encryption-aws-kms-key-id: arn:aws:kms:eu-west-1:123456789012:key/your-key-id

S3 then uses SSE KMS with the specific KMS key you configured. For this to work, the IAM principal that runs Terraform needs kms:Encrypt, kms:Decrypt, and kms:GenerateDataKey permissions on that key.

Case 3: encrypt = true with sse_customer_key

There is also an option to use SSE C, where you provide the encryption key material yourself via sse_customer_key. Terraform then sends the relevant x-amz-server-side-encryption-customer-* headers. This is less common for Terraform state, because you now have to manage the lifecycle of that key material yourself.

Case 4: encrypt omitted or set to false

If encrypt is not set or is explicitly set to false, Terraform does not ask S3 for any particular encryption mode. It simply performs a normal PutObject with no server side encryption headers.

In that case S3 does whatever the bucket configuration says. Historically this meant the object could be stored unencrypted if the bucket did not have default encryption. That changed in 2023.

S3 default encryption and the 2023 change

Since January 5, 2023, S3 automatically encrypts all new objects at rest using SSE S3 if you do not specify another encryption mode in the request. This applies both to new buckets and to existing buckets that were not already using a different default encryption configuration.

On top of that base behavior, S3 still supports configurable bucket default encryption, where you can choose between:

  • SSE S3
  • SSE KMS with either an AWS managed KMS key or a customer managed KMS key

The important rule is:

  • If the PutObject request includes server side encryption headers, those headers win and override the bucket default.
  • If the request does not include encryption headers, the bucket default encryption is used.

This is where Terraform’s encrypt flag and your bucket configuration interact.

How encrypt = true interacts with bucket default encryption

It helps to think through a few common scenarios.

Scenario 1: Bucket default SSE S3, backend has encrypt = false

  • Terraform does not send encryption headers.
  • S3 applies the bucket default SSE S3.
  • The state file is encrypted at rest using S3 managed keys.

In modern S3 this already gives you encryption at rest, even though encrypt is false.

Scenario 2: Bucket default SSE S3, backend has encrypt = true (no kms_key_id)

  • Terraform explicitly asks for SSE S3.
  • S3 uses SSE S3, exactly as the bucket default would have done.
  • Effective behavior is basically the same as Scenario 1.

The difference is that the encryption mode is now encoded in each object upload request. Even if someone later changes the bucket default, Terraform will continue to send the SSE S3 header as long as encrypt = true is set without a KMS key.

Scenario 3: Bucket default SSE KMS with a dedicated KMS key, backend has encrypt = false

In this scenario, the bucket is configured to always encrypt new objects with SSE KMS using a specific KMS key, for example a customer managed CMK.

  • Terraform does not send encryption headers.
  • S3 uses the bucket default SSE KMS with that KMS key.
  • The state file is encrypted with your dedicated KMS key.

This is a perfectly valid pattern. You might also enforce this with a bucket policy that denies uploads unless the request results in SSE KMS with the desired key.

Scenario 4: Bucket default SSE KMS, backend has encrypt = true but no kms_key_id

This is the surprising one.

  • Terraform asks for SSE S3 because encrypt = true and there is no kms_key_id.
  • S3 honors the request and uses SSE S3.
  • This overrides the bucket default SSE KMS.

If you have a strict bucket policy that requires SSE KMS with a particular key, uploads from Terraform will start to fail with AccessDenied, because they do not meet the policy conditions. Even if they do not fail, your state objects will now be encrypted with SSE S3 instead of your dedicated KMS key, which may not be what you want.

Scenario 5: Bucket default SSE KMS with Key A, backend has encrypt = true and kms_key_id = Key A

Here Terraform and the bucket both agree on SSE KMS and on which key to use.

  • Terraform asks for SSE KMS with Key A.
  • S3 applies SSE KMS with Key A.
  • This matches the bucket default and any bucket policy that enforces SSE KMS with that key.

This is the most explicit pattern when you want to guarantee that Terraform always uses the same KMS key for state.

So, do you still need encrypt = true now that S3 encrypts everything

Thanks to S3’s change in 2023, all new objects are encrypted at rest with SSE S3 by default even if you never touch encrypt or bucket default encryption. That begs the question: is encrypt = true still useful

The answer is “it depends on what you want”.

If SSE S3 is good enough for you

If you are comfortable with S3 managed keys and do not need a dedicated KMS key for Terraform state, you can rely on S3’s default behavior and bucket default encryption.

Reasonable options in that case are:

  • Leave encrypt unset or explicitly set it to false and rely entirely on S3 defaults.
  • Optionally set encrypt = true without a KMS key for clarity, knowing that you are explicitly selecting SSE S3.

From a pure “is my state encrypted at rest” perspective, there is no security difference between those two as long as the bucket remains configured with SSE S3 and you do not have special bucket policies.

If you want a dedicated KMS key

If you want more control and auditability, using SSE KMS with a dedicated customer managed key is usually the recommended approach. It gives you:

  • Explicit control over key policies and who can use the key.
  • CloudTrail logging of key usage.
  • The ability to rotate keys under your own policy.

In this case you should:

  1. Configure the bucket to use SSE KMS with your chosen KMS CMK as its default encryption.
  2. Configure the Terraform backend with encrypt = true and kms_key_id pointing to that same key.
  3. Ensure the IAM principal that runs Terraform has the required KMS permissions.
  4. Optionally enforce this with a bucket policy that only allows uploads which result in SSE KMS with that key.

This way you get consistent behavior even if someone tries to change the bucket configuration later.

If the bucket already has SSE KMS default and you do not care about kms_key_id in the backend

Sometimes the S3 bucket is managed by a central platform team, and they handle KMS configuration, keys, and policies. In that case you might decide to keep the Terraform backend minimal and not mention the KMS key at all.

For that scenario a simple and safe option is:

  • Do not set encrypt in the backend.
  • Let the bucket default SSE KMS configuration decide how objects are encrypted.

As long as the bucket default and bucket policy enforce the desired KMS key, Terraform will automatically follow those rules without needing to know the key ID.

To make this concrete, here are a few patterns that tend to work well in real environments.

Pattern 1: Small setups, happy with SSE S3

Backend configuration:

terraform {
  backend "s3" {
    bucket  = "my-tf-state"
    key     = "env/dev/terraform.tfstate"
    region  = "eu-west-1"
    encrypt = true
  }
}

Bucket configuration:

  • Leave default encryption as SSE S3, which is the current default for all buckets anyway.

Why this works

  • Terraform explicitly requests SSE S3.
  • S3 encrypts every state object with S3 managed keys.
  • This is simple and good enough for many non regulated environments.

Pattern 2: Dedicated KMS key for Terraform state

Backend configuration:

terraform {
  backend "s3" {
    bucket     = "my-tf-state"
    key        = "env/prod/terraform.tfstate"
    region     = "eu-west-1"
    encrypt    = true
    kms_key_id = "arn:aws:kms:eu-west-1:123456789012:key/your-key-id"
  }
}

Bucket configuration:

  • Default encryption set to SSE KMS with the same CMK.
  • Optional bucket policy requiring SSE KMS with that key.

Why this works

  • Terraform and S3 both agree to use the same KMS key.
  • You can express strong security requirements in both the bucket and the key policy.
  • Auditors see a clear story for how Terraform state is protected.

Pattern 3: Central platform managed bucket, Terraform is a tenant

Backend configuration:

terraform {
  backend "s3" {
    bucket = "platform-tf-state"
    key    = "team-a/prod/terraform.tfstate"
    region = "eu-west-1"
    # no encrypt, no kms_key_id
  }
}

Bucket configuration:

  • Platform team configures SSE KMS default encryption with their chosen CMK.
  • Platform team enforces encryption via bucket policy.

Why this works

  • Terraform does not need to know anything about keys.
  • The platform team can change keys or rotation policies without touching tenant Terraform configs, as long as they maintain a compatible bucket policy and key policy.

Final thoughts

The encrypt = true flag on the Terraform S3 backend is a switch that controls what server side encryption mode S3 uses for your state and lock files, but it does not live in isolation. S3 now encrypts everything by default, and bucket level default encryption plus KMS settings play a big role in the final outcome.

If you remember only a few things from this post, let it be these:

  • encrypt = true with no kms_key_id asks for SSE S3 and can override a bucket that is configured for SSE KMS.
  • If you want a specific KMS key, set both encrypt = true and kms_key_id and make sure IAM and bucket policies align with that choice.
  • If your platform team already enforces SSE KMS at the bucket level, you can often omit encrypt in the backend and simply rely on their configuration.

As always, the right answer depends on your threat model, compliance needs, and how you organize responsibilities across teams. The important part is to understand the interactions so you can choose a pattern deliberately, rather than relying on defaults that may not match your intent.

Resources