S3 stores files ("objects") in containers called buckets. Bucket names are global, objects are addressed by key (which looks like a path), and almost every security problem comes from making a bucket public by accident. This post gets you productive without that risk.
Create a bucket
aws s3 mb s3://acme-team-reports-2026 --region eu-west-1
aws s3api get-public-access-block --bucket acme-team-reports-2026
New buckets have Block Public Access on and default encryption with S3-managed keys. Leave Block Public Access on. Turn on versioning now, not after the first accident:
aws s3api put-bucket-versioning --bucket acme-team-reports-2026 --versioning-configuration Status=Enabled
Tip
Bucket names must be globally unique, lowercase, and are visible in URLs. Do not put customer names or secrets in them.
Upload, list, download
aws s3 cp report.csv s3://acme-team-reports-2026/2026/10/report.csv
aws s3 ls s3://acme-team-reports-2026/2026/ --recursive --human-readable --summarize
aws s3 cp s3://acme-team-reports-2026/2026/10/report.csv ./downloaded.csv
Folders are just key prefixes. 2026/10/report.csv is one object with that key; there is no real 2026 directory.
Copy folders and sync
aws s3 cp ./exports s3://acme-team-reports-2026/exports/ --recursive
aws s3 sync ./site s3://acme-team-reports-2026/site/
aws s3 sync ./site s3://acme-team-reports-2026/site/ --exclude "*.tmp" --include "*.html"
sync copies only new or changed files. With --delete it also removes objects that are not in the source:
aws s3 sync ./site s3://acme-team-reports-2026/site/ --delete --dryrun
Caution
Always run
--deletewith--dryrunfirst. With versioning on you can recover, but it is slower than not making the mistake.
Share one file safely: presigned URL
Instead of making anything public, generate a link that expires:
aws s3 presign s3://acme-team-reports-2026/2026/10/report.csv --expires-in 3600
Anyone with the link can download that one object until it expires (one hour here). The link carries your permissions, so it stops working if your credentials expire sooner, for example an SSO session.
Delete and recover
aws s3 rm s3://acme-team-reports-2026/2026/10/report.csv
aws s3api list-object-versions --bucket acme-team-reports-2026 --prefix 2026/10/report.csv \
--query "{v:Versions[].{Id:VersionId,Latest:IsLatest}, d:DeleteMarkers[].{Id:VersionId,Latest:IsLatest}}"
# remove the delete marker to bring the object back
aws s3api delete-object --bucket acme-team-reports-2026 --key 2026/10/report.csv --version-id <delete-marker-version-id>
Storage classes in one line each
| Class | Use for |
|---|---|
| S3 Standard | Default, frequently read data |
| S3 Intelligent-Tiering | Unknown or changing access patterns |
| S3 Standard-IA / One Zone-IA | Read rarely but need fast access (retrieval fees apply) |
| S3 Glacier Instant / Flexible / Deep Archive | Archives; slower and cheaper the deeper you go |
aws s3 cp big-archive.zip s3://acme-team-reports-2026/archive/ --storage-class GLACIER_IR
Common errors
| Error | Cause | Fix |
|---|---|---|
AccessDenied | Missing IAM permission, bucket policy deny, or KMS key permission | Check aws sts get-caller-identity; review bucket policy and key policy |
BucketAlreadyExists | Name taken by someone else, anywhere | Pick a more specific name |
NoSuchKey | Key typo, or wrong prefix case (keys are case sensitive) | aws s3 ls the prefix |
| Slow uploads of big files | Single stream | The CLI already uses multipart; raise aws configure set default.s3.max_concurrent_requests 20 |
Prevent it next time
- Keep Block Public Access on at account level:
aws s3control put-public-access-block --account-id <id> --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true. - Versioning plus a lifecycle rule that expires old versions after N days, so recovery is possible but storage does not grow forever.
- Share with presigned URLs, not public objects.