[AWS] Amazon S3 보안

silver's avatar
Nov 19, 2025
[AWS] Amazon S3 보안

S3 암호화 (Encryption)

암호화 방식 비교

S3는 4가지 서버 측 암호화(SSE) 방식을 제공

1. SSE-S3 (기본값, 권장)

특징:
  • AWS가 관리하는 키로 암호화
  • AES-256 암호화
  • 추가 비용 없음
  • 기본으로 활성화 (2023년 1월부터)
작동 방식:
객체 업로드 ↓ S3가 자동으로 암호화 키 생성 ↓ AES-256으로 암호화 ↓ 암호화된 객체 저장 암호화 키는 별도 암호화하여 저장
설정:
# 업로드 시 암호화 (기본값이므로 명시 불필요) aws s3 cp myfile.txt s3://my-bucket/ # 명시적으로 지정 aws s3 cp myfile.txt s3://my-bucket/ \ --server-side-encryption AES256 # 버킷 기본 암호화 설정 aws s3api put-bucket-encryption \ --bucket my-bucket \ --server-side-encryption-configuration '{ "Rules": [{ "ApplyServerSideEncryptionByDefault": { "SSEAlgorithm": "AES256" }, "BucketKeyEnabled": true }] }'
장점:
  • 설정 간단
  • 추가 비용 없음
  • 키 관리 불필요
단점:
  • 키 회전 제어 불가
  • 키 접근 감사 불가
  • 키 삭제 불가

2. SSE-KMS (AWS Key Management Service)

특징:
  • KMS로 키 관리
  • 키 회전 자동화
  • 키 사용 감사 가능 (CloudTrail)
  • 키 접근 제어 가능 (IAM)
비용:
  • KMS 키: $1/월
  • API 요청: $0.03/10,000 요청
설정:
# KMS 키 생성 aws kms create-key \ --description "S3 encryption key" # 별칭 생성 aws kms create-alias \ --alias-name alias/s3-encryption \ --target-key-id 1234abcd-12ab-34cd-56ef-1234567890ab # 업로드 시 KMS 사용 aws s3 cp myfile.txt s3://my-bucket/ \ --server-side-encryption aws:kms \ --ssekms-key-id arn:aws:kms:region:account:key/1234abcd-12ab-34cd-56ef-1234567890ab # 버킷 기본 암호화 (KMS) aws s3api put-bucket-encryption \ --bucket my-bucket \ --server-side-encryption-configuration '{ "Rules": [{ "ApplyServerSideEncryptionByDefault": { "SSEAlgorithm": "aws:kms", "KMSMasterKeyID": "arn:aws:kms:region:account:key/1234abcd" }, "BucketKeyEnabled": true }] }'
S3 Bucket Key 최적화:
Bucket Key 없음: - 각 객체마다 KMS API 호출 - 1,000개 객체 = 1,000 KMS 요청 = $0.03 Bucket Key 사용: - 버킷 레벨 키 생성 - S3가 객체 키 생성 - 1,000개 객체 = 1 KMS 요청 = $0.000003 → 99% 비용 절감
KMS 키 정책:
{ "Version": "2012-10-17", "Statement": [ { "Sid": "Enable IAM User Permissions", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::123456789012:root" }, "Action": "kms:*", "Resource": "*" }, { "Sid": "Allow S3 to use the key", "Effect": "Allow", "Principal": { "Service": "s3.amazonaws.com" }, "Action": [ "kms:Decrypt", "kms:GenerateDataKey" ], "Resource": "*" }, { "Sid": "Allow specific users to decrypt", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::123456789012:user/alice" }, "Action": [ "kms:Decrypt", "kms:DescribeKey" ], "Resource": "*" } ] }
장점:
  • 키 회전 자동화
  • 세밀한 접근 제어
  • 감사 로그 (CloudTrail)
  • 키 비활성화/삭제 가능
단점:
  • 추가 비용
  • KMS API 제한 (초당 5,500-10,000 요청)
  • 복잡한 설정

3. SSE-C (Customer-Provided Keys)

특징:
  • 고객이 암호화 키 제공 및 관리
  • AWS는 키를 저장하지 않음
  • 각 요청마다 키 전송 필요
설정:
# 키 생성 (256-bit) openssl rand -base64 32 > encryption.key # 업로드 aws s3api put-object \ --bucket my-bucket \ --key myfile.txt \ --body myfile.txt \ --sse-customer-algorithm AES256 \ --sse-customer-key fileb://encryption.key \ --sse-customer-key-md5 $(openssl dgst -md5 -binary encryption.key | base64) # 다운로드 (동일한 키 필요) aws s3api get-object \ --bucket my-bucket \ --key myfile.txt \ --sse-customer-algorithm AES256 \ --sse-customer-key fileb://encryption.key \ --sse-customer-key-md5 $(openssl dgst -md5 -binary encryption.key | base64) \ myfile-downloaded.txt
Python 예시:
import boto3 import os from base64 import b64encode # 키 생성 key = os.urandom(32) key_b64 = b64encode(key).decode() s3 = boto3.client('s3') # 업로드 s3.put_object( Bucket='my-bucket', Key='myfile.txt', Body=b'My secret data', SSECustomerAlgorithm='AES256', SSECustomerKey=key_b64 ) # 다운로드 response = s3.get_object( Bucket='my-bucket', Key='myfile.txt', SSECustomerAlgorithm='AES256', SSECustomerKey=key_b64 ) data = response['Body'].read()
장점:
  • 완전한 키 제어
  • AWS가 키를 저장하지 않음
  • 규정 준수 (특정 산업)
단점:
  • 키 관리 책임
  • 키 분실 시 복구 불가
  • HTTPS 필수
  • 복잡한 구현

4. DSSE-KMS (Dual-layer Server-Side Encryption)

특징:
  • 2중 암호화 레이어
  • 규정 준수 요구사항 충족
  • 2023년 출시
작동 방식:
객체 ↓ 첫 번째 암호화 (Application Layer) ↓ 두 번째 암호화 (Infrastructure Layer) ↓ 저장
설정:
aws s3 cp myfile.txt s3://my-bucket/ \ --server-side-encryption aws:kms:dsse \ --ssekms-key-id arn:aws:kms:region:account:key/1234abcd
사용 사례:
  • 금융 기관
  • 의료 데이터
  • 정부 기관

클라이언트 측 암호화

애플리케이션에서 암호화 후 S3에 업로드
from cryptography.fernet import Fernet import boto3 # 키 생성 및 저장 key = Fernet.generate_key() cipher = Fernet(key) # 데이터 암호화 data = b"Sensitive information" encrypted = cipher.encrypt(data) # S3 업로드 s3 = boto3.client('s3') s3.put_object( Bucket='my-bucket', Key='encrypted-file', Body=encrypted ) # 나중에 다운로드 및 복호화 response = s3.get_object(Bucket='my-bucket', Key='encrypted-file') encrypted_data = response['Body'].read() decrypted = cipher.decrypt(encrypted_data)
장점:
  • 전송 중에도 암호화
  • AWS는 암호화된 데이터만 봄
  • 완전한 제어
단점:
  • 암호화/복호화 오버헤드
  • 키 관리 복잡
  • S3 기능 일부 사용 불가 (S3 Select 등)

암호화 강제

버킷 정책으로 암호화되지 않은 업로드를 차단
{ "Version": "2012-10-17", "Statement": [ { "Sid": "DenyUnencryptedObjectUploads", "Effect": "Deny", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::my-bucket/*", "Condition": { "StringNotEquals": { "s3:x-amz-server-side-encryption": "AES256" } } }, { "Sid": "DenyNonKMSUploads", "Effect": "Deny", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::my-secure-bucket/*", "Condition": { "StringNotEquals": { "s3:x-amz-server-side-encryption": "aws:kms" } } } ] }

CORS (Cross-Origin Resource Sharing)

개념

웹 브라우저가 다른 도메인의 S3 리소스에 접근할 수 있도록 허용
브라우저 (https://myapp.com) ↓ Ajax 요청 S3 (https://my-bucket.s3.amazonaws.com) ↓ CORS 검증 허용 또는 거부

CORS 설정

<CORSConfiguration> <CORSRule> <AllowedOrigin>https://myapp.com</AllowedOrigin> <AllowedOrigin>https://www.myapp.com</AllowedOrigin> <AllowedMethod>GET</AllowedMethod> <AllowedMethod>PUT</AllowedMethod> <AllowedMethod>POST</AllowedMethod> <AllowedMethod>DELETE</AllowedMethod> <AllowedHeader>*</AllowedHeader> <MaxAgeSeconds>3000</MaxAgeSeconds> <ExposeHeader>ETag</ExposeHeader> </CORSRule> </CORSConfiguration>
JSON 형식:
[ { "AllowedOrigins": ["https://myapp.com"], "AllowedMethods": ["GET", "PUT", "POST"], "AllowedHeaders": ["*"], "MaxAgeSeconds": 3000, "ExposeHeaders": ["ETag", "x-amz-meta-custom-header"] } ]
CLI로 설정:
aws s3api put-bucket-cors \ --bucket my-bucket \ --cors-configuration file://cors.json

실전 예시

프론트엔드에서 직접 업로드

// React 예시 async function uploadToS3(file) { const formData = new FormData(); formData.append('file', file); try { const response = await fetch( 'https://my-bucket.s3.ap-northeast-2.amazonaws.com/uploads/' + file.name, { method: 'PUT', body: file, headers: { 'Content-Type': file.type } } ); if (response.ok) { console.log('Upload successful!'); } } catch (error) { console.error('Upload failed:', error); } }
주의사항:
  • CORS는 브라우저 보안 기능
  • API 요청에는 적용 안 됨
  • AllowedOrigin에 정확한 도메인 지정 (보안)

MFA Delete

개념

Multi-Factor Authentication으로 중요한 작업을 보호
MFA 필요 작업:
  1. 버전 관리 일시 중지/중단
  1. 객체 버전 영구 삭제
MFA 불필요:
  • 일반 객체 업로드/다운로드
  • 삭제 마커 추가 (일반 삭제)

설정 방법

# 1. 버전 관리 활성화 (필수) aws s3api put-bucket-versioning \ --bucket my-bucket \ --versioning-configuration Status=Enabled # 2. MFA Delete 활성화 (루트 계정만 가능!) aws s3api put-bucket-versioning \ --bucket my-bucket \ --versioning-configuration Status=Enabled,MFADelete=Enabled \ --mfa "arn:aws:iam::123456789012:mfa/root-account-mfa-device 123456"
⚠️ 중요: MFA Delete는 루트 계정 자격 증명으로만 활성화/비활성화 가능!

사용 예시

# MFA 없이 삭제 시도 (삭제 마커만 추가) aws s3api delete-object \ --bucket my-bucket \ --key important-file.txt # → 성공 (삭제 마커) # 버전 영구 삭제 시도 (MFA 필요) aws s3api delete-object \ --bucket my-bucket \ --key important-file.txt \ --version-id "abc123" \ --mfa "arn:aws:iam::123456789012:mfa/root-account-mfa-device 123456" # → MFA 코드 검증 후 삭제

모범 사례

중요 데이터 버킷: ├─ 버전 관리: Enabled ├─ MFA Delete: Enabled ├─ 버킷 정책: 관리자만 접근 └─ CloudTrail: 모든 API 로깅

S3 액세스 로그 (Access Logs)

개념

모든 S3 버킷 요청을 로깅합니다.
Source Bucket (my-app-bucket) ↓ 모든 요청 기록 Logging Bucket (my-logs-bucket/logs/) ↓ log1.txt: GET /photo.jpg 200 OK log2.txt: PUT /data.csv 200 OK log3.txt: DELETE /old.txt 403 Forbidden

설정

# 로그 버킷 생성 aws s3 mb s3://my-logs-bucket # 로깅 활성화 aws s3api put-bucket-logging \ --bucket my-app-bucket \ --bucket-logging-status '{ "LoggingEnabled": { "TargetBucket": "my-logs-bucket", "TargetPrefix": "app-logs/" } }'

로그 형식

79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be my-app-bucket [06/Feb/2024:00:00:38 +0000] 192.0.2.3 79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be 3E57427F3EXAMPLE REST.GET.VERSIONING - "GET /my-app-bucket?versioning HTTP/1.1" 200 - 113 - 7 - "-" "S3Console/0.4" - s9lzHYrFp76ZVxRcpX9+5cjAnEH2ROuNkd2BHfIa6UkFVdtjf5mKR3/eTPFvsiP/XV/VLi31234= SigV2 ECDHE-RSA-AES128-GCM-SHA256 AuthHeader my-app-bucket.s3.amazonaws.com TLSV1.2
주요 필드:
  • Bucket Owner
  • Bucket
  • Time
  • Remote IP
  • Requester
  • Request ID
  • Operation (REST.GET.OBJECT, REST.PUT.OBJECT 등)
  • Key
  • HTTP Status
  • Error Code
  • Bytes Sent
  • Total Time
  • User Agent

로그 분석

Athena로 분석

CREATE EXTERNAL TABLE s3_access_logs( bucket_owner string, bucket string, request_datetime string, remote_ip string, requester string, request_id string, operation string, key string, request_uri string, http_status int, error_code string, bytes_sent bigint, object_size bigint, total_time int, turn_around_time int, referer string, user_agent string ) ROW FORMAT SERDE 'org.apache.hadoop.hive.serde2.RegexSerDe' WITH SERDEPROPERTIES ( 'input.regex' = '([^ ]*) ([^ ]*) \\[(.*?)\\] ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) (\"[^\"]*\"|-) (-|[0-9]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) (\"[^\"]*\"|-) ([^ ]*)' ) LOCATION 's3://my-logs-bucket/app-logs/'; -- 가장 많이 접근된 파일 TOP 10 SELECT key, COUNT(*) as requests FROM s3_access_logs WHERE http_status = 200 GROUP BY key ORDER BY requests DESC LIMIT 10; -- 실패한 요청 분석 SELECT key, http_status, error_code, COUNT(*) as errors FROM s3_access_logs WHERE http_status >= 400 GROUP BY key, http_status, error_code ORDER BY errors DESC; -- IP별 요청 수 SELECT remote_ip, COUNT(*) as requests FROM s3_access_logs GROUP BY remote_ip ORDER BY requests DESC LIMIT 100;

비용 최적화

로그는 빠르게 쌓이므로 Lifecycle 정책으로 관리
{ "Rules": [ { "Id": "ArchiveLogs", "Status": "Enabled", "Filter": { "Prefix": "app-logs/" }, "Transitions": [ { "Days": 30, "StorageClass": "STANDARD_IA" }, { "Days": 90, "StorageClass": "GLACIER" } ], "Expiration": { "Days": 365 } } ] }

사전 서명된 URL (Pre-Signed URLs)

개념

임시로 S3 객체에 접근할 수 있는 URL을 생성합니다.
사용자 → 애플리케이션 서버 ↓ Pre-Signed URL 생성 (유효기간: 1시간) ↓ 사용자 → S3 (Pre-Signed URL로 직접 접근)

생성 방법

CLI

# 다운로드용 URL (기본 1시간) aws s3 presign s3://my-bucket/private-file.pdf # 유효기간 지정 (최대 7일) aws s3 presign s3://my-bucket/private-file.pdf \ --expires-in 3600 # 1시간 (초 단위) # 업로드용 URL aws s3 presign s3://my-bucket/upload/newfile.txt \ --expires-in 300 \ --http-method PUT

Python SDK

import boto3 from botocore.exceptions import ClientError s3_client = boto3.client('s3') # 다운로드 URL def create_presigned_url(bucket, key, expiration=3600): try: url = s3_client.generate_presigned_url( 'get_object', Params={ 'Bucket': bucket, 'Key': key }, ExpiresIn=expiration ) return url except ClientError as e: print(e) return None # 사용 download_url = create_presigned_url('my-bucket', 'private-file.pdf', 3600) print(f"Download URL: {download_url}") # 업로드 URL def create_presigned_upload_url(bucket, key, expiration=3600): try: url = s3_client.generate_presigned_url( 'put_object', Params={ 'Bucket': bucket, 'Key': key, 'ContentType': 'application/pdf' }, ExpiresIn=expiration, HttpMethod='PUT' ) return url except ClientError as e: print(e) return None upload_url = create_presigned_upload_url('my-bucket', 'uploads/newfile.pdf', 300)

JavaScript (Node.js)

const AWS = require('aws-sdk'); const s3 = new AWS.S3(); // 다운로드 URL const params = { Bucket: 'my-bucket', Key: 'private-file.pdf', Expires: 3600 }; s3.getSignedUrl('getObject', params, (err, url) => { if (err) { console.error(err); } else { console.log('Download URL:', url); } }); // 업로드 URL const uploadParams = { Bucket: 'my-bucket', Key: 'uploads/newfile.pdf', Expires: 300, ContentType: 'application/pdf' }; s3.getSignedUrl('putObject', uploadParams, (err, url) => { if (err) { console.error(err); } else { console.log('Upload URL:', url); } });

실전 활용

1. 안전한 파일 다운로드

# Flask 애플리케이션 from flask import Flask, redirect import boto3 app = Flask(__name__) s3 = boto3.client('s3') @app.route('/download/<file_id>') def download_file(file_id): # 권한 확인 if not user_has_permission(file_id): return "Forbidden", 403 # Pre-Signed URL 생성 url = s3.generate_presigned_url( 'get_object', Params={ 'Bucket': 'my-private-bucket', 'Key': f'files/{file_id}' }, ExpiresIn=60 # 1분 ) # 리다이렉트 return redirect(url)

2. 브라우저에서 직접 업로드

// 프론트엔드 async function uploadFile(file) { // 백엔드에서 Pre-Signed URL 요청 const response = await fetch('/api/get-upload-url', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ filename: file.name, contentType: file.type }) }); const { uploadUrl } = await response.json(); // S3에 직접 업로드 await fetch(uploadUrl, { method: 'PUT', body: file, headers: { 'Content-Type': file.type } }); console.log('Upload successful!'); } // 백엔드 (Node.js/Express) app.post('/api/get-upload-url', async (req, res) => { const { filename, contentType } = req.body; const params = { Bucket: 'my-uploads-bucket', Key: `uploads/${Date.now()}-${filename}`, ContentType: contentType, Expires: 300 // 5분 }; const uploadUrl = await s3.getSignedUrlPromise('putObject', params); res.json({ uploadUrl }); });

보안 고려사항

# ❌ 나쁜 예: 긴 유효기간 url = s3.generate_presigned_url( 'get_object', Params={'Bucket': 'my-bucket', 'Key': 'file.pdf'}, ExpiresIn=604800 # 7일 - 너무 김! ) # ✅ 좋은 예: 짧은 유효기간 url = s3.generate_presigned_url( 'get_object', Params={'Bucket': 'my-bucket', 'Key': 'file.pdf'}, ExpiresIn=300 # 5분 ) # ✅ 더 좋은 예: 조건 추가 url = s3.generate_presigned_url( 'get_object', Params={ 'Bucket': 'my-bucket', 'Key': 'file.pdf', 'ResponseContentDisposition': 'attachment; filename="document.pdf"', 'ResponseContentType': 'application/pdf' }, ExpiresIn=300 )

S3 Glacier Vault Lock & Object Lock

S3 Glacier Vault Lock

개념: Glacier 볼트를 "잠가서" 데이터를 변경 불가능하게 함
사용 사례:
  • 규정 준수 (WORM: Write Once Read Many)
  • 법적 요구사항
  • 감사 로그 보호
# Vault Lock 정책 설정 aws glacier initiate-vault-lock \ --account-id - \ --vault-name my-compliance-vault \ --policy file://vault-lock-policy.json
정책 예시:
{ "Version": "2012-10-17", "Statement": [ { "Sid": "deny-delete-before-7-years", "Effect": "Deny", "Principal": "*", "Action": "glacier:DeleteArchive", "Resource": "arn:aws:glacier:region:account-id:vaults/my-compliance-vault", "Condition": { "NumericLessThan": { "glacier:ArchiveAgeInDays": "2555" } } } ] }

S3 Object Lock

개념: S3 객체를 지정된 기간 동안 삭제/수정 불가능하게 만듭니다.
모드:

1. Governance Mode (거버넌스 모드)

  • 특별한 권한이 있는 사용자는 삭제 가능
  • 보호 설정 수정 가능
  • 일반 사용자는 삭제 불가

2. Compliance Mode (규정 준수 모드)

  • 누구도 삭제/수정 불가 (루트 계정 포함)
  • 보호 기간 단축 불가
  • 완전한 불변성
  • 가장 강력한 보호

Object Lock 설정

# 1. 버킷 생성 시 Object Lock 활성화 (필수) aws s3api create-bucket \ --bucket my-locked-bucket \ --object-lock-enabled-for-bucket # 2. 기본 보존 설정 aws s3api put-object-lock-configuration \ --bucket my-locked-bucket \ --object-lock-configuration '{ "ObjectLockEnabled": "Enabled", "Rule": { "DefaultRetention": { "Mode": "GOVERNANCE", "Days": 365 } } }' # 3. 객체 업로드 (보존 기간 지정) aws s3api put-object \ --bucket my-locked-bucket \ --key important-doc.pdf \ --body document.pdf \ --object-lock-mode COMPLIANCE \ --object-lock-retain-until-date "2025-12-31T00:00:00Z"

법적 보존 (Legal Hold)

무기한으로 객체를 보호(기간 없음).
# Legal Hold 설정 aws s3api put-object-legal-hold \ --bucket my-bucket \ --key evidence.pdf \ --legal-hold Status=ON # Legal Hold 해제 (특별 권한 필요) aws s3api put-object-legal-hold \ --bucket my-bucket \ --key evidence.pdf \ --legal-hold Status=OFF

실전 시나리오

규정 준수 아카이브

import boto3 from datetime import datetime, timedelta s3 = boto3.client('s3') def archive_with_lock(bucket, key, file_path, retention_years=7): # 보존 종료일 계산 retain_until = datetime.now() + timedelta(days=retention_years*365) # 업로드 with Object Lock with open(file_path, 'rb') as f: s3.put_object( Bucket=bucket, Key=key, Body=f, ObjectLockMode='COMPLIANCE', ObjectLockRetainUntilDate=retain_until ) print(f"Archived: {key} (locked until {retain_until})") # 의료 기록 7년 보관 archive_with_lock('medical-records', 'patient-123/record-2024.pdf', 'record.pdf', 7) # 금융 기록 10년 보관 archive_with_lock('financial-records', 'audit-2024.pdf', 'audit.pdf', 10)

Governance 모드에서 강제 삭제

# 특별 권한으로 삭제 (s3:BypassGovernanceRetention 필요) aws s3api delete-object \ --bucket my-bucket \ --key protected-file.pdf \ --version-id "abc123" \ --bypass-governance-retention
IAM 정책:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:BypassGovernanceRetention", "s3:DeleteObject", "s3:DeleteObjectVersion" ], "Resource": "arn:aws:s3:::my-bucket/*" } ] }

S3 액세스 포인트 (Access Points)

개념

버킷에 대한 여러 진입점을 생성하여 접근을 단순화합니다.
단일 버킷: s3://my-data-bucket/ 여러 액세스 포인트: ├─ finance-ap → /finance/* (재무팀만) ├─ engineering-ap → /engineering/* (엔지니어링팀만) └─ analytics-ap → /analytics/* (분석팀만)

생성 및 사용

# 액세스 포인트 생성 aws s3control create-access-point \ --account-id 123456789012 \ --name finance-ap \ --bucket my-data-bucket \ --vpc-configuration VpcId=vpc-12345678 # 액세스 포인트 정책 aws s3control put-access-point-policy \ --account-id 123456789012 \ --name finance-ap \ --policy '{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::123456789012:role/FinanceRole" }, "Action": ["s3:GetObject", "s3:PutObject"], "Resource": "arn:aws:s3:region:account-id:accesspoint/finance-ap/object/finance/*" } ] }'

액세스 포인트 사용

# 일반 버킷 대신 액세스 포인트 사용 aws s3api get-object \ --bucket arn:aws:s3:region:account-id:accesspoint/finance-ap \ --key finance/report.pdf \ report.pdf # Python SDK import boto3 s3 = boto3.client('s3') response = s3.get_object( Bucket='arn:aws:s3:ap-northeast-2:123456789012:accesspoint/finance-ap', Key='finance/report.pdf' )

Multi-Region Access Point

여러 리전의 버킷을 단일 글로벌 엔드포인트로 통합
Global Endpoint: my-app.mrap.accesspoint.s3-global.amazonaws.com ↓ 자동 라우팅 ├─ ap-northeast-2 bucket (아시아 사용자) ├─ us-east-1 bucket (미국 사용자) └─ eu-west-1 bucket (유럽 사용자)
장점:
  • 단일 엔드포인트
  • 자동 장애 조치
  • 지연 시간 최소화

S3 Object Lambda

개념

S3에서 객체를 검색할 때 Lambda 함수를 실행하여 데이터를 변환합니다.
사용자 요청: GET /data.csv ↓ Object Lambda Access Point ↓ Lambda 함수 호출 데이터 변환 (예: 개인정보 마스킹) ↓ 변환된 데이터 반환

사용 사례

  1. 개인정보 보호
      • 민감한 정보 마스킹
      • 부서별 필터링
  1. 데이터 형식 변환
      • JSON → XML
      • CSV → JSON
      • 이미지 리사이징
  1. 동적 콘텐츠 생성
      • 워터마크 추가
      • 압축/압축 해제

설정 예시

1. Lambda 함수 생성

import boto3 import json s3 = boto3.client('s3') def lambda_handler(event, context): # Object Lambda 이벤트 파싱 object_context = event['getObjectContext'] request_route = object_context['outputRoute'] request_token = object_context['outputToken'] s3_url = object_context['inputS3Url'] # 원본 객체 가져오기 response = s3.get_object( Bucket=event['configuration']['accessPointArn'].split('/')[-1], Key=event['userRequest']['url'].split('/')[-1] ) original_data = response['Body'].read().decode('utf-8') # 데이터 변환 (예: 이메일 마스킹) import re masked_data = re.sub( r'[\w\.-]+@[\w\.-]+', '***@***.com', original_data ) # 변환된 데이터 반환 s3.write_get_object_response( RequestRoute=request_route, RequestToken=request_token, Body=masked_data ) return {'statusCode': 200}

2. Object Lambda Access Point 생성

# 1. 표준 액세스 포인트 생성 aws s3control create-access-point \ --account-id 123456789012 \ --name my-ap \ --bucket my-bucket # 2. Object Lambda Access Point 생성 aws s3control create-access-point-for-object-lambda \ --account-id 123456789012 \ --name my-lambda-ap \ --configuration '{ "SupportingAccessPoint": "arn:aws:s3:region:account-id:accesspoint/my-ap", "TransformationConfigurations": [{ "Actions": ["GetObject"], "ContentTransformation": { "AwsLambda": { "FunctionArn": "arn:aws:lambda:region:account-id:function:my-transform-function" } } }] }'

3. 사용

import boto3 s3 = boto3.client('s3') # Object Lambda Access Point로 요청 response = s3.get_object( Bucket='arn:aws:s3-object-lambda:region:account-id:accesspoint/my-lambda-ap', Key='sensitive-data.csv' ) # 마스킹된 데이터 반환 data = response['Body'].read() print(data) # 이메일이 마스킹됨

실전 예시

이미지 리사이징

from PIL import Image from io import BytesIO import boto3 s3 = boto3.client('s3') def lambda_handler(event, context): object_context = event['getObjectContext'] s3_url = object_context['inputS3Url'] # 원본 이미지 가져오기 response = s3.get_object( Bucket=event['configuration']['payload']['bucket'], Key=event['userRequest']['url'].split('/')[-1] ) # 이미지 리사이징 image = Image.open(BytesIO(response['Body'].read())) image.thumbnail((200, 200)) # 버퍼에 저장 buffer = BytesIO() image.save(buffer, 'JPEG') buffer.seek(0) # 변환된 이미지 반환 s3.write_get_object_response( RequestRoute=object_context['outputRoute'], RequestToken=object_context['outputToken'], Body=buffer.read() ) return {'statusCode': 200}

부서별 데이터 필터링

import json import boto3 s3 = boto3.client('s3') def lambda_handler(event, context): # 사용자 정보 (IAM 태그 등에서 가져옴) user_department = event['userRequest']['headers'].get('x-department', 'unknown') # 원본 JSON 데이터 response = s3.get_object( Bucket=event['configuration']['payload']['bucket'], Key=event['userRequest']['url'].split('/')[-1] ) data = json.loads(response['Body'].read()) # 부서별 필터링 filtered_data = [ record for record in data if record.get('department') == user_department ] # 필터링된 데이터 반환 s3.write_get_object_response( RequestRoute=event['getObjectContext']['outputRoute'], RequestToken=event['getObjectContext']['outputToken'], Body=json.dumps(filtered_data) ) return {'statusCode': 200}

보안 체크리스트

필수 보안 설정

✅ 퍼블릭 액세스 차단 (기본 활성화) ✅ 버킷 정책으로 최소 권한 원칙 ✅ 암호화 활성화 (SSE-S3 또는 SSE-KMS) ✅ 버전 관리 활성화 (중요 데이터) ✅ MFA Delete (매우 중요한 데이터) ✅ 액세스 로그 활성화 ✅ CloudTrail로 API 모니터링 ✅ AWS Config로 규정 준수 확인

IAM 정책 예시

{ "Version": "2012-10-17", "Statement": [ { "Sid": "AllowListBucket", "Effect": "Allow", "Action": "s3:ListBucket", "Resource": "arn:aws:s3:::my-bucket", "Condition": { "StringLike": { "s3:prefix": ["${aws:username}/*"] } } }, { "Sid": "AllowUserFolder", "Effect": "Allow", "Action": [ "s3:GetObject", "s3:PutObject", "s3:DeleteObject" ], "Resource": "arn:aws:s3:::my-bucket/${aws:username}/*" }, { "Sid": "DenyUnencryptedUploads", "Effect": "Deny", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::my-bucket/*", "Condition": { "StringNotEquals": { "s3:x-amz-server-side-encryption": "AES256" } } } ] }

버킷 정책 예시

{ "Version": "2012-10-17", "Statement": [ { "Sid": "DenyInsecureTransport", "Effect": "Deny", "Principal": "*", "Action": "s3:*", "Resource": [ "arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket/*" ], "Condition": { "Bool": { "aws:SecureTransport": "false" } } }, { "Sid": "AllowOnlyFromVPC", "Effect": "Deny", "Principal": "*", "Action": "s3:*", "Resource": [ "arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket/*" ], "Condition": { "StringNotEquals": { "aws:SourceVpce": "vpce-1234567890abcdef0" } } }, { "Sid": "AllowCloudFrontOnly", "Effect": "Allow", "Principal": { "Service": "cloudfront.amazonaws.com" }, "Action": "s3:GetObject", "Resource": "arn:aws:s3:::my-bucket/*", "Condition": { "StringEquals": { "AWS:SourceArn": "arn:aws:cloudfront::account-id:distribution/E1234567890ABC" } } } ] }

모니터링 및 감사

CloudWatch 지표

# 버킷 크기 모니터링 aws cloudwatch get-metric-statistics \ --namespace AWS/S3 \ --metric-name BucketSizeBytes \ --dimensions Name=BucketName,Value=my-bucket Name=StorageType,Value=StandardStorage \ --start-time 2024-11-01T00:00:00Z \ --end-time 2024-11-19T23:59:59Z \ --period 86400 \ --statistics Average # 요청 수 모니터링 aws cloudwatch get-metric-statistics \ --namespace AWS/S3 \ --metric-name NumberOfObjects \ --dimensions Name=BucketName,Value=my-bucket Name=StorageType,Value=AllStorageTypes \ --start-time 2024-11-19T00:00:00Z \ --end-time 2024-11-19T23:59:59Z \ --period 3600 \ --statistics Sum

CloudWatch 알람

# 큰 객체 업로드 알림 aws cloudwatch put-metric-alarm \ --alarm-name large-object-upload \ --alarm-description "Alert when large objects are uploaded" \ --metric-name BytesUploaded \ --namespace AWS/S3 \ --statistic Sum \ --period 300 \ --evaluation-periods 1 \ --threshold 1073741824 \ --comparison-operator GreaterThanThreshold \ --alarm-actions arn:aws:sns:region:account-id:my-topic

CloudTrail 로그 분석

-- Athena 쿼리: 삭제 작업 추적 SELECT useridentity.principalid, eventtime, requestparameters.bucketname, requestparameters.key FROM cloudtrail_logs WHERE eventname = 'DeleteObject' AND eventtime > '2024-11-01' ORDER BY eventtime DESC; -- 실패한 접근 시도 SELECT sourceipaddress, useridentity.principalid, eventname, errorcode, COUNT(*) as attempts FROM cloudtrail_logs WHERE errorcode IS NOT NULL AND eventtime > '2024-11-01' GROUP BY sourceipaddress, useridentity.principalid, eventname, errorcode ORDER BY attempts DESC;

실전 통합 시나리오

시나리오 1: 금융 문서 보관 시스템

요구사항: - 7년 보관 의무 - 변경 불가 - 접근 감사 - 암호화 필수 구현: ├─ S3 Bucket (버전 관리 + Object Lock) │ ├─ Encryption: SSE-KMS │ ├─ Object Lock: COMPLIANCE (7년) │ └─ MFA Delete: Enabled ├─ 액세스 로그 → Glacier (장기 보관) ├─ CloudTrail → 모든 API 로깅 └─ EventBridge → 이상 활동 알림

시나리오 2: 멀티테넌트 SaaS 플랫폼

요구사항: - 고객별 데이터 격리 - 고객별 접근 제어 - 암호화 구현: ├─ S3 Bucket: saas-customer-data ├─ 액세스 포인트 (고객별) │ ├─ customer-a-ap → /customer-a/* │ ├─ customer-b-ap → /customer-b/* │ └─ customer-c-ap → /customer-c/* ├─ 각 액세스 포인트에 정책 │ └─ 해당 고객 역할만 접근 └─ SSE-KMS (고객별 키)

시나리오 3: 개인정보 보호 데이터 레이크

요구사항: - 민감 정보 자동 마스킹 - 부서별 데이터 필터링 - 감사 로그 구현: ├─ S3 Bucket (원본 데이터) ├─ Object Lambda Access Point │ ├─ Lambda: 개인정보 마스킹 │ └─ Lambda: 부서별 필터링 ├─ 부서별 IAM 역할 │ └─ Object Lambda AP 접근 └─ 모든 접근 로깅

비용 최적화 with 보안

암호화 비용

SSE-S3: 무료 SSE-KMS: $1/월 (키) + $0.03/10,000 요청 SSE-KMS + Bucket Key: 99% 절감 SSE-C: 무료 (관리 부담)

액세스 로그 비용 관리

{ "Rules": [ { "Id": "ManageAccessLogs", "Status": "Enabled", "Filter": {"Prefix": "logs/"}, "Transitions": [ {"Days": 30, "StorageClass": "STANDARD_IA"}, {"Days": 90, "StorageClass": "GLACIER"} ], "Expiration": {"Days": 2555} } ] }

💡
S3 보안은 다층 방어(Defense in Depth) 전략이 필요
  • 암호화: SSE-S3 (기본) 또는 SSE-KMS (고급)
  • CORS: 웹 애플리케이션 통합
  • MFA Delete: 중요 데이터 보호
  • 액세스 로그: 모든 활동 추적
  • Pre-Signed URLs: 임시 접근 제어
  • Object Lock: 규정 준수 (WORM)
  • 액세스 포인트: 복잡한 접근 제어 단순화
  • Object Lambda: 동적 데이터 변환
Share article

silver