S3 Security and Encryption
Implement SSE-S3, SSE-KMS, and SSE-C encryption with access controls on S3.
Introduction
30 Second Summary
Every time you store a file in the cloud, you trust that no one else can read it, tamper with it, or share it without permission. In practice, that trust depends on layers of encryption, access policies, and audit trails working together.
In this project, you will build a multi-layered S3 security architecture that demonstrates three encryption methods side by side, enforces transport-layer security, locks objects against deletion, and proves temporary access control through presigned URLs.
What You'll Build
A comprehensive S3 security architecture covering every encryption, access control, and data protection concept tested on the AWS SAA-C03 exam.
By the end of this project, you'll have:
- S3 buckets with three encryption methods (SSE-S3, SSE-KMS, SSE-C) implemented and verified side by side, plus a customer-managed KMS key with rotation enabled and S3 Bucket Keys for cost optimization.
- A bucket with Versioning and Object Lock for WORM compliance, Block Public Access verified, Object Ownership set to ACLs disabled, and HTTPS-only enforced via bucket policy.
- Access logging flowing to a dedicated bucket, a scoped Access Point restricted to a specific prefix, and a tested presigned URL proving temporary access control.
- Secret Mission: Create a VPC-restricted Access Point and configure a bucket policy that requires all uploads to use SSE-KMS, combining network isolation with encryption enforcement.
Are there any prerequisites?
You need an AWS account with the AWS CLI already configured. This project also assumes familiarity with S3 bucket policies and basic S3 operations from a prior lab.
Create a Bucket with SSE-S3, Block Public Access, and Object Ownership
Every object stored in S3 is encrypted at rest. AWS offers multiple encryption methods, each with different levels of control and auditability.
In this step, you will create your first S3 bucket with the most common security defaults: SSE-S3 encryption, Block Public Access, and disabled ACLs. You will then upload a file and verify that encryption is applied automatically.
In this step, get ready to:
- Create an S3 bucket with SSE-S3 default encryption, Block Public Access, and Object Ownership configured.
- Upload a sample text file to the bucket.
- Verify that SSE-S3 encryption (AES-256) is applied automatically.
Create your S3 bucket with security defaults
S3 bucket names must be globally unique across all AWS accounts. You will create a bucket with a name that includes a unique suffix to avoid conflicts.
- In the AWS Management Console, use the search bar at the top to search for S3.
- Click on S3 to open the S3 console.
- Click Create bucket.
- For Bucket name, enter security-lab-sse-s3- followed by a unique suffix (for example, your initials and a random number like security-lab-sse-s3-jd-7291).
- Leave AWS Region set to your preferred region.
Now scroll down to review the security settings that are enabled by default.
- Under Object Ownership, confirm that ACLs disabled (recommended) is selected.
- Under Block Public Access settings for this bucket, confirm that Block all public access is checked.
- Scroll down to the Default encryption section. Confirm that Encryption type is set to Server-side encryption with Amazon S3 managed keys (SSE-S3).
- Click Create bucket at the bottom of the page.
What are these security defaults?
Block Public Access overrides any bucket policy or ACL that would grant public access. It acts as an account-level safety net, preventing accidental data exposure.
With ACLs disabled (Object Ownership set to Bucket owner enforced), all access is managed exclusively through IAM policies and bucket policies. This is the AWS-recommended approach for new buckets.
Seeing a bucket name error?
S3 bucket names must be globally unique. If you see a naming error, try a different suffix. Use only lowercase letters, numbers, and hyphens. The name must be between 3 and 63 characters long.
help me fix my S3 bucket name error
Upload a file and verify SSE-S3 encryption
With your bucket created, you will upload a sample text file and confirm that S3 automatically encrypts it with SSE-S3.
- On your computer, create a simple text file named sample.txt containing any text (for example, "Hello S3 Security Lab").
- Back in the S3 console, click on your newly created bucket name to open it.
- Click Upload.
- Click Add files, then select the sample.txt file from your computer.
- Click Upload at the bottom of the page.
- After the upload completes, click Close.
Now verify that SSE-S3 encryption was applied to your uploaded object.
- Click on the sample.txt object name to open its details.
- Click the Properties tab.
- Scroll down to the Server-side encryption settings section.
- Confirm that it shows Server-side encryption as Amazon S3 managed keys (SSE-S3) and Encryption algorithm as AES-256 (SSE-S3).
Why is my file automatically encrypted?
Starting January 5, 2023, all new object uploads to Amazon S3 are automatically encrypted with SSE-S3 (AES-256) at no additional cost. You do not need to configure anything for this base-level encryption to apply.
SSE-S3 is fully managed by AWS. S3 handles key generation, encryption, and decryption transparently. For many workloads this is sufficient, but it does not provide CloudTrail audit visibility into key usage. That is where SSE-KMS becomes valuable.
Don't see the encryption settings?
Make sure you clicked on the object name (sample.txt) to open its detail page, not just the checkbox next to it. Then click the Properties tab and scroll down to find Server-side encryption settings.
help me find the encryption settings for my S3 object
Your first bucket is locked down with SSE-S3 encryption, public access blocked, and ACLs disabled. Next up, you will create a KMS customer-managed key and configure a second bucket with SSE-KMS encryption for full audit trail visibility.
Configure SSE-KMS with a Customer-Managed Key, Bucket Key, and Key Rotation
In Step 1, you confirmed that SSE-S3 encrypts every object automatically. But SSE-S3 has a limitation: you cannot see who decrypted what or when.
For compliance-heavy workloads, you need an audit trail showing every encryption and decryption event. SSE-KMS solves this by routing encryption through AWS KMS, where every operation is logged in CloudTrail. In this step, you will create your own customer-managed KMS key and use it to encrypt a dedicated S3 bucket.
In this step, get ready to:
- Create a customer-managed KMS key with a custom key policy.
- Enable automatic key rotation on the key.
- Create a new S3 bucket with SSE-KMS encryption and S3 Bucket Key enabled.
Create a customer-managed KMS key
A customer-managed key is a KMS key that you own and control. Unlike AWS-managed keys, you define who can administer and use it through a key policy.
- In the AWS Console search bar at the top, type KMS and select Key Management Service.
- In the left navigation pane, click Customer managed keys.
- Click Create key.
- For Key type, select Symmetric.
- For Key usage, confirm Encrypt and decrypt is selected.
- Click Next.
- Type an alias for the key (for example, security-lab-key).
- Click Next.
- On the Define key administrative permissions page, select your IAM user or role as a key administrator.
- Click Next.
- On the Define key usage permissions page, select the same IAM user or role as a key user.
- Click Next.
- Review the key policy preview, then click Finish.
What is a customer-managed key?
AWS KMS offers three types of keys: AWS owned keys (managed entirely by AWS services), AWS managed keys (visible in your account but managed by AWS), and customer-managed keys (created and controlled by you).
Customer-managed keys give you full control over the key policy, rotation schedule, and who can use the key for cryptographic operations. This control is what enables the audit trails that SSE-KMS provides.
- After creation, note your key's Key ID and ARN from the key details page.
- You will need these values when configuring bucket encryption in the next substep.
This key costs money
A customer-managed KMS key costs $1/month (prorated hourly). You will delete this key in the Before You Go section to stop charges.
Don't see the Create key button?
Make sure you are on the Customer managed keys page, not the AWS managed keys page. AWS managed keys cannot be created or modified by you.
Also confirm your IAM user has kms:CreateKey permission. Help me troubleshoot KMS key creation permissions.
Enable automatic key rotation
Key rotation generates new cryptographic material on a schedule while keeping old material available for decrypting previously encrypted data. This limits the blast radius if key material is ever compromised.
- On your key's detail page, click the Key rotation tab.
- In the Automatic key rotation section, click Edit.
- Select Enable for Key rotation.
- Leave the rotation period at the default of 365 days.
- Click Save.
You should now see Automatic key rotation: Enabled on the Key rotation tab.
How does key rotation work?
When KMS rotates a key, it generates new key material and marks it as the current version for all new encryption requests. All previous versions of the key material remain available indefinitely to decrypt data that was encrypted with them.
The rotation period is configurable between 90 days and 2,560 days (approximately 7 years). Cost-wise, the first and second rotations each add $1/month, capping the total cost at $3/month per key.
Don't see the Key rotation tab?
The Key rotation tab only appears for symmetric encryption keys with key material generated by AWS KMS. If you created an asymmetric key or imported key material, you will not see this tab. Help me check why I don't see the Key rotation tab.
Create the SSE-KMS bucket with Bucket Key enabled
Now you will create a second bucket that uses your customer-managed key for default encryption. You will also enable S3 Bucket Keys to reduce the number of KMS API calls per object.
- Navigate back to the S3 Console (search for S3 in the top search bar).
- Click Create bucket.
- Enter a bucket name like security-lab-sse-kms- followed by a unique suffix (for example, your initials and today's date).
- Confirm Block all public access is checked.
- Under Default encryption, change the Encryption type to Server-side encryption with AWS Key Management Service keys (SSE-KMS).
- Under AWS KMS key, select Choose from your AWS KMS keys and pick the customer-managed key you just created.
- Check the box for Bucket Key to enable it.
- Click Create bucket.
Your new bucket now encrypts every uploaded object with your customer-managed KMS key.
What is an S3 Bucket Key?
Without a Bucket Key, S3 calls KMS for every single object encryption. With high-throughput workloads, this creates thousands of KMS API calls per second.
An S3 Bucket Key generates a time-limited bucket-level key from KMS, and S3 uses that key to create unique data keys for each object. This can reduce KMS costs by up to 99%.
What is DSSE-KMS?
DSSE-KMS (Dual-layer Server-Side Encryption with KMS) applies two layers of encryption to objects. It is designed for highly regulated workloads that mandate dual-layer encryption (for example, CNSSP 15 compliance).
You configure it the same way as SSE-KMS but select the DSSE-KMS option instead. For this lab, SSE-KMS is sufficient.
Don't see your key in the dropdown?
Ensure you are creating the bucket in the same AWS Region where you created the KMS key. KMS keys are region-specific and will not appear in a different region's dropdown. Help me find my KMS key when creating an S3 bucket.
Your KMS key is ready and your SSE-KMS bucket is in place. Next up, you will verify the audit trail by checking CloudTrail for encryption events after uploading an object to this bucket.
Verify SSE-KMS Audit Trail in CloudTrail
In the previous step, you configured a KMS-encrypted bucket with a customer-managed key and S3 Bucket Key enabled. Now it's time to see the biggest advantage SSE-KMS has over SSE-S3: a full audit trail.
Every time S3 encrypts or decrypts an object using your KMS key, CloudTrail logs the API call. This gives compliance teams verifiable proof of who accessed what data and when.
In this step, get ready to:
- Upload an object to your SSE-KMS bucket.
- Locate the GenerateDataKey event in CloudTrail Event history.
- Understand why this audit visibility matters for the SAA-C03 exam.
Upload an object to the KMS bucket
- Return to the S3 Console and open your security-lab-sse-kms-<unique> bucket.
- Click Upload.
- Click Add files and select any small text file from your computer (or use the same sample.txt you used earlier).
- Click Upload at the bottom of the page.
- Once the upload succeeds, click Close.
- Click on the uploaded object's name to open its details.
- Scroll down to Server-side encryption settings.
- Confirm that Encryption type shows Server-side encryption with AWS Key Management Service keys (SSE-KMS) and that the AWS KMS key ARN matches your customer-managed key.
Why verify encryption on the object?
The bucket's default encryption setting tells S3 which method to use for new uploads. Checking the object's properties confirms that S3 actually applied SSE-KMS (not SSE-S3) to this specific object.
Find the GenerateDataKey event in CloudTrail
When S3 encrypts an object with SSE-KMS, it calls the KMS GenerateDataKey API to create a unique data key for that object. CloudTrail records this call automatically.
- Open the CloudTrail console by searching for CloudTrail in the AWS search bar and selecting it.
- In the left navigation pane, click Event history.
What is Event history?
CloudTrail Event history shows the last 90 days of management events in your current region at no extra cost. It is enabled by default on every AWS account.
- You may see a default filter of Read-only set to false. Click the X next to this filter to clear it so all events appear.
- Click the filter dropdown (it may say Event name by default) and change it to Event source.
- In the search field, type kms.amazonaws.com and press Enter.
The filtered list now shows only KMS-related events.
- Look for an event with the Event name GenerateDataKey. It should appear close to the top if you uploaded your object recently.
- Click on the GenerateDataKey event to expand its details.
- In the event record, locate the resources section. You will see your KMS key ARN listed, confirming this encryption event used your customer-managed key.
What does this event prove?
The GenerateDataKey call means S3 requested a unique data key from KMS to encrypt your object. The event record shows who triggered it, which key was used, and the exact timestamp.
This is the primary advantage of SSE-KMS over SSE-S3 for compliance. With SSE-S3, encryption happens silently with no per-object audit trail. With SSE-KMS, every encryption and decryption operation is logged in CloudTrail.
Events may take a few minutes to appear
CloudTrail events can take up to 15 minutes to show in Event history. If you do not see GenerateDataKey yet, wait a few minutes and refresh the page.
Don't see any KMS events?
Make sure you cleared the default Read-only: false filter. This filter excludes read events, and GenerateDataKey may be categorized differently depending on timing.
Also confirm you are viewing Event history in the same region where your KMS bucket lives. CloudTrail Event history is region-specific.
Still stuck? Help me find KMS events in CloudTrail Event history
Understand the exam relevance of SSE-KMS audit trails
Why does the SAA-C03 exam test this?
The exam frequently asks which encryption method provides an audit trail for compliance. The answer is always SSE-KMS, because it generates CloudTrail events for every GenerateDataKey and Decrypt call.
SSE-S3 encrypts objects automatically but does not produce per-object KMS audit events. For regulated workloads that need proof of encryption activity, SSE-KMS is the correct choice.
What about KMS request costs at scale?
Every SSE-KMS encryption or decryption triggers a KMS API call, which has a per-request cost. For high-throughput workloads (thousands of objects per second), these calls add up.
This is exactly the problem S3 Bucket Keys solve. By generating a time-limited bucket-level key from KMS, S3 reduces KMS API calls by up to 99%. You already enabled this on your KMS bucket in the previous step.
You now have concrete proof that SSE-KMS creates a verifiable audit trail. Next up, you will use SSE-C to encrypt an object with your own key that AWS never stores.
Upload and Retrieve an Object with SSE-C via CLI
You have now seen two encryption methods where AWS manages the keys for you (SSE-S3 and SSE-KMS). SSE-C flips this model entirely. With SSE-C, you provide your own encryption key on every upload and download request.
S3 uses your key to encrypt the object, then immediately discards it. If you lose the key, the data is gone forever. In this step, you will enable SSE-C on your bucket, generate a 256-bit key, upload an object with that key, and prove that only someone holding the key can retrieve the data.
In this step, get ready to:
- Enable SSE-C on your SSE-S3 bucket via the CLI.
- Generate a 256-bit encryption key and upload an object with it.
- Prove that retrieval fails without the key and succeeds with it.
Enable SSE-C on your bucket
Since April 2026, all new S3 buckets have SSE-C disabled by default. You must explicitly allow SSE-C via the PutBucketEncryption API before any SSE-C upload will succeed.
- Create a JSON file that defines the encryption configuration. Create a new file called encryption-config.json in your working directory and paste the following content:
{
"Rules": [
{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "AES256"
},
"BucketKeyEnabled": false
}
],
"AllowedEncryptionTypes": ["SSE-S3", "SSE-C"]
}
What does this configuration do?
The AllowedEncryptionTypes field explicitly permits both SSE-S3 and SSE-C uploads. Without this, any SSE-C upload request would be rejected with an HTTP 403 error.
The default encryption remains SSE-S3 (AES-256), so normal uploads without SSE-C headers still work as before.
- Apply the configuration to your SSE-S3 bucket by running this command:
aws s3api put-bucket-encryption --bucket [[SSE_S3_BUCKET="security-lab-sse-s3-yourname"]] --server-side-encryption-configuration file://encryption-config.json
If the command succeeds, you will see no output. That means the configuration was applied.
Seeing an access denied error?
Make sure your CLI credentials have the s3:PutEncryptionConfiguration permission on the bucket. If you created the bucket with your current IAM user, you already have this permission as the bucket owner.
Also confirm you saved encryption-config.json in the same directory where you are running the CLI command. The file:// prefix reads from the current working directory.
Help me troubleshoot my put-bucket-encryption command for enabling SSE-C
Generate a key and upload with SSE-C
SSE-C requires a 256-bit (32-byte) encryption key that you generate and manage yourself. The AWS CLI expects this key as raw binary bytes when using the fileb:// prefix.
- Generate a random 32-byte key and save it to a file called keyfile by running this command:
python -c "import os, base64; key = os.urandom(32); open('keyfile', 'wb').write(key); print('Key (base64):', base64.b64encode(key).decode())"
What does this command do?
This generates 32 random bytes (256 bits) and writes them as raw binary to a file called keyfile. It also prints the base64-encoded version so you can verify the key was created.
Save or note the base64 output somewhere safe. If you lose this key, you cannot decrypt the object. S3 does not store your SSE-C key.
- Confirm the keyfile was created by running:
ls keyfile
You should see keyfile listed in the output.
- Make sure you have a local file to upload. If you no longer have sample.txt locally, create one by running:
echo "This is a test file for SSE-C encryption" > sample.txt
- Upload the file to your SSE-S3 bucket using SSE-C encryption by running:
aws s3 cp sample.txt s3://[[SSE_S3_BUCKET="security-lab-sse-s3-yourname"]]/ssec-object.txt --sse-c AES256 --sse-c-key fileb://keyfile
What do these flags mean?
- --sse-c AES256 tells S3 to use customer-provided key encryption with the AES-256 algorithm.
- --sse-c-key fileb://keyfile provides the raw binary key from your local file. The fileb:// prefix tells the CLI to read the file as binary data.
- The key must NOT be base64-encoded when passed to the CLI. The CLI handles encoding internally.
You should see an upload confirmation showing the file was copied to S3.
Upload failing with an access denied error?
If you see an HTTP 403 error, SSE-C may not be enabled on your bucket yet. Go back and verify the put-bucket-encryption command completed successfully.
If you see an error about the key, confirm your keyfile is exactly 32 bytes. You can check with python -c "print(len(open('keyfile','rb').read()))" which should output 32.
Help me debug my SSE-C upload command
Retrieve the object with and without your key
The real power of SSE-C is that only the key holder can access the data. You will now prove this by attempting retrieval twice.
- First, attempt to download the object WITHOUT providing your encryption key:
aws s3 cp s3://[[SSE_S3_BUCKET="security-lab-sse-s3-yourname"]]/ssec-object.txt downloaded.txt
This command will fail. You should see an error indicating that the object was encrypted with a customer-provided key and you must supply it to retrieve the object.
Why does this fail?
S3 stored only an HMAC hash of your key for validation. It cannot decrypt the object without the original key. Every GET request for an SSE-C object must include the same key that was used during upload.
- Now retrieve the object WITH your encryption key by running:
aws s3 cp s3://[[SSE_S3_BUCKET="security-lab-sse-s3-yourname"]]/ssec-object.txt downloaded.txt --sse-c AES256 --sse-c-key fileb://keyfile
This time the download succeeds. You should see a confirmation that the file was copied to downloaded.txt.
- Verify the file content matches your original by running:
cat downloaded.txt
You should see the original text content from your sample.txt file.
Key exam facts about SSE-C
- SSE-C requires HTTPS. Amazon S3 rejects any requests made over HTTP when using SSE-C.
- SSE-C is not supported in the Amazon S3 Console. You must use the CLI or SDK.
- S3 does NOT store your key. You must provide the exact same key on every GET request.
- If you lose the key, the data is permanently inaccessible.
Download still failing even with the key?
Make sure you are using the exact same keyfile from the upload step. If you regenerated the key between upload and download, S3 will reject the request because the key hash will not match.
Also verify you are downloading from the correct bucket and object path (ssec-object.txt).
Help me troubleshoot my SSE-C download
You have now proven that SSE-C puts full encryption control in the customer's hands. Next up, you will enforce encryption standards at the bucket policy level, requiring HTTPS and specific encryption headers for all uploads.
Enforce HTTPS and Require SSE-KMS via Bucket Policy
In the previous step, you proved that SSE-C gives you full control over the encryption key. But encryption at rest is only half the story.
Data is also vulnerable while it travels between your application and S3. A bucket policy can enforce encryption standards at the API level. In this step, you will add a policy that blocks unencrypted transport (HTTP) and requires every upload to use SSE-KMS encryption.
In this step, get ready to:
- Add a bucket policy that denies insecure (HTTP) requests.
- Add a policy statement that denies uploads not encrypted with SSE-KMS.
- Test both enforcement rules via the AWS CLI.
Add the HTTPS and encryption enforcement policy
S3 bucket policies use condition keys to inspect each incoming request. The aws:SecureTransport condition checks whether a request arrived over HTTPS. The s3:x-amz-server-side-encryption condition checks which encryption method the upload specifies.
- Return to the S3 console and click on your SSE-S3 bucket (security-lab-sse-s3-...).
- Choose the Permissions tab.
- Scroll down to Bucket policy and click Edit.
- Delete any existing content in the policy editor, then paste the following policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::[[SSE_S3_BUCKET="security-lab-sse-s3-yourname"]]",
"arn:aws:s3:::[[SSE_S3_BUCKET="security-lab-sse-s3-yourname"]]/*"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
},
{
"Sid": "DenyNonKMSEncryptedUploads",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::[[SSE_S3_BUCKET="security-lab-sse-s3-yourname"]]/*",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption": "aws:kms"
}
}
}
]
}
What does this policy do?
This policy has two Deny statements that work together to lock down your bucket:
- DenyInsecureTransport blocks ALL S3 actions (s3:*) when the request arrives over plain HTTP instead of HTTPS. The condition key aws:SecureTransport is false for HTTP requests.
- DenyNonKMSEncryptedUploads blocks any PutObject request that does not include aws:kms as the encryption method. This forces every upload to explicitly specify SSE-KMS.
- Click Save changes.
You should see the policy appear under the Bucket policy section on the Permissions tab.
Seeing an error when saving?
- Make sure you replaced the bucket name placeholder with your actual bucket name in all three places (both Resource arrays and the single Resource string).
- Verify the JSON is valid. A missing comma or extra trailing comma will cause a syntax error.
- Check that you copied the full policy including the opening and closing braces.
Help me fix my bucket policy error.
Test the policy with the AWS CLI
With the policy in place, any upload that does not include SSE-KMS encryption headers will be denied. The AWS CLI always uses HTTPS by default, so you will only trigger the encryption enforcement statement in this test.
- First, test uploading without specifying KMS encryption by running this command:
aws s3 cp sample.txt s3://[[SSE_S3_BUCKET="security-lab-sse-s3-yourname"]]/test-policy.txt
What should I see?
You should see an access denied error. Because you did not include --sse aws:kms in the command, the s3:x-amz-server-side-encryption condition is not satisfied. The policy's DenyNonKMSEncryptedUploads statement kicks in and blocks the upload.
- Now test a successful upload by specifying SSE-KMS encryption:
aws s3 cp sample.txt s3://[[SSE_S3_BUCKET="security-lab-sse-s3-yourname"]]/test-policy.txt --sse aws:kms --sse-kms-key-id [[KMS_KEY_ARN="your-kms-key-arn"]]
What does this command do?
- --sse aws:kms tells S3 to use SSE-KMS encryption for this upload.
- --sse-kms-key-id specifies which customer-managed KMS key to use. Paste the key ARN you noted in Step 2.
This time the upload should succeed because the request satisfies both policy conditions: it uses HTTPS (the CLI default) and specifies aws:kms encryption.
You should see the upload complete with no errors. The file test-policy.txt now exists in your bucket, encrypted with your KMS key.
Still getting access denied on the KMS upload?
- Double-check you included both --sse aws:kms and --sse-kms-key-id in your command. Missing either one can trigger the deny.
- Verify the key ARN is correct and that your IAM user has kms:GenerateDataKey permission on the key.
- Make sure your CLI is configured with the same AWS account that owns the bucket and the KMS key.
Help me fix my SSE-KMS upload that is being denied by the bucket policy.
Why enforce encryption via bucket policy?
Default encryption only applies when the client does not specify encryption. A bucket policy with an explicit Deny gives you a hard guarantee. Even if a misconfigured application forgets to set encryption headers, the upload fails instead of silently using a weaker method.
This pattern is a common exam scenario on the SAA-C03. AWS recommends using condition keys like aws:SecureTransport and s3:x-amz-server-side-encryption to enforce encryption standards at the bucket level.
Your bucket now rejects any request that arrives over HTTP or tries to upload without SSE-KMS. Next up, you will enable S3 Versioning and Object Lock to protect objects from accidental or malicious deletion.
Enable S3 Versioning and Object Lock
So far you have encrypted objects with three different methods and enforced upload rules via bucket policy. Encryption protects data at rest, but what about protecting data from accidental or malicious deletion?
In this step, you will enable S3 Versioning and S3 Object Lock to implement WORM (write-once-read-many) storage. This pattern prevents objects from being deleted or overwritten for a defined retention period, meeting regulatory compliance requirements like SEC 17a-4 and HIPAA.
In this step, get ready to:
- Create a new bucket with Versioning and Object Lock enabled.
- Configure a default Governance mode retention period and upload objects.
- Attempt to delete a locked object and observe the protection in action.
Create a bucket with Versioning and Object Lock
Object Lock can only be enabled at bucket creation time. It also automatically enables Versioning, because Object Lock protects individual object versions.
- In the S3 Console, click General purpose buckets in the left navigation pane.
- Click Create bucket.
- For Bucket name, enter security-lab-lock- followed by a unique suffix (e.g. your initials and today's date).
- Under Bucket Versioning, choose Enabled.
- Scroll down to Advanced settings.
- Under Object Lock, choose Enable.
- Check the acknowledgement box confirming that enabling Object Lock will permanently allow objects in this bucket to be locked.
- Click Create bucket.
Why does Object Lock require Versioning?
Object Lock protects specific object versions. When you "delete" an object in a versioned bucket, S3 adds a delete marker instead of removing data. The locked version remains intact underneath that marker.
This is why Object Lock automatically enables Versioning. Without it, there would be no version to protect.
- Upload a sample text file to your new bucket. You can use the Upload button in the Console and select any small .txt file from your computer.
Don't see the Object Lock option?
The Object Lock setting only appears under Advanced settings at the bottom of the Create bucket page. Make sure you scroll all the way down past Tags. If you already created the bucket without it, you cannot retroactively enable Object Lock on the same bucket in all cases. Create a new bucket instead.
help me enable Object Lock on my S3 bucket
Configure default retention and upload a second object
Now you will set a default retention rule so that every new object uploaded to this bucket is automatically protected.
- Click your security-lab-lock- bucket to open it.
- Go to the Properties tab.
- Scroll down to Object Lock and click Edit.
- Enable Default retention.
- Set Default retention mode to Governance.
- Set Default retention period to 1 day.
- Click Save changes.
- Upload a second text file to this bucket. This new object will automatically receive Governance mode retention for 1 day.
Governance mode vs Compliance mode
Governance mode allows users with the s3:BypassGovernanceRetention permission to override the lock. This is useful for testing and for cases where you need an emergency escape hatch.
Compliance mode cannot be overridden by anyone, including the root account, until the retention period expires. Use Compliance mode for strict regulatory requirements where data must be truly immutable.
Test Object Lock protection by attempting deletion
The real proof of Object Lock is seeing what happens when you try to delete a protected object. You will attempt deletion and observe how S3 protects the locked version.
- Go to the Objects tab of your security-lab-lock- bucket.
- Select the second file you uploaded (the one with retention applied).
- Click Delete.
- Confirm the deletion by typing the required confirmation text and click Delete objects.
The delete appears to succeed. S3 has added a delete marker on top of the object. The object itself is still protected underneath.
- Toggle Show versions at the top of the Objects list to reveal all versions.
- You should see the delete marker and the original object version still listed below it.
- Select the original object version (not the delete marker).
- Click Delete to attempt a permanent deletion of this specific version.
- Type the confirmation text and click Delete objects.
You should see an error indicating the deletion was denied. The object version is protected by the Governance mode retention period.
What just happened?
A standard delete in a versioned bucket just adds a delete marker. The locked version stays intact. To permanently remove a locked version, you would need the s3:BypassGovernanceRetention permission AND include the x-amz-bypass-governance-retention:true header in your request.
In Compliance mode, not even the root account could bypass this protection. The object is truly immutable until the retention date passes.
SAA-C03 exam key points for Object Lock
- Object Lock requires Versioning (automatically enabled when Object Lock is turned on at bucket creation).
- Governance mode: users with s3:BypassGovernanceRetention permission can override.
- Compliance mode: cannot be overridden by anyone, including root, until retention expires.
- Use cases: regulatory compliance (SEC 17a-4, HIPAA), protecting backups from ransomware.
Deletion succeeded unexpectedly?
If the version-level delete did not produce an error, check that you uploaded the file after setting the default retention to Governance mode. Objects uploaded before setting default retention are not automatically protected.
Also confirm you selected the actual object version, not the delete marker. Delete markers themselves can always be removed.
help me understand why my Object Lock protection is not working
You have proven that Object Lock protects object versions from permanent deletion. Next up, you will configure access logging to track who is accessing your buckets and when.
Enable S3 Access Logs
You now have encryption, Object Lock, and bucket policies in place. The next layer of security is visibility.
In this step, you will configure S3 server access logging so that every request to your bucket is recorded. These logs are critical for compliance auditing, security investigation, and understanding access patterns.
In this step, get ready to:
- Create a dedicated logging bucket with SSE-S3 encryption.
- Enable server access logging on your source bucket.
- Generate traffic and verify log delivery.
Create the dedicated logging bucket
S3 access logs need to be stored somewhere. AWS requires that the destination bucket uses SSE-S3 encryption specifically. If you choose SSE-KMS instead, log delivery can fail because the logging service cannot access your custom key.
- In the S3 Console, click Create bucket.
- Enter a bucket name like security-lab-logs- followed by a unique suffix (e.g. your initials and a number).
- Leave Block all public access checked (default).
- Under Default encryption, confirm the encryption type is set to Server-side encryption with Amazon S3 managed keys (SSE-S3). This is the default for new buckets.
- Click Create bucket.
Why must the logging bucket use SSE-S3?
If the destination bucket uses SSE-KMS encryption, the S3 logging service may not have permission to use your KMS key. This causes log delivery to silently fail.
AWS requires that server access log destination buckets use Amazon S3 managed keys (SSE-S3). This ensures the logging service can always write logs without key access issues.
Always use a separate bucket for logs
Never log a bucket's access to itself. This creates an infinite loop where each log delivery generates another log entry, increasing storage costs and making it harder to find the logs you need.
Enable server access logging on the source bucket
Now you will point your SSE-S3 bucket's logging configuration at the new logging bucket. Every request made to the source bucket will generate a log record in the destination.
- In the S3 Console, navigate to your security-lab-sse-s3- bucket.
- Click the Properties tab.
- Scroll down to Server access logging and click Edit.
- Select Enable.
- Under Target bucket, click Browse S3 and select your security-lab-logs- bucket.
- In the Target prefix field, enter access-logs/.
- Click Save changes.
What does a target prefix do?
The target prefix groups all log files under a common path inside the destination bucket. With the prefix access-logs/, every log object key starts with that string, making log files easy to locate and distinguish from other objects.
What about permissions?
When you enable logging through the S3 Console, AWS automatically updates the destination bucket's policy to grant the S3 logging service principal permission to write objects. You do not need to configure this manually.
Server access logging won't save?
Make sure your logging destination bucket is in the same AWS Region as the source bucket. Both buckets must also be owned by the same account.
Also confirm the destination bucket does not have Object Lock or Requester Pays enabled, as these block log delivery.
Having trouble with a different error? Help me troubleshoot my S3 access logging configuration.
Generate traffic and inspect the log files
S3 access logs are not delivered in real time. It typically takes a few hours for log files to appear, though you may see them sooner. Let's generate some traffic to produce log entries.
- Go back to your security-lab-sse-s3- bucket's Objects tab.
- Upload a new file (any small text file will do).
- Download one of your existing objects (click the object name, then click Download).
- Repeat a few upload and download operations to generate multiple log entries.
- Wait 5-10 minutes (logs can take time to be delivered).
- Navigate to your security-lab-logs- bucket.
- Open the access-logs/ prefix. You should see log files appear as objects.
- Click on any log file and download it. Open it in a text editor to inspect the contents.
What fields are in a log entry?
Each log record is a single line with space-delimited fields. Key fields to look for include:
- Bucket Owner - the canonical ID of the bucket owner.
- Timestamp - when the request was received (UTC).
- Requester - the IAM identity that made the request.
- Operation - the type of request (e.g. REST.PUT.OBJECT, REST.GET.OBJECT).
- HTTP Status - the response code (e.g. 200, 403).
- Encryption Status - which encryption method was used for the object.
Any field set to - means the data was unavailable or not applicable for that request.
Don't see any log files yet?
S3 access logs are delivered on a best-effort basis. Delivery can take anywhere from a few minutes to a few hours. If you do not see logs after 10 minutes, try generating more traffic and checking back later.
Also confirm you are looking in the correct bucket under the access-logs/ prefix. If the prefix folder does not appear, the logging configuration may not have saved correctly.
Still stuck? Help me figure out why my S3 access logs are not appearing.
Your logging infrastructure is live. You now have a complete audit trail of every request hitting your source bucket. Next up, you will create an S3 Access Point to scope down access to a specific prefix.
Create an S3 Access Point Scoped to a Prefix
You now have encryption, transport security, versioning, and logging configured across your buckets. The next challenge is access management at scale.
When multiple teams or applications share a bucket, maintaining a single bucket policy with dozens of statements becomes unwieldy. S3 Access Points solve this by giving each consumer its own named endpoint with its own policy. In this step, you will create an Access Point scoped to a single prefix, proving that you can enforce least-privilege data access without touching the bucket policy.
In this step, get ready to:
- Create an S3 Access Point attached to your SSE-S3 bucket with Internet network origin.
- Attach a policy restricting access to objects under the finance/ prefix only.
- Test the Access Point by uploading to the allowed prefix and confirming denial outside it.
Create the Access Point in the S3 Console
An Access Point is a named network endpoint attached to exactly one bucket. Each Access Point gets its own DNS name and its own resource policy, so you can grant or restrict access without editing the bucket policy itself.
- In the S3 Console, click Access Points in the left navigation panel.
- Click Create access point.
- For Access point name, enter finance-data-ap.
- For Bucket name, choose Choose a bucket in this account and select your security-lab-sse-s3- bucket.
- For Network origin, select Internet.
- Under Block Public Access settings for this Access Point, confirm that Block all public access is checked.
- Skip the Access Point policy section for now. You will add the policy after creation.
- Click Create access point.
What is an S3 Access Point?
Access Points are named network endpoints attached to a bucket. Each one has its own ARN, its own DNS name, and its own resource policy. They simplify managing access for shared datasets at scale because you can give each application or team its own Access Point with specific permissions.
The bucket policy must also permit the access (either directly or via delegation to the Access Point). Since your IAM identity owns the bucket, your existing permissions already satisfy this requirement.
- After creation, click on finance-data-ap in the Access Points list.
- Note the Access Point ARN shown at the top. It follows the format arn:aws:s3:<region>:<account-id>:accesspoint/finance-data-ap. You will need this ARN for CLI commands later.
Access Point creation failed?
Make sure you selected a bucket in your own account. Access Point names must be lowercase, 3-50 characters, and cannot contain underscores.
help me troubleshoot why my S3 Access Point creation failed
Attach a prefix-scoped Access Point policy
Without a policy, the Access Point inherits whatever permissions the bucket and IAM policies allow. To restrict this Access Point to only the finance/ prefix, you will attach a resource policy that limits s3:GetObject and s3:PutObject to objects under that prefix.
- On the finance-data-ap details page, click the Permissions tab.
- Under Access Point policy, click Edit.
- Paste the following policy, replacing <REGION> with your AWS region, and <ACCOUNT-ID> with your 12-digit AWS account ID:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowFinancePrefixOnly",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::<ACCOUNT-ID>:root"},
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:<REGION>:<ACCOUNT-ID>:accesspoint/finance-data-ap/object/finance/*"
}
]
}
What does this policy do?
- Principal grants access to the root of your AWS account, which covers all IAM users and roles in your account.
- Action allows only s3:GetObject and s3:PutObject. No listing, no deleting.
- Resource uses the Access Point ARN format with /object/finance/* to restrict operations to only objects under the finance/ prefix.
- Click Save.
- You should see the policy displayed under the Permissions tab confirming it was saved successfully.
Seeing a policy error?
Double-check that your region and account ID are correct. The ARN must follow the exact format arn:aws:s3:<region>:<account-id>:accesspoint/finance-data-ap/object/finance/* with no trailing spaces.
help me fix my S3 Access Point policy that is returning an error
Test the Access Point via CLI
Now you will prove the policy works by uploading through the Access Point ARN. The AWS CLI accepts Access Point ARNs in place of bucket names using the s3:// URI format.
- Upload an object to the finance/ prefix through the Access Point by running this command:
aws s3 cp sample.txt s3://arn:aws:s3:[[REGION="your-region"]]:[[ACCOUNT_ID="your-account-id"]]:accesspoint/finance-data-ap/finance/report.txt --sse aws:kms --sse-kms-key-id [[KMS_KEY_ARN="your-kms-key-arn"]]
Why include the SSE-KMS flags?
Your bucket policy from Step 5 denies any PutObject that does not specify aws:kms encryption. Even when uploading through an Access Point, the bucket policy still applies. You must include --sse aws:kms and --sse-kms-key-id to satisfy that condition.
- You should see output confirming a successful upload, such as upload: ./sample.txt to s3://arn:aws:s3:...accesspoint/finance-data-ap/finance/report.txt.
- Now attempt to access an object outside the finance/ prefix through the Access Point by running:
aws s3 cp s3://arn:aws:s3:[[REGION="your-region"]]:[[ACCOUNT_ID="your-account-id"]]:accesspoint/finance-data-ap/sample.txt downloaded.txt
- This command should fail with an Access Denied error because the Access Point policy only allows operations on objects under finance/*.
Why was access denied?
The Access Point policy explicitly allows only s3:GetObject and s3:PutObject on the resource accesspoint/finance-data-ap/object/finance/*. Any request targeting an object outside that prefix has no matching Allow statement, so S3 returns an implicit deny.
On the exam, remember that Access Points simplify managing access for shared datasets at scale. Each Access Point has its own DNS name and policy. The bucket policy must also permit the access, either directly or via delegation.
Upload through Access Point failed with Access Denied?
If even the finance/report.txt upload failed, check these common causes:
- Verify you included --sse aws:kms --sse-kms-key-id <your-key-arn> in the command. The bucket policy denies unencrypted uploads.
- Confirm your region and account ID in the Access Point ARN are correct. A typo causes S3 to return Access Denied rather than a friendly error.
- Make sure the Access Point policy was saved successfully (check the Permissions tab).
help me troubleshoot why my upload through the S3 Access Point is being denied
Your Access Point is locked down to a single prefix. Next, you will generate a presigned URL to prove you can share temporary access to individual objects without giving out AWS credentials.
Generate and Test a Presigned URL
In the previous step, you created an Access Point to scope down data access to a specific prefix. Now you'll tackle another common scenario: sharing an S3 object with someone who doesn't have AWS credentials at all.
Presigned URLs solve this by generating a temporary, signed link that grants time-limited access to a specific object. In this step, you'll create presigned URLs using both the Console and CLI, then watch them expire in real time.
In this step, get ready to:
- Generate a presigned URL in the S3 Console with a 1-minute expiration.
- Test the URL in a browser before and after it expires.
- Generate a presigned URL via the AWS CLI and review key exam facts.
Generate a presigned URL in the Console
Sometimes you need to share an object temporarily with a contractor, client, or external system that has no AWS access. A presigned URL embeds authentication directly into the link, granting access for a specific duration without requiring credentials.
- Return to the S3 Console and click on your security-lab-sse-s3- bucket.
- Click on sample.txt to open its object detail page.
- Click Object actions in the upper-right area of the page.
- Select Share with a presigned URL from the dropdown menu.
- In the time interval field, enter 1 and select Minutes from the dropdown.
- Click Create presigned URL.
A green banner appears confirming the presigned URL was created and copied to your clipboard.
What is a presigned URL?
A presigned URL is a time-limited link that grants access to a specific S3 object without requiring the recipient to have AWS credentials. The URL contains authentication information embedded in the query string parameters.
The credentials used by the presigned URL are those of the IAM principal who generated it. Anyone with the link can perform the action (download, in this case) until the URL expires.
Test the URL before and after expiration
Now you'll prove that the presigned URL works by downloading the object without credentials, then confirm it stops working after the 1-minute window closes.
- Open a new browser tab (or an incognito/private window for a cleaner test).
- Paste the presigned URL into the browser address bar and press Enter.
- Confirm that sample.txt downloads successfully. No AWS sign-in was required.
Now wait at least 1 minute for the URL to expire, then test it again.
- Paste the same presigned URL into your browser address bar again and press Enter.
- Observe the XML error response in the browser. You should see a message indicating Request has expired.
What should I see?
The browser displays an XML response with an error code and a message about the request expiration. This proves that once the time window passes, the link becomes useless. The object is still safe in your bucket.
Generate a presigned URL via CLI
The S3 Console limits presigned URL expiration to 12 hours. For longer durations (up to 7 days), you use the AWS CLI.
- Generate a presigned URL with a 60-second expiration by running this command:
aws s3 presign s3://[[BUCKET_SSE_S3="your-security-lab-sse-s3-bucket-name"]]/sample.txt --expires-in 60
What does this command do?
- aws s3 presign generates a presigned URL for the specified object.
- --expires-in 60 sets the URL to expire after 60 seconds. The default is 3600 seconds (1 hour).
- The output is the full presigned URL you can share or paste into a browser.
- Copy the URL from the terminal output and paste it into your browser to confirm it downloads the file.
Key exam facts for presigned URLs
- Console maximum expiration: 12 hours from the time of creation.
- CLI/SDK maximum expiration: 7 days (604800 seconds).
- The presigned URL inherits the permissions of the IAM identity that generated it. If the identity loses access, the URL stops working even before expiration.
- Use cases: temporary download links for external users, allowing file uploads without giving users AWS credentials, sharing data with time-sensitive access requirements.
URL not working?
- If you get an access denied error immediately (not an expiration error), check that your IAM user has s3:GetObject permission on the bucket. The presigned URL can only grant permissions that the generator already has.
- If the URL expired before you could test it, generate a new one with a longer --expires-in value (e.g., 300 for 5 minutes).
Need help debugging presigned URL issues?
You've now proven that presigned URLs provide controlled, time-limited access to S3 objects without sharing credentials. Next up, you'll take on the Secret Mission and combine network-level isolation with encryption enforcement for a multi-layered security architecture.
Secret mission
VPC-Restricted Access Point + Encryption Enforcement
You have already built Internet-origin Access Points and enforced encryption via bucket policies. Now combine both techniques into a single hardened architecture. Create a VPC-restricted Access Point that rejects all traffic from outside your VPC, then verify the existing encryption enforcement policy blocks any upload that does not use SSE-KMS.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. The KMS customer-managed key costs $1/month (prorated hourly), and S3 storage costs are minimal (pennies).
Cost warning
Your KMS customer-managed key charges $1/month even when not in use. If you are done with this lab, schedule it for deletion in the Delete tab below to stop charges after the 7-day waiting period.
S3 storage costs are negligible for the small files in this lab, but the Object Lock bucket retains versions that cannot be deleted until the 1-day Governance retention expires (or you bypass it).
Resources you used:
- S3 bucket: security-lab-sse-s3-<unique> (SSE-S3, SSE-C enabled, contains objects, bucket policy, access logging).
- S3 bucket: security-lab-sse-kms-<unique> (SSE-KMS with customer-managed key, Bucket Key enabled).
- S3 bucket: security-lab-logs-<unique> (access logging destination, SSE-S3).
- S3 bucket: security-lab-lock-<unique> (Versioning and Object Lock enabled, Governance mode).
- S3 Access Points: finance-data-ap and vpc-restricted-ap.
- KMS customer-managed key (symmetric, with automatic rotation enabled).
- VPC endpoint for S3 (Gateway type, created for Secret Mission).
- Local file: keyfile (SSE-C encryption key).
Keep everything running
No action needed. Choose this if you are still actively experimenting with S3 encryption and security configurations.
- Your KMS key will continue to cost $1/month (prorated hourly).
- S3 storage costs are pennies for the small files in this lab.
- The Object Lock bucket retains objects until their 1-day retention period expires.
Pause - I'll come back to this later
Delete objects from your buckets to eliminate storage costs but keep the buckets and KMS key for reference.
- In the S3 Console, open each bucket and select all objects, then choose Delete.
- For the Object Lock bucket (security-lab-lock-<unique>), wait for the 1-day Governance retention to expire before deleting objects. Alternatively, if your IAM user has s3:BypassGovernanceRetention permission, you can delete versions immediately by including the governance bypass header.
- Keep the KMS key active for future reference ($1/month).
- Delete the local keyfile if you no longer need it.
Delete - I don't want to use this again
Remove all project resources and stop all charges. Follow these steps in order.
Delete all objects from your S3 buckets:
- In the S3 Console, open security-lab-sse-s3-<unique>. Select all objects and choose Delete. Confirm the deletion.
- Repeat for security-lab-sse-kms-<unique> and security-lab-logs-<unique>.
- For security-lab-lock-<unique>: wait for the 1-day Governance retention to expire, then delete all object versions. If you cannot wait, use the CLI with the x-amz-bypass-governance-retention:true header (requires s3:BypassGovernanceRetention permission).
Delete the S3 buckets:
- Once each bucket is empty, select it in the S3 Console and choose Delete. Type the bucket name to confirm.
- Delete all four buckets: security-lab-sse-s3-<unique>, security-lab-sse-kms-<unique>, security-lab-logs-<unique>, and security-lab-lock-<unique>.
Delete the S3 Access Points:
- In the S3 Console, go to Access Points in the left navigation.
- Select finance-data-ap, choose Delete, type the name to confirm, and choose Delete.
- Repeat for vpc-restricted-ap.
Schedule deletion of the KMS key:
- Open the KMS Console and select Customer managed keys.
- Select the checkbox next to the key you created.
- Choose Key actions, then Schedule key deletion.
- Set the waiting period to 7 days (the minimum). Confirm and choose Schedule deletion.
- The key enters a Pending deletion state. After 7 days, it is permanently deleted and charges stop.
Delete the VPC endpoint (Secret Mission only):
- Open the VPC Console and select Endpoints in the left navigation.
- Select the S3 Gateway endpoint you created.
- Choose Actions, then Delete VPC endpoints.
- Type delete to confirm and choose Delete.
Delete local files:
- Delete the local keyfile and any sample text files you created for this lab.
That's a Wrap!
That's a Wrap!
Nice work! You've built a comprehensive S3 security architecture covering the complete spectrum of encryption, access control, and data protection tested on the SAA-C03 exam.
You've learned how to:
- Implemented three S3 encryption methods side by side (SSE-S3, SSE-KMS with S3 Bucket Keys, and SSE-C) and verified each through CloudTrail audit trails, object metadata inspection, and key-holder-only retrieval.
- Enforced encryption in transit via bucket policies, configured Block Public Access, managed KMS key rotation, and enabled Object Lock (Governance mode) for WORM compliance.
- Configured server access logging to a dedicated bucket, created a prefix-scoped S3 Access Point for least-privilege data access, and generated presigned URLs to prove temporary credential-free object sharing.
- Built a VPC-restricted Access Point with encryption enforcement, combining network isolation and data security into a single architecture.
Ready to quiz yourself?