5편까지 mincloud엔 계정이 딱 하나였다. 부팅할 때 심는 개발용 씨앗 자격증명. 그걸로 STS도 부르고 IAM 키도 발급했다. 그런데 그 씨앗을 안 쓰고 맨손으로 첫 키를 만들어 보려 하면, 벽에 부딪힌다.
$ aws iam create-access-key --user-name friend01 \
--endpoint-url http://localhost:9910
Unable to locate credentials.
당연하다. create-access-key는 IAM 요청이고, IAM 요청은 SigV4로 서명해야 하고, 서명하려면 이미 자격증명이 있어야 한다. 키를 만들려면 키가 있어야 한다. 그럼 애초에 최초의 하나는 어떻게 존재하는가?
서명할 수 없는 문
답부터 말하면, 최초의 계정은 우리 API로 만들 수 없다. 만들면 안 된다.
CreateAccount 같은 걸 SigV4 API에 넣었다고 하자. 그 요청에 서명하려면 키가 필요하고, 그 키는 계정이 있어야 나오고, 계정은… 순환이다. 그래서 AWS도 가입(sign-up)을 SigV4 API로 만들지 않았다. 가입은 완전히 다른 평면이다.
- 서비스 평면 — sts, iam, ec2. SigV4로 인증한다.
awsCLI가 여기랑 대화한다. - 가입/콘솔 평면 — 인증이 아니라 다른 신뢰(이메일 확인·결제)로 계정과 루트를 계정 밖에서 찍어낸다. 여기엔 액세스키가 필요 없다 — 그걸 처음 만드는 곳이니까.
즉 부트스트랩은 서명의 바깥에 있어야 한다. mincloud도 똑같이 둘로 나눈다. 서비스 평면은 그대로 두고, 계정을 찍어내는 일은 서명하지 않는 별도의 문에 둔다. 그 문의 신뢰는 서명이 아니라 “이 배포에 손을 댈 수 있는가"다.
| |
지금까지 쓰던 씨앗 자격증명은, 사실 “이미 가입돼 있는 계정 하나를 미리 박아둔 것"이었다. 이제 그 문을 진짜로 연다.
계정을 찍어내다
admin create-account는 새 계정을 만든다. 계정 ID, 루트 유저, 그리고 그 계정만의 루트 액세스키/시크릿을 발급해 credstore(5편의 DynamoDB)에 저장하고, 운영자에게 건넨다.
$ mincloud admin create-account
account created — hand the root credential to the account owner:
Account: 340106438124
Root AccessKeyId: AKIAE6L7MMXWPV4XWCNQ
Root SecretKey: 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
(여기 값은 로컬에서 방금 만든 가짜다.) 이 문은 배포 안에서만 연다. 서명이 없으니 아무 데서나 두드리게 두면 안 되고, “박스에 접근할 수 있는 운영자” 가 신뢰의 경계다. 이게 AWS 콘솔 가입의 mincloud판이다 — 계정 밖 신뢰로 루트를 찍어내는 문.
받은 루트키로 STS에 물으면, 신원이 돌아온다.
$ aws sts get-caller-identity --endpoint-url http://localhost:9900
{
"UserId": "340106438124",
"Account": "340106438124",
"Arn": "arn:aws:iam::340106438124:root"
}
Arn이 :root로 끝나고, UserId가 계정 ID와 똑같다. 이게 루트의 표식이다. (뒤에서 IAM 유저와 대조한다.)
12자리는 어디서 오나
여기서 멈칫할 수 있다. 저 340106438124라는 12자리는 그냥 랜덤으로 뽑은 숫자인가? 그러면 계정 ID와 액세스키가 아무 관계 없는 두 난수가 된다. 그런데 진짜 AWS는 그렇게 안 한다.
잘 알려진 사실인데(Tal Be’ery, Aidan Steele의 연구), AWS 계정 ID는 액세스키 ID 안에 인코딩돼 있다. AKIA… 키만 있으면 API 호출 없이 오프라인으로 계정을 복원할 수 있다.
| |
mincloud도 이 방식을 그대로 따랐다. create-account가 12자리를 정하면, 발급하는 액세스키가 그 계정을 base32 본문에 품도록 만든다. 그래서 우리가 만든 키를 진짜 공개 디코더에 넣어도 우리 계정이 나온다.
$ mincloud admin whoami AKIAE6L7MMXWPV4XWCNQ
AKIAE6L7MMXWPV4XWCNQ -> account 340106438124
$ python3 decode.py AKIAE6L7MMXWPV4XWCNQ
340106438124
계정이 “떠다니는 난수"가 아니라 키가 지니고 다니는 값이 된 것이다. 눈으로도 보인다. 같은 계정에서 발급한 두 키는 앞부분을 공유한다.
AKIAE6L7MMXW PV4XWCNQ ← 루트
AKIAE6L7MMXW D6RJRYGX ← 같은 계정의 다른 키
──────────── ────────
계정 인코딩 키별 랜덤
앞 열두 글자는 계정에서 파생돼 똑같고, 뒤는 키마다 다른 랜덤이다.
루트가 유저를 만든다
이제 계정이 여럿이 될 수 있으니, 격리가 진짜 문제가 된다. 방금 만든 루트로 자기 계정 안에 IAM 유저 alice를 발급해 보자.
$ aws iam create-access-key --user-name alice --endpoint-url http://localhost:9910
...AccessKeyId: AKIAE6L7MMXWD6RJRYGX...
$ aws sts get-caller-identity --endpoint-url http://localhost:9900 # alice 키로
{
"UserId": "AIDAY7WVA4T7QWXELDV5",
"Account": "340106438124",
"Arn": "arn:aws:iam::340106438124:user/alice"
}
alice는 이 루트가 속한 계정(340106438124) 안의 유저다. 루트가 만든 키가 다른 계정으로 새지 않는다 — 자격증명마다 자기 계정을 품고 다니기 때문이다. alice의 키를 오프라인 디코드해도 같은 340106438124가 나온다.
그리고 루트와 IAM 유저는 겉모습으로 구별된다.
| 정체 | ARN 끝 | UserId |
|---|---|---|
| 루트 | :root | 계정 ID와 동일 |
| IAM 유저 | :user/<이름> | AIDA… 로 시작 |
get-caller-identity가 돌려주는 이 한 줄이, “너가 루트냐 유저냐"를 그대로 말해 준다. 이 구분은 뒤에서 인가(authorization)를 다룰 때 핵심 열쇠가 된다 — 루트는 암묵적으로 다 되고, :user/…는 정책이 있어야 뭔가 할 수 있다.
두 평면, 그리고 남은 빚
정리하면, mincloud는 이제 두 개의 문을 가진다. 서명하는 서비스 평면과, 서명하지 않는 부트스트랩 평면. 후자는 서명이 없어야 한다는 게 핵심이다 — 서명을 붙이는 순간 최초의 계정을 만들 길이 다시 막힌다.
이건 은근히 미끄러운 규칙이다. AWS CLI는 커스텀 모델(aws configure add-model)로 새 명령을 붙일 수 있어서, 마음만 먹으면 aws mincloud create-account 같은 걸 만들 수도 있다. 하지만 이 문만은 거기 두면 안 된다. CLI 명령이 되는 순간 SigV4로 서명되고, chicken-and-egg가 부활한다. 부트스트랩은 끝까지 서명 바깥에 있어야 한다.
정직하게 남는 빚도 있다. 씨앗 계정은 여전히 하드코딩된 특별한 존재고, create-account는 지금 누구 손을 거쳐야 하는지(승인·쿼터)를 따지지 않는다. “박스에 접근하면 계정을 무한정 찍는다"는 건 데모니까 넘어가는 것이지, 진짜 가입 플로우는 그 앞에 결제와 검증이 있다.
그런데 여기까지 오는 동안, 클라이언트는 늘 aws CLI가 명령 하나를 던지는 식이었다. 다음 편에서는 훨씬 까다로운 손님을 부른다 — Terraform. plan → apply → destroy를 돌리는 이 손님은, 명령 하나로는 만족하지 않는다. mincloud의 반쪽짜리 IAM이 진짜 CRUD가 되어야 하는 순간이 거기서 온다.