글

S3 미리 서명된 URL: 개념, 작동 방식, 만료 제한, 코드 예시

S3 미리 서명된 URL이란 무엇인지, X-Amz 매개변수의 의미, 7일 만료 제한, 업로드와 다운로드 코드(CLI, boto3, JS), 흔한 오류 해결 방법을 알아보세요.

S3 미리 서명된 URL은 비공개 버킷의 객체 하나를 가리키는 임시 링크예요. 쿼리 문자열에 서명이 들어 있어서, 링크를 가진 사람은 링크가 만료될 때까지 AWS 자격 증명 없이 그 객체를 다운로드(또는 업로드)할 수 있고, 버킷은 절대 공개되지 않아요. 코드나 AWS CLI에서 내 키로 만들며, 1초부터 7일까지 유지할 수 있어요. 이 가이드에서는 작동 방식, 모든 X-Amz-* 매개변수의 의미, 유지 기간, 다운로드와 업로드 코드, 그리고 가장 자주 마주치는 오류를 다뤄요. 제한은 2026년 10월 기준 AWS와 Cloudflare 문서에서 가져왔어요.

S3 미리 서명된 URL 한눈에 보기

질문답변
무엇을 허용하나요?객체 하나에 대한 작업 하나 (GET, PUT, HEAD, DELETE…)
최대 만료 시간SigV4와 IAM 사용자 키로 7일 (604,800초)
임시 키 사용 시 만료역할 세션 또는 STS 자격 증명이 끝날 때, 둘 중 먼저 오는 시점
S3 콘솔 범위1분에서 12시간
기본 만료 시간AWS CLI와 boto3는 3,600초, JavaScript v3 presigner는 900초
여러 번 사용할 수 있나요?네, 만료될 때까지 누구든 원하는 만큼 사용할 수 있어요
URL 하나만 취소할 수 있나요?아니요, 서명한 자격 증명을 취소하거나 정책으로 거부해야만 해요
생성할 때 S3를 호출하나요?아니요, 서명은 로컬에서 계산돼요

미리 서명된 URL의 작동 방식

  1. 코드(또는 CLI)가 버킷, 객체 키, HTTP 메서드, 만료 시간을 받아요.
  2. SDK가 AWS Signature Version 4를 사용해 시크릿 액세스 키로 이 값들에 서명해요. 이 시점에는 S3에 아무것도 전송되지 않아요.
  3. 만들어진 URL을 누군가에게 전달해요: 브라우저, 동료, 모바일 앱 등.
  4. 그 사람이 URL을 사용하면 S3가 URL에 있는 액세스 키에 해당하는 시크릿으로 서명을 다시 계산해요. 서명이 일치하고, URL이 만료되지 않았고, 서명한 주체에게 그 작업 권한이 있으면 요청이 처리돼요.

URL에는 서명한 주체의 권한이 담겨 있어요. AWS는 이를 분명히 밝혀요: URL을 생성한 IAM 사용자나 역할이 객체를 읽을 수 없다면, 서명이 아무리 유효해도 URL은 403을 반환해요.

X-Amz 매개변수의 의미

SigV4 미리 서명된 URL은 다음과 같은 형태예요(줄 바꿈 추가):

https://my-bucket.s3.eu-west-1.amazonaws.com/reports/q3.pdf
  ?X-Amz-Algorithm=AWS4-HMAC-SHA256
  &X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261017%2Feu-west-1%2Fs3%2Faws4_request
  &X-Amz-Date=20261017T093000Z
  &X-Amz-Expires=3600
  &X-Amz-SignedHeaders=host
  &X-Amz-Signature=5f2c...e91a
매개변수의미
X-Amz-Algorithm서명 알고리즘: SigV4는 AWS4-HMAC-SHA256
X-Amz-Credential액세스 키 ID와 범위: <key-id>/<YYYYMMDD>/<region>/s3/aws4_request
X-Amz-DateURL에 서명한 시점, UTC 기준, yyyyMMddTHHmmssZ
X-Amz-ExpiresX-Amz-Date 이후 URL이 유효한 시간(초), 1부터 604800까지
X-Amz-SignedHeaders서명에 포함된 헤더, 최소한 항상 host
X-Amz-Signature서명, 16진수 64자
X-Amz-Security-Token임시 자격 증명(STS, 역할, EC2, ECS, SSO)으로 서명한 경우에만 있음

서명된 부분(키, 메서드, 만료 시간, 서명된 헤더) 중 하나라도 바꾸면 서명이 더 이상 일치하지 않아요. URL이 평문으로 드러내는 정보도 알아 두세요: 버킷 이름, 전체 객체 키, 액세스 키 ID. 시크릿 키는 절대 URL에 포함되지 않아요.

미리 서명된 URL의 유지 기간

제한은 서명하는 자격 증명에 따라 달라요:

서명에 사용한 자격 증명최대 유효 기간
IAM 사용자 액세스 키 (SigV4)7일, SigV4 서명 키 자체가 7일 동안 유효하기 때문이에요
IAM 역할 세션 (STS AssumeRole)세션이 끝날 때까지, 기본값은 1시간
EC2 인스턴스 프로필역할 자격 증명이 교체될 때까지, 최대 약 6시간
ECS 작업 역할작업 자격 증명이 교체될 때까지, 보통 1시간에서 6시간마다
취소되거나 삭제된 모든 자격 증명X-Amz-Expires 값과 관계없이 즉시 멈춰요
S3 콘솔1분에서 12시간 사이로 선택

이것이 가장 흔한 의외의 상황을 설명해 줘요: Lambda 함수나 EC2 인스턴스에서 7일을 요청했는데 링크가 1시간이나 몇 시간 뒤에 끊기는 경우예요. URL은 그 뒤에 있는 자격 증명만큼만 유지돼요. 긴 링크가 필요하다면 그 버킷에 대한 읽기 권한만 있는 전용 IAM 사용자로 서명하세요.

S3는 요청이 시작될 때 만료 여부를 확인해요. 마감 1초 전에 시작한 대용량 다운로드는 정상적으로 끝나지만, 마감 이후의 재시도는 AccessDenied와 “Request has expired” 메시지로 실패해요. 604,800초를 넘게 요청하면 바로 거부돼요.

미리 서명된 URL 생성하기

AWS CLI

aws s3 presign s3://my-bucket/reports/q3.pdf --expires-in 3600

이 명령은 다운로드(GET) URL에 서명해요. 기본값은 3,600초, 최대값은 604,800초예요. 최신 CLI 버전은 항상 SigV4를 사용하므로 리전이 설정되어 있는지 확인하세요. 업로드 URL에는 SDK가 필요해요.

Python (boto3)

import boto3
from botocore.config import Config

s3 = boto3.client('s3', region_name='eu-west-1', config=Config(signature_version='s3v4'))

url = s3.generate_presigned_url(
    ClientMethod='get_object',
    Params={
        'Bucket': 'my-bucket',
        'Key': 'reports/q3.pdf',
        'ResponseContentDisposition': 'attachment; filename="q3.pdf"',
    },
    ExpiresIn=3600,
)

signature_version='s3v4'를 명시적으로 설정하세요. boto3 참조 문서에 따르면, 그렇지 않으면 하위 호환성 때문에 아직 이를 허용하는 리전에서 이전 Signature Version 2로 서명할 수 있으며, 이 경우 Range 같은 일부 매개변수와 헤더가 조용히 빠져요. ResponseContentDisposition은 선택 사항이에요: 브라우저가 파일을 바로 열지 않고 깔끔한 이름으로 저장하게 해 줘요.

JavaScript (AWS SDK v3)

import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'

const client = new S3Client({ region: 'eu-west-1' })

const url = await getSignedUrl(
  client,
  new GetObjectCommand({ Bucket: 'my-bucket', Key: 'reports/q3.pdf' }),
  { expiresIn: 3600 }
)

expiresIn을 생략하면 JavaScript presigner는 900초(15분)를 사용해요.

미리 서명된 업로드 URL (PUT)

같은 원리가 반대 방향으로도 작동해요. 키 하나에 대한 PutObject에 서명하면 URL을 가진 사람이 그 객체를 쓸 수 있어요. 파일은 그 사람의 기기에서 버킷으로 바로 이동하며, 서버는 바이트를 전혀 다루지 않아요. 그 키의 객체가 이미 있으면 교체돼요.

import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'

const client = new S3Client({ region: 'eu-west-1' })

const url = await getSignedUrl(
  client,
  new PutObjectCommand({
    Bucket: 'my-bucket',
    Key: 'incoming/brief.pdf',
    ContentType: 'application/pdf',
  }),
  { expiresIn: 900, signableHeaders: new Set(['content-type']) }
)

await fetch(url, {
  method: 'PUT',
  headers: { 'Content-Type': 'application/pdf' },
  body: file,
})

signableHeaders에 content-type을 추가하면 URL이 그 유형에 묶이므로, 다른 Content-Type으로 업로드하면 거부돼요. boto3에서는 generate_presigned_url('put_object', Params={'Bucket': ..., 'Key': ..., 'ContentType': 'application/pdf'})가 같은 역할을 하며, 클라이언트도 같은 헤더를 보내야 해요.

브라우저에서 업로드하기 전에 확인할 세 가지:

  • CORS. 버킷에 내 오리진과 PUT 메서드를 허용하는 CORS 규칙이 필요해요. 멀티파트 업로드라면 브라우저가 각 파트에서 ETag 헤더를 읽어야 하므로 이 헤더도 노출해야 해요.
  • 고정 키. 방문자가 다른 사람의 파일을 덮어쓰지 못하도록 서버에서 고유한 키를 생성하세요(예: UUID 접두사 사용).
  • 체크섬. 최신 AWS SDK는 기본적으로 업로드에 CRC 체크섬을 추가해요. S3 호환 프로바이더에서 미리 서명된 업로드가 서명 또는 체크섬 오류로 실패한다면 클라이언트에 requestChecksumCalculation: 'WHEN_REQUIRED'를 설정하세요.

미리 서명된 POST는 일반 HTML 양식을 위한 별도의 방식으로, 크기와 콘텐츠 유형을 제한하는 정책을 둘 수 있어요. 모든 S3 호환 프로바이더가 지원하지는 않으므로(R2는 지원하지 않아요), 미리 서명된 PUT이 이식성이 더 좋은 선택이에요.

대용량 파일: 미리 서명된 멀티파트 업로드

S3에 대한 단일 PUT은 5 GB로 제한되며, AWS는 약 100 MB부터 멀티파트를 권장해요. 멀티파트는 파일을 최대 10,000개의 파트로 나누며, 마지막 파트를 제외한 각 파트는 5 MiB에서 5 GiB이고, 파트마다 독립적으로, 병렬로, 재시도하며 업로드돼요. 미리 서명은 파트별로 작동해요: 서버가 업로드를 시작하고 파트마다 UploadPart URL 하나에 서명하면, 브라우저가 각 조각을 보내고 ETag를 알려 주며, 서버가 업로드를 완료해요.

import {
  CreateMultipartUploadCommand,
  UploadPartCommand,
  CompleteMultipartUploadCommand,
} from '@aws-sdk/client-s3'

const { UploadId } = await client.send(
  new CreateMultipartUploadCommand({ Bucket: 'my-bucket', Key: 'video/raw.mov' })
)

const partUrl = await getSignedUrl(
  client,
  new UploadPartCommand({ Bucket: 'my-bucket', Key: 'video/raw.mov', UploadId, PartNumber: 1 }),
  { expiresIn: 3600 }
)

await client.send(
  new CompleteMultipartUploadCommand({
    Bucket: 'my-bucket',
    Key: 'video/raw.mov',
    UploadId,
    MultipartUpload: { Parts: [{ PartNumber: 1, ETag: etagFromPartResponse }] },
  })
)

완료되지 않은 멀티파트 업로드를 며칠 후 중단하는 수명 주기 규칙을 추가하세요. 그렇지 않으면 버려진 파트가 계속 청구 대상 스토리지를 차지해요.

R2, B2, Spaces, Wasabi의 미리 서명된 URL

SigV4로 S3 API를 구현한 프로바이더는 같은 코드를 받아들여요: 엔드포인트, 리전, 키만 바꾸면 돼요. 세부 사항은 다르므로 프로바이더 문서를 확인하세요:

  • Cloudflare R2는 1초에서 7일까지의 만료 시간으로 미리 서명된 GET, HEAD, PUT, DELETE를 지원해요. 미리 서명된 POST는 지원하지 않으며, 미리 서명된 URL은 사용자 지정 도메인이 아닌 S3 API 도메인(<ACCOUNT_ID>.r2.cloudflarestorage.com)에서만 작동해요. region: 'auto'를 사용하세요.
  • Backblaze B2는 S3 호환 API가 AWS CLI나 SDK로 생성한 미리 서명된 URL을 지원한다고 밝히고 있어요.
  • DigitalOcean Spaces와 Wasabi는 각자의 엔드포인트를 지정하면 같은 SDK 호출로 작동해요.

프로바이더별 요금은 S3, R2, Spaces, Wasabi, B2 비교를 참고하세요.

흔한 미리 서명된 URL 오류와 해결 방법

오류일반적인 원인해결 방법
SignatureDoesNotMatch메서드, 헤더, 쿼리가 서명된 것과 다르거나 시계가 맞지 않음같은 Content-Type과 헤더를 보내고, NTP로 시계를 동기화하세요
AccessDenied “Request has expired”URL이 X-Amz-Date + X-Amz-Expires를 지남새 URL을 생성하고, 서명과 사용 사이의 간격을 줄이세요
요청보다 일찍 만료되는 URL임시 자격 증명(역할, EC2, ECS, Lambda)으로 서명함링크가 유지되어야 하는 기간만큼 유효한 자격 증명으로 서명하세요
ExpiredTokenX-Amz-Security-Token 뒤의 STS 세션이 끝남서명하기 전에 자격 증명을 새로 고치세요
새 URL에서 403 AccessDenied서명한 주체에 s3:GetObject나 s3:PutObject 권한이 없거나 정책이 거부함서명 주체에 권한을 부여하고 버킷 정책을 확인하세요
AuthorizationQueryParametersError만료 시간이 604,800초를 넘거나 자격 증명 범위가 잘못됨X-Amz-Expires를 7일 이하로 유지하고 버킷의 리전에서 서명하세요
브라우저가 CORS로 차단함버킷에 내 오리진과 메서드에 대한 CORS 규칙이 없음오리진, GET/PUT, 노출된 ETag가 있는 CORS 규칙을 추가하세요
HeadersNotSigned가 있는 AccessDeniedRange는 서명됐지만 If-Range가 서명 없이 전송됨서명된 헤더에 If-Range를 추가하세요

헤더나 쿼리 문자열을 다시 쓰는 회사 프록시도 서명을 깨뜨려요. URL이 한 네트워크에서는 작동하고 다른 네트워크에서는 작동하지 않는다면 프록시 없이 테스트해 보세요.

보안 모범 사례

AWS는 미리 서명된 URL을 베어러 토큰이라고 부르며 보호할 것을 권장해요. 실제로는:

  • 짧게 유지하세요. 업로드와 민감한 파일은 몇 분, 일반 다운로드는 몇 시간이면 돼요. 누군가 파일이 다시 필요하면 새 URL을 생성하세요.
  • 범위가 좁은 신원으로 서명하세요. 해당 접두사에 대해 읽기만(또는 쓰기만) 할 수 있는 전용 IAM 사용자나 역할을 쓰면 링크가 유출되어도 피해를 줄일 수 있어요.
  • 버킷 정책에 안전장치를 추가하세요. s3:signatureAge 조건은 정해진 시간보다 오래된 서명을 거부하고, aws:SourceIp는 사용할 수 있는 네트워크를 제한해요.
  • 로그에 남기지 마세요. 애플리케이션 로그, 분석 도구, 채팅 기록에 남은 전체 미리 서명된 URL은 만료될 때까지 작동하는 자격 증명이에요.
  • 없는 기능을 감안해 계획하세요. 비밀번호, 다운로드 횟수 제한, 링크별 취소가 없으므로, 이런 기능이 필요하면 앞에 애플리케이션을 두세요.

이런 제어 기능을 더하는 방법까지 포함해 공유 측면을 깊이 알아보려면 버킷을 공개하지 않고 S3 파일을 공유하는 방법을 참고하세요.

Nubbo가 미리 서명된 URL을 사용하는 방식

Nubbo는 이 패턴을 기반으로 만들어졌어요. 탐색기에서 업로드하면 서버가 PUT(100 MB 이상이면 세 파트를 병렬로 올리는 멀티파트 업로드)에 서명하고, 브라우저가 바이트를 버킷으로 직접 보내요. 단일 파일 다운로드에는 1시간 동안 유효한 미리 서명된 GET URL을 사용해요. 액세스 키는 AES-256-GCM으로 암호화되어 저장되며 서버에서 서명할 때만 사용돼요.

공유 링크는 순수한 미리 서명된 URL에 없는 기능을 더해요: Nubbo가 유효 시간이 짧은 URL을 발급하기 전에 확인하는 선택적 비밀번호, 만료일, 다운로드 횟수 제한이 있으며, 언제든지 링크를 비활성화할 수 있어요. 여러 파일 ZIP 다운로드는 브라우저에서 만들어지며 브라우저가 각 파일을 내 버킷에서 바로 다운로드해요: Nubbo는 내용을 전혀 보지 않아요. 솔직하게 밝히는 예외 한 가지: 썸네일은 내 버킷의 .nubbo-thumbs/ 아래에 기록돼요. Amazon S3, Cloudflare R2, DigitalOcean Spaces, Wasabi, Backblaze B2, 공개 S3 호환 엔드포인트에서 똑같이 작동해요.

자주 묻는 질문

S3의 미리 서명된 URL이란 무엇인가요?

비공개 버킷의 객체 하나를 가리키는 일반 HTTPS 링크에 서명을 쿼리 문자열로 덧붙인 것이에요. 링크를 가진 사람은 만료될 때까지 서명된 단일 작업, 보통 다운로드(GET)나 업로드(PUT)를 수행할 수 있어요. 버킷은 비공개로 유지되며 링크를 사용하는 사람에게 AWS 자격 증명이 필요 없어요.

S3 미리 서명된 URL의 최대 만료 시간은 얼마인가요?

Signature Version 4와 장기 IAM 사용자 키로 서명하면 7일(604,800초)이에요. 수임한 역할이나 EC2 인스턴스 프로필 같은 임시 자격 증명으로 서명한 URL은 더 길게 요청했더라도 그 자격 증명이 만료되면 작동을 멈춰요. S3 콘솔에서는 1분에서 12시간 사이로 선택할 수 있어요.

X-Amz-Algorithm, X-Amz-Credential, X-Amz-Date, X-Amz-Expires, X-Amz-SignedHeaders, X-Amz-Signature는 무엇을 의미하나요?

SigV4 인증 매개변수예요. Algorithm은 서명 방식(AWS4-HMAC-SHA256), Credential은 액세스 키 ID와 날짜, 리전, 서비스 범위, Date는 URL에 서명한 시점, Expires는 유효한 시간(초), SignedHeaders는 서명에 포함된 헤더 목록, Signature는 서명 자체예요. X-Amz-Security-Token은 임시 자격 증명으로 서명한 경우에만 나타나요.

만료 전에 S3 미리 서명된 URL을 취소할 수 있나요?

하나씩은 취소할 수 없어요. 미리 서명된 URL이 일찍 작동을 멈추는 것은 서명한 자격 증명이 취소, 삭제, 비활성화되거나 버킷 정책이 요청을 거부할 때뿐이에요. 개별 링크를 취소해야 한다면, 자체 확인을 거친 뒤에만 유효 시간이 짧은 URL을 발급하는 애플리케이션을 앞에 두세요.

S3 미리 서명된 URL은 안전한가요?

서명은 위조하거나 변경할 수 없고 버킷은 비공개로 유지돼요. 하지만 AWS는 미리 서명된 URL을 베어러 토큰이라고 설명해요: 링크를 얻은 사람은 누구든 만료될 때까지 원하는 만큼 사용할 수 있어요. 만료 시간을 짧게 하고, HTTPS를 사용하고, 복사될 수 있는 곳에 링크를 올리지 마세요.

미리 서명된 URL은 Cloudflare R2, Backblaze B2, Wasabi에서도 작동하나요?

Signature Version 4로 S3 API를 구현한 프로바이더는 대체로 지원하며, 프로바이더 엔드포인트로 같은 SDK 호출을 사용하면 돼요. 예를 들어 Cloudflare R2는 1초에서 7일까지의 만료 시간으로 미리 서명된 GET, HEAD, PUT, DELETE를 지원하지만, POST 양식 업로드와 사용자 지정 도메인은 지원하지 않아요.

미리 서명된 URL이 SignatureDoesNotMatch를 반환하는 이유는 무엇인가요?

AWS는 흔한 원인 세 가지를 들어요: 컴퓨터 시계가 맞지 않는 경우, 프록시가 헤더나 쿼리 문자열을 변경하는 경우, 요청의 메서드, 헤더, 매개변수가 서명된 것과 다른 경우예요. 업로드에서는 URL을 생성할 때와 다른 Content-Type을 보내는 것이 보통의 원인이에요.

출처

요금과 제한은 아래 공식 페이지를 기준으로 해요. 프로바이더가 변경할 수 있으니 결정하기 전에 확인하세요.