Back to posts
Jun 16, 2026
14 min read

Amazon S3 Security: Encryption, Object Lock, Access Points, and What to Remember for the SAA Exam

You’ve just moved a system that stores medical records and financial documents onto Amazon S3. Everything runs smoothly — until the compliance team knocks on your door with a checklist that makes you sweat:

  • Is the data in the bucket encrypted? Who holds the key — AWS, or you?
  • If an employee (or an attacker with valid credentials) tries to delete a record, can we block it? Can we prove to an auditor that the records cannot be altered for 7 years?
  • Who accessed which object, and when?
  • How do we let a partner download exactly one file within 10 minutes, without making the bucket public?
  • Five different teams need access to five different folders in the same bucket — how do we manage permissions without chaos?
  • When the analytics team reads the data, how do we automatically redact ID numbers and credit card numbers without creating a separate processed copy?

Here’s the interesting part: each question above maps to exactly one S3 security feature. S3 isn’t just a “cheap file store” — it’s a set of defense layers, each one born to answer a specific question. This post organizes all of those features into four memorable layers, explains what problem each one solves, when to use it, and pins down the points that tend to show up on the SAA exam.


1. The big picture: four defense layers

Instead of memorizing 13 scattered features, tie each one to the security question it answers. They all group into four layers:

Question compliance asksDefense layerS3 features
”Is the data encrypted? Who holds the key?”EncryptionSSE-S3, SSE-KMS, SSE-C, DSSE-KMS, Client-Side, TLS in-transit
”Can we prevent tampering/deletion?”Integrity & RetentionVersioning, S3 Object Lock, Glacier Vault Lock, MFA Delete
”Who can do what, and how do we grant it?”Access ControlBucket Policy/IAM, Pre-signed URLs, Access Points, CORS, Object Lambda
”Who accessed what, and when?”ObservabilityS3 Server Access Logs, CloudTrail

Two ideas run through the whole post — pin them down first:

  • Encryption and access control are two different things. Encryption answers “if the data is taken, can the thief read it”, access control answers “who is allowed to touch the data”. A perfectly encrypted bucket can still leak if the policy is wide open, and vice versa.
  • Versioning is the foundation. The two most important anti-deletion features — Object Lock and MFA Delete — both require Versioning to be enabled first. Versioning means S3 retains every version of an object, so “delete” doesn’t mean “lost”.

2. Encryption at rest: four ways to hold the key

This is the densest part of Section 14, and the one most likely to appear on the exam. “Encryption at rest” means the data is encrypted while it sits on disk, so that even if someone gets hold of the physical hardware, they can’t read the contents. The core question that distinguishes the options isn’t “is it encrypted or not”, but: where does the encryption happen, and who holds the key.

2.1. SSE-S3 — let AWS handle everything

SSE-S3 is the default and simplest form of encryption, fully managed by AWS. It works the same way as SSE-KMS, the only difference being that the master key is now managed by S3 rather than KMS — so it never touches KMS and therefore never runs into throttling problems.

Each object is encrypted with a different data key, derived from the master key.

Enable it via the header x-amz-server-side-encryption: AES256.

2.2. SSE-KMS — key control and audit

SSE-KMS still lets S3 do the encrypting, but the key lives in AWS KMS — AWS’s key management service. The big difference from SSE-S3 comes down to one word: control.

You create the key yourself, set a policy for who can use it, enable key rotation, and most importantly, every access to the key is recorded in CloudTrail.

Every time you upload an object, S3 calls the KMS API GenerateDataKey; every time you download, S3 calls Decrypt. These calls count against the KMS quota per region (a few thousand requests per second by default). For very high-traffic buckets, you can hit throttling (KMS rejecting excess requests once the quota is exceeded).

To solve the throttling problem when using SSE-KMS, S3 Bucket Keys were introduced — S3 asks KMS to create a key for the bucket (called a bucket key), and the bucket uses that bucket key to generate a data key for each object instead of calling KMS every time, cutting KMS calls by roughly 99% (reducing both cost and the risk of throttling).

A bucket key is not a master key. It is really just an intermediate key layer inserted into the chain, turning envelope encryption from two tiers into three. The master key stays safe inside the KMS service.

The downside of S3 Bucket Keys is that the blast radius (the extent of damage if a key is exposed) of a bucket key is slightly wider than a data key’s: if one bucket key is exposed, all objects within the time window it served are affected, rather than just one object.

The details of how KMS generates keys, envelope encryption, and the key types live in the AWS Security & Encryption post — this article focuses only on how S3 puts them to use.

2.3. SSE-C — you bring the key, S3 encrypts for you

SSE-C is for the case where you want to fully manage the key outside AWS, but still let S3 do the encryption work.

How it works: you attach the key in the headers of every request. S3 uses that key to encrypt (or decrypt) the object, then discards the key immediately — AWS never stores your key.

  • The problem it solves: “I’m required to keep the key outside AWS, but I don’t want to write encryption code myself.”
  • Use case: compliance requirements that the key must not reside in the cloud provider’s infrastructure.

Because the key travels over the network in every request, HTTPS is mandatory. Lose the key = lose the data, because AWS keeps no copy.

2.4. DSSE-KMS — two-layer encryption for strict compliance

DSSE-KMS is like SSE-KMS but applies two layers of encryption stacked on top of each other, both using KMS keys. It was created to meet very strict compliance standards (for example, certain US government/defense requirements) that demand multi-layer encryption.

  • The problem it solves: “regulations require the data to be encrypted with two independent layers.”
  • Use case: organizations under a specific compliance framework that requires dual-layer encryption.

2.5. Client-Side Encryption — the server never sees plaintext

With client-side encryption, you encrypt the data before sending it to S3 and hold the key yourself. S3 only ever receives and stores ciphertext — it never sees the original content or the key. On download, the client itself decrypts.

  • The problem it solves: “I don’t trust handing the encryption work to a server, not even AWS.”
  • Use case: extremely sensitive data, a zero-trust model toward the cloud provider.

Because the key travels over the network in every request, HTTPS is mandatory. Lose the key = lose the data, because AWS keeps no copy.

2.6. Encryption in transit (encryption in flight)

The four sections above are about data at rest. What about data in motion between the client and S3? That’s encryption in transit, or SSL/TLS. S3 offers both an HTTP endpoint and an HTTPS endpoint.

To enforce HTTPS only, you add a condition to the bucket policy that denies the request when aws:SecureTransport is false.

As noted, SSE-C mandates HTTPS already.


3. Default Encryption & enforcing encryption

Knowing the encryption types is one thing, ensuring no object slips through unencrypted is another. This is where two supporting mechanisms come in.

3.1. Default Encryption

Since the start of 2023, AWS enables default encryption on every bucket: every new object is automatically encrypted with SSE-S3, at no extra cost and with no configuration. You can change the bucket default to SSE-KMS if you want key control.

Default Encryption ensures the object is encrypted, but it does not prevent someone from deliberately uploading with a different encryption type. If you need to enforce a specific type, use a bucket policy.

3.2. Enforcing encryption with a bucket policy

To force every upload to use exactly one encryption type (for example, accept only SSE-KMS), you write a bucket policy that denies the s3:PutObject action if the desired encryption header is missing or wrong.

The policy below denies any s3:PutObject that isn’t encrypted with SSE-KMS. The condition keys off s3:x-amz-server-side-encryption — the header the client uses to declare its encryption type on upload:

{ "Version": "2012-10-17", "Statement": [ { "Sid": "DenyNonKmsUploads", "Effect": "Deny", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::my-bucket/*", "Condition": { "StringNotEquals": { "s3:x-amz-server-side-encryption": "aws:kms" } } } ] }

StringNotEquals is true whenever the header isn’t aws:kms — including when the header is absent entirely — so this one statement blocks both wrong-type uploads and uploads that declare no encryption at all. To pin it down to a specific key, add a condition on the s3:x-amz-server-side-encryption-aws-kms-key-id header with that key’s ARN.

The bucket policy is checked before Default Encryption applies. That means the policy is the “gatekeeper” — it can stop an invalid request right at the door, while Default Encryption is just a safety net that applies encryption to requests that were already allowed through.


4. Integrity & anti-deletion: Object Lock, Glacier Vault Lock, MFA Delete

Encryption protects confidentiality. This group of features protects integrity — preventing data from being modified or deleted, whether by accident, by an insider, or by ransomware. The central concept is WORM: write once, and no one can modify/delete it afterward.

4.1. S3 Object Lock

Requires: Versioning enabled

Object Lock applies the WORM model to each object version: it locks an object version against deletion for a defined period of time.

There are three concepts to keep distinct:

  • Retention mode — there are two:
    • Governance mode: most users can’t delete/overwrite, but a few with a special permission (s3:BypassGovernanceRetention) can still override when needed. Suitable when you want strong protection but keep an “escape hatch” for admins.
    • Compliance mode: no one can delete/overwrite, not even the root account. Once set, the retention period can’t be shortened and the mode can’t be changed. Use it when regulations allow no exceptions whatsoever.
  • Retention Period: protects the object for a fixed period of time, it can be extended.
  • Legal Hold: protects the object indefinitely, completely independent of the retention period, freely turned on/off with the s3:PutObjectLegalHold permission. Think of it as a “freeze order” in a lawsuit: keep it as long as needed, remove it when done.

4.2. Glacier Vault Lock

While Object Lock protects objects in S3, Glacier Vault Lock protects data in the Amazon S3 Glacier cold-storage archive. It also follows the WORM model, but its defining feature is a lock policy — once locked, that policy becomes immutable.

4.3. MFA Delete

Requires: Versioning enabled

MFA Delete forces users to enter an MFA code for the most dangerous operations on a bucket. Only the bucket owner (root account) can enable/disable MFA Delete.

Specifically, MFA is required to:

  • Permanently delete an object version.
  • Suspend versioning on the bucket.

Conversely, MFA is not needed to enable Versioning or to list deleted versions.


5. Observability & temporary access

5.1. S3 Server Access Logs

To answer “who accessed which object, and when”, you enable S3 Server Access Logging. It records every request to the bucket (including denied requests, and requests from other accounts), then delivers the logs to a different S3 bucket in the same region for later analysis (for example, querying with Athena).

Don’t make the log bucket the same as the bucket being monitored. Doing so creates a loop: writing a log generates a request, which generates another log… and the bucket grows exponentially.

5.2. Pre-signed URLs

A Pre-signed URL is a URL you sign that lets whoever holds it access a specific object for a limited time — inheriting exactly the permissions of the signer. This way you grant upload/download access without making the bucket public.

  • Use case: allow downloads of premium content only to logged-in users, a constantly changing list of who can download, let a user upload directly to an exact location in the bucket.
  • Remember: the URL is time-limited (CLI default is one hour, configurable). The bucket stays fully private.

This feature has its own deep-dive — covering the upload flow with presigned POST, downloads, and CloudFront Signed Cookies integration for images: S3 Presigned URL.


6. Management at scale & data transformation

6.1. S3 Access Points

Picture a shared data bucket that five teams use: finance reads/writes the finance/ folder, sales reads sales/ only, partners read shared/ only… If you cram all those rules into one bucket policy, it balloons into a giant JSON blob that’s hard to read and easy to get wrong. With just three teams, the company-data-lake bucket policy already looks like this:

{ "Version": "2012-10-17", "Statement": [ { "Sid": "FinanceReadWrite", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::111122223333:role/finance-app" }, "Action": ["s3:GetObject", "s3:PutObject"], "Resource": "arn:aws:s3:::company-data-lake/finance/*" }, { "Sid": "FinanceList", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::111122223333:role/finance-app" }, "Action": "s3:ListBucket", "Resource": "arn:aws:s3:::company-data-lake", "Condition": { "StringLike": { "s3:prefix": "finance/*" } } }, { "Sid": "SalesReadOnly", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::111122223333:role/sales-app" }, "Action": "s3:GetObject", "Resource": "arn:aws:s3:::company-data-lake/sales/*" }, { "Sid": "PartnerReadShared", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::444455556666:role/partner-app" }, "Action": "s3:GetObject", "Resource": "arn:aws:s3:::company-data-lake/shared/*" } ] }

Four statements for just three teams. Add analytics, ml-training, logs… each team drags in a few more statements, all piled into a single document. A bucket policy also has a 20 KB size limit, so you will eventually hit the ceiling — and worse, every time you edit one line for one team you risk breaking permissions for every other team.

S3 Access Points solve this by splitting access into multiple “front doors”. Each access point has its own DNS name and its own access point policy (like a bucket policy but scoped to just that access point). You grant each team an access point with a tidy policy, instead of one complex central policy.

Concretely: the bucket keeps a single statement that delegates control to every access point owned by the account — written once and never growing, because the s3:DataAccessPointAccount condition filters by account ID rather than listing each team:

{ "Version": "2012-10-17", "Statement": [ { "Sid": "DelegateToAccessPoints", "Effect": "Allow", "Principal": "*", "Action": "*", "Resource": ["arn:aws:s3:::company-data-lake", "arn:aws:s3:::company-data-lake/*"], "Condition": { "StringEquals": { "s3:DataAccessPointAccount": "111122223333" } } } ] }

Each team’s real permissions live in its own access point policy. For example, the finance-ap access point for the finance team — exactly one statement, with no entanglement with any other team:

{ "Version": "2012-10-17", "Statement": [ { "Sid": "FinanceTeamAccess", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::111122223333:role/finance-app" }, "Action": ["s3:GetObject", "s3:PutObject"], "Resource": "arn:aws:s3:ap-southeast-1:111122223333:accesspoint/finance-ap/object/finance/*" } ] }

An object’s ARN when accessed through an access point takes the form .../accesspoint/<ap-name>/object/<prefix>, distinct from the direct bucket ARN. The finance team calls through the finance-ap DNS name without needing to know the underlying bucket name. Adding a new team is just creating another access point with its own small, independent policy, instead of inserting another line into the central JSON blob.

  • VPC Origin: an access point can have its origin set to either the Internet or a VPC. If you choose VPC Origin, the access point is reachable only from within the VPC, and you must create a VPC Endpoint (Gateway or Interface) to reach it — the VPC endpoint policy must allow access to both the bucket and the access point.

Access Points are for scaling permission management (many teams/prefixes). Each Access Point = one policy + one DNS.

6.2. S3 Object Lambda

Sometimes different applications need different versions of the same object: the analytics team needs a PII-redacted copy, a legacy app needs XML instead of JSON, the website needs a resized image. The old way is to create and store multiple processed copies — costly and hard to keep in sync.

S3 Object Lambda lets you insert an AWS Lambda function in the middle to transform the object at the moment it’s read (GET), without storing copies. The required architecture: one bucket, one supporting S3 Access Point, and one S3 Object Lambda Access Point. The application calls the Object Lambda Access Point → it invokes the Lambda → the Lambda reads the original object through the supporting access point, transforms it, then returns the result to the caller.

  • Use case: redacting sensitive information like PII for analytics environments, converting formats (XML → JSON), resizing/watermarking images on the fly, enriching data from another source.
  • Remember: only one bucket, the key point is transforming on read, using the Access Point + Object Lambda Access Point pair.

6.3. S3 CORS

CORS is a browser security mechanism, not an S3-specific one. An origin is defined by scheme + host + port (e.g. https://www.example.com). By default, a browser blocks JavaScript from one origin from calling resources on another origin — unless the target origin allows it via CORS headers.

How it works: before the real request, the browser sends a “probe” request (a preflight) to S3, if the bucket’s CORS configuration allows that origin, the browser then sends the real request.

  • Classic exam use case: a static website hosted on bucket A loads files (images, fonts, data) from bucket B → you must enable the CORS configuration on bucket B (the target bucket), allowing bucket A’s origin.
  • Remember: see “browser”, “cross-origin”, “static website calling another bucket” → CORS, and the configuration goes on the bucket being called.

Related