IAM 공부 1편
IAM을 맨날 사용하고 보안에서도 많이 나오는데 이게 어떻게 인증이 되는거야?
버튼 하나를 눌렀을 때 — IAM 여행의 출발점
이 글은 “왜 이걸 공부하게 됐는지”에서 시작해서, 질문 하나를 끝까지 따라가는 이야기다. “내가 AWS에서 뭔가를 할 때, 그 순간 실제로 무슨 일이 벌어지는가?”
시작은 사소한 업무였다
모든 고객의 iam은 mfa를 사용해야한다 -> 그래서 적용할건데 iam 그룹으로 적용해야하네? (MSP 구조에서 iam identity center를 사용 못함)
콘솔에서 그룹 만들고, 정책 붙이고, 체크박스 몇 개. 업무 자체는 30분이면 끝난다. 그런데 하다 보니 iam 이놈이 계정이 되고 ec2의 역할로 붙고 이렇게 그냥 사용만 했는데 좀 더 자세히 알아보고 싶어졌다
그래서 여기서 시작한다: aws s3 ls
가장 단순한 명령 하나를 보면
1
aws s3 ls
버킷 목록을 보여주는, 별것 아닌 명령이다. 그런데 이 한 줄이 IAM의 거의 전부를 관통한다.
aws s3 ls는 사실 그냥 HTTP 요청 하나다.
1 GET https://s3.ap-northeast-2.amazonaws.com/특별한 건 딱 하나. 이 요청에 “이건 내가 보낸 게 맞다”는 서명(signature)이 붙어 있다는 것뿐이다.
AWS API 호출은 “서명이 붙은 평범한 HTTPS 요청”이다.
공식문서를 참고
공식문서 · How IAM works — “요청은 보낼때 인증과 함께 보냄” · AWS Signature Version 4 for API requests — “AWS Signature Version 4 (SigV4)는 AWS API 요청에 인증 정보를 추가하기 위한 AWS 서명 프로토콜”
요청은 두 개의 관문을 통과한다
이 요청이 AWS에 도착하면, 순서대로 두 개의 문을 지난다. IAM을 이해한다는 건 결국 이 두 문을 이해하는 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[ aws s3 ls 요청 도착 ]
│
▼
┌─────────────────────┐
│ 1번 문: 인증 (AuthN) │ "너, 진짜 너 맞아?"
│ Authentication │
└─────────────────────┘
│ 통과 → "이건 확실히 김민정의 요청이다"
▼
┌─────────────────────┐
│ 2번 문: 인가 (AuthZ) │ "김민정, 그거 해도 돼?"
│ Authorization │
└─────────────────────┘
│
▼
Allow(해라) / Deny(안 돼)
- 1번 문 (인증): 요청에 붙은 서명을 보고 “이 사람이 진짜 자기가 주장하는 그 사람인지” 확인한다.
- 2번 문 (인가): 신원이 확정된 뒤에, “그 사람이 이 행동을 할 권한이 있는지” 정책을 뒤져서 판단한다.
순서가 중요하다. 인증을 통과해야 인가를 시작한다. 누군지도 모르는데 권한을 따질 수는 없으니까.
그러면 MFA는 처음 인증 아닌가?
내가 강제하려던 MFA는 1번 문(인증)에 관한 이야기다. “비밀번호만 맞으면 통과”가 아니라 “비밀번호 + 폰의 6자리 코드까지 맞아야 통과”로 문을 더 튼튼하게 만드는 것.
그리고 “MFA 안 했으면 아무것도 못 하게” 막는 정책은 2번 문(인가)에 관한 이야기다. “이 세션이 MFA로 들어왔는가?”라는 조건을 보고 판결을 바꾸는 것.
내 요청의 정체 확인
① 나는 지금 누구로 요청하고 있나?
1
aws sts get-caller-identity
이렇게 나온다.
1
2
3
4
5
6
$ aws sts get-caller-identity
{
"UserId": "AIDACKCEVSQ6C2EXAMPLE",
"Account": "123456789012",
"Arn": "arn:aws:iam::123456789012:user/minjeong"
}
Arn이user/...→ 나는 지금 장기 사용자로 요청 중이다.Arn이assumed-role/...→ 나는 지금 역할을 빌려 쓴 임시 신분으로 요청 중이다.
② 서명이 실제로 붙는 걸 눈으로 보기
1
2
3
$ aws s3 ls --debug 2>&1 | grep "Authorization"
2026-07-26 22:50:15,765 - MainThread - botocore.endpoint - DEBUG - Sending http request: <AWSPreparedRequest stream_output=False, method=GET, url=https://s3.ap-northeast-2.amazonaws.com/, headers={'User-Agent': b'aws-cli/2.35.17 md/awscrt#0.35.0 ua/2.1 os/macos#25.5.0 md/arch#arm64 lang/python#3.14.6 md/*****/command#s3.ls', 'X-Amz-Date': b'20260726T135015Z', 'X-Amz-Content-SHA256': b'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', 'Authorization': b'AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20260726/ap-northeast-2/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=f6310b*****917cbc784****', 'amz-sdk-invocation-id': b'8*****5d******-fb904******', 'amz-sdk-request': b'attempt=1'}>
Authorization 헤더에 Signature=...가 붙어 있다. 이게 바로 “1번 문을 통과하려고 붙인 서명”의 실물이다.
Signature=값은 있는데, 내 비밀 키(SecretKey)는 어디에도 안 보인다.
비밀 키는 절대 전송되지 않는다
방금 관찰한 것의 정체가 IAM 인증의 핵심 원리다.
공식 문서가 이렇게 못 박는다.
“You don’t use your secret access key to sign API requests. Instead, you use the SigV4 signing process.” (요청 서명에 비밀 액세스 키를 직접 쓰지 않는다. 대신 SigV4 서명 절차를 거친다.)
무슨 뜻이냐면:
1
2
3
4
5
6
나: 비밀 키로 "서명(짧은 계산 결과)"만 만들어서 보낸다.
AWS: 같은 비밀 키를 자기도 알고 있으니, 같은 서명을 다시 계산해본다.
두 서명이 같으면 → "비밀 키를 아는 사람이 보냈군" → 인증 통과.
→ 비밀 키 원본은 네트워크에 절대 안 나간다.
중간에서 요청을 훔쳐봐도 서명값만 보이지, 비밀 키는 못 얻는다.
자격증명 분석하기
“1번 문(인증)”을 통과하려면 서명이 필요하다는 걸 봤다. 그럼 그 서명을 만드는 재료, 내 신분은 대체 뭘로 되어 있는가?
AWS는 이름으로 사람을 기억하지 않는다
| Prefix | Resource type |
|---|---|
| ABIA | AWS STS service bearer token |
| ACCA | Context-specific credential |
| AGPA | User group |
| AIDA | IAM user |
| AIPA | Amazon EC2 instance profile |
| AKIA | Access key |
| ANPA | Managed policy |
| ANVA | Version in a managed policy |
| APKA | Public key |
| AROA | Role |
| ASCA | Certificate |
| ASIA | Temporary (AWS STS) access key IDs use this prefix, but are unique only in combination with the secret access key and the session token. |
먼저 AIDA의 정체.
AWS의 모든 신원 객체에는 타입 접두사 4글자 + 랜덤 문자열로 된 고유 ID가 붙는다.
1
2
3
4
5
6
AIDA... IAM User
AROA... IAM Role
AGPA... IAM Group
ANPA... Managed Policy
AKIA... 장기 Access Key ID
ASIA... 임시(STS) Access Key ID
접두사 4글자만 봐도 그게 뭔지를 알 수 있다. 이건 그냥 관례가 아니라 실무에서 바로 써먹는 진단 신호다. (어떤키가 유출되었는지 확인하고 조치를 바로 취할 수 잇따)
1
aws sts get-caller-identity
UserId가AIDA...→ 나는 지금 장기 IAM 사용자로 호출 중UserId가AROA...:세션이름→ 나는 지금 역할을 걸친 임시 세션으로 호출 중- Access Key가
AKIA→ 장기 키 /ASIA→ 임시 키
공식문서 · IAM identifiers — Unique identifiers — “When IAM creates a user, role, policy… it assigns a unique ID to each resource” / 접두사 표(AIDA, AROA, AGPA, AKIA, ASIA …)가 여기 있다
이분법: 장기 vs 임시
실무에서는 왠만하면 임시 작격 증명을 이용은 권장한다. 드러면 user/…와 assumed-role/…`의 차이는?
이 차이의 뿌리는 자격증명이 몇 조각으로 되어 있느냐다.
1
2
3
4
5
6
7
8
9
10
11
12
장기 자격증명 임시 자격증명
(Long-term) (Temporary / STS)
─────────────────────────────────────────────────────────────────────
구성 AccessKeyId(AKIA) AccessKeyId(ASIA)
+ SecretAccessKey + SecretAccessKey
+ SessionToken ← 이게 추가!
(2조각) (3조각)
만료 없음 (직접 지우기 전까지 영구) 있음 (15분~수시간 후 자동 소멸)
발급 주체 IAM (사용자에 부착) STS (요청할 때 즉석 발급)
붙는 곳 사람, 오래된 스크립트 역할, MFA세션, 페더레이션, EC2
유출되면 무기한 재앙 곧 만료 → 피해가 시간제한됨
2조각이냐 3조각이냐.
공식문서 · Temporary security credentials in IAM
세 번째 조각 SessionToken의 정체
임시 자격증명에만 붙는 이 SessionToken, 그냥 긴 랜덤 문자열처럼 생겼지만 아니다.
1
2
3
4
5
6
7
SessionToken = STS가 "이 세션의 메타데이터"를 담아 서명·암호화한 봉인 덩어리(blob).
안에 들어 있는 것(개념적으로):
· 어떤 role/user의 세션인지
· 언제 만료되는지
· MFA로 인증됐는지 ← ★
· 세션 정책·세션 태그는 무엇인지
요청을 보낼 때 이 봉인을 x-amz-security-token 헤더에 실어 보낸다. 서버는 이걸 열어보고 “이 임시 세션이 진짜이고 아직 살아 있는가”를 확인한다.
그래서 검증 단계가 장기보다 하나 더 있다.
1
2
장기: 서명 재계산 대조 (= SecretKey를 안다는 증명) 만
임시: 서명 대조 + SessionToken 봉인 검증 + 만료 확인
“이 세션이 MFA로 인증됐나”라는 정보가 바로 이 SessionToken 안에 봉인돼 있다.
그래서
aws:MultiFactorAuthPresent는 “이 사용자가 MFA 기기를 가졌나”가 아니라 “지금 이 세션이 MFA로 들어왔나”다. 그리고 장기 키에는 SessionToken이 아예 없으니 이 정보 자체가 존재하지 않는다. → 조건 키가 “없을” 수 있다 → 그래서BoolIfExists가 필요해진다.
공식문서 · Temporary security credentials — 임시 자격증명의 구성과 수명 · aws:MultiFactorAuthPresent — “This key is present only when the principal uses temporary credentials”
SDK는 내 키를 어디서 찾아오나 — 자격증명 체인
여기서 실무자가 제일 많이 당하는 함정이 나온다.
aws s3 ls를 칠 때, CLI는 내 키를 정해진 순서로 찾아 헤맨다. 이걸 자격증명 체인(credential chain)이라 부른다.
1
2
3
4
5
6
① 명령행 옵션 --profile 등
② 환경변수 AWS_ACCESS_KEY_ID / _SECRET_ACCESS_KEY / _SESSION_TOKEN
③ CLI 자격증명 파일 ~/.aws/credentials 의 [profile]
④ CLI 설정 파일 ~/.aws/config (SSO, source_profile → assume_role 포함)
⑤ 컨테이너 자격증명 ECS: AWS_CONTAINER_CREDENTIALS_RELATIVE_URI 로 HTTP 조회
⑥ 인스턴스 메타데이터 EC2: IMDSv2로 인스턴스 프로파일 역할의 임시 키 조회
규칙은 하나. 위에서부터 “처음 발견된” 것을 쓰고 멈춘다.
공식문서 · Configure the AWS CLI — credential precedence — CLI가 설정/자격증명을 찾는 우선순위
“EC2에 키를 안 넣어도 되는 마법”의 정체
체인의 ⑥번, 이게 꽤 인상적이다.
EC2에 역할을 붙이면 그 안의 앱이 액세스 키 없이 AWS를 호출한다. 마법 같지만 메커니즘은 단순하다.
1
2
3
4
5
6
EC2 인스턴스 안에서:
1) PUT http://169.254.169.254/latest/api/token ← 세션 토큰 획득 (IMDSv2)
2) GET .../latest/meta-data/iam/security-credentials/<role-name>
→ STS가 발급한 임시 자격증명(ASIA + SessionToken + 만료)이 JSON으로 나온다
3) SDK가 이걸 받아 쓰고, 만료 전에 알아서 재발급받는다
즉 EC2 안엔 장기 키가 없다. 인스턴스는 메타데이터 서비스에서 자동으로 순환되는 임시 키를 계속 받아 쓸 뿐이다.
169.254.169.254는 링크로컬 주소라서 그 인스턴스 안에서만 닿는다. 다만 이것도 완전히 안전하지는 않다. IMDSv2가 굳이 PUT으로 토큰을 먼저 받게 만드는 건, SSRF 공격으로 이 자격증명을 빼가는 걸 막기 위한 방어다.
공식문서 · Use an IAM role to grant permissions to applications running on EC2 — “the application… retrieves temporary credentials from the instance metadata” · Instance metadata service (IMDSv2) — 세션 지향 방식이 SSRF를 막는 이유
서명은 어떻게 계산되는가 — SigV4를 바이트 단위로
--debug에서 봤던 그 문자열들
이걸 쳤을 때,
1
2
3
4
5
$ aws s3 ls --debug 2>&1 | grep -E "CanonicalRequest:|StringToSign:|Signature:"
... botocore.auth - DEBUG - CanonicalRequest:
... botocore.auth - DEBUG - StringToSign:
... botocore.auth - DEBUG - Signature:
CanonicalRequest, StringToSign, Signature 같은 이름들이 우르르 찍혔다. 그때는 “아 서명이 붙는구나” 하고 넘겼다.
이름만 나오고 값이 안 보이는 이유: botocore가
logger.debug('CanonicalRequest:\n%s', ...)처럼 값을 다음 줄에 찍기 때문이다.grep은 줄 단위라 이름이 있는 줄만 집어 온다. 값까지 보려면-A(after) 옵션으로 뒤따르는 줄을 같이 가져와야 한다 — 아래에서 그렇게 한다.
이번 편은 그 문자열들이 각각 무엇이고, 어떤 순서로 만들어지는지를 뜯는다. 다 보고 나면 --debug 출력이 암호가 아니라 읽을 수 있는 계산 과정이 된다.
왜 굳이 “서명”이라는 걸 만들었나
설계 동기부터 보면 이해가 빠르다.
1
2
3
4
5
6
7
8
9
10
11
❌ 방법 A: 요청에 SecretKey를 그냥 넣어 보낸다
→ 중간에서 한 번만 훔쳐보면 SecretKey 유출. 끝.
❌ 방법 B: SecretKey의 해시를 넣어 보낸다
→ 그 해시를 훔친 사람이 그대로 재사용(replay)하면 그만.
"해시를 아는 사람 = 권한자"가 되어버린다.
✅ SigV4: "SecretKey로 이 요청의 내용 전체를 서명"한다
→ 서명이 요청 내용에 묶여 있어서, 다른 요청에 재사용 불가.
→ SecretKey 원문은 안 나가고, 서명에서 역산도 안 된다 (HMAC은 단방향).
→ 타임스탬프가 서명 재료에 들어가서, 오래된 서명은 거부된다.
그래서 SigV4는 세 가지를 동시에 달성한다.
- 신원 증명 — SecretKey를 아는 자만 이 서명을 만들 수 있다
- 무결성 — 요청이 한 글자라도 변조되면 서명이 깨진다
- replay 방어 — 시간에 묶여서 오래된 서명은 못 쓴다
공식문서 · AWS Signature Version 4 for API requests — “왜 요청이 서명되는가”: ① 요청자의 신원 확인 ② 전송중인 데이터 보호 ③ 재사용 방어 -> 타임 스템프
전체 계산은 딱 4단계
사진 출처 https://medium.com/@sonal.sadafal/aws-signature-version-4-sigv4-a-practical-guide-0b8cde3ecddc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
요청 SecretKey
│ │
▼ ▼
[1] Canonical Request [3] Signing Key 파생
요청을 정규화된 하나의 문자열로 kDate (+ 날짜)
│ → kRegion (+ 리전)
│ SHA256 → kService (+ 서비스)
▼ → kSigning
[2] String to Sign │
"무엇에 서명할지" 확정 │
(알고리즘 + 시각 + 스코프 + 해시) │
│ │
└──────────────┐ ┌──────────────────┘
▼ ▼
[4] Signature = HMAC(kSigning, StringToSign)
└─ 키 ─┘ └── 메시지 ──┘
│
▼
Authorization 헤더에 담아 전송
번호는 1-2-3-4지만 일렬이 아니다. 두 갈래가 따로 가다가 4단계에서 만난다.
- 1→2단계 (메시지 트랙): 무엇에 서명할지를 만든다. 요청 내용이 여기로 들어간다.
- 3단계 (키 트랙): 무엇으로 서명할지를 만든다. 요청 내용을 전혀 모른다. SecretKey와 스코프(날짜·리전·서비스)만 있으면 계산된다.
- 4단계: HMAC은 (키, 메시지) 둘이 필요하다. 3단계가 키를, 2단계가 메시지를 대주고 여기서 합류한다.
3단계가 요청과 무관하다는 건 실용적인 결과로 이어진다. 그날 그 리전 그 서비스로 보내는 모든 요청이 같은 kSigning을 재사용한다. 요청마다 새로 만드는 건 2단계뿐이다.
그리고 날짜·리전·서비스가 양쪽 트랙에 모두 들어가는 점에 주목하자. 키 트랙에서는 키를 그 스코프에 가두고(유출돼도 범위 제한), 메시지 트랙에서는 서버가 “어느 스코프로 검증할지” 알 수 있게 한다.
하나씩 본다.
[1단계] Canonical Request — 왜 “정규화”부터 하나
문제 하나. 같은 요청이라도 헤더 순서, 대소문자, 공백이 제각각이면 어떻게 될까?
클라이언트와 서버가 서로 다른 문자열을 만들게 되고, 해시가 달라지고, 서명이 안 맞는다.
그래서 양쪽이 똑같은 규칙으로 정규화한 형태를 먼저 만든다.
1
2
3
4
5
6
7
CanonicalRequest =
HTTPMethod + '\n' 예: GET
CanonicalURI + '\n' 예: /
CanonicalQueryString + '\n' 쿼리 파라미터를 정렬·인코딩
CanonicalHeaders + '\n' 헤더를 소문자화·정렬 (host, x-amz-date …)
SignedHeaders + '\n' 서명에 포함한 헤더 이름 목록 (host;x-amz-date)
HashedPayload 본문(body)의 SHA256 (GET이면 빈 문자열의 해시)
여기서 눈여겨볼 두 가지.
- 본문까지 해시에 포함된다 → 누가 중간에서 body를 한 글자만 바꿔도 해시가 달라져 검증 실패.
SignedHeaders에 명시한 헤더만 서명 대상이다 → 중간 프록시가 무관한 헤더를 덧붙여도 서명은 안 깨진다. 현실적인 타협이다.
그리고 이 전체를 한 번 더 해시한다.
1
HashedCanonicalRequest = SHA256(CanonicalRequest)
1
2
3
4
5
6
7
8
9
10
11
$ aws s3 ls --debug 2>&1 | grep -A10 -E "CanonicalRequest:|StringToSign:"
2026-07-27 00:12:31,576 - MainThread - botocore.auth - DEBUG - CanonicalRequest:
GET
/
host:s3.ap-northeast-2.amazonaws.com
x-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
x-amz-date:20260726T151231Z
host;x-amz-content-sha256;x-amz-date
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
여기서 서명 대상 헤더가 3개뿐이라는 것도 같이 보인다 (host;x-amz-content-sha256;x-amz-date). 실제 전송되는 헤더는 User-Agent, amz-sdk-invocation-id 등 더 많은데, 그것들은 서명 밖이다. 프록시나 SDK 계층이 헤더를 덧붙여도 서명이 안 깨지게 하려는 타협이다.
aws s3 ls --debug에서 봤던CanonicalRequest = ...블록이 정확히 이것이고, 그 아래 붙은 hex 문자열이HashedCanonicalRequest다.
공식문서 · Create a signed AWS API request — canonical request 조립 규칙(정렬·인코딩·헤더 소문자화)의 정확한 정의
[2단계] String to Sign
이제 “실제로 HMAC에 밀어넣을 대상”을 조립한다.
1
2
3
4
5
StringToSign =
"AWS4-HMAC-SHA256" + '\n' 알고리즘 식별자
<RequestDateTime> + '\n' x-amz-date, 예: 20260722T091500Z
<CredentialScope> + '\n' 20260722/ap-northeast-2/s3/aws4_request
<HashedCanonicalRequest> 1단계 결과
CredentialScope를 보자.
1
2
3
4
5
6
7
8
9
CredentialScope = 날짜 / 리전 / 서비스 / "aws4_request"
20260722 / ap-northeast-2 / s3 / aws4_request
(위와 같은 실행의 이어지는 출력)
2026-07-27 00:12:31,576 - MainThread - botocore.auth - DEBUG - StringToSign:
AWS4-HMAC-SHA256
20260726T151231Z
20260726/ap-northeast-2/s3/aws4_request
859043aa0cbb7fa48adf45d5351bfb25f8abed34bd328d0698d19e7d9b84de1a
마지막 줄은 1단계 CanonicalRequest의 SHA256이라고 했다.
1
2
3
$ printf 'GET\n/\n\nhost:s3.ap-northeast-2.amazonaws.com\nx-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\nx-amz-date:20260726T151231Z\n\nhost;x-amz-content-sha256;x-amz-date\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' | shasum -a 256
859043aa0cbb7fa48adf45d5351bfb25f8abed34bd328d0698d19e7d9b84de1a -
정확히 일치한다. 1단계 결과가 2단계 입력으로 들어가는 게 재현됐다.
여기서 중요한 사실 하나. 서명 대상 문자열은 이 4줄이 전부다. “그럼 요청 전체가 아니라 일부만 서명하는 거 아닌가?” — 아니다. 4번째 줄이 요청 전체를 압축한 해시이기 때문에, 요청의 어느 바이트가 바뀌어도 → 해시가 바뀌고 → StringToSign이 바뀌고 → 서명이 안 맞는다. 수 GB 본문을 직접 HMAC에 넣지 않고 고정 길이 다이제스트를 서명하는 이 방식을 hash-then-sign이라 한다(처음 알았습니다…). 계산 입력은 짧지만 보호 범위는 요청 전체다.
서명이 날짜·리전·서비스에 묶인다. 서울 리전 S3용으로 만든 서명은 도쿄 리전에도, EC2에도 못 쓴다.
“리전이 틀리면 정책이 아니라 서명부터 실패한다”고 했던 게 이 때문이다. 리전이 서명 재료에 박혀 있으니까.
그리고 RequestDateTime. 이게 서명 재료에 들어가기 때문에 내 노트북 시계가 틀어지면 서명 자체가 어긋난다.
[3단계] Signing Key 파생
SigV4는 SecretKey를 서명에 직접 쓰지 않는다. 대신 4단계 HMAC 체인으로 “제한된 서명 키”를 파생한다.
HMAC = 키가 있는 해시임 키 + 값을 이용해서 해시 -> 키가 없으면 불가능
1
2
3
4
kDate = HMAC-SHA256("AWS4" + SecretAccessKey, "20260722") ← 날짜
kRegion = HMAC-SHA256(kDate, "ap-northeast-2") ← 리전
kService = HMAC-SHA256(kRegion, "s3") ← 서비스
kSigning = HMAC-SHA256(kService, "aws4_request") ← 고정 종결자
각 단계에서 앞 결과를 키로 삼아 다음 재료를 HMAC한다. 마지막 kSigning이 실제 서명에 쓰이는 키다.
왜 이렇게 겹겹이 파생하나?
1
2
3
4
5
6
7
파생된 kSigning은 "그 날짜, 그 리전, 그 서비스"에만 유효한 키다.
→ 만약 kSigning이 유출돼도, 딱 그 하루·그 리전·그 서비스 범위로만 악용 가능.
→ 피해 범위가 자동으로 좁혀진다.
SecretKey 원본은 파생의 맨 첫 단계에 한 번 들어가고 끝이다.
→ 이후 단계에는 아예 등장하지 않는다.
→ HMAC은 단방향이라 kDate에서 SecretKey를 역산할 수 없다.
이것이 “SecretKey가 물리적으로 노출되지 않는다”의 근거
[4단계] Signature와 Authorization 헤더
1
signature = HexEncode( HMAC-SHA256(kSigning, StringToSign) )
앞의 그 실행에서 계산된 서명이 이것이다.
1
2
3
(같은 실행의 이어지는 출력)
2026-07-27 00:12:31,576 - MainThread - botocore.auth - DEBUG - Signature:
c610192ecc6ce6edf5b85a7d2af6d91ec0f86f6fdcf8aa3b6e42e67e0306b955
그리고 이 모든 걸 헤더 하나에 담아 보낸다.
1
2
3
4
Authorization: AWS4-HMAC-SHA256
Credential=AKIAIOSFODNN7EXAMPLE/20260726/ap-northeast-2/s3/aws4_request,
SignedHeaders=host;x-amz-content-sha256;x-amz-date,
Signature=c610192ecc6ce6edf5b85a7d2af6d91ec0f86f6fdcf8aa3b6e42e67e0306b955
방금 Signature: 로그에서 본 값이 그대로 헤더에 실렸다. 1단계부터 여기까지가 한 줄로 이어진다.
1
2
3
4
5
6
7
CanonicalRequest ──SHA256──▶ 859043aa...
│ (StringToSign 4번째 줄로 들어감)
▼
StringToSign ──HMAC(kSigning)──▶ c610192e...
│
▼
Authorization 헤더의 Signature=
여기서 뭐가 공개되고 뭐가 없는지를 보자.
Credential=에는 AccessKeyId(공개값)와 CredentialScope가 그대로 노출된다 → 서버가 이걸 보고 “누구 키인지, 어느 스코프인지”를 안다.Signature=만 비밀 계산 결과다.- SecretKey는 어디에도 없다. 이 헤더 전체를 훔쳐도 SecretKey는 못 얻는다.
- 임시 자격증명이면 여기에 더해
x-amz-security-token: <SessionToken blob>헤더가 붙는다.
그래서 AccessKeyId는 “공개된 사용자명”, SecretKey는 “비밀번호”에 가깝다. AccessKeyId가 로그에 찍혀도 (그것만으로는) 위험이 낮은 이유가 이것.
서버 쪽에서는 무슨 일이 벌어지나
서버는 클라이언트와 완전히 동일한 1~4단계를 반복한다. 그게 전부다.
1
2
3
4
5
6
7
8
1. Authorization 헤더에서 AccessKeyId 추출
2. 내부 저장소에서 그 AccessKeyId의 짝 SecretKey 조회
3. 수신한 요청으로 CanonicalRequest → StringToSign → kSigning → signature 재계산
4. 재계산한 signature 와 헤더의 Signature 비교
같다 → "SecretKey를 아는 자가 보냈다" 증명됨 = 인증 성공
다르다 → SignatureDoesNotMatch
5. x-amz-date가 허용 시간창(약 5분)을 벗어나면 거부 = replay 방어
(임시 자격증명이면 SessionToken 복호화·만료검증도 추가)
에러 메시지를 다시 읽기
인증 에러와 인가 에러를 구분해야한다 이제 왜 그런 에러가 나는지까지 말할 수 있다.
| 에러 | 계산의 어디서 깨졌나 | 진짜 원인 | 하면 안 되는 것 |
|---|---|---|---|
SignatureDoesNotMatch | 4단계 비교 실패 | SecretKey 오타, 시계 오차(2단계), 리전 불일치(2단계) | |
InvalidClientTokenId | 2단계 조회 실패 | AccessKeyId가 없거나 비활성 | |
ExpiredToken / TokenRefreshRequired | SessionToken 만료 | 임시 자격증명 수명 끝 | 재발급 |
AccessDenied | 여기까지 다 통과함 | 정책이 막음 | ← 이때만 정책을 본다 |
공식문서 · Troubleshoot IAM error messages · AWS Signature Version 4 for API requests — 5분 시간창 규칙



