루트키를 넘기지 마라 — 역할과 AssumeRole

Cloud Control Plane
  1. 1. AWS CLI는 무엇을 보내는가
  2. 2. SigV4 검증기를 Go로 밑바닥부터
  3. 3. CLI가 속아 넘어가는 순간
  4. 4. IAM과 STS를 나누다
  5. 5. credstore를 프로세스 밖으로 — DynamoDB에 얹다
  6. 6. 최초의 계정은 어디서 오는가
  7. 7. 맨 아래 거북이 — 제네시스 계정과 신뢰의 뿌리
  8. 8. 인증은 됐는데 권한이 없다 — 서명된 CreateAccount
  9. 9. 루트키를 넘기지 마라 — 역할과 AssumeRole
전체 9편
9 / 9

8편에서 CreateAccount는 새 계정의 루트 자격증명을 통째로 돌려줬다. 편했지만, 그러면서 정직하게 인정한 게 있다 — 이건 AWS가 실제로 하는 안전한 방식이 아니라고. 장기 루트키는 유출되면 영원히, 전권으로 뚫린다. 이번 편은 그 빚을 갚는다. 루트키를 넘기는 대신, 역할을 빌려 임시 자격증명으로 들어간다.

역할은 신분이 아니라 빌리는 모자

지금까지 mincloud의 모든 신원은 영구 자격증명을 가졌다. 루트도, IAM 유저도 각자 키+시크릿이 있고, 안 지우면 영원히 유효했다.

역할(Role)은 다르다. 자기 자격증명이 없다. 역할은 잠깐 쓰고 벗는 모자다. 두 가지를 가진다 — 이 모자를 쓰면 뭘 할 수 있나(권한), 그리고 누가 이 모자를 써도 되나(신뢰 정책). 아무도 역할로 로그인하지 않는다. 자기 자신으로 시작해서, 역할을 빌려서(assume) 잠깐 그것이 된다.

그러니 순서가 정해진다. 역할이 있어야 빌릴 수 있다. CreateRoleAssumeRole보다 먼저다.

먼저: 역할을 만든다

멤버 계정을 하나 만들고(8편의 그 문), 그 계정 안에 역할을 만든다. 핵심은 --assume-role-policy-document신뢰 정책이다.

$ aws iam create-role --role-name OrgAccess \
    --assume-role-policy-document '{
      "Version": "2012-10-17",
      "Statement": [{
        "Effect": "Allow",
        "Principal": { "AWS": "arn:aws:iam::000000000001:root" },
        "Action": "sts:AssumeRole"
      }]
    }' --endpoint-url http://localhost:9910

arn:aws:iam::207346699092:role/OrgAccess

이 JSON이 이 시리즈에서 처음 등장하는 진짜 정책 문서다. 8편의 인가는 isManagementRoot라는 하드코딩 한 줄이었다. 여기선 “누가 이 역할을 빌려도 되나"를 데이터로 적는다 — “관리 계정(000000000001)의 루트만.” 서버는 나중에 이 문서를 읽어 판단한다.

1
2
3
4
func TrustAllows(trustJSON, callerARN, callerAccount string) bool {
    // 문서를 파싱해, Effect=Allow / Action=sts:AssumeRole 이고
    // Principal 이 호출자를 포함하는 Statement 가 있으면 허용.
}

하드코딩 게이트가 처음으로 읽어서 평가하는 정책이 된 순간이다.

다음: 역할을 빌린다

이제 관리 루트(마스터)로 그 역할을 빌린다.

$ aws sts assume-role \
    --role-arn arn:aws:iam::207346699092:role/OrgAccess \
    --role-session-name jeff-session \
    --endpoint-url http://localhost:9900

돌아오는 건 영구키가 아니다. 임시 자격증명이다.

AccessKeyId:  ASIADARWTRNKAN3U4M7T          ← ASIA (임시) 프리픽스
Expiration:   2026-07-06T16:36:01+00:00     ← 1시간 뒤 죽음
AssumedRole:  arn:aws:sts::207346699092:assumed-role/OrgAccess/jeff-session

AKIA가 아니라 ASIA로 시작한다 — STS가 발급한 임시키의 표식이다. 그리고 세 조각이 더 있다: SecretAccessKey, SessionToken, 그리고 만료 시각.

이 임시키로 신원을 물으면, 우리는 멤버 계정 안에 들어가 있다.

$ AWS_ACCESS_KEY_ID=ASIA... AWS_SECRET_ACCESS_KEY=... AWS_SESSION_TOKEN=... \
    aws sts get-caller-identity --endpoint-url http://localhost:9900
{
    "UserId": "AROAVX9IJ90X1PM0DI57:jeff-session",
    "Account": "207346699092",
    "Arn": "arn:aws:sts::207346699092:assumed-role/OrgAccess/jeff-session"
}

계정 207346699092. 관리 루트는 이 멤버의 루트키를 한 번도 손에 쥐지 않았다. 자기 키로 역할을 잠깐 빌렸을 뿐이고, 1시간 뒤엔 이 임시키도 죽는다. 유출돼도 피해 반경이 “영원"이 아니라 “한 시간"이다. 이게 8편이 못했던, AWS의 안전한 방식이다.

session token과 만료는 서버가 지킨다

임시키는 그냥 키+시크릿이 아니다. 요청마다 SessionToken(X-Amz-Security-Token 헤더)을 같이 실어야 하고, 만료 전이어야 한다. Authenticate가 서명을 검증한 뒤 이 둘을 더 본다.

1
2
3
4
5
6
7
8
if cred.SessionToken != "" { // 임시 자격증명
    if r.Header.Get("X-Amz-Security-Token") != cred.SessionToken {
        return ..., &AuthError{Code: "InvalidClientTokenId", ...}
    }
    if !cred.Expires.IsZero() && time.Now().After(cred.Expires) {
        return ..., &AuthError{Code: "ExpiredToken", ...}
    }
}

영구키엔 이 둘이 비어 있어 그냥 지나간다. 임시키만 이 관문을 더 통과한다.

신뢰하지 않으면 거부한다

신뢰 정책은 관리 계정만 적어 뒀다. 그럼 멤버 계정의 루트 자신이 이 역할을 빌리려 하면?

$ ...(멤버 루트 키로)... aws sts assume-role \
    --role-arn arn:aws:iam::207346699092:role/OrgAccess --role-session-name intruder

An error occurred (AccessDenied) when calling the AssumeRole
operation: User: arn:aws:iam::207346699092:root is not authorized
to perform: sts:AssumeRole on resource: .../OrgAccess

AccessDenied. 8편에선 이 판단이 하드코딩 한 줄이었지만, 지금은 역할에 붙은 JSON 문서가 내린 결정이다. 신뢰 정책에 없으면 못 빌린다.

루프가 닫혔다

8편에서 멤버 계정을 만들었고, 9편에서 그 계정에 안전하게 들어가는 법을 얻었다. 진짜 AWS의 OrganizationAccountAccessRole이 바로 이거다 — 계정 만들 때 관리 계정을 신뢰하는 역할을 하나 심어두고, 관리 계정이 그걸 assume해서 들어간다. 우리가 손으로 한 create-role(신뢰=관리 계정) + assume-role이 정확히 그 그림이다. 마법이 아니라, 원시 도구 두 개의 조합.

정직하게 남는 것도 있다. 지금 CreateRole권한 검사가 없다 — 인증만 되면 누구나 자기 계정에 역할을 만든다. 그리고 역할은 신뢰 정책(누가 빌리나)은 있지만, 아직 권한 정책(빌린 뒤 뭘 할 수 있나)은 없다. 임시키는 지금 사실상 그 계정의 전권을 갖는다.

그 두 구멍이 다음 편을 가리킨다. 오늘 trust policy로 “누가 빌리나"를 데이터로 판단하기 시작했으니, 같은 엔진을 “누가, 무슨 행동을, 무슨 리소스에” 로 일반화하면 — 그게 IAM 정책, 진짜 인가 엔진이다. 하드코딩 한 줄에서 시작해 신뢰 정책까지 온 이 흐름의 종착지다.