2편에서 서명 검증을 실제 AWS CLI가 만든 서명과 바이트 단위로 맞췄다. 이제 서버는 “이 요청을 보낸 사람이 시크릿을 아는 본인"이라고 판정할 수 있다. 하지만 판정만으로는 아무 일도 일어나지 않는다. 액세스키 ID로 시크릿을 어디서 찾을지, 통과한 요청에 무엇을 돌려줄지, 틀린 요청은 어떻게 거절할지가 아직 비어 있다.
이번 편은 그 세 조각을 붙여서, aws sts get-caller-identity가 우리 서버에 신원을 묻고 답을 받아 가는 왕복을 완성한다.
액세스키 ID에서 시크릿으로 — credstore
서명을 재현하려면 시크릿이 있어야 한다. 요청은 시크릿을 담아 오지 않는다(1편). 담아 오는 건 액세스키 ID뿐이다. 그러니 서버 어딘가에 액세스키 ID → 시크릿 대응표가 있어야 한다. 그게 credstore다.
| |
핵심은 액세스키 ID가 시크릿뿐 아니라 신원(Identity)까지 함께 가리킨다는 점이다. 서명이 맞았다는 건 “이 액세스키 ID의 주인"임이 증명됐다는 뜻이고, 그 주인이 누구인지(어느 계정의 어느 사용자인지)가 곧 get-caller-identity가 되돌려줄 답이다. 그래서 저장소의 값 하나에 시크릿과 신원이 같이 묶여 있다.
지금은 개발용 자격증명 하나를 서버 기동 시 심어 둔다. 전부 잘 알려진 가짜 값이다.
| |
인증 순서 — 어디서 거절하고, 무엇을 돌려주나
요청이 들어오면 검증은 정해진 순서로 진행된다. 각 단계에서 실패하면 진짜 AWS와 같은 에러 코드로 즉시 거절한다. 이 코드들이 중요하다. CLI와 SDK는 이 코드를 보고 “자격증명이 잘못됐다"인지 “서명이 틀렸다"인지 스스로 판단하고 사용자에게 안내하기 때문이다. 코드가 다르면 클라이언트 입장에선 우리가 진짜 AWS와 다르게 행동하는 것이 된다.
| |
두 갈래를 구분하는 게 핵심이다.
- 없는 액세스키 →
InvalidClientTokenId. 저장소에 그 키가 아예 없다. 시크릿을 찾을 수조차 없으니 서명을 재현할 대상이 없다. “네가 댄 신분증이 우리 명부에 없다"에 해당한다. - 틀린 시크릿 →
SignatureDoesNotMatch. 키는 명부에 있는데, 그 키의 진짜 시크릿으로 재현한 서명이 요청에 담긴 서명과 다르다. “명부엔 있는데, 네가 낸 답이 틀렸다"에 해당한다.
이 둘을 뭉뚱그려 하나로 거절하면 편하지만, 진짜 AWS는 이렇게 나눈다. 우리도 나눈다. 그래야 CLI가 두 상황을 다르게 안내한다.
통과했다면 — CLI가 알아듣는 XML
인증을 통과하면 본문의 Action을 읽어 무슨 작업인지 정한다. GetCallerIdentity면 credstore에서 딸려 온 신원을 XML로 직렬화해 돌려준다.
| |
1편 끝에서 CLI가 invalid XML received로 토라졌던 걸 기억할 것이다. 그때 우리는 not implemented라는 평문을 돌려줬다. STS의 Query protocol은 응답도 정해진 XML 형태를 기대한다. 루트 엘리먼트 이름(GetCallerIdentityResponse), 네임스페이스, Arn/UserId/Account의 태그명 — 이게 CLI가 파싱해서 필드로 꺼내 쓸 계약이다. 태그명 하나만 어긋나도 CLI는 값을 못 찾는다.
속아 넘어가는 순간
이제 붙여서 돌려 본다. 서버를 띄우고, 1편에서 쓴 그 명령을 다시 친다. 이번엔 --endpoint-url이 진짜 mincloud를 가리킨다.
AWS_ACCESS_KEY_ID=MINCLOUDTESTKEY0000A \
AWS_SECRET_ACCESS_KEY=mincloud-test-secret-not-real \
aws sts get-caller-identity \
--endpoint-url http://localhost:9900 \
--region ap-northeast-2
CLI는 이렇게 답한다.
| |
이 출력이 이번 시리즈의 클라이맥스다. CLI는 자기가 진짜 AWS와 이야기하는지, 로컬호스트에 떠 있는 Go 프로그램 몇백 줄과 이야기하는지 전혀 구분하지 못한다. 요청을 서명해 보냈고, 기대한 형태의 XML을 받아 파싱했고, 신원을 꺼내 사용자에게 보여줬다. CLI 입장에서 이건 그냥 성공한 AWS 호출이다.
참고로 CLI는 XML을 받아 위처럼 JSON으로 바꿔 보여준다. 우리가 돌려준 건 XML이고, CLI가 그걸 파싱해 자기 출력 형식으로 옮긴 것이다. 서버 로그에는 sts GetCallerIdentity by arn:aws:iam::123456789012:user/jeff 한 줄이 찍혀, 인증을 통과한 신원으로 응답이 나갔음을 보여준다.
두 번째 증인 — curl --aws-sigv4
CLI 하나가 속았다고 “AWS와 호환된다"고 말하긴 이르다. AWS CLI에만 있는 버릇에 우연히 맞춘 것일 수도 있다. 그래서 완전히 독립적인 두 번째 클라이언트로 같은 걸 시켜 본다. curl에는 SigV4 서명을 스스로 붙이는 --aws-sigv4 옵션이 있다.
curl -s http://localhost:9900/ \
--aws-sigv4 "aws:amz:ap-northeast-2:sts" \
--user "MINCLOUDTESTKEY0000A:mincloud-test-secret-not-real" \
-H "X-Amz-Date: $(date -u +%Y%m%dT%H%M%SZ)" \
-H "Content-Type: application/x-www-form-urlencoded; charset=utf-8" \
-d "Action=GetCallerIdentity&Version=2011-06-15"
curl은 AWS CLI와 코드 한 줄 공유하지 않는 별개 구현이다. 그 curl이 붙인 서명도 우리 검증기를 그대로 통과하고, 우리 XML을 받아 온다.
| |
서로 무관한 두 클라이언트가 같은 규칙으로 서명하고, 우리 서버가 둘 다 통과시킨다는 것 — 이게 “호환된다"의 실질이다. 우리가 AWS CLI의 특정 동작을 흉내 낸 게 아니라, SigV4라는 공개된 규칙 자체를 맞게 구현했다는 증거다.
틀린 요청은 진짜처럼 거절한다
속이는 데 성공했으면, 반대로 속지 않는 것도 진짜 AWS처럼 해야 한다. 시크릿을 일부러 틀리게 넣어 본다.
AWS_SECRET_ACCESS_KEY=this-is-the-wrong-secret \
aws sts get-caller-identity --endpoint-url http://localhost:9900 --region ap-northeast-2
An error occurred (SignatureDoesNotMatch) when calling the GetCallerIdentity
operation: The request signature we calculated does not match the signature
you provided. Check your AWS Secret Access Key and signing method.
액세스키 ID는 명부에 있으니 시크릿을 찾아 서명을 재현했지만, 틀린 시크릿에서 파생한 서명은 요청의 서명과 다르다 → SignatureDoesNotMatch. 명부에 없는 키를 대면 다른 코드가 나온다.
AWS_ACCESS_KEY_ID=AKIADOESNOTEXIST0000 \
AWS_SECRET_ACCESS_KEY=whatever \
aws sts get-caller-identity --endpoint-url http://localhost:9900 --region ap-northeast-2
An error occurred (InvalidClientTokenId) when calling the GetCallerIdentity
operation: The security token included in the request is invalid.
두 에러 문구는 진짜 AWS가 같은 상황에서 내는 것과 같다. 통과도 AWS처럼, 거절도 AWS처럼 — 그래야 CLI가 우리를 진짜와 구분하지 못한다.
다음 편 — 서비스를 나눈다
지금은 서버 하나가 STS 요청을 다 처리한다. 그런데 진짜 AWS의 구조는 그렇지 않다. 신원과 자격증명을 관리하는 IAM은 계정 전체에 하나뿐인 글로벌 서비스이고, 토큰을 발급하는 STS는 리전별로 각자 엔드포인트가 있다. 2편에서 서명 범위(scope)에 리전과 서비스 이름이 박혀 있던 걸 떠올려 보면 — 서명은 이미 “어느 서비스, 어느 리전으로 가는 요청인가"를 알고 있었다.
다음 편에서는 이 경계를 실제로 그린다. 하나의 서버를 IAM과 STS로 쪼개고, 한 서비스로 서명된 요청을 다른 서비스가 거절하게 만든다. 그리고 IAM으로 새 액세스키를 발급받아, 그 키로 STS에 신원을 물어보는 — 자격증명이 스스로 자격증명을 낳는 왕복을 만든다.