Presigned URL 메커니즘 완전 가이드
TL;DR
- 이 글은 Presigned URL 메커니즘 완전 가이드의 핵심 개념과 실제 적용 포인트를 빠르게 정리한다.
- 왜 이 패턴/기법이 등장했는지 배경과 도입 이유를 함께 설명한다.
- 실무에서 바로 쓰기 위한 특징과 상세 내용을 원문 기반으로 정리한다.
1. 개념
Presigned URL 메커니즘 완전 가이드의 정의와 핵심 원리를 먼저 이해하면 뒤의 구현 전략을 훨씬 정확하게 판단할 수 있다.
2. 배경
기존 방식의 한계와 운영상의 문제를 해결하기 위해 이 접근이 발전했다.
3. 이유
확장성, 안정성, 유지보수성, 보안을 함께 높이기 위해 이 설계가 필요하다.
4. 특징
핵심 특징은 표준화된 구조, 명확한 책임 분리, 그리고 운영 관점에서의 예측 가능성이다.
5. 상세 내용
Presigned URL 메커니즘 완전 가이드
작성일: 2026-03-16 카테고리: Cloud / AWS / S3 / Security 키워드: Presigned URL, AWS Signature Version 4, HMAC-SHA256, Query String Authentication, Canonical Request, Signing Key, Capability-based Security, CloudFront Signed URL, Azure SAS Token, GCP Signed URL
1. Presigned URL이란?
1.1 핵심 개념: “미리 서명된 URL”
Presigned URL은 S3 객체에 대한 접근 권한을 URL 자체에 내포시킨 임시 접근 토큰이다. 서버가 자신의 IAM 자격증명으로 URL에 서명(sign)을 미리(pre) 완료하면, 해당 URL을 받은 클라이언트는 AWS 자격증명 없이도 S3에 직접 접근할 수 있다.
┌─────────────────────────────────────────────────────────────┐
│ │
│ Presigned URL 동작 흐름 │
│ │
│ ┌──────────┐ 1. 다운로드 요청 ┌──────────────┐ │
│ │ │ ──────────────────────→ │ │ │
│ │ Client │ │ App Server │ │
│ │ (브라우저)│ ←────────────────────── │ (IAM 키 보유)│ │
│ │ │ 2. Presigned URL 반환 │ │ │
│ └────┬─────┘ └──────────────┘ │
│ │ │
│ │ 3. Presigned URL로 직접 요청 (GET) │
│ │ (Authorization 헤더 불필요!) │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ AWS S3 │ │
│ │ │ │
│ │ 4. URL 내 서명 검증 │ │
│ │ 5. 만료 시간 확인 │ │
│ │ 6. 검증 통과 → 객체 반환 │ │
│ └──────────────────────────────────────┘ │
│ │
│ 핵심: 서버는 URL만 생성, 실제 데이터 전송은 S3가 직접 │
│ → 서버 대역폭 절약 + IAM 키 미노출 │
│ │
└─────────────────────────────────────────────────────────────┘
“Pre-signed”의 의미: 클라이언트가 URL을 사용하는 시점 이전에(pre) 서버가 서명을 완성(signed)하여 URL 쿼리 스트링에 내포시킨다. 서명은 완전히 로컬 계산이며, AWS API 호출이 발생하지 않는다.
1.2 왜 필요한가?
| 문제 | Presigned URL 해결 방식 |
|---|---|
| IAM 키 노출 위험 | 클라이언트에게 키를 주지 않고, 서명된 URL만 전달 |
| 서버 대역폭 부담 | 서버가 파일을 중계하지 않음. S3 → 클라이언트 직접 전송 |
| 브라우저 직접 다운로드 | URL 자체에 인증 정보 포함 → <a href>, <img src> 등에서 바로 사용 |
| 임시 접근 제어 | 만료 시간 설정으로 시간 제한 접근 가능 |
| 업로드 부하 분산 | 클라이언트가 S3에 직접 PUT → 서버 무부하 업로드 |
2. 서명 메커니즘 상세
2.1 Presigned URL의 구조
실제 Presigned URL 예시:
https://my-bucket.s3.ap-northeast-2.amazonaws.com/reports/2026/Q1.pdf
?X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE/20260316/ap-northeast-2/s3/aws4_request
&X-Amz-Date=20260316T120000Z
&X-Amz-Expires=3600
&X-Amz-SignedHeaders=host
&X-Amz-Signature=a1b2c3d4e5f6... (64자리 hex)
| 파라미터 | 의미 | 예시 값 |
|---|---|---|
X-Amz-Algorithm |
서명 알고리즘 | AWS4-HMAC-SHA256 |
X-Amz-Credential |
자격증명 + 범위 (Access Key / 날짜 / 리전 / 서비스 / 요청 유형) | AKIA.../20260316/ap-northeast-2/s3/aws4_request |
X-Amz-Date |
서명 생성 시각 (UTC ISO 8601) | 20260316T120000Z |
X-Amz-Expires |
URL 유효 시간 (초) | 3600 (1시간) |
X-Amz-SignedHeaders |
서명에 포함된 HTTP 헤더 목록 | host |
X-Amz-Signature |
최종 서명값 (HMAC-SHA256 결과, hex 인코딩) | a1b2c3d4e5f6... |
2.2 왜 브라우저에서 그냥 접근 가능한가?
일반적인 AWS API 호출은 HTTP 요청에 Authorization 헤더를 추가해야 한다:
Authorization: AWS4-HMAC-SHA256
Credential=AKIAIOSFODNN7EXAMPLE/20260316/ap-northeast-2/s3/aws4_request,
SignedHeaders=host;x-amz-content-sha256;x-amz-date,
Signature=a1b2c3d4e5f6...
문제: 브라우저의 <a href>, <img src>, 주소창 직접 입력 등은 HTTP 헤더를 커스텀할 수 없다. GET 요청만 보낼 뿐이다.
Query String Authentication이 이를 해결한다. Authorization 헤더에 들어갈 모든 정보를 URL 쿼리 파라미터로 옮긴 것이 Presigned URL이다.
┌─────────────────────────────────────────────────────────────┐
│ │
│ 일반 API 호출 (프로그래밍 필요): │
│ ┌───────────────────────────────────────────────┐ │
│ │ GET /reports/Q1.pdf HTTP/1.1 │ │
│ │ Host: my-bucket.s3.amazonaws.com │ │
│ │ Authorization: AWS4-HMAC-SHA256 Credential=.. │ ← 헤더 │
│ │ X-Amz-Date: 20260316T120000Z │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ Presigned URL (브라우저 주소창에서 바로 접근): │
│ ┌───────────────────────────────────────────────┐ │
│ │ GET /reports/Q1.pdf │ │
│ │ ?X-Amz-Algorithm=AWS4-HMAC-SHA256 │ ← URL │
│ │ &X-Amz-Credential=... │ │
│ │ &X-Amz-Signature=... │ │
│ │ Host: my-bucket.s3.amazonaws.com │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ 인증 정보의 위치만 다를 뿐, 검증 로직은 동일하다 │
│ │
└─────────────────────────────────────────────────────────────┘
S3 서버는 두 방식 모두 동일한 SigV4 검증 로직으로 처리한다.
2.3 AWS SigV4 서명 과정 Step-by-Step
┌─────────────────────────────────────────────────────────────┐
│ │
│ AWS Signature Version 4 서명 과정 (4단계) │
│ │
│ Step 1 Step 2 Step 3 Step 4 │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │Canon-│ │String│ │Sign- │ │최종 │ │
│ │ical │──────→│To │──────→│ing │──────→│서명 │ │
│ │Request│ │Sign │ │Key │ │생성 │ │
│ └──────┘ └──────┘ └──────┘ └──────┘ │
│ │
│ HTTP 요소를 정규화된 Secret Key HMAC-SHA256│
│ 정규화 요청의 해시를 에서 파생된 (SigningKey,│
│ (표준 형식) 포함한 서명 키 (4단계 StringTo │
│ 대상 문자열 HMAC 체인) Sign) │
│ │
└─────────────────────────────────────────────────────────────┘
Step 1: Canonical Request 생성
HTTP 요청의 주요 요소를 정해진 규칙(canon)대로 정규화한 문자열이다.
CanonicalRequest =
HTTPMethod + "\n" + ← GET
CanonicalURI + "\n" + ← /reports/2026/Q1.pdf (URI 인코딩)
CanonicalQuery + "\n" + ← X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...
CanonicalHeaders + "\n" + ← host:my-bucket.s3.ap-northeast-2.amazonaws.com\n
SignedHeaders + "\n" + ← host
HashedPayload ← UNSIGNED-PAYLOAD (Presigned URL의 경우)
| 구성 요소 | 정규화 규칙 |
|---|---|
HTTPMethod |
대문자 (GET, PUT, DELETE) |
CanonicalURI |
URI 경로를 UTF-8 퍼센트 인코딩, 중복 슬래시 정규화 |
CanonicalQuery |
파라미터를 이름순 정렬, 각각 URI 인코딩, &로 연결 |
CanonicalHeaders |
소문자 변환, 연속 공백 제거, 이름순 정렬, \n 종료 |
SignedHeaders |
서명에 포함된 헤더 이름을 ;로 연결 |
HashedPayload |
요청 본문의 SHA-256 해시 (Presigned URL은 UNSIGNED-PAYLOAD) |
Step 2: StringToSign 생성
StringToSign =
"AWS4-HMAC-SHA256" + "\n" + ← 알고리즘
"20260316T120000Z" + "\n" + ← 타임스탬프
"20260316/ap-northeast-2/s3/aws4_request" + "\n" + ← Credential Scope
SHA256(CanonicalRequest) ← Step 1 해시값
Credential Scope는 서명의 유효 범위를 날짜 / 리전 / 서비스 / 요청 유형 4단위로 제한한다. 같은 서명을 다른 리전이나 다른 서비스에 재사용할 수 없다.
Step 3: Signing Key 파생 (4단계 HMAC 체인)
Secret Access Key에서 시작하여 4단계 HMAC 연산으로 Signing Key를 파생한다.
┌─────────────────────────────────────────────────────────────┐
│ │
│ Signing Key 파생 과정 (4단계 HMAC 체인) │
│ │
│ "AWS4" + SecretAccessKey │
│ │ │
│ ▼ HMAC-SHA256(key, "20260316") │
│ ┌──────────┐ │
│ │ DateKey │ ← 날짜로 범위 한정 │
│ └────┬─────┘ │
│ ▼ HMAC-SHA256(DateKey, "ap-northeast-2") │
│ ┌──────────────┐ │
│ │ DateRegionKey│ ← 리전으로 범위 한정 │
│ └────┬─────────┘ │
│ ▼ HMAC-SHA256(DateRegionKey, "s3") │
│ ┌─────────────────────┐ │
│ │ DateRegionServiceKey│ ← 서비스로 범위 한정 │
│ └────┬────────────────┘ │
│ ▼ HMAC-SHA256(DateRegionServiceKey, "aws4_request") │
│ ┌──────────┐ │
│ │SigningKey│ ← 최종 파생 키 │
│ └──────────┘ │
│ │
│ 효과: Secret Key가 직접 노출되지 않으며, │
│ 각 키는 특정 날짜/리전/서비스에만 유효 │
│ │
└─────────────────────────────────────────────────────────────┘
Step 4: 최종 서명 생성
Signature = Hex( HMAC-SHA256( SigningKey, StringToSign ) )
결과값은 64자리 16진수 문자열이며, 이것이 X-Amz-Signature 파라미터의 값이 된다.
중요: Presigned URL 생성은 완전히 로컬 계산이다. AWS에 어떤 API 호출도 하지 않는다. HMAC-SHA256 연산만으로 구성되므로 생성 시간은 1ms 미만이다.
2.4 URL Path 변조가 실패하는 이유
이 섹션이 가장 핵심이다. Presigned URL의 보안은 서명에 요청의 모든 주요 요소가 바인딩되어 있다는 점에 근거한다.
시나리오 A: Path 변조 → 403 Forbidden
┌─────────────────────────────────────────────────────────────┐
│ │
│ 원본 URL: .../reports/2026/Q1.pdf?...&X-Amz-Signature=abc│
│ │
│ 공격자 변조: .../secrets/passwords.txt?...&X-Amz-Sig=abc │
│ ↑ 경로만 바꿈, 서명은 그대로 │
│ │
│ S3 서버 검증 과정: │
│ 1. 변조된 URI로 Canonical Request 재구성 │
│ CanonicalURI = "/secrets/passwords.txt" │
│ 2. StringToSign에 SHA256(CanonicalRequest) 포함 │
│ 3. Signing Key로 HMAC-SHA256 계산 → 서명값 xyz │
│ 4. URL의 서명 abc ≠ 계산된 서명 xyz │
│ 5. → 403 SignatureDoesNotMatch │
│ │
│ 원리: URI가 Canonical Request에 포함되므로, │
│ 경로 1글자만 바뀌어도 서명 전체가 달라진다 │
│ │
└─────────────────────────────────────────────────────────────┘
시나리오 B: 만료 시간 변조 → 403 Forbidden
원본: X-Amz-Expires=3600 (1시간)
변조: X-Amz-Expires=604800 (7일로 연장)
→ X-Amz-Expires는 Canonical Query String에 포함
→ 값이 바뀌면 Canonical Request 해시 변경
→ 서명 불일치 → 403
시나리오 C: HTTP 메서드 변경 → 403 Forbidden
원본: GET 용으로 서명된 URL
변조: PUT 요청으로 사용 시도
→ HTTPMethod가 Canonical Request의 첫 줄
→ GET ≠ PUT → 서명 불일치 → 403
시나리오 D: 다른 호스트(버킷)로 재사용 → 403 Forbidden
원본: Host: my-bucket.s3.amazonaws.com
변조: Host: other-bucket.s3.amazonaws.com
→ host 헤더가 Canonical Headers에 포함
→ 호스트명 변경 → 서명 불일치 → 403
변조 방지 요약
| 변조 대상 | 서명에 바인딩된 위치 | 결과 |
|---|---|---|
| URI 경로 | CanonicalURI | 403 |
| 쿼리 파라미터 | CanonicalQueryString | 403 |
| HTTP 메서드 | HTTPMethod (Canonical Request 1행) | 403 |
| Host 헤더 | CanonicalHeaders | 403 |
| 만료 시간 | CanonicalQueryString 내 X-Amz-Expires | 403 |
| 서명 자체 위조 | Secret Access Key 없이 유효한 서명 계산 불가 | 403 |
2.5 S3 서버 측 검증 프로세스
┌─────────────────────────────────────────────────────────────┐
│ │
│ S3 서버: Presigned URL 수신 시 검증 플로우 │
│ │
│ 요청 수신 │
│ │ │
│ ▼ │
│ ┌────────────────────────┐ │
│ │ 1. URL 파라미터 파싱 │ │
│ │ X-Amz-Credential │ │
│ │ X-Amz-Date │ │
│ │ X-Amz-Expires │ │
│ │ X-Amz-Signature │ │
│ └──────────┬─────────────┘ │
│ ▼ │
│ ┌────────────────────────┐ No │
│ │ 2. 만료 확인 │──────→ 403 AccessDenied │
│ │ now > Date + Expires? │ (Request has expired) │
│ └──────────┬─────────────┘ │
│ │ Yes (유효) │
│ ▼ │
│ ┌────────────────────────┐ No │
│ │ 3. 시계 오차 확인 │──────→ 403 RequestTimeToo │
│ │ |now - Date| < 15분? │ Skewed │
│ └──────────┬─────────────┘ │
│ │ Yes │
│ ▼ │
│ ┌────────────────────────┐ No │
│ │ 4. Access Key 유효성 │──────→ 403 InvalidAccessKey │
│ │ Credential의 키 확인 │ │
│ └──────────┬─────────────┘ │
│ │ Yes │
│ ▼ │
│ ┌────────────────────────┐ │
│ │ 5. 서명 재계산 │ │
│ │ 수신된 요청으로 │ │
│ │ Canonical Request 구성 │ │
│ │ → StringToSign 생성 │ │
│ │ → Signing Key 파생 │ │
│ │ → HMAC-SHA256 계산 │ │
│ └──────────┬─────────────┘ │
│ ▼ │
│ ┌────────────────────────┐ No │
│ │ 6. 서명 비교 │──────→ 403 Signature │
│ │ 계산값 == URL 서명? │ DoesNotMatch │
│ └──────────┬─────────────┘ │
│ │ Yes │
│ ▼ │
│ ┌────────────────────────┐ No │
│ │ 7. IAM 권한 확인 │──────→ 403 AccessDenied │
│ │ 서명자가 해당 객체에 │ │
│ │ 요청된 작업 권한 있는가│ │
│ └──────────┬─────────────┘ │
│ │ Yes │
│ ▼ │
│ 200 OK + 객체 반환 │
│ │
└─────────────────────────────────────────────────────────────┘
핵심: S3 서버는 URL의 서명을 “신뢰”하지 않는다. 수신된 요청의 모든 요소로 서명을 처음부터 다시 계산하여 URL에 포함된 서명과 비교한다. Secret Access Key는 서버(S3)와 서명자(앱 서버) 모두 알고 있으므로 동일한 계산 결과가 나와야 한다.
2.6 만료 메커니즘
만료 시간은 X-Amz-Date + X-Amz-Expires 조합으로 결정된다.
만료 시점 = X-Amz-Date + X-Amz-Expires
예시: X-Amz-Date=20260316T120000Z, X-Amz-Expires=3600
→ 만료 시점 = 2026-03-16 13:00:00 UTC
자격증명 유형별 최대 유효기간
| 자격증명 유형 | 최대 X-Amz-Expires | 이유 |
|---|---|---|
| IAM User (장기 키) | 604,800초 (7일) | 키 자체에 만료가 없으므로 AWS가 상한 설정 |
| STS 임시 자격증명 | 세션 토큰 만료까지 | 토큰이 먼저 만료되면 URL도 무효 |
| EC2 Instance Role | ~6시간 | 인스턴스 역할 토큰이 보통 6시간 |
| Lambda 실행 역할 | ~15분 ~ 1시간 | Lambda 세션 토큰 유효기간에 종속 |
주의:
X-Amz-Expires를 아무리 길게 설정해도, 서명에 사용된 자격증명(Access Key, STS 토큰)이 만료/삭제되면 URL은 즉시 무효화된다.
3. 용어 사전
| 용어 | 원어 | 설명 |
|---|---|---|
| Pre-signed | Pre(미리) + Signed(서명된) | 사용 시점 이전에 서명이 완성되어 URL에 내포된 상태 |
| HMAC | Hash-based Message Authentication Code | 비밀 키와 해시 함수를 결합한 메시지 인증 코드. RFC 2104 (1997) |
| HMAC-SHA256 | HMAC with SHA-256 | HMAC에 SHA-256 해시 함수를 사용한 변형. SigV4의 핵심 알고리즘 |
| SHA-256 | Secure Hash Algorithm 256-bit | 256비트 해시를 생성하는 암호학적 해시 함수. FIPS 180-4 (2012) |
| Canonical Request | 정규 요청 | HTTP 요청을 정해진 규칙(canon)에 따라 표준화한 문자열 |
| StringToSign | 서명 대상 문자열 | 알고리즘 + 타임스탬프 + Credential Scope + Canonical Request 해시 |
| Signing Key | 서명 키 | Secret Key에서 4단계 HMAC 체인으로 파생된 키 |
| Credential Scope | 자격증명 범위 | 날짜/리전/서비스/aws4_request 형식. 서명의 유효 범위 제한 |
| SigV4 | AWS Signature Version 4 | 2012년 도입된 AWS 서명 프로토콜. 현행 표준 |
| SigV2 | AWS Signature Version 2 | 구 서명 프로토콜. 2020년 6월 완전 종료 |
| Query String Authentication | 쿼리 문자열 인증 | Authorization 헤더 대신 URL 파라미터로 인증 정보를 전달하는 방식 |
| Capability URL | 능력 URL | URL 자체가 접근 권한(능력)을 담고 있는 URL. Presigned URL은 이 패턴 |
| STS | Security Token Service | AWS 임시 보안 자격증명을 발급하는 서비스 |
| X-Amz-Algorithm | - | 서명 알고리즘 식별자. AWS4-HMAC-SHA256 |
| X-Amz-Credential | - | Access Key ID + Credential Scope |
| X-Amz-Date | - | 서명 생성 시각 (ISO 8601 UTC) |
| X-Amz-Expires | - | URL 유효 시간 (초 단위) |
| X-Amz-SignedHeaders | - | 서명에 포함된 헤더 목록 |
| X-Amz-Signature | - | 최종 HMAC-SHA256 서명값 (64자리 hex) |
| X-Amz-Security-Token | - | STS 임시 자격증명 사용 시 세션 토큰 |
| X-Amz- 접두사 | Extension-Amazon | HTTP 비표준 확장 헤더임을 나타내는 Amazon 전용 접두사 |
4. 역사와 진화
4.1 연대표
┌─────────────────────────────────────────────────────────────┐
│ │
│ Presigned URL 관련 연대표 │
│ │
│ 1966 Dennis & Van Horn │
│ └── Capability-based Security 개념 제안 (ACM 논문) │
│ │
│ 1988 Norm Hardy │
│ └── Confused Deputy Problem 정의 │
│ │
│ 1997 RFC 2104 (Bellare, Canetti, Krawczyk) │
│ └── HMAC 표준화 │
│ │
│ 2006 Amazon S3 출시 (2006.03.14) │
│ └── Query String Authentication 출시 당시부터 존재 │
│ (SigV2 기반) │
│ │
│ 2012 AWS Signature Version 4 도입 │
│ └── HMAC-SHA256 기반, 리전별 서명 범위 제한 │
│ SHA-256: FIPS 180-4 표준화 │
│ │
│ 2013 GCP Signed URL 출시 │
│ └── RSA + SHA-256 기반 (비대칭 키 방식) │
│ │
│ 2013 Azure SAS Token 출시 │
│ └── HMAC-SHA256 기반, 3가지 SAS 유형 │
│ │
│ 2014 SigV4 신규 리전 의무화 (2014.01.30~) │
│ └── 기존 리전은 SigV2 병행 허용 │
│ │
│ 2020 SigV2 완전 종료 (2020.06) │
│ └── 모든 리전에서 SigV4만 허용 │
│ │
└─────────────────────────────────────────────────────────────┘
4.2 SigV2 vs SigV4 비교표
| 항목 | SigV2 | SigV4 |
|---|---|---|
| 해시 알고리즘 | HMAC-SHA1 / HMAC-SHA256 | HMAC-SHA256 전용 |
| 서명 범위 | 전역 (리전 구분 없음) | 날짜/리전/서비스로 범위 제한 |
| 키 파생 | Secret Key 직접 사용 | 4단계 HMAC 체인으로 파생 |
| Credential Scope | 없음 | 날짜/리전/서비스/aws4_request |
| 보안 수준 | 키 재사용 범위가 넓음 | 키가 특정 날짜+리전+서비스에만 유효 |
| Streaming 서명 | 미지원 | Chunked Transfer 서명 지원 |
| 현재 상태 | 2020.06 완전 종료 | 현행 표준 |
SigV4의 핵심 개선점: Secret Key에서 매일/리전별/서비스별 파생 키를 생성하므로, 키가 유출되어도 피해 범위가 특정 날짜/리전/서비스로 한정된다.
4.3 Capability-based Security 이론
Presigned URL은 Capability-based Security 패러다임의 실전 구현이다.
| 개념 | 설명 | Presigned URL 대응 |
|---|---|---|
| Capability | 객체에 대한 접근 권한을 담은 위조 불가능한 토큰 | URL 자체가 capability 토큰 |
| Possession is authority | 토큰을 소유한 것만으로 권한 행사 | URL을 가진 누구나 접근 가능 |
| Unforgeable | 토큰 위조 불가 | HMAC-SHA256 서명으로 보장 |
| Attenuable | 권한을 줄일 수 있음 (위임 시) | 만료 시간, HTTP 메서드, 경로로 제한 |
| Confused Deputy | 대리인이 의도치 않게 권한 남용 | Credential Scope로 범위 제한 |
Dennis & Van Horn(1966)이 제안한 이론이 60년 후 클라우드 스토리지에서 실현된 것이다.
5. 대안 비교
5.1 AWS 내: S3 Presigned URL vs CloudFront Signed URL vs Signed Cookies
| 항목 | S3 Presigned URL | CloudFront Signed URL | CloudFront Signed Cookies |
|---|---|---|---|
| 서명 방식 | HMAC-SHA256 (대칭 키) | RSA-SHA1 (비대칭 키) | RSA-SHA1 (비대칭 키) |
| 키 관리 | IAM Access Key | CloudFront Key Pair | CloudFront Key Pair |
| 범위 | 단일 S3 객체 | 단일 URL 또는 와일드카드 패턴 | 다수 파일 (쿠키 범위) |
| 캐싱 | S3 직접 접근 (캐시 없음) | 엣지 캐시 활용 | 엣지 캐시 활용 |
| 성능 | S3 리전까지 RTT | 가장 가까운 엣지 | 가장 가까운 엣지 |
| IP 제한 | 불가 (Bucket Policy 조합 필요) | Policy 내 IP 조건 가능 | Policy 내 IP 조건 가능 |
| 생성 비용 | < 1ms (로컬 HMAC) | ~3ms (ECDSA) / ~34ms (RSA) | ~3ms (ECDSA) |
| 최적 용도 | 단일 파일 업/다운로드 | CDN 배포 + 접근 제어 | 스트리밍, 다수 리소스 |
5.2 클라우드 간: AWS vs GCP vs Azure
| 항목 | AWS S3 Presigned URL | GCP Signed URL | Azure SAS Token |
|---|---|---|---|
| 서명 알고리즘 | HMAC-SHA256 | RSA-SHA256 또는 HMAC | HMAC-SHA256 |
| 키 유형 | 대칭 키 (Secret Access Key) | 비대칭 키 (서비스 계정) 또는 HMAC | 대칭 키 (Storage Key) |
| 최대 유효기간 | 7일 (IAM User) | 7일 (V4) | 무제한 (Account SAS) |
| SAS 유형 | 단일 유형 | 단일 유형 | 3유형: User Delegation / Service / Account |
| 권한 세분화 | HTTP 메서드 단위 | HTTP 메서드 단위 | 읽기/쓰기/삭제/목록 등 개별 지정 |
| IP 제한 | Bucket Policy 조합 | signedIP 파라미터 | sip (signedIP) 파라미터 |
| HTTPS 강제 | 별도 설정 | signedProtocol | spr=https |
| 취소 메커니즘 | 키 삭제/비활성화 | 서비스 계정 키 삭제 | Stored Access Policy 변경 |
| V2/V4 구분 | V4 (V2 종료) | V2(무기한)/V4(7일 상한) | N/A |
Azure SAS 3유형
| 유형 | 키 소스 | 특징 |
|---|---|---|
| User Delegation SAS | Azure AD + Storage Key | 가장 안전. Azure AD 인증 필요 |
| Service SAS | Storage Account Key | 특정 서비스(Blob, Queue 등) 범위 |
| Account SAS | Storage Account Key | 계정 수준 전체 범위 |
5.3 상황별 최적 선택 가이드
| 시나리오 | 최적 선택 | 이유 |
|---|---|---|
| 단일 파일 다운로드 링크 생성 | S3 Presigned URL | 가장 단순, 추가 인프라 불필요 |
| 브라우저 직접 업로드 | S3 Presigned URL (PUT) 또는 POST Policy | 서버 무부하 업로드 |
| CDN 경유 콘텐츠 배포 + 접근 제어 | CloudFront Signed URL | 엣지 캐시 + 접근 제어 |
| HLS/DASH 스트리밍 (다수 세그먼트) | CloudFront Signed Cookies | 세그먼트마다 URL 불필요 |
| 불특정 다수에게 공개 | Bucket Policy (public-read) | Presigned URL 불필요 |
| Cross-account 접근 | IAM Role + Bucket Policy | 영구적 접근이면 Presigned URL 부적합 |
| 대용량 파일 (5GB 초과) | Presigned URL + Multipart Upload | 파트별 Presigned URL 생성 |
| 동적 콘텐츠 변환 (리사이즈 등) | S3 Object Lambda | 접근 시점에 변환 |
| 전 세계 사용자 대상 정적 사이트 | CloudFront + S3 OAC | CDN 캐싱, HTTPS, DDoS 방어 |
| 규정 준수 감사 추적 필요 | STS + CloudTrail | 임시 자격증명 추적 가능 |
| 멀티클라우드 호환 | 각 CSP Signed URL | 서명 방식이 다르므로 추상화 레이어 필요 |
6. 실전 구현 패턴
6.1 다운로드 아키텍처
┌─────────────────────────────────────────────────────────────┐
│ │
│ 안전한 다운로드 아키텍처 │
│ │
│ ┌────────┐ 1. GET /api/download/report-q1 │
│ │ Client │ ──────────────────────────────→ ┌────────────┐ │
│ │ │ │ App Server │ │
│ │ │ ←────────────────────────────── │ (Python) │ │
│ │ │ 2. 302 Redirect │ │ │
│ │ │ Location: https://bucket │ ① 권한 확인│ │
│ │ │ .s3...?X-Amz-Signature=... │ ② URL 생성 │ │
│ └───┬────┘ └────────────┘ │
│ │ │
│ │ 3. GET (Presigned URL로 리다이렉트) │
│ ▼ │
│ ┌─────────────────┐ │
│ │ AWS S3 │ 4. 서명 검증 → 객체 반환 │
│ │ (데이터 직접 │ │
│ │ 전송) │ │
│ └─────────────────┘ │
│ │
│ 서버는 URL만 생성 (1ms), 실제 전송은 S3가 담당 │
│ │
└─────────────────────────────────────────────────────────────┘
Python 코드 (boto3)
import boto3
from botocore.config import Config
s3_client = boto3.client(
's3',
region_name='ap-northeast-2',
config=Config(signature_version='s3v4')
)
def generate_download_url(bucket: str, key: str, expires_in: int = 300) -> str:
"""
다운로드용 Presigned URL 생성.
expires_in: 유효 시간 (초). 기본 5분.
"""
url = s3_client.generate_presigned_url(
ClientMethod='get_object',
Params={
'Bucket': bucket,
'Key': key,
'ResponseContentDisposition': 'attachment; filename="report.pdf"',
},
ExpiresIn=expires_in,
)
return url
# 사용 예
url = generate_download_url('my-bucket', 'reports/2026/Q1.pdf', expires_in=300)
# → 5분간 유효한 다운로드 URL 반환
6.2 업로드 아키텍처
PUT vs POST Policy 비교
| 항목 | Presigned PUT URL | POST Policy (Presigned POST) |
|---|---|---|
| HTTP 메서드 | PUT | POST (multipart/form-data) |
| 파일 키 결정 | 서버가 미리 지정 | Policy 조건으로 패턴 허용 가능 |
| Content-Type 제한 | 서명 시 지정 가능 | Policy 조건으로 제한 |
| 파일 크기 제한 | 불가 (서명에 미포함) | content-length-range 조건으로 제한 |
| 추가 메타데이터 | 헤더로 전달 | 폼 필드로 전달 |
| 브라우저 호환성 | fetch / XMLHttpRequest 필요 |
<form> 태그로 직접 제출 가능 |
| 추천 용도 | 프로그래밍 환경 업로드 | 브라우저 폼 업로드, 크기 제한 필요 시 |
Python 코드 - PUT 업로드 URL 생성
def generate_upload_url(bucket: str, key: str, content_type: str = 'application/octet-stream',
expires_in: int = 300) -> str:
"""업로드용 Presigned PUT URL 생성."""
url = s3_client.generate_presigned_url(
ClientMethod='put_object',
Params={
'Bucket': bucket,
'Key': key,
'ContentType': content_type,
},
ExpiresIn=expires_in,
)
return url
# 클라이언트 측 (JavaScript)
# fetch(url, { method: 'PUT', body: file, headers: { 'Content-Type': 'image/png' } })
Python 코드 - POST Policy 업로드
def generate_post_policy(bucket: str, key_prefix: str, max_size_mb: int = 10,
expires_in: int = 300) -> dict:
"""POST Policy 기반 업로드 폼 데이터 생성. 파일 크기 제한 가능."""
conditions = [
['starts-with', '$key', key_prefix],
['content-length-range', 1, max_size_mb * 1024 * 1024],
{'Content-Type': 'image/'},
]
post = s3_client.generate_presigned_post(
Bucket=bucket,
Key=f'{key_prefix}/$',
Conditions=conditions,
ExpiresIn=expires_in,
)
return post
# 반환: { 'url': 'https://bucket.s3...', 'fields': { 'key': ..., 'policy': ..., ... } }
7. 보안 위험과 베스트 프랙티스
7.1 URL 유출 경로
| 유출 경로 | 설명 | 위험도 |
|---|---|---|
| 서버 접근 로그 | Nginx, Apache, ALB 로그에 전체 URL 기록 | 높음 |
| Referer 헤더 | Presigned URL 페이지에서 외부 링크 클릭 시 Referer로 전달 | 높음 |
| 브라우저 히스토리 | 주소창에 노출된 URL이 히스토리에 저장 | 중간 |
| 메신저/이메일 | URL을 공유하면 서버에 로그 남음 (미리보기 봇 접근) | 높음 |
| CDN/프록시 캐시 | 중간 프록시가 URL을 캐시 키로 저장 | 중간 |
| 스크린샷/화면 공유 | 주소창에 노출된 URL이 캡처 | 낮음 |
| 클라이언트 측 JS 로그 | 에러 트래킹 도구(Sentry 등)가 URL 수집 | 높음 |
7.2 안티패턴
| 안티패턴 | 문제점 | 대안 |
|---|---|---|
| X-Amz-Expires=604800 (7일) | 유출 시 7일간 악용 가능 | 최소 필요 시간만 설정 (5~15분 권장) |
| 공개 버킷 + Presigned URL | Presigned URL이 무의미 (이미 누구나 접근 가능) | Block Public Access 활성화 |
| 프론트엔드에 IAM 키 노출 | Secret Key가 브라우저에 노출 | 서버에서만 URL 생성, 프론트엔드는 URL만 수신 |
| IDOR (경로 추측) | 사용자가 경로 패턴 추측하여 다른 사용자 파일 요청 | 서버에서 권한 검증 후 URL 생성 |
| HTTP로 Presigned URL 전달 | 중간자가 URL 탈취 가능 | HTTPS 전용 |
| 단일 장기 IAM User 키 사용 | 키 유출 시 피해 범위가 넓음 | STS 임시 자격증명 사용 |
7.3 베스트 프랙티스
| 프랙티스 | 설명 |
|---|---|
| 최소 만료 시간 | 용도에 맞는 최소 시간 설정. 다운로드: 5분, 업로드: 15분 |
| HTTPS 전용 | Presigned URL 자체와 URL을 전달하는 API 모두 HTTPS |
| STS 임시 자격증명 | IAM User 장기 키 대신 AssumeRole로 임시 키 사용 |
| Content-Type 제한 | 업로드 시 허용 Content-Type을 서명에 포함 |
| s3:signatureAge 조건 | Bucket Policy에서 서명 나이 강제 제한 |
| 서버 측 권한 검증 | URL 생성 전 반드시 요청자의 권한 확인 |
| 고유 키 생성 | UUID 기반 키로 경로 추측 방지 |
| VPC Endpoint | 내부 트래픽은 VPC Endpoint 경유로 인터넷 미노출 |
| CloudTrail 활성화 | S3 데이터 이벤트 로깅으로 접근 추적 |
| Referer 차단 | Referrer-Policy: no-referrer 헤더 설정 |
s3:signatureAge Bucket Policy 예시
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "DenyOldSignatures",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"NumericGreaterThan": {
"s3:signatureAge": 300000
}
}
}]
}
이 정책은 서명 생성 후 5분(300,000ms) 초과된 요청을 거부한다. X-Amz-Expires를 7일로 설정해도 서버 측에서 5분으로 강제 제한할 수 있다.
7.4 보안 체크리스트
┌─────────────────────────────────────────────────────────────┐
│ │
│ Presigned URL 보안 체크리스트 │
│ │
│ [ ] Block Public Access 4개 항목 모두 활성화 │
│ [ ] X-Amz-Expires를 최소 필요 시간으로 설정 (5~15분) │
│ [ ] STS 임시 자격증명 사용 (IAM User 장기 키 미사용) │
│ [ ] HTTPS 전용 (HTTP 접근 차단) │
│ [ ] 서버 측에서 요청자 권한 검증 후 URL 생성 │
│ [ ] s3:signatureAge Bucket Policy 설정 │
│ [ ] 업로드 시 Content-Type 제한 │
│ [ ] UUID 기반 객체 키 (경로 추측 방지) │
│ [ ] 서버 로그에서 쿼리 스트링 마스킹 또는 제외 │
│ [ ] Referer 헤더 전파 차단 (Referrer-Policy) │
│ [ ] CloudTrail S3 데이터 이벤트 로깅 활성화 │
│ [ ] VPC Endpoint 사용 (내부 트래픽) │
│ [ ] 에러 트래킹 도구에서 URL 파라미터 필터링 │
│ [ ] POST Policy 사용 시 content-length-range 조건 설정 │
│ [ ] 자격증명 교체(rotation) 자동화 │
│ │
└─────────────────────────────────────────────────────────────┘
8. 트러블슈팅
8.1 SignatureDoesNotMatch
가장 흔한 오류. 클라이언트가 보낸 요청으로 S3가 재계산한 서명과 URL의 서명이 불일치.
| 원인 | 상세 | 해결 |
|---|---|---|
| Region 불일치 | URL 생성 시 us-east-1, 실제 버킷은 ap-northeast-2 |
버킷 리전과 클라이언트 리전 일치시킴 |
| Virtual-hosted vs Path-style | SDK가 Path-style로 서명, S3가 Virtual-hosted로 해석 | s3v4 서명 + Virtual-hosted 사용 |
| URL 인코딩 불일치 | 특수문자 키를 이중 인코딩 또는 인코딩 누락 | SDK의 URL 생성 함수 사용 (수동 인코딩 금지) |
| Content-Type 불일치 | 서명 시 image/png, 요청 시 application/octet-stream |
서명과 요청의 Content-Type 일치시킴 |
| Transfer Acceleration | 일반 엔드포인트로 서명, Acceleration 엔드포인트로 요청 | 서명 시 s3-accelerate 엔드포인트 사용 |
| 시계 오차 | 서명 생성 서버의 시간이 틀림 | NTP 동기화 확인 |
8.2 RequestTimeTooSkewed
┌─────────────────────────────────────────────────────────────┐
│ │
│ 오류: RequestTimeTooSkewed │
│ │
│ 원인: 서명의 X-Amz-Date와 S3 서버 현재 시간 차이 > 15분 │
│ │
│ 발생 시나리오: │
│ ├── URL 생성 서버의 시계가 15분 이상 어긋남 │
│ ├── URL 생성 후 15분 이상 경과 (X-Amz-Expires와 별개) │
│ └── 클라이언트 측 시계 문제 (S3는 서버 시계 사용) │
│ │
│ 해결: │
│ ├── NTP 서비스 활성화 및 시계 동기화 │
│ ├── Amazon Time Sync Service 사용 (169.254.169.123) │
│ └── URL 생성 직후 바로 사용 (캐싱 금지) │
│ │
│ 참고: 15분 허용 오차는 AWS가 설정한 고정값 (변경 불가) │
│ │
└─────────────────────────────────────────────────────────────┘
8.3 ExpiredToken
| 상황 | 원인 | 해결 |
|---|---|---|
| STS 토큰 만료 | AssumeRole로 받은 임시 토큰이 만료됨 |
토큰 갱신 후 URL 재생성 |
| EC2 역할 토큰 만료 | 인스턴스 메타데이터에서 자동 갱신된 토큰으로 서명해야 함 | SDK 최신 버전 사용 (자동 갱신) |
| Lambda 콜드 스타트 후 토큰 | 캐시된 오래된 토큰 사용 | 매 호출 시 새 클라이언트 생성 또는 토큰 갱신 확인 |
| IAM 키 삭제/비활성화 | 서명에 사용된 Access Key가 삭제됨 | 새 키로 URL 재생성 |
주의:
X-Amz-Expires가 아직 유효해도, 서명에 사용된 자격증명이 만료/삭제되면 URL은 즉시 무효화된다. AWS는 만료 시간과 자격증명 유효성을 독립적으로 검증한다.
9. 참고 자료
| 자료 | 링크/출처 |
|---|---|
| AWS 공식: Authenticating Requests (Query String) | docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html |
| AWS 공식: Signature Version 4 Signing Process | docs.aws.amazon.com/general/latest/gr/signature-version-4.html |
| RFC 2104: HMAC (1997) | Bellare, Canetti, Krawczyk |
| FIPS 180-4: SHA-256 (2012) | NIST |
| Dennis & Van Horn (1966) | “Programming Semantics for Multiprogrammed Computations”, ACM |
| Norm Hardy (1988) | “The Confused Deputy” |
| GCP Signed URLs | cloud.google.com/storage/docs/access-control/signed-urls |
| Azure SAS Tokens | learn.microsoft.com/en-us/azure/storage/common/storage-sas-overview |
| AWS SigV2 종료 공지 (2020) | aws.amazon.com/blogs/aws/amazon-s3-path-deprecation-plan-the-rest-of-the-story/ |
관련 키워드
Presigned URL, AWS Signature Version 4, SigV4, HMAC-SHA256, Query String Authentication, Canonical Request, StringToSign, Signing Key, Credential Scope, X-Amz-Signature, X-Amz-Expires, X-Amz-Credential, Capability-based Security, Capability URL, CloudFront Signed URL, Signed Cookies, Azure SAS Token, GCP Signed URL, S3, STS, 임시 자격증명, 서명 검증, POST Policy, s3:signatureAge, RFC 2104, Dennis & Van Horn