The AWS S3 Cheat Sheet I Actually Use: cp, sync, presign, and Bucket Policies
A practical AWS S3 cheat sheet for the CLI commands you run daily — cp, sync, ls, rb, presigned URLs, storage classes, bucket policies vs ACLs, and static hosting.
The AWS S3 Cheat Sheet I Actually Use: cp, sync, presign, and Bucket Policies
Most S3 documentation is a wall of API parameters you will never type. What you reach for day to day is a much smaller set: copy a file up, mirror a build folder, hand someone a temporary link, pick the right storage class, and stop a bucket from being accidentally public. This post is that smaller set — the commands I keep within arm's reach, with the gotchas that have bitten me written next to them.
The Core Four: cp, sync, ls, rb
Almost everything starts with four verbs. aws s3 ls lists, aws s3 cp copies, aws s3 sync mirrors, and aws s3 rb removes a bucket. The high-level aws s3 interface handles multipart uploads and pagination for you, which is why you want it for normal work.
# List a bucket's top level, then a prefix
aws s3 ls s3://acme-assets/
aws s3 ls s3://acme-assets/images/ --human-readable --recursive
# Copy one file up, then a whole folder
aws s3 cp report.pdf s3://acme-reports/2024/
aws s3 cp ./logs s3://acme-logs/ --recursive --exclude "*.tmp"
# Remove a bucket and everything in it
aws s3 rb s3://throwaway-bucket --force
The --exclude/--include pair is order-sensitive: filters are applied left to right, so put the broad --exclude "*" first and the narrow --include "*.json" after it when you want an allowlist. Get them backwards and you will quietly upload nothing.
sync, --delete, and the Mirror You Have to Respect
aws s3 sync only transfers files that changed (by size and timestamp), which makes it the right tool for repeated deploys. Add --delete and it becomes a true mirror: anything present at the destination but missing from the source gets removed.
# Mirror a build folder, removing stale objects, with a dry run first
aws s3 sync ./dist s3://acme-www/ --delete --dryrun
aws s3 sync ./dist s3://acme-www/ --delete
That --delete flag is also the single most dangerous thing in this post. The failure mode is forgetting the source subfolder: aws s3 sync . s3://bucket/ --delete run from your home directory will push whatever junk is sitting there and delete everything else in the bucket. I always pin the source to a real build folder (./dist, ./out) and run --dryrun the first time I deploy to any new environment. If you have ever watched a production bucket empty itself, you only need to learn that lesson once.
Presigned URLs: Sharing Without Sharing Credentials
A presigned URL grants temporary, signed access to a single private object — no IAM user, no public bucket, no leaked keys. The link carries a signature that expires after the TTL you set.
# A 24-hour link to a private invoice
aws s3 presign s3://acme-invoices/2024-Q4.pdf --expires-in 86400 --profile prod
The one catch worth remembering: the URL is signed by the calling identity, so its real lifetime is the shorter of your --expires-in value and the credentials' own validity. A link signed by temporary role credentials that expire in an hour will die in an hour regardless of an 86400 TTL. For browser uploads rather than downloads, reach for s3api create-presigned-post, which returns a form policy the browser POSTs directly to S3.
Storage Classes and Lifecycle: Paying Less for Cold Data
Every object has a storage class, and you can set it at upload time or transition to a cheaper tier later. Standard is the default; Glacier and Deep Archive cost a fraction of it for data you rarely touch.
# Upload an archive straight to Glacier
aws s3 cp backup-2024.tar.gz s3://acme-archive/ --storage-class GLACIER
# Or sync nightly logs to a cheaper class
aws s3 sync ./logs s3://acme-logs/ --storage-class STANDARD_IA
For ongoing cost control, attach a lifecycle rule with aws s3api put-bucket-lifecycle-configuration so objects transition to Glacier after, say, 90 days and expire after a year — plus an AbortIncompleteMultipartUpload rule, because orphaned multipart parts sit there billing you silently until something cleans them up.
Bucket Policies vs ACLs (and Why ACLs Lost)
This is the part that trips up anyone returning to S3 after a couple of years. Buckets created after April 2023 ship with Object Ownership set to BucketOwnerEnforced, which disables ACLs entirely. The old --acl public-read will return AccessControlListNotSupported. Access is now governed by bucket policies — a JSON document attached to the bucket — and the four Block Public Access switches.
To make a bucket publicly readable for a website, the order is: turn off Block Public Access, then attach a policy granting s3:GetObject to Principal: "*". To audit a legacy bucket that someone left open via an old ACL, check aws s3api get-bucket-ownership-controls, flip it to BucketOwnerEnforced, and replace any ACL grants with an explicit policy. I lean toward policies for everything now because they are one document you can read, diff, and version-control, instead of grants scattered across individual objects.
Worked Scenario: Deploying a Static Site to S3
Here is the end-to-end flow I run for a static front end — a Next.js export, a Vite build, doesn't matter. Assume the bucket acme-www already exists.
# 1. Enable website hosting
aws s3 website s3://acme-www --index-document index.html --error-document 404.html
# 2. Allow public reads (BPA off, then a read-only policy)
aws s3api put-public-access-block --bucket acme-www \
--public-access-block-configuration \
BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false
aws s3api put-bucket-policy --bucket acme-www --policy '{
"Version":"2012-10-17",
"Statement":[{"Effect":"Allow","Principal":"*","Action":"s3:GetObject","Resource":"arn:aws:s3:::acme-www/*"}]
}'
# 3. Deploy the build — long cache for assets, no cache for HTML
aws s3 sync ./dist s3://acme-www/ --delete \
--cache-control "max-age=31536000,immutable" --exclude "*.html"
aws s3 sync ./dist s3://acme-www/ \
--cache-control "no-cache" --exclude "*" --include "*.html"
The two-pass sync gives hashed assets a one-year immutable cache while keeping HTML fresh, so a redeploy is visible immediately but CSS and JS stay cached. One more thing: the S3 website endpoint is HTTP only. Put CloudFront in front of it for HTTPS and a real domain — the bucket policy above stays exactly the same.
A Quick Reference to Keep Open
When you are mid-incident you do not want prose; you want the exact flag. That is what the AWS S3 Cheatsheet is for — 58+ commands across core operations, sync, presigned URLs, versioning, lifecycle, encryption, CORS, and the low-level s3api calls, each with the pitfall written next to it and copy-ready examples. It runs entirely in your browser, so you can paste real bucket names without a single request leaving the page. If your day involves more than just S3 — IAM profiles, EC2, the credential chain — pair it with the broader AWS CLI Cheatsheet so the whole aws toolchain is one search box away.
S3 is simple until the day it isn't, and the difference is usually one flag you half-remembered. Keep the cheat sheet open, run --dryrun before anything destructive, and enable versioning before you store anything you would be sad to lose.
Made by Toolora · Updated 2026-06-13