AWS S3 Basics: Buckets, Objects, and Permissions
A hands-on tour of Amazon S3 — buckets vs objects, naming rules, regions, public vs private access, presigned URLs, and the everyday aws s3 CLI commands.
What you'll learn
- ✓The difference between a bucket and an object — and why both names matter
- ✓S3 bucket naming rules and why region choice is permanent
- ✓How public vs private works, and why Block Public Access is on by default
- ✓How to share private objects safely with presigned URLs
- ✓The aws s3 CLI: mb, cp, ls, sync, and rm
Prerequisites
- •An AWS account and a basic mental model — see What Is AWS?
- •The AWS CLI installed and configured with an IAM user
Amazon S3 (Simple Storage Service) is the oldest service in AWS and almost certainly the one you will use first. It stores files — anywhere from a tiny config blob to terabytes of video — durably, cheaply, and with a simple HTTP API. This post is the practical tour: what S3 is, how to use it from the CLI, and how to avoid the security mistakes that make the news.
Buckets and objects
Two words cover almost the entire S3 vocabulary.
Bucket. A top-level container for objects. Buckets are globally unique across all of AWS — once someone has registered the name my-photos, nobody else can use it. Each bucket lives in exactly one region, chosen at creation time, and that region cannot be changed.
Object. A single file plus its metadata. Each object has a key (the full path-like name, e.g. users/123/avatar.png), a body (the actual bytes), and metadata (content type, custom headers, etc.). There are no real folders — the slashes in the key are just characters. The console shows them as folders for convenience.
That is it. No filesystems, no permissions per directory, no symbolic links. Just buckets containing objects.
Quick definition. S3 is object storage: large numbers of immutable blobs accessed via HTTP. It is the opposite of a filesystem, where you have a tree of mutable directories. The object model is what makes S3 cheap and durable at scale.
Naming rules
Bucket names look forgiving but have surprisingly strict rules. The full list:
- 3 to 63 characters long
- Lowercase letters, numbers, dots, and hyphens only
- Must start and end with a letter or number
- Must not look like an IP address (
192.168.1.1) - Must be globally unique across all AWS accounts and regions
- Dots in bucket names cause problems with HTTPS — avoid them unless you know why you need them
A reasonable convention is <company>-<purpose>-<environment>, for example acme-uploads-prod. The name shows up in URLs, logs, and budgets, so a tiny bit of thought now saves real friction later.
Creating a bucket
The fastest path is the AWS CLI. Pick a region — us-east-1 is the default in most tutorials — and a name nobody else has taken:
# Make a bucket in us-east-1
aws s3 mb s3://acme-uploads-demo --region us-east-1
# make_bucket: acme-uploads-demo
mb is short for make bucket. If the name is already taken, you get a clear error and need to try another.
To see your buckets:
aws s3 ls
# 2026-06-15 10:00:00 acme-uploads-demo
To see what is inside a specific bucket (empty for now):
aws s3 ls s3://acme-uploads-demo
Uploading and downloading objects
The aws s3 cp command works just like a local cp, but either side can be an S3 URL.
# Upload a single file
echo "hello s3" > hello.txt
aws s3 cp hello.txt s3://acme-uploads-demo/hello.txt
# upload: ./hello.txt to s3://acme-uploads-demo/hello.txt
# Download it back
aws s3 cp s3://acme-uploads-demo/hello.txt ./downloaded.txt
cat downloaded.txt
# hello s3
# Upload an entire directory
aws s3 cp ./site s3://acme-uploads-demo/site --recursive
aws s3 sync is cp with smarter behaviour — it only transfers files that differ. It is the right choice for “make this remote folder match this local folder”:
# Push local changes to S3 (only uploads new/changed files)
aws s3 sync ./site s3://acme-uploads-demo/site
# Mirror back the other way
aws s3 sync s3://acme-uploads-demo/site ./site
To delete:
aws s3 rm s3://acme-uploads-demo/hello.txt
aws s3 rm s3://acme-uploads-demo/site --recursive
That handful of commands — mb, ls, cp, sync, rm — covers the daily workload of most projects.
Try it yourself. Create a bucket with a unique name. Upload a file. List the bucket. Download the file under a different name. Delete it. Then delete the bucket with aws s3 rb s3://your-bucket-name. Five commands. Total cost: well under a cent.
Public vs private
By default, every object you upload to S3 is private. You can read it with credentials; nobody else can read it at all. This is the right default. The objects most likely to make the news (medical records, customer data, internal documents) are the ones that were accidentally made public.
AWS reinforces this with Block Public Access, a bucket-level setting that is on by default for every new bucket. With it on, no policy or ACL can make objects public — even if you misconfigure something, the block wins. Leave it on unless you have a specific reason.
A specific reason might be: a static website where every object is genuinely meant to be world-readable. In that case, you would disable Block Public Access on that bucket, add a bucket policy granting s3:GetObject to *, and accept that everything in the bucket is public.
A safer pattern for most use cases is:
- Keep Block Public Access on
- Keep all objects private
- Serve public content through CloudFront with an Origin Access Identity, so the bucket itself stays locked down
- Serve user uploads to authenticated users via presigned URLs
The web is full of “exposed S3 bucket” headlines. They almost all trace back to someone disabling Block Public Access and then writing a too-permissive policy.
Object URLs
Every object has a URL of the shape:
https://<bucket>.s3.<region>.amazonaws.com/<key>
For example:
https://acme-uploads-demo.s3.us-east-1.amazonaws.com/hello.txt
If the object is public, that URL just works in a browser. If the object is private (the default), hitting that URL returns AccessDenied. Which is exactly what you want for non-public content.
Presigned URLs
Presigned URLs are S3’s killer feature for sharing private content safely. A presigned URL is a regular S3 URL with extra query parameters that act as a time-limited, action-scoped signature. Anyone with the URL can perform the signed action (usually GetObject or PutObject) until it expires.
Generate one with the CLI:
aws s3 presign s3://acme-uploads-demo/hello.txt --expires-in 600
# https://acme-uploads-demo.s3.amazonaws.com/hello.txt?...&X-Amz-Expires=600&X-Amz-Signature=...
That URL works in any browser for the next ten minutes, then stops working. Common uses:
- Email a customer a one-time download link for their invoice
- Let a browser upload directly to S3 without proxying through your server
- Share a large file with a colleague without making the whole bucket public
In application code, the SDK has the same primitive. Here is a small Node.js example with the AWS SDK v3:
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const s3 = new S3Client({ region: 'us-east-1' });
const url = await getSignedUrl(
s3,
new GetObjectCommand({ Bucket: 'acme-uploads-demo', Key: 'hello.txt' }),
{ expiresIn: 600 }
);
console.log(url);
Send that URL to your user, and only they can fetch the object — for the next ten minutes.
Try it yourself. Upload a private file to your bucket. Try the plain https://bucket.s3.region.amazonaws.com/key URL in a browser — you should see an AccessDenied XML response. Now generate a presigned URL with aws s3 presign. Paste that into the browser. It should download the file. Wait ten minutes and try again — Request has expired.
Simple, real use cases
A few patterns that come up in nearly every project:
Hosting a static website. Upload an HTML/CSS/JS folder, put CloudFront in front, point your domain at CloudFront. Pennies per month for low traffic.
Receiving user uploads. Your server generates a presigned PUT URL, the browser uploads straight to S3. Your application server never touches the bytes — cheaper, faster, more scalable.
Database backups. Your nightly backup script dumps the database and runs aws s3 cp backup.sql.gz s3://acme-backups/. Add a lifecycle policy to move older backups to cheaper storage classes after 30 days.
Logs and analytics. Almost every AWS service can write logs to S3. Tools like Athena let you query them directly with SQL without loading them into a database.
Build artifacts. CI uploads build outputs to S3 so deploy steps can fetch them later. Common pattern in any non-trivial pipeline.
Storage classes
Not all data deserves the same speed (or price). S3 offers several storage classes with different cost-versus-access trade-offs:
- Standard. The default. Cheap to read, available in milliseconds.
- Intelligent-Tiering. S3 monitors access patterns and moves objects to cheaper tiers automatically. Good default for “I don’t know how often this will be accessed.”
- Standard-IA / One Zone-IA. Lower storage cost, higher retrieval cost. For data accessed less than once a month.
- Glacier Instant Retrieval / Flexible Retrieval / Deep Archive. Very cheap storage, slow (and sometimes expensive) retrieval. For long-term archives you rarely touch.
Most beginner projects stay on Standard. Once a bucket starts holding terabytes, lifecycle rules that move old objects to cheaper classes can cut the bill dramatically.
Cost, briefly
S3 charges for three things:
- Storage — per GB per month, depending on storage class
- Requests — per thousand
PUT,GET,LIST, etc. - Data transfer out — per GB of data leaving AWS to the internet
The first 5 GB of Standard storage are free for 12 months under the free tier, with reasonable request and transfer allowances. For small projects, S3 bills measured in cents per month are normal. The classic surprise bill comes from data transfer out — if you accidentally serve a viral file directly from S3 instead of through CloudFront, the egress can add up quickly.
Recap
You now know:
- Buckets are globally unique containers; objects are the files inside them — keyed by a path-like string
- A bucket lives in one region, chosen at creation and permanent
- Objects are private by default, and Block Public Access is on by default — leave it that way
- Presigned URLs are the right tool for time-limited sharing of private content
- The CLI verbs
mb,ls,cp,sync, andrmcover the daily workload - Storage classes and lifecycle rules keep large buckets affordable
S3 is one of those services where a small amount of knowledge unlocks an enormous amount of practical usefulness. Most production AWS architectures touch S3 somewhere.
Next steps
You have now toured containers, Git, CI/CD, Kubernetes, and AWS. The natural next move is to pick a small real project — a personal site, a side API, a hobby tool — and put it through the whole pipeline: container, CI on push, deploy to a cloud service, assets on S3. Building one thing end-to-end teaches you more than ten more articles.
Useful adjacent reading from this series:
- What Is CI/CD? — to wire automated tests and deploys around your project
- GitHub Actions: Your First Workflow — for the practical first pipeline
- What Is Docker? — if you have not containerised the app yet
Questions or feedback? Email codeloomdevv@gmail.com.