작업에 맞는 최소 권한
75분 안팎
학습 목표
리소스·행동·명시적 거부를 구분합니다.
개념
권한을 오류 해결의 마지막 버튼으로 쓰지 않습니다
안내판 서비스가 다른 환경의 설정을 읽지 못하는 것은 이번 과제의 성공 조건입니다. 접근 오류가 생길 때 관리자 권한을 주면 요청은 살아날 수 있지만 그 서비스가 침해되었을 때 읽을 수 있는 자료도 늘어납니다. 교육 담당자는 신입에게 명령이 성공했는지만 묻지 않고 어떤 주체가 어떤 대상에 어떤 행동을 했는지 먼저 설명하게 합니다. 최소 권한은 작은 권한을 무작정 고르는 일이 아니라 맡은 작업을 수행하는 데 필요한 범위를 구체적으로 남기는 일입니다.
실습 환경과 입력 계약
이 모듈의 브라우저 코드는 Python 3의 json과 표준 입력을 사용합니다. 로컬 zip은 압축을 푼 루트에서 bash check.sh로 검사합니다. Docker 실습에는 개인 Docker daemon과 Python 3, 미션에는 JDK 17과 OpenTofu 1.8 이상이 추가로 필요합니다. Java 안내는 ./mvnw test이며 작성 검증은 캐시를 사용하는 오프라인 실행으로 합니다. Docker와 tofu 결과는 외부 검증 대기로 표시합니다. 실제 회사 비밀 대신 실행 중 생성하는 교육용 토큰만 사용합니다. 실습별 README는 입력 경로와 정리할 자기 자원을 설명합니다.
주체와 리소스와 행동을 분리합니다
서비스 계정은 안내판 프로세스를 실행하는 주체이고 배포 계정은 검증한 이미지를 선택하여 실행 구성을 바꾸는 주체입니다. 리소스는 board/demo처럼 접근할 대상이며 행동은 notice:Read와 notice:Write처럼 수행할 작업입니다. 서비스가 조회만 한다면 자기 안내판 Read를 허용하고 다른 안내판 Read와 자기 안내판 Write는 별도 요청으로 거부합니다. 한 주체에 모든 행동과 모든 대상을 주는 정책은 업무의 경계를 설명하지 못합니다. 권한 표에는 정상 작업과 거부되어야 하는 작업을 나란히 적습니다.
기본 거부는 일치하는 허용이 없는 상태입니다
유효한 요청이라도 허용 규칙이 일치하지 않으면 DENY를 출력합니다. rules가 비어 있는 경우도 정책 오류가 아니라 허용을 찾지 못한 상태입니다. 없는 리소스 접근과 없는 행동 접근을 각각 테스트하면 어느 조건을 빠뜨렸는지 찾을 수 있습니다. 허용 규칙은 행동과 리소스가 함께 맞아야 합니다. 행동만 확인하면 demo용 서비스가 other 환경도 읽게 되고 리소스만 확인하면 조회 계정이 삭제 행동까지 할 수 있습니다. 비교 조건을 두 개 적는 이유를 코드로 설명할 수 있어야 합니다.
명시적 거부는 허용보다 우선합니다
한 요청에 Allow와 Deny가 모두 맞으면 DENY입니다. 앞에서 허용을 찾았다고 즉시 반환하면 뒤의 거부 규칙을 놓칩니다. 이 실습은 전체 정책의 형식을 검증한 뒤 일치하는 효과를 모아 거부부터 확인합니다. 규칙의 파일 순서로 보안 결과가 바뀌지 않도록 허용과 거부의 위치를 바꾼 테스트도 수행합니다. 기본 거부는 허용이 없는 상태이고 명시적 거부는 맞는 Deny 규칙이 있는 상태입니다. 둘은 결과가 같아도 정책 변경을 검토할 때 근거가 다릅니다.
와일드카드는 일부러 작은 계약으로 제한합니다
실습에서 별표 하나는 해당 필드 전체와 일치합니다. notice:* 같은 부분 패턴은 지원하지 않고 ERROR로 처리합니다. startswith로 이를 대충 구현하면 notice:ReadExtra가 의도와 다르게 허용될 수 있습니다. 실제 AWS 정책은 더 많은 요소와 패턴을 지원하지만 이번 판정기는 그 전체 구현이 아닙니다. 교육 모델의 Resource는 짧은 이름이며 AWS ARN이 아닙니다. 허용할 행동을 늘리려면 별표로 바꾸기 전에 추가 작업 하나와 그 대상을 구체적인 규칙으로 적고 검토합니다.
손상된 정책을 정상 거부와 구분합니다
효과가 소문자 allow이거나 rules가 문자열이면 ERROR입니다. 빈 표준 입력과 손상된 JSON도 같은 오류 출력 계약을 사용합니다. 정상 요청에서 정책이 맞지 않는 DENY와 입력을 읽지 못하는 ERROR를 나누면 권한 확대가 필요한지 입력 생산자 수정이 필요한지 조사할 수 있습니다. 첫 규칙이 허용이고 둘째 규칙이 잘못됐을 때도 ERROR가 나야 합니다. 손상된 부분을 몰래 버리면 검토한 정책과 실행한 정책이 달라집니다. 출력에는 입력 원문을 덧붙이지 않습니다.
파이썬 코드의 판단 순서를 정합니다
json.load로 객체를 받고 action과 resource가 비어 있지 않은 문자열인지 확인합니다. rules가 목록인지 확인한 뒤 각 규칙의 effect와 두 필드를 검사합니다. 마지막으로 일치하는 규칙의 effect만 모읍니다. 이 방식은 데이터 검증과 권한 결정을 따로 생각하게 합니다. KeyError는 필수 키가 빠진 입력, ValueError는 JSON이나 값의 형식 문제를 가리킵니다. 예외를 ERROR로 바꾸더라도 테스트에서는 어떤 입력이 어떤 분기를 거쳤는지 이름을 남겨야 수정을 설명할 수 있습니다.
실제 IAM의 범위를 짧게 확인합니다
AWS IAM에는 identity policy 이외에도 resource policy, permissions boundary, 조직 정책과 세션 정책 등이 관여합니다. 적용되는 명시적 거부가 허용을 막는다는 원리는 사용하지만 단순한 rules 목록이 실제 계정의 최종 권한을 계산한다고 주장하지 않습니다. 같은 계정과 교차 계정의 정책 평가도 조건이 다릅니다. 실제 정책 리뷰에서는 어떤 정책 집합과 주체를 포함했는지 먼저 적습니다. 공식 평가 문서는 이 레슨의 출처 링크로 연결하며 클라우드 계정에 정책을 붙이는 작업은 하지 않습니다.
AWS 예시는 객체 한 개로 좁힙니다
미션의 iam-example.json은 s3:GetObject와 교육용 버킷의 releases/notice.json 한 객체 ARN만 허용하는 예시입니다. 버킷 목록 조회와 객체 삭제는 허용하지 않습니다. 주체에 붙이는 identity policy라 Principal 필드를 넣지 않습니다. 예시 문자열을 실제 자격증명처럼 쓰지 않고 사람이 행동과 리소스 범위를 검토하는 자료로 제출합니다. 다른 정책이나 암호화 키가 요구하는 접근까지 해결한 정책은 아닙니다. 로컬 Docker 계정의 권한 표와 AWS 예시를 같은 구현이라고 설명하지 않습니다.
테스트는 허용 하나와 거부 여러 개를 갖춥니다
자기 안내판 Read는 허용하고 같은 대상의 Write, 다른 대상의 Read, 빈 정책은 거부합니다. 전체 허용 규칙과 특정 거부 규칙이 겹칠 때도 거부합니다. 잘못된 효과, 지원하지 않는 부분 별표, 키 누락, 빈 입력은 오류입니다. 이 조합은 성공 사례만 맞춘 코드와 모든 요청을 거부하는 코드를 함께 잡습니다. 테스트를 통과시키려고 모든 효과를 Allow로 바꾸는 수정은 요구사항을 깨뜨립니다. 실패한 입력에서 행동과 리소스를 하나씩 바꾸어 원인을 좁힙니다.
정책 검토 결과를 작업 언어로 말합니다
최종 설명에는 규칙 개수보다 서비스가 실제로 할 수 있는 작업을 적습니다. 예를 들어 자기 안내판 조회를 허용하고 다른 환경 조회와 쓰기를 막으며 명시적 거부가 겹치면 차단한다고 말합니다. 배포 계정이 컨테이너를 생성할 수 있다는 사실은 서비스 프로세스가 그 권한을 갖는다는 뜻이 아닙니다. 다음 레슨에서는 이 구분을 파일과 UID에 적용합니다. sudo 위임의 세부 문법은 더 읽기로 보내고 여기서는 주체·대상·행동을 판정하는 코드를 스스로 완성합니다.
정책 원리의 출처: AWS IAM 정책 평가 공식 문서. 실제 적용 전에 포함되는 정책 종류를 확인합니다.
따라하기
허용 요청 만들기
같은 행동과 리소스가 일치하는 사례를 실행합니다.
import json,sys
def decide(d):
if not isinstance(d,dict): raise ValueError()
action=d['action'];resource=d['resource'];rules=d['rules']
if not isinstance(action,str) or not action or not isinstance(resource,str) or not resource or not isinstance(rules,list): raise ValueError()
matched=[]
for rule in rules:
if not isinstance(rule,dict): raise ValueError()
if rule['effect'] not in ['Allow','Deny']: raise ValueError()
for key in ['action','resource']:
if not isinstance(rule[key],str) or not rule[key]: raise ValueError()
if '*' in rule[key] and rule[key]!='*': raise ValueError()
if rule['action'] in [action,'*'] and rule['resource'] in [resource,'*']:
matched.append(rule['effect'])
if 'Deny' in matched: return 'DENY'
return 'ALLOW' if 'Allow' in matched else 'DENY'
try:
print(decide(json.load(sys.stdin)))
except (ValueError,KeyError,TypeError):
print('ERROR')
실행 결과
ALLOW
겹치는 거부 확인
Allow가 앞에 있어도 뒤의 Deny가 우선하는 결과를 확인합니다.
import json,sys
def decide(d):
if not isinstance(d,dict): raise ValueError()
action=d['action'];resource=d['resource'];rules=d['rules']
if not isinstance(action,str) or not action or not isinstance(resource,str) or not resource or not isinstance(rules,list): raise ValueError()
matched=[]
for rule in rules:
if not isinstance(rule,dict): raise ValueError()
if rule['effect'] not in ['Allow','Deny']: raise ValueError()
for key in ['action','resource']:
if not isinstance(rule[key],str) or not rule[key]: raise ValueError()
if '*' in rule[key] and rule[key]!='*': raise ValueError()
if rule['action'] in [action,'*'] and rule['resource'] in [resource,'*']:
matched.append(rule['effect'])
if 'Deny' in matched: return 'DENY'
return 'ALLOW' if 'Allow' in matched else 'DENY'
try:
print(decide(json.load(sys.stdin)))
except (ValueError,KeyError,TypeError):
print('ERROR')
실행 결과
DENY
정책 순서 뒤집기
효과 집합을 모은 뒤 판단하면 순서가 바뀌어도 거부가 유지됩니다.
effects=['Deny','Allow']
print('DENY' if 'Deny' in effects else 'ALLOW')실행 결과
DENY
오류와 기본 거부 구별
브라우저 실습에서 빈 rules와 빈 입력을 각각 실행하고 DENY와 ERROR가 다른 결과임을 확인합니다.
확인 문제
실습
action·resource·rules JSON 객체를 읽고 ALLOW, DENY, ERROR 중 한 줄을 출력합니다. rules의 effect는 Allow 또는 Deny, action·resource는 비어 있지 않은 문자열입니다. 정확한 문자열 또는 별표 하나만 비교하며 부분 별표는 오류입니다. 정책 전체를 검증하고 일치 Deny가 있으면 거부, 아니면 일치 Allow가 있어야 허용합니다. 빈 목록은 DENY, 빈 입력·키 누락·손상 정책은 ERROR입니다.
모범 답안
import json,sys
def decide(d):
if not isinstance(d,dict): raise ValueError()
action=d['action'];resource=d['resource'];rules=d['rules']
if not isinstance(action,str) or not action or not isinstance(resource,str) or not resource or not isinstance(rules,list): raise ValueError()
matched=[]
for rule in rules:
if not isinstance(rule,dict): raise ValueError()
if rule['effect'] not in ['Allow','Deny']: raise ValueError()
for key in ['action','resource']:
if not isinstance(rule[key],str) or not rule[key]: raise ValueError()
if '*' in rule[key] and rule[key]!='*': raise ValueError()
if rule['action'] in [action,'*'] and rule['resource'] in [resource,'*']:
matched.append(rule['effect'])
if 'Deny' in matched: return 'DENY'
return 'ALLOW' if 'Allow' in matched else 'DENY'
try:
print(decide(json.load(sys.stdin)))
except (ValueError,KeyError,TypeError):
print('ERROR')
더 읽기
면접 질문
- 서비스 계정과 배포 계정의 권한을 어떻게 나누며 최소 권한을 검증하나요?