DevOps

Building a Secure and Scalable Asset Storage Architecture with AWS S3 and CloudFront

Images, documents, avatars, attachments, and other uploaded files are often treated as a secondary concern during the early stages of a project.

The application works, users can upload files, and everything seems fine.

Until one day the storage structure becomes difficult to maintain, access control becomes inconsistent, CDN integration becomes necessary, or storage costs start growing unexpectedly.

A well-designed asset storage architecture prevents many of these problems before they appear.

This article covers a practical approach to storing and delivering assets using Amazon S3 and CloudFront while following modern AWS best practices.

The Architecture

A production-ready asset delivery architecture typically looks like this:

Users
   ↓
CloudFront CDN
   ↓
Private S3 Bucket

Instead of exposing S3 directly to the internet, CloudFront becomes the public entry point while S3 remains private.

This approach provides several advantages:

  • Better security
  • Global content delivery
  • Improved caching
  • Easier infrastructure management
  • Greater flexibility for future changes

Most importantly, storage and content delivery become separate concerns.

S3 stores files.

CloudFront delivers files.

Each service does one job well.

Why Private S3 Is Better Than Public S3

Many projects start with public S3 URLs:

https://bucket-name.s3.amazonaws.com/file.webp

While this works, it introduces unnecessary exposure.

Modern AWS recommendations favor:

  • Block Public Access enabled
  • ACLs disabled
  • Access controlled through IAM and bucket policies
  • CloudFront used for public delivery

With this model, files are never publicly exposed through S3 itself.

CloudFront becomes the only service allowed to read content from the bucket.

The result is a cleaner and more secure architecture.

Organize Storage Around Ownership

One of the most common mistakes is storing files in a flat structure:

uploads/
images/
documents/

This works initially but becomes difficult to manage as the system grows.

A better approach is organizing assets based on ownership and relationships.

Example:

users/{userId}/avatars/

entities/{entityId}/images/

entities/{entityId}/attachments/

Or for content-driven systems:

articles/{articleId}/hero/

articles/{articleId}/images/

articles/{articleId}/attachments/

The exact structure depends on the application, but the principle remains the same:

Every asset should clearly belong to something.

This makes cleanup, migration, auditing, and troubleshooting significantly easier.

Never Trust File Names as Identifiers

Users upload files with identical names every day:

image.jpg
photo.jpg
document.pdf
resume.pdf

Using original filenames as storage identifiers eventually causes collisions.

A better approach is:

articles/{articleId}/images/{assetId}.webp

or

entities/{entityId}/attachments/{assetId}.pdf

The original filename can still be stored in the database if needed for downloads or display purposes.

Storage keys should be stable and unique.

Separate Development and Production Storage

Development and production assets should never share the same bucket.

A common structure is:

project-assets-dev
project-assets-prod

Benefits include:

  • Safer testing
  • Easier cleanup
  • Reduced risk of accidental production modifications
  • Simpler permission management

Environment isolation is one of the easiest improvements to make and one of the most valuable.

Use CloudFront URLs Instead of S3 URLs

Applications should expose CDN URLs:

https://cdn.example.com/images/logo.webp

not raw S3 URLs:

https://bucket-name.s3.amazonaws.com/images/logo.webp

This creates an important layer of abstraction.

If the underlying infrastructure changes later, the application and frontend remain unaffected.

For example, you might eventually:

  • Rename buckets
  • Move to a different region
  • Replace CloudFront
  • Introduce image optimization services

Applications that expose CDN URLs are much easier to evolve.

Configure Cache Headers Properly

Caching is one of the biggest advantages of using a CDN.

Static assets should generally be cached aggressively.

For versioned images:

Cache-Control: public, max-age=31536000, immutable

For downloadable files:

Cache-Control: public, max-age=86400

For frequently changing resources:

Cache-Control: public, max-age=300

Correct cache headers improve performance while reducing infrastructure costs.

Use Origin Access Control

CloudFront should access S3 through Origin Access Control (OAC).

This allows:

  • Private buckets
  • Secure access between CloudFront and S3
  • Removal of legacy ACL-based approaches

OAC is the current AWS-recommended solution for serving private S3 content through CloudFront.

Apply Least-Privilege IAM Permissions

Application credentials should receive only the permissions they require.

For most asset services, that means:

s3:PutObject
s3:GetObject
s3:DeleteObject
s3:ListBucket

Avoid granting broad permissions such as:

s3:*

unless there is a specific operational requirement.

Smaller permission scopes reduce risk and simplify security reviews.

Enable Versioning

Versioning is often overlooked until a mistake occurs.

It protects against:

  • Accidental deletion
  • Overwritten files
  • Deployment errors
  • Recovery scenarios

The storage overhead is usually worth the additional safety.

For production environments, versioning should be strongly considered.

Define Lifecycle Rules Early

Storage tends to grow faster than expected.

Lifecycle policies help keep costs under control.

Useful examples include:

Delete Incomplete Multipart Uploads

7 days

Archive Rarely Accessed Files

90 days

Remove Temporary Assets

30 days

Lifecycle policies become much easier to manage when the storage structure is organized from the beginning.

Common Mistakes

Exposing S3 Directly

Using CloudFront provides better security and flexibility.

Mixing Development and Production Assets

This eventually causes operational problems.

Relying on Original Filenames

File name collisions are inevitable.

Using Public ACLs

Modern AWS architectures generally do not require them.

Returning Raw S3 URLs

Applications should expose CDN URLs instead.

Granting Excessive IAM Permissions

Least privilege should always be the default.

Final Thoughts

Asset storage is rarely the most exciting part of a system, but it becomes increasingly important as applications grow.

A solid foundation typically includes:

  • Private S3 buckets
  • CloudFront as the delivery layer
  • Origin Access Control
  • Structured storage paths
  • Versioning
  • Lifecycle management
  • Least-privilege IAM policies

The initial setup requires a little more effort than a public S3 bucket, but it provides a cleaner, safer, and more maintainable architecture for the long term.

Investing in a good storage design early is much easier than rebuilding it later.