S3 API Usage - Working with MinIO via aws CLI and mc
Status: Active
Last Updated: 2026-08-26
Category: Cloud - Object Storage Operations
Prerequisites: minio-setup, cloud-storage-concepts
Time: 2-3 hours
Tags: s3, minio, aws-cli, mc, presigned-urls, lifecycle
Summary
Hands-on guide to speaking the S3 API against the fogserv.cloud MinIO deployment: configuring both the aws CLI and MinIO's native mc client, bucket and object operations, access policies, time-limited presigned URLs, and lifecycle rules for automatic expiration. Everything here works identically against AWS S3 later โ that portability is why S3 is the lingua franca of storage.
๐ฏ What You'll Learn
By the end of this article, you'll be able to:
- โ
Configure
awsandmcprofiles pointing at MinIO - โ Create buckets, upload/download objects, and sync directories
- โ Write least-privilege bucket policies and create users with scoped policies
- โ Generate presigned URLs for controlled temporary access
- โ Apply lifecycle rules to expire or transition objects automatically
Table of Contents
- Why the S3 API Matters
- Client Setup: aws CLI
- Client Setup: mc
- Everyday Object Operations
- Policies & Access Control
- Presigned URLs
- Lifecycle Rules
Context / Why This Matters
MinIO gets the server running; this article is how everything else in the stack talks to it. Restic uses S3 as a backend (backup-to-object-storage), application code can use any S3 SDK, and scripts use aws or mc. Learning the API once pays off across every tier described in storage-backup-strategies.
Implementation / Core Content
Client Setup: aws CLI
sudo apt install awscli # or pipx install awscli
aws configure # then enter credentials interactively
For MinIO, the endpoint must be passed on every call (or set per-profile):
# ~/.aws/credentials
[minio]
aws_access_key_id = YOURACCESSKEY
aws_secret_access_key = your-secret-key
# ~/.aws/config
[profile minio]
region = us-east-1 # MinIO ignores it but the CLI requires one
s3 =
addressing_style = path # required: no virtual-host DNS on internal names
endpoint_url = http://minio.internal:9000
Usage:
aws --profile minio s3 ls
aws --profile minio s3 ls s3://backups/
Note: older aws-cli v1 does not support endpoint_url in config; pass --endpoint-url http://minio.internal:9000 explicitly instead.
Client Setup: mc
mc is MinIO's purpose-built admin-and-file client. It understands both S3 semantics and MinIO-specific administration.
curl -o /usr/local/bin/mc https://dl.min.io/client/mc/release/linux-amd64/mc \
&& chmod +x /usr/local/bin/mc
# Register an alias ("remote") once:
mc alias set local http://minio.internal:9000 YOURACCESSKEY your-secret-key
mc alias list
Everything else becomes mc <verb> alias/...:
mc ls local # list buckets
mc mb local/photos # make bucket
mc rb local/old-bucket # remove bucket
mc admin info local # server health, drives, uptime
Everyday Object Operations
Both clients, side by side:
| Task | aws cli | mc |
|---|---|---|
| List objects | aws s3api list-objects-v2 --bucket photos |
mc ls local/photos --recursive |
| Upload file | aws s3 cp f.jpg s3://photos/f.jpg |
mc cp f.jpg local/photos/ |
| Download | aws s3 cp s3://photos/f.jpg . |
mc cp local/photos/f.jpg . |
| Mirror dir | aws s3 sync ./dir s3://photos/dir |
mc mirror ./dir local/photos/dir |
| Delete | aws s3 rm s3://photos/f.jpg |
mc rm local/photos/f.jpg |
| Disk usage | aws s3 ls --recursive --summarize s3://photos | tail -1 |
mc du local/photos |
Tips:
- Prefer path-style addressing everywhere (set in config above); virtual-host style fails on internal hostnames.
mc mirror --watchruns a continuous sync daemon โ handy for Tier-2 replication jobs.- Add
--checksum/ verify large transfers by comparing sizes:mc stat local/photos/f.jpg.
Server-side copy between buckets (no client bandwidth):
aws s3api copy-object \
--bucket archive --key 2026/report.pdf \
--copy-source photos/2026/report.pdf
Policies & Access Control
MinIO users are distinct from root credentials. Principle: applications get their own user with the narrowest policy that works.
# Create a user + key pair
mc admin user add local backup-agent 'S3curePassw0rd'
# Attach a built-in policy...
mc admin policy attach local readwrite --user backup-agent
# ...or better: define a custom least-privilege one
cat > backup-policy.json <<'EOF'
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:GetObject", "s3:ListBucket",
"s3:DeleteObject", "s3:GetBucketLocation"],
"Resource": [
"arn:aws:s3:::backups",
"arn:aws:s3:::backups/*"
]
}]
}
EOF
mc admin policy create local backup-policy backup-policy.json
mc admin policy attach local backup-policy --user backup-agent
Anonymous public read for a specific prefix (e.g., static assets):
mc anonymous set download local/public-assets
mc anonymous set none local/public-assets # revoke
Audit what a user can actually do:
mc admin user info local backup-agent
Presigned URLs
Presigned URLs embed authentication in the query string so an unauthenticated client can perform exactly one operation until expiry โ ideal for giving a phone app, a colleague, or a CI job temporary upload/download rights without sharing keys.
# Download link, valid 1 hour
mc share download --expire 1h local/photos/vacation.zip
# Upload-capable URL
mc share upload --expire 24h local/incoming/report.pdf
# Client uploads with: curl -T report.pdf '<returned-url>'
# Same via aws cli
aws s3 presign s3://photos/vacation.zip --expires-in 3600
Rules:
- Expiry maxes out at 7 days (signature v4 limitation tied to static credentials).
- The URL grants whatever permissions the signing identity has โ sign with the scoped user, never root.
- Anyone holding the URL has access until expiry; treat leaked URLs as compromised and rotate the underlying access key to invalidate them.
Lifecycle Rules
Lifecycle rules age data out without cron jobs. Classic patterns:
# Expire noncurrent versions after 30 days (bucket versioning enabled)
mc ilm rule add local/backups \
--noncurrent-expire-days 30
# Expire objects under logs/ after 14 days
mc ilm rule add local/appdata --prefix "logs/" --expire-days 14
# Transition to a colder remote tier (tiering via ILM)
mc admin tier add remote minio local COLD http://coldstore:9000 cold-bucket ACCESSKEY SECRETKEY
mc ilm rule add local/backups --transition-days 90 --transition-tier COLD
# Inspect & manage
mc ilm rule ls local/backups
mc ilm rule rm local/backups --id "<rule-id>"
The equivalent JSON document via aws s3api put-bucket-lifecycle-configuration also works โ MinIO implements the standard S3 lifecycle schema.
Practical Examples
Example 1: Provision a bucket for a new restic repo
mc mb local/restic-nextcloud
mc version enable local/restic-nextcloud # ransomware safety net
mc quota set local/restic-nextcloud --size 500GiB
mc admin user add local nc-backup '<generated-password>'
mc admin policy attach local backup-policy --user nc-backup
Then point restic at s3:http://minio.internal:9000/restic-nextcloud using this user's keys (see backup-to-object-storage).
Example 2: One-off secure handoff of a large file
mc cp big-dump.tar.zst local/incoming/
mc share download --expire 24h local/incoming/big-dump.tar.zst
# Send the printed URL over chat; recipient downloads without accounts.
mc rm local/incoming/big-dump.tar.zst # clean up afterwards
Example 3: Verify lifecycle behavior
mc ilm rule ls local/appdata
mc ilm restore ls local/appdata # check transitions
# Force-check by inspecting object timestamps vs rules:
mc ls --recursive local/appdata/logs/ | head
Lifecycle evaluation runs periodically (roughly daily), not instantly โ don't panic when test objects survive an hour.
Troubleshooting & Common Pitfalls
| Problem | Cause | Fix |
|---|---|---|
SignatureDoesNotMatch with correct-looking keys |
Clock skew >15 min between client and MinIO | Fix NTP on both hosts |
NoSuchBucket although listed earlier |
Wrong alias/endpoint, or typo'd path-style | mc alias list; always path-style |
Virtual-host style errors (bucket.host unresolvable) |
Default DNS addressing on internal names | Set addressing_style = path |
| Presigned URL rejected immediately | Signed with different endpoint than the one used to fetch | Use the identical hostname/port in the URL |
| Lifecycle rule seems ignored | Evaluation is periodic; prefix mismatch | Wait up to 24 h; verify exact prefix casing |
| Backup agent can read other buckets | Attached broad readwrite canned policy |
Custom scoped policy like backup-policy.json |
| Deleted objects reappear in listing | Bucket versioning shows old versions | List with versions (mc ls --versions); rely on ILM noncurrent expiry |
Next Steps / Ops Actions
- Point restic at a dedicated, versioned bucket: backup-to-object-storage
- Give every application its own scoped MinIO user โ audit with
mc admin user list - Schedule lifecycle reviews quarterly alongside storage-backup-strategies retention checks
- For TLS in front of MinIO's S3 endpoint: tls-configuration
Sources & Related Articles
External references consulted:
- https://min.io/docs/minio/linux/administrator.html
- https://docs.aws.amazon.com/cli/latest/reference/s3/
- https://min.io/docs/minio/linux/administration/identity-access-management/policy-based-access-control.html
Related knowledge-base articles:
Change Log
2026-08-26
- Initial creation by KB writing session.