1. Important Points#
Amazon S3 是 object storage,用 bucket 承载 namespace,用 object key 定位对象。它适合保存文件、日志、备份、静态资源、数据湖对象、Terraform state;不适合当作低延迟文件系统、强事务数据库、POSIX shared volume。
S3 的核心对象是 object,不是 file。Object key 可以写成 logs/2026/07/app.log,但 / 只是 key 里的字符,folder 是 console 和工具展示出来的视图。
Bucket name 是全局 namespace 的一部分,创建后不能改名;object key 是 bucket 内的完整名字,prefix 设计会影响 IAM boundary、lifecycle、list/debug、数据湖分区和清理策略。
S3 的 access control 比很多 AWS service 更特殊:它同时支持 identity-based policy 和 resource-based policy。Bucket policy / access point policy 可以直接写 Principal,所以跨账号共享、public access、CloudFront OAC、VPC endpoint 限制通常都在 bucket policy 里表达。
S3 权限不是只看 IAM role。一次请求可能同时被 IAM policy、bucket policy、access point policy、ACL、Block Public Access、VPC endpoint policy、SCP/RCP、KMS key policy 影响;explicit deny 优先,SSE-KMS 对象还需要 KMS 权限。
现代 S3 应默认使用 Object Ownership = Bucket owner enforced,让 ACL disabled,权限主要回到 IAM policy + bucket/access point policy。ACL 是 legacy 机制,除非兼容旧系统,不要把 ACL 当主权限模型。
S3 默认不是 public storage。生产 bucket 应默认 Block Public Access、Bucket owner enforced、SSE、deny insecure transport。真正需要 public read 时,也要让它成为显式设计,而不是某个 ACL 或 bucket policy 漏出来的结果。
Versioning 解决误删、误覆盖、程序 bug 写坏对象后的恢复问题;Object Lock 是 WORM retention,用于审计、合规、证据保全,不是 Terraform state lock。
Lifecycle 是 S3 成本控制的一部分。开启 versioning 后如果没有 noncurrent version 清理策略,长期成本会被历史版本和未完成 multipart upload 放大。
Bucket name must be boring, unique, and non-sensitive. Object key should be generated by backend, not trusted from user input. Enable versioning for state/log/backup/critical config buckets, pair versioning with lifecycle policy, and use CloudTrail data events for sensitive buckets.2. S3 Naming Convention#
S3 naming 分两层:bucket name 负责全局唯一和 ownership signal,object key 负责业务路径、权限边界、生命周期和查询习惯。
bucket rules:
3-63 chars
lowercase letters / numbers / hyphen / period
begin and end with letter or number
globally unique within AWS partition
cannot be renamed
do not include sensitive data
avoid dots unless static website hosting really needs them
bucket pattern:
<org>-<app>-<env>-<purpose>-<region>
<org>-<app>-<env>-<purpose>-<account-id>-<region>
examples:
acme-order-prod-uploads-ap-east-1
acme-platform-prod-terraform-state-333333333333-ap-east-1object key rules:
key is case-sensitive
/ is delimiter convention, not real directory
use stable prefixes for IAM, lifecycle, analytics, and cleanup
use UUID/ULID/hash suffix for user uploads
avoid raw user filename as the full key
avoid standalone . or .. path segments
key pattern:
<domain>/<entity>/<partition>/<object-name>
examples:
uploads/user-123/2026/07/02/9f3c2a1e.png
reports/daily/dt=2026-07-02/orders.csv
events/source=checkout/dt=2026-07-02/hour=13/part-0001.jsonl
terraform/prod/network/terraform.tfstateBrowser upload 的 key 应由 backend 生成:
good:
uploads/<user-id>/<uuid>.<ext>
bad:
frontend sends arbitrary full S3 key
original filename is trusted as storage path3. S3 Versioning#
S3 Versioning 在同一个 bucket 里保留同一个 object key 的多个版本。它主要解决 accidental overwrite、accidental delete、application bug 覆盖对象后的恢复问题。
适合:
Terraform state bucket
audit / compliance logs
backup artifacts
release artifacts
user uploaded file recovery
critical configuration files
不适合单独解决:
ransomware full recovery without backup isolation
regulatory WORM retention
lifecycle cost control
cross-region disaster recoverystates:
Unversioned:
default state for new bucket
Enabled:
new objects get version IDs
overwrite creates a new version
delete current object creates a delete marker
Suspended:
bucket does not return to unversioned
old versions remain
new objects use null version IDEnable versioning:
aws s3api put-bucket-versioning \
--bucket acme-app-prod-data-ap-east-1 \
--versioning-configuration Status=EnabledCheck status:
aws s3api get-bucket-versioning \
--bucket acme-app-prod-data-ap-east-1List versions:
aws s3api list-object-versions \
--bucket acme-app-prod-data-ap-east-1 \
--prefix configs/app.yamlRestore a previous version:
aws s3api copy-object \
--bucket acme-app-prod-data-ap-east-1 \
--copy-source "acme-app-prod-data-ap-east-1/configs/app.yaml?versionId=3HL4kqtJlcpXroDTDmJ+rmSpXd3dIbrHY" \
--key configs/app.yamlRemove a delete marker:
aws s3api delete-object \
--bucket acme-app-prod-data-ap-east-1 \
--key configs/app.yaml \
--version-id "delete-marker-version-id"Lifecycle for versioned bucket:
{
"Rules": [
{
"ID": "expire-noncurrent-versions",
"Status": "Enabled",
"Filter": {
"Prefix": ""
},
"NoncurrentVersionExpiration": {
"NoncurrentDays": 90
},
"AbortIncompleteMultipartUpload": {
"DaysAfterInitiation": 7
}
}
]
}aws s3api put-bucket-lifecycle-configuration \
--bucket acme-app-prod-data-ap-east-1 \
--lifecycle-configuration file://lifecycle.jsonFor audit / legal / finance logs, validate retention requirements before expiring noncurrent versions.
4. S3 Object Lock#
S3 Object Lock 是 WORM protection:write once, read many。它保护 object versions 在 retention window 内不被删除或覆盖。
Object Lock is for:
regulatory retention
audit log immutability
security evidence
compliance archives
Object Lock is not for:
Terraform S3 backend state locking
normal application concurrency control
short-lived lock files
cache objects| Mode | Meaning | Use |
|---|---|---|
GOVERNANCE |
privileged user can bypass with explicit permission/header | internal protection, first rollout |
COMPLIANCE |
retention cannot be shortened or bypassed | regulatory WORM only |
retention period:
fixed until RetainUntilDate
can be default bucket retention or object-level retention
legal hold:
no expiration date
independent of retention period
must be explicitly removed
Terraform state:
use_lockfile creates a normal .tflock object
do not Object-Lock .tflock objectsCreate a new Object Lock bucket:
aws s3api create-bucket \
--bucket acme-audit-log-333333333333-ap-east-1 \
--region ap-east-1 \
--create-bucket-configuration LocationConstraint=ap-east-1 \
--object-lock-enabled-for-bucketApply default retention:
aws s3api put-object-lock-configuration \
--bucket acme-audit-log-333333333333-ap-east-1 \
--object-lock-configuration '{"ObjectLockEnabled":"Enabled","Rule":{"DefaultRetention":{"Mode":"GOVERNANCE","Days":30}}}'Set retention on one object version:
aws s3api put-object-retention \
--bucket acme-audit-log-333333333333-ap-east-1 \
--key cloudtrail/AWSLogs/333333333333/example.json.gz \
--version-id "3HL4kqtJlcpXroDTDmJ+rmSpXd3dIbrHY" \
--retention '{"Mode":"GOVERNANCE","RetainUntilDate":"2026-12-31T00:00:00Z"}'Legal hold:
aws s3api put-object-legal-hold \
--bucket acme-audit-log-333333333333-ap-east-1 \
--key cloudtrail/AWSLogs/333333333333/example.json.gz \
--version-id "3HL4kqtJlcpXroDTDmJ+rmSpXd3dIbrHY" \
--legal-hold Status=ONextra permissions:
s3:GetObjectRetention / s3:PutObjectRetention
s3:GetObjectLegalHold / s3:PutObjectLegalHold
s3:GetBucketObjectLockConfiguration
s3:PutBucketObjectLockConfiguration
dangerous:
s3:BypassGovernanceRetention
only for tightly controlled break-glass role5. Bucket Policy#
Bucket policy 是 bucket 上的 resource-based permission。它适合做 bucket-level guardrail、cross-account access、explicit deny。
S3 policy 特殊点:
bucket policy / access point policy 是 resource-based policy,必须写 Principal
identity policy attached to IAM role/user answers "who can call what"
bucket policy answers "who can access this bucket/object under what condition"
bucket-level action uses bucket ARN: arn:aws:s3:::bucket
object-level action uses object ARN: arn:aws:s3:::bucket/prefix/*
cross-account access often needs both sides to be correct: caller identity permissions + bucket resource policy
Block Public Access can override public bucket policy / ACL
Object Ownership = Bucket owner enforced disables ACL authorization
SSE-KMS object access also needs KMS key policy / grant / IAM permission
VPC endpoint policy can still deny a request even when IAM and bucket policy allow itRecommended baseline:
normal private bucket:
keep Block Public Access enabled
keep Object Ownership = Bucket owner enforced
use bucket policy for explicit deny guardrails
use IAM identity policy for application role permissions
avoid ACL-based sharing
cross-account sharing:
prefer bucket policy or access point policy
name exact role/account principals
use prefix-scoped resources where possible
test with the real caller role, not only bucket owner credentialsdeny insecure transport#
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::amzn-s3-demo-bucket",
"arn:aws:s3:::amzn-s3-demo-bucket/*"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
}
]
}restrict access to one VPC endpoint#
Only use this when all legitimate access goes through that S3 VPC endpoint. It can block console, CI, and cross-account automation.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyAccessOutsideSpecificVpce",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::amzn-s3-demo-bucket",
"arn:aws:s3:::amzn-s3-demo-bucket/*"
],
"Condition": {
"StringNotEquals": {
"aws:SourceVpce": "vpce-0abcdef1234567890"
}
}
}
]
}allow specific account principals#
Use aws:PrincipalAccount as a guardrail. It does not replace identity IAM permissions.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyPrincipalOutsideAccount",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::amzn-s3-demo-bucket",
"arn:aws:s3:::amzn-s3-demo-bucket/*"
],
"Condition": {
"StringNotEquals": {
"aws:PrincipalAccount": "333333333333"
}
}
}
]
}6. CORS And Presigned Upload#
Browser direct upload 通常是 backend 生成 short-lived presigned PUT URL,browser 直接上传到 S3。CORS 只决定 browser 是否允许发请求;IAM / bucket policy / signature 才决定 S3 是否接受请求。
good fit:
app server should not proxy large file body
backend controls bucket, key prefix, content type, expiry
frontend uploads directly to S3
not enough:
user can choose arbitrary object key
object must be virus-scanned before readable
size/content validation only exists in frontendTemporary download URL:
aws s3 presign s3://acme-order-prod-uploads-ap-east-1/reports/2026-06-summary.csv \
--expires-in 300 \
--region ap-east-1Production browser upload usually needs SDK-generated presigned PUT URL, not aws s3 presign.
Bucket CORS:
[
{
"AllowedOrigins": [
"https://app.example.com"
],
"AllowedMethods": [
"PUT",
"GET",
"HEAD"
],
"AllowedHeaders": [
"content-type",
"x-amz-meta-*",
"x-amz-server-side-encryption"
],
"ExposeHeaders": [
"ETag"
],
"MaxAgeSeconds": 3000
}
]aws s3api put-bucket-cors \
--bucket acme-order-prod-uploads-ap-east-1 \
--cors-configuration file://cors.jsonCORS rules:
AllowedOrigins should be exact production origins
AllowedMethods needs PUT for upload
AllowedHeaders must include headers the browser sends
if presigned URL signs Content-Type, browser must send the same Content-Type
upload works with curl but not browser usually means CORS, not IAMIAM for presign service:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowPresignUploadPrefix",
"Effect": "Allow",
"Action": [
"s3:PutObject"
],
"Resource": "arn:aws:s3:::acme-order-prod-uploads-ap-east-1/uploads/*"
}
]
}Backend presign code:
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presignerimport { randomUUID } from "node:crypto";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const s3 = new S3Client({ region: "ap-east-1" });
const bucket = "acme-order-prod-uploads-ap-east-1";
export async function createUploadUrl({ userId, fileName, contentType }) {
if (!contentType.startsWith("image/")) {
throw new Error("only image upload is allowed");
}
const ext = fileName.split(".").pop()?.toLowerCase() || "bin";
const key = `uploads/${userId}/${randomUUID()}.${ext}`;
const command = new PutObjectCommand({
Bucket: bucket,
Key: key,
ContentType: contentType,
ServerSideEncryption: "AES256",
Metadata: {
"uploaded-by": userId
}
});
const uploadUrl = await getSignedUrl(s3, command, {
expiresIn: 300
});
return { bucket, key, uploadUrl, expiresIn: 300 };
}Browser PUT must send headers that match signed headers:
await fetch(uploadUrl, {
method: "PUT",
headers: {
"content-type": file.type,
"x-amz-server-side-encryption": "AES256"
},
body: file
});Common mistakes:
| Symptom | Check |
|---|---|
| browser preflight fails | bucket CORS AllowedOrigins, AllowedMethods, AllowedHeaders |
SignatureDoesNotMatch |
browser headers do not match signed headers, especially content-type |
AccessDenied |
presign role lacks s3:PutObject or bucket policy denies request |
| upload works with curl but not browser | CORS issue |
| object uploaded to wrong prefix | backend trusted user-provided key |
| public read unexpectedly works | bucket policy / ACL / public access block |
7. VPC Endpoint#
Private subnet access to S3 normally uses S3 Gateway VPC Endpoint. It avoids NAT Gateway data processing cost and keeps traffic on AWS network paths.
Gateway endpoint:
route table target for S3 prefix list
no hourly charge
common choice for EC2/ECS/EKS private subnet access to S3
Interface endpoint:
PrivateLink ENI
useful for some specific S3 APIs / on-prem private connectivity cases
has hourly and data processing costCheck endpoint:
aws ec2 describe-vpc-endpoints \
--filters Name=service-name,Values=com.amazonaws.ap-east-1.s3Check prefix list:
aws ec2 describe-prefix-lists \
--filters Name=prefix-list-name,Values=com.amazonaws.ap-east-1.s38. Operational Checklist#
normal private application bucket:
block public access enabled
Object Ownership = Bucket owner enforced
versioning enabled if recovery matters
lifecycle for noncurrent versions and incomplete multipart uploads
deny insecure transport
SSE enabled
least-privilege IAM policy
CloudTrail data events if sensitive
Terraform state bucket:
versioning enabled
S3 Object Lock not required for use_lockfile
do not Object-Lock .tflock objects
state access limited to Terraform operators/roles
audit/compliance log bucket:
versioning enabled
consider Object Lock
validate Governance vs Compliance with compliance owner
tightly control s3:BypassGovernanceRetention
lifecycle only after retention review
browser upload bucket:
backend generates key and presigned URL
CORS origin/header/method list is narrow
upload prefix is isolated by IAM
public read is disabled unless explicitly required