🔥 Assume Role: 임시 자격증명이 만들어지는 순간
강의 목차
처음 콘솔에서 AssumeRole을 눌렀을 때, 솔직히 "이게 왜 API 호출 하나짜리 일인가" 싶었다. Role을 누르고, "역할 전환"을 누르면 오른쪽 위 이름이 바뀐다. 이게 끝? 뒤에서 정확히 뭐가 오가는지는 한참 뒤에야 STS 문서를 뒤져 가며 이해했다. 그 뒤에서 일어나는 일을 따라간다.
User, Group, Role: 세 가지 주체의 차이에서 Role은 장기 자격증명 없이, AssumeRole 호출이 들어오는 순간에만 STS가 임시 자격증명을 만들어 내준다고 적었다. 그 문장 뒤에 생략된 시퀀스를 바로 따라간다. 누가, 무엇을 들고, 어디로, 왜 가는가.
한 번의 AssumeRole 호출이 걷는 길
호출은 단순하다. 자격증명을 이미 가진 주체(User의 장기 키든, 다른 Role에서 받은 임시 자격증명이든, EC2 Instance Profile이 대신 관리해 주는 자격증명이든)가 STS에 "나는 이 Role을 쓰고 싶다"라고 요청한다. STS는 두 가지를 확인한다.
- 요청한 주체가 그 Role의 Trust Policy에 명시된 대상 안에 드는가.
- 호출한 주체에게 붙은 Identity-based 정책이 이 Role ARN에 대해
sts:AssumeRole을 Allow하고 있는가.
같은 계정 안이라면 Trust Policy가 특정 User를 직접 Principal로 허용하는 구성으로 Identity-based 정책 없이도 통과하는 경우가 있다. 실무에서는 두 정책 모두 명시적으로 sts:AssumeRole을 허용하는 쪽으로 설정하는 게 일반적이다. 크로스 계정이면 예외 없이 양쪽 정책이 모두 Allow여야 한다. 정책 평가 흐름: Allow와 Deny가 함께 나오면에서 본 교집합 규칙 그대로다.
호출자가 두 조건을 모두 만족하면 STS는 Access Key ID, Secret Access Key, Session Token, 이렇게 세 조각의 임시 자격증명을 돌려준다. 세 조각의 이름은 User, Group, Role 비교 글에서 이미 본 그대로다. STS는 응답에 AssumedRoleUser(지금 이 세션을 부르는 이름과 ID)와 PackedPolicySize(세션 정책·태그가 내부 압축 한도를 몇 % 쓰고 있는지)도 함께 담아 준다.
PackedPolicySize 값이 100을 넘기면 호출 자체가 PackedPolicyTooLarge로 실패한다. 세션 정책과 세션 태그를 잔뜩 넘길 때 가끔 보게 되는 에러다. 100이 다가오고 있으면 관리형 ARN 방식으로 넘기거나 태그 수를 줄여 준다.

Trust Policy: 한 Role이 가진 또 하나의 정책
여기서 결정적으로 헷갈리기 쉬운 게 있다. Role에는 정책이 두 종류 붙는다. 하나는 Policy: JSON으로 권한을 표현하는 법에서 본 Identity-based 정책으로, 이 Role을 가정한 뒤 무엇을 할 수 있는지를 적는다. 다른 하나가 Trust Policy, 누가 이 Role을 가정할 수 있는지를 적는 항목이다.
둘 다 같은 JSON 문법을 쓰지만 놓이는 곳과 의미가 다르다. Trust Policy에는 반드시 Principal이 적혀야 한다. Action은 Principal의 종류에 맞춰 쓴다. AWS 계정·User·Role·AWS 서비스가 들어오면 sts:AssumeRole, SAML IdP가 들어오면 sts:AssumeRoleWithSAML, OIDC/Web Identity면 sts:AssumeRoleWithWebIdentity다. 세션 태그나 소스 아이덴티티를 따로 허용해 주려면 sts:TagSession·sts:SetSourceIdentity가 별개 Statement로 덧붙는다. 기본 Action을 대체하는 게 아니라 같이 쓰는 쪽이다.
Resource 필드는 없다. IAM Access Analyzer의 정책 검사 규칙도 이걸 명시한다. Trust Policy는 Role 자체에 붙어 있고, 이 정책의 Resource는 늘 "이 Role"이기 때문에 적을 필요가 없다.
가장 단순한 Trust Policy는 이렇게 생겼다. "이 계정의 특정 User만 Role을 가정할 수 있다"는 선언이다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111111111111:user/alice" },
"Action": "sts:AssumeRole"
}
]
}{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111111111111:user/alice" },
"Action": "sts:AssumeRole"
}
]
}Principal 항목에 Service: "ec2.amazonaws.com"이 들어가면 EC2가 가정할 수 있는 Role이 되고, Federated가 들어가면 OIDC·SAML 페더레이션용 Role이 된다. 이 Principal 항목의 허용 범위는 앞서 User, Group, Role 비교 글에서 짚은 내용과 같다. AWS 계정, User, Role, AWS 서비스, 페더레이션 주체, Role 세션 같은 몇 가지 주체 타입이 허용되는데, 여기서 한 줄 기억해 둘 건 공식 Principal 문서에서 Group은 여전히 Principal로 쓸 수 없다고 분명히 적혀 있다는 점이다.
이 두 정책의 분업을 감각적으로 잡아 두면 나중이 편하다. Trust Policy가 무너지면 아예 AssumeRole 호출이 성립하지 않고, Identity-based 정책이 비어 있으면 Role을 가정하는 데는 성공하지만 그 뒤 아무 API도 못 부른다.

DurationSeconds: TTL이 있는 자격증명
공식 AssumeRole API 레퍼런스 기준으로 세션 수명은 최소 900초(15분), 기본 3,600초(1시간), 최대 43,200초(12시간)다. Role마다 관리자가 정해 놓은 MaxSessionDuration이 상한이고, 호출자는 이 상한 안에서만 DurationSeconds 값을 정할 수 있다.
주의할 대목은 role chaining이다. 임시 자격증명으로 다시 다른 Role을 가정하는 걸 role chaining이라고 부른다. 공식 문서는 여기서 상한을 1시간으로 분명히 적는다. "if you assume a role using role chaining and provide a DurationSeconds parameter value greater than one hour, the operation fails." Role chain 두 번째 구간부터는 무조건 1시간이 최대다. 12시간을 바라고 쓰면 API 호출이 그냥 실패한다.
실무에서 체인이 등장하는 전형은 이렇다. GitHub Actions가 OIDC로 AWS Role A를 가정하고, A의 세션에서 다시 계정 B의 Role을 가정한다. A→B 두 단계짜리 체인이다. 두 번째 AssumeRole의 DurationSeconds를 넉넉하게 주고 싶어도, 1시간이 실제 상한이다. 배치가 길면 체인 중간에 자격증명이 만료되고, 재발급 로직을 안에 심어 두지 않으면 그 시점에 작업이 실패한다.
그래서 체인은 가능한 한 얕게 유지하는 쪽이 운영 피로도가 낮다. "이 체인이 꼭 2단계여야 하나, 1단계로 줄일 수는 없나"를 먼저 의심하는 게 낫다.

ExternalId: confused deputy를 막는 한 줄
크로스 계정 Role을 설계하다 보면 반드시 등장하는 이름이 ExternalId다. 공식 문서에는 Confused Deputy 문제라는 제목으로 소개돼 있다. 풀어서 말하면 이런 장면이다.
외부 SaaS(예: 어떤 모니터링 회사)가 고객 AWS 계정에 접근하려고 Role을 요청한다. 그 Role의 Trust Policy 원칙이 "SaaS 회사의 AWS 계정을 허용"이다. 여기까지는 자연스럽다. 문제는 이 SaaS 회사가 여러 고객을 서빙한다는 데에 있다. 고객 B가 어떤 이유로 고객 A의 Role ARN을 알아내서, SaaS에게 "이 Role로 접근해서 이런 작업을 해 달라"고 API를 건다면, SaaS는 자기 자격증명으로 그 Role을 가정해 고객 A의 자원을 건드릴 수 있다. SaaS 입장에서는 계약된 대로 Role을 가정했을 뿐인데, 그 Role이 실제로는 제3의 고객 것이었다. 이게 "혼동된 대리인(confused deputy)"이다.
공식 가이드가 제시하는 해결은 단순하다. SaaS가 자기 고객마다 고유한 ExternalId 값을 발급해 주고, 고객은 그 값을 자기 Role의 Trust Policy Condition에 박아 둔다. 그러면 SaaS가 AssumeRole을 부를 때 ExternalId를 함께 실어야만 호출이 성공한다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::222222222222:root" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": { "sts:ExternalId": "unique-per-customer-12345" }
}
}
]
}{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::222222222222:root" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": { "sts:ExternalId": "unique-per-customer-12345" }
}
}
]
}여기서 자주 거꾸로 이해되는 대목이 하나 있다. ExternalId는 비밀 값(secret)은 아니다. 공식 가이드는 ExternalId를 비밀로 취급할 필요가 없다고 분명히 적는다. 보안 효과는 "아무도 모르기 때문에"가 아니라 구조에서 나온다. 서드파티가 고객마다 유일한 값을 관리하고, 각 고객 Role의 Trust Policy Condition이 자기 고유의 값을 요구한다. 그러면 고객 B의 값을 누군가 알아내도 그 값으로 고객 A의 Role을 가정할 수 없다. 값의 비밀성이 아니라 값-고객-Role의 매핑이 보안을 만든다.
뒤집어 말하면, 내가 직접 Role을 만들면서 "그냥 UUID 하나 넣어 둘까"라고 쓰면 confused deputy가 막히지 않는다. 값을 관리하는 주체는 Role을 가정해 들어오는 바깥 회사여야 한다. 그 쪽이 고객마다 다른 값을 일관되게 찍어 주는 구조여야 비로소 약속이 성립한다.
AssumeRole의 두 사촌: WithSAML과 WithWebIdentity
sts:AssumeRole과 거의 같은 형태의 API가 둘 더 있다. AssumeRoleWithSAML과 AssumeRoleWithWebIdentity. 이름대로다. 전자는 회사의 SAML IdP(오래된 기업용 SSO)에서 온 응답을 실어서 부르고, 후자는 OIDC 토큰(JWT)을 실어서 부른다.
셋의 결정적인 차이 한 가지만 짚으면, AssumeRole은 호출자가 이미 AWS 자격증명이 있는 주체여야 한다. 반면 AssumeRoleWithSAML과 AssumeRoleWithWebIdentity는 호출자에게 AWS 자격증명이 없다. SAML 응답이나 OIDC JWT 자체가 "나는 이 사람이다"라는 증빙을 대신한다.
GitHub Actions가 AWS에 비밀 키 없이 배포하는 요즘 방식은 그래서 AssumeRoleWithWebIdentity다. 워크플로우가 GitHub에서 OIDC JWT를 받고, AWS에 그 JWT를 실어 보낸다. AWS 쪽에는 GitHub의 OIDC 공급자를 IAM 페더레이션 Identity Provider로 등록해 두는 한 번의 설정이 있다. 그다음부터는 장기 Access Key가 저장소 어디에도 남지 않는다.
지난 편에서 장기 Access Key를 CI에 박아 두는 관행을 "되돌아올 숙제"라고 불렀던 이유가 이것이다. 현대적인 정답은 Web Identity 쪽으로 넘어갔다.
Cross-account: 두 계정이 서로를 믿는 방식
크로스 계정 시나리오는 AssumeRole의 본격적인 활용 무대다. 구도는 이렇다. 계정 A에 User Alice가 있고, 계정 B에 어떤 S3 버킷이 있다. Alice가 계정 B의 자원을 쓰려면 계정 B가 미리 "외부 Role"을 하나 만들어 둬야 한다. 그 Role의 Trust Policy Principal에 계정 A(또는 계정 A의 특정 User)를 허용으로 박아 둔다.
그다음 Alice 쪽에서는 자신이 속한 계정 A의 관리자가 Alice에게 sts:AssumeRole을 계정 B의 Role ARN에 허용해 주는 Identity-based 정책을 걸어 준다.
이 두 허용이 맞물려야 Alice가 AssumeRole(계정 B의 Role ARN)을 부를 수 있다. 받은 임시 자격증명은 계정 A의 Alice가 아니라 계정 B의 Role을 쓰는 세션이다. 그걸로 계정 B의 S3 버킷에 접근한다.
정책 평가 흐름: Allow와 Deny가 함께 나오면에서 "크로스 계정은 합집합이 아니라 교집합"이라는 규칙을 짚었는데, 그 규칙이 여기서도 그대로 작동한다. 두 계정의 허용이 양쪽 모두 있어야 호출이 통과한다. 한쪽만 있으면 즉시 AccessDenied다.

언제 AssumeRole을 쓰지 말아야 하는가
체크리스트로 한 번 짚어 둔다.
- 체인을 3단계 이상 쌓지 말자. 두 번째부터
DurationSeconds가 1시간으로 고정되고, 갱신 로직이 필요하다. 체인 1단계로 해결할 수 있으면 거의 늘 그쪽이 낫다. - 한 사람(또는 한 CI)에 수십 개의 Role을 만들지 말자. "정확한 권한 하나에 Role 하나"를 극단까지 밀면 Role이 폭발한다. 작업별 Role보다 환경별 Role + 세션 정책(2,048자 범위)으로 좁히는 쪽이 관리가 낫다는 감각은 Session Policy 얘기에서 이미 짚었다.
- ExternalId를 내가 만들지 말자. 내가 만든 값은 confused deputy를 막아 주지 못한다. 서드파티에게서 받아서 Trust Policy Condition에 넣어야 한다.
STS 자체는 추가 요금이 없는 서비스다. 공식 문서가 쓰는 표현은 "무료"가 아니라 at no additional charge에 가깝다. 어느 쪽이든 돈이 새어 나오는 곳은 다른 데 있다. Trust Policy와 ExternalId의 운영 책임, 체인 만료 대응, 세션 정책 길이 관리 같은 것들이다. 과금이 없다고 가볍게 대할 수 있는 서비스는 아니다.
오늘의 3줄
- Role에는 두 장의 정책이 따라붙는다. 들어오는 문을 여는 Trust Policy와 들어온 다음 쓸 권한을 여는 Identity-based Policy.
DurationSeconds는 최대 12시간이지만, role chaining의 두 번째 구간부터는 1시간이 상한이다.- ExternalId의 보안성은 "비밀"이 아니라 "서드파티가 고객마다 유일하게 관리하는 값"이라는 구조에서 온다. 내가 Role을 만들며 직접 UUID를 박아 넣어서는 confused deputy가 막히지 않는다.
이 흐름은 Instance Profile: EC2는 IAM을 어떻게 얻는가로 곧장 연결한다. AssumeRole 시퀀스는 EC2 인스턴스 안에서는 IMDS라는 메타데이터 서비스를 거쳐 투명하게 돌아간다. IMDSv1·v2 차이도 거기서 함께 짚는다.
참고 자료
- AssumeRole, AWS STS API Reference: 파라미터·응답 필드·DurationSeconds 한도·role chaining 1시간 제한의 공식 근거
- The confused deputy problem, IAM User Guide: ExternalId가 왜 서드파티가 관리해야 하는 값인지에 대한 공식 설명
- AssumeRoleWithWebIdentity, AWS STS API Reference: OIDC JWT 기반 Role 가정의 공식 참조
- AssumeRoleWithSAML, AWS STS API Reference: 기업용 SAML IdP 페더레이션용 AssumeRole 변형
- Configuring OpenID Connect in AWS, GitHub Docs: GitHub Actions에서 장기 키 없이 AWS Role을 가정하는 현재 권장 절차











