장기 Access Key
수동 비활성화나 교체 전까지 계속 유효하다. 공개 저장소에 노출되면 자동 수집과 abuse가 분 단위로 시작될 수 있고, 내부 로그나 private repo 유출은 발견이 훨씬 늦어질 수 있다.
임시 자격증명, federation, Roles Anywhere를 전혀 쓸 수 없는 예외적인 외부 시스템분류: Layer 3 - AWS 인프라 & 보안
AWS IAM(Identity and Access Management)은 “누가 AWS의 어떤 리소스에 어떤 작업을 할 수 있는지”를 결정하는 권한 시스템이다. 여기서 “누구”는 사람만이 아니라 ECS 태스크, Lambda 함수, GitHub Actions 워크플로처럼 AWS API를 호출하는 모든 주체를 포함한다.
IAM을 처음 볼 때는 기능 목록보다 한 문장을 먼저 잡는 편이 좋다. IAM은 자격증명(credentials)을 확인하고, 정책(policy)을 평가해, API 호출을 허용하거나 거부한다. EC2를 생성하는 콘솔 클릭, S3 객체 업로드, CI/CD 배포 명령, 애플리케이션의 SQS 전송은 모두 이 흐름을 지난다.
보안 경계도 여기서 바로 나온다. 브라우저에 내려가는 번들에는 AWS Access Key를 절대 넣지 않는다. 클라이언트는 애플리케이션 서버에 요청하고, AWS 접근은 서버 워크로드에 붙은 IAM Role이 맡게 한다.
AWS에서 권한은 부가 기능이 아니라 기본 전제다. 권한이 너무 좁으면 배포와 운영이 멈추고, 너무 넓으면 한 번 유출된 자격증명이 계정 전체 사고로 커진다. IAM 학습의 목표는 “AccessDenied를 없애는 법”이 아니라 허용해야 할 요청만 설명 가능한 방식으로 허용하는 법을 익히는 것이다.
아래 수치는 특정 보고서와 집계 자료 기준이므로 “최신 정답”이 아니라 위험의 크기를 잡는 참고치로 읽는다.
학습 관점에서 중요한 결론은 하나다. IAM 실수는 단순한 “권한 에러”가 아니라 누가 무엇을 할 수 있는지의 모델이 무너진 신호다. 그래서 IAM 문서는 명령어보다 모델을 먼저 익혀야 한다.
IAM은 AWS 계정에 “여러 주체”와 “주체별 권한”을 도입하기 위해 등장했다. AWS의 회고 글에 따르면 IAM은 2010년 preview, 2011년 GA로 공개됐다 (AWS — Happy 10th Birthday IAM). 그 전에는 AWS 계정 소유자(root)의 이메일/비밀번호와 단일 Access Key 1쌍이 사실상 계정 접근의 중심이었다. root 계정은 AWS 계정의 최상위 소유자이므로 일상 작업자가 공유해서 쓰는 자격증명이 되면 안 된다.
root-only 방식은 작게는 단순해 보인다. 한 사람이 혼자 테스트 계정에서 S3 버킷 하나를 만질 때는 root만 있어도 일이 된다. 하지만 팀과 서비스가 늘어나는 순간 세 가지가 동시에 깨진다.
| 깨지는 지점 | root-only에서 생기는 문제 | IAM이 만든 해법 |
|---|---|---|
| 권한 세분화 | 10명이 한 root 자격증명을 공유하면 모두가 계정 전체 권한을 가진다. | User, Group, Role로 주체를 나누고 Policy로 권한 범위를 좁힌다 |
| 키 교체 비용 | 1명 퇴사 때 root 비밀번호와 Access Key를 모든 노트북, CI, 서버에서 동시에 바꿔야 한다. | 사람/서비스 단위로 자격증명을 회수하거나 Role 세션을 만료한다 |
| 감사 추적 | CloudTrail에 같은 root 주체만 남으면 “누가 삭제했는지”를 끝까지 구분할 수 없다. | userIdentity.arn으로 호출 주체를 분리해 추적한다 |
| 워크로드 권한 분리 | EC2, ECS, Lambda가 모두 같은 장기 키를 쓰면 한 서비스 침해가 전체 계정 침해로 번질 수 있다. | 서비스별 IAM Role과 임시 자격증명으로 노출창을 줄인다 |
CloudTrail은 AWS 계정에서 발생한 API 호출 이력을 남기는 감사 로그 서비스다. IAM을 공부할 때 CloudTrail을 함께 보는 이유는 권한 모델이 “누가 어떤 API를 호출했는가”라는 기록으로 검증되기 때문이다.
이 lineage가 이 문서 전체의 중심이다. IAM은 단순히 “권한 메뉴”가 아니라 AWS에 주체(subject), 리소스(resource), 작업(action), **조건(condition)**이라는 축을 만든 시스템이다. 뒤에서 보는 ARN(Amazon Resource Name) 패턴, Role 신뢰 정책(Trust Policy), 임시 자격증명 발급 서비스인 STS(Security Token Service), 조직 상한선인 SCP(Service Control Policy), 외부 토큰 신뢰 방식인 OIDC(OpenID Connect)도 모두 root-only 구조의 한계를 줄이기 위한 후속 장치다.
처음 읽을 때는 용어를 한꺼번에 외우지 말고 요청 한 건이 어떤 요소로 쪼개지는지부터 본다.
주체(Principal) └─ 자격증명으로 자신을 증명한다 └─ 정책(Policy)이 Action, Resource, Condition을 평가한다 └─ AWS API 요청이 Allow 또는 Deny로 결정된다| 용어 | 첫 정의 | 처음 잡아야 할 경계 |
|---|---|---|
| IAM | Identity and Access Management. AWS API 호출의 주체와 권한을 관리하는 서비스다. | 리소스를 직접 실행하는 서비스가 아니라, 실행 가능 여부를 판정하는 권한 계층이다. |
| Principal | 요청을 보내는 주체다. IAM User, IAM Role, AWS 서비스, 외부 로그인 시스템에서 들어온 세션이 될 수 있다. | 사람 이름과 같지 않다. 같은 사람이 여러 Role 세션을 쓸 수 있다. |
| IdP / federation | IdP(Identity Provider)는 사용자를 인증하는 외부 로그인 시스템이고, federation은 그 인증 결과를 AWS가 신뢰해 임시 자격증명을 발급하는 방식이다. | AWS가 비밀번호 원본을 직접 보관하지 않아도 되지만, 신뢰 조건을 좁혀야 한다. |
| IAM User | AWS 계정 안에 만든 장기 사용자다. 비밀번호와 Access Key를 가질 수 있다. | 일반 사람 접근의 기본값으로 두기보다 federation이나 중앙 SSO 계층을 우선 검토한다. |
| IAM Group | 여러 User에 같은 정책을 붙이기 위한 묶음이다. | Role은 Group에 들어가지 않는다. Group은 User 관리 도구다. |
| IAM Role | 직접 로그인하는 사용자가 아니라, 누군가가 임시로 assume해서 쓰는 권한 그릇이다. | Role에는 “무엇을 할 수 있는지”와 “누가 맡을 수 있는지”가 모두 필요하다. |
| Policy | Allow 또는 Deny 규칙을 담은 JSON 문서다. | Policy 자체는 실행 주체가 아니다. User나 Role 등에 붙어야 효과가 난다. |
| ARN | Amazon Resource Name. AWS 리소스를 전역적으로 가리키는 문자열이다. 예: arn:aws:s3:::my-bucket/*. | S3처럼 버킷 ARN과 객체 ARN이 다른 서비스가 있다. |
| STS | Security Token Service. Role을 assume할 때 임시 자격증명을 발급하는 AWS 서비스다. | 임시 자격증명도 권한이 강하면 위험하다. 다만 수명이 짧아 노출창이 줄어든다. |
| Trust Policy | Role을 누가 assume할 수 있는지 정하는 정책이다. | Permission Policy가 있어도 Trust Policy가 틀리면 Role을 맡을 수 없다. |
| Permission Boundary | Permission Boundary는 User나 Role이 가질 수 있는 최대 권한 상한선이다. | Boundary는 권한을 부여하지 않는다. 상한을 좁힐 뿐이다. |
| SCP | Service Control Policy. AWS Organizations에서 계정이나 OU에 적용하는 조직 단위 권한 상한선이다. | SCP도 권한을 부여하지 않는다. 계정 안의 Allow를 더 좁히는 가드레일이다. |
| OIDC | OpenID Connect. 외부 시스템이 서명된 토큰으로 자신을 증명하고 Role을 assume하게 하는 표준 프로토콜이다. | Trust Policy의 조건을 넓게 열면 다른 repo나 브랜치도 Role을 맡을 수 있다. |
| MFA | Multi-Factor Authentication. 비밀번호 외 OTP, 보안 키 같은 두 번째 인증 요소를 요구하는 방식이다. | 권한 범위를 줄이지는 않지만, 사람 계정 탈취 확률을 낮춘다. |
| IAM Identity Center | 여러 AWS 계정의 사람 접근을 SSO와 임시 자격증명 중심으로 관리하는 서비스다. | IAM User의 완전 대체라기보다 workforce 접근을 중앙화하는 상위 관리 계층이다. |
IAM Policy는 “누가”를 제외한 나머지 판단을 JSON으로 표현한다. 가장 중요한 세 필드는 다음이다.
Action: 어떤 API 작업인가. 예: s3:GetObject, ecs:UpdateServiceResource: 어떤 리소스에 대한 작업인가. 보통 ARN(Amazon Resource Name)으로 쓴다.Condition: 어떤 조건에서만 적용되는가. 리전, MFA 여부, IP, 시간, OIDC 토큰 클레임 등을 제한한다.아래 예시는 애플리케이션 업로드 버킷을 대상으로 한 작은 worked example이다.
{ "Version": "2012-10-17", "Statement": [ { "Sid": "AllowObjectReadWrite", "Effect": "Allow", "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"], "Resource": [ "arn:aws:s3:::my-app-uploads/*", "arn:aws:s3:::my-app-uploads-dev/*" ] }, { "Sid": "AllowListOnlySelectedPrefixes", "Effect": "Allow", "Action": "s3:ListBucket", "Resource": "arn:aws:s3:::my-app-uploads", "Condition": { "StringLike": { "s3:prefix": ["uploads/*", "temp/*"] } } }, { "Sid": "DenyPublicObjectAcl", "Effect": "Deny", "Action": "s3:PutObjectAcl", "Resource": "arn:aws:s3:::my-app-uploads/*", "Condition": { "StringEquals": { "s3:x-amz-acl": ["public-read", "public-read-write"] } } } ]}이 JSON이 실제로 말하는 내용을 문장으로 풀면 다음과 같다.
my-app-uploads와 my-app-uploads-dev 버킷 안의 객체는 읽고 쓰고 지울 수 있다.uploads/와 temp/ prefix에 대해서만 허용한다.ARN 패턴은 IAM 초반에 가장 자주 틀리는 지점이다.
| ARN 패턴 | 의미 | 자주 나는 실수 |
|---|---|---|
arn:aws:s3:::my-bucket/* | 버킷 안의 모든 객체 | s3:ListBucket에는 이 ARN이 맞지 않는다. |
arn:aws:s3:::my-bucket | 버킷 자체 | s3:GetObject에는 객체 ARN이 필요하다. |
arn:aws:ecs:ap-northeast-2:123456789012:service/my-cluster/* | 특정 ECS 클러스터의 모든 서비스 | 계정 ID나 리전이 실제 리소스와 다르면 매칭되지 않는다 |
"Resource": "*" | 모든 리소스 또는 ARN 미지원 작업 대상 | 편하지만 최소 권한과 충돌한다. 이유를 남겨야 한다. |
Condition은 “허용하되 좁히는” 장치다. 아래 조건은 같은 Allow라도 리전, MFA(Multi-Factor Authentication), IP, 시간으로 더 제한한다.
{ "Condition": { "StringEquals": { "aws:RequestedRegion": "ap-northeast-2" }, "Bool": { "aws:MultiFactorAuthPresent": "true" }, "IpAddress": { "aws:SourceIp": ["203.0.113.0/24"] }, "DateLessThan": { "aws:CurrentTime": "2026-12-31T23:59:59Z" } }}반례를 하나 보자. s3:GetObject를 허용하려고 Resource에 arn:aws:s3:::my-app-uploads만 넣으면 버킷 자체에는 매칭되지만 객체에는 매칭되지 않는다. 정책은 붙어 있고 문법 오류도 없지만 실제 객체 조회는 AccessDenied가 난다. 이 경우 필요한 것은 더 큰 권한이 아니라 올바른 객체 ARN인 arn:aws:s3:::my-app-uploads/*다.
IAM은 기본적으로 닫힌 시스템이다. 요청을 허용하려면 명시적인 Allow가 필요하고, 아무 정책도 매칭되지 않으면 Implicit Deny, 즉 기본 거부가 된다. 그 위에 Explicit Deny가 있으면 모든 Allow보다 우선한다.
AWS가 요청을 평가하는 핵심 순서는 다음과 같다.
flowchart TD
Request["AWS API 요청"] --> Deny{"Explicit Deny가 있는가?"}
Deny -->|Yes| Reject["즉시 거부"]
Deny -->|No| Allow{"Allow가 있는가?"}
Allow -->|Yes| Permit["허용"]
Allow -->|No| Implicit["Implicit Deny로 거부"] 이 흐름은 단일 Policy 안에서만 적용되는 것이 아니다. Identity Policy(주체에 붙는 정책), resource-based policy(버킷·큐처럼 리소스 자체에 붙는 정책), Permission Boundary, SCP(Service Control Policy), session policy(AssumeRole로 만든 임시 세션에 추가로 붙이는 일시적 상한 정책)가 함께 작동할 때도 “명시적 거부가 있으면 끝”이라는 원칙은 유지된다.
작은 사고 실험을 해보자.
| 상황 | 결과 | 이유 |
|---|---|---|
Role Policy가 s3:PutObject Allow | 허용 가능 | 상위 Deny가 없고 Resource가 맞으면 Allow가 성립한다. |
| Role Policy가 Allow지만 SCP가 해당 리전에서 Deny | 거부 | SCP의 Explicit Deny가 Allow를 덮는다. |
Policy에 s3:GetObject 언급이 없음 | 거부 | 아무 Allow도 없으므로 Implicit Deny다. |
| Allow 10개가 있고 Bucket Policy에 같은 요청 Deny 1개 | 거부 | Deny는 개수 싸움이 아니다. 하나면 충분하다. |
퀴즈
힌트: Allow보다 위에 있거나 다른 정책 계층에서 오는 거부를 떠올린다.
SCP, Permission Boundary, 리소스 기반 정책 Deny, 세션 정책, Trust Policy 같은 다른 평가 계층을 먼저 확인한다. 단순히 Role Policy에 Allow를 하나 더 붙이면 원인이 가려질 수 있다.
더 보기: AWS IAM Policy Evaluation Logic 공식 문서
Access Key는 AWS API를 호출하기 위한 장기 자격증명이다. 보통 AWS_ACCESS_KEY_ID와 AWS_SECRET_ACCESS_KEY 쌍으로 다룬다. 반대로 IAM Role은 STS(Security Token Service)가 발급한 임시 자격증명으로 사용된다. 핵심 차이는 권한의 강도가 아니라 **노출창(window of exposure)**이다.
수동 비활성화나 교체 전까지 계속 유효하다. 공개 저장소에 노출되면 자동 수집과 abuse가 분 단위로 시작될 수 있고, 내부 로그나 private repo 유출은 발견이 훨씬 늦어질 수 있다.
임시 자격증명, federation, Roles Anywhere를 전혀 쓸 수 없는 예외적인 외부 시스템보통 1시간 단위로 발급되고 설정에 따라 더 짧거나 길 수 있다. SDK가 갱신을 처리하며, 유출되어도 세션 만료가 피해 시간을 제한한다.
EC2, ECS, Lambda, GitHub Actions OIDC, 대부분의 서버 워크로드수치 감각으로 보면 차이가 더 분명하다. 장기 Access Key가 유출되면 “탐지 시간 + 담당자 대응 시간 + 모든 사용처 교체 시간”이 노출창이 된다. 공개 GitHub 저장소에 노출된 AWS 키는 실험·honeypot 성격의 보고에서 1~4분 안에 abuse 시도가 관측되기도 했다. 반면 IAM Role 세션은 보통 약 1시간 단위로 쓰이고 Role의 max session duration으로 상한을 둘 수 있다. private repo, 내부 wiki, 로그 파일 유출은 발견이 훨씬 늦어질 수 있으므로 수치 자체보다 장기 키는 발견 전까지 계속 살아 있고, Role은 세션 만료가 기본 방어선이 된다는 직관을 잡는다.
그래서 AWS 공식 best practice는 워크로드에 장기 키를 넣기보다 Role 기반 임시 자격증명을 쓰라고 안내한다 (AWS IAM Best Practices). 사람 접근도 IAM User를 새로 늘리는 대신 IAM Identity Center나 외부 IdP federation으로 임시 자격증명을 발급하는 흐름을 우선 검토한다.
실무 선택 기준은 다음처럼 잡을 수 있다.
| 접근 주체 | 기본 선택 | 판단 기준 |
|---|---|---|
| 사람의 콘솔/CLI 접근 | IAM Identity Center 또는 외부 IdP federation | 장기 키를 개인 노트북에 남기지 않고, 입퇴사와 MFA를 중앙에서 관리한다 |
| ECS, EC2, Lambda 워크로드 | IAM Role | 서비스가 실행되는 위치에 Role을 붙이고 SDK가 임시 자격증명을 받는다 |
| GitHub Actions 등 외부 CI/CD | OIDC(OpenID Connect) + AssumeRoleWithWebIdentity | 저장된 secret 없이 워크플로 실행 때마다 짧은 세션을 만든다 |
| 온프레미스 서버 | IAM Roles Anywhere 등 인증서 기반 임시 자격증명 | 장기 키를 서버 파일에 두지 않는 방향을 우선 검토한다 |
| 비상 접근 | root와 제한된 break-glass 관리자 사용자, 강한 MFA 적용 | Identity Center 장애 같은 예외에 대비하되 사용 빈도를 낮게 유지한다 |
| 임시 자격증명을 못 쓰는 legacy | 최소 권한 Access Key + 회전 + 사용처 문서화 | 예외 사유와 교체 책임자가 없으면 만들지 않는다 |
MFA(Multi-Factor Authentication)는 여기서 사람 계정의 탈취 가능성을 낮추는 장치다. MFA가 권한 범위를 좁히는 것은 아니지만, root와 콘솔 접근 계정에는 “비밀번호 하나가 곧 AWS 계정”이 되는 상황을 막아준다.
Role은 권한을 담는 그릇이지만, 스스로 실행되지 않는다. 누군가가 Role을 assume해야 한다. 이때 필요한 정책은 두 종류다.
| 정책 종류 | 묻는 질문 | 예시 |
|---|---|---|
| Permission Policy | 이 Role을 맡은 뒤 무엇을 할 수 있나? | s3:PutObject를 my-app-uploads/*에 허용 |
| Trust Policy | 누가 이 Role을 맡을 수 있나? | ECS 태스크 서비스인 ecs-tasks.amazonaws.com이 assume 가능하다 |
ECS Task Role의 Trust Policy는 다음처럼 생겼다.
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "ecs-tasks.amazonaws.com" }, "Action": "sts:AssumeRole" } ]}여기서 sts:AssumeRole은 STS(Security Token Service)에 “이 Role의 임시 자격증명을 발급해도 되는가”라고 묻는 동작이다. Permission Policy가 아무리 넓어도 Trust Policy가 ECS를 신뢰하지 않으면 컨테이너는 그 Role을 사용할 수 없다.
ECS에서는 Task Role과 Execution Role도 반드시 구분해야 한다.
컨테이너 안의 애플리케이션 코드가 AWS 서비스에 접근할 때 쓰는 Role이다.
애플리케이션이 S3 업로드, SQS 송신, Secrets Manager 조회를 수행할 때ECS 플랫폼이 태스크를 시작하기 위해 쓰는 Role이다.
ECR 이미지 pull, CloudWatch Logs 전송, 태스크 시작 시 secret 주입을 수행할 때반례: 애플리케이션이 S3에 업로드해야 하는데 Execution Role에만 s3:PutObject를 붙이면 컨테이너 코드의 권한은 늘지 않는다. ECR pull과 로그 전송은 될 수 있지만, 애플리케이션의 S3 호출은 여전히 AccessDenied가 난다. 이때 필요한 것은 Execution Role 권한 추가가 아니라 Task Role에 올바른 Permission Policy를 붙이는 것이다.
더 보기: Amazon ECS Task IAM Role 공식 문서
GitHub Actions 같은 외부 CI/CD가 AWS에 배포해야 할 때 과거에는 AWS Access Key를 GitHub Secrets에 저장하는 방식이 흔했다. 더 나은 기본값은 OIDC(OpenID Connect)로 GitHub가 발급한 토큰을 AWS가 검증하고, 조건에 맞을 때만 Role을 assume하게 하는 것이다.
- name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789012:role/github-actions-role aws-region: ap-northeast-2이 YAML은 secret을 읽는 것이 아니라 GitHub 워크플로 실행 컨텍스트를 근거로 AWS Role 세션을 받는다. 보안상 중요한 부분은 YAML보다 IAM Role의 Trust Policy다. OIDC claim은 토큰 안에 들어 있는 속성이다. sub는 subject, 즉 “어떤 repo/ref에서 온 실행인가”를 나타내고, aud는 audience, 즉 토큰을 받을 대상 서비스를 뜻한다. GitHub에서는 repository와 ref claim으로 특정 저장소와 브랜치·태그 범위를 더 명확히 좁힐 수 있다.
OIDC vs GitHub Secrets — 결정 기준
| 항목 | GitHub Secrets에 Access Key 저장 | OIDC + AssumeRoleWithWebIdentity |
|---|---|---|
| 자격증명 수명 | 저장된 secret은 사람이 rotate하거나 비활성화할 때까지 장기 유지 | 워크플로 실행 단위의 임시 세션. 보통 짧은 duration으로 발급한다 |
| 유출 시 노출창 | 발견과 키 비활성화 전까지 지속 | 세션 만료까지의 잔여 시간으로 제한 |
| 교체 비용 | repo/조직 secret 갱신, 키 비활성화, 파이프라인 재검증 필요 | Trust Policy 조건 변경만으로 미래 assume을 막을 수 있고 앱 compute 재배포가 필요 없다 |
| 감사 단위 | 같은 키를 여러 workflow가 쓰면 호출자 구분이 흐려진다 | repo, ref, workflow claim을 CloudTrail과 조건에서 함께 추적할 수 있다 |
| 실수 반례 | secret이 로그, fork, 빌드 산출물에 새면 장기 키가 남는다 | Trust Policy를 repo:*처럼 넓게 열면 원치 않는 repo도 assume 가능 |
| 적합한 경우 | OIDC를 지원하지 않는 legacy CI의 예외 경로 | GitHub Actions, GitLab, 외부 SaaS 배포의 기본값 |
OIDC도 자동으로 안전해지는 마법은 아니다. Trust Policy 조건이 너무 넓으면 “Access Key를 없앴다”는 장점만 남고, 어떤 외부 주체가 Role을 맡을 수 있는지의 경계가 흐려진다. 따라서 CI/CD Role은 권한 정책보다 먼저 누가 assume할 수 있는가를 좁혀야 한다.
IAM을 디버깅할 때 가장 흔한 착각은 “Role Policy에 Allow가 있으니 허용될 것”이라고 생각하는 것이다. 실제 AWS 계정에서는 여러 상한선이 겹친다.
조직 전체 가드레일 └─ SCP (Service Control Policy): 조직/OU/계정에 적용되는 최대 허용 범위 └─ 계정 내부 상한 └─ Permission Boundary: 특정 User/Role이 가질 수 있는 최대 권한 └─ IAM Role: 워크로드나 사람이 assume하는 권한 그릇 └─ IAM Policy: 실제 Allow/Deny 규칙이 계층은 “위에서 아래로 권한을 더한다”가 아니다. 위 계층은 대체로 하위 계층의 상한을 정한다. SCP가 us-east-1과 ap-northeast-2 외 리전 생성을 Deny하면, 계정 안의 관리자 Role에 ec2:* Allow가 있어도 다른 리전의 EC2 생성은 거부된다. Permission Boundary도 마찬가지다. Boundary에 없는 작업은 Role Policy가 Allow해도 결과적으로 불가능하다.
작은 예시를 보자.
{ "Effect": "Deny", "Action": "*", "Resource": "*", "Condition": { "StringNotEquals": { "aws:RequestedRegion": ["ap-northeast-2", "us-east-1"] } }}이 SCP가 적용된 계정에서 도쿄 리전 외부에 리소스를 만들려 하면, Role Policy의 Allow 여부와 무관하게 거부된다. 그래서 AccessDenied를 만났을 때는 아래 순서로 생각한다.
최소 권한(Least Privilege)은 필요한 권한만 주는 원칙이다. 하지만 실무에서는 “권한을 적게”보다 “권한을 설명 가능한 축으로 좁히기”가 더 유용하다.
| 축 | 넓은 권한의 예 | 좁힌 권한의 예 | 실패 신호 |
|---|---|---|---|
| Action | s3:* | s3:GetObject, s3:PutObject | 읽기만 필요한 작업이 삭제 권한까지 가진다 |
| Resource | "Resource": "*" | arn:aws:s3:::my-app-uploads/* | 테스트 버킷 권한이 운영 버킷까지 닿는다 |
| Condition | 조건 없음 | aws:RequestedRegion, aws:SourceIp, aws:MultiFactorAuthPresent | 같은 Role이 어디서나 같은 권한을 가진다 |
| Principal | 모든 계정 또는 모든 repo | 특정 AWS 계정, 특정 GitHub repository와 ref | 의도하지 않은 외부 주체가 Role을 assume한다 |
| Time | 장기 Access Key 무기한 | STS 세션, 정기 회전, 미사용 키 제거 | 사용하지 않는 키가 90일 이상 남아 있다 |
IAM Access Analyzer는 실제 사용 이력이나 외부 공유 가능성을 바탕으로 권한을 줄일 후보를 찾는 데 도움을 준다. 단, 도구가 “unused”라고 표시한다고 바로 삭제하기보다 그 권한이 비상 절차, 드문 배포 경로, 계절성 작업에 필요한지 확인해야 한다. 최소 권한의 목표는 권한을 없애는 것이 아니라 필요한 권한을 근거와 함께 남기는 것이다.
IAM에서는 “정책을 붙였는데 동작하지 않음”이 자주 생긴다. 문법 오류가 없고 콘솔에도 정책이 보이기 때문에 더 위험하다. 이때는 실패 메시지보다 평가 축을 다시 분해해야 한다.
| 패턴 | 발생 시나리오 | 왜 조용한가 | 먼저 확인할 것 |
|---|---|---|---|
| 잘못된 Resource ARN | s3:GetObject를 arn:aws:s3:::bucket에 Allow해서 객체 접근이 계속 실패한다. | 정책은 유효하지만 요청 객체 ARN과 매칭되지 않는다. | 실제 요청 ARN이 arn:aws:s3:::bucket/key 형태인지 시뮬레이션한다. |
| Trust Policy 누락 | Permission Policy는 있는데 ECS가 Role을 assume하지 못한다. | Role 권한만 보면 충분해 보이지만 세션 발급 전 단계가 막힌다. | Trust Policy의 Principal이 ecs-tasks.amazonaws.com인지 확인한다. |
| SCP 충돌 | 개발 계정 Role은 Allow인데 조직 SCP가 리전이나 서비스를 Deny한다. | Role Policy Simulator만 보면 Allow처럼 보일 수 있다. | Organizations SCP 포함 시뮬레이션 또는 CloudTrail의 deny context를 확인한다. |
| Permission Boundary | 개발자가 새 Policy를 붙였는데 Boundary 밖 Action이라 효과가 없다. | Policy attach는 성공하지만 effective permission은 늘지 않는다. | Role/User의 PermissionsBoundary를 확인한다. |
| Task Role/Execution 혼동 | 애플리케이션 S3 권한을 Execution Role에 붙인다. | 태스크 시작은 정상이라 권한 설정이 맞아 보인다. | 앱 로그의 assumed-role/... ARN이 Task Role인지 확인한다. |
| OIDC 조건 과소/과대 | GitHub Actions Role의 sub 조건이 너무 좁아 배포가 실패하거나, 너무 넓어 다른 repo가 assume할 수 있다. | IAM Role과 OIDC Provider는 존재하므로 설정 완료처럼 보인다. | CloudTrail의 AssumeRoleWithWebIdentity와 Trust Policy Condition을 함께 본다. |
실패 신호를 줄이면 다음 세 문장으로 요약된다.
AccessDenied인데 Role Policy가 Allow라면 상위 Deny나 다른 정책 계층을 의심한다.Unable to locate credentials라면 권한 평가 이전에 자격증명 공급 경로가 끊긴 것이다.CloudTrail 이벤트 필드는 서비스와 인증 경로에 따라 달라질 수 있다. 알림이나 SIEM(Security Information and Event Management) 쿼리를 만들 때는 특정 표시 이름 하나에만 의존하지 말고 account, ARN, session issuer(임시 세션을 발급한 원래 Role 정보), event name, error code를 함께 보도록 설계한다.
IAM Managed Policy는 버전 기록을 가진다. 최대 5개 버전을 보관하며, 기본 버전(default version)을 바꾸면 이전 정책으로 빠르게 되돌릴 수 있다 (AWS IAM Policy Versioning 공식 문서).
이 기능은 단순 편의 기능이 아니라 권한 변경의 안전장치다. 권한 정책은 배포 직후에 “너무 좁아서 장애”와 “너무 넓어서 사고”를 모두 만들 수 있다. Managed Policy 버전을 쓰면 새 버전을 만든 뒤 문제가 생겼을 때 이전 정상 버전을 default로 되돌리는 방식으로 blast radius(잘못된 변경이나 침해가 영향을 미치는 범위)를 줄일 수 있다.
정량 감각도 필요하다.
Resource: "*"로 넓힌 정책을 검토 없이 배포해도 된다는 뜻은 아니다. rollback은 사고 대응 장치이지 리뷰 대체물이 아니다.IAM에서 배운 원칙은 Kubernetes RBAC, OAuth scope, PostgreSQL 권한을 읽을 때도 쓸 수 있다. 이름은 달라도 대부분의 권한 시스템은 네 질문으로 분해된다.
| 질문 | IAM 예시 | Kubernetes RBAC | PostgreSQL |
|---|---|---|---|
| 누가 접근하는가? | IAM User, IAM Role, Role session | ServiceAccount | DB User, DB Role |
| 무엇에 접근하는가? | S3 ARN, ECS service ARN | Namespace, Pod, Secret | Table, Schema |
| 무엇을 할 수 있는가? | s3:GetObject, ecs:UpdateService | get, list, create | SELECT, INSERT |
| 기본값은 허용인가 거부인가? | Implicit Deny | 보통 명시 권한만 허용 | 부여된 권한만 허용 |
| 상위에서 덮는 제어가 있는가? | SCP, Boundary, Resource Policy | Admission, OPA, PSP 대체 정책 | Superuser, Row-Level Security |
이 표의 목적은 IAM을 다른 도구에 억지로 비유하는 것이 아니다. 권한 에러를 만나면 늘 Subject → Action → Resource → Condition → 상위 Deny 순서로 쪼개 볼 수 있다는 사고 습관을 만드는 것이다.
| 개념 A | 개념 B | 차이점 |
|---|---|---|
| User | Role | User는 장기 자격증명을 가질 수 있는 계정 내 사용자, Role은 assume 대상 |
| Policy | Role | Policy는 권한 규칙, Role은 그 규칙을 가진 임시 권한 그릇 |
| Permission Policy | Trust Policy | Permission은 맡은 뒤 할 일, Trust는 누가 맡을 수 있는지 |
| Inline Policy | Managed Policy | Inline은 한 엔티티에 종속, Managed는 재사용과 버전 관리 가능 |
| Root Account | IAM User | Root는 계정 최상위 권한, IAM User는 제한 가능한 개별 주체 |
| IAM User | IAM Identity Center | User는 계정 안 장기 사용자, Identity Center는 사람 접근 중앙 관리 계층 |
| Permission Boundary | SCP | Boundary는 User/Role 상한, SCP는 조직/계정 상한 |
| Task Role | Execution Role | Task Role은 앱 코드 권한, Execution Role은 ECS 플랫폼 시작 권한 |
AccessDenied, ExpiredTokenException, Unable to locate credentials 원인을 분해할 때본문 이해에 필수인 내용은 위에 설명했다. 아래는 실제 계정에서 확인할 때 펼쳐 보는 절차다.
Policy Simulator:IAM User/Role 선택 → 서비스 선택 → 작업 선택 → Resource ARN 입력 → Run Simulation필요 시 Organizations SCP 포함 시뮬레이션을 켠다.
Access Analyzer:IAM → Access Analyzer → FindingsExternal access: 외부 계정이나 public 접근 가능성 확인Unused access: 장기간 사용되지 않은 권한과 Access Key 후보 확인주의: Simulator에서 Allow가 나와도 실제 요청이 거부될 수 있다. 리소스 기반 정책, SCP, Permission Boundary, 세션 정책, 실제 호출 주체가 같은지 함께 확인한다.
aws sts get-caller-identity예상 출력에서 Arn이 자신이 의도한 User 또는 assumed Role인지 확인한다.
{ "UserId": "AIDAXXXXXXXXXXXXXXXXX", "Account": "123456789012", "Arn": "arn:aws:sts::123456789012:assumed-role/my-task-role/session"}Unable to locate credentials는 권한 부족이 아니라 자격증명 공급 경로가 없는 상태다. SSO 로그인, profile, 환경변수, ECS Task Role 연결부터 확인한다.
aws cloudtrail lookup-events \ --lookup-attributes AttributeKey=EventName,AttributeValue=PutObject \ --max-results 10찾은 이벤트에서 errorCode, 호출자 userIdentity.arn, sessionIssuer, 대상 리소스를 함께 본다. 호출자가 예상한 Role이 아니면 정책을 더 붙이기 전에 자격증명 공급 경로부터 고친다.
aws iam list-policy-versions \ --policy-arn arn:aws:iam::123456789012:policy/MyPolicy \ --query 'Versions[*].{Version:VersionId,IsDefault:IsDefaultVersion,Created:CreateDate}'예상 결과에서 이전 정상 버전이 v2, 현재 문제 버전이 v3이라면 default version을 되돌린다.
aws iam set-default-policy-version \ --policy-arn arn:aws:iam::123456789012:policy/MyPolicy \ --version-id v2문제 버전은 기본 버전이 아닐 때만 삭제할 수 있다.
aws iam delete-policy-version \ --policy-arn arn:aws:iam::123456789012:policy/MyPolicy \ --version-id v3AccessDenied 이벤트 하나를 찾아 호출 주체 ARN과 error code를 읽어본다.AWS STS, AssumeRole, AssumeRoleWithWebIdentity, MFA 적용, IAM Policy Simulator, Service Control Policy(SCP), Permission Boundary, Resource-based Policy, Cross-Account Access, IAM Identity Center, OIDC, IAM Access Analyzer