S3 Presigned URL 생성 완전 가이드 — SDK로 1시간 임시 다운로드 링크 만들기
프라이빗 S3 버킷에 저장된 파일을 외부 사용자에게 일시적으로 공개해야 할 때, 버킷 정책을 건드리거나 퍼블릭 ACL을 열지 않고도 해결하는 방법이 바로 Presigned URL이다. 실제 운영 환경에서는 '이 파일 한 번만 다운로드할 수 있게 해줘'라는 요구가 생각보다 자주 온다 — 계약서 전달, 리포트 공유, 미디어 파일 배포 등. S3 Presigned URL은 서버 측에서 서명된 임시 접근 토큰을 URL에 포함시켜, 지정된 만료 시간 내에만 해당 객체에 접근할 수 있도록 제어한다.
TL;DR — S3 Presigned URL 핵심 요약
| 항목 | 내용 |
|---|---|
| 목적 | IAM 자격증명 없이 특정 S3 객체에 임시 접근 허용 |
| 서명 방식 | SigV4 (AWS Signature Version 4) |
| 기본 만료 범위 | 최소 1초 ~ 최대 7일 (IAM 역할 기반 서명 시 최대 12시간) |
| SDK 메서드 | Python: generate_presigned_url / JS: getSignedUrl (v2), getSignedUrl from @aws-sdk/s3-request-presigner (v3) |
| 주요 주의사항 | 서명에 사용된 자격증명이 만료되면 URL도 즉시 무효화됨 |
S3 Presigned URL 동작 원리
Presigned URL은 S3 서비스가 생성하는 것이 아니다. SDK를 실행하는 서버(혹은 Lambda)가 로컬에서 서명을 계산해 URL에 포함시킨다. S3는 요청이 들어왔을 때 URL에 포함된 서명 파라미터를 검증할 뿐이다. 이 차이가 중요한 이유는 — 서명을 생성한 자격증명(IAM 사용자 또는 역할)이 비활성화되거나 만료되면, URL 자체의 만료 시간이 남아 있어도 요청이 거부된다.
- 애플리케이션 서버가 AWS SDK를 사용해 로컬에서 SigV4 서명을 계산한다.
- 서명, 만료 시간, 자격증명 정보가 쿼리 파라미터로 인코딩된 URL이 생성된다.
- 서버가 이 URL을 클라이언트(브라우저, 앱 등)에 전달한다.
- 클라이언트는 S3에 직접 GET 요청을 보낸다 — 서버를 거치지 않는다.
- S3는 URL의 서명과 만료 시간을 검증한 후 객체를 반환하거나 403을 응답한다.
Presigned URL은 '위임장'에 가깝다. 서명자의 권한을 URL 소지자에게 일시적으로 위임하는 것이지, S3 버킷의 접근 정책을 변경하는 것이 아니다.
S3 Presigned URL 생성 전 IAM 권한 확인
URL을 생성하는 주체(IAM 사용자 또는 역할)는 해당 S3 객체에 대한 s3:GetObject 권한을 가지고 있어야 한다. 권한이 없는 자격증명으로 생성된 Presigned URL은 만료 전이라도 S3 요청 시 AccessDenied를 반환한다.
최소 권한 원칙에 따른 IAM 정책 예시:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::example-bucket/private/*"
}
]
}
버킷 정책에 명시적 Deny가 있다면 IAM Allow를 덮어쓴다. Presigned URL 생성 전에 버킷 정책도 반드시 확인해야 한다.
Python (boto3)로 S3 Presigned URL 생성하기
boto3의 generate_presigned_url 메서드는 가장 직관적인 방법이다. ExpiresIn 파라미터는 초 단위이므로 1시간은 3600으로 지정한다.
import boto3
from botocore.exceptions import ClientError
def generate_presigned_url(bucket_name: str, object_key: str, expiration: int = 3600) -> str | None:
"""
S3 프라이빗 객체에 대한 Presigned URL을 생성한다.
:param bucket_name: S3 버킷 이름
:param object_key: S3 객체 키 (경로 포함)
:param expiration: 만료 시간 (초 단위, 기본값 3600 = 1시간)
:return: Presigned URL 문자열, 실패 시 None
"""
s3_client = boto3.client('s3', region_name='us-east-1')
try:
url = s3_client.generate_presigned_url(
ClientMethod='get_object',
Params={
'Bucket': bucket_name,
'Key': object_key
},
ExpiresIn=expiration
)
return url
except ClientError as e:
print(f'Presigned URL 생성 실패: {e}')
return None
# 사용 예시
if __name__ == '__main__':
url = generate_presigned_url(
bucket_name='example-bucket',
object_key='private/report-2024-q4.pdf'
)
if url:
print(f'다운로드 URL: {url}')
리전을 명시적으로 지정하지 않으면 서명 불일치로 인해 특정 리전에서 AuthorizationQueryParametersError가 발생할 수 있다. region_name은 항상 버킷이 위치한 리전으로 고정하는 것이 안전하다.
Node.js AWS SDK v3로 S3 Presigned URL 생성하기
AWS SDK v3는 v2와 달리 Presigned URL 생성 기능이 별도 패키지(@aws-sdk/s3-request-presigner)로 분리되어 있다. v2 방식의 코드를 그대로 v3에 적용하면 동작하지 않는다 — 이 부분에서 마이그레이션 시 자주 막힌다.
🔽 Node.js SDK v3 전체 코드 보기
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const s3Client = new S3Client({ region: 'us-east-1' });
async function generatePresignedUrl(
bucketName: string,
objectKey: string,
expiresInSeconds: number = 3600
): Promise<string> {
const command = new GetObjectCommand({
Bucket: bucketName,
Key: objectKey,
});
const url = await getSignedUrl(s3Client, command, {
expiresIn: expiresInSeconds,
});
return url;
}
// 사용 예시
(async () => {
const url = await generatePresignedUrl(
'example-bucket',
'private/report-2024-q4.pdf'
);
console.log('다운로드 URL:', url);
})();
패키지 설치:
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
AWS CLI로 S3 Presigned URL 빠르게 생성하기
로컬 테스트나 스크립트 자동화 시 CLI가 가장 빠르다. --expires-in은 초 단위다.
aws s3 presign s3://example-bucket/private/report-2024-q4.pdf \
--expires-in 3600 \
--region us-east-1
CLI는 현재 설정된 AWS 프로파일의 자격증명으로 서명한다. IAM 역할을 assume한 상태라면 임시 자격증명의 만료 시간이 --expires-in보다 짧을 수 있다 — 이 경우 URL은 역할 세션 만료 시점에 먼저 무효화된다.
운영 중 마주치는 실제 문제 — 서명은 됐는데 403이 뜬다
Presigned URL을 생성했는데 브라우저에서 403 AccessDenied가 반환되는 경우, 대부분 세 가지 원인 중 하나다. URL 만료는 403이 아니라 403 RequestExpired 또는 403 AccessDenied로 응답하므로 에러 XML 본문을 직접 확인해야 한다.
또는 서명 불일치"] B --> D["AccessDenied
(권한 문제)"] B --> E["RequestExpired
(만료)"] C --> C1["리전 불일치 확인
region_name 명시"] D --> D1{"원인 분류"} D1 --> D2["IAM 정책에
s3:GetObject 없음"] D1 --> D3["버킷 정책
명시적 Deny"] D1 --> D4["역할 세션
만료"] D2 --> D2a["IAM 정책 시뮬레이터로 확인"] D3 --> D3a["get-bucket-policy로 확인"] D4 --> D4a["MaxSessionDuration vs ExpiresIn 비교"] E --> E1["URL 재생성 필요"]
원인 1: 서명 자격증명에 s3:GetObject 권한 없음
IAM 정책 시뮬레이터나 아래 CLI로 확인한다:
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::123456789012:role/MyAppRole \
--action-names s3:GetObject \
--resource-arns arn:aws:s3:::example-bucket/private/report-2024-q4.pdf
원인 2: 버킷 정책에 명시적 Deny 존재
버킷 정책을 직접 확인한다:
aws s3api get-bucket-policy \
--bucket example-bucket \
--region us-east-1
원인 3: IAM 역할 세션이 URL 만료 전에 종료됨
Lambda나 ECS Task 역할로 서명한 경우, 역할의 최대 세션 지속 시간이 Presigned URL의 ExpiresIn보다 짧으면 역할 세션 만료 시점에 URL이 무효화된다. 역할의 최대 세션 시간을 확인한다:
aws iam get-role \
--role-name MyAppRole \
--query 'Role.MaxSessionDuration'
이 값이 3600(1시간)이고 역할을 assume한 지 이미 50분이 지났다면, 생성된 Presigned URL은 10분 후 무효화된다 — ExpiresIn을 3600으로 설정했더라도.
S3 Presigned URL 만료 시간 설계 기준
만료 시간은 짧을수록 보안상 유리하지만, 사용자 경험과 트레이드오프가 있다. 운영 환경에서 자주 쓰는 기준:
| 사용 시나리오 | 권장 만료 시간 | 이유 |
|---|---|---|
| 즉시 다운로드 (버튼 클릭) | 5~15분 | URL 생성 직후 사용, 장시간 노출 불필요 |
| 이메일/메시지로 전달 | 1~24시간 | 수신자가 나중에 열 수 있음 |
| 대용량 파일 멀티파트 업로드 | 업로드 예상 시간 + 여유 | 업로드 중 만료되면 전체 재시도 필요 |
| 공개 배포 금지 파일 | 최대 1시간 이하 | URL 유출 시 피해 최소화 |
S3 Presigned URL 생성 마무리 및 다음 단계
Presigned URL은 구현 자체는 단순하지만, 서명 자격증명의 수명 관리와 버킷 정책 충돌이 운영 중 가장 자주 문제가 된다. 특히 Lambda나 ECS처럼 임시 자격증명을 사용하는 환경에서는 역할 세션 만료 시간을 반드시 고려해야 한다.
다음 단계로 고려할 수 있는 패턴:
- Presigned URL 생성 API를 Lambda + API Gateway로 래핑해 서버리스 다운로드 엔드포인트 구성
- CloudFront Signed URL / Signed Cookie와 비교해 CDN 캐싱이 필요한 경우 적합한 방식 선택
- S3 액세스 로그 또는 CloudTrail로 Presigned URL을 통한 실제 다운로드 감사
공식 문서: AWS S3 — Sharing objects with presigned URLs
핵심 용어 정리
| 용어 | 설명 |
|---|---|
| Presigned URL | 서명된 쿼리 파라미터를 포함해 임시 접근을 허용하는 S3 URL |
| SigV4 (Signature Version 4) | AWS 요청 서명 프로토콜. Presigned URL의 서명 방식 |
| ExpiresIn | Presigned URL의 유효 기간 (초 단위) |
| IAM 역할 세션 만료 | 임시 자격증명 기반 서명 시, 역할 세션 종료 시점에 URL도 무효화됨 |
| s3:GetObject | S3 객체 다운로드에 필요한 IAM 액션 |
댓글
댓글 쓰기