~/.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, 등록 통로는 열어 둔다. - 검증 도구도 틀린다. 개수를 찍어 보고, 대조군을 둔다.
다음 편은 운영 전환이다. 인증을 앞단으로 옮기고, 알람을 걸고, 다음 사람이 첫날 무엇을 읽어야 하는지까지 정리했다.
'AWS' 카테고리의 다른 글
| CloudFront 캐시 무효화 비용 — 파일 수가 아니라 경로 수로 센다 (0) | 2026.09.18 |
|---|---|
| 계정 이관 마지막 열흘: 인증 앞단화, 알람, 그리고 문서 (0) | 2026.09.15 |
| Aurora 메이저 버전 업그레이드는 성공한 다음에 터진다 (0) | 2026.09.09 |
| S3 스토리지 클래스 전환 비용 계산 — 작은 객체는 옮길수록 손해다 (0) | 2026.09.07 |
| ALB 액세스 로그가 꺼져 있으면 야간 트래픽 9.5배도 못 읽는다 (0) | 2026.09.05 |
댓글