- Published on
Azure Blob 403 AuthorizationFailure - SAS·RBAC·시간오차
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에서 Azure Blob Storage에 업로드/다운로드를 붙이다 보면 가장 당황스러운 에러 중 하나가 403 AuthorizationFailure입니다. 메시지는 보통 단순하지만, 실제 원인은 크게 세 갈래로 갈립니다.
- SAS 토큰 문제(서명, 권한 범위, 만료/시작 시간, 리소스 타입)
- RBAC 권한 문제(역할 미부여, 스코프 오류, 전파 지연)
- 시간 오차(Clock Skew)(컨테이너/VM/NTP 불일치로 SAS 검증 실패)
이 글은 “어디서부터 확인해야 하는지”를 체크리스트처럼 정리하고, 재현 가능한 테스트와 코드 예제로 해결까지 연결합니다. (권한/네트워크 계층에서 403이 뜨는 맥락은 EKS Pod에서 IPv6로만 STS 403 뜰 때 해결처럼 ‘정상처럼 보이지만 특정 조건에서만 인증이 실패’하는 유형과 유사합니다.)
1) 403 AuthorizationFailure의 정체: 어떤 403인가?
Azure Storage의 403은 비슷해 보여도 원인이 다릅니다. 먼저 응답 헤더와 에러 바디를 확보하세요.
x-ms-error-code:AuthorizationFailure,AuthenticationFailed,AuthorizationPermissionMismatch등x-ms-request-id,x-ms-client-request-id: Azure 지원/로그 추적에 필수- 바디 XML의
AuthenticationErrorDetail: SAS 서명/시간 문제일 때 결정적 힌트
빠른 진단: 에러 코드별 감
AuthorizationFailure: 권한/인증 전반(가장 포괄적)AuthorizationPermissionMismatch: SAS에sp(permissions) 부족 또는 RBAC 역할 부족AuthenticationFailed: SAS 서명/시간/리소스 스코프 불일치가 많음
2) SAS 기반 접근에서 가장 흔한 실수 8가지
SAS는 “URL에 붙는 토큰”이라 디버깅이 쉽지 않습니다. 아래 항목을 순서대로 확인하면 대부분 잡힙니다.
2.1 st(start time)와 se(expiry time) + 시간 오차
SAS는 기본적으로 서버 시간 기준으로 유효성을 검증합니다. 클라이언트(컨테이너/VM/로컬)가 몇 분만 어긋나도 다음이 발생합니다.
st가 미래로 인식되어 “아직 유효하지 않음”se가 이미 지난 것으로 인식되어 “만료됨”
권장 패턴:
st를 현재보다 5~15분 과거로 설정(Clock skew 흡수)se는 최소 필요 시간만 부여
2.2 sp(permissions) 부족
Blob 읽기인데 sp=r이 없거나, 업로드인데 sp=w/sp=c가 없으면 403이 납니다.
- 단일 Blob 업로드(put blob): 보통
w또는c필요 - 컨테이너에 새 Blob 생성:
c가 필요한 케이스가 많음
2.3 리소스 타입/서비스 타입 불일치
User Delegation SAS / Account SAS / Service SAS에 따라 필드가 다르고, 대상 리소스(Blob/Container)와 SAS의 sr이 다르면 실패합니다.
- Blob 대상이면
sr=b - Container 대상이면
sr=c
2.4 서명 문자열(Canonicalized Resource) 불일치
직접 SAS를 구성하거나 프록시가 URL을 변형하면 서명 검증이 깨집니다.
- URL 인코딩(특히
+,%2F, 공백) 변경 - 프록시가 쿼리 파라미터 순서/인코딩을 바꾸는 경우
2.5 스토리지 계정 설정: “SAS 허용”/“공유 키 허용”
조직 보안 정책으로 Shared Key access를 막아둔 경우가 있습니다.
- Account SAS는 Shared Key 기반이므로 차단될 수 있음
- 이 경우 **User Delegation SAS(AAD 기반)**로 전환 고려
2.6 HTTPS 강제(spr=https)인데 HTTP로 호출
SAS에 spr=https가 들어있는데 호출이 HTTP면 실패합니다.
2.7 IP 제한(sip)이 실제 클라이언트 IP와 다름
NAT/프록시/Ingress 뒤에서 실제 egress IP가 바뀌면 403이 됩니다.
2.8 컨테이너/Blob 이름 대소문자/경로 문제
Blob 경로는 케이스 센서티브합니다. MyFile.png와 myfile.png는 다른 리소스입니다.
3) RBAC(Azure AD) 기반 접근에서 흔한 실수 6가지
SAS가 아닌 Managed Identity / Service Principal로 접근할 때는 RBAC가 핵심입니다.
3.1 역할(Role) 자체가 다름: “Reader”는 안 된다
Blob 데이터 접근은 관리 plane이 아니라 데이터 plane 역할이 필요합니다.
자주 쓰는 역할:
Storage Blob Data Reader(읽기)Storage Blob Data Contributor(읽기/쓰기/삭제)Storage Blob Data Owner(권한 포함, 보안상 주의)
3.2 스코프(scope) 착각: 계정 vs 컨테이너 vs 리소스 그룹
권한을 부여한 위치가 실제 호출 리소스와 맞아야 합니다.
- Storage Account에 부여하면 전체 컨테이너에 적용
- 특정 컨테이너/Blob에만 주려면 더 좁은 스코프에 부여
3.3 역할 전파 지연
RBAC 변경 직후 몇 분~최대 수십 분 전파 지연이 있을 수 있습니다. “방금 부여했는데 403”이면 시간 두고 재시도하세요.
3.4 방화벽/네트워크 규칙 때문에 “권한처럼 보이는 403”
Storage Account의 네트워크 설정에서
- Selected networks
- Private Endpoint 강제
- “Allow trusted Microsoft services” 비활성
등이면 인증이 맞아도 접근이 막힐 수 있습니다. 이 경우 403/404가 섞여 보이기도 합니다.
3.5 SDK 인증 체인 혼선(DefaultAzureCredential)
로컬에서는 Azure CLI 로그인으로 되는데, 서버에서는 Managed Identity가 없어서 실패하는 케이스가 많습니다.
- 로컬:
AzureCliCredential로 성공 - 서버:
ManagedIdentityCredential실패 → 403/401
3.6 계정에 “Azure AD authorization” 관련 설정/정책
조직 정책으로 AAD 기반만 허용하거나, 반대로 Shared Key만 허용하는 혼합 정책이 있을 수 있습니다. 접근 방식(SAS vs AAD)을 정책과 맞추세요.
4) 시간 오차(Clock Skew)로 SAS가 깨지는 메커니즘
SAS 검증은 서버가 st <= now <= se를 만족하는지 확인합니다. 컨테이너 시간이 느리거나 빠르면 다음이 벌어집니다.
- 클라이언트가 SAS를 생성: 생성 시점의
st/se가 잘못됨 - 클라이언트가 SAS를 전달받음(백엔드가 생성): 전달받은 SAS가 정상이어도, 백엔드가 생성할 때 시간 오차가 있으면 이미 실패한 SAS가 됨
특히 Kubernetes 노드/컨테이너에서 NTP가 어긋나면 “특정 노드에서만 403”이 재현됩니다. 이런 유형은 원인이 단순 권한이 아니라 환경(시간/네트워크)일 가능성이 큽니다.
5) 재현과 검증: curl로 ‘최소 단위’ 테스트하기
SDK를 끼면 변수가 늘어납니다. 먼저 curl로 Blob 한 개를 대상으로 검증하세요.
5.1 SAS로 HEAD 요청
curl -I "https://<account>.blob.core.windows.net/<container>/<blob>?<sas>"
- 성공:
200 OK또는206/304등 - 실패:
403+x-ms-error-code확인
5.2 에러 바디까지 보기
curl -v "https://<account>.blob.core.windows.net/<container>/<blob>?<sas>"
AuthenticationErrorDetail에 Signature did not match 또는 Server failed to authenticate the request가 나오면 SAS 문자열/시간을 의심하세요.
6) Node.js 예제: SAS 생성 시 시간 오차 흡수하기
아래는 Service SAS를 생성하는 예시입니다(Shared Key 필요). 핵심은 startsOn을 과거로 두는 것입니다.
import {
StorageSharedKeyCredential,
generateBlobSASQueryParameters,
BlobSASPermissions
} from "@azure/storage-blob";
const accountName = process.env.AZURE_STORAGE_ACCOUNT;
const accountKey = process.env.AZURE_STORAGE_KEY;
const sharedKeyCredential = new StorageSharedKeyCredential(accountName, accountKey);
export function createBlobReadSas(containerName, blobName) {
const now = new Date();
// Clock skew 흡수: 10분 과거부터 유효
const startsOn = new Date(now.getTime() - 10 * 60 * 1000);
const expiresOn = new Date(now.getTime() + 30 * 60 * 1000);
const sas = generateBlobSASQueryParameters(
{
containerName,
blobName,
permissions: BlobSASPermissions.parse("r"),
startsOn,
expiresOn,
protocol: "https"
},
sharedKeyCredential
).toString();
return `https://${accountName}.blob.core.windows.net/${containerName}/${blobName}?${sas}`;
}
실무 팁:
- 업로드 SAS면 권한을
c/w포함으로 조정 - 프록시/게이트웨이에서 URL 인코딩을 바꾸지 않도록 주의
7) Python 예제: AAD(RBAC)로 접근해 SAS 자체를 제거하기
SAS가 반복적으로 문제를 일으키면, 서버-서버 통신에서는 RBAC + Managed Identity가 운영 안정성이 좋습니다.
from azure.identity import DefaultAzureCredential
from azure.storage.blob import BlobServiceClient
account_url = "https://<account>.blob.core.windows.net"
credential = DefaultAzureCredential()
bsc = BlobServiceClient(account_url=account_url, credential=credential)
blob_client = bsc.get_blob_client(container="my-container", blob="hello.txt")
data = blob_client.download_blob().readall()
print(data.decode("utf-8"))
이 코드가 403이면 체크:
- 실행 주체(Managed Identity/Service Principal)에
Storage Blob Data Reader이상 부여 - 네트워크(Private Endpoint/Firewall) 정책
DefaultAzureCredential이 어떤 자격 증명을 선택했는지 로그로 확인
8) 운영에서의 트러블슈팅 루틴(추천 순서)
403을 만났을 때 시간을 아끼는 순서입니다.
8.1 1단계: 에러 코드/헤더 확보
x-ms-error-codex-ms-request-idDate헤더(서버 시간 힌트)
8.2 2단계: 접근 방식 확정
- 지금 호출은 SAS인가?
- 아니면 AAD(RBAC) 인가?
- 혹은 SDK가 내부적으로 다른 Credential을 쓰고 있나?
8.3 3단계: SAS면 시간부터 의심
st/se확인startsOn을 과거로 당겨 재발급- 호출 머신의 시간 동기화(NTP) 확인
리눅스에서 시간 확인:
date -u
# systemd 환경
timedatectl status
# chrony 사용 시
chronyc tracking
8.4 4단계: RBAC면 역할/스코프/전파 지연 확인
- 역할이 Data plane인지
- 스코프가 맞는지
- 부여 직후면 전파 대기
8.5 5단계: 네트워크 정책 확인
- Storage firewall
- Private Endpoint DNS
- 프록시/NAT로 IP 제한(sip) 깨짐
9) 자주 받는 질문(FAQ)
Q1. “어제까지 되던 SAS가 오늘 갑자기 403”
- 만료(
se) 가능성 - 스토리지 계정 정책 변경(Shared Key 차단)
- 배포된 컨테이너/노드의 시간 오차 확대
Q2. “로컬에서는 되는데 서버에서만 403”
- 서버의 시간 오차
- 서버는 Private Endpoint 강제 구간인데 DNS가 퍼블릭으로 감
DefaultAzureCredential이 로컬과 서버에서 다른 인증 수단을 선택
Q3. “권한을 줬는데도 계속 403”
- RBAC 전파 지연
- 역할이
Storage Blob Data Contributor가 아니라Contributor(관리 역할)로 잘못 부여 - 호출 리소스(계정/컨테이너)가 다른 환경(테넌트/구독)일 가능성
10) 마무리: 403을 ‘권한 문제’로만 보지 말 것
Azure Blob의 403 AuthorizationFailure는 단순 권한 미부여뿐 아니라 SAS 파라미터/서명, RBAC 스코프, 시간 오차처럼 “환경과 설정이 교차”하는 지점에서 자주 발생합니다.
정리하면:
- SAS를 쓴다면
st를 과거로, 권한(sp)과 리소스 타입(sr)을 정확히 - RBAC를 쓴다면 Data plane 역할을 올바른 스코프에 부여
- 특정 노드/특정 시간대에만 터지면 Clock skew를 1순위로 의심
비슷하게 ‘조건이 맞을 때만 인증이 실패’하는 문제를 다룬 글로는 EKS Pod에서 IPv6로만 STS 403 뜰 때 해결도 참고할 만합니다.