Amazon S3 심화 정리 2편. 버킷 정책을 중심으로 S3 보안의 모든 갈래를 회사 출입카드와 금고 비유로 풀어 — 공유 책임 모델, IAM 정책 vs 버킷 정책 vs ACL의 결정적 차이, CORS, 암호화 4종(SSE-S3/SSE-KMS/SSE-C/CSE), Block Public Access, Presigned URL, MFA Delete, Object Lock(WORM), HTTPS 강제 정책, VPC 엔드포인트까지 친절하게 정리.
이 글은 Amazon S3 심화 정리 시리즈의 두 번째 편입니다. 1편에서 S3의 뼈대 — 객체·버킷·키·접두사·스토리지 클래스 — 를 잡았다면, 이번엔 그 사물함에 자물쇠와 출입카드 시스템을 다는 차례예요. 핵심 도구는 버킷 정책입니다. IAM과 함께 S3 보안 설계의 양대 축이라, 이번 편 절반은 버킷 정책을 어떻게 짜는지에 쓰여요.
S3 보안은 시험에서도, 실무에서도 출제·사고 빈도가 가장 높은 영역입니다. 잘못 설정하면 회사 자료가 인터넷에 그대로 노출되거나, 반대로 너무 꽉 잠가서 정작 필요한 사람이 못 들어가는 일이 자주 생겨요. AWS도 S3 보안 모범 사례 문서로 공식 가이드를 따로 두고 있을 정도입니다. 이번 편에서 그 균형을 친절하게 잡아 갑니다.
왜 버킷 정책이 처음엔 어렵게 느껴질까요
이유는 세 가지예요.
첫째, 접근 제어 도구가 너무 많아 보입니다. IAM 정책, 버킷 정책, ACL, Block Public Access, Presigned URL, VPC 엔드포인트 — 비슷해 보이는 단어가 줄지어 나오는데 각자 자리가 다릅니다. "도대체 뭘 언제 써야 하지?" 싶죠.
둘째, 암호화가 4가지나 됩니다. SSE-S3, SSE-KMS, SSE-C, CSE — 알파벳만 살짝 바꾼 이름이 줄지어 나오는데 각자 키를 누가 만들고 누가 보관하느냐가 다릅니다. 이게 헷갈리기 시작하면 답이 안 보여요.
셋째, JSON 정책 문법이 낯섭니다. Effect, Principal, Action, Resource, Condition — 처음 보면 외계어처럼 느껴집니다.
해결법은 한 가지예요. S3 보안 = "회사 출입카드 + 사물함 자물쇠 + 금고 시스템" 이렇게 한 그림으로 잡고 들어가면, 도구마다 어디에 붙는 자물쇠인지가 또렷해집니다. 이 글은 그 비유를 따라 처음부터 풀어 갑니다.
공유 책임 모델 — 누가 어디까지 책임지나요
S3 보안의 출발점이 공유 책임 모델(Shared Responsibility Model) 입니다. AWS가 모든 걸 해 주는 게 아니에요. 책임이 둘로 나뉘어 있습니다.
| 구분 | 책임 영역 |
|---|---|
| AWS 책임 | 저장소 플랫폼 자체 보안, 물리적 인프라, 네트워크, 시설 |
| 고객 책임 | 데이터 보호, 접근 관리, 암호화 구성, 버킷·객체 권한 설정 |
회사 비유로 풀면 — AWS는 건물·전기·소방·경비원을 책임지고, 고객(우리)은 내 사물함에 누가 어떻게 들어올 수 있는지를 책임지는 식이에요. 건물이 튼튼해도 내가 사물함 문을 활짝 열어 두면 그건 내 책임이라는 뜻입니다.
여기서 시험 함정이 하나 있어요. "S3 데이터가 유출됐다 = AWS 잘못" 이라는 보기는 거의 항상 함정이에요. 데이터 유출 사고의 거의 대부분은 고객의 잘못된 권한 설정 때문입니다. AWS는 플랫폼만 책임지고, 누가 들어올 수 있는지는 우리가 정합니다.
우리가 막아야 하는 5가지 위협
S3 보안을 짤 때 머리에 두는 위협 시나리오가 다섯 가지예요.
- 무단 접근(Unauthorized Access) — 잘못된 버킷 정책이나 IAM 구성으로 외부인이 들어옴
- 내부 위협(Insider Threats) — 악의적 직원·계약자가 데이터를 오남용
- 악성코드 및 랜섬웨어(Malware/Ransomware) — 악성 파일을 버킷에 업로드해 다른 시스템 감염
- 데이터 누출(Data Leakage) — 의도치 않은 공개 설정으로 자료가 인터넷에 노출
- 우발적 삭제(Accidental Deletion) — 잘못된 운영으로 데이터 손실
뒤에서 풀어 갈 도구들이 각각 어떤 위협을 막아 주는지 머리에 그려 두면 좋아요. IAM·버킷 정책·ACL은 1·2번, Block Public Access는 4번, MFA Delete·Object Lock·버전 관리는 5번, 암호화는 1·3번을 막는 식이에요.
S3 보안의 7개 레이어
S3 보안은 한 겹이 아니라 여러 레이어가 겹쳐서 작동합니다.
- IAM 정책 및 버킷 정책
- ACL (Access Control List)
- 암호화 (Encryption at rest & in transit)
- Block Public Access
- MFA Delete
- Object Lock
- VPC 엔드포인트
이번 글은 이 7개 레이어를 순서대로 돌면서 "이건 어디 붙는 자물쇠고, 언제 쓰는지"를 풀어 갑니다.
IAM — 회사 출입카드와 권한 발급 부서
IAM(Identity and Access Management) 은 AWS 리소스에 대한 접근 제어 서비스예요. S3에만 쓰이는 게 아니라 모든 AWS 리소스에 공통으로 깔리는 권한 시스템입니다.
회사 비유로 풀면 — IAM은 회사 출입카드와 권한 발급 부서예요. 누가 어느 사물함을, 어떤 시간대에, 무엇을(읽기·쓰기·삭제) 할 수 있는지 — 이걸 카드에 새겨서 나눠 주는 부서입니다.
IAM은 네 가지 구성 요소로 이루어져요.
- 사용자(User) — 사람 한 명에게 발급되는 정직원 출입카드
- 그룹(Group) — 같은 권한을 묶어서 한 번에 부여 (예: "마케팅팀")
- 역할(Role) — 잠시 빌려 쓰는 임시 통행증. 사람이 아니라 서비스나 다른 계정이 빌려 가요
- 정책(Policy) — 카드에 새겨지는 "어떤 권한을 가졌는가" 명세서
여기서 잠깐, Role(역할)이 가장 헷갈립니다. User는 사람한테 주는 카드인데, Role은 사람이 아니라 EC2·Lambda 같은 서비스가 잠시 빌려 쓰는 통행증이에요. 예를 들어 EC2 인스턴스에 "S3 읽기 권한 Role"을 붙이면, 그 EC2 안에서 도는 코드가 별도 키 없이 S3에 접근할 수 있어요. Role이 만료되면 통행증이 자동으로 회수되는 게 핵심입니다.
IAM 정책 — JSON 출입증
IAM 정책은 JSON 문서예요. 한 번 차근차근 보면 그렇게 어렵지 않습니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowS3BucketList",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::my-specific-bucket"
},
{
"Sid": "AllowS3ObjectAccess",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::my-specific-bucket/*"
}
]
}
읽는 법은 이래요.
- Version — 정책 문법 버전. 거의 항상
"2012-10-17" - Statement — 권한 규칙들의 배열. 여러 개 가능
- Sid — 규칙 이름. 사람이 알아보기 쉽게 붙이는 라벨
- Effect —
Allow(허용) 또는Deny(거부) - Action — 허용·거부할 작업.
s3:GetObject같은 형식 - Resource — 어느 리소스에 적용. ARN(Amazon Resource Name) 형식
여기서 시험 함정이 하나 있어요. 위 정책을 자세히 보면 Resource가 두 줄에 다르게 적혀 있어요.
arn:aws:s3:::my-specific-bucket— 버킷 자체arn:aws:s3:::my-specific-bucket/*— 버킷 안의 모든 객체
이 둘이 다른 리소스입니다. s3:ListBucket(목록 조회)은 버킷 자체에 거는 권한이고, s3:GetObject(객체 읽기)는 객체에 거는 권한이라 각자 다른 ARN을 써야 해요. 끝의 /* 슬래시 별표 빠뜨리면 권한이 안 통합니다.
KMS 키 권한 추가하기
암호화된 객체를 다루려면 S3 권한만으로는 부족해요. KMS(Key Management Service) 키에 대한 권한도 같이 줘야 합니다. 뒤에서 SSE-KMS를 다룰 때 다시 나와요.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowKMSAccess",
"Effect": "Allow",
"Action": [
"kms:Decrypt",
"kms:Encrypt",
"kms:DescribeKey"
],
"Resource": "arn:aws:kms:us-east-1:123456789012:key/key-id"
}
]
}
여기서 시험 함정이 하나 있어요. SSE-KMS 암호화된 객체를 못 읽는다며 권한 문제로 헤매는 사례가 정말 많은데, 원인은 거의 kms:Decrypt 권한 누락이에요. S3에 GetObject 권한이 있어도 키를 풀 권한이 없으면 객체를 못 읽어요.
최소 권한 원칙
IAM 설계의 황금 규칙이 최소 권한 원칙(Principle of Least Privilege) 이에요.
- 사용자·역할에 필요한 최소한의 권한만 부여
- 전체 S3 접근 대신 특정 버킷·작업만 허용
- 정기적인 권한 검토 및 불필요한 권한 제거
회사 비유로 — 모든 직원에게 "사장 카드"를 주면 편하긴 한데 사고 한 번이면 끝장이에요. 자기 사물함만 열 수 있는 카드를 발급하는 게 보안의 기본입니다.
# IAM 사용자 생성
aws iam create-user --user-name s3-user
# 정책 생성
aws iam create-policy \
--policy-name S3ReadOnlyPolicy \
--policy-document file://s3-policy.json
# 사용자에 정책 첨부
aws iam attach-user-policy \
--user-name s3-user \
--policy-arn arn:aws:iam::123456789012:policy/S3ReadOnlyPolicy
버킷 정책 — 사물함 칸 자체에 붙인 출입 규칙
여기서 헷갈리는 부분이 시작됩니다. IAM 정책이랑 버킷 정책(Bucket Policy) 이랑 뭐가 다르냐는 거죠.
핵심 차이를 한 줄로 풀면 — IAM 정책은 카드(주체)에 붙고, 버킷 정책은 사물함 칸(리소스)에 붙어요.
- IAM 정책 — "이 사용자는 어떤 리소스에 접근할 수 있는가" (주체 중심)
- 버킷 정책 — "이 버킷에는 누가 접근할 수 있는가" (리소스 중심)
회사 비유로 — IAM 정책은 직원 카드에 새겨진 권한이고, 버킷 정책은 사물함 칸 자체에 붙어 있는 출입 규칙판이에요. 사물함 옆에 "이 칸은 마케팅팀과 본사 IP 대역만 접근 가능"이라고 적혀 있는 표지판 같은 거죠.
버킷 정책 기본 구조
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "정책 설명 ID",
"Effect": "Allow | Deny",
"Principal": "접근 주체",
"Action": "허용/거부 작업",
"Resource": "적용 리소스",
"Condition": {
"조건 연산자": {
"조건 키": "조건 값"
}
}
}
]
}
IAM 정책과 거의 같은데 Principal 필드가 추가됐어요. 버킷 정책은 리소스에 붙는 거라 "누구를(Principal) 허용·거부하는지"를 명시해야 합니다. IAM 정책은 이미 "이 사용자의 카드"에 붙어 있으니 Principal이 필요 없어요.
자주 나오는 버킷 정책 4종
특정 계정에만 접근 허용
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowSpecificAccount",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::ACCOUNT_ID:root"
},
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
]
}
]
}
다른 AWS 계정 전체에게 접근을 열어주는 정책. 크로스 계정 공유의 가장 흔한 패턴이에요.
HTTPS만 허용 (HTTP 차단)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyNonHTTPS",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
}
]
}
이게 시험과 실무 둘 다 자주 출제되는 정책이에요. 풀어 보면 — "SecureTransport(HTTPS)가 false면(즉, HTTP면) 모두 거부". 결과적으로 HTTPS 트래픽만 허용됩니다. 거의 모든 프로덕션 버킷에 기본으로 까는 정책이에요.
공개 읽기 허용 (정적 웹사이트)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-website-bucket/*"
}
]
}
S3로 정적 웹사이트를 호스팅할 때 쓰는 패턴. **Principal: "*" = 인터넷 누구나, Action: s3:GetObject = 읽기만** 허용. 단, 이 정책을 적용하려면 뒤에 나오는 Block Public Access를 풀어야 해요.
IP 주소 기반 접근 제한
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowFromSpecificIP",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"NotIpAddress": {
"aws:SourceIp": ["203.0.113.0/24", "198.51.100.0/24"]
}
}
}
]
}
회사 IP 대역이 아니면 거부하는 정책. 재택근무 정책 짤 때 함정이 — VPN 안 거치는 직원이 IP에 안 잡혀서 접근 거부될 수 있어요. 도입 전에 IP 풀을 정확히 정리해야 합니다.
# 버킷 정책 적용
aws s3api put-bucket-policy \
--bucket my-bucket \
--policy file://bucket-policy.json
# 버킷 정책 조회
aws s3api get-bucket-policy \
--bucket my-bucket
# 버킷 정책 삭제
aws s3api delete-bucket-policy \
--bucket my-bucket
ACL — 구식 자물쇠 (요즘은 잘 안 씀)
ACL(Access Control List) 은 S3 초창기부터 있던 접근 제어 목록이에요. 버킷이나 개별 객체에 적용해서 다른 AWS 계정 및 그룹에 기본적인 읽기·쓰기 권한을 부여합니다.
회사 비유로 — ACL은 구식 자물쇠예요. 작동은 하는데, 요즘은 거의 다 더 좋은 자물쇠(버킷 정책)로 갈아탔습니다. AWS도 공식적으로 버킷 정책 사용을 권장하고 있어요.
사전 정의된 그룹
ACL에는 미리 정의된 그룹 세 개가 있어요.
| 그룹 | 설명 |
|---|---|
AllUsers | 모든 인터넷 사용자 (익명 포함) |
AuthenticatedUsers | 모든 AWS 인증된 사용자 |
LogDelivery | S3 서버 접근 로그 전달 그룹 |
여기서 시험 함정이 하나 있어요. AuthenticatedUsers 그룹은 "내 계정 사용자"가 아니라 "AWS에 가입된 모든 사용자" 예요. 이걸 모르고 권한을 주면 — 전 세계 AWS 계정 보유자가 다 들어올 수 있어요. 거의 공개와 다름없는 설정입니다.
ACL 권한 유형
| 권한 | 버킷 적용 | 객체 적용 |
|---|---|---|
| READ | 버킷 내 객체 목록 조회 | 객체 데이터 및 메타데이터 읽기 |
| WRITE | 버킷에 객체 생성/덮어쓰기/삭제 | 해당 없음 |
| READ_ACP | 버킷 ACL 읽기 | 객체 ACL 읽기 |
| WRITE_ACP | 버킷 ACL 쓰기 | 객체 ACL 쓰기 |
| FULL_CONTROL | 위 모든 권한 | 위 모든 권한 |
# 버킷 ACL 조회
aws s3api get-bucket-acl --bucket my-bucket
# 객체 ACL 조회
aws s3api get-object-acl --bucket my-bucket --key file.txt
# 객체를 공개로 설정
aws s3api put-object-acl \
--bucket my-bucket \
--key file.txt \
--acl public-read
잠깐, 이 부분이 헷갈리는데 — 세 가지 도구(IAM 정책 / 버킷 정책 / ACL) 중 뭘 써야 하나요? 정답은 다음과 같아요.
- IAM 정책 — 내 계정 안 사용자·역할에 권한을 줄 때 (가장 자주 씀)
- 버킷 정책 — 버킷 자체에 정책 걸 때, 크로스 계정 공유, HTTPS 강제, IP 제한 같은 조건부 정책
- ACL — 거의 안 씀. 옛날 자료나 레거시 시스템 호환 목적
새 프로젝트에서 ACL을 처음부터 끄집어낼 일은 거의 없어요. AWS도 신규 버킷에서는 ACL을 비활성화하는 게 기본값으로 바뀌었습니다.
Block Public Access — 외부 공개 차단 마스터 스위치
Block Public Access(BPA) 는 버킷과 계정 수준에서 공개 접근을 차단하는 마스터 스위치예요. 회사 비유로 — 사물함 시스템 전체에 걸린 "외부 공개 금지" 큰 스위치 같은 거예요. 개별 자물쇠를 잘못 풀어 둬도 이 스위치가 켜져 있으면 외부에서 못 들어옵니다.
4가지 세부 설정
BPA는 4개 옵션으로 나뉘어요.
| 설정 | 설명 |
|---|---|
BlockPublicAcls | 새로운 공개 ACL 추가 차단 |
IgnorePublicAcls | 기존 공개 ACL 무시 |
BlockPublicPolicy | 공개 접근을 허용하는 버킷 정책 추가 차단 |
RestrictPublicBuckets | 기존 공개 버킷 정책 무시 |
Block은 "새로 못 만들게", Ignore/Restrict는 "이미 있는 걸 무시"라고 기억하면 돼요. 권장 설정은 4개 다 활성화입니다.
여기서 시험 함정이 하나 있어요. BPA는 버킷 정책·ACL을 덮어 씁니다. 버킷 정책에서 Principal: "*"로 공개를 허용해도, BPA가 켜져 있으면 무효예요. 그래서 정적 웹사이트 호스팅처럼 진짜 공개가 필요한 버킷에서는 BPA를 풀어야 합니다. 그 외 모든 버킷은 BPA를 켜 두는 게 정답이에요.
계정 수준 vs 버킷 수준
BPA는 계정 수준에서도 켤 수 있어요. 이게 가장 강력한 안전장치입니다 — 계정 수준에서 BPA가 켜져 있으면, 그 계정의 어떤 버킷도 공개 못 됩니다.
# 모든 공개 접근 차단 (버킷 수준)
aws s3api put-public-access-block \
--bucket my-bucket \
--public-access-block-configuration \
"BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"
# 공개 접근 차단 설정 조회
aws s3api get-public-access-block --bucket my-bucket
# 계정 수준 Block Public Access 설정
aws s3control put-public-access-block \
--account-id 123456789012 \
--public-access-block-configuration \
"BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"
암호화 — 자물쇠와 열쇠
암호화(Encryption) 는 데이터에 자물쇠를 거는 작업이에요. 두 종류가 있어요.
- 전송 중 암호화(Encryption in Transit) — 데이터가 이동 중일 때. HTTPS/TLS로 처리. 앞에서 본 HTTPS 강제 정책이 이걸 보장합니다
- 저장 시 암호화(Encryption at Rest) — 데이터가 디스크에 누워 있을 때. 누군가 디스크를 빼 가도 못 읽게 함
저장 시 암호화에는 서버 측(SSE) 3종 + 클라이언트 측(CSE) 1종 = 총 4가지 방식이 있어요.
| 암호화 유형 | 설명 | 키 관리 |
|---|---|---|
| SSE-S3 | S3 관리 키 사용 | AWS가 완전 관리 |
| SSE-KMS | KMS 서비스 키 사용 | AWS KMS에서 관리 |
| SSE-C | 고객 제공 키 사용 | 고객이 직접 관리 |
| CSE | 클라이언트 측 암호화 | 고객이 업로드 전 직접 |
2023년부터 모든 새 버킷에 SSE-S3가 기본값으로 자동 적용됩니다. 그래서 별도 설정 없이도 데이터는 디스크에 암호화돼서 누워 있어요. 그래도 4가지 차이는 알아 둬야 합니다 — 시험에 거의 항상 나와요.
SSE-S3 — AWS가 알아서 잠그고 풀어주는 자동 자물쇠
가장 단순한 방식. AWS가 키를 완전히 관리해요. 우리는 키 신경 쓸 필요가 전혀 없습니다.
- AWS가 키를 완전 관리
- 추가 비용 없음
- 규정 준수 요구사항이 없다면 기본 선택
회사 비유로 — 사물함에 자동 자물쇠가 달려 있어서 AWS가 알아서 잠그고, 권한 있는 사람이 오면 알아서 풀어 주는 식입니다. 가장 편해요.
SSE-KMS — 회사 마스터키 보안실에서 키 관리
AWS KMS(Key Management Service) 라는 별도 서비스가 키를 관리합니다. SSE-S3와의 차이는 — 키에 대한 세밀한 통제와 감사 추적이 가능하다는 점이에요.
- 키 사용 이력 추적 가능 (AWS CloudTrail)
- 고객이 키 정책으로 세밀한 접근 제어 가능
- 버킷 키(Bucket Key)로 KMS API 호출 비용 절감
회사 비유로 — 회사 안에 별도 보안실(KMS) 이 있고, 마스터키들이 거기 보관돼 있어요. 누가 언제 키를 꺼내 썼는지 로그가 다 남고, 키별로 누구한테 빌려줄지 정책을 따로 만들 수 있어요. 규정 준수가 빡빡한 산업(금융·의료)에서 거의 필수예요.
버킷 키 — KMS 비용 폭탄 막기
여기서 시험 함정이 하나 있어요. SSE-KMS는 객체 하나 읽고 쓸 때마다 KMS API를 호출합니다. 객체가 수백만 개면 KMS 요금만으로도 큰 돈이 나가요.
해결책이 버킷 키(Bucket Key) 예요. S3가 KMS 키에서 데이터 키를 한 번 만들어서 버킷에 임시 저장하고, 한동안은 그걸로 처리합니다. 개별 객체마다 KMS 호출을 안 하니까 비용이 확 줄어요. 수백만 객체 버킷에서는 거의 필수.
SSE-C — 내가 직접 가져온 열쇠로 잠금
고객이 직접 암호화 키를 제공합니다. AWS는 키를 저장하지 않아요.
- 고객이 직접 키 제공
- AWS는 키를 저장하지 않음
- HTTPS를 통해서만 사용 가능
- 키를 잃으면 데이터 복구 불가
회사 비유로 — 내가 직접 만든 열쇠를 가져와서 사물함을 잠그는 식입니다. 잠그고 풀 때마다 열쇠를 같이 보내야 하고, AWS는 키를 안 보관해요. 내가 키를 잃으면 끝입니다. AWS도 못 풀어 줘요.
CSE — 내가 미리 잠가서 보내고 AWS도 평문 못 봄
Client-Side Encryption — 데이터를 S3에 업로드하기 전에 고객이 직접 암호화합니다. AWS SDK의 클라이언트 측 암호화 라이브러리를 써요.
회사 비유로 — 자료를 내가 먼저 봉투에 넣고 봉인한 다음 사물함에 넣는 식. AWS는 봉인된 봉투만 봐서, AWS조차 평문을 본 적이 없어요. 가장 강한 비밀 유지가 필요할 때 씁니다.
4가지 비교 — 한 표로 정리
| 방식 | 키 만든 사람 | 키 보관 | AWS가 평문을 보나 |
|---|---|---|---|
| SSE-S3 | AWS | AWS | 네 (내부적으로) |
| SSE-KMS | AWS KMS | AWS KMS | 네 (내부적으로) |
| SSE-C | 고객 | 고객 (전송 시만 AWS) | 네 (잠깐 메모리에) |
| CSE | 고객 | 고객 | 아니요 (봉인된 채 들어옴) |
# SSE-S3로 객체 업로드
aws s3 cp file.txt s3://my-bucket/ \
--sse AES256
# SSE-KMS로 객체 업로드
aws s3 cp file.txt s3://my-bucket/ \
--sse aws:kms \
--sse-kms-key-id arn:aws:kms:us-east-1:123456789012:key/key-id
# 기본 암호화 설정 (버킷 수준)
aws s3api put-bucket-encryption \
--bucket my-bucket \
--server-side-encryption-configuration '{
"Rules": [{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "arn:aws:kms:us-east-1:123456789012:key/key-id"
},
"BucketKeyEnabled": true
}]
}'
# 암호화 설정 확인
aws s3api get-bucket-encryption --bucket my-bucket
CORS — 다른 회사 사람이 내 사물함을 보러 올 때
CORS(Cross-Origin Resource Sharing) 는 한 출처의 웹 애플리케이션이 다른 출처의 리소스에 접근할 수 있도록 허용하는 메커니즘이에요.
회사 비유로 — 다른 회사 사람이 우리 사물함을 보러 올 때 허용 목록이에요. 평소에는 외부 사이트(https://www.example.com)에서 우리 S3 버킷의 자료를 직접 못 가져갑니다(브라우저가 막아요). CORS를 켜서 "이 도메인에서 오는 요청은 허용" 이라고 명시해야 통과됩니다.
언제 필요하느냐 — S3에 정적 웹사이트를 올렸는데, 다른 도메인의 JavaScript에서 이 S3의 파일을 가져와야 할 때. 이 자리에 CORS 설정이 필요해요.
CORS 설정 예시
특정 도메인 허용
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE"],
"AllowedOrigins": ["https://www.example.com"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3000
}
]
각 필드가 뭔지 짚으면:
- AllowedHeaders — 허용할 요청 헤더
- AllowedMethods — 허용할 HTTP 메서드
- AllowedOrigins — 허용할 출처(도메인)
- ExposeHeaders — 브라우저가 읽을 수 있는 응답 헤더
- MaxAgeSeconds — preflight 결과 캐시 시간
모든 도메인 허용 (개발 환경)
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET"],
"AllowedOrigins": ["*"],
"ExposeHeaders": []
}
]
여기서 시험 함정이 하나 있어요. **AllowedOrigins: ["*"]는 개발 환경에서만 쓰세요.** 프로덕션에서 *를 박아 두면 인터넷 누구나 우리 버킷의 자료를 가져갈 수 있어요. 항상 명시적인 도메인을 적는 게 안전합니다.
# CORS 설정 적용
aws s3api put-bucket-cors \
--bucket my-bucket \
--cors-configuration file://cors-config.json
# CORS 설정 조회
aws s3api get-bucket-cors --bucket my-bucket
# CORS 설정 삭제
aws s3api delete-bucket-cors --bucket my-bucket
MFA Delete — 삭제할 때 두 번째 인증 강제
MFA Delete 는 객체 버전 삭제나 버전 관리 상태 변경 시 MFA(Multi-Factor Authentication) 코드를 추가로 요구하는 기능이에요.
회사 비유로 — 평소엔 카드만 찍으면 되는데, 삭제처럼 위험한 작업에는 두 번째 인증(휴대폰 OTP 등)을 추가로 요구하는 식입니다. 우발적·악의적 삭제를 막는 추가 레이어예요.
MFA의 3가지 요소
- 지식 요소 — 암호, PIN
- 소유 요소 — 휴대폰, 토큰 장치, Google Authenticator
- 생체 요소 — 지문, 얼굴 인식
MFA Delete는 보통 소유 요소(OTP) 를 씁니다.
작동 방식
- MFA Delete 비활성화 상태 — 일반적으로 객체 삭제 가능
- MFA Delete 활성화 상태 — 6자리 MFA 코드 없이 삭제 거부
제약사항 — 시험에 자주 나옴
여기서 시험 함정이 하나 있어요. MFA Delete는 제약이 꽤 빡빡합니다.
- 버전 관리 활성화 필수 — 버전 관리 안 켜면 MFA Delete 못 켭니다
- 버킷 소유자(루트 계정)만 활성화 가능 — IAM 사용자는 못 켜요
- AWS CLI 또는 API를 통해서만 구성 가능 — 콘솔 못 씀
- MFA Delete 활성화된 객체는 콘솔에서 삭제 불가
이 4개가 한 묶음으로 시험에 나와요. 특히 "콘솔에서 못 켠다" 가 핵심.
# MFA Delete 활성화 (루트 계정의 MFA 시리얼 번호 및 코드 필요)
aws s3api put-bucket-versioning \
--bucket my-bucket \
--versioning-configuration Status=Enabled,MFADelete=Enabled \
--mfa "arn:aws:iam::123456789012:mfa/root-account-mfa-device MFA_CODE"
# MFA Delete 비활성화
aws s3api put-bucket-versioning \
--bucket my-bucket \
--versioning-configuration Status=Enabled,MFADelete=Disabled \
--mfa "arn:aws:iam::123456789012:mfa/root-account-mfa-device MFA_CODE"
# MFA Delete 활성화 시 객체 버전 삭제
aws s3api delete-object \
--bucket my-bucket \
--key file.txt \
--version-id VERSION_ID \
--mfa "MFA_SERIAL MFA_CODE"
S3 Object Lock — WORM 봉인
S3 Object Lock 은 WORM(Write Once Read Many) 보호를 제공하는 기능이에요. 한 번 넣은 객체는 변경·삭제가 안 되도록 봉인합니다.
회사 비유로 — Object Lock은 봉인 도장이에요. 한 번 자료를 넣고 봉인하면, 정해진 기간 동안은 누구도 — 심지어 회사 사장도 — 자료를 빼거나 바꿀 수 없어요. 금융·의료처럼 법적으로 보존 기간이 정해진 자료에 거의 필수입니다.
특징:
- 새 버킷 생성 시에만 활성화 가능 (기존 버킷은 AWS Support 통해)
- 추가 요금 없음
- 규정 준수(금융, 의료 등), 감사 기록 보존에 활용
여기서 시험 함정이 하나 있어요. 1편에서도 짚었지만 다시 — Object Lock은 버킷 생성 시점에만 켤 수 있어요. 만든 뒤에 "아 이거 켜야겠다" 해도 콘솔에서 못 켭니다. 미리 계획하고 만들어야 해요.
보존 모드 2종
Governance Mode (거버넌스 모드)
- 특별 권한(
s3:BypassGovernanceRetention)이 있는 사용자는 잠금 해제 가능 - 대부분의 사용자는 보호된 기간 동안 삭제·덮어쓰기 불가
회사 비유로 — 봉인 도장을 풀 수 있는 별도 권한이 있는 임원만 풀 수 있는 모드. 일반 직원은 못 건드려요.
Compliance Mode (규정 준수 모드)
- 누구도 보존 기간 동안 객체를 변경하거나 삭제 불가
- 루트 계정도 불가
- 가장 강력한 보호
회사 비유로 — 봉인 도장을 풀 수 있는 사람이 아예 없는 모드. 사장도 못 건드립니다. 법적 의무가 빡빡한 자료에 사용해요.
보존 기간 vs 법적 보류
- 보존 기간(Retention Period) — 객체가 잠금 상태로 유지되어야 하는 기간 지정. 기간이 지나면 잠금 해제
- 법적 보류(Legal Hold) — 보존 기간 없이 무기한 보호.
s3:PutObjectLegalHold권한으로 설정·해제
소송이나 감사 중에 자료 보존이 필요할 때 — 기간을 모르니까 무기한으로 묶어 두는 게 Legal Hold예요.
# Object Lock 버킷 생성
aws s3api create-bucket \
--bucket my-lock-bucket \
--object-lock-enabled-for-bucket
# Object Lock 기본 보존 설정
aws s3api put-object-lock-configuration \
--bucket my-lock-bucket \
--object-lock-configuration '{
"ObjectLockEnabled": "Enabled",
"Rule": {
"DefaultRetention": {
"Mode": "COMPLIANCE",
"Days": 365
}
}
}'
# 객체 업로드 시 Object Lock 설정
aws s3api put-object \
--bucket my-lock-bucket \
--key important-record.pdf \
--body important-record.pdf \
--object-lock-mode COMPLIANCE \
--object-lock-retain-until-date "2025-12-31T00:00:00Z"
# 법적 보류 설정
aws s3api put-object-legal-hold \
--bucket my-lock-bucket \
--key important-record.pdf \
--legal-hold Status=ON
# 법적 보류 해제
aws s3api put-object-legal-hold \
--bucket my-lock-bucket \
--key important-record.pdf \
--legal-hold Status=OFF
Presigned URL — 한 시간만 유효한 일회용 출입 패스
Presigned URL(사전 서명된 URL) 은 임시 접근 URL을 만드는 기능이에요. 버킷을 공개하지 않고도 특정 사용자에게 일시적 접근을 허용할 수 있어요.
회사 비유로 — 한 시간만 유효한 일회용 출입 패스예요. 외부인이 잠시 자료를 가져가야 할 때, 사물함 자체를 공개로 풀 필요 없이 딱 그 사람한테 시간 제한 패스를 발급하는 식입니다. 시간이 지나면 자동 만료.
자주 쓰는 사용 사례
- 파트너사·외부 사용자에게 임시 다운로드 링크 제공
- 애플리케이션이 사용자 대신 임시 업로드 URL 생성 (브라우저에서 직접 S3로 업로드)
- 비디오 스트리밍에서 콘텐츠 일시 접근 제공
URL 유효 기간 — 자격 증명 유형별
여기서 시험 함정이 하나 있어요. Presigned URL의 유효 기간은 URL을 만든 사람의 자격 증명 유형에 따라 상한이 다릅니다.
| 자격 증명 유형 | 최대 유효 기간 |
|---|---|
| IAM 사용자 (콘솔/CLI) | 7일 (604,800초) |
| STS 임시 자격 증명 | 36시간 |
| EC2 인스턴스 프로필 (IAM Role) | 6시간 |
특히 — URL 유효 기간을 7일로 설정해도, EC2 인스턴스 프로필로 만들었으면 6시간 후 만료돼요. 자격 증명이 더 일찍 만료되면 URL도 함께 죽는 거예요. 이걸 모르고 "왜 URL이 일찍 죽지?" 하는 사례가 정말 많습니다.
또 한 가지 — Presigned URL은 만든 사람의 권한을 그대로 상속받습니다. 권한이 없는 객체에 대해 URL을 만들면 그 URL도 작동 안 해요.
# 다운로드용 Presigned URL 생성 (1시간 유효)
aws s3 presign s3://my-bucket/file.txt --expires-in 3600
# 업로드용 Presigned URL 생성 (Python SDK 예시)
# aws s3 presign은 GET만 지원, PUT은 SDK 사용
import boto3
from datetime import datetime
s3_client = boto3.client('s3')
# 다운로드용 Presigned URL
download_url = s3_client.generate_presigned_url(
'get_object',
Params={'Bucket': 'my-bucket', 'Key': 'file.txt'},
ExpiresIn=3600 # 1시간
)
# 업로드용 Presigned URL
upload_url = s3_client.generate_presigned_url(
'put_object',
Params={
'Bucket': 'my-bucket',
'Key': 'upload/new-file.txt',
'ContentType': 'application/pdf'
},
ExpiresIn=3600
)
print(f"Download URL: {download_url}")
print(f"Upload URL: {upload_url}")
VPC 엔드포인트 — 사옥 내 전용 통로
VPC 엔드포인트(VPC Endpoint) 는 VPC 내부 리소스가 인터넷을 거치지 않고 S3에 접근하게 해 주는 기능이에요.
회사 비유로 — 사옥 안에서 사물함 시스템으로 가는 전용 통로입니다. 평소엔 사옥 → 정문 → 인터넷 → S3 데이터센터 이렇게 빙 둘러 갔는데, VPC 엔드포인트를 깔면 사옥 내부 통로로 바로 갑니다. 인터넷 안 거치니까 더 안전하고, 데이터 전송 비용도 줄어요.
두 가지 유형
- 게이트웨이 엔드포인트(Gateway Endpoint) — 라우팅 테이블에 추가. 추가 비용 없음. S3·DynamoDB에서만 사용 가능
- 인터페이스 엔드포인트(Interface Endpoint) — ENI(Elastic Network Interface) 형태. 시간당 요금 있음. 더 많은 서비스 지원
S3에서는 게이트웨이 엔드포인트가 기본 선택이에요. 추가 비용도 없고요.
VPC 엔드포인트만 허용하는 버킷 정책
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowVPCAccess",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"StringEquals": {
"aws:SourceVpce": "vpce-xxxxxxxx"
}
}
}
]
}
이 정책의 핵심은 aws:SourceVpce 조건이에요. 특정 VPC 엔드포인트를 거쳐 들어온 요청만 허용합니다. 인터넷에서 직접 들어오는 요청은 다 막혀요. 보안 강화의 끝판왕 패턴 중 하나입니다.
# VPC 게이트웨이 엔드포인트 생성
aws ec2 create-vpc-endpoint \
--vpc-id vpc-xxxxxxxx \
--service-name com.amazonaws.us-east-1.s3 \
--route-table-ids rtb-xxxxxxxx
# VPC만 접근 허용하는 버킷 정책
aws s3api put-bucket-policy \
--bucket my-secure-bucket \
--policy file://vpc-bucket-policy.json
보안 모범 사례 — 실무 체크리스트
여기까지가 S3 보안 도구 풀세트입니다. 실무에서는 이 도구들을 한 줄로 묶어서 체크리스트로 돌려요. 새 버킷 만들 때 한 번 훑어 보세요.
- [ ] Block Public Access 활성화 (계정 및 버킷 수준)
- [ ] IAM 최소 권한 원칙 적용
- [ ] 모든 데이터에 기본 암호화 적용
- [ ] HTTPS 전용 접근 버킷 정책 적용
- [ ] 중요 데이터에 버전 관리 활성화
- [ ] 중요 데이터에 MFA Delete 활성화
- [ ] 규정 준수 데이터에 Object Lock 사용
- [ ] 서버 접근 로그 활성화
- [ ] CloudTrail 이벤트 로깅 활성화
- [ ] 정기적인 S3 Storage Lens 검토
- [ ] AWS Config로 구성 변경 모니터링
HTTPS 강제 정책 — 프로덕션 기본
거의 모든 프로덕션 버킷에 까는 정책이에요. 한 번 더 적어 둡니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "EnforceHTTPS",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
}
]
}
이 정책 하나만 박아 둬도 — HTTP 평문 트래픽이 원천 차단됩니다. 의외로 빠뜨리는 경우가 많아요.
시험 직전 한 번 더 — 자주 헷갈리는 함정 모음
여기까지가 S3 2편(보안)의 핵심입니다. 시험 직전 또는 실무 점검 시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.
- 공유 책임 모델 — AWS는 플랫폼·인프라, 고객은 데이터·접근 설정. 유출 사고는 거의 다 고객 책임
- IAM = 출입카드 부서. User(사람), Group(묶음), Role(서비스용 임시 통행증), Policy(권한 명세)
- IAM 정책의 Resource —
bucket-name(버킷)과bucket-name/*(객체)는 다른 ARN. 둘 다 명시 필요 - SSE-KMS 암호화 객체는
kms:Decrypt권한이 없으면 못 읽음 - IAM 정책은 카드(주체)에, 버킷 정책은 사물함(리소스) 에 붙음. 버킷 정책엔
Principal필수 - HTTPS 강제 —
aws:SecureTransport: "false"거부 조건이 표준 패턴 - ACL의
AuthenticatedUsers는 "내 계정"이 아니라 "AWS 가입자 전체" — 거의 공개와 같음 - ACL은 구식, 새 프로젝트는 IAM 정책 + 버킷 정책으로 가세요
- Block Public Access 4개 옵션 모두 활성화 권장. 계정 수준에서도 켤 수 있음
- BPA는 버킷 정책·ACL을 덮어 씀. 진짜 공개 버킷에서만 BPA를 풀어야
- 2023년부터 신규 버킷은 SSE-S3 기본 적용
- 암호화 4종 — SSE-S3(AWS 자동), SSE-KMS(KMS 관리·감사 추적), SSE-C(고객 키 직접), CSE(업로드 전 봉인)
- SSE-KMS에서 객체 많으면 버킷 키(Bucket Key) 로 KMS 호출 비용 절감
- SSE-C·CSE — 고객이 키 잃으면 데이터 복구 불가
- **CORS의
AllowedOrigins: ["*"]는 개발 환경 전용**. 프로덕션은 명시적 도메인 - MFA Delete는 버전 관리 필수, 루트만 켤 수 있고, CLI/API로만 가능, 콘솔 불가
- Object Lock은 버킷 생성 시점에만 활성화 가능. 추가 요금 없음
- Object Lock — Governance(특별 권한자만 해제) vs Compliance(누구도 해제 X, 루트도 X)
- Legal Hold — 보존 기간 없이 무기한 보호, 소송·감사 시 사용
- Presigned URL 자격 증명별 상한 — IAM 사용자 7일, STS 36시간, EC2 인스턴스 프로필 6시간
- Presigned URL은 만든 사람 권한 상속. 자격 증명 만료 시 URL도 같이 죽음
- VPC 엔드포인트 — S3는 게이트웨이 엔드포인트(무료) 기본. 인터넷 우회로 보안·비용 둘 다 이득
aws:SourceVpce조건으로 특정 VPC 엔드포인트만 접근 허용 가능
다음 글(3편)에서는 S3 성능 최적화 — 멀티파트 업로드 심화, Transfer Acceleration, S3 Select·Glacier Select, 바이트 범위 가져오기, 요청 처리 한계와 접두사 전략까지 풀어 갑니다. 보안을 단단히 잡았으니 이제 "어떻게 더 빠르고 저렴하게 쓰느냐"로 넘어가요.
시리즈 다른 편
같은 시리즈의 다른 글들도 같은 친절 톤으로 묶어 정리되어 있어요.
- 1편 — 기초
- 2편 — 보안 (현재 글)
- 3편 — 성능 최적화
- 4편 — 복제
- 5편 — 라이프사이클
- 6편 — 고급 기능
- 7편 — AWS 통합 (완)