Published on

Azure Blob 403 AuthorizationFailure - SAS·RBAC·시간오차

Authors

서버에서 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.pngmyfile.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>"

AuthenticationErrorDetailSignature 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-code
  • x-ms-request-id
  • Date 헤더(서버 시간 힌트)

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 뜰 때 해결도 참고할 만합니다.