Steven's Knowledge

Best Practices

Production object storage - security, encryption, cost, observability, naming, pitfalls, IAM

Best Practices

Object storage is "boring infrastructure" until you wake up to a public-bucket leak, a five-figure egress bill, or a deleted bucket. These habits keep it boring.

Security: Don't Make a Public Bucket Accident

The #1 cloud security incident is "bucket left public." Defenses:

MechanismWhat it does
Block Public Access (BPA) at account levelHard-rejects any policy that would make objects public
BPA at bucket levelSame, per-bucket
Resource policies (bucket policy)Explicit allow/deny; deny wins
Object Ownership: Bucket Owner EnforcedDisables ACLs entirely (the cause of historical leaks)
Periodic scanningAWS Macie / open-source s3-scanner / your own audit

For new accounts: turn on BPA at the account level, set Object Ownership to "Bucket Owner Enforced" on every bucket, deny * Principal in bucket policies. Static-site hosting goes through a CDN with origin auth — the bucket itself stays private.

IAM: Least Privilege

Per-object-storage roles, not "S3 full access":

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": "arn:aws:s3:::user-uploads/*"
    },
    {
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::user-uploads",
      "Condition": {
        "StringLike": { "s3:prefix": "uploads/${aws:userid}/*" }
      }
    }
  ]
}
PrivilegeWho gets it
s3:ListBucketReads bucket-level metadata; surprisingly powerful
s3:GetObjectRead objects
s3:PutObjectWrite
s3:DeleteObjectDelete (consider Deny with MFA for production)
s3:PutBucketPolicyChange bucket policy — admins only
s3:DeleteBucketDrop the entire bucket — admins only, MFA-required

For CI/CD and runtime workloads, use OIDC to assume short-lived roles — never long-lived access keys (see Secrets Management).

Encryption

At Rest

Always on; the question is which key:

OptionNotes
SSE-S3 (AES256)AWS-managed key; free; default for many providers
SSE-KMS (aws:kms)Customer-managed KMS key; auditable, rotatable; small cost
SSE-CYou supply the key per request; awkward; rarely worth it
Client-side encryptionYou encrypt before upload; storage sees only ciphertext; max control, max complexity

For most workloads: SSE-KMS with a customer-managed key. Auditable via CloudTrail; key access can be revoked.

In Transit

Always HTTPS. Bucket policy can enforce it:

{
  "Effect": "Deny",
  "Principal": "*",
  "Action": "s3:*",
  "Resource": ["arn:aws:s3:::mybucket", "arn:aws:s3:::mybucket/*"],
  "Condition": { "Bool": { "aws:SecureTransport": "false" } }
}

Bucket Naming

A naming convention you don't have to think about:

<org>-<env>-<purpose>-<region>
example-prod-uploads-us-east-1
example-staging-logs-eu-west-1
example-prod-backups-ap-southeast-2
  • Org prefix because S3 bucket names are globally unique.
  • Env in name prevents staging/prod confusion at a glance.
  • Purpose so logs aren't mixed with user data.
  • Region suffix so multi-region setups are obvious.

Avoid dots in bucket names — they cause TLS issues with virtual-hosted URLs. Lowercase, hyphens only.

Object Key Design

Keys aren't directories, but they look like them. Two principles:

Don't put hot prefixes at the front

Old S3 partitioned by key prefix. Modern S3 (2018+) auto-partitions and this matters less, but for R2 and other providers, hot prefixes still matter. Pattern:

Bad:   2025-05-21/user-42/event-...     (everyone writes to today's prefix)
OK:    user-42/2025-05-21/event-...     (writes spread across users)
Best:  <hash-prefix>/user-42/...        (truly even distribution)

For uploads from many users, putting user-id first naturally spreads load.

Make keys deterministic where it helps

For idempotent uploads, derive the key from content (sha256:abc...) — same input always produces the same key, accidental re-uploads are no-ops. For per-user uploads, prefix with user-id so per-user IAM policies are easy.

Cost Control

Object storage looks cheap but bills surprise. Watch:

Line itemWhat it means
Storage (per GB-month)The big bucket of bytes
Requests (per 1000 reads/writes)Many small files cost more than fewer big ones
Egress (per GB)Often the biggest line; near-zero on R2, hefty on S3
Inter-region transferReplication, cross-region access
Storage class transitionsMoving between tiers costs per object
Restore from GlacierOne-time retrieval fees plus storage

Optimization checklist

  • Lifecycle to IA/Glacier for cold data.
  • Compress before upload if applicable (gzip text, webp images).
  • CloudFront / CDN in front to avoid egress on cache hits.
  • Use R2 if you have heavy distribution traffic.
  • Abort incomplete multipart uploads (lifecycle rule).
  • Delete old versions after a retention window.
  • Right-size storage class — STANDARD for everything is rarely optimal.

For AWS, Storage Lens and Cost Explorer show the breakdown. R2 has its dashboard. Audit monthly.

Observability

SignalWhy
Request error rate (5xx)Provider issue or a misconfigured policy
4xx rateOften misconfigured client; could be an attack
LatencyTTFB on GetObject; >100 ms suggests cold tier or distant region
Bytes downloaded / uploadedCost forecasting
Replication lagIf you've enabled cross-region replication
Lifecycle transitionsConfirms rules are running
Object count per prefixQuick capacity check

Most providers ship metrics to their monitoring service (CloudWatch, Cloudflare Analytics). Pipe interesting ones into Prometheus & Grafana for cross-cutting dashboards.

Server access logging (S3 server access logs, R2's analytics) goes to another bucket. Useful for security audits and debugging.

Common Pitfalls

PitfallSymptomFix
Public bucket from a too-broad policyData leak in the newsBlock Public Access; periodic scanning
Streaming uploads through your appApp OOMs under loadPresigned URLs
No lifecycle rulesBill grows linearly foreverSet tier transitions + version cleanup
Many tiny filesDisproportionate request costsBundle small files; or use a real DB
ListObjectsV2 without paginationTruncated resultsUse the paginator
Setting Content-Type based on extension onlytext/html for .html.txt etcSniff or trust client only after validation
Public bucket as CDN originSlow + public exposurePrivate bucket + CDN with OAC
No backup beyond versioningAccount compromise = total lossCross-account / cross-provider mirror
Hardcoded long-lived access keysOne leak = full access foreverOIDC + short-lived credentials
Mixing storage providers without abstractionLock-in painUse S3-compatible SDK; one client across providers

Compliance and Object Lock

For regulated data (PCI, HIPAA, financial records):

  • Object Lock in Compliance mode — objects can't be deleted before retention period, even by root. Use for audit trails and financial records.
  • Object Lock in Governance mode — admins with the right permission can delete; default for "important but not legally locked."
  • Bucket policies that deny delete without MFA.
  • Audit logs (CloudTrail / equivalent) shipped off-account.

Multi-Provider Strategy

For redundancy across providers:

  • Primary bucket on R2 for cheap egress.
  • Replicated to S3 for AWS-side integrations.
  • Cold-copy to Backblaze quarterly for DR / cost.

Tools: rclone for ad-hoc sync, Cyberduck / s3-mirror / various pipelines for production. Storage cost roughly doubles; some peace of mind.

Checklist

Production object storage checklist

  • Block Public Access enabled at account + bucket level
  • Object Ownership set to "Bucket Owner Enforced" (no ACLs)
  • Bucket policies deny non-TLS access
  • Encryption at rest with customer-managed KMS key
  • OIDC short-lived credentials in CI/CD and workloads
  • Lifecycle rules: incomplete multipart abort, old version cleanup, tier transitions
  • Versioning on for buckets holding irreplaceable data
  • Object Lock for compliance-critical data
  • Cross-region or cross-account replication for DR-critical data
  • Server access logs to a separate bucket; shipped to SIEM
  • Periodic public-bucket scan (Macie / s3-scanner / etc)
  • Per-team IAM roles; no s3:* Resource *
  • Naming convention: <org>-<env>-<purpose>-<region>
  • Direct browser uploads use presigned URLs/POST policies
  • CDN in front of buckets serving public traffic
  • Cost monitoring; alert on storage / egress anomalies
  • Documented disaster-recovery procedure with tested restore

On this page