AWS Credential Chain
boto3가 credential을 찾는 순서와 자주 보는 에러
- Haram Lee
- 2026-07-22
- memo
로컬 컨테이너에서 S3를 읽는 테스트를 돌리다 credential 문제로 몇 번 막혔다. 그때 정리한 내용이다.
credential은 두 종류다
| 정적 키 | 임시 credential | |
|---|---|---|
| 생김새 | AKIA... + secret, 2개 | ASIA... + secret + session token, 3개 |
| 수명 | 로테이션 전까지 무기한 | 15분에서 12시간 |
| 발급처 | IAM 유저 | STS (role assume, SSO 로그인, pod role 등) |
access key id 앞 네 글자로 구분한다. AKIA면 정적 키, ASIA면 임시 credential이고 session token이 반드시 따라온다.
이 차이에서 두 가지 제약이 따라 나온다.
- 설정 칸이 access key id와 secret 두 개뿐인 외부 서비스에는 임시 credential을 넣을 수 없다. session token 자리가 없어서다.
aws configure export-credentials로 뽑은 값은 임시 credential의 스냅샷이다. 컨테이너 환경 변수나.env에 고정해 두면 값이 갱신되지 않는다. 만료 시각이 지나면 그때부터 요청이 실패한다.
credential과 권한은 별개다
credential은 “누구냐”, 권한은 “무엇을 할 수 있냐"다. 같은 사람이라도 어떤 role을 assume했느냐에 따라 다른 주체가 된다. --profile my-profile은 어떤 role로 들어갈지 고르는 옵션이다. 버킷 A에는 접근되는데 B에는 안 된다면 credential보다 그 주체의 정책을 먼저 본다.
지금 누구로 요청하는지는 이렇게 확인한다.
aws sts get-caller-identity --profile my-profileboto3가 credential을 찾는 순서
botocore 1.43 소스(botocore/credentials.py의 create_credential_resolver) 기준이다. 위에서부터 찾고, 처음 나온 것을 쓴다.
0. 코드에서 직접 넘긴 값 (boto3.client(..., aws_access_key_id=...))
1. 환경 변수 AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN
2. 프로필의 assume role 설정 (role_arn + source_profile)
3. web identity (AWS_WEB_IDENTITY_TOKEN_FILE, 쿠버네티스 IRSA가 이 경로)
4. SSO 프로필 (aws sso login으로 받은 캐시)
5. ~/.aws/credentials
6. aws login 캐시
7. credential_process
8. ~/.aws/config 안의 키
9. 컨테이너 credential 엔드포인트 (ECS, EKS Pod Identity)
10. EC2 인스턴스 메타데이터 (IMDS)환경에 따라 실제로 걸리는 곳은 대개 하나다.
- 로컬 컨테이너: 1번.
.env를 컨테이너 환경 변수로 넣는다. - 쿠버네티스 pod: 3번이나 9번. pod에 붙은 role로 임시 credential을 받고 SDK가 알아서 갱신한다. 환경 변수를 넣을 필요가 없다.
- EC2: 10번.
함정: 환경 변수가 프로필을 이긴다
환경 변수에 만료된 키가 남아 있으면 멀쩡한 프로필이 있어도 요청이 계속 실패한다. AWS_PROFILE을 설정해도 마찬가지다. AWS_PROFILE은 2번 이후에서 어느 프로필을 읽을지 정할 뿐이고, 1번은 그대로 먼저 확인한다.
코드에서 Session(profile_name=...)으로 프로필을 직접 넘기면 다르다. 이때는 1번을 건너뛴다. 가짜 키로 확인한 결과다.
# 환경: AWS_ACCESS_KEY_ID=AKIAENVEXAMPLE, AWS_PROFILE=my-profile
# ~/.aws/credentials의 [my-profile]에는 AKIAPROFILEEXAMPLE
import boto3
c = boto3.Session().get_credentials()
c.access_key, c.method
# ('AKIAENVEXAMPLE', 'env')
c = boto3.Session(profile_name='my-profile').get_credentials()
c.access_key, c.method
# ('AKIAPROFILEEXAMPLE', 'shared-credentials-file')get_credentials().method를 출력하면 어느 단계에서 credential을 찾았는지 바로 보인다. 헷갈릴 때 가장 먼저 해 볼 일이다.
도구에 저장한 키는 기본 체인에 들어가지 않는다
워크플로 도구의 “connection"이나 앱 설정에 키를 넣었다고 프로세스의 기본 체인이 채워지지는 않는다. 그 키는 connection id를 지정해 조회하는 코드에서만 쓰인다. 같은 프로세스에서 boto3.client('s3')를 그냥 부르는 코드는 여전히 위 순서로 찾는다. 둘은 따로 채워야 한다.
에러 코드
| 증상 | 뜻 |
|---|---|
400 Bad Request (S3 HeadObject) | 임시 credential 만료일 가능성이 크다. 에러 코드는 ExpiredToken인데 HEAD 응답에는 본문이 없어 상태 코드만 보인다 |
403 Forbidden / AccessDenied | credential은 유효하지만 권한이 없다 |
InvalidAccessKeyId | 키가 없거나 삭제됐다 |
SignatureDoesNotMatch | secret이 키와 맞지 않는다 |
NoCredentialsError | 체인 전체에서 아무것도 찾지 못했다 |
MissingDependencyException: botocore[crt] | aws login 방식 credential은 CRT가 필요하다. 구버전 botocore에서는 export-credentials나 정적 키로 우회한다 |
400과 403만 구분해도 키를 갱신할지, 정책을 볼지가 정해진다.