인증은 됐는데 권한이 없다 — 서명된 CreateAccount

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편
8 / 9

6편에서 계정을 만드는 문은 “서명 없는 admin 문"이었다. 박스에 접근할 수 있으면 누구나 계정을 찍었다. 7편에서 제네시스 관리 계정(000000000001)을 세우고 나니, 그 문이 너무 헐겁다는 게 보인다. 진짜 AWS에서 계정 생성은 아무 문이 아니다 — 관리 계정의 권한이다.

이번 편은 그 문을 옮긴다. 서명 없는 문은 이제 제네시스 하나에만 남기고, 멤버 계정은 관리 루트가 서명한 CreateAccount 로만 태어나게 한다. 그리고 그 문 앞에서, 이 시리즈에서 처음으로 “인증은 됐는데 권한이 없다” 는 대답을 만난다.

서명되는 계정 생성

admin create-account(서명 없음)를 은퇴시키고, 새 서명 서비스에 CreateAccount를 둔다. 요청을 받아 처음 하는 일은 지금까지의 모든 서비스와 똑같다 — 서명 검증.

1
2
3
4
5
cred, authErr := service.Authenticate(r, body, store, serviceName) // serviceName = "mincloud"
if authErr != nil {
    writeError(w, authErr.Status, authErr.Code, authErr.Message)
    return
}

서명이 통과하면 우리는 호출자가 누구인지 안다. 4편의 그 골격이 여기서도 공짜로 돌아간다. 하지만 이번엔 그것만으로 부족하다.

인증 다음에 오는 질문

지금까지 mincloud는 “넌 누구냐"에 답하면 뭐든 통과시켰다. CreateAccount는 다르다. 누구냐를 알아낸 뒤, 한 걸음 더 나가 묻는다 — 넌 이걸 해도 되냐?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func createAccount(w http.ResponseWriter, store credstore.Store, caller credstore.Credential) {
    if !isManagementRoot(caller.Identity) {
        writeError(w, http.StatusForbidden, "AccessDenied", fmt.Sprintf(
            "User: %s is not authorized to perform: mincloud:CreateAccount", caller.Identity.ARN))
        return
    }
    // ... 멤버 계정 + 루트 발급 ...
}

func isManagementRoot(id credstore.Identity) bool {
    return id.Account == managementAccountID && strings.HasSuffix(id.ARN, ":root")
}

이 열 줄이 이 시리즈의 새로운 축이다. Authenticate인증(authentication) — “이 서명이 이 신원의 것이 맞나”. isManagementRoot인가(authorization) — “이 신원이 이 행동을 해도 되나”. 둘은 다르고, 여기서 처음으로 갈라진다.

aws CLI가 모르는 명령 가르치기

문제가 하나 있다. mincloud:CreateAccount는 AWS에 없는 우리만의 오퍼레이션이다 (게다가 진짜 AWS의 organizations:CreateAccount와 달리 루트 자격증명을 돌려준다 — 학생한테 넘겨야 하니까). aws CLI가 이 명령을 알 리 없다.

그런데 aws CLI는 하드코딩된 명령이 없다. 전부 JSON 서비스 모델에서 런타임에 생성된다. 그래서 우리 모델을 하나 만들어 설치하면 된다.

$ aws configure add-model \
    --service-model file://models/mincloud/service-2.json \
    --service-name mincloud

$ aws mincloud help | grep create-account
  o create-account

이제 진짜 aws mincloud create-account가 생겼다. 우리가 짠 서버에, 마스터 키로 SigV4 서명해서 요청을 보내는 일급 CLI 명령이다.

문 앞에서

관리 루트(마스터)로 문을 두드리면, 새 계정이 나온다.

$ aws mincloud create-account --endpoint-url http://localhost:9930
{
    "Account": {
        "AccountId": "955722713203",
        "RootAccessKeyId": "AKIAN5BMDJBZ3I5VMDXG",
        "RootSecretAccessKey": "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"
    }
}

(가짜 값이다.) 이 계정의 루트 자격증명을 그대로 손에 쥐었다. 이제 바로 그 멤버 루트로 같은 문을 두드려 보자 — 저도 루트니까 되지 않을까?

$ AWS_ACCESS_KEY_ID=AKIAN5BMDJBZ3I5VMDXG \
  AWS_SECRET_ACCESS_KEY=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 \
  aws mincloud create-account --endpoint-url http://localhost:9930

An error occurred (AccessDenied) when calling the CreateAccount
operation: User: arn:aws:iam::955722713203:root is not authorized
to perform: mincloud:CreateAccount

AccessDenied. AWS를 배우다 보면 누구나 한 번쯤 노려봤을 그 문장이, 이제 우리 서버에서 나온다. 중요한 건, 이게 서명이 틀려서가 아니라는 것이다. 같은 멤버 루트로 STS에 물어보면, 신원은 멀쩡히 돌아온다.

$ ...(같은 멤버 키로)... aws sts get-caller-identity --endpoint-url http://localhost:9900
{
    "UserId": "955722713203",
    "Account": "955722713203",
    "Arn": "arn:aws:iam::955722713203:root"
}

인증은 통과했다. 서버는 이 요청이 955722713203의 루트임을 확실히 안다. 다만 그 루트가 mincloud:CreateAccount해도 되는 사람이 아닐 뿐이다. 문지기가 신분증은 확인했고(인증), 그다음 “당신은 이 방에 들어올 명단에 없습니다”(인가)라고 한 것이다.

아직은 한 줄짜리 정책

정직하게 말하면, 지금의 인가는 정책 엔진이 아니다. isManagementRoot 한 줄, “관리 계정의 루트인가?“라는 단일 하드코딩 규칙이다. 진짜 AWS는 이걸 정책으로 한다 — 누가, 어떤 행동을, 어떤 리소스에, 허용/거부인지를 데이터로 기술하고 평가한다(IAM 정책, SCP). “friend01은 인스턴스를 띄울 순 있지만 끌 순 없다” 같은 규칙을 코드 수정 없이 붙이고 떼는 게 그 세계다.

또 하나. 우리 CreateAccount는 새 계정의 루트 키를 그대로 돌려준다. 편하지만, AWS가 실제로 하는 안전한 방식은 아니다. 진짜 AWS는 루트 키를 넘기지 않는다 — 멤버 계정에 역할을 심어두고, 관리 계정이 AssumeRole로 임시 자격증명을 받아 들어간다. 장기 루트 키를 손에 쥐는 대신, 필요할 때 짧게 빌려 쓰고 버리는 것이다.

그래서 앞으로 두 갈래가 열린다. 하나는 이 한 줄짜리 게이트를 진짜 정책 엔진으로 키우는 길(인가), 다른 하나는 루트 키를 넘기는 대신 임시 자격증명(AssumeRole) 으로 가는 길(더 안전한 접근). 둘 다, 오늘 문 앞에서 처음 마주친 이 AccessDenied에서 시작된다.