Amazon S3 encryption at rest is not one feature but a spectrum, and the axis that organizes the whole spectrum is a single question: who holds the key? At one end, AWS holds everything and encryption is a checkbox you barely notice; at the other end, you hold everything, AWS never sees your plaintext, and you own every hard problem of key management. Between those poles sit four concrete options — SSE-S3, SSE-KMS, SSE-C, and client-side encryption — and choosing among them is really choosing how much custody, audit, and operational burden you are willing to take on in exchange for control. This piece walks the whole model: what at-rest encryption actually protects against and what it does not, how each option assigns key custody and who performs the cryptography, the envelope-encryption machinery underneath KMS and client-side, the cost and throttling levers like S3 Bucket Keys, how to enforce encryption so nobody can opt out, and — with an interactive lab — a framework for landing on the right option for your threat model.

Why encrypt at rest, and what it does not protect

Encryption at rest means the bytes S3 writes to disk are ciphertext, not your plaintext. The immediate driver is usually compliance — PCI-DSS, HIPAA, GDPR, SOC 2, and most internal security standards expect data at rest to be encrypted, and ‘encrypted with a documented key-management process’ is table stakes for an audit. But compliance is only the paperwork reason. The real reason is defense in depth: if a disk is decommissioned improperly, if a backup snapshot leaks, or if raw storage is somehow exfiltrated, the attacker gets ciphertext they cannot read without the key.

It is just as important to be honest about what at-rest encryption does not do. It does not protect against a caller who has valid IAM permissions: if a principal can call GetObject, S3 decrypts transparently and hands them plaintext — the encryption is invisible to them. It does not stop a leaked credential, an overly broad bucket policy, or a public-access misconfiguration. And it is not a substitute for encryption in transit, which is a separate control. At-rest encryption defends the storage layer and the physical media; it does nothing about the access-control layer above it. That distinction — storage protection versus access control — is the single most common misunderstanding in this whole topic, and it frames every option below.

Advertisement

The four options at a glance, by key custody

Every S3 encryption decision is a decision about custody: who holds the key that can turn your ciphertext back into plaintext, and who runs the cipher. Laid out on that axis, the four options form a clean progression from ‘AWS holds everything’ to ‘you hold everything.’

OptionWho holds the keyWho does the cryptoCost
SSE-S3AWS (S3-managed)AWS / S3Free
SSE-KMSAWS KMS, your CMK & policyAWS / S3 + KMSKMS key + per-request
SSE-CYou (sent on each request)AWS / S3Free (you run key mgmt)
Client-SideYou (never leaves you)Your clientFree (you run everything)

Read the table top to bottom and the trade is unmistakable. As you descend, AWS does less for you and you gain more control: SSE-S3 is a checkbox, SSE-KMS adds an audit trail and access policies at a small cost, SSE-C hands you key custody while AWS still runs the cipher, and client-side moves both the key and the cryptography entirely into your world so AWS never sees your plaintext at all. Notice that the cipher — AES-256 — is effectively the same throughout; what changes is not the math but the custody model. The lab near the end of this article is built directly on this table, so keep it in mind.

SSE-S3: AES-256, fully AWS-managed, free, now the default

SSE-S3 (server-side encryption with Amazon S3-managed keys) is the simplest option and the one you get for free. S3 manages the entire key hierarchy for you: it encrypts each object with a unique data key using AES-256, and it rotates and protects the underlying keys with no action on your part. There is nothing to configure beyond turning it on — no key to create, no policy to write, no per-request charge. As of early 2023, SSE-S3 is applied automatically as the default for every new bucket, so unless you deliberately choose otherwise, your objects are already encrypted at rest.

The catch of SSE-S3 is precisely its simplicity: because AWS manages the keys entirely, you get no key policies, no per-object access control tied to a key, and no dedicated audit trail of who decrypted what. You cannot say ‘this team may use this key but not that one,’ because from your side there is no key to point a policy at. For a large fraction of workloads that is completely fine — the data is encrypted at rest, the compliance checkbox is ticked, and the cost is zero. Reach past SSE-S3 only when you have a concrete requirement it cannot meet: an audit trail of key usage, granular key-level access control, key material you must supply yourself, or a mandate that AWS never see your plaintext. If none of those apply, SSE-S3 is the correct, boring, default answer.

SSE-KMS: customer master keys, audit trail, and policies

SSE-KMS keeps the server-side model — AWS still encrypts and decrypts your objects — but moves the key into AWS Key Management Service, where you control it. Instead of an opaque S3-managed key, your objects are protected under a KMS customer master key (a KMS key) that you create, name, and govern. That single change unlocks the three things SSE-S3 cannot give you. First, a full CloudTrail audit trail: every Encrypt, Decrypt, and GenerateDataKey call against the key is logged, so you can answer ‘who decrypted this object, and when?’ Second, key policies and grants: you attach IAM-style policy to the key itself, so access to the data now requires access to the key as a second, independent gate. Third, controlled rotation of key material on your schedule.

That power is not free. Every object read or write that touches a KMS key makes a KMS API call, which carries a per-request cost and, more importantly, counts against a per-account, per-Region KMS request-rate limit. A high-throughput workload — think a data lake serving thousands of objects per second — can hit KMS throttling and see ThrottlingException errors bubble up as slow or failed S3 operations. That failure mode is real and surprising the first time it happens, and it is exactly the problem S3 Bucket Keys (below) exist to solve. SSE-KMS is the right default the moment ‘we need to prove who accessed this’ or ‘access to the data must be gated by a key policy’ appears in your requirements.

Envelope encryption: the model under KMS and client-side

To understand SSE-KMS costs, bucket keys, and client-side encryption, you have to understand envelope encryption — the pattern that underlies all of them. The problem it solves is that master keys are precious and slow to use directly: you do not want to ship gigabytes of object data to KMS to be encrypted, and you do not want your single master key doing bulk crypto. So instead of encrypting data with the master key, you encrypt data with a freshly-generated data key, and then encrypt that data key with the master key.

The flow is: request a new data key from KMS; KMS returns it in two forms — a plaintext data key and an encrypted (wrapped) copy of it. You use the plaintext data key to encrypt the object locally with fast symmetric AES-256, then you throw the plaintext data key away and store the wrapped data key alongside the ciphertext. To decrypt later, you send the wrapped data key back to KMS, KMS unwraps it with the master key and returns the plaintext data key, and you decrypt the object. The master key never leaves KMS and never touches your bulk data; only tiny data keys cross the wire. This is why SSE-KMS makes a KMS call per operation (it is wrapping or unwrapping a data key), why S3 Bucket Keys can cut those calls by reusing a data key, and why client-side encryption with the AWS Encryption SDK works the same way on your side of the wire. One pattern, three features.

S3 Bucket Keys: cutting KMS cost and throttling

S3 Bucket Keys are the direct fix for SSE-KMS’s per-request cost and throttling. Recall that vanilla SSE-KMS calls KMS on essentially every object operation to wrap or unwrap a data key. At scale those calls dominate your KMS bill and, worse, can blow through the account’s KMS request-rate limit and throttle your S3 traffic. A Bucket Key breaks that one-call-per-object coupling.

With a Bucket Key enabled, S3 asks KMS for a short-lived, bucket-level data key and then uses that key to derive per-object keys for many objects over a time window, entirely inside S3, without calling back to KMS each time. The result is a dramatic reduction in KMS requests — AWS documents reductions on the order of up to 99% for busy buckets — which cuts both the per-request KMS cost and the throttling pressure at the same time. You still get SSE-KMS’s core benefits: your CMK, your key policy, and an audit trail (though the CloudTrail events are now at the bucket-key level rather than one per object, which is usually a feature, not a loss). Enabling a Bucket Key is a one-line property on the bucket or the request. In practice, if you are using SSE-KMS on any high-throughput bucket, a Bucket Key should be your default — it is nearly all upside for exactly the workloads where KMS throttling would otherwise bite.

SSE-C: you supply the key on every request

SSE-C (server-side encryption with customer-provided keys) is the option for teams that must hold the key material themselves but still want AWS to run the cipher. The deal is precise: you generate and hold the AES-256 key; AWS uses it to encrypt on write and decrypt on read; and AWS stores no copy of the key — it keeps only a salted HMAC to verify that you supplied the right key on future requests. You send the key with every single request, over TLS, in a set of headers:

aws s3api put-object \
  --bucket my-bucket --key report.pdf --body report.pdf \
  --sse-customer-algorithm AES256 \
  --sse-customer-key $BASE64_KEY \
  --sse-customer-key-md5 $KEY_MD5

# GET must present the exact same key, or S3 returns 403
aws s3api get-object \
  --bucket my-bucket --key report.pdf out.pdf \
  --sse-customer-algorithm AES256 \
  --sse-customer-key $BASE64_KEY \
  --sse-customer-key-md5 $KEY_MD5

The responsibility that comes with SSE-C is total and unforgiving. Because AWS keeps no copy of your key, if you lose the key, the object is permanently unrecoverable — there is no reset, no recovery, no support ticket that helps. You own key storage, key rotation, key distribution to every caller, and the security of the channel. You also give up conveniences: because S3 needs the key to touch the bytes, features that operate on object content are constrained. SSE-C makes sense when a regulatory or contractual requirement forbids AWS from managing the key, yet you still want to avoid building a full client-side encryption pipeline. It is a narrow niche, and you should choose it deliberately, eyes open to the ‘lose the key, lose the data’ reality.

Client-side encryption: AWS never sees plaintext

Client-side encryption is the far end of the custody spectrum: you encrypt the data before it ever leaves your application, so only ciphertext travels to S3 and AWS never sees your plaintext or your keys at any point. The tooling for this is the AWS Encryption SDK (or the S3 encryption client), which implements the same envelope-encryption pattern described earlier — but entirely on your side of the wire. You configure a key provider (commonly a KMS key you own, or a raw key you manage), the SDK generates a data key, encrypts your object locally, and packages the ciphertext with the wrapped data key so you can decrypt later.

This is the strongest guarantee available: even a fully compromised AWS account, a subpoena served on AWS, or an insider at the cloud provider yields only ciphertext. That is why client-side encryption shows up in the most sensitive contexts — regulated financial data, health records, end-to-end-confidential workloads. The trade-off is equally sharp. Because S3 only ever holds opaque ciphertext, no server-side feature that needs to read object content will work: you cannot run S3 Select or Athena queries over the bytes, server-side content operations are off the table, and even simple things like content-type-aware handling are on you. You also own the entire key-management lifecycle and the correctness of your encryption code, where a subtle bug can silently weaken or destroy your protection. Client-side is the right call when ‘AWS must never be able to read this’ is a hard requirement — and overkill, with real costs, when it is not.

Advertisement

The key-custody spectrum: one axis, four points

Step back and every option collapses onto a single axis: how much of the key custody and the cryptography lives with AWS versus with you. That one line is the mental model that makes the whole topic easy to reason about, and it is exactly what the interactive lab draws.

Position on the axisOptionWhere your plaintext exists
AWS holds everythingSSE-S3Reaches S3 (AWS encrypts it there)
AWS holds the key, you hold the policySSE-KMSReaches S3 (AWS encrypts it there)
You hold the key, AWS runs the cipherSSE-CReaches S3 (AWS encrypts with your key, then forgets it)
You hold everythingClient-SideNever leaves your client

The crucial column is the last one: where your plaintext is allowed to exist. For the three server-side options, your plaintext bytes travel to S3 — over TLS, so never exposed on the wire — and AWS momentarily holds them in the clear to perform the encryption. Only with client-side encryption does your plaintext never reach AWS at all. Everything else is a question of who manages the key and whether you get an audit trail. If you internalize just one thing from this article, make it this axis: pick the leftmost point that still satisfies your real threat model, because every step rightward buys control at the price of convenience, features, and operational burden.

In transit vs at rest: two separate controls

A recurring confusion is conflating encryption in transit with encryption at rest. They are orthogonal controls that defend different things. At rest protects the stored bytes on S3’s disks — that is everything above. In transit protects the bytes as they move between your client and S3, and it is handled by TLS/HTTPS, not by any of the SSE options. You can have one without the other, and a serious posture needs both.

The important operational point is that encryption in transit is not automatically mandatory — S3 will happily accept a plain HTTP request unless you forbid it. You forbid it with a bucket policy condition on aws:SecureTransport, denying any request that did not arrive over TLS:

{
  "Sid": "DenyInsecureTransport",
  "Effect": "Deny",
  "Principal": "*",
  "Action": "s3:*",
  "Resource": [
    "arn:aws:s3:::my-bucket",
    "arn:aws:s3:::my-bucket/*"
  ],
  "Condition": { "Bool": { "aws:SecureTransport": "false" } }
}

With that in place, any non-HTTPS request is rejected outright, closing the in-transit gap regardless of which at-rest option you chose. Treat the two as a pair: SSE (or client-side) for at rest, a SecureTransport deny for in transit. Neither substitutes for the other.

Enforcing encryption so nobody can opt out

Choosing an encryption option is not the same as guaranteeing it. Because SSE-S3 is now the default for new buckets, fresh objects are encrypted even if a caller says nothing — but if your requirement is a specific option (say, SSE-KMS with a particular key), you must enforce it so no upload can slip through with weaker or no encryption. There are two layers, and mature setups use both.

The first layer is default bucket encryption: configure the bucket so that any object stored without an explicit encryption header is automatically encrypted with the option you chose (for example, SSE-KMS under a named key with a Bucket Key enabled). The second layer is a bucket policy that denies non-conforming PUTs outright, so a caller cannot deliberately request the wrong thing:

{
  "Sid": "DenyWrongEncryption",
  "Effect": "Deny",
  "Principal": "*",
  "Action": "s3:PutObject",
  "Resource": "arn:aws:s3:::my-bucket/*",
  "Condition": {
    "StringNotEquals": {
      "s3:x-amz-server-side-encryption": "aws:kms"
    }
  }
}

Together, default encryption sets the floor and the Deny policy welds the door shut: an upload that omits encryption gets encrypted anyway, and an upload that explicitly asks for the wrong scheme is rejected with an AccessDenied. That belt-and-suspenders pattern — a default plus an explicit Deny — is how you turn ‘we prefer encryption’ into ‘unencrypted objects are impossible here,’ which is the sentence auditors actually want to hear.

See it: the encryption chooser

The four options differ on exactly two axes: who holds the key and where your plaintext is allowed to exist. Reading that from a table is one thing; seeing it is another. The lab below asks you two questions — who must control the keys, and where encryption should happen — and then recommends SSE-S3, SSE-KMS, SSE-C, or Client-Side. For the option it picks, it draws who holds the key, where the encryption actually runs, and whether AWS ever sees your bytes in the clear. Toggle the answers and watch the diagram redraw.

The lesson the visualizer makes obvious is the custody spectrum. Slide toward ‘AWS is fine’ and your plaintext lands in S3 where AWS briefly holds it in the clear to encrypt it — simple, cheap, and fine for the vast majority of workloads. Slide toward ‘AWS must never see my plaintext’ and the padlock jumps to the client: nothing but ciphertext ever crosses the wire, but you inherit every hard problem of key management. There is no free lunch on that axis — every step you take toward control is a step away from convenience, and the right answer is wherever your threat model and compliance obligations actually sit.

Performance and cost in practice

The options differ far more in operational cost than in raw cryptographic performance — AES-256 is fast enough that the cipher itself is essentially never your bottleneck. What varies is the overhead of key management. SSE-S3 has none: no per-request charge, no key to pay for, no extra API in the path. Client-side and SSE-C also have no AWS-side per-request key charge, but they move the entire operational cost — storing, rotating, and distributing keys — onto you, which is a real cost even though it does not show on the AWS bill.

SSE-KMS is where cost and performance become an architecture concern. Every operation makes a KMS call, so you pay a small per-request KMS charge and, more consequentially, you consume the account’s per-Region KMS requests-per-second limit. On a high-throughput bucket — a data lake, a busy media pipeline, an analytics workload reading millions of small objects — that limit is a genuine ceiling, and hitting it surfaces as ThrottlingException and stalled S3 traffic. This is exactly why S3 Bucket Keys matter so much in practice: by collapsing many per-object KMS calls into occasional bucket-level ones, they remove both the cost spike and the throttling risk, often by an order of magnitude or more. The practical rule: SSE-S3 costs you nothing and needs no thought; SSE-KMS is worth its cost when you need audit and access control, but on any hot bucket you should pair it with a Bucket Key; and SSE-C or client-side trade a zero AWS-side charge for a nonzero engineering burden you must budget for.

A decision framework

Strip away the acronyms and the choice comes down to a short interrogation of your threat model, your compliance obligations, and your appetite for operational work. Answer these honestly and the right option usually names itself.

If your driving requirement is…Choose
‘Encrypt at rest’ with zero fuss and zero costSSE-S3 (the default)
An audit trail of key use and key-level access policiesSSE-KMS (+ Bucket Key if hot)
You must supply and hold the key, but AWS can run the cipherSSE-C
AWS must never be able to read your plaintext, everClient-Side
High throughput and SSE-KMS is throttling youKeep SSE-KMS, add an S3 Bucket Key

The meta-rule behind the table is the custody spectrum: pick the leftmost option — the one that leaves the most work with AWS — that still satisfies your actual requirements. Most workloads are genuinely well served by SSE-S3 or SSE-KMS; SSE-C and client-side are for the specific, usually regulatory, cases where key custody or ‘AWS must never see the plaintext’ is a hard line. Reaching for client-side encryption ‘to be safe’ when you have no such requirement buys you nothing but lost features, lost queryability, and a key-management burden that is itself a new risk. Match the option to the threat, enforce it with default encryption and a Deny policy so it cannot be bypassed, pair it with a SecureTransport deny for in transit, and you have an S3 encryption posture that is both correct and defensible.

S3 encryption at rest is a single axis of key custody with four points on it. SSE-S3 is the free, zero-effort default: AWS holds the key and runs AES-256 for you. SSE-KMS keeps the server-side model but gives you a CloudTrail audit trail and key policies via a customer master key — at a per-request cost and a throttling ceiling that S3 Bucket Keys are built to relieve. SSE-C hands you key custody while AWS still runs the cipher and stores no key — lose the key and the data is gone. Client-side encryption, via the AWS Encryption SDK, is the only option where AWS never sees your plaintext, bought at the price of server-side features and the full key-management burden. Remember that at-rest encryption protects the storage layer, not the access-control layer, and pair it with a SecureTransport deny for in transit. Enforce your choice with default bucket encryption plus a Deny-unencrypted-PUT policy, then pick the leftmost option that still meets your threat model — the lab makes that custody spectrum visible.