본문 바로가기
AWS

장기 액세스 키 대신 브리지 프로필로 Terraform 돌리기

by 꼼냥냥 2026. 9. 11.
728x90

~/.aws/credentials에 든 장기 액세스 키의 문제는 유출 가능성 자체가 아니다. 유출을 알아챌 방법이 없다는 것이다. 만료가 없으니 계속 통하고, CloudTrail에는 사람이 아니라 IAM 사용자 이름만 남으며, 회수하려면 그 키를 쓰는 사람 전원이 다시 설정해야 한다. 그래서 사람에게는 키를 주지 않기로 했다. 그리고 이 편에서 제일 아찔했던 건 정책이 아니라 검증 도구가 틀린 것이었다.

왜 없앴나

장기 키의 문제를 하나씩 보면 이렇다.

  • 만료가 없으니 계속 통한다
  • CloudTrail에는 사람이 아니라 그 IAM 사용자 이름만 남는다. 누가 썼는지 모른다
  • 회수하려면 그 키를 쓰는 사람 전원이 다시 설정해야 한다. 한 사람만 뺄 수가 없다

세 번째가 실무에서 제일 크다. 사람 하나가 나갈 때 키를 못 돌린다.

구조

개인 로그인 (임시 세션)
        │
        ▼
   브리지 프로필  ── credential_process ──▶ CLI 세션을 표준 형식으로 변환
        │
        ├──▶ terraform-read   (plan 전용)
        └──▶ terraform-write  (apply 전용)

브리지 프로필이 왜 필요한가

이게 이 구조에서 제일 안 알려진 부분이다.

최신 AWS CLI가 만드는 로그인 세션은 CLI 안에서만 읽히는 형식이다. CLI는 알지만 번들된 Go SDK는 모른다. 그래서 Terraform이 그대로 못 읽는다.

CLI로 임시 자격증명을 뽑아 표준 형식으로 넘겨주는 역할만 하는 프로필을 하나 둔다.

[profile bridge]
region = <리전>
credential_process = aws configure export-credentials --profile <login> --format process

[profile terraform-read]
source_profile = bridge
role_arn       = arn:aws:iam::<account>:role/terraform-read

[profile terraform-write]
source_profile = bridge
role_arn       = arn:aws:iam::<account>:role/terraform-write

부수 효과가 하나 있다. 자바든 Go든 다른 SDK도 이 프로필을 그대로 쓴다. 언어별로 자격증명을 따로 챙길 필요가 없어졌다.

읽기와 쓰기를 나눈 이유

plan은 읽기, apply는 쓰기. 습관적으로 apply를 치는 사고를 막기 위해서다. (1편에서 apply -auto-approve가 배포 중이던 타깃을 도로 등록한 게 이런 사고다.) 읽기 프로필로는 애초에 권한이 없어 실패한다.

state 백엔드는 예외다. plan도 state를 잠그느라 쓰기가 필요해서, 백엔드에는 쓰기 프로필을 박아 두고 인프라 프로바이더 쪽만 환경변수로 갈아끼운다.

AWS_PROFILE=terraform-read  terraform -chdir=envs/<region> plan
AWS_PROFILE=terraform-write terraform -chdir=envs/<region> apply

한 번 데었다. cd로 디렉터리를 옮겨 두고 셸을 재사용하다가 디렉터리는 A인데 프로필은 B인 조합이 만들어졌다. 그 뒤로 AWS_PROFILE과 -chdir을 항상 같이 쓴다. 타이핑이 길어지는 대신 사고가 없다.

옮기면서 나온 것들

역할 세션은 항상 1시간이다

임시 자격증명으로 역할을 맡으면 역할 체이닝 상한이 걸려서, MaxSessionDuration을 얼마로 두든 1시간에 잘린다.

긴 apply 중간에 끊긴다. 긴 작업 전에는 로그인을 새로 한다.

신뢰 정책에 사용자 ARN을 직접 쓰지 않는다

이게 가장 무섭다.

신뢰 정책에 특정 사용자 ARN을 적어 두면, 그 사용자를 지우는 순간 AWS가 ARN을 내부 고유 ID로 치환한다. 결과는 아무도 못 맡는 역할이다.

그리고 같은 이름으로 다시 만들어도 안 붙는다. 고유 ID가 다르기 때문이다.

Principal: root + 그룹/조건식으로 관문을 옮긴다.

PowerUserAccess는 IAM을 통째로 뺀다

NotAction: ["iam:*", ...]이고 예외는 CreateServiceLinkedRole, DeleteServiceLinkedRole, ListRoles뿐이다. 즉 iam:GetRole이 없다.

그래서 data "aws_iam_role" 하나가 AccessDenied로 죽으면서 전체 plan이 종료코드 1로 끝난다.

고약한 건 타이밍이다.

  • 읽기 프로필(ReadOnlyAccess)로는 통과한다 → 평소 plan에서 안 보인다
  • 쓰기로 apply할 때 처음 드러난다

같은 뿌리로 IAM 생성도 막힌다. CreateGroup·CreatePolicy가 없어서 Terraform으로 그룹·정책을 만들 수 없다. 필요한 만큼 좁힌 인라인 정책을 따로 붙였다.

aws login은 콘솔 액세스가 켜진 사용자만 된다

콘솔 로그인 세션을 근거로 임시 자격증명을 받는 구조라, 비밀번호가 없는 사용자는 그룹·정책이 다 맞아도 CLI가 안 된다.

증상이 권한 오류처럼 보여서 엉뚱한 데를 찾게 된다.

권한을 좁힐 때 놓치기 쉬운 것

iam:AttachGroupPolicy는 그룹 ARN만 검사한다

Resource를 그룹 하나로 좁혀도 거기에 AdministratorAccess를 붙일 수 있다. "그룹 하나만 만질 수 있게 했다"가 곧 "권한 상승 불가"가 아니다. 2편의 포트포워딩 문서와 같은 구조다 — 권한은 무엇을 할 수 있는가가 아니라 어디까지 닿는가로 읽어야 한다.

붙일 수 있는 정책까지 막으려면 조건을 함께 건다.

"Condition": { "ArnEquals": { "iam:PolicyARN": [ "...허용할 정책들..." ] } }

MFA 강제는 두 군데를 틀리기 쉽다

하나 — 등록 통로를 열어 둬야 한다. 안 그러면 MFA를 등록하려면 MFA가 있어야 하는 잠금이 된다. NotAction으로 연다.

둘 — 조건은 Bool이 아니라 BoolIfExists. Bool은 컨텍스트 키가 아예 없는 요청에서 조건이 안 맞아 Deny를 건너뛴다. 막으려던 요청이 그대로 통과한다.

거는 것보다 먼저, 실제 값을 확인한다

aws:MultiFactorAuthPresent 조건을 걸면 CLI가 막힐까 봐 걱정했는데, 안 막혔다.

aws login 세션은 MFA 컨텍스트를 CLI 호출에도 싣는다. CloudTrail의 userIdentity.sessionContext.attributes.mfaAuthenticated가 aws-cli userAgent 호출에서도 true였다.

조건을 걸기 전에 이 필드로 먼저 확인한다. 추측으로 정하면 둘 중 하나다 — 겁나서 안 걸거나, 걸고 나서 다 막히거나.

검증 도구도 틀린다

이 편에서 제일 아찔했던 건 정책이 아니라 검증이 틀린 것이었다.

while read가 마지막 줄을 버렸다

정책 3개를 시뮬레이터에 넘겼는데 2개만 갔다. 파일 끝에 개행이 없었기 때문이다.

그리고 빠진 게 하필 Deny 정책이었다. 결과는 "MFA 없이도 허용된다"는 정반대 결론이었다.

zsh라면 ${(f)$(<file)}로 읽으면 영향이 없다. 어느 쪽이든 넘긴 개수를 찍어 보기 전에는 못 알아챈다.

시뮬레이터가 변수를 안 풀어 준다

simulate-custom-policy는 ${aws:username}을 --caller-arn에서 안 풀어 준다. --context-entries로 따로 넣어야 한다.

안 넣으면 본인 리소스 정책이 implicitDeny로 나와서 정책이 잘못된 것처럼 보인다.

인증 검사에는 대조군을 둔다

저장소가 공개인지 git ls-remote로 봤는데, macOS keychain에 저장된 자격증명을 조용히 써서 비공개 저장소가 "공개"로 나왔다.

git -c credential.helper= ls-remote <url>

헬퍼를 끄고, 확실한 공개 저장소와 없는 저장소를 같이 돌려 검사 자체가 동작하는지 확인한다. 대조군 없는 검증은 검증이 아니다.

여담 — 설정 파일에 한글 주석을 달았다가 CLI가 죽었다

[ERROR]: Unable to parse config file: ~/.aws/config

파일은 UTF-8인데 시스템 기본 인코딩이 다른 환경이었다. 구조는 멀쩡했고 주석 한 줄 때문에 파싱이 통째로 실패했다.

지금은 이 파일 주석을 ASCII로만 쓴다. ASCII는 어떤 인코딩으로 읽어도 같으니까.

정리

  • 정적 키의 문제는 유출이 아니라 유출을 모른다는 것이다.
  • 브리지 프로필을 두면 Terraform과 다른 SDK가 같은 자격증명을 쓴다.
  • 역할 세션은 1시간. 긴 apply 전에 재로그인.
  • 신뢰 정책에 사용자 ARN을 쓰지 않는다. 지우면 복구가 안 된다.
  • PowerUserAccess는 IAM을 통째로 뺀다. 쓰기로 apply할 때 처음 드러난다.
  • MFA 조건은 BoolIfExists, 등록 통로는 열어 둔다.
  • 검증 도구도 틀린다. 개수를 찍어 보고, 대조군을 둔다.

다음 편은 운영 전환이다. 인증을 앞단으로 옮기고, 알람을 걸고, 다음 사람이 첫날 무엇을 읽어야 하는지까지 정리했다.

댓글