NPE 도입이 드러낸 IAM 정책의 민낯
AWS IAM(Identity and Access Management)은 “누가 어떤 AWS 자원에 접근할 수 있는가”를 제어하는 서비스다. 서비스에 **역할(Role)**을 부여하고, 그 역할에 **정책(Policy)**을 붙여서 S3, SQS 같은 자원 접근을 제한한다. 최소 권한 원칙(Least Privilege) — 실제로 필요한 권한만 부여 — 이 핵심이다.
ECS에서는 왜 그냥 돌아갔나
ECS 환경에서는 크게 두 종류의 IAM 역할이 있었다.
| 역할 | 용도 |
|---|---|
| Task Execution Role | ECR에서 이미지 풀, CloudWatch 로그 쓰기, Parameter Store 읽기, KMS 복호화 — 컨테이너 실행 인프라가 쓰는 권한 |
| Task Role | 애플리케이션 코드가 실제로 쓰는 권한 — S3 읽기/쓰기, SQS 송수신, SNS 발행, RDS 접근 등 |
문제는 Task Role이었다. 이 서비스가 접근하는 AWS 자원이 많았다:
- SQS 큐 7개 — audit event, 이메일 구성, 사용자 등록, 내보내기 등 용도별 큐
- SQS Dead Letter Queue 7개 — 각 큐의 DLQ
- 크로스 어카운트 SNS 구독 — 다른 AWS 계정의 고객 데이터 업데이트 수신
- SNS 토픽 — 사용자 이벤트 발행
- S3 버킷 — 내보내기 파일 저장
- Parameter Store — 환경별 설정값
- Secrets Manager — 민감한 자격 증명
이 많은 권한이 하나의 Task Role에 넓은 와일드카드로 묶여 있었다. sqs:*에 arn:aws:sqs:*:*:admin-portal-* 같은 형태다. ECS에서는 하나의 태스크 정의에 하나의 역할을 붙이면 되니, 이래도 “일단 돌아가는” 상태였다.
NPE가 왜 이걸 문제로 만들었나
NPE는 쿠버네티스의 서비스 어카운트(Service Account) 기반으로 IAM 역할을 매핑한다. IRSA(IAM Roles for Service Accounts) 라는 메커니즘인데, 핵심은 Pod 단위로 서로 다른 IAM 역할을 부여할 수 있다는 것이다.
ECS: Task Definition → Task Role (하나의 넓은 정책)
NPE: Pod → Service Account → IAM Role (서비스별 최소 권한)NPE에 서비스를 올리려면 각 서비스의 서비스 어카운트에 정확히 필요한 권한만 매핑해야 했다. 기존처럼 sqs:*를 넣으면 플랫폼 리뷰에서 거부된다. 이 요구사항이 기존 IAM 정책의 문제를 한꺼번에 드러냈다.
와일드카드의 실체
기존 정책을 열어보면 이런 식이었다:
{
"Effect": "Allow",
"Action": "sqs:*",
"Resource": "arn:aws:sqs:*:*:my-platform-*"
}이 한 줄이 문제인 이유:
sqs:*는sqs:DeleteQueue,sqs:PurgeQueue까지 포함한다 — 애플리케이션이 큐를 삭제할 일은 없다.my-platform-*는 이벤트 큐도, 내보내기 큐도, DLQ도 전부 매칭된다 — 서비스 A가 서비스 B 전용 큐에 접근할 이유가 없다.- 어떤 서비스가 어떤 큐를 쓰는지 정책만 봐서는 알 수 없다.
Terraform aws_iam_policy_document로 재구성
Terraform의 aws_iam_policy_document data source는 IAM 정책을 HCL 코드로 선언적으로 작성하는 방법이다. JSON으로 정책을 직접 쓰는 것보다 몇 가지 이점이 있다:
- 타입 체크와 자동완성 — HCL에서
effect,actions,resources를 구조체로 다루기 때문에 오타나 형식 오류를 IDE 단계에서 잡는다. - 정책 조합 —
source_policy_documents로 공통 정책과 서비스별 정책을 합칠 수 있다. JSON에서는 정책을 합치려면 수동으로 statement 배열을 이어붙여야 한다. - 가독성 — statement마다
sid(Statement ID)를 넣어서 각 권한의 목적을 코드로 설명할 수 있다.
공통 정책과 서비스별 정책 분리
공통 정책
data "aws_iam_policy_document" "common_logging" {
statement {
sid = "AllowCloudWatchLogs"
effect = "Allow"
actions = [
"logs:CreateLogStream",
"logs:PutLogEvents"
]
resources = ["arn:aws:logs:*:*:log-group:/ecs/my-platform/*"]
}
}
data "aws_iam_policy_document" "common_parameter_store" {
statement {
sid = "AllowParameterStoreRead"
effect = "Allow"
actions = [
"ssm:GetParameter",
"ssm:GetParametersByPath"
]
resources = ["arn:aws:ssm:*:*:parameter/my-platform/*"]
}
}source_policy_documents가 하는 일
이 속성은 여러 aws_iam_policy_document의 statement를 하나의 정책으로 합쳐준다. 공통 정책(로깅, Parameter Store)을 한 번 정의해두고, 각 서비스 정책에서 source_policy_documents로 가져와 조합하는 구조다.
서비스 A 정책 = common_logging + common_parameter_store + 서비스 A 고유 권한
서비스 B 정책 = common_logging + common_parameter_store + 서비스 B 고유 권한JSON으로 이걸 하려면 statement 배열을 수동으로 복사/붙여넣기해야 한다. 공통 정책이 바뀌면 모든 JSON 파일을 찾아서 고쳐야 하고, 빠뜨리면 서비스별 권한이 어긋난다. source_policy_documents는 이 문제를 코드 레벨에서 해결한다.
크로스 어카운트 접근
이 플랫폼에는 다른 AWS 계정에서 발행되는 SNS 토픽을 구독하는 SQS 큐도 있었다. 이런 크로스 어카운트 접근은 양쪽 계정에 정책이 필요하다:
data "aws_iam_policy_document" "external_event_queue_policy" {
statement {
sid = "AllowCrossAccountSnsPublish"
effect = "Allow"
actions = ["sqs:SendMessage"]
principals {
type = "Service"
identifiers = ["sns.amazonaws.com"]
}
resources = [
aws_sqs_queue.external_event_queue.arn
]
condition {
test = "ArnEquals"
variable = "aws:SourceArn"
values = [
"arn:aws:sns:${var.region}:${var.shared_account_id}:external-data-*"
]
}
}
}condition 블록으로 “이 특정 SNS 토픽에서 온 메시지만 허용”이라는 제약을 명시할 수 있다. 와일드카드로 sqs:*를 열어두는 것과 비교하면, 접근 경로가 코드에서 읽힌다는 점이 핵심이다.Before / After 비교
| 측면 | Before (와일드카드) | After (서비스별 분리) |
|---|---|---|
| SQS 권한 | sqs:* on admin-portal-* | sqs:ReceiveMessage, sqs:DeleteMessage on 특정 큐 이름 |
| S3 권한 | s3:* on 전체 버킷 | s3:PutObject, s3:GetObject on export 전용 경로 |
| 권한 공유 | 모든 서비스가 동일한 정책 | identity-service는 export 큐에 접근 불가 |
| 정책 변경 | JSON 복사/붙여넣기, 누락 위험 | source_policy_documents 조합, 공통 정책 한 곳에서 관리 |
| 감사 대응 | ”왜 이 서비스에 이 권한이 있나?” 설명 불가 | sid와 코드 구조로 목적 설명 가능 |
이 리팩토링이 운영에 준 영향
- 서비스별 권한 경계를 설명하기 쉬워졌다 — 감사나 보안 점검에서 “이 서비스가 왜 이 권한을 갖고 있나?”에 대한 답이 코드에 있다.
- NPE의 서비스 어카운트에 정확히 필요한 권한만 매핑할 수 있었다.
- 이후 Audit Event 파이프라인에서 SNS, SQS, EventBridge 권한을 추가할 때도 같은 패턴으로 확장했다.
- 새 서비스를 추가할 때 “공통 정책 + 서비스 고유 정책”이라는 템플릿이 이미 있어서 시작이 빨랐다.
물론 최소 권한 원칙이 항상 편한 건 아니다. 디버깅할 때 임시로 넓은 권한이 필요한 순간도 있고, 정책을 너무 잘게 쪼개면 전체 그림이 오히려 안 보인다. 새 큐가 추가되거나 크로스 어카운트 구독이 바뀌면 Terraform도 함께 손봐야 한다.
그래도 NPE 도입이 진짜 남긴 변화 중 하나는 권한을 서비스 단위로 다시 읽게 만들었다는 점이다. IAM을 다시 설계하고 나서야 플랫폼은 “같은 서버 안에 있던 서비스들”이 아니라 **“서로 다른 책임을 가진 실행 단위들”**처럼 보이기 시작했다.
aws_iam_policy_document의 source_policy_documents 조합은 단순한 Terraform 팁이 아니라, **“공통 규칙은 한 곳에서 관리하고, 서비스별 차이만 각자 선언한다”**는 설계 원칙을 코드로 표현한 것이다.
다음 시리즈에서는 기반 정비를 마친 뒤, 관리자 플랫폼 자체의 권한 모델과 운영 관측성을 어떻게 다시 정리했는지 이어서 본다.