🔥 Policy: JSON으로 권한을 표현하는 법
강의 목차
콘솔에서 AmazonS3ReadOnlyAccess를 처음 열었을 때, 화면을 가득 채운 JSON을 한참 읽었다. 키 이름은 단순한데 어디서부터가 한 규칙이고 어디까지가 한 묶음인지 잘 잡히지 않았다. "Policy는 결국 한 장의 JSON이다"라는 한 문장이 머리에 들어오기 전까지는 정책 평가니 명시적 Deny니 하는 이야기가 다 추상 개념으로만 남았다.
Policy는 IAM의 네 번째 벽돌이다. 권한이 JSON 한 장에 어떻게 담기는지, 그 안에서 Effect·Action·Resource·Condition이 무엇을 맡는지, 같은 형식의 JSON을 왜 AWS 관리형·고객 관리형·인라인이라는 세 종류로 운영하는지를 따라간다. 정책이 서로 충돌할 때 AWS가 어떤 규칙으로 결론을 내리는지는 정책 평가 흐름: Allow와 Deny가 만나면에서 다루고, 여기서는 Policy 문서 한 장의 구조까지만 잡는다.
Policy의 골격: Version과 Statement
IAM JSON 정책 문법의 최상위는 단순하다. Version, Id(선택), Statement[] 세 개뿐이다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowReadOnlyS3",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": "arn:aws:s3:::my-bucket/*"
}
]
}{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowReadOnlyS3",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": "arn:aws:s3:::my-bucket/*"
}
]
}Version은 가장 먼저 헷갈리는 항목이다. 새 버전이 나올 것 같은 이름이지만, 공식 가이드는 "2012-10-17" 사용을 권한다. 옛 값인 "2008-10-17"도 있긴 한데, 그쪽은 정책 변수(${aws:username} 같은 표현) 같은 현대적인 기능을 못 쓴다. 새 정책을 짤 일이 있다면 그냥 "2012-10-17"이라고 적어 두는 편이 안전하다.
Statement는 배열이다. 한 정책 안에 규칙을 여러 개 넣을 수 있다는 뜻이고, AWS는 각 규칙을 따로 평가한다. 한 statement가 Allow를 내고 다른 statement가 Deny를 내면 어떻게 합쳐지는가는 뒤에서 따로 다룰 주제니, 여기서는 "그냥 규칙 여러 개를 한 통에 담는 그릇"이라고 보면 된다.
Statement의 다섯 항목: Effect / Action / Resource / Principal / Condition
Statement 안의 객체 하나가 한 권한 규칙이다. 구성 요소는 다섯 개다.
Effect:"Allow"아니면"Deny". 둘뿐이다.Action: 어떤 API 호출에 적용할지.s3:GetObject처럼{서비스}:{API이름}형태로 적는다. 와일드카드(s3:*)도 된다. 반대 표현인NotAction도 있는데, 같은 statement에Action과NotAction을 동시에 못 적는 상호 배타 규칙이 있다.Resource: 어느 ARN에 적용할지. 위의 예시처럼 단일 ARN을 적거나 배열로 여러 개를 나열한다."*"도 되지만 이쪽이 흔한 사고의 시작점이다. 이 짝꿍도NotResource와 동시에 못 쓴다.Principal: 누구에게 권한을 줄지. 신원에 붙는 정책(identity-based)에서는 보통 안 쓴다. 이미 신원에 붙어 있으니 누구인지가 자명하기 때문이다. 대신 S3 버킷 정책 같은 리소스에 붙는 정책(resource-based)에서 "이 버킷에 접근할 수 있는 주체는 누구"인지를 적을 때 핵심이 된다. Trust Policy의 Principal 규칙은 지난 편 Group 항목에서 짚었다.Condition: 어떤 조건일 때만 적용할지. 시간·IP·MFA 사용 여부·요청 출처가 단골이다. 공식 연산자 목록에는StringEquals,IpAddress,DateGreaterThan,Bool,Null같은 게 있다.
여기에 선택 항목 하나를 더 붙일 수 있다.
Sid: Statement 식별자. 사람이 읽기 위한 라벨이다. AWS는 평가에 쓰지 않는다. 안 적어도 되지만, 정책이 길어지면 디버깅할 때 어느 statement가 막혔는지 추적하기 위해 적어 두는 편이 낫다.
Action이나 Resource는 단일 문자열로 적어도 되고 배열로 적어도 된다. AWS는 양쪽 다 받는다. 코드 리뷰에서 한 줄로 적힌 정책을 배열 형식으로 펴는 디프가 자주 보이는 이유다.

두 방향: 신원에 붙는 정책 vs 리소스에 붙는 정책
같은 JSON 문법을 써도 정책은 붙는 대상에 따라 두 종류로 나눈다. 어느 쪽에 붙이느냐가 Principal 항목의 필요 여부를 결정한다.
- Identity-based policy: User·Group·Role에 붙는다. "이 신원에게 무엇을 허용할까"를 적는다.
Principal을 적지 않는다(이미 누구인지 정해져 있으니까). - Resource-based policy: S3 버킷·SQS 큐·KMS 키 같은 리소스에 직접 붙는다. "이 리소스에 누가 접근할 수 있는가"를 적는다.
Principal항목을 반드시 적는다.
두 방향의 정책이 어떻게 합쳐져 최종 결정이 나오는지는 뒤에서 따로 다룬다. 여기서는 "같은 JSON이라도 붙는 대상을 바꾸면 Principal에 적을 내용과 필요 여부도 함께 바꿔야 한다" 정도만 잡으면 충분하다.
같은 JSON, 다른 살림집: AWS 관리형 · 고객 관리형 · 인라인
여기서부터가 실전에서 가장 자주 헷갈리는 대목이다. 같은 형식의 JSON도 어디에 두고 어떻게 관리하느냐에 따라 세 종류로 나눈다. 공식 비교 문서가 정한 분류를 그대로 옮기면 이렇다.
| 종류 | 누가 만드나 | 누가 수정하나 | 재사용 | ARN | 신원 삭제 시 |
|---|---|---|---|---|---|
| AWS 관리형 | AWS | AWS만 (나는 못 고침) | 여러 신원에 N:M | arn:aws:iam::aws:policy/* | 정책은 살아남음 |
| 고객 관리형 | 나 | 나 | 여러 신원에 N:M | arn:aws:iam::{account-id}:policy/* | 정책은 살아남음 |
| 인라인 | 나 | 나 | 한 신원에 1:1 | 별도 ARN 없음 | 함께 사라짐 |
세 종류 모두 한 정책의 크기는 공식 쿼터 기준으로 관리형은 6,144자, 인라인은 붙는 신원에 따라 User 2,048자·Group 5,120자·Role 10,240자다(공백은 안 친다). 한 신원에 붙일 수 있는 관리형 정책 수는 기본 10개로 같지만, 쿼터 증액 한도는 User 20개·Role 25개·Group 10개로 다르게 잡혀 있다는 점도 여기서 꼭 짚어 두고 싶다.

'관리형'이라는 단어가 감추는 비용
여기서도 같은 질문을 던져야 한다. AWS 관리형 정책은 분명 편하다. 콘솔에서 검색만 하면 AdministratorAccess나 AmazonS3ReadOnlyAccess 같은 잘 알려진 이름이 떠 있으니까. 그런데 그 편의가 감추는 비용이 셋 있다.
첫째, 권한 범위를 내가 못 좁힌다. AWS 관리형 정책은 AWS만 수정한다. 우리 팀의 운영 패턴에 비해 너무 넓다 싶어도 손댈 수가 없다. 좁히고 싶으면 같은 형식의 JSON을 복사해서 고객 관리형으로 새로 만들거나, Permission Boundary·SCP로 위에서 범위를 제한해야 한다.
둘째, 관리형 정책의 권한 셋이 시간이 지나면서 조용히 바뀐다. AWS가 새 API를 추가하면 그에 맞춰 관리형 정책에도 액션을 더한다. 보통은 좋은 일이지만, 보안 감사 관점에서는 "내가 동의한 적 없는 권한이 어느 날 늘어나 있을 수도 있다"는 뜻이다. 변경 이력이 AWS managed policies 문서에 따라 일부 정책에 한해 공개돼 있긴 하지만, 모든 정책에 대해서는 아니다.
셋째, 팀이 정책 이름에 의존한다. AmazonS3ReadOnlyAccess처럼 직관적인 이름은 코드·문서에 그대로 새기기 좋지만, AWS가 정책을 더 좁게 쪼갠 새 이름을 권장하기 시작하면 마이그레이션 부담이 내 쪽으로 넘어온다.
요약하면 "관리형이니까 안심"이 아니라, 관리권을 AWS에 위임한 만큼 조정 권한도 내준다는 트레이드오프다.
인라인 정책을 쓰지 말아야 할 때
인라인 정책은 첫인상이 깔끔하다. 한 Role에만 적용되니 영향 범위가 좁고, 신원을 지우면 정책도 같이 사라지니 청소가 자동이다. 작은 팀이 한두 개의 일회성 권한을 바를 때는 충분하다.
문제는 신원이 두세 개로 늘어난 다음부터다. 같은 권한을 두 Role에 동시에 주려면 인라인 정책 두 벌을 따로 작성해야 한다. 한쪽을 고치면 반대쪽도 손으로 따라가야 한다. 정책 감사 도구로 "이 권한이 누구에게 부여돼 있는가"를 검색할 때도 인라인은 한 명씩 뒤져야 한다. 공식 비교 문서에도 같은 기준이 나온다. 의역하면 "두 개 이상의 신원에 붙일 가능성이 조금이라도 있으면 관리형으로 짜라"는 뜻이다(Managed policies and inline policies).
내 경험으로는 다음 셋 중 하나라도 해당되면 인라인을 피하는 편이 나았다.
- 동일 권한을 두 신원 이상에 줄 가능성이 있다.
- 정책 변경 이력을 git으로 추적하고 싶다(고객 관리형은 ARN을 IaC 코드에 직접 적을 수 있어서 추적이 쉽다).
- 권한 부여 정책을 별도로 감사받아야 한다.
반대로 "이 임시 Role에만 붙고 다른 데 절대 재사용 안 한다"는 확신이 있을 때, 그리고 정책 길이가 짧을 때 인라인은 깔끔한 선택이다.
Policy 평가로 넘어갈 지점
Policy 한 장의 구조와, 같은 JSON을 어디에 두느냐가 운영 비용을 어떻게 바꾸는지까지 다뤘다. 정책 평가 흐름: Allow와 Deny가 만나면에서는 정책 여러 장이 겹칠 때 AWS가 어떤 순서로 결론을 내리는지 다룬다. 핵심 질문은 "명시적 Deny를 왜 항상 먼저 적용하느냐"다.
참고 자료
- IAM JSON policy reference (AWS 공식 문서): Policy 문서 전반의 1차 레퍼런스
- IAM JSON policy elements reference (AWS 공식 문서): Effect·Action·Resource·Principal·Condition 정의의 근거
- Policy element: Version (AWS 공식 문서):
2012-10-17을 쓰라는 권고와2008-10-17의 한계 - Managed policies and inline policies (AWS 공식 문서): 세 종류의 정책 비교 1차 근거
- IAM and AWS STS quotas (AWS 공식 문서): 관리형 6,144자·인라인 신원별 한도·정책 갯수 쿼터 출처
- IAM JSON policy elements: Condition operators (AWS 공식 문서):
StringEquals·IpAddress·DateGreaterThan등 연산자 목록











