Terraform 레포 public 공개 여부에 대한 조사 - ARN 노출을 중심으로Terraform 레포 public 공개 여부에 대한 조사 - ARN 노출을 중심으로

Terraform 레포 public 공개 여부에 대한 조사 - ARN 노출을 중심으로

생성 일시
Sep 10, 2026 08:51 AM
최종 편집 일시
Last updated September 10, 2026
태그
terraform
AWS
IAM
GitHub Actions
작성자
@박진혁
Date

멘토 피드백에서 팀 인프라 레포(kintoun-infra)를 포트폴리오용으로 public 전환하는 게 권장됐다. 컴플라이언스 파트에서 사전 검토를 먼저 진행했고, 파일·코드 수준에서는 민감정보가 없다는 결론이 나왔다. 다만 검토 과정에서 한 가지 질문이 남았다.
Terraform 코드와 CI 로그에 남는 ARN은 지워야 하는 값인가?
"당연히 지워야지"라고 답하기는 쉽다. 하지만 그렇게 결론 내리면 왜 지워야 하는지, 지우고 나면 안전해지는지에 대해 아무것도 설명하지 못한다. AWS 공식 문서와 실제 공격 기법을 근거로 다시 따져봤다.

TL;DR

  • AWS 공식 문서는 ARN과 계정 ID를 비밀·민감·기밀 정보로 분류하지 않는다.
  • 그렇다고 "노출돼도 무해"한 건 아니다. 정확한 표현은 "노출을 전제로 방어가 성립해야 한다"이다.
  • ARN을 알아야만 성립하는 공격 경로는 실제로 존재한다. 하지만 그 경로들의 취약점은 ARN 노출이 아니라 리소스 정책·신뢰 정책의 조건 부재다.
  • 따라서 조치는 "ARN을 지운다"가 아니라 "ARN이 공개돼도 무해한 상태인지 검증한다"가 되어야 한다.

1. AWS는 뭐라고 하나

IAM 사용자 가이드의 ARN 설명 문단에 이런 문장이 있다.
ARN은 다른 식별 정보와 마찬가지로 신중하게 사용하고 공유해야 하지만, 비밀·민감·기밀 정보로 간주되지 않습니다.
계정 ID를 설명하는 Account Management 문서에도 토씨까지 같은 문장이 들어 있다. 우연이 아니다. 이 문구는 2022년 논쟁 이후 AWS가 문서를 명확히 하겠다고 밝히면서 정리된 결과다.
re:Post의 관련 아티클은 계정 ID 노출을 낮은 위험으로 분류하면서, 액세스 키 전면 회전이나 계정 이관 같은 대응은 과잉이라고 명시한다. 계정 ID를 전화번호에 비유한 표현이 적절하다. 나를 식별하지만, 안다고 해서 우리 집에 들어올 수 있는 건 아니다.
실제로 공개 생태계를 보면 이건 예외가 아니라 관행에 가깝다.
  • Datadog 같은 SaaS 벤더는 크로스 계정 신뢰 관계 설정을 위해 자사 계정 ID를 공식 문서에 대놓고 공개한다. Scott Piper는 이런 벤더 계정 ID 목록을 별도로 관리해 왔다.
  • Corey Quinn은 논쟁을 끝내겠다며 본인 계정 ID를 블로그 본문에 그대로 게시했다.
  • Trivy 프로젝트는 "계정 ID를 시크릿으로 탐지하는 건 오탐"이라는 이슈를 통해 영향도를 낮음으로 정리했다.
AWS 공식 문서와 예제가 111122223333 같은 placeholder를 쓰는 것은 사실이다. 다만 이것이 민감정보라서인지, 독자가 실제 값으로 오인해 그대로 복사하는 것을 막기 위해서인지는 AWS가 명시한 적이 없다. 문서 본문의 명시적 문장이 더 강한 근거라고 봤다.

2. 반대 진영의 주장도 틀리지 않았다

Plerion은 "계정 ID는 사실상 시크릿"이라는 반론을 냈다. 논지는 이렇다. AWS에서 뭔가를 공격하려면 대상이 필요하다. 다른 계정의 역할을 assume하려면 그 역할 ARN이 필요하고, 남의 SNS 토픽에 메시지를 밀어넣으려면 토픽 ARN이 필요하다. 그리고 계정 ID는 바꿀 수가 없다. 변경 불가능한 값을 비밀로 취급하는 건 Kerckhoffs 원칙을 정면으로 어기는 설계다.
여기서 흥미로운 지점은, 양쪽 다 맞다는 것이다. 논쟁을 정리하려면 "비밀인가?"가 아니라 "노출됐을 때 무슨 일이 벌어지나?"를 물어야 한다. 실제 공격 경로를 하나씩 뜯어보면 답이 나온다.
공격 경로
성립 조건
실제 통제 지점
계정 ID로 퍼블릭 EBS/RDS 스냅샷·AMI 발견
해당 계정에 퍼블릭 공유된 스냅샷·AMI가 존재
스냅샷·AMI 퍼블릭 공유 차단
역할 ARN을 서드파티 포털에 제출하는 confused deputy
신뢰 정책에 sts:ExternalId 등 조건 부재 + 외부 SaaS 연동 존재
신뢰 정책 조건
UpdateAssumeRolePolicy 반복 호출로 principal 존재 여부 열거
공격자가 자신의 AWS 자격증명 보유 + 대상 계정 ID 인지
역할 이름 예측 불가능화
SNS/SQS에 외부에서 publish·receive
리소스 정책 Principal이 * 또는 과다 허용
리소스 정책
ECR 엔드포인트 식별 (<account>.dkr.ecr.<region>.amazonaws.com)
ECR 사용 중
인증
세 번째 항목이 특히 시사적이다. Pacu의 iam__enum_roles 모듈은 역할 신뢰 정책에 존재하지 않는 principal ARN을 넣으면 IAM이 MalformedPolicyDocumentException을 반환한다는 성질을 이용해, 대상 계정에 어떤 역할·사용자가 존재하는지 인증 없이 열거한다. 그런데 이 기법은 계정 ID만 있으면 성립한다. 코드에서 ARN을 지워도 방어되지 않는다는 뜻이다.
정리하면, 모든 경로에서 ARN 노출은 발견 비용을 낮추는 역할일 뿐 취약점 자체가 아니다. 취약점은 항상 정책 조건 쪽에 있다.

3. ARN 종류별 위험도

그렇다고 모든 ARN이 동등한 건 아니다. 종류별로 나눠봤다.
구분
위험도
판단 근거
계정 ID 포함 ARN
낮음
공식적으로 비밀 아님. 다만 재발급 불가라 불필요한 노출은 지양
IAM Role ARN
낮음~중간
ARN 자체는 무해. 신뢰 정책에 조건이 없거나 이름이 관례적으로 추측 가능하면 중간
IAM Policy ARN
낮음
ARN이 아니라 정책 문서 내용의 과다 권한이 실제 문제. 정책 JSON 공개는 오히려 외부 검토를 받을 수 있는 이점
S3 버킷 이름·ARN
중간
버킷명은 전역 네임스페이스라 열거 대상. 특히 로그 버킷명 공개는 공격자에게 "지워야 할 대상"을 알려주는 효과
KMS Key ARN
낮음~중간
키 정책이 통제. 공개 여부보다 키 정책의 Principal 범위가 중요
SNS·SQS ARN
중간
리소스 정책이 개방적이면 외부에서 메시지 주입 가능. 탐지 알림 채널에 오탐을 밀어넣을 수 있다
Lambda ARN
낮음
함수 리소스 정책이 통제
액세스 키 / 사설키
치명
즉시 유출 사고. 커밋 이력에 남으면 삭제해도 복구 불가
tfstate / tfvars
치명
모든 리소스 속성이 평문. sensitive = true는 출력만 가리지 저장 계층을 보호하지 않는다

공개해도 되는 것 / 반드시 빼야 하는 것

공개해도 되는 것: ARN 구조, IAM 역할·그룹·정책 이름, 리전, 리소스 타입, 태그 스킴, IAM 정책 JSON, Terraform 리소스 정의와 모듈 구조
반드시 빼야 하는 것:
*.tfstate *.tfstate.* *.tfvars # example.tfvars만 placeholder로 유지 **/.terraform/* crash.log crash.*.log tfplan
그 외 액세스 키, 사설키, DB 패스워드, 외부 통합 시크릿, 웹훅 URL, 팀원 개인정보.
"보안"이 아닌 다른 이유로 변수화를 권하는 것: 계정 ID 하드코딩은 data.aws_caller_identity.current.account_id로 대체하는 게 낫다. 다만 근거는 보안이 아니라 코드 재사용성과 다중 계정 대응이다. 근거를 정확히 붙이는 게 중요하다. 보안을 이유로 들면 나중에 "그럼 로그에 찍히는 건 왜 괜찮냐"는 질문에 답할 수 없게 된다.

4. 실제로 남은 문제: Variables는 마스킹되지 않는다

내부 검토에서 실질적으로 걸린 건 여기였다.
우리 워크플로는 OIDC로 AWS에 인증한다. 장기 액세스 키가 아예 없다는 점에서 구조 자체는 모범적이다.
- uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ vars.AWS_APPLY_ROLE_ARN }}
문제는 vars다. GitHub은 Secrets는 로그 출력 시 자동으로 *** 마스킹하지만, Variables는 마스킹하지 않는다. 즉 코드에 값이 없어도 워크플로가 돌 때마다 실행 로그에 역할 ARN이 평문으로 남는다. 그리고 레포를 public으로 전환하면 과거에 쌓인 Actions 실행 기록 전체도 함께 공개된다. grep으로는 절대 못 잡는 부분이다.
사실 확인은 정확하다. 그런데 "Secrets로 옮기자"는 근본 조치가 아니다.
이 역할에 대한 실제 접근 통제는 IAM 역할의 OIDC 신뢰 정책 조건이다. token.actions.githubusercontent.com:sub가 해당 레포로 한정되고 :aud가 sts.amazonaws.com으로 고정돼 있다면, ARN이 전 세계에 공개돼도 그 레포의 워크플로 외에는 누구도 이 역할을 assume할 수 없다. 이 글 첫머리의 프레이밍이 실제로 성립하는 사례다.
반대로 sub 조건이 없거나 와일드카드(repo:org/*)라면, ARN 공개 여부와 무관하게 다른 레포나 포크에서 역할을 탈취할 수 있는 심각한 결함이다. 이 경우에도 조치는 ARN 은닉이 아니라 조건 수정이다.
Secrets 이전에는 부작용도 있다. 마스킹이 걸리면 CI 디버깅 때 어느 역할로 인증했는지 로그에서 확인할 수 없고, GitHub 마스킹은 값이 분할·변형되어 출력되면 동작하지 않는 부분적 방어다. 계정 ID가 다른 문맥에 등장할 때 예기치 않게 ***로 치환되는 문제도 있다.
그래서 순서를 이렇게 잡았다.
  1. 신뢰 정책의 sub / aud 조건 검증 (선행, 필수)
  1. 조건이 적절하면 → Variables 유지. 판단 근거를 문서로 남긴다
  1. 조건이 부적절하면 → 조건 수정이 최우선. ARN 은닉은 부수적
한 가지 함정. 2026년 7월 15일 이후 생성된 레포는 owner ID와 repository ID를 포함하는 immutable sub 형식을 기본값으로 쓴다. 레포 생성 시점에 따라 신뢰 정책에 적어야 할 값의 형식이 달라지므로, 문서만 보고 쓰지 말고 실제 발급되는 토큰의 claim을 직접 확인하는 편이 안전하다.

5. 내부 검토에서 다루지 않았던 것: 포크 PR

terraform-plan.yml은 pull_request 이벤트로 돈다. 레포가 public이 되면 누구나 포크해서 임의의 HCL이 담긴 PR을 열 수 있다.
terraform plan은 생각보다 위험한 명령이다. external 데이터 소스나 커스텀 프로바이더를 통해 임의 코드 실행이 가능하다. 포크 PR에 AWS 자격증명이 전달되면 사실상 RCE다.
다행히 GitHub의 기본 동작은 안전한 편이다.
  • 포크 PR은 레포·조직 Secrets에 접근할 수 없다 (승인 후에도 마찬가지다)
  • GITHUB_TOKEN이 읽기 전용으로 강등된다
  • public 레포에서 최초 기여자의 워크플로는 승인 후에만 실행된다
다만 확인해야 할 지점이 있다.
  • 트리거가 pull_request인지. pull_request_target이면 base 브랜치 컨텍스트로 승인 설정과 무관하게 항상 실행된다. 여기에 PR 코드 checkout이 붙으면 치명적이다.
  • Actions 설정의 승인 정책을 최초 기여자 한정에서 모든 외부 기여자로 올릴 것.
  • Plan 역할의 권한이 실제로 읽기 전용인지. 위 두 가지가 뚫렸을 때의 최후 봉쇄선이다.
부수적으로, conftest/OPA 정책 파일과 IAM 가드레일 검사 로직이 공개되면 가드레일이 잡지 못하는 영역을 역산할 수 있다는 점도 있다. 큰 문제는 아니지만, 프로젝트 후반에 의도된 취약점을 배치하는 단계에서는 고려 요소다.

6. 남은 하나: 커밋 히스토리

지금까지의 검토는 전부 현재 시점의 파일 상태에 대한 것이다. git은 지운 값도 히스토리에 그대로 남긴다. 과거 어느 시점에 실수로 액세스 키나 실제 .tfvars를 커밋했다가 나중에 지웠더라도 git log와 커밋 diff로 여전히 조회된다.
이번 프로젝트가 재현하려는 시나리오 자체가 "하드코딩된 자격증명이 공개 저장소를 통해 유출되는 경로"라는 걸 생각하면, 같은 실수를 우리 레포에서 반복하는 건 좀 그렇다. public 전환 전에 gitleaks나 trufflehog로 전체 히스토리를 한 번 훑는 게 맞다.
gitleaks detect --log-opts="--all"
현재 파일이 깨끗한 것과 히스토리 전체가 깨끗한 것은 다른 문제다.

마무리

이 조사에서 제일 크게 배운 건 결론 자체보다 결론을 내리는 방식이었다.
"ARN은 공개하면 안 된다"는 외우기 쉽고 지키기 쉽다. 하지만 이걸 그대로 적용하면 실제 통제인 신뢰 정책 조건은 손도 안 대고, 로그만 가린 채 "조치 완료"라고 적게 된다. 겉으로는 지적사항이 닫히지만 공격 표면은 그대로다.
보안 조치를 정할 때 한 번 더 물어볼 만한 질문이 있다. 이 값이 공격자에게 넘어갔다고 가정하면, 그래도 시스템이 버티나? 버틴다면 그 값은 굳이 숨길 필요가 없다. 버티지 못한다면 숨기는 것으로는 부족하다.

참고 자료