Skip to Content
Infra & DevOpsNPE 도입이 드러낸 IAM 정책의 민낯
☁️ Infra & DevOps2025년 4월 10일

NPE 도입이 드러낸 IAM 정책의 민낯

#identity-platform#terraform#iam#aws#security#npe
Identity Platform · Series 1 · Platform Modernization4 / 15Infra & DevOps
NPE 도입을 계기로, 와일드카드 IAM 정책을 서비스별로 분리하고 Terraform 문서 조합으로 재구성한 과정.
Note

AWS IAM(Identity and Access Management)은 “누가 어떤 AWS 자원에 접근할 수 있는가”를 제어하는 서비스다. 서비스에 **역할(Role)**을 부여하고, 그 역할에 **정책(Policy)**을 붙여서 S3, SQS 같은 자원 접근을 제한한다. 최소 권한 원칙(Least Privilege) — 실제로 필요한 권한만 부여 — 이 핵심이다.

ECS에서는 왜 그냥 돌아갔나

ECS 환경에서는 크게 두 종류의 IAM 역할이 있었다.

역할용도
Task Execution RoleECR에서 이미지 풀, 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 역할을 부여할 수 있다는 것이다.

plaintext
ECS: Task Definition → Task Role (하나의 넓은 정책) NPE: Pod → Service Account → IAM Role (서비스별 최소 권한)

NPE에 서비스를 올리려면 각 서비스의 서비스 어카운트에 정확히 필요한 권한만 매핑해야 했다. 기존처럼 sqs:*를 넣으면 플랫폼 리뷰에서 거부된다. 이 요구사항이 기존 IAM 정책의 문제를 한꺼번에 드러냈다.

와일드카드의 실체

기존 정책을 열어보면 이런 식이었다:

json
conceptual/legacy-policy.json
{ "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)를 넣어서 각 권한의 목적을 코드로 설명할 수 있다.

공통 정책과 서비스별 정책 분리

hcl
conceptual/modules/iam/common.tf
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로 가져와 조합하는 구조다.

plaintext
서비스 A 정책 = common_logging + common_parameter_store + 서비스 A 고유 권한 서비스 B 정책 = common_logging + common_parameter_store + 서비스 B 고유 권한

JSON으로 이걸 하려면 statement 배열을 수동으로 복사/붙여넣기해야 한다. 공통 정책이 바뀌면 모든 JSON 파일을 찾아서 고쳐야 하고, 빠뜨리면 서비스별 권한이 어긋난다. source_policy_documents는 이 문제를 코드 레벨에서 해결한다.

크로스 어카운트 접근

이 플랫폼에는 다른 AWS 계정에서 발행되는 SNS 토픽을 구독하는 SQS 큐도 있었다. 이런 크로스 어카운트 접근은 양쪽 계정에 정책이 필요하다:

hcl
conceptual/modules/iam/cross-account-subscription.tf
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_documentsource_policy_documents 조합은 단순한 Terraform 팁이 아니라, **“공통 규칙은 한 곳에서 관리하고, 서비스별 차이만 각자 선언한다”**는 설계 원칙을 코드로 표현한 것이다.

다음 시리즈에서는 기반 정비를 마친 뒤, 관리자 플랫폼 자체의 권한 모델과 운영 관측성을 어떻게 다시 정리했는지 이어서 본다.

Last updated on