🔥 정책 평가 흐름: Allow와 Deny가 충돌할 때

1557자
19분

같은 계정 안의 같은 사용자가, 같은 S3 버킷에 GetObject를 요청한다. User에 붙은 정책은 Allow. 버킷 정책도 Allow. 그런데 거절당한다. 콘솔 에러 메시지는 딱 한 줄, AccessDenied.

이 장면 앞에서 한참 멈췄다. Allow가 둘인데 왜 막힐까? 머리로는 "어딘가 Deny가 하나 섞였겠지"까지 가지만, 그 Deny가 어디서 나왔는지 짚으려면 IAM의 정책 평가 순서를 끝까지 따라가 봐야 한다.

Policy: JSON으로 권한을 표현하는 법에서는 Policy 한 장의 생김새와 Effect·Action·Resource·Principal·Condition 다섯 항목이 어떻게 맞물리는지 따라갔다. 여기서는 그런 Policy 여러 장이 한 요청 위에 동시에 놓일 때 어떤 순서와 규칙으로 최종 결정을 내리는지 짚는다. 실습은 없다. 그림 몇 장과 시나리오 몇 개만 있다.

User 정책도 Allow, 버킷 정책도 Allow인데 Denied가 나오는 장면. 숨은 Deny의 3대 범인은 SCP, 같은 버킷 정책의 다른 Deny Statement, Permission Boundary

한 요청이 걷는 길

처음 짚고 갈 건 기본값이다. AWS 공식 평가 문서는 이 점을 한 줄로 분명히 적는다. 모든 요청은 implicit deny(암묵적 거절)에서 출발한다. 출발점이지 도착점이 아니다. 아무 정책도 안 붙어 있는 새 User를 만들면, 그 User는 콘솔 로그인만 될 뿐 다른 어떤 API도 호출하지 못한다. 허용은 누군가가 명시적으로 적어 줘야 생긴다.

섹션 오프너 IAM이란 무엇인가: 계정 보안의 문지기에서 요청 평가가 "인증 → 컨텍스트 → 인가" 3단계로 돈다는 점을 한 번 훑었다. 여기서 볼 대상은 그 세 번째 단계, "인가"의 내부다. AWS 코드가 정책들을 줄 세운 뒤 한 장씩 확인하는 순간이다.

순서는 이렇게 돈다. 공식 "How AWS evaluates requests" 문서가 정리한 흐름 그대로다.

  1. Explicit Deny 전수 검사: 모든 정책 타입(SCP·Resource·Identity·Permissions boundary·Session 등)을 한꺼번에 훑는다. 이 요청에 맞는 "Effect": "Deny"를 하나라도 찾으면 AWS가 평가를 거기서 멈춘다. 결과는 Deny다.
  2. Organizations RCP/SCP 검사: 다중 계정 조직이라면 먼저 RCP(Resource Control Policy), 이어서 SCP(Service Control Policy)가 이 액션을 Allow 안에 포함하는지 본다. 한 단계라도 Allow가 없으면 AWS가 거기서 평가를 끊는다. 결과는 Deny. 단일 계정에서는 이 단계를 건너뛴다.
  3. Resource-based policy 검사: 버킷 정책·큐 정책 같은 리소스 직통 정책이 이 주체에게 이 액션을 Allow로 주고 있나?
  4. Identity-based policy 검사: User·Group·Role에 붙은 정책이 Allow를 내고 있나?
  5. Permissions boundary 검사: 경계가 걸려 있다면, 이 액션이 경계 안쪽에 있나?
  6. Session Policy 검사: AssumeRole로 만든 임시 자격증명에 세션 정책을 함께 넣었다면, 그 정책도 Allow를 내는지 본다.

3번부터 6번까지는 서로 다른 관문이지만, "Allow 한 번이면 통과"가 아니다. Resource-based policy가 Allow를 내도 3번만 지난 셈이고 4번부터 6번이 남아 있다. 네 관문 가운데 실제로 적용되는 항목은 모두 Allow해야 AWS가 요청을 최종 허용한다. 어느 관문도 Allow를 못 내면 출발점의 implicit deny가 그대로 결과가 된다.

핵심을 한 줄로 줄이면, Deny는 어디서든 한 번이면 충분하고, Allow는 적용되는 모든 관문을 통과해야 한다. 이 비대칭이 IAM을 "보안 우선"이라고 부르는 이유다.

AWS IAM 정책 평가 순서도. Implicit Deny에서 시작해 Explicit Deny, SCP, Resource, Identity, Boundary, Session 순으로 체크

명시적 Deny를 먼저 보는 이유

첫 관문에 Explicit Deny를 두는 이유는 보안 설계의 기본 감각 때문이다. 누군가가 "이건 절대 안 된다"라고 적어 두면, 그 선언은 다른 어떤 Allow보다 세게 작동해야 안전하다. AllowDeny를 이기면 운영자는 권한을 뺄 수 없다. "넣는 건 쉽고 빼는 건 어렵다"라는 말은 IAM에서도 그대로 통한다.

공식 문서의 평가 규칙은 이 점을 아주 짧게 적는다. 명시적 Deny는 어떤 Allow보다 우선한다. 어떤 User 정책이 s3:*Allow로 주고 있어도, 같은 계정 어딘가에 NotResource로 한 버킷을 Deny해 놓은 가드 정책이 붙어 있으면 그 버킷만은 건드리지 못한다.

이 규칙 덕분에 운영팀이 쓰는 패턴 중 하나가 "가드 정책(guard policy)"이다. 권한은 개별 팀이 자유롭게 Allow로 늘리게 두되, 건드려서는 안 되는 리소스(프로덕션 KMS 키, 감사 로그 버킷 같은 것)에는 계정 전역에 Deny를 걸어 둔다. 새로운 팀이 실수로 *:*을 붙여도, 그 Deny는 막을 수 없다.

글 첫머리의 시나리오에 대한 답도 여기에 있다. User 정책이 Allow, 버킷 정책도 Allow인데 실패했다면, 거의 대부분은 어딘가 Explicit Deny가 끼어든 것이다. 흔한 범인은 세 개다. 관리 계정이 아닌 멤버 계정에서 조직 SCP가 해당 서비스를 통째로 Deny하고 있거나, 버킷 정책의 다른 Statement에 "Effect": "Deny"가 들어 있거나, 권한 경계가 그 액션을 경계 밖으로 쳐냈거나다.

Identity-based와 Resource-based가 만나면

위 순서에서 3번 Resource-based policy와 4번 Identity-based policy, 곧 리소스에 붙은 정책과 신원에 붙은 정책이 한 요청 위에 함께 놓이면 어떻게 될까. 이것도 공식 평가 문서에 한 문장으로 정리돼 있다. "같은 계정 안에서는 AWS가 두 정책의 권한을 합집합으로 계산한다."

합집합이라는 말을 풀면, 둘 중 하나만 Allow여도 AWS가 요청을 허용한다는 뜻이다. User의 Identity-based 정책은 s3:GetObject를 안 적고 있지만, 그 버킷의 Resource-based 정책이 "이 User는 Read 가능"이라고 적어 놨다면, 3번·4번 체크는 통과한다. 반대도 마찬가지다. 버킷 정책은 조용한데 User 정책이 s3:GetObject를 허용한다면, 이것도 통과다.

이 대목이 Policy: JSON으로 권한을 표현하는 법에서 같은 JSON 문법을 써도 Identity-based policy와 Resource-based policy를 구분한 이유다. 둘은 "두 방향의 서로 다른 정책"이라기보다 "한 요청을 허용하는 두 입구"에 가깝다. 어느 한쪽이 Allow를 내면 AWS는 이 두 관문을 함께 통과한 것으로 친다.

다만 계정이 다르면 규칙이 바뀐다. 크로스 계정 요청에서는 AWS가 두 정책을 합집합이 아니라 교집합으로 계산한다. 양쪽 계정에서 각각 Allow가 나와야 통과한다. 이 구분은 Assume Role을 다루는 다음 글에서 한 번 더 짚는다.

같은 계정 안에서는 Identity-based와 Resource-based가 합집합(OR), 크로스 계정에서는 교집합(AND). 왼쪽 Venn은 두 원 모두에 Allow 표시가 있고, 오른쪽은 겹치는 영역만 통과로 강조된다

권한 경계: 권한 상한을 정하는 울타리

여기서 처음 막히는 사람이 많다. 권한 경계는 사용자나 역할에 붙이는 정책이지만, 권한을 직접 주지 않는다. 이 정책은 그 신원이 가질 수 있는 권한의 상한을 정한다. 말 그대로 울타리다.

권한 경계는 Identity-based policy와 같은 지점에서 함께 본다. 차이는 의미에 있다. Identity-based policy가 "이 신원에게 무엇을 준다"라면, 권한 경계는 "이 신원이 무엇을 가질 수 있다"를 정한다. AWS는 이 둘을 교집합으로 계산한다. 한쪽만 Allow해서는 못 지난다. 둘 다 Allow해야 AWS가 그 액션을 허용한다.

왜 이런 게 필요한가. 공식 가이드가 드는 대표 유스케이스는 "권한 위임"이다. 플랫폼팀이 개발자에게 IAM User·Role을 직접 만들 권한을 주고 싶다. 그런데 그냥 iam:CreateUser만 주면, 그 개발자가 만든 User에 AdministratorAccess를 붙여 자기 자신을 승격시킬 수도 있다. 권한 경계가 이 위험을 막아 준다. 플랫폼팀은 "네가 만드는 신원은 반드시 이 경계 안쪽에 있어야 해"라는 계약을 JSON 한 장으로 걸어 둔다. 개발자가 자유롭게 Allow를 붙여도, 그 신원은 경계 밖 권한을 결코 얻지 못한다.

여기서 기억할 게 하나 더 있다. 권한 경계는 교집합이라, 신원이 이미 Allow로 받은 권한도 경계 밖이면 잘려 나간다. "경계를 걸었으니 이제 안심이다"라는 감각이 이 대목에서 생긴다. 반대로 말하면, 한 신원에게 넓은 Allow를 주고 권한 경계로 좁혀 놓으면 "정책 두 장을 다 읽어야 실제 권한을 알 수 있다"라는 부작용도 생긴다. 정책 디버깅의 피로도가 늘어난다는 뜻이다.

Identity-based 정책과 Permission Boundary의 교집합이 유효 권한

권한 경계와 SCP: 비슷해 보여도 적용 단계가 다르다

둘 다 "권한의 최대치를 정의한다"는 점에서 닮았고, 둘 다 직접 권한을 주지 않는다는 점도 같다. 차이는 적용 단계가 다르다는 데 있다. 그래서 둘을 자주 섞어 생각한다.

SCP는 AWS Organizations 차원에서 계정 또는 OU 전체에 적용한다. AWS는 그 계정 안의 모든 User와 Role에 이 규칙을 한꺼번에 적용한다. 단, 관리 계정(management account)의 User·Role에는 영향을 주지 않는다는 단서가 있다.

권한 경계는 계정 안에서 각 신원에 따로 붙인다. 같은 계정 안에서도 어떤 Role에는 붙이고 어떤 Role에는 빼둘 수 있다.

세 정책 타입(SCP + 권한 경계 + Identity-based policy)이 한 요청을 동시에 누른다면, 공식 문서가 정한 규칙은 분명하다. 셋 모두가 Allow해야 AWS가 요청을 허용한다. 세 정책 가운데 어느 하나에 Explicit Deny가 있으면 AWS가 요청을 거기서 막는다.

실무에서는 이렇게 나누면 분명하다. 조직 차원에서 서비스 자체를 막고 싶다면 SCP를 쓴다. 개인 개발자가 만들 수 있는 Role의 권한 범위를 잘라 두고 싶다면 권한 경계를 쓴다. 두 도구가 풀려는 문제는 겹치는 구석이 있지만, 운영 팀이 짜는 계층은 대체로 다르다.

네 개의 중첩된 층. 바깥에서 안쪽으로 Organization(SCP·RCP) → Account → Identity(Permission Boundary) → Session(Session Policy). 걸리는 층이 다르다는 걸 네스팅으로 보여 준다

세션 정책: 임시 자격증명에 거는 마지막 필터

순서의 맨 끝, 6번 관문에 놓인 마지막 정책이 Session Policy다. User, Group, Role: 세 가지 주체의 차이에서 Role은 AssumeRole을 호출할 때만 임시 자격증명을 만든다고 설명했다. Session Policy는 그 호출에 옵션으로 넘기는 정책이다.

Session Policy의 성격은 권한 경계와 비슷하다. "이 세션의 권한은 경계를 넘지 않는다"라는 선언이다. 다만 공식 문서는 이 점을 분명히 적는다. Session Policy는 role의 identity-based policy가 이미 허용한 범위 밖으로 권한을 넓히지 못한다. 세션 정책은 권한을 넓히는 도구가 아니라 잘라내는 도구다.

인라인 세션 정책은 2,048자 제한이 있고, 관리형 정책은 ARN으로 최대 10개까지 넘길 수 있다(STS AssumeRole 참조).

쓰이는 장면은 이렇다. 한 팀이 공용 Role을 하나 쓰는데, 오늘 이 스크립트는 그 중 S3 읽기 권한만 필요하다면 AssumeRole 호출에 "S3 Read만 허용" 세션 정책을 얹는다. 공용 Role의 원래 정책은 건드리지 않고도, 이번 세션만은 더 좁은 권한으로 돌아간다.

STS AssumeRole 호출에 Role ARN과 Session Policy를 넘기면, Role 본래 정책(s3·ec2·dynamodb 전체)과 Session Policy(s3:GetObject)의 교집합만 남긴 임시 자격증명이 발급된다

언제 이것들을 쓰지 말아야 하는가

이 섹션의 맥락 하나는 "언제 이 서비스를 쓰지 말아야 하는가"다. 권한 경계와 SCP에도 그 질문이 그대로 붙는다.

  • 권한 경계를 "권한 관리의 만능 해답"처럼 쓰지 말자. 신원이 적은 환경에서는 Identity-based policy 자체를 좁게 짜는 편이 더 단순하다. 권한 경계를 얹으면 정책이 두 장으로 늘고, 디버깅 난이도도 함께 올라간다.
  • SCP로 개별 User 하나를 제한하지 말자. SCP는 계정이나 OU 전체에 퍼지는 도구다. 한 명만 제한하려면 그 신원에 권한 경계를 붙여야 한다.
  • Condition을 "엄격하면 안전하다"는 생각으로 도배하지 말자. MFA·IP·시간대 Condition을 겹겹이 얹으면 나중에 Deny 이유를 추적하는 비용이 크게 늘어난다. Allow에는 최소 한정, Deny에는 명확한 한정을 건다는 감각이 편하다.

오늘의 3줄

  • Deny는 어디서든 한 번이면 충분하고, Allow는 적용되는 모든 관문을 통과해야 한다.
  • 같은 계정의 Identity-based와 Resource-based는 합집합이지만, 크로스 계정은 교집합이 된다.
  • 권한 경계와 SCP, Session Policy는 모두 권한을 주는 도구가 아니라 권한 상한을 자르는 도구다.

이 흐름은 곧장 Assume Role: 임시 자격증명을 만드는 순간으로 연결한다. 임시 자격증명신뢰 정책이 STS 호출 한 번에서 어떻게 맞물려 작동하는지 거기서 본다.

참고 자료

YouTube 영상

채널 보기
직교성과 벡터 투영 | 선형대수학
벡터의 정의와 덧셈 연산 | 선형대수학
트라이(Trie)를 이용한 자동 완성 알고리즘 | Trie 자료구조 이야기
마지막편, 트라이 노드를 50% 이상 줄이는 방법? 압축 트라이 성능 분석 | Trie 자료구조 이야기
AI 추천 시스템의 원리, 벡터 사이의 각도와 코사인 유사도 | 선형대수학
숫자 하나가 AI 모델의 운명을 바꾼다? | 선형대수학
트라이(Trie) 자료구조: 파이썬으로 삽입(Insert) 연산 구현하기 | Trie 자료구조 이야기
트라이(Trie)에서 단어를 삭제하는 방법 | Trie 자료구조 이야기