S3 encryption, option by option

Lesson 1 of 4 in Protecting Data at Rest.

Every object written to Amazon S3 is encrypted at rest. That has been true by default since January 2023 — new objects without an explicit encryption setting get SSE-S3, AES-256 with keys AWS manages entirely. So the exam almost never asks “is it encrypted?”. It asks which option, and every option exists to satisfy a requirement the others cannot.

SSE-S3 is free, invisible, and needs no configuration. You cannot audit individual key use, you cannot set a key policy, and you cannot revoke access by disabling a key — because the key is not yours. It is the right answer when the requirement is simply “data must be encrypted at rest”.

SSE-KMS uses a KMS key, which buys three things: CloudTrail records every key use, the key policy becomes a second gate on the data independent of the bucket policy, and disabling the key makes the objects unreadable immediately regardless of S3 permissions. Those three are the reasons to pay for it, and each maps to a requirement phrase the exam reuses — “audit trail of key usage”, “separation of duties between bucket owner and key owner”, “ability to revoke access to all objects at once”.

S3 Bucket Keys fix the one real problem with SSE-KMS. Without them, S3 calls KMS for every object operation, so a workload reading millions of small objects generates millions of KMS requests and a bill to match. A bucket key is a bucket-level key that S3 uses for a limited time to derive per-object data keys, cutting KMS request costs by up to 99%. Enable it for any high-volume bucket; it is one setting, applies to new objects, and requires no application change. Note the exception that makes a good exam question: Bucket Keys are not supported for DSSE-KMS.

DSSE-KMS applies two independent layers of server-side encryption, both KMS-based. It exists for a narrow set of regimes — certain national-security and defence requirements — that mandate two layers at rest. It costs more and halves nothing else. Choose it only when a question names dual-layer explicitly; it is otherwise a distractor.

SSE-C inverts the model: you send the encryption key with every single request, S3 uses it and discards it, and AWS stores no copy of it. The server-side cryptography is AWS’s, the key custody is entirely yours — and so is the consequence: lose the key and the object is unrecoverable, with no support path. SSE-C also requires HTTPS for every request, for the obvious reason that the key is in the headers.

Client-side encryption goes further still: the data is encrypted before it ever leaves your process, with the AWS Encryption SDK or the S3 Encryption Client, and S3 receives ciphertext it cannot read even in principle. This is the only option for “AWS must never be able to decrypt our data”, and it moves key management, key rotation and the entire operational burden to you.

The six S3 encryption options and the requirement each one uniquely satisfies
OptionWho holds the keyAuditable per key useRequest cost at volumeThe requirement it answers

SSE-S3 (default)

AWS, entirely

No

None — no KMS requests

“Data must be encrypted at rest.” Nothing more

SSE-KMS

You, as a KMS key

Yes — CloudTrail per Decrypt

One KMS request per object operation

“Audit every use of the key”, “separate key control from bucket control”, “revoke access to everything at once”

SSE-KMS + Bucket Keys

You, as a KMS key

Yes, at bucket-key granularity

Up to 99% lower

“SSE-KMS, but our KMS bill is enormous”

DSSE-KMS

You, as a KMS key

Yes

Highest — and Bucket Keys are not supported

“Two independent layers of encryption at rest”, named explicitly

SSE-C

You — supplied on every request, stored by AWS never

Not by AWS; you log it yourself

None — no KMS involved

“AWS must not store our key.” Lose it and the data is gone

Client-side

You — AWS never sees plaintext at all

Only what you log

Depends on your key source

“AWS must not be able to decrypt our data under any circumstance”

Which S3 encryption option does this requirement demand?

Interactive decision tree — outcomes:

  • Client-side encryption

    Use the AWS Encryption SDK or the S3 Encryption Client, with keys from KMS, CloudHSM or your own key manager. S3 stores ciphertext it cannot interpret. You now own key management, rotation, and the consequences of losing a key — budget for that operational weight before choosing it.

  • SSE-C

    You pass the key in headers on every PUT and GET, over HTTPS, and AWS keeps no copy — so there is no recovery path whatsoever if you lose it. Also note that S3 features which need to read object content (and some management operations) cannot work with SSE-C objects.

  • SSE-S3

    The default since January 2023: AES-256, AWS-managed, free, nothing to configure, no KMS request charges. If no requirement mentions auditing key use, controlling a key policy, or revoking by key, this is the correct and cheapest answer.

  • DSSE-KMS

    Two layers of server-side encryption, both with KMS keys, for regimes that mandate dual-layer at rest. It costs more per request and cannot use S3 Bucket Keys. Choose it only when the requirement says dual-layer; otherwise it is the trap answer.

  • SSE-KMS with S3 Bucket Keys enabled

    Full KMS control and CloudTrail auditability, with KMS request costs cut by up to 99% because S3 derives per-object keys from a time-limited bucket-level key. One bucket setting, applies to new objects, no application change. This is the default recommendation for any busy bucket on SSE-KMS.

  • SSE-KMS

    A customer managed key gives you the key policy as an independent gate, CloudTrail on every use, and immediate revocation by disabling the key. Even at modest volume, enabling Bucket Keys costs nothing and removes a future surprise — so there is rarely a reason not to.

Setting a default is not the same as requiring one. Default bucket encryption decides what happens to an object that arrives without encryption headers, and since the 2023 change every bucket has one. But a client may still specify a different option on PutObject — including a weaker one — so if a standard says “all objects in this bucket must use our KMS key”, the default is the convenience and a bucket policy condition is the control.

Two conditions do the work. s3:x-amz-server-side-encryption carries the requested algorithm (aws:kms, AES256, aws:kms:dsse), and s3:x-amz-server-side-encryption-aws-kms-key-id carries the specific key ARN — so you can deny any PutObject that does not name your key. Write these as denies, and remember the subtlety that generates support tickets: because default encryption applies when headers are absent, a deny on StringNotEquals for the header value will also reject uploads that specify nothing at all. If you want to allow the default path, use the Null operator to distinguish “header absent” from “header present with the wrong value”.

Require a specific KMS key

“Objects in this bucket must be encrypted with our key, not just any KMS key.”

{
  "Sid": "RequireOurKmsKey",
  "Effect": "Deny",
  "Principal": "*",
  "Action": "s3:PutObject",
  "Resource": "arn:aws:s3:::clinical-records/*",
  "Condition": {
    "StringNotEquals": {
      "s3:x-amz-server-side-encryption-aws-kms-key-id":
        "arn:aws:kms:eu-west-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab"
    }
  }
}

Pair it with a deny on uploads that request AES256, or the client can simply ask for SSE-S3 and bypass the key entirely.

Require SSE-KMS at all

The broader form, allowing any KMS key:

{
  "Sid": "RequireSseKms",
  "Effect": "Deny",
  "Principal": "*",
  "Action": "s3:PutObject",
  "Resource": "arn:aws:s3:::clinical-records/*",
  "Condition": {
    "StringNotEqualsIfExists": {
      "s3:x-amz-server-side-encryption": "aws:kms"
    },
    "Null": {
      "s3:x-amz-server-side-encryption": "false"
    }
  }
}

The Null condition restricts this deny to requests that did send the header, letting bucket default encryption handle the ones that did not. Drop it and you reject well-behaved clients that rely on the default.

Verifying what is actually there

Policies govern new objects. For the ones already in the bucket:

  • S3 Inventory reports the encryption status and KMS key id of every object on a schedule — the only practical way to answer “are all 40 million objects on the right key?”.
  • AWS Config rules (s3-bucket-server-side-encryption-enabled, s3-default-encryption-kms) check bucket configuration continuously and can report into Security Hub.
  • Re-encrypting existing objects means copying them — S3 Batch Operations with a copy job, or a CopyObject with the new encryption headers. There is no in-place re-encryption, and the copy creates a new version in a versioned bucket.

Key terms: SSE-S3, SSE-KMS, DSSE-KMS, SSE-C, S3 Bucket Keys, Client-side encryption

Interactive checkpoint quiz (2 questions) — open this page in a browser to take it.