
SSE-S3, SSE-KMS and SSE-C differ in who manages the encryption key. With SSE-S3, Amazon S3 manages it, and with SSE-KMS, AWS KMS manages it and logs every use. With SSE-C, you send the key with every request and S3 never stores it.
TL;DR:
Updated Oct 29, 2026: new sections on checking which encryption an object uses and re-encrypting existing objects, plus KMS key rotation and cost, an HTTPS-only bucket policy, and a labelled diagram of who holds the key.
| SSE-S3 | SSE-KMS | SSE-C | |
|---|---|---|---|
| Who holds the key | Amazon S3 | AWS KMS (AWS managed or customer managed key) | You |
| Setup | None, it's the default | A KMS key plus KMS permissions | Three headers on every request, HTTPS only |
| Audit trail of key use | None to inspect | KMS calls recorded in CloudTrail | Yours to keep |
| Extra cost | None | AWS KMS charges | Your own key management |
| Main risk | Least control | Access Denied when KMS permission is missing | Lose the key, lose the object |
AWS also offers DSSE-KMS, a dual-layer variant of SSE-KMS. All four options are "mutually exclusive," and AWS states that "you can't apply different types of server-side encryption to the same object simultaneously."
SSE-S3 is what you get if you do nothing. AWS states that it "is the default encryption configuration for every bucket in Amazon S3." Since January 5, 2023, "all new object uploads to Amazon S3 are automatically encrypted at no additional cost and with no impact on performance." Each object gets a unique AES-256 key, which is itself encrypted with "a root key that it regularly rotates," so you never see or handle a key yourself.
SSE-KMS hands key management to AWS Key Management Service. In AWS's words, "with AWS KMS, you have more control over your keys. For example, you can view separate keys, edit control policies, and follow the keys in AWS CloudTrail." If you face an audit or compliance requirement, that trail of key use is the main reason to choose it.
SSE-KMS never sends your object to KMS. On upload, S3 asks KMS for a data key, and KMS returns it in plaintext plus a copy encrypted under your KMS key. S3 encrypts the object with the plaintext key, drops that key from memory, and stores the encrypted copy as object metadata. On download, S3 sends the encrypted data key to KMS in a Decrypt call and uses the result to decrypt the object.
So reading an SSE-KMS object needs two permissions to line up: S3 access to the object and KMS access to the key. AWS lists kms:GenerateDataKey for uploads, kms:Decrypt for downloads, and both for multipart uploads. A user with perfect S3 permissions but no KMS permission gets Access Denied, which surprises most teams the first time. Every one of those KMS calls is also what CloudTrail records.
If you don't name a key, S3 creates an AWS managed key (aws/s3) in your account the first time you store an SSE-KMS object in a bucket. A customer managed key gives "more flexibility and control," because you can create, rotate and disable it, set its policy, and audit it. You need one for sharing, since AWS states that objects encrypted with AWS managed keys can't be shared across accounts. AWS rotates AWS managed keys automatically about every 365 days, and you can't turn that off. For a customer managed key, you turn on automatic rotation yourself, with a default period of 365 days that you can change, or you rotate it on demand. When you name a customer managed key, use its full ARN, because an alias resolves in the requester's account. The key must sit in the same Region as the bucket, and S3 supports only symmetric KMS keys.
Plain SSE-KMS calls KMS for each object, which adds up on busy buckets. Each KMS key you create costs $1 a month, and AWS's own pricing example (one key, 10,000 uploads and 2,000,000 reads a month) comes to $6.97, mostly request charges at $0.03 per 10,000 requests. An S3 Bucket Key "can reduce your AWS KMS request costs by up to 99 percent," per AWS, because S3 derives per-object data keys from one bucket-level key for a limited time. With a Bucket Key on, CloudTrail's encryption context shows the bucket ARN instead of each object ARN. A bucket that receives S3 server access logs must use SSE-S3. Otherwise logs can arrive encrypted with a key you can't use, as covered in how S3 access logs work.

Use SSE-C only when nobody but you may hold the key and you can manage that key yourself. AWS describes the mechanism: "with the encryption key that you provide as part of your request, Amazon S3 manages data encryption as it writes to disks and data decryption when you access your objects." The part that matters most comes next: "S3 never stores the encryption key when you use SSE-C," so you supply it every time anyone downloads the object. S3 keeps only a salted HMAC of the key to validate later requests, and that value can't decrypt anything.
The costs all follow from that design. AWS warns that "if you lose the encryption key, you lose the object," and that sharing the key with "other users, roles, or AWS services" is difficult. Day to day, you also live with these limits:
Those costs are why AWS changed the default. "Starting April 2026, SSE-C is disabled by default for all new general purpose buckets," because "most modern workloads use" SSE-S3 or SSE-KMS instead, per AWS. Existing buckets in accounts with no SSE-C objects were switched off too. While SSE-C is blocked, any upload, copy or replication request that specifies it fails with HTTP 403 AccessDenied. To allow it, you set BlockedEncryptionTypes to NONE in the bucket's default encryption configuration. That only permits SSE-C uploads: PutBucketEncryption can't make SSE-C the bucket default, because the key has to arrive with every request.
The three types look different on the wire, which is where many real errors start:
| Type | How you ask for it |
|---|---|
| SSE-S3 | Nothing, it's the default. To be explicit, send the x-amz-server-side-encryption header. |
| SSE-KMS | x-amz-server-side-encryption: aws:kms, optionally with x-amz-server-side-encryption-aws-kms-key-id naming a specific key |
| SSE-C | Three headers on every request: x-amz-server-side-encryption-customer-algorithm (must be AES256), -customer-key (the 256-bit key, base64-encoded), and -customer-key-MD5 (the base64-encoded MD5 digest of the key, used as an integrity check) |
Two rules trip people up when they first mix encryption types. For SSE-KMS objects, don't send encryption headers on GET or HEAD, because AWS returns HTTP 400 Bad Request; S3 already knows which key to ask KMS for. For SSE-C it's the opposite, and GET, HEAD and copy requests must all carry the key, with a second set of x-amz-copy-source-server-side-encryption-customer-* headers when the copy source is SSE-C.
Both KMS and SSE-C requests must use TLS. S3 rejects SSE-C requests over plain HTTP, and AWS recommends treating "any key that you erroneously send over HTTP" as compromised and rotating it.
You check an object with a HEAD request. The x-amz-server-side-encryption response header names the type: AES256 for SSE-S3, aws:kms for SSE-KMS and aws:kms:dsse for DSSE-KMS. For SSE-KMS, x-amz-server-side-encryption-aws-kms-key-id names the key, and x-amz-server-side-encryption-bucket-key-enabled shows whether a Bucket Key was used. SSE-C objects return the customer algorithm and key MD5 headers instead, and your HEAD request has to carry the key.
aws s3api head-object --bucket my-bucket --key report.pdf
aws s3api get-bucket-encryption --bucket my-bucketThe CLI prints those headers as JSON fields such as ServerSideEncryption, SSEKMSKeyId and BucketKeyEnabled. The second command shows the bucket's default encryption, which tells you how new uploads will be encrypted, not how existing objects were.
Changing the bucket default doesn't touch objects that are already stored, because AWS applies the default to new objects. To move an existing object to another type or key, copy it over itself and name the encryption you want. AWS says the encryption in a copy request takes precedence over the bucket default:
# not run here: rewrite one object in place under a customer managed KMS key
aws s3 cp s3://my-bucket/report.pdf s3://my-bucket/report.pdf \
--sse aws:kms \
--sse-kms-key-id arn:aws:kms:us-east-1:111122223333:key/EXAMPLE-KEY-IDFor a whole bucket, AWS points to the S3 Batch Operations Copy operation, which can rewrite billions of objects as encrypted copies in one job. Check two side effects before you start. A copy resets system metadata such as the creation date, and AWS notes that copied objects don't keep the original Object Lock settings. In a versioned bucket, the copy also becomes a new version, so older versions keep their old encryption until you delete them (see how S3 object versioning works).
Rather than trusting every uploader to send the right headers, you can reject uploads that don't. AWS's example denies s3:PutObject when the KMS key header is missing:
{
"Version": "2012-10-17",
"Id": "PutObjectPolicy",
"Statement": [
{
"Sid": "DenyObjectsThatAreNotSSEKMS",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"Null": {
"s3:x-amz-server-side-encryption-aws-kms-key-id": "true"
}
}
}
]
}The Null condition matches when the header is absent, so the deny fires on any upload without a KMS key. To require one particular key, compare the full key ARN with the same condition key, keeping in mind that IAM doesn't check the ARN points to a real key. For SSE-C, use the s3:x-amz-server-side-encryption-customer-algorithm condition key, and send the header on CreateMultipartUpload, UploadPart and CompleteMultipartUpload too.
Encryption at rest says nothing about the network path. AWS recommends allowing only HTTPS by denying any request where aws:SecureTransport is false:
{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": ["arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket/*"],
"Condition": { "Bool": { "aws:SecureTransport": "false" } }
}This statement goes into the same Statement array as the KMS rule above.
AIOZ Storage doesn't offer the three AWS modes by name. Our FAQ states that "AIOZ Storage uses server-side encryption, and if a user wants end-to-end encryption, they can build a self-hosted gateway." That gateway is announced but not released: its GitHub repo holds only a README, with no release to download (checked Oct 29, 2026). On the server side, each uploaded object is split into segments, and every segment is encrypted with AES-GCM under its own random key. Those keys are stored only in encrypted form, wrapped by a key derived from a passphrase you create. When you create a bucket, you confirm that "AIOZ doesn't have or save encryption passphrases. Without the passphrase, your files can't be accessed."
Here is how that model lines up with the three AWS options and with client-side encryption:
| AWS option | Closest AIOZ Storage equivalent |
|---|---|
| SSE-S3 (provider encrypts by default) | Default server-side encryption, with keys derived from your passphrase |
| SSE-KMS (managed keys with an audit trail) | No KMS integration documented |
| SSE-C (you hold the key) | No per-request key headers documented; you hold the passphrase, and losing it means losing access |
| Client-side encryption | End-to-end mode through a self-hosted gateway is documented but not released, so for now you encrypt files yourself before upload |
For the S3 API, you send an access grant to AIOZ's registry service, which creates S3 credentials and encrypts the grant with the S3 Access ID. If you need data encrypted before it leaves your machine, you have to do that yourself for now. The docs' CLI page covers the AWS CLI against the hosted endpoint, which uses server-side encryption, and the gateway commands on the docs' gateway page (gw setup, then gw server) belong to a tool you can't download yet.
What the docs don't cover yet:
x-amz-server-side-encryption or SSE-C headers.For the full scheme, read How Encryption Is Implemented in the AIOZ Storage docs.
Which encryption type does S3 use by default?
SSE-S3, which has encrypted every new upload automatically and at no extra cost since January 5, 2023.
What is the difference between SSE-C and SSE-KMS?
With SSE-KMS, AWS KMS holds the key and logs its use, while with SSE-C you hold the key, send it on every request, and lose the object if you lose the key.
Why do I get Access Denied on an SSE-KMS object when my S3 permissions are correct?
You also need KMS permission on the key: kms:Decrypt to read, kms:GenerateDataKey to write, and both for multipart uploads.
Can I share SSE-KMS objects with another AWS account?
Only with a customer managed key whose policy allows that account, because objects under AWS managed keys can't be shared across accounts.
Why did AWS disable SSE-C by default in 2026?
Because sending the key with every request makes sharing access with other users, roles and AWS services impractical, so AWS says most workloads use SSE-S3 or SSE-KMS instead.
Can I make SSE-C the default encryption for a bucket?
No, PutBucketEncryption can only allow or block SSE-C uploads through BlockedEncryptionTypes, since the key must arrive with each request.
Does AIOZ Storage use SSE-S3, SSE-KMS or SSE-C?
None of them by name. It uses its own server-side encryption with keys derived from a passphrase it doesn't store. The docs also describe end-to-end encryption through a self-hosted gateway, but that gateway hasn't been released.

Distributed storage keeps data on many independent nodes that act as one system. See how it works, replication vs erasure coding, its types and trade-offs.

Egress fees are charges for data leaving a storage provider's network. See what triggers them, how to estimate them, and what AIOZ Storage's docs say.

An object storage vector database keeps vectors in the storage layer, not a cluster. See how it differs, when to pick it, and where AIOZ Storage fits in.

AIOZ Storage vector database pricing line by line: upload, storage, queries, data searched and data returned, with a worked monthly example you can reuse.

Create a bucket and upload a file on AIOZ Storage in five steps: sign up, set a passphrase, add credits, and upload. Plus what the passphrase cannot recover.

The AIOZ Storage rate limit is 100 requests per second. See what the docs do and don't say, and how to stay under it with boto3 retries and a limiter.