이번 주는 ML 솔루션을 "안전하고 책임감 있게, 그리고 싸게" 운영하는 영역이고, 첫날 주제는 권한이다. SageMaker는 사용자가 직접 호출하는 작업과 SageMaker가 사용자를 대신해 수행하는 작업이 분리되어 있다. 이 둘을 헷갈리면 권한 시나리오를 매번 틀린다.
[사람 · CI 파이프라인 · SDK 호출자] │ ① sagemaker:CreateTrainingJob ← 사용자 IAM 정책 │ ② iam:PassRole (역할을 넘겨줌) ← 사용자 IAM 정책 ▼┌────────────────────────────┐│ SageMaker 서비스 │└────────────────────────────┘ │ ③ sts:AssumeRole ← 실행 역할의 "신뢰 정책" ▼┌────────────────────────────┐│ SageMaker 실행 역할 │└────────────────────────────┘ │ ④ 실제 리소스 접근 ← 실행 역할의 "권한 정책" ├──▶ S3 : 학습 데이터 읽기 / model.tar.gz 쓰기 ├──▶ ECR : 알고리즘 컨테이너 이미지 pull ├──▶ CloudWatch : 로그 스트림 · 학습 지표 └──▶ KMS : 암호화된 버킷·EBS 볼륨의 키 사용
경로 ①②는 요청을 받아줄지를, 경로 ③④는 작업이 실제로 돌아갈지를 결정한다. 끊긴 지점이 어디냐에 따라 증상이 완전히 달라진다.
is not authorized to perform: sagemaker:... / iam:PassRole
AccessDenied (S3 · ECR · KMS)
붙이는 대상
IAM 사용자·그룹·사람 쪽 역할
역할 하나. SDK의 role= 파라미터로 지정
핵심은 이것이다 — 학습 작업이 "S3 접근 거부"로 실패하면 그건 사용자 정책 문제가 아니라 실행 역할의 S3 권한 문제다. 시험은 이 구분을 반복해서 묻는다.
💡 관련 이론: 이 구조는 IAM의 "신뢰 정책 + 권한 정책" 분리에서 나온다. 사용자는 자신의 권한으로 SageMaker에 요청을 넣을 뿐, 그 뒤에 벌어지는 데이터 접근은 서비스가 떠맡은 역할의 신분으로 일어난다. 사용자에게 S3 권한이 아무리 많아도 실행 역할에 없으면 학습은 실패하고, 반대로 실행 역할이 아무리 세도 사용자에게 CreateTrainingJob이 없으면 작업은 만들어지지도 않는다.
의도적으로 좁힌 부분은 다음과 같다. S3는 특정 버킷 ARN으로 제한했고 읽기 버킷과 쓰기 버킷을 분리했다. 데이터 버킷에 s3:DeleteObject나 s3:*를 넣지 않는 것이 최소 권한의 핵심이다. 학습 작업이 실제로 필요로 하는 권한은 아래 다섯 갈래로 정리된다.
⚠️ 함정: KMS는 역할의 IAM 정책과 KMS 키 정책 양쪽에서 허용돼야 한다. IAM 정책에만 kms:Decrypt를 넣고 키 정책을 손대지 않아 계속 거부되는 사례가 흔하다. "S3 목록은 되는데 객체 읽기만 실패한다"면 십중팔구 SSE-KMS 키 권한이다.
최소 권한(least privilege)은 "필요한 만큼만"이 전부가 아니다. 세 축으로 좁힌다 — ① 액션(s3:* 대신 GetObject만), ② 리소스(* 대신 특정 버킷 ARN), ③ 조건(Condition 블록으로 특정 VPC·태그·암호화 강제). 시험에서 "가장 안전한 정책"을 고르라면 보통 액션과 리소스가 가장 좁은 보기가 정답이다.
마지막 세 줄이 중요한 이유는 거버넌스를 사후 점검이 아니라 생성 시점에 막는 방식이기 때문이다. "VPC 밖에서 학습을 돌리지 마세요"라고 문서로 공지하는 대신, VPC 설정 없는 CreateTrainingJob 요청 자체를 IAM에서 거부하면 규칙이 우회될 여지가 없다.
Resource를 특정 역할 ARN으로 못박는다. "Resource": "*"는 사실상 계정 내 아무 역할이나 서비스에 실어 보낼 수 있다는 뜻이다.
iam:PassedToService 조건으로 "이 역할은 SageMaker에만 넘길 수 있다"고 한정한다.
두 가지를 다 빼면 권한 상승(privilege escalation) 통로가 된다 — 권한이 약한 사용자가 강한 역할을 골라 서비스에 실어 보내고, 그 역할의 신분으로 코드를 실행할 수 있다.
iam:PassRole은 시험 범위에 든다. "사용자가 학습 작업을 만들 권한은 있는데 자꾸 실패한다"는 시나리오의 정답은 대개 PassRole 누락이다. PassRole은 "역할을 떠넘기는 행위"에 대한 권한이지 역할 자체의 권한이 아니다. 오류 메시지에 iam:PassRole이 그대로 찍히므로 식별도 쉽다.
SageMaker Studio와 노트북 인스턴스도 각자의 실행 역할로 동작한다. 데이터 사이언티스트가 노트북에서 학습을 돌리면 그 노트북의 실행 역할 권한으로 S3 접근과 학습이 수행된다. 권한을 너무 넓게 주면(예: AdministratorAccess) 노트북이 곧 백도어가 된다.
주체
어떤 역할로 도나
흔한 실수
Studio 도메인
도메인 생성 시 지정한 기본 실행 역할
팀 전체가 한 역할을 공유해 데이터 경계가 무너짐
Studio 사용자 프로필
프로필별 실행 역할로 도메인 기본값을 덮어씀
프로필 역할을 안 나눠 최소 권한이 무력화됨
노트북 인스턴스
인스턴스 생성 시 지정한 역할
AdministratorAccess 부여 → 노트북이 사실상 백도어
학습·처리·배치 작업
SDK의 role=로 넘긴 역할
노트북 역할을 그대로 재사용해 권한이 뒤섞임
엔드포인트
모델 생성 시 지정한 역할
학습용 역할을 재사용해 불필요한 쓰기 권한을 안고 감
팀·프로젝트별로 역할을 분리하고, 각 역할이 필요한 데이터 버킷만 허용하는 것이 표준이다. 태그 기반 Condition을 쓰면 "프로젝트 태그가 내 것인 작업만 볼 수 있다" 같은 경계를 정책 하나로 만들 수 있다.
권한 사고가 났을 때 추측 대신 확인부터 한다. 작업 설명에는 실제로 사용된 역할 ARN과 실패 사유가 그대로 들어 있다.
import boto3sm = boto3.client('sagemaker')desc = sm.describe_training_job(TrainingJobName='my-training-job')print(desc['RoleArn']) # 이 작업이 실제로 사용한 실행 역할print(desc['FailureReason']) # AccessDenied면 어떤 리소스인지 여기에 찍힌다# 그 역할에 실제로 붙어 있는 정책 확인iam = boto3.client('iam')role_name = desc['RoleArn'].split('/')[-1]print(iam.list_attached_role_policies(RoleName=role_name)) # 관리형 정책print(iam.list_role_policies(RoleName=role_name)) # 인라인 정책
FailureReason에 찍힌 리소스 ARN이 정책의 Resource와 한 글자라도 다르면(버킷 이름 오타, prefix 누락) 그게 원인이다. 정책이 맞는데도 거부된다면 명시적 Deny를 의심한다.
IAM 평가 순서는 명시적 Deny > 명시적 Allow > 암묵적 Deny다. 역할 정책이 완벽해도 조직의 SCP, 역할에 걸린 권한 경계(permissions boundary), S3 버킷 정책 중 어느 하나가 Deny면 거부된다. "권한을 다 줬는데 왜 안 되지"의 답은 대개 이 세 곳 중 하나다.