Using AWS S3 From the Terminal With AWS CLI v2: Full Guide

Use AWS S3 from the terminal with AWS CLI v2: install it, then copy, sync, presign, filter and delete objects, use s3api for policies, and target MinIO.

Amazon Simple Storage Service (Amazon S3) is an object storage service that offers industry-leading scalability, data availability, security, and performance.

In this guide we will see how to work with S3 from the terminal using the AWS CLI – the aws command. We will install AWS CLI v2, configure credentials, then go through the everyday operations: creating buckets, copying, syncing, moving and deleting objects, generating presigned URLs, and dropping down to aws s3api for things like bucket policies and versioning. Every command also works against S3-compatible storage such as MinIO by adding --endpoint-url.

Installation

Older versions of this guide installed the CLI with pip. That was AWS CLI v1 (the package is awscli, not aws-cli), and v1 is in maintenance mode. Install AWS CLI v2 from the official bundle instead.

On Linux (works on both x86_64 and ARM, uname -m picks the right zip):

curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-$(uname -m).zip" -o awscliv2.zip
unzip -q awscliv2.zip
sudo ./aws/install
aws --version
aws-cli/2.39.7 Python/3.14.8 Linux/7.2.12-orbstack-00394-gbecdd6130068 exe/aarch64.debian.12

To upgrade later, re-run the installer with sudo ./aws/install --update.

On macOS use Homebrew (brew install awscli) or the .pkg from the AWS docs. If you would rather not install anything, the official Docker image works too:

docker run --rm -it -v ~/.aws:/root/.aws -v "$PWD:/aws" amazon/aws-cli s3 ls

Configure credentials

Create an access key for an IAM user (or use IAM Identity Center) and run:

aws configure
AWS Access Key ID [None]: AKIAIOSFODNN7EXAMPLE
AWS Secret Access Key [None]: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Default region name [None]: us-east-1
Default output format [None]: json

That writes ~/.aws/credentials and ~/.aws/config. For more than one account, add --profile <name> to aws configure and to every command (or export AWS_PROFILE). If your organisation uses SSO, use aws configure sso and aws sso login instead of long-lived keys.

Confirm who you are before touching anything:

aws sts get-caller-identity

aws s3 vs aws s3api

The CLI has two S3 command sets:

  • aws s3 – high-level commands (ls, cp, mv, rm, sync, mb, rb, presign). They work on files and prefixes, handle multipart uploads and parallelism for you, and accept --recursive, --exclude and --include. Use these for day-to-day work.
  • aws s3api – a one-to-one mapping of the S3 REST API (put-bucket-policy, put-bucket-versioning, head-object, list-objects-v2…). Use it for bucket configuration and anything the high-level commands do not expose. Output is JSON, so --query works.

Basic operations

The examples below use a bucket called citizix-demo-bucket. Bucket names are global across all AWS accounts, so pick your own.

Create a bucket

aws s3 mb s3://citizix-demo-bucket --region eu-west-1
make_bucket: citizix-demo-bucket

List buckets and objects

# all buckets in the account
aws s3 ls

# top level of a bucket; "PRE" lines are prefixes (folders)
aws s3 ls s3://citizix-demo-bucket
                           PRE reports/
2026-11-10 08:19:39          7 report.csv

Everything, with sizes and a total:

aws s3 ls s3://citizix-demo-bucket --recursive --human-readable --summarize
2026-11-10 08:19:39    7 Bytes report.csv
2026-11-10 08:19:40    7 Bytes reports/2026/report.csv

Total Objects: 2
   Total Size: 14 Bytes

Copy files to and from a bucket

# upload, keeping the file name
aws s3 cp report.csv s3://citizix-demo-bucket/

# upload to a specific key
aws s3 cp report.csv s3://citizix-demo-bucket/reports/2026/report.csv

# download
aws s3 cp s3://citizix-demo-bucket/report.csv ./downloaded.csv

# copy a whole directory
aws s3 cp ./backups s3://citizix-demo-bucket/backups/ --recursive
upload: ./report.csv to s3://citizix-demo-bucket/report.csv

Useful cp flags: --storage-class STANDARD_IA (or GLACIER_IR, INTELLIGENT_TIERING), --sse AES256 or --sse aws:kms, and --content-type when serving files from a bucket.

Move and delete objects

aws s3 mv s3://citizix-demo-bucket/report.csv s3://citizix-demo-bucket/archive/report.csv

aws s3 rm s3://citizix-demo-bucket/archive/report.csv

# everything under a prefix
aws s3 rm s3://citizix-demo-bucket/site/ --recursive

Sync directories and buckets

sync only copies files that are new or changed (by size and modification time), which makes it the right tool for backups and static site deploys.

# local -> bucket
aws s3 sync site/ s3://citizix-demo-bucket/site/

# bucket -> local
aws s3 sync s3://citizix-demo-bucket/site/ ./site-copy/

# bucket -> bucket (copied server-side)
aws s3 sync s3://citizix-demo-bucket s3://citizix-demo-backup

By default sync never deletes. Add --delete to remove files from the destination that no longer exist in the source, which is exactly the flag that bites people, so pair it with the next section.

Preview with –dryrun

Any cp, mv, rm or sync accepts --dryrun: it prints what it would do and changes nothing.

aws s3 sync site/ s3://citizix-demo-bucket/site/ --exclude "logs/*" --delete --dryrun
(dryrun) upload: site/about.html to s3://citizix-demo-bucket/site/about.html
(dryrun) delete: s3://citizix-demo-bucket/site/index.html

Run the same command again without --dryrun once the list looks right.

Filter with –exclude and –include

Filters apply in order and later ones win. Everything is included by default, so to upload only some files exclude everything first, then include what you want:

# skip the logs directory
aws s3 sync site/ s3://citizix-demo-bucket/site/ --exclude "logs/*"

# only HTML files (preview first)
aws s3 sync site/ s3://citizix-demo-bucket/site/ --exclude "*" --include "*.html" --dryrun

# delete only .tmp files under a prefix
aws s3 rm s3://citizix-demo-bucket/uploads/ --recursive --exclude "*" --include "*.tmp"
(dryrun) upload: site/about.html to s3://citizix-demo-bucket/site/about.html

Patterns are matched against the path relative to the source directory, and you need the quotes so your shell does not expand *.

Share a private object with presign

presign creates a time-limited URL for a private object. Anyone with the link can download it until it expires; no AWS credentials are needed on their side. The default is one hour, the maximum is 7 days (604800 seconds).

aws s3 presign s3://citizix-demo-bucket/archive/report.csv --expires-in 3600

The output is a URL carrying X-Amz-Algorithm, X-Amz-Credential, X-Amz-Expires and X-Amz-Signature query parameters. The URL is signed locally, so it is generated even if the object does not exist; it also stops working early if the credentials that signed it expire (for example temporary SSO credentials).

Bucket settings with s3api

Inspect objects

aws s3api head-object --bucket citizix-demo-bucket --key archive/report.csv
{
    "AcceptRanges": "bytes",
    "LastModified": "2026-11-10T08:19:44+00:00",
    "ContentLength": 7,
    "ETag": "\"a9346fbaf920e99acc512e8dcc57fa3c\"",
    "ContentType": "text/csv",
    "Metadata": {}
}
aws s3api list-objects-v2 --bucket citizix-demo-bucket \
  --query 'Contents[].{Key:Key,Size:Size}' --output table
-------------------------------------
|           ListObjectsV2           |
+---------------------------+-------+
|            Key            | Size  |
+---------------------------+-------+
|  archive/report.csv       |  7    |
|  reports/2026/report.csv  |  7    |
|  site/css/app.css         |  7    |
|  site/index.html          |  3    |
+---------------------------+-------+

Enable versioning

aws s3api put-bucket-versioning --bucket citizix-demo-bucket \
  --versioning-configuration Status=Enabled
aws s3api get-bucket-versioning --bucket citizix-demo-bucket
{
    "Status": "Enabled"
}

Bucket policy

The original version of this post granted s3:* with no principal, which is both invalid in a bucket policy (it needs a Principal) and far too broad. Here is a policy that makes only the site/ prefix publicly readable, saved as policy.json:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicReadForSiteFolder",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::citizix-demo-bucket/site/*"
    }
  ]
}
aws s3api put-bucket-policy --bucket citizix-demo-bucket --policy file://policy.json
aws s3api get-bucket-policy --bucket citizix-demo-bucket --query Policy --output text

On real AWS, new buckets have Block Public Access turned on, so a public policy is rejected with AccessDenied until you relax it for that bucket:

aws s3api put-public-access-block --bucket citizix-demo-bucket \
  --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=false,RestrictPublicBuckets=false

Only do that for buckets that are meant to be public. ACLs are also disabled on new buckets (Object Ownership is “bucket owner enforced”), so get-bucket-acl will just show the owner; use policies rather than ACLs.

Delete a bucket

# fails unless the bucket is empty
aws s3 rb s3://citizix-demo-backup

# delete all objects, then the bucket
aws s3 rb s3://citizix-demo-backup --force
delete: s3://citizix-demo-backup/report.csv
remove_bucket: citizix-demo-backup

Using the CLI with MinIO and other S3-compatible storage

Point the CLI at another endpoint with --endpoint-url. With a local MinIO on port 9000:

export AWS_ACCESS_KEY_ID=minioadmin
export AWS_SECRET_ACCESS_KEY=minioadmin
export AWS_DEFAULT_REGION=us-east-1

aws --endpoint-url http://localhost:9000 s3 mb s3://citizix-demo-bucket
aws --endpoint-url http://localhost:9000 s3 ls

Instead of repeating the flag, set it once with export AWS_ENDPOINT_URL=http://localhost:9000, or per profile in ~/.aws/config:

[profile minio]
region = us-east-1
endpoint_url = http://localhost:9000

Every command in this guide was run against MinIO this way. See How to run MinIO in Docker and Docker Compose to get one running.

Troubleshooting

  • Unable to locate credentials – no keys found. Run aws configure, export AWS_PROFILE, or log in with aws sso login.

  • SignatureDoesNotMatch – the secret key is wrong, or the system clock is off. Check timedatectl.

  • NoSuchBucket – typo in the bucket name, or you are pointing at the wrong endpoint or account.

  • AccessDenied on put-bucket-policy – Block Public Access is on for the bucket or account (see above).

  • BucketNotEmpty even with rb --force – the bucket is versioned. --force deletes the current objects but not old versions and delete markers. Delete the versions (or add a lifecycle rule to expire them) before removing the bucket:

    remove_bucket failed: s3://citizix-demo-bucket An error occurred (BucketNotEmpty) when calling the DeleteBucket operation: The bucket you tried to delete is not empty. You must delete all versions in the bucket.
    
  • PermanentRedirect / 301 – the bucket is in a different region than your default. Pass --region.

comments powered by Disqus
Citizix Ltd
Built with Hugo
Theme Stack designed by Jimmy