버킷명·IAM 사용자명·이메일 등 식별자는 예시로 바꿔 적는다.
구버전 서비스를 종료하면서 버킷 상태를 확인했다. 구버전 리소스 버킷 두 개의 정책이 Principal: "*" + Action: s3:*였다. 인터넷 누구나 읽기·쓰기·삭제가 가능한 상태였다.
버킷 구조 파악 먼저
계획 문서에 적힌 버킷명이 실제와 달랐다. 구버전과 신버전이 완전히 다른 버킷을 쓰고 있었다.
| 버킷(예시) | 용도 | 서비스 |
|---|---|---|
legacy-dev-files | 문서·첨부파일 | 구버전 (종료) |
legacy-prod-files | 문서·첨부파일 | 구버전 (종료) |
app-files-dev | presigned 파일 업로드 | 신버전 dev |
app-files-prod | presigned 파일 업로드 | 신버전 prod |
app-terms | 약관 HTML 퍼블릭 서빙 | 신버전 공용 |
버킷명은 계획서가 아니라 실제 .env의 AWS_S3_PRIVATE_BUCKET, AWS_S3_TERMS_BUCKET 값으로 확인해야 한다.
수행한 작업
Block Public Access 활성화
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일 자동 삭제
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 관리용 사용자와 백엔드 서비스용 사용자는 분리하는 게 맞다.
{ "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는 서비스 키에 붙이지 않는다.