DEV Community

Tejas Shinkar
Tejas Shinkar

Posted on

Amazon S3 Hands-On Practicals

I recently worked through a hands-on Amazon S3 practical series covering the features I would actually expect to use while working with AWS storage.

Instead of only documenting definitions, this post focuses on what I configured, the commands I used, how I verified the behavior, and what I observed when something went wrong.

For the concepts behind these practicals, I have already covered S3 in two detailed sessions:

This post is the practical companion to those two sessions. The concepts are covered there; here I focus on actually building, testing, verifying, and troubleshooting the S3 features.

The labs covered:

  1. S3 bucket configuration and lifecycle management
  2. Bucket policies with IAM, EC2 and HTTPS-only access
  3. SSE-KMS encryption with CloudTrail verification
  4. Pre-signed URLs
  5. AWS CLI s3 sync
  6. S3 Versioning and version recovery
  7. Static website hosting
  8. S3 CORS
  9. S3 Object Lock

Note: This is a practical write-up, so I have intentionally kept the focus on implementation and verification rather than turning it into a generic S3 theory article.


1. S3 Bucket Configuration and Lifecycle Management

Objective

Create an S3 bucket with a secure baseline and configure a lifecycle rule that automatically transitions objects to lower-cost storage classes over time.

Configuration

For the lab:

  • Block Public Access remained enabled.
  • Bucket Versioning was enabled.
  • Lifecycle rule: s3-lab-lifecycle
  • The rule applied to all objects.
  • Current objects transition to:
    • Standard-IA after 30 days
    • Glacier Flexible Retrieval after 90 days

The lifecycle flow was:

Day 0
  ↓
Object uploaded
  ↓
Day 30 → Standard-IA
  ↓
Day 90 → Glacier Flexible Retrieval
Enter fullscreen mode Exit fullscreen mode

Result

The lifecycle rule was successfully created and enabled, and the S3 console confirmed the configured transition periods.

What this demonstrates

Instead of manually moving old objects, S3 Lifecycle can automatically transition objects between storage classes based on age and the rules we define.


2. S3 Bucket Policy — IAM Read, EC2 Upload and HTTPS-only Access

Objective

Configure a bucket policy that:

  • Allows a specific IAM user to read objects.
  • Allows an EC2 role to upload objects.
  • Denies S3 access when the request is not using HTTPS.

Resources

Bucket: policytesting-bucket-ts
IAM User: Tejas-IAM
EC2 Role: EC2-S3-ReadOnly-Role
Region: ap-south-1
Enter fullscreen mode Exit fullscreen mode

Policy design

The policy contained three logical statements:

IAM User
   ↓
s3:GetObject
   ↓
S3 bucket


EC2 IAM Role
   ↓
s3:PutObject
   ↓
S3 bucket


HTTP request
   ↓
Explicit Deny
   ↓
S3 bucket
Enter fullscreen mode Exit fullscreen mode

The important security condition was:

"Bool": {
    "aws:SecureTransport": "false"
}
Enter fullscreen mode Exit fullscreen mode

This denies requests that are not using HTTPS.

Verification

The IAM user successfully downloaded an object from S3 using AWS CLI.

An EC2 instance was launched with EC2-S3-ReadOnly-Role attached, and the role was verified from the instance. The EC2 instance was then able to upload an object to S3.

Result

Both permission paths worked as intended:

  • IAM user → object read
  • EC2 role → object upload
  • Non-HTTPS access → explicitly denied

Practical takeaway

A useful S3 security pattern is to combine:

  • identity-based permissions,
  • resource-based bucket policies,
  • and explicit security-condition denies.

3. SSE-KMS Encryption with CloudTrail Verification

Objective

Configure S3 default encryption using a customer-managed KMS key and verify the resulting cryptographic operations through CloudTrail.

KMS configuration

The lab used:

Key alias: s3-lab-key
Key type: Symmetric
Key usage: Encrypt and decrypt
Region: ap-south-1
Key administrator: Tejas-IAM
Key user: Tejas-IAM
Enter fullscreen mode Exit fullscreen mode

Configure S3 encryption

The bucket:

sse-kms-bucket-tejas
Enter fullscreen mode Exit fullscreen mode

was configured for server-side encryption using the customer-managed KMS key.

Verification

After uploading and downloading an object, I checked CloudTrail Event History.

The relevant KMS operations were:

GenerateDataKey
Decrypt
Enter fullscreen mode Exit fullscreen mode

GenerateDataKey appeared during the encryption flow, while Decrypt events were associated with accessing encrypted data.

Troubleshooting

Initially, the expected cryptographic events were not obvious in CloudTrail.

Instead of relying only on the initial event list, I changed the lookup to search by Event name. That exposed the expected:

GenerateDataKey
Decrypt
Enter fullscreen mode Exit fullscreen mode

events.

Result

SSE-KMS was successfully configured and the related KMS operations were visible through CloudTrail.

Practical takeaway

Encryption configuration alone is not always enough when troubleshooting. CloudTrail can provide the audit trail needed to understand which AWS services and cryptographic operations were involved.


4. S3 Pre-signed URL

Objective

Keep an S3 object private while providing temporary access through a pre-signed URL.

Object

Bucket: sse-kms-bucket-tejas
Object: presigned-test.txt
Enter fullscreen mode Exit fullscreen mode

The object itself remained private.

No public permissions were added.

Generate the URL

aws s3 presign s3://sse-kms-bucket-tejas/presigned-test.txt --expires-in 300
Enter fullscreen mode Exit fullscreen mode

300 seconds = 5 minutes.

Test

I opened the generated URL in an Incognito browser window.

The private object was accessible successfully.

After the five-minute validity period expired, I refreshed the same URL.

S3 returned:

AccessDenied
Request has expired

Result

The test demonstrated the main behavior of a pre-signed URL:

Private S3 object
       +
Temporary signed URL
       ↓
Temporary access
       ↓
URL expires
       ↓
Access denied
Enter fullscreen mode Exit fullscreen mode

Practical takeaway

A pre-signed URL is useful when an application needs to provide temporary access to a private S3 object without making the object itself public.


5. AWS CLI s3 sync

Objective

Use aws s3 sync to synchronize a local directory with S3 and verify that only changed files are uploaded during a subsequent sync.

Create the test directory

mkdir s3-sync-lab
cd s3-sync-lab

echo "File 1 - original content" > file1.txt
echo "File 2 - original content" > file2.txt
Enter fullscreen mode Exit fullscreen mode

Initial sync

aws s3 sync . s3://sse-kms-bucket-tejas/s3-sync-lab/
Enter fullscreen mode Exit fullscreen mode

Then verify:

aws s3 ls s3://sse-kms-bucket-tejas/s3-sync-lab/
Enter fullscreen mode Exit fullscreen mode

The first synchronization uploaded both files.

Modify one file

echo "File 1 - UPDATED content" > file1.txt
Enter fullscreen mode Exit fullscreen mode

Run sync again:

aws s3 sync . s3://sse-kms-bucket-tejas/s3-sync-lab/
Enter fullscreen mode Exit fullscreen mode

This time, only file1.txt was uploaded.

Result

The practical demonstrated the differential behavior of:

aws s3 sync
Enter fullscreen mode Exit fullscreen mode

Unchanged content did not need to be uploaded again.

Practical takeaway

This is useful for scripts, backups and deployment workflows where repeatedly uploading an entire directory would be unnecessary.


6. S3 Versioning and Version Recovery

Objective

Enable Versioning, upload the same object key multiple times, identify the generated Version IDs, retrieve an older version, and remove the current version to demonstrate recovery.

Enable Versioning

Versioning was enabled on:

sse-kms-bucket-tejas
Enter fullscreen mode Exit fullscreen mode

Create multiple versions

The same object key was reused:

version-test-tejas.txt
Enter fullscreen mode Exit fullscreen mode

Version 1:

echo "Version 1 - Created by Tejas" > version-test-tejas.txt

aws s3 cp version-test-tejas.txt \
s3://sse-kms-bucket-tejas/version-test-tejas.txt
Enter fullscreen mode Exit fullscreen mode

Version 2:

echo "Version 2 - Updated by Tejas" > version-test-tejas.txt

aws s3 cp version-test-tejas.txt \
s3://sse-kms-bucket-tejas/version-test-tejas.txt
Enter fullscreen mode Exit fullscreen mode

Version 3:

echo "Version 3 - Final update by Tejas" > version-test-tejas.txt

aws s3 cp version-test-tejas.txt \
s3://sse-kms-bucket-tejas/version-test-tejas.txt
Enter fullscreen mode Exit fullscreen mode

List versions

aws s3api list-object-versions \
  --bucket sse-kms-bucket-tejas \
  --prefix version-test-tejas.txt
Enter fullscreen mode Exit fullscreen mode

The practical produced multiple retained versions for the same object key.

The latest version was marked as current, while previous versions remained available.

Retrieve an older version

aws s3api get-object \
  --bucket sse-kms-bucket-tejas \
  --key version-test-tejas.txt \
  --version-id <VERSION_ID> \
  version-1-retrieved.txt
Enter fullscreen mode Exit fullscreen mode

Then:

cat version-1-retrieved.txt
Enter fullscreen mode Exit fullscreen mode

The historical content was:

Version 1 - Created by Tejas
Enter fullscreen mode Exit fullscreen mode

Delete the current version

A specific Version ID was deleted:

aws s3api delete-object \
  --bucket sse-kms-bucket-tejas \
  --key version-test-tejas.txt \
  --version-id <CURRENT_VERSION_ID>
Enter fullscreen mode Exit fullscreen mode

Then the versions were listed again:

aws s3api list-object-versions \
  --bucket sse-kms-bucket-tejas \
  --prefix version-test-tejas.txt
Enter fullscreen mode Exit fullscreen mode

The previous version became the latest available version.

Troubleshooting

The version listing output opened in the terminal's less viewer.

To return to the shell:

q
Enter fullscreen mode Exit fullscreen mode

There was also an initial retrieval mistake where:

cat version-1-retrieved.txt
Enter fullscreen mode Exit fullscreen mode

failed because the file had not yet been created.

After running get-object successfully, the historical content could be read.

Result

This lab demonstrated that Versioning can preserve historical object states and allow an older version to be retrieved even after a newer version becomes current.


7. S3 Static Website Hosting

Objective

Host a simple static website directly from an S3 bucket and configure a custom error page.

Bucket

Bucket: s3-static-website-tejas
Region: ap-south-1
Enter fullscreen mode Exit fullscreen mode

For this lab, Block Public Access was disabled because the website content needed to be publicly readable through the S3 website endpoint.

Website configuration

Static website hosting was enabled with:

Index document: cors-test.html
Error document: error.html
Enter fullscreen mode Exit fullscreen mode

Verification

The S3 region-specific website endpoint was opened in a browser.

The website loaded successfully.

A non-existent page was then requested to trigger the configured error document.

The custom 404 page was returned successfully.

Result

Both paths were verified:

Valid request
     ↓
Index / website content


Invalid request
     ↓
Custom error.html
Enter fullscreen mode Exit fullscreen mode

Practical takeaway

S3 website hosting is useful for simple static content, although real production architectures may place additional services such as CloudFront in front of the content.


8. S3 CORS

Objective

Demonstrate a browser cross-origin request failure and then allow the request using an S3 CORS configuration.

This was one of the more useful troubleshooting labs because the browser behavior made the difference between S3 access and browser CORS permission very visible.

Resources

Origin website bucket: s3-cors-origin-tejas
Object bucket: s3-cors-lab-tejas
Object: cat.jpg
Enter fullscreen mode Exit fullscreen mode

The webpage was hosted from one S3 website origin and attempted to fetch the image from another S3 origin.

Test before CORS

The webpage attempted a JavaScript fetch() request for cat.jpg.

The browser blocked the response because the required:

Access-Control-Allow-Origin
Enter fullscreen mode Exit fullscreen mode

header was not present.

The browser console showed the CORS failure.

Configure CORS

For the practical, the object bucket was configured with:

[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["GET"],
    "AllowedOrigins": ["*"],
    "ExposeHeaders": []
  }
]
Enter fullscreen mode Exit fullscreen mode

Test again

A fresh pre-signed URL was generated and the browser test was repeated.

The final response showed:

Response type: cors
Status: 200
Access-Control-Allow-Origin: *
Blob: image/jpeg

The image was successfully returned as a Blob.

Important troubleshooting observation

During the lab, an expired pre-signed URL initially produced:

HTTP 403
Enter fullscreen mode Exit fullscreen mode

with an XML error response.

That was separate from the CORS configuration itself.

After generating a fresh pre-signed URL, the request returned:

HTTP 200
Enter fullscreen mode Exit fullscreen mode

and included the CORS response header.

Another useful observation was that fetch() does not automatically reject its promise just because the HTTP status is 403. The response status should be checked explicitly.

For example:

fetch(url)
  .then(response => {
    console.log("Status:", response.status);
    console.log("CORS header:",
      response.headers.get("Access-Control-Allow-Origin"));
    return response.blob();
  })
  .then(data => console.log("Blob:", data));
Enter fullscreen mode Exit fullscreen mode

Result

The practical demonstrated both sides of CORS:

No CORS header
      ↓
Browser blocks cross-origin access


CORS configured
      ↓
HTTP 200 + CORS header
      ↓
JavaScript receives the object
Enter fullscreen mode Exit fullscreen mode

9. S3 Object Lock

Objective

Protect an S3 object version using Governance Mode and verify that a normal delete request is blocked while retention is active.

Bucket configuration

A new bucket was created:

s3-object-lock-tejas
Enter fullscreen mode Exit fullscreen mode

Versioning and Object Lock were enabled.

Default retention was left disabled because retention would be applied to the specific test object version.

Create and upload the test object

echo "Object Lock test created by Tejas" > tejas-object-lock.txt

aws s3 cp tejas-object-lock.txt \
s3://s3-object-lock-tejas/
Enter fullscreen mode Exit fullscreen mode

Find the Version ID

aws s3api list-object-versions \
  --bucket s3-object-lock-tejas \
  --prefix tejas-object-lock.txt
Enter fullscreen mode Exit fullscreen mode

The response provided the Version ID for the uploaded object.

Apply Governance retention

The object version was configured with Governance Mode and a one-day retention period:

aws s3api put-object-retention \
  --bucket s3-object-lock-tejas \
  --key tejas-object-lock.txt \
  --version-id "<VERSION_ID>" \
  --retention "Mode=GOVERNANCE,RetainUntilDate=<RETENTION_DATE>"
Enter fullscreen mode Exit fullscreen mode

Verify retention

aws s3api get-object-retention \
  --bucket s3-object-lock-tejas \
  --key tejas-object-lock.txt \
  --version-id "<VERSION_ID>"
Enter fullscreen mode Exit fullscreen mode

The response showed:

{
  "Retention": {
    "Mode": "GOVERNANCE",
    "RetainUntilDate": "<RETENTION_DATE>"
  }
}

Attempt deletion

A normal delete was attempted:

aws s3api delete-object \
  --bucket s3-object-lock-tejas \
  --key tejas-object-lock.txt \
  --version-id "<VERSION_ID>"
Enter fullscreen mode Exit fullscreen mode

S3 rejected the operation with:

AccessDenied
Access Denied because object protected by object lock.

Result

The retention worked as expected.

The object version could not be normally deleted while the Object Lock retention period was active.

Governance vs Compliance

The practical used Governance Mode.

The important distinction is:

  • Governance Mode provides retention protection while allowing appropriately authorized bypass operations.
  • Compliance Mode is stricter and is designed for scenarios where even privileged administrators should not be able to bypass the retention.

What These Practicals Demonstrated

After completing the labs, the S3 concepts were no longer just console options or documentation terms.

The practical sequence covered:

Feature Practical verification
Lifecycle Automatic storage-class transition rules
Bucket Policy IAM read + EC2 upload + HTTPS-only deny
SSE-KMS S3 encryption using customer-managed KMS
CloudTrail GenerateDataKey and Decrypt events
Pre-signed URL Temporary private-object access
CLI Sync Differential synchronization
Versioning Historical versions and recovery
Static Website Website endpoint + custom 404
CORS Browser block → CORS-enabled success
Object Lock Governance retention → delete blocked

Troubleshooting Lessons

A few issues during the labs were particularly useful because they showed how AWS behaves in real situations.

1. CloudTrail events were not immediately obvious

Searching by event name exposed the expected KMS operations:

GenerateDataKey
Decrypt
Enter fullscreen mode Exit fullscreen mode

2. Long AWS CLI output can open in less

When a command such as list-object-versions produces a long response:

q
Enter fullscreen mode Exit fullscreen mode

returns to the shell.

3. Generate the output file before reading it

Running:

cat version-1-retrieved.txt
Enter fullscreen mode Exit fullscreen mode

before running get-object results in:

No such file or directory
Enter fullscreen mode Exit fullscreen mode

The correct sequence is:

S3 version
   ↓
get-object
   ↓
local file created
   ↓
cat local file
Enter fullscreen mode Exit fullscreen mode

4. CORS and HTTP errors are different problems

A request can fail because:

  • the object URL expired,
  • S3 returned 403,
  • the object does not exist,
  • or the browser blocks a successful cross-origin response because the CORS headers are missing.

Testing with a fresh pre-signed URL helped separate the authentication/expiry issue from the CORS issue.

5. Object Lock actually blocks deletion

The Object Lock test was especially clear because S3 returned an explicit:

AccessDenied
Enter fullscreen mode Exit fullscreen mode

when a protected object version was deleted.


Final Takeaway

The biggest value of these S3 labs was not memorizing the S3 console.

It was seeing how the different controls interact:

                 Amazon S3
                     |
       +-------------+-------------+
       |             |             |
   Security       Storage        Access
       |             |             |
  IAM / Policy   Lifecycle     Pre-signed URL
  HTTPS-only     Versioning    CORS
  SSE-KMS        Object Lock
       |
   CloudTrail
Enter fullscreen mode Exit fullscreen mode

S3 becomes much easier to reason about once these features are connected to actual operations:

  • Who can access the object?
  • How is the object encrypted?
  • How long should it remain in a storage class?
  • What happens when the object is overwritten?
  • Can an old version be recovered?
  • Can the object be accessed temporarily without making it public?
  • Can a browser access it cross-origin?
  • Can the object be deleted during a retention period?

These practicals gave me hands-on verification of each of those questions rather than only theoretical answers.


Commands Used in the Labs

A compact list of the most important commands from the practicals:

# Pre-signed URL
aws s3 presign s3://sse-kms-bucket-tejas/presigned-test.txt --expires-in 300

# Sync local directory to S3
aws s3 sync . s3://sse-kms-bucket-tejas/s3-sync-lab/

# List object versions
aws s3api list-object-versions \
  --bucket sse-kms-bucket-tejas \
  --prefix version-test-tejas.txt

# Retrieve a specific version
aws s3api get-object \
  --bucket sse-kms-bucket-tejas \
  --key version-test-tejas.txt \
  --version-id <VERSION_ID> \
  version-1-retrieved.txt

# Delete a specific version
aws s3api delete-object \
  --bucket sse-kms-bucket-tejas \
  --key version-test-tejas.txt \
  --version-id <VERSION_ID>

# List Object Lock versions
aws s3api list-object-versions \
  --bucket s3-object-lock-tejas \
  --prefix tejas-object-lock.txt

# Apply Object Lock retention
aws s3api put-object-retention \
  --bucket s3-object-lock-tejas \
  --key tejas-object-lock.txt \
  --version-id "<VERSION_ID>" \
  --retention "Mode=GOVERNANCE,RetainUntilDate=<RETENTION_DATE>"

# Verify Object Lock retention
aws s3api get-object-retention \
  --bucket s3-object-lock-tejas \
  --key tejas-object-lock.txt \
  --version-id "<VERSION_ID>"

# Attempt deletion of a protected version
aws s3api delete-object \
  --bucket s3-object-lock-tejas \
  --key tejas-object-lock.txt \
  --version-id "<VERSION_ID>"
Enter fullscreen mode Exit fullscreen mode

Conclusion

This S3 practical series covered the service from basic bucket configuration through security, encryption, access control, synchronization, version recovery, static hosting, browser CORS behavior and immutable-object protection.

The most useful part was not simply getting a "Success" message from AWS. It was deliberately testing the expected behavior — including expired URLs, historical versions, browser CORS failures and an Object Lock deletion failure.

That is the kind of hands-on understanding I want to build as I continue learning AWS, Cloud and DevOps.

Top comments (0)