어제 Role을 보며 "STS가 임시 자격증명을 발급한다"고 한 줄로 넘어갔다. 그런데 그 임시 자격증명이 정확히 어떤 구조이고, 어떻게 검증되고, 어떻게 만료되는지를 모르면 실무에서 만나는 가장 흔한 IAM 문제들 — "왜 SDK가 갑자기 401을 던지지?", "왜 federation 토큰이 1시간 후에 죽지?", "왜 Lambda가 자기 함수 ARN을 모르지?" — 를 풀 수 없다.
오늘은 STS의 4개 API를 깊이 보고, 정책의 Condition 키들을 ABAC 관점에서 분석하고, 리소스 기반 정책의 cross-account 패턴 — 특히 confused deputy 문제와 External ID — 를 본다. 시험에 가장 자주 나오는 영역 중 하나이면서, 실무에서 IAM이 깨질 때 90%가 이 영역이다.
STS는 임시 자격증명을 발급하는 글로벌 서비스다. 2011년 출시됐고, 처음엔 sts.amazonaws.com(글로벌)만 있었지만 2018년부터 리전별 endpoint(sts.ap-northeast-2.amazonaws.com)가 권장된다. 이유는 latency와 신뢰성. 글로벌 endpoint는 us-east-1에 물리적으로 위치하므로, us-east-1 장애 시 글로벌 endpoint도 죽는다. 리전 endpoint를 쓰면 그 리전 안에서만 의존성이 닫힌다.
STS가 왜 별도의 서비스로 분리됐는지를 보면 클라우드 자격증명 모델의 본질이 보인다. 전통적인 시스템에서는 인증(authentication)과 인가(authorization)가 한 곳에 묶여 있었다(LDAP, AD, Kerberos KDC). 그런데 AWS처럼 수백 개 서비스에 수억 개 자원이 흩어진 환경에서는 그 모델이 안 통한다. 그래서 AWS는 인증의 결과를 검증 가능한 토큰으로 만들어 흘려보내고, 각 서비스가 그 토큰만 보고 권한 평가를 한다. STS가 토큰 발급자(token issuer)이고, IAM은 권한 정책 저장소이고, 각 서비스(S3, DynamoDB...)는 token verifier 역할을 한다. 이게 OAuth2/OIDC 모델과 정확히 같은 구조다.
STS의 핵심 API는 다섯 가지다.
| API | 입력 | 출력 | 용도 |
|---|---|---|---|
AssumeRole | RoleArn, RoleSessionName | 임시 자격증명 (15분~12시간) | 같은 계정·cross-account Role 전환 |
AssumeRoleWithSAML | RoleArn, PrincipalArn, SAMLAssertion | 임시 자격증명 (15분~12시간) | AD/Okta SAML federation |
AssumeRoleWithWebIdentity | RoleArn, WebIdentityToken | 임시 자격증명 (15분~12시간) | Google/Facebook/Cognito OIDC, EKS IRSA |
GetSessionToken | (DurationSeconds, MFA token) | 임시 자격증명 (15분~36시간) | IAM User의 MFA 강화 세션 |
GetFederationToken | Name, Policy | 임시 자격증명 (15분~36시간) | 커스텀 federation broker |
GetCallerIdentity | (none) | 현재 호출자의 ARN | 디버깅 |
🔍 더 깊이: AssumeRole이 반환하는 자격증명은 ① AccessKeyId(시작이
ASIA로 다름, 영구 키는AKIA), ② SecretAccessKey, ③ SessionToken(JWT 비슷한 구조의 base64 문자열) 3종 세트다. SDK가 API를 호출할 때 SigV4 서명에 모든 3개를 사용한다. AWS 서비스는 받은 SessionToken을 STS 공개키로 검증해 만료 여부와 권한을 확인한다. 영구 키와 달리 임시 키는 STS 측에서 즉시 revoke가 가능하다(aws sts revoke-credentials또는revoke older sessions액션). 더 흥미로운 점은 SessionToken 안에 원래 Role의 권한 스냅샷이 박혀 있어, 발급 이후 Role의 Permission Policy를 바꿔도 그 세션엔 반영되지 않는다. AWS가 검증을 분산할 수 있는 이유다.
선택지를 클릭하면 정답·해설이 펼쳐집니다.
문제 1
한 회사가 외부 SaaS 백업 서비스에게 자기 S3에 백업 데이터를 쓰게 하려고 한다. 가장 안전한 설정은?
문제 2
Lambda 함수에서 `boto3.client('s3').list_buckets()`가 `ExpiredToken` 에러를 반환했다. 가장 가능성 높은 원인은?
문제 3
다음 Trust Policy가 의미하는 것은? ```json {"Principal": {"Service": "ec2.amazonaws.com"}, "Action": "sts:AssumeRole"} ```
문제 4
ABAC 패턴: 모든 자원에 `Project` 태그가 붙고, IAM Principal에도 `Project` 태그가 있다. "자기 프로젝트 자원에만 접근 가능"을 표현하는 Condition은?
문제 5
STS Regional endpoint(`sts.ap-northeast-2.amazonaws.com`) 대신 글로벌 endpoint(`sts.amazonaws.com`)를 쓰면 어떤 위험이 있나?
문제 6
EKS Pod에서 AWS API를 호출하려고 한다. 가장 안전한 방법은?
문제 7
AssumeRole 호출 시 RoleSessionName의 역할은?
문제 8
A 계정의 사용자가 B 계정의 S3 버킷에 접근해야 한다. 어떤 정책 조합이 필요한가?
💡 관련 이론: STS의 임시 자격증명은 OAuth 2.0의 access token, Kerberos의 ticket-granting ticket과 같은 계열의 메커니즘이다. 모두 "단기 유효 토큰 + 갱신 메커니즘"이라는 패턴을 공유한다. 보안 측면에서 이 패턴이 우월한 이유는 유출되어도 자동으로 무효화된다는 점. 1시간 후 자동 만료되는 토큰을 훔쳐도 다음 시간엔 못 쓴다. 반면 IAM User의 access key는 명시적으로 회전하지 않는 한 영구 유효하다. 이 차이가 Capital One 사고(IMDSv1으로 EC2의 영구 access key를 훔침)와 Twitter 해킹(직원 세션이 1시간으로 제한됐다면 피해 축소) 양쪽의 핵심이다.
🔍 더 깊이: AccessKeyId의 prefix가 자격증명 종류를 알려준다.
AKIA는 영구 IAM User key,ASIA는 STS 임시 키,AROA는 Role 식별자,AGPA는 Group,AIDA는 User. 보안 도구가 GitHub에 키가 노출됐는지 검사할 때 이 prefix로 정규식을 짠다. truffleHog, GitGuardian 같은 도구가 정확히 이 패턴을 본다.ASIA로 시작하는 키가 노출되면 정상적으론 곧 만료되지만, 발급 후 12시간 이내라면 여전히 위험하다.
Role을 만들 때 함께 정의하는 Trust Policy는 "누가 이 Role을 assume할 수 있는가"를 결정한다. 이건 일반 Permission Policy와 다른 종류의 정책이다. Permission Policy가 "이 Role을 쓰는 사람이 무엇을 할 수 있나"를 정한다면, Trust Policy는 그 한 단계 위의 게이트키퍼다.
// Trust Policy 예시: EC2 서비스가 이 Role을 assume할 수 있음
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "ec2.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}
// Cross-Account: 다른 계정의 모든 User/Role이 assume 가능
{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::123456789012:root"},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {"sts:ExternalId": "my-shared-secret-12345"},
"Bool": {"aws:MultiFactorAuthPresent": "true"}
}
}Principal이 누구를 허용할지를 결정한다. Service는 AWS 서비스(예: ec2.amazonaws.com, lambda.amazonaws.com), AWS는 같은 또는 다른 계정의 IAM 엔터티, Federated는 SAML/OIDC provider다. Trust Policy가 막으면 Permission Policy가 아무리 넓어도 assume이 안 된다.
서비스 principal의 이름이 직관과 다르게 잡힌 경우가 있다. 예를 들어 ECS Task Role의 Trust Principal은 ecs.amazonaws.com이 아니라 ecs-tasks.amazonaws.com이다. Lambda는 lambda.amazonaws.com이지만 Lambda@Edge용 Role은 lambda.amazonaws.com과 edgelambda.amazonaws.com 두 개를 동시에 trust해야 한다. RDS Enhanced Monitoring은 monitoring.rds.amazonaws.com이라는 이상한 이름을 쓴다. 이런 비대칭은 AWS의 서비스 진화 역사에서 비롯됐고, 시험에 종종 함정으로 나온다.
⚠️ 함정: Trust Policy의 Principal에
"AWS": "arn:aws:iam::123456789012:root"을 적었다고 해서 "그 계정의 root user만" 허용하는 게 아니다. 그 계정 안의 모든 IAM 엔터티가 잠재적 허용 대상이라는 뜻이다. 실제 assume이 일어나려면 그 엔터티가 자기 계정 안에서sts:AssumeRole권한도 가져야 한다(양쪽 평가). 그래서 cross-account는 항상 양쪽 정책이 모두 허용해야 동작한다.
이게 시험에 자주 나오면서 실무에서도 자주 사고 나는 부분이다. 시나리오를 보자. SaaS 백업 회사 SaaS-Backup이 우리 S3에 백업 데이터를 쓰기 위해 우리 계정에 Role을 만든다. Trust Policy의 Principal은 SaaS-Backup의 계정 ID로 잡는다.
문제는 SaaS-Backup이 다른 고객 X도 받았을 때다. 공격자 X가 SaaS-Backup에게 "우리 백업도 처리해줘"라고 요청하면서 우리 Role의 ARN을 넣어버린다면? SaaS-Backup은 자기 계정으로 AssumeRole을 호출할 권한이 있으므로 우리 Role을 빌려 우리 S3에 X의 백업을 덮어쓸 수 있다. SaaS가 "고객을 헷갈려서" 다른 고객의 자원에 접근하는 이 현상을 confused deputy라고 한다. 이 용어는 1988년 Norm Hardy가 capability security 맥락에서 처음 명명했고, 본질은 "권한을 가진 대리인(deputy)이 누구를 위해 일하는지 모를 때 생기는 권한 오남용"이다.
해결책이 External ID다. 우리 Role의 Trust Policy에 sts:ExternalId Condition을 추가해 우리만 아는 비밀 문자열을 강제한다. SaaS-Backup이 우리 Role을 assume하려면 우리가 알려준 External ID를 같이 보내야 한다. X는 우리 External ID를 모르므로 SaaS-Backup이 X의 요청을 처리하려 해도 우리 Role 검증에 실패한다.
⚠️ 함정: External ID는 흔히 "비밀번호"로 오해되지만, 실은 "고객 식별자"에 가깝다. 강력한 무작위성보다 "각 고객마다 다른 값"이 핵심이다. SaaS 측에서는 고객별 External ID를 DB에 저장해 매번 다르게 사용한다. AWS는 모든 third-party SaaS Role 설정 시 External ID를 강제로 사용하라고 권장한다. Datadog, New Relic, Splunk, Snowflake 모두 가입 시 External ID를 자동 생성해서 보여준다.
📚 사례: 2023년 5월에 일부 보안 연구자들이 AWS의 SaaS 통합 패턴에서 External ID를 안 쓰거나, 모든 고객에게 같은 External ID를 부여하는 third-party 벤더들을 찾아냈다. 이들은 confused deputy 공격에 무방비였고, 일부는 다른 고객의 S3 데이터에 접근 가능했다. AWS는 이후 공식 가이드에서 "고객별로 다른, 추측 불가능한 External ID"를 명시적으로 요구하기 시작했다.
Policy의 Condition 절은 IAM 평가의 가장 강력한 부분이다. AWS는 수백 개의 condition key를 제공하는데, 시험에 자주 나오는 것들을 분류해두면 빠르다.
| 카테고리 | 키 | 예시 |
|---|---|---|
| 글로벌 (모든 서비스) | aws:RequestedRegion | "StringEquals": "ap-northeast-2" |
aws:SourceIp | "IpAddress": "203.0.113.0/24" | |
aws:SourceVpc | "StringEquals": "vpc-abc123" | |
aws:SecureTransport | "Bool": "true" (HTTPS 강제) | |
aws:MultiFactorAuthPresent | "Bool": "true" | |
aws:MultiFactorAuthAge | "NumericLessThan": "3600" (MFA 1시간 이내) | |
aws:PrincipalTag/Dept | "StringEquals": "Engineering" | |
aws:RequestTag/Env | "StringEquals": "prod" | |
aws:CurrentTime | "DateGreaterThan": "2026-01-01T00:00:00Z" | |
aws:UserAgent | "StringLike": "aws-cli/*" | |
| S3 서비스 | s3:prefix | "사용자별 폴더 강제" |
s3:x-amz-server-side-encryption | "StringEquals": "AES256" | |
s3:RequestObjectTag/Sensitivity | "StringEquals": "Public" | |
| EC2 서비스 | ec2:InstanceType | "t3 패밀리만 허용" |
ec2:ResourceTag/Owner | 인스턴스 태그 기준 |
🔍 더 깊이:
aws:PrincipalTag와aws:RequestTag의 조합은 ABAC의 핵심이다. Engineering 부서 사용자는Dept=Engineering태그를 갖고, Engineering이 만드는 자원도Dept=Engineering태그를 가져야 한다고 강제하면, 단일 정책으로 부서별 자원 분리를 구현할 수 있다. Identity와 자원에 같은 태그가 붙어 있을 때만 액션을 허용하는 패턴:"Condition": {"StringEquals": {"aws:RequestTag/Dept": "${aws:PrincipalTag/Dept}"}}. RBAC라면 부서마다 Group과 Policy를 만들어야 하는데, ABAC로는 정책 하나로 끝난다. NIST는 SP 800-162에서 ABAC를 RBAC 다음 세대로 명시했고, AWS는 2017년 이를 IAM에 도입했다.
💡 관련 이론: SigV4 서명 알고리즘(AWS API의 표준 인증)은 HMAC-SHA256 기반이다. 클라이언트가 ① canonical request(method, URI, headers, body hash), ② string-to-sign(timestamp, scope, canonical request hash), ③ signing key(SecretAccessKey를 다섯 단계 HMAC로 derive)를 만들어 최종 서명을 생성한다. 임시 자격증명에서는 SessionToken을
X-Amz-Security-Token헤더로 같이 보낸다. SigV4는 RFC 형태로 표준화되진 않았지만 공식 가이드가 그 자리를 한다. 2024년 후반에 출시된 SigV4a는 동일 서명으로 여러 리전 endpoint에 동시에 호출 가능한 새 버전이고, S3 Multi-Region Access Point에서 쓰인다. 서명 자체에 리전을 박지 않고 별도 ECDSA 키를 derive해 multi-region 검증을 가능케 한다.
⚠️ 함정:
aws:SourceIp는 AWS 서비스를 경유하면 동작하지 않는다. 예를 들어 Lambda가 S3를 호출할 때aws:SourceIp로 사내 IP만 허용하는 정책은 막혀 버린다. Lambda → S3는 AWS 내부망에서 호출되므로 SourceIp가 사내 IP가 아닌 AWS 내부 IP다. 이런 경우aws:SourceVpc나aws:SourceVpce(VPC Endpoint) 키를 써야 한다. 실제 시험에 자주 나온다.
EKS(Kubernetes on AWS)에서 Pod에 IAM 권한을 주는 표준 방법이 IRSA(IAM Roles for Service Accounts)다. 핵심 아이디어는 Kubernetes Service Account를 OIDC 토큰의 issuer로 만들고, AWS가 그 OIDC provider를 신뢰해서 AssumeRoleWithWebIdentity를 허용하는 것이다.
1. Pod 시작 → Service Account의 OIDC 토큰을 자동 주입 (kube-apiserver가 sign)
2. AWS SDK가 환경변수 AWS_ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE 확인
3. SDK가 STS AssumeRoleWithWebIdentity 호출 (OIDC 토큰 함께)
4. STS가 OIDC JWKS endpoint에서 EKS 클러스터 공개키 조회 → 토큰 서명 검증
5. STS가 임시 자격증명 발급 (15분 ~ Role MaxSessionDuration)
6. SDK가 만료 5분 전에 자동 재호출로 갱신
7. Pod 안의 코드가 그 자격증명으로 AWS API 호출
이 메커니즘 덕분에 노드 단위가 아닌 Pod 단위 권한 분리가 가능해진다. 같은 워커 노드 위에 있는 두 Pod이 완전히 다른 IAM Role을 가질 수 있다. 2023년 출시된 EKS Pod Identity는 OIDC를 거치지 않는 더 단순한 대안으로, eks-pod-identity-agent라는 데몬셋이 메타데이터 endpoint를 노출해 SDK가 직접 받아간다. 차이가 중요한데, IRSA는 클러스터 외부 OIDC provider 설정이 필요하고 multi-cluster 환경에서 관리가 복잡한 반면, Pod Identity는 EKS 콘솔에서 클릭으로 끝난다. 단 Pod Identity는 IRSA보다 늦게 나와서 일부 third-party 운영자(Karpenter, Cluster Autoscaler 구버전)와 호환되지 않는다.
🔍 더 깊이: IRSA의 OIDC 검증 흐름은 standard JWT 검증이다. STS는 EKS 클러스터의 OIDC issuer URL(예:
oidc.eks.ap-northeast-2.amazonaws.com/id/ABCDEF1234)에서 JWKS를 가져와 캐싱하고, 토큰의 서명을 검증한다. 토큰의sub클레임이system:serviceaccount:default:my-sa형태라서, IAM Role의 Trust Policy에서oidc.eks...:subCondition으로 특정 Service Account만 허용할 수 있다. 만약 Condition을 빼면 클러스터의 모든 Pod이 그 Role을 빌릴 수 있어 큰 보안 구멍이 된다. 시험에 자주 나오는 함정.
| 패턴 | 메커니즘 | 적합한 경우 |
|---|---|---|
| Role chaining | A 계정 사용자 → B 계정 Role assume → C 계정 Role assume | 다단 위임 |
| Resource-based policy | B 계정 S3 버킷 정책에 A 계정 사용자를 Principal로 명시 | S3/SQS/SNS/Lambda 등 |
| RAM (Resource Access Manager) | 자원을 다른 계정과 공유 (Transit Gateway, Subnet 등) | 인프라 공유 |
| AWS Organizations + delegated admin | 마스터 계정이 멤버 계정에 위임 | Security Hub, GuardDuty 중앙 관리 |
Role chaining에는 숨은 제약이 있다. 체이닝된 세션은 최대 1시간이다. A 계정에서 B Role을 assume할 때 DurationSeconds=12*3600을 줘도, B Role을 통해 C Role을 다시 assume하면 그 세션은 자동으로 1시간 제한이다. AWS가 무한 chaining 공격을 막기 위해 둔 제약인데, 장기 배치 워크로드에서 갑자기 토큰이 만료되는 원인이 되곤 한다. 해결: chaining 대신 처음부터 가장 깊은 Role을 직접 assume하거나, SDK의 자동 refresh 로직을 신뢰한다.
Resource-based policy는 두 방향에서 평가된다는 점이 중요하다. A 계정의 사용자가 B 계정의 S3 버킷에 접근하려면 ① A 계정의 IAM 정책이 S3 접근을 허용하고, ② B 계정의 버킷 정책이 A 계정을 허용해야 한다. OR가 아니라 AND다. 단 같은 계정 안의 resource-based policy는 OR로 평가되는데(IAM Policy가 허용하지 않아도 Bucket Policy가 허용하면 OK), cross-account는 AND다. 이 비대칭이 종종 정답을 가른다.
📚 사례: 2020년 7월 Twitter 해킹. 공격자들이 사회공학으로 직원 자격증명을 탈취해 admin 도구에 접근, 130개 계정(Obama, Biden, Musk 등)을 비트코인 사기에 동원했다. AWS 사고는 아니지만, 직접적 교훈은 세션 길이 제한과 MFA다. 만약 사용자 세션이 1시간 max, MFA 강제였다면 공격 시간 창이 훨씬 좁았을 것이다. AWS에서는 IAM Role의
MaxSessionDuration을 짧게(1시간) 유지하고, Trust Policy에aws:MultiFactorAuthPresent조건을 거는 것이 표준 가이드다.
📚 사례: 2022년 3월 Okta 해킹. Lapsus$ 그룹이 Okta의 third-party support 직원 노트북을 통해 일부 고객 데이터에 접근했다. Okta는 즉시 모든 영향 세션을 revoke했지만, 일부 OIDC 토큰은 expiration까지 유효했다. 교훈: federation을 쓰면 IdP가 뚫렸을 때 다운스트림 AWS 세션을 강제 만료하는 메커니즘이 필요하다. AWS는 이후 IAM Role의
aws:TokenIssueTimeCondition을 활용해 "특정 시점 이전 발급 토큰 거부" 패턴을 권장하기 시작했다.
시험에 가장 자주 나오면서 가장 자주 틀리는 게 IAM 평가 순서다. AWS는 공식 문서에서 명시적으로 정의한다.
1. SCP (Service Control Policy) — Organizations에서 강제. 거부면 즉시 거부.
2. Resource-based policy — S3 버킷 정책 등. 같은 계정이면 OR, cross-account면 AND.
3. Identity-based policy — User/Group/Role의 attached policies.
4. Permission boundary — Role/User에 설정된 상한. 거부면 즉시 거부.
5. Session policy — AssumeRole 시 inline으로 좁힌 범위. 거부면 즉시 거부.
6. Explicit Deny — 어떤 단계에서든 Deny가 나오면 최종 결과는 Deny.
Explicit Deny가 모든 Allow를 이긴다는 점, 그리고 SCP/Permission Boundary는 상한을 정한다는 점(권한을 부여하지 않고 차감만 한다)이 핵심이다. SCP를 처음 도입하는 조직이 가장 자주 하는 실수가 "SCP에 Allow를 적었으니 권한이 생긴다"는 오해다. SCP는 차단만 한다.
오늘의 핵심은 네 가지다. 첫째, STS는 임시 자격증명의 공장이고 AssumeRole·AssumeRoleWithSAML·AssumeRoleWithWebIdentity가 그 3대 주력 API다. 둘째, Condition 절은 IAM이 ABAC로 진화한 엔진이고, aws:PrincipalTag와 aws:RequestTag의 조합으로 정책 폭발 없이 부서별 자원 분리가 가능하다. 셋째, cross-account 시나리오의 confused deputy 문제는 External ID로 해결한다. 넷째, IAM 평가는 SCP → Resource-based → Identity-based → Boundary → Session policy 순으로 흐르고, Explicit Deny가 모든 Allow를 이긴다.
다음 글에서는 이 위에 올라가는 AWS CLI, SDK, CloudShell의 실전 사용 — 자격증명 체이닝, 프로필 관리, 서명 디버깅 — 을 본다.