Cloud Modernization 25
IAM 정책 JSON 해부학: 권한 판단의 비밀을 읽다
정책 JSON의 Version, Effect, Action, Resource, Condition이 문법처럼만 보여 실제 권한 판단 구조가 잡히지 않는다.
근거 · 교안 p106-p115
1장: 정책 JSON, 그 안에 숨겨진 권한의 ‘무엇을’: Effect와 Action
솔라의 손가락이 노트북 화면을 가리켰지만, 시선은 허공에 머물러 있었다. 화면에는 괄호와 따옴표로 가득 찬 텍스트, IAM 정책 JSON이 떠 있었다. 한참을 들여다보던 솔라는 작게 한숨을 쉬었다.
“언니, 이거 좀 봐. Version, Statement, Effect, Action, Resource… 분명 다 아는 단어인데, 합쳐 놓으니까 무슨 암호문 같아. 그냥 문법 규칙을 나열한 것 같달까? 그래서 이걸로 뭘 어떻게 하라는 건지 도무지 감이 안 와.”
솔라의 말에는 단순한 지식의 부재가 아닌, 눈앞의 정보가 의미 있는 구조로 잡히지 않는 데서 오는 답답함이 묻어났다.
옆에서 조용히 자신의 노트북을 보던 루나가 고개를 돌렸다. 솔라의 화면을 잠시 들여다보던 루나는 마우스를 잡아 솔라에게 건넸다.
“그럼 다른 건 다 지워버린다고 생각해 보자. 딱 두 개만 남겨보는 거야. 거기서 Effect랑 Action 부분만 선택해서 복사해 볼래?”
솔라는 고개를 갸웃거리면서도 루나가 시키는 대로 했다. 텍스트 편집기를 열어 복잡한 JSON 구조에서 Effect와 Action 두 줄만 떼어내 붙여넣었다.
"Effect": "Allow",
"Action": "s3:GetObject"
덩그러니 남은 두 줄은 아까보다 훨씬 단출했다. 하지만 솔라의 표정은 여전히 물음표였다. “이렇게 떼어놓고 보니까 더 모르겠는데? 그냥 ‘허용’이랑 ‘s3 객체 가져오기’잖아. 이게 다야?”
“응, 그게 다야. 한번 그대로 읽어봐. 두 단어를 붙여서.”
루나의 말에 솔라는 마지못해 입을 열었다. “‘허용한다… s3 객체 가져오기를.’”
문장을 내뱉는 순간, 솔라의 눈빛이 미세하게 달라졌다. 단어의 나열이 아니라 하나의 완결된 의미를 가진 지시문처럼 들렸기 때문이다.
“어?”
솔라가 다시 중얼거렸다. 이번에는 좀 더 확신에 찬 목소리였다.
“s3:GetObject라는 ‘행위’를 Allow, ‘허용한다’는 뜻이네. 그럼 만약에 Effect가 Deny였다면, ‘s3 객체 가져오기’를 ‘거부한다’는 거고.”
솔라는 원래의 JSON 정책으로 돌아가 Effect와 Action을 다시 찾아봤다. 아까는 그저 무의미한 키워드 덩어리처럼 보였던 것들이, 이제는 선명한 ‘행위-결정’의 쌍으로 보이기 시작했다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:ListBucket",
"s3:GetObject"
],
"Resource": "arn:aws:s3:::example-bucket"
},
{
"Effect": "Deny",
"Action": "s3:DeleteObject",
"Resource": "arn:aws:s3:::example-bucket"
}
]
}
“아! 알겠다. 이 정책은 두 개의 문장으로 되어 있는 거구나. 첫 번째 문장은 ‘example-bucket’이라는 곳의 목록을 보고(ListBucket), 객체를 가져오는(GetObject) 행위를 ‘허용한다’는 거고. 두 번째 문장은 ‘example-bucket’에서 객체를 삭제하는(DeleteObject) 행위는 ‘거부한다’는 거네.”
솔라는 방금 자신이 발견한 규칙에 신이 난 듯 말을 이었다.
“그러니까 Effect는 ‘허용’할지 ‘거부’할지, 그 결과, 즉 ‘결론’을 내리는 역할이고, Action은 그 결론이 적용될 구체적인 ‘행위’의 목록인 거야. 이 둘이 짝을 이뤄야 비로소 ‘무엇을 어떻게 할지’에 대한 하나의 완전한 지시가 만들어지는 거였어. 그냥 문법이 아니라, 권한의 가장 핵심적인 내용, ‘무엇을’ 할 수 있는지를 정의하는 거였네.”
마치 암호 해독의 첫 열쇠를 찾아낸 탐정처럼 솔라의 얼굴에 자신감이 어렸다. 복잡하게만 보였던 JSON 구조의 한 축이 명확하게 잡혔다.
하지만 그 기쁨은 길지 않았다. 솔라는 다시 화면을 가리켰다. 여전히 풀리지 않은 부분이 남아 있었다.
“좋아, ‘무엇을’ 허용하고 거부하는지는 이제 알겠어. 그런데… s3:GetObject을 허용한다고? 그래서 어디에 있는 S3 객체를 가져온다는 거야? 세상의 모든 S3? 아니면 특정 폴더? Resource에 적힌 저 arn:aws:s3:::example-bucket 이게 그걸 알려주는 것 같긴 한데…”
Effect와 Action이 ‘무엇을’에 대한 답이라는 걸 깨닫자, 자연스럽게 ‘어디에’라는 새로운 질문이 떠올랐다. 정책의 나머지 부분은 더 이상 무의미한 문법이 아니라, 풀어야 할 다음 수수께끼처럼 보였다.
2장: 정책 JSON, 권한의 ‘어디에’: Resource의 범위 해석
솔라가 알아낸 Effect와 Action의 관계를 되짚어보던 루나는, 말없이 S3 정책이 떠 있던 화면을 다른 예시로 바꾸었다. 이전의 S3 버킷 정책은 사라지고, EC2 인스턴스에 대한 새로운 정책 JSON이 화면을 채웠다. 두 정책은 비슷해 보였지만, 솔라의 눈길은 한 곳에 못 박혔다.
{
"Effect": "Allow",
"Action": [
"ec2:StartInstances",
"ec2:StopInstances"
],
"Resource": "arn:aws:ec2:*:*:instance/*"
}
솔라의 미간이 미세하게 찌푸려졌다. 이전 장에서 얻은 자신감으로 Effect와 Action을 먼저 해석했다. ‘EC2 인스턴스를 시작하고 중지하는 행위를 허용한다.’ 거기까지는 명확했다. 하지만 곧바로 다음 질문이 뒤따랐다. ‘어떤 EC2 인스턴스를?’
“Resource가 arn:aws:s3:::example-bucket일 때는 그냥 ‘example-bucket’이라는 특정 장소라고 생각하면 됐는데… 이건 좀 이상해.”
솔라가 화면의 Resource 값을 손가락으로 짚었다. 별표(*)가 여러 개 찍혀 있는 모습이 영 낯설었다.
“arn:aws:ec2:*:*:instance/*… 이건 무슨 뜻이야? 이름이 별표인 인스턴스? 아니면 그냥 세상의 모든 EC2 인스턴스를 뜻하는 건가? 이렇게 모호하게 권한을 줄 리는 없을 텐데. 최소 권한 원칙에 어긋나잖아.”
솔라는 특정 리소스의 고유한 이름(ARN)으로만 대상을 지정할 수 있다고 생각했다. 그래서 별표의 등장은 규칙의 예외처럼 느껴졌고, 권한의 범위를 어떻게 한정해야 할지 갈피를 잡을 수 없었다.
루나는 설명하는 대신, 화면 한쪽에 작은 텍스트 편집기 창을 열었다. 그리고는 아무 말 없이 몇 가지 다른 Resource ARN 예시를 타이핑하기 시작했다.
A. "arn:aws:ec2:ap-northeast-2:123456789012:instance/i-123abc456def"
B. "arn:aws:ec2:ap-northeast-2:123456789012:instance/*"
C. "arn:aws:ec2:*:123456789012:instance/*"
D. "arn:aws:ec2:*:*:instance/*"
“자, 솔라. 이 네 개의 주소가 각각 어떤 대상을 가리키는 문이라고 생각해 봐. A 문은 어느 방으로 통할까?”
솔라는 A를 보았다. 별표 없이 모든 정보가 명확하게 적혀 있었다. “이건 쉽네. ‘i-123abc456def’라는 ID를 가진, 특정 계정(123456789012)의 서울 리전(ap-northeast-2)에 있는 딱 하나의 EC2 인스턴스. 아주 구체적인 방문이야.”
“좋아. 그럼 B 문은?”
“음… 마지막에 별표가 있네. instance/까지는 똑같으니까… 아마도 그 특정 계정의 서울 리전에 있는 ‘모든’ 인스턴스를 말하는 거겠지? 하나의 방이 아니라, 그 층에 있는 모든 방의 열쇠 같은 느낌?”
솔라는 방금 자신이 한 말을 되새겨 보았다. 별표가 ‘모든 것’을 의미하는 일종의 만능 키(wildcard)처럼 작동한다는 것을 스스로 깨달은 것이다.
“그럼 C는 어때?” 루나가 물었다.
“리전 자리에 별표가 있네. 그럼… 계정 번호는 정해져 있으니, 그 계정이 소유한 ‘모든 리전’에 있는 모든 인스턴스? 서울이든, 도쿄든, 버지니아든 상관없이 말이야.”
솔라의 목소리에 점점 확신이 붙었다. 루나가 제시한 예시들을 비교 분석하며, 그녀는 Resource 필드가 단순한 이름표가 아님을 직감했다. 그것은 권한이 적용될 대상을 선별하는 ‘필터’나 ‘패턴’에 가까웠다.
마지막으로 솔라는 처음에 그녀를 혼란스럽게 했던 D를 보았다. 이제 그것은 더 이상 이상한 이름으로 보이지 않았다.
“알겠다! 이건 리전과 계정 ID 자리에 모두 별표가 있으니까… 어떤 계정이든, 어떤 리전이든 상관없이 ‘모든 EC2 인스턴스’에 대한 권한이구나! 내가 처음 생각했던 게 맞긴 한데, 그게 ‘모호한’ 게 아니라 ‘광범위한 범위’를 의도적으로 지정하는 방식이었어.”
솔라는 다시 원래의 정책으로 시선을 돌렸다. 이제 Resource 필드는 더 이상 암호가 아니었다. 그것은 권한이 발휘될 영토의 경계선을 명확히 그려주는 지도였다.
Effect와 Action이 ‘무엇을’ 할지에 대한 답이라면, Resource는 그 행위가 ‘어디에’ 영향을 미치는지 정의하는 역할. 이로써 권한 판단의 두 번째 축이 제자리를 찾았다.
자신감을 회복한 솔라는 다른 정책 예시를 찾아보기 시작했다. 하지만 이내 새로운 벽에 부딪혔다. 그녀가 찾은 한 정책에는 이전에 보지 못했던 Condition이라는 요소가 포함되어 있었다.
“언니, 이건 또 뭐야? Effect, Action, Resource는 이제 알겠는데… 이 Condition은 왜 있는 거지? ‘ec2:ResourceTag/Owner’ 값이 사용자 이름과 같을 때만 허용한다니… 그럼 이 조건이 안 맞으면 Effect가 Allow라도 소용없다는 뜻인가?”
권한의 ‘무엇을’과 ‘어디에’를 이해하자, 이제 ‘어떤 조건으로’라는 더 까다로운 질문이 모습을 드러냈다.
3장: 정책 JSON, 권한의 ‘어떤 조건으로’: Condition의 유효성 판단
솔라의 질문이 채 공기 중에서 흩어지기도 전에, 루나는 이미 노트북 화면에 새로운 판을 짜고 있었다. 방금 전까지 함께 보던 정책 JSON 옆으로, 루나는 두 개의 작은 텍스트 창을 나란히 더 띄웠다. 세 개의 창에는 거의 동일해 보이는 정책이 들어 있었지만, 자세히 보니 미묘하게 달랐다.
첫 번째 창(A)에는 아주 기본적인 EC2 시작 권한 정책이 있었다.
// 정책 A
{
"Effect": "Allow",
"Action": "ec2:StartInstances",
"Resource": "arn:aws:ec2:*:*:instance/i-012345abcdef"
}
두 번째 창(B)과 세 번째 창(C)은 A 정책을 기반으로 하지만, 각각 Condition이라는 블록을 추가로 포함하고 있었다. 솔라가 아까 보고 혼란스러워했던 바로 그 요소였다.
“네 질문, ‘조건이 안 맞으면 Allow라도 소용없냐’는 거. 직접 확인해 보자.”
루나는 마치 보드게임의 말을 놓듯, 상황을 설정했다. “여기 세 개의 다른 규칙(정책)이 있어. 그리고 ‘솔라’라는 플레이어가 ‘i-012345abcdef’ 인스턴스를 시작하려고 해. 규칙 A를 적용하면, 결과는 어떻게 될까?”
솔라는 정책 A를 훑어보았다. 이제 Effect와 Action, Resource의 역할은 명확했다. “당연히 ‘허용’이지. ‘i-012345abcdef’ 인스턴스를 시작하는 걸 허용한다고 명시되어 있으니까.”
“맞아.” 루나는 고개를 끄덕이며 마우스를 두 번째 창(B)으로 옮겼다.
// 정책 B
{
"Effect": "Allow",
"Action": "ec2:StartInstances",
"Resource": "arn:aws:ec2:*:*:instance/i-012345abcdef",
"Condition": {
"StringEquals": {
"ec2:ResourceTag/Owner": "${aws:username}"
}
}
}
“그럼 규칙 B를 적용해 보자. 상황을 두 개로 나눠볼게. 첫 번째 상황, 목표 인스턴스에 ‘Owner’라는 태그가 있고, 그 값이 ‘sola’야. 결과는?”
솔라는 Condition 블록을 유심히 들여다봤다. ec2:ResourceTag/Owner는 인스턴스의 ‘Owner’ 태그를 의미하는 것 같았고, ${aws:username}은 요청을 보낸 사용자, 즉 ‘sola’를 가리키는 변수일 터였다. “조건이 ‘인스턴스의 Owner 태그 값이 사용자 이름과 같아야 한다’는 거네. 지금 Owner 태그가 ‘sola’고, 사용자도 ‘sola’니까… 조건이 참이네. 그러니 허용되겠지.”
“좋아. 그럼 두 번째 상황.” 루나의 목소리는 평온했지만, 질문은 날카로웠다. “인스턴스의 ‘Owner’ 태그 값이 ‘luna’라면?”
순간 솔라의 머릿속이 복잡해졌다. Effect는 분명 Allow였다. 하지만 Condition은 명백히 거짓이 된다. Owner 태그(luna)와 사용자 이름(sola)이 다르기 때문이다. 솔라는 자신이 처음 던졌던 질문과 정면으로 마주했다. ‘Allow’와 ‘Condition’, 둘 중 무엇이 이길까? 그녀는 Condition이 단순히 부가적인 정보가 아닐 것이라는 예감을 따르기로 했다.
“…거부. 아니, 정확히는 허용되지 않을 거야.” 솔라는 조심스럽게 단어를 골랐다. “정책의 Effect가 Allow라도, Condition이라는 관문을 통과하지 못했으니까. 이 정책 자체가 적용되지 않는 거나 마찬가지인 거지.”
Condition을 정책의 복잡성을 더하는 부차적 요소로 여겼던 처음의 생각은 완전히 뒤집혔다. 그것은 권한이 발동되기 위한 ‘필수 전제 조건’이었다. 이 조건이 충족되지 않으면, Allow라는 허가증은 아무런 힘을 발휘하지 못했다.
루나는 마지막 창(C)을 가리켰다.
// 정책 C
{
"Effect": "Allow",
"Action": "ec2:StartInstances",
"Resource": "arn:aws:ec2:*:*:instance/i-012345abcdef",
"Condition": {
"IpAddress": {
"aws:SourceIp": "192.168.1.10/32"
}
}
}
“마지막 규칙. 솔라 네가 도서관에서 이 인스턴스를 시작하려고 해. 도서관의 IP 주소는 ‘203.0.113.5’야. 결과는?”
이제 솔라에게는 망설임이 없었다. “당연히 허용 안 돼. 정책은 ‘192.168.1.10’이라는 특정 IP 주소에서 온 요청만 허용한다고 조건을 걸었으니까. 내가 어디서 접속했는지, 그 ‘상황’이 조건에 맞지 않으면 Allow는 의미가 없어.”
솔라는 세 개의 정책을 나란히 보며 깨달음을 정리했다. Effect와 Action이 ‘무엇을’ 할지, Resource가 ‘어디에’ 할지를 정한다면, Condition은 바로 ‘어떤 조건으로’ 그 모든 것을 허용할지를 결정하는 최종 검문소였다. 최소 권한 원칙을 더욱 정교하게 다듬어주는 핵심 장치였던 것이다.
“알겠다. Condition은 그냥 장식이 아니었어. 권한의 유효성을 판단하는 결정적인 열쇠였네. 태그, IP 주소, 시간… 이런 구체적인 맥락에 따라 권한을 줬다 뺏었다 할 수 있는 스위치 같은 거였구나.”
권한 판단의 세 번째 축인 ‘어떤 조건으로’가 제자리를 찾자, 솔라는 비로소 IAM 정책의 전체 구조가 눈에 들어오는 듯했다.
하지만 그 순간, 새로운 의문이 고개를 들었다. 지금까지 분석한 정책 A, B, C는 모두 사용자에게 직접 연결해서使う 자격 증명 기반 정책(Identity-based Policy)의 예시였다. 문득 솔라는 이전에 스쳐 지나갔던 다른 종류의 정책, 즉 Principal이라는 항목이 있던 정책을 떠올렸다.
“언니, 그럼 이 정책들은 전부… 나한테 적용되는 거야? 아니면 우리 팀 전체? 지금까지 ‘누가’라는 주어는 당연히 ‘나’라고 생각하고 있었는데… 왜 이 정책들에는 ‘누가’에 해당하는 부분이 없지? 어떤 정책에는 Principal이라는 게 있던데, 그건 또 뭐야?”
‘무엇을’, ‘어디에’, ‘어떤 조건으로’를 이해하자, 권한의 가장 근본적인 질문, ‘누가’라는 주어의 부재가 선명하게 드러나기 시작했다.
4장: 정책 JSON, 권한의 ‘누가’: Principal과 정책 유형의 연결
솔라의 질문이 남긴 정적을 깬 것은 루나의 키보드 소리였다. 루나는 대답 대신, 솔라가 보고 있던 노트북 화면을 둘로 나누었다. 왼쪽과 오른쪽에 거의 비슷해 보이는 두 개의 정책 JSON 창이 나란히 떴다. 두 정책 모두 S3 버킷에 대한 접근을 다루고 있었지만, 결정적인 차이점이 있었다.
왼쪽 창의 정책(A)에는 Principal이라는 항목이 없었다. 우리가 지금까지 봐왔던 익숙한 형식이었다.
// 정책 A (자격 증명 기반)
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"
}]
}
하지만 오른쪽 창의 정책(B)은 달랐다. 맨 위에 Principal이라는, 낯선 항목이 떡하니 자리를 잡고 있었다.
// 정책 B (리소스 기반)
{
"Version": "2012-10-17",
"Statement": [{
"Principal": {"AWS": "444455556666"},
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"
}]
}
두 개의 정책을 번갈아 보던 솔라의 눈썹이 꿈틀거렸다. ‘무엇을’, ‘어디에’, ‘어떤 조건으로’에 대한 단서는 찾았지만, 정작 가장 기본적인 ‘누가’라는 주어가 빠져 있다는 사실을 깨달은 뒤끝이었다.
“이거 봐, 언니. 왼쪽 정책은 주어가 없어. 그냥 ‘DOC-EXAMPLE-BUCKET에서 객체를 가져오는 걸 허용한다’고만 되어 있어. 그래서 당연히 이 정책을 가진 ‘나’나 ‘우리 팀’한테 적용되는 거겠지, 하고 생각했거든.”
솔라는 말을 이었다. 그녀의 목소리에는 새로운 가설을 세우고 검증하려는 탐정의 열의가 실려 있었다.
“그런데 오른쪽 정책은 이상해. Principal이라는 게 있어서 ‘444455556666’라는 계정에게 허용한다고 ‘누구에게’를 콕 집어서 말하고 있어. 왜 어떤 정책은 주어를 숨기고, 어떤 정책은 이렇게 대놓고 드러내는 거야? 둘 다 그냥 사용자한테 붙여주면 되는 거 아니야?”
솔라의 생각은 ‘모든 정책은 모자처럼 사용자나 그룹에 씌워주는 것’이라는 모델에 갇혀 있었다. 그러니 정책 내부에 주어가 따로 명시된 B와 같은 경우가 예외적이고 비효율적인 것처럼 보였다.
루나는 두 정책을 가리키며 다른 비유를 꺼냈다.
“솔라, 정책을 규칙이 적힌 ‘문서’라고 생각해 봐. 그 문서가 어디에 놓이느냐에 따라 내용이 달라져. A 정책은 너의 ‘신분증’에 직접 인쇄된 규칙이야. 네가 어딜 가든 너를 따라다니지. 신분증에 ‘이 사람은 도서관 출입 가능’이라고 적혀 있다면, 그 규칙이 누구에게 적용되는지 굳이 또 쓸 필요가 있을까?”
“아니. 당연히 신분증 주인인 나한테 적용되는 거지.”
“바로 그거야. 그게 자격 증명 기반 정책(Identity-based Policy)이야. 사용자, 그룹, 역할 같은 ‘자격 증명(Identity)’에 붙이는 정책이지. 정책을 적용받는 주체가 이미 정해져 있으니, 정책 안에는 Principal이 필요 없는 거야. 정책의 주어는 그 정책을 가지고 있는 바로 그 사람이나 역할이니까.”
루나의 설명에 솔라의 눈이 동그래졌다. 그녀는 왼쪽의 정책 A를 다시 보았다. Principal이 없는 것이 더 이상 ‘빠진’ 것이 아니라, ‘불필요한’ 것이었음을 깨달았다.
“그럼… 오른쪽 정책 B는?”
“그건 ‘리소스’에 붙이는 규칙이야. 신분증이 아니라, 도서관 정문에 붙은 ‘출입 안내문’ 같은 거지.”
루나는 오른쪽 정책 B를 가리켰다.
“이 안내문은 도서관이라는 ‘리소스’ 자체에 붙어 있어. 그러니 안내문에는 ‘누가’ 들어올 수 있는지 명단을 반드시 적어야 해. ‘솔라 학생증 소지자만 출입 가능’ 이런 식으로. 여기서 Principal이 바로 그 ‘출입 명단’ 역할을 하는 거야. 특정 S3 버킷(리소스)에 ‘이 AWS 계정(Principal)만 파일을 올릴 수 있다’고 규칙을 붙여놓는 거지. 이게 리소스 기반 정책(Resource-based Policy)이야.”
순간, 모든 조각이 제자리를 찾았다. 솔라는 두 정책을 다시 보았다. 이제 그 둘은 더 이상 비슷해 보이지 않았다. 완전히 다른 목적과 장소에 사용되는 두 종류의 규칙으로 보였다.
“알겠다! A는 사용자에게 주는 권한 목록, ‘내가 할 수 있는 일’의 리스트고, B는 리소스에 붙여 놓는 허가 목록, ‘나에게 할 수 있는 일’의 리스트구나! 주체가 다르네. A의 주체는 정책을 가진 ‘나’고, B의 주체는 정책 안에 명시된 Principal이야.”
솔라는 이제 Principal 필드의 유무만 보고도 이 정책이 누구의 관점에서 쓰였는지, 어디에 붙어야 하는지를 단번에 파악할 수 있게 되었다. 정책의 종류(자격 증명/리소스)와 Principal의 관계를 이해하자, 권한의 네 번째 축인 ‘누가’가 선명하게 모습을 드러냈다.
Effect와 Action (무엇을), Resource (어디에), Condition (어떤 조건으로), 그리고 Principal/정책 유형 (누가). 흩어져 있던 암호 조각들이 마침내 하나의 의미 있는 문장을 완성했다.
하지만 완성된 문장은 또 다른 거대한 질문을 낳았다.
“좋아, 그럼 이제 각 요소가 무슨 뜻인지는 알겠어. 그런데… 만약 내 신분증(자격 증명 기반 정책)에는 ‘모든 버킷의 파일을 읽을 수 있다’고 되어 있는데, 어떤 특정 버킷의 출입 안내문(리소스 기반 정책)에는 ‘오직 루나 언니만 접근 가능’이라고 쓰여 있으면 어떻게 돼? 심지어 또 다른 정책에서는 ‘주말에는 모든 S3 접근 금지’라고 한다면? 대체 AWS는 이 모든 규칙들을 어떤 순서로 확인하고 최종 결정을 내리는 거지?”
개별 정책을 해부하는 데 성공하자, 이제 여러 정책이 충돌하고 협력하는 복잡한 권한 판단의 ‘전체 흐름’이 새로운 미스터리로 떠올랐다.
5장: IAM 권한 판단의 종합: 최종 흐름도 그리기
솔라의 질문이 남긴 여운은 새로운 질문으로 이어지지 않았다. 대신 루나는 노트북 화면에 복잡한 시나리오를 펼쳐 보였다. 화면 중앙에는 ‘솔라의 S3 객체 읽기 요청’이라는 제목이 붙어 있고, 그 아래로 세 개의 정책 조각이 카드처럼 나란히 놓여 있었다. 각 카드는 서로 다른 출처를 가지고 있었다.
- 카드 A (자격 증명 정책): 솔라의 IAM 사용자에게 직접 연결된 정책.
Effect: "Allow", Action: "s3:*", Resource: "*"라고 쓰여 있어, 마치 모든 것을 할 수 있는 만능 열쇠처럼 보였다. - 카드 B (리소스 정책): 목표 S3 버킷에 붙어 있는 정책.
Principal: {"AWS": "루나의 계정 ID"}, Effect: "Allow", ...라고 되어 있어, 특정인만 환영하는 출입문 같았다. - 카드 C (조직 정책): 상위 조직 레벨에서 내려온 규칙.
Effect: "Deny", Action: "s3:*", Condition: { "aws:CurrentTime > 18:00:00Z" }. 마치 저녁 6시 이후의 통행금지령처럼 보였다.
이전까지 개별 정책을 해부하는 데 집중했던 솔라는, 이렇게 여러 정책이 한데 얽힌 상황을 마주하자 잠시 숨을 골랐다. 마치 각기 다른 법 조항을 손에 쥔 변호사처럼, 어느 것을 먼저 적용해야 할지 혼란스러웠다.
“흠… 내 신분증(카드 A)은 뭐든지 할 수 있다고 하고, 버킷의 안내문(카드 B)은 언니만 들어오라고 하고, 조직의 규칙(카드 C)은 야간 통행을 금지하고 있네.”
솔라는 잠시 고민하더니, 가장 강력해 보이는 카드 A를 집어 들었다. “내 권한이 가장 중요하지 않을까? 내가 ‘모든 S3 작업 허용’ 권한을 가지고 있으니까, 다른 규칙에 상관없이 요청이 성공해야 할 것 같아.”
솔라의 판단은 ‘허용’이 ‘거부’보다 우선하거나, 자신에게 직접 부여된 권한이 더 강력할 것이라는 직관에 기반했다. 여러 규칙이 충돌할 때, AWS가 이들을 어떤 순서로 조합하고 판단하는지에 대한 명확한 그림이 없었기 때문이다.
루나는 솔라의 대답에 고개를 젓는 대신, 카드 C, 즉 통행금지령처럼 보였던 조직 정책을 가리켰다. 그리고는 그 카드 위로 아주 커다랗고 붉은 ‘거부’ 도장을 찍는 시늉을 했다.
“AWS의 권한 판단은 협상이 아니야, 솔라. 아주 엄격한 절차를 따르는 심사 과정이지. 그리고 그 과정에서 가장 먼저 찾는 건 ‘거부(Deny)’라는 명시적 지시야.”
루나는 솔라가 늘어놓은 세 개의 카드를 심사관이 서류를 검토하듯 순서대로 짚어 나갔다. “네가 요청을 보내는 지금 시각이 저녁 7시라고 해보자. AWS는 네 신분증(카드 A)의 ‘허용’을 보기 전에, 적용되는 모든 정책을 훑으면서 혹시 명시적인 ‘거부’가 있는지부터 확인해.”
루나의 손가락이 카드 C에서 멈췄다. “그리고 여기서 그걸 찾았지. ‘저녁 6시 이후 모든 S3 작업 거부’. 이 조건에 해당되니까, 이 정책은 너의 요청을 ‘거부’하라고 소리치고 있어. 이 순간, 다른 정책에 뭐라고 쓰여 있든 심사는 그걸로 끝나.”
“아…” 솔라의 입에서 짧은 탄성이 터져 나왔다. “내 만능 열쇠가 소용없어지는 거네. ‘허용’은 여러 개가 있어도 소용없고, 단 하나의 명시적인 ‘거부’가 모든 걸 덮어쓰는구나.”
가장 강력한 Allow가 이길 것이라 생각했던 솔라의 가설이 무너지는 순간이었다. 권한 판단 흐름의 가장 중요한 첫 번째 원칙, ‘명시적 거부는 항상 이긴다’가 선명하게 드러났다.
“그럼 만약 지금이 낮이라서 카드 C의 거부 조건에 해당하지 않는다면?” 솔라가 되물었다.
“좋은 질문이야. 거부가 없으면, 심사관은 그제야 ‘허용(Allow)’을 찾기 시작해. 네 신분증(카드 A)이 ‘허용’이라고 말하고 있지. 버킷의 안내문(카드 B)은 너를 허용 목록에 포함하고 있지 않지만, 다른 곳에서 이미 허가를 받았으니 괜찮아. 여러 허용 정책 중 하나라도 요청과 일치하면 돼. 그럼 최종 결과는 ‘허용’이 되는 거지.”
솔라는 비로소 흩어져 있던 퍼즐 조각들이 하나의 거대한 흐름도로 맞춰지는 것을 느꼈다.
- 먼저, 적용 가능한 모든 정책(자격 증명, 리소스, 조직 등)을 통틀어 명시적인
Deny가 있는지 확인한다. 있다면, 즉시거부하고 모든 과정이 종료된다. - 명시적인
Deny가 없다면, 그제야Allow를 찾는다. 적용 가능한 정책 중 단 하나라도 요청을 허용하는Allow문이 있다면, 요청은허용된다. - 만약 어디에도 명시적인
Deny가 없고, 어디에서도Allow를 찾을 수 없다면? 그때는 기본값인거부가 된다.
이것이 바로 Principal (누가), Action (무엇을), Resource (어디에), Condition (어떤 조건으로)이라는 개별 요소들이 모여 만들어내는 최종 권한 판단의 흐름이었다. 각 요소는 이 흐름 속에서 자신의 역할을 수행하며 최종 결정을 향해 달려가는 부품과 같았다.
깨달음을 얻은 솔라는 루나가 새로 띄워준 또 다른 정책 파일을 바라보았다. ‘신입 개발자 김솔라, 왜 S3 로그 파일에 접근할 수 없나요?’라는 제목의 문제 상황이었다.
솔라는 더 이상 정책의 문법을 하나하나 해독하지 않았다. 대신, 방금 머릿속에 완성된 ‘통합 권한 판단 흐름도’를 따라 생각하기 시작했다.
“좋아, 신입 개발자 김솔라가 S3 로그 파일 읽기를 요청했어. 먼저, 적용되는 정책 중에 명시적인 Deny가 있나? …없네. 그럼 Allow를 찾아보자. 아, 여기 그룹 정책에 s3:GetObject을 허용하는 구문이 있구나. 그런데… 잠깐, Condition이 붙어있네. ‘사무실 IP 대역에서만 접근 허용’. 이 개발자는 지금 재택근무 중이야. 조건이 맞지 않으니 이 Allow는 효력이 없어.”
솔라는 마침내 원인을 찾아냈다. “알겠다! 명시적인 Deny에 걸린 건 아니야. 하지만 어떤 Allow 정책도 이 상황을 허용해주지 않으니까, 최종적으로 기본값인 ‘거부’가 된 거네. 문이 잠겨 있어서 못 들어간 게 아니라, 아무도 문을 열어주지 않아서 못 들어간 거였어.”
복잡한 암호문 같았던 IAM 정책 JSON이, 이제 솔라의 눈에는 명확한 의도를 가진 하나의 논리적인 흐름으로 보였다. 그녀는 이제 어떤 정책을 마주하더라도, 그 이면에 숨겨진 권한 판단의 거대한 기계가 어떻게 작동할지 그려낼 수 있게 되었다.