leo.dev
infra

전체 공개였던 S3 버킷 막기

2026.04.173 min read awss3iamsecurity

버킷명·IAM 사용자명·이메일 등 식별자는 예시로 바꿔 적는다.

구버전 서비스를 종료하면서 버킷 상태를 확인했다. 구버전 리소스 버킷 두 개의 정책이 Principal: "*" + Action: s3:*였다. 인터넷 누구나 읽기·쓰기·삭제가 가능한 상태였다.

버킷 구조 파악 먼저

계획 문서에 적힌 버킷명이 실제와 달랐다. 구버전과 신버전이 완전히 다른 버킷을 쓰고 있었다.

버킷(예시)용도서비스
legacy-dev-files문서·첨부파일구버전 (종료)
legacy-prod-files문서·첨부파일구버전 (종료)
app-files-devpresigned 파일 업로드신버전 dev
app-files-prodpresigned 파일 업로드신버전 prod
app-terms약관 HTML 퍼블릭 서빙신버전 공용

버킷명은 계획서가 아니라 실제 .envAWS_S3_PRIVATE_BUCKET, AWS_S3_TERMS_BUCKET 값으로 확인해야 한다.

수행한 작업

Block Public Access 활성화

Terminal window
aws s3api put-public-access-block \
--bucket legacy-prod-files \
--public-access-block-configuration \
"BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"

ACL이나 버킷 정책으로 오브젝트가 공개되는 걸 AWS 레벨에서 원천 차단한다. dev 버킷에도 동일하게 적용.

버킷 정책 교체 — HTTPS-only

기존 정책(Principal: "*" + Action: s3:*)을 제거하고, HTTP 평문 요청만 거부하는 정책으로 교체했다.

버킷 정책
{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Condition": { "Bool": { "aws:SecureTransport": "false" } }
}

IAM 인증 요청은 그대로 허용하고, HTTP로 오는 요청만 거부한다.

Lifecycle Rule — 미완료 멀티파트 7일 자동 삭제

Terminal window
aws s3api put-bucket-lifecycle-configuration \
--bucket legacy-prod-files \
--lifecycle-configuration '{
"Rules": [{
"ID": "cleanup-incomplete-multipart",
"Status": "Enabled",
"Filter": {"Prefix": ""},
"AbortIncompleteMultipartUpload": {"DaysAfterInitiation": 7}
}]
}'

업로드 중단으로 남은 멀티파트 조각이 무기한 과금되는 걸 막는다.

IAM 최소권한 교체

구버전 전용 IAM 사용자에서 AmazonS3FullAccess를 분리하고, PutObject·GetObject·DeleteObject 3개만 허용하는 inline policy로 교체했다.

서비스 계정에는 딱 필요한 리소스·액션만 준다. 키가 유출돼도 피해 범위가 그 버킷으로 한정된다. 반대로 AdministratorAccess 같은 광범위 권한을 서비스 계정 키에 붙이면, 키 하나 유출에 AWS 계정 전체가 날아간다. CLI 관리용 사용자와 백엔드 서비스용 사용자는 분리하는 게 맞다.

least-privilege inline policy (예시)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "S3Access",
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject"],
"Resource": [
"arn:aws:s3:::app-files-dev/*",
"arn:aws:s3:::app-files-prod/*",
"arn:aws:s3:::app-terms/*"
]
},
{
"Sid": "SESEmailSend",
"Effect": "Allow",
"Action": ["ses:SendEmail", "ses:SendRawEmail"],
"Resource": ["arn:aws:ses:<region>:*:identity/no-reply@example.com"]
}
]
}

신버전에 미치는 영향

구버전 버킷은 신버전 백엔드가 쓰지 않는다. 신버전은 app-files-*를 사용하며, 이 버킷은 원래부터 Block Public Access가 모두 ON이고 버킷 정책이 없다. 변경하지 않았으므로 동작에 영향 없다.

app-terms는 약관 HTML을 퍼블릭 URL로 서빙해야 하므로 의도적으로 Block Public Access가 OFF다. 버킷 정책은 Principal: "*" + Action: s3:GetObject로 읽기 전용 공개만 허용한다. 이것도 건드리지 않았다.

교훈

계획 문서의 버킷명을 그대로 믿으면 안 된다. 실제 .env를 열어 AWS_S3_PRIVATE_BUCKET 값을 확인하는 게 정답이다. 이번에 계획서와 실제가 달라서 작업 전 구조 파악에 시간을 썼다.

퍼블릭 서빙이 필요한 버킷(app-terms)은 private 버킷과 반드시 분리한다. 하나에 섞으면 Block Public Access 설정이 충돌한다.

서비스 계정 권한은 최소로 좁힌다. 광범위 권한, 특히 AdministratorAccess는 서비스 키에 붙이지 않는다.

↑↓ 이동 열기esc 닫기