- Published on
EKS IRSA로 Pod IAM 권한 최소화 실전 가이드
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
EKS에서 애플리케이션 Pod가 AWS API를 호출해야 할 때, 가장 흔한 안티패턴은 node instance role 에 과도한 권한을 몰아주는 방식입니다. 이 방식은 한 번 열어둔 권한이 클러스터 전체로 확산되기 쉽고, 특정 네임스페이스나 특정 서비스만 써야 하는 권한을 통제하기도 어렵습니다.
IRSA(IAM Roles for Service Accounts)는 Kubernetes ServiceAccount 와 IAM Role을 1:1로 연결해 Pod 단위로 권한을 부여하는 메커니즘입니다. 핵심은 다음 한 줄입니다.
- Pod는 노드의 자격 증명을 쓰지 않고, OIDC 기반으로 STS
AssumeRoleWithWebIdentity를 통해 “자기 ServiceAccount에 매핑된 Role”만 임시 자격 증명으로 획득한다
이 글에서는 IRSA를 “동작하게 만드는 수준”이 아니라, 운영에서 사고를 줄이는 “권한 최소화 실전” 관점으로 설계와 구현 체크리스트를 제공합니다. AccessDenied로 막힐 때는 함께 보면 좋은 글로 EKS IRSA AccessDenied 권한 오류 빠른 해결도 참고하세요.
IRSA가 권한 최소화에 유리한 이유
1) 권한 경계가 노드에서 Pod로 내려온다
노드 Role은 기본적으로 해당 노드에서 실행되는 모든 Pod에 영향을 줄 수 있습니다. 반면 IRSA는 ServiceAccount 단위로 Role을 분리하므로, 예를 들어 payments 네임스페이스의 s3-uploader Pod만 S3 PutObject 권한을 갖게 할 수 있습니다.
2) 임시 자격 증명으로 회전과 유출 리스크를 줄인다
IRSA는 STS 기반 임시 자격 증명을 사용합니다. 정적 Access Key를 Secret에 넣는 방식 대비 유출 시 피해 범위와 지속 시간이 줄어듭니다.
3) 감사와 추적이 쉬워진다
CloudTrail에서 AssumeRoleWithWebIdentity 와 Role 단위 이벤트로 애플리케이션별 행위를 분리해 추적할 수 있습니다.
사전 준비: OIDC Provider 연결 확인
IRSA는 EKS 클러스터에 OIDC Provider가 연결되어 있어야 합니다. 보통 eksctl utils associate-iam-oidc-provider 로 구성합니다.
aws eks describe-cluster \
--name my-eks \
--query "cluster.identity.oidc.issuer" \
--output text
출력 예시는 https://oidc.eks.ap-northeast-2.amazonaws.com/id/XXXXXXXX 형태입니다. 이 URL이 IAM OIDC Provider로 등록되어 있어야 합니다.
eksctl 사용 예시는 다음과 같습니다.
eksctl utils associate-iam-oidc-provider \
--cluster my-eks \
--approve
실전 설계: 최소 권한을 위한 4가지 원칙
원칙 1) Role은 “업무 단위”로 쪼개고 재사용을 경계한다
예를 들어 “백엔드 공통 Role” 같이 크게 묶으면 다시 권한이 비대해집니다.
s3-uploader-roledynamodb-reader-rolesqs-consumer-role
처럼 기능 단위로 쪼개고, 정말 필요할 때만 합칩니다.
원칙 2) Trust Policy에서 sub 를 네임스페이스와 SA로 고정한다
IRSA의 최소 권한은 권한 정책만이 아니라 “누가 이 Role을 Assume할 수 있는가”가 결정합니다.
Trust Policy에서 Condition 의 sub 를 system:serviceaccount:{namespace}:{serviceaccount} 로 고정해야 합니다.
원칙 3) 권한 정책은 Resource를 좁히고, 가능한 경우 조건을 건다
S3라면 특정 버킷과 prefix로 제한하고, DynamoDB라면 특정 테이블 ARN으로 제한합니다.
원칙 4) Pod에서 ServiceAccount 토큰 자동 마운트를 통제한다
IRSA는 웹 아이덴티티 토큰을 사용합니다. 필요하지 않은 Pod까지 토큰이 마운트되면 공격 표면이 커집니다.
- 기본: 네임스페이스 또는 SA에서
automountServiceAccountToken을 명시 - 필요 없는 워크로드는
false
예제 시나리오: 특정 버킷에만 업로드하는 Pod
요구사항:
- 네임스페이스
apps - ServiceAccount
s3-uploader - S3 버킷
my-company-uploads - prefix
incoming/아래에만PutObject허용 - 버킷 리스트 권한은 최소화
1) IAM Policy 작성
PutObject 는 오브젝트 ARN에 적용되므로 arn:aws:s3:::버킷/프리픽스* 형태로 제한합니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowPutOnlyToIncomingPrefix",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:AbortMultipartUpload",
"s3:ListMultipartUploadParts"
],
"Resource": "arn:aws:s3:::my-company-uploads/incoming/*"
},
{
"Sid": "AllowBucketLocationIfNeeded",
"Effect": "Allow",
"Action": [
"s3:GetBucketLocation"
],
"Resource": "arn:aws:s3:::my-company-uploads"
}
]
}
애플리케이션이 버킷을 탐색할 필요가 없다면 s3:ListBucket 은 넣지 않는 편이 안전합니다. SDK가 내부적으로 필요로 하는 액션만 최소로 추가하세요.
2) IAM Role Trust Policy 작성
아래에서 issuer 는 OIDC issuer에서 https:// 를 제거한 값이 들어갑니다. 또한 sub 를 네임스페이스와 ServiceAccount로 고정합니다.
주의: 본문에 부등호가 노출되면 MDX 빌드가 깨질 수 있으니, 비교 연산자나 제네릭 표기가 필요하면 항상 인라인 코드로 감싸세요.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.ap-northeast-2.amazonaws.com/id/XXXXXXXX"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.ap-northeast-2.amazonaws.com/id/XXXXXXXX:sub": "system:serviceaccount:apps:s3-uploader",
"oidc.eks.ap-northeast-2.amazonaws.com/id/XXXXXXXX:aud": "sts.amazonaws.com"
}
}
}
]
}
여기서 최소 권한의 핵심은 sub 고정입니다. system:serviceaccount:apps:* 같이 와일드카드를 열어두면 같은 네임스페이스의 다른 Pod가 Role을 가로채기 쉬워집니다.
3) ServiceAccount에 Role ARN 어노테이션
EKS는 eks.amazonaws.com/role-arn 어노테이션을 통해 IRSA를 연결합니다.
apiVersion: v1
kind: ServiceAccount
metadata:
name: s3-uploader
namespace: apps
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/s3-uploader-role
automountServiceAccountToken: true
automountServiceAccountToken 은 IRSA를 쓰는 Pod에 필요하므로 true 로 둡니다. 반대로 IRSA가 필요 없는 ServiceAccount는 false 를 고려하세요.
4) Deployment에서 ServiceAccount 지정
apiVersion: apps/v1
kind: Deployment
metadata:
name: uploader
namespace: apps
spec:
replicas: 2
selector:
matchLabels:
app: uploader
template:
metadata:
labels:
app: uploader
spec:
serviceAccountName: s3-uploader
containers:
- name: app
image: public.ecr.aws/docker/library/python:3.12-slim
command: ["python", "-c"]
args:
- |
import boto3
s3 = boto3.client("s3")
s3.put_object(Bucket="my-company-uploads", Key="incoming/hello.txt", Body=b"hi")
print("uploaded")
이제 이 Pod는 노드 Role이 아니라 s3-uploader-role 로만 S3에 접근합니다.
검증: Pod가 어떤 IAM을 쓰는지 확인하는 방법
1) 환경 변수 확인
IRSA가 정상이라면 컨테이너 내부에 다음 환경 변수가 존재합니다.
AWS_ROLE_ARNAWS_WEB_IDENTITY_TOKEN_FILE
kubectl -n apps exec deploy/uploader -- env | grep AWS_
2) STS Caller Identity로 Role 확인
kubectl -n apps exec deploy/uploader -- bash -lc \
'python - <<"PY"
import boto3
print(boto3.client("sts").get_caller_identity())
PY'
출력의 Arn 이 assumed-role/s3-uploader-role 형태로 나오면 성공입니다.
운영에서 자주 터지는 함정과 대응
함정 1) AccessDenied인데 정책은 맞는 것 같다
대부분은 Trust Policy의 sub 또는 OIDC issuer 불일치입니다. 또는 Pod가 기대한 ServiceAccount를 쓰지 않고 default 를 쓰는 경우가 많습니다.
- Deployment의
serviceAccountName확인 - ServiceAccount 어노테이션 확인
- Trust Policy의 issuer 문자열,
aud확인
빠른 디버깅 루틴은 EKS IRSA AccessDenied 권한 오류 빠른 해결에 정리해 두었습니다.
함정 2) 권한을 노드 Role과 IRSA에 이중으로 주고 “되니까” 방치
이러면 IRSA로 최소 권한을 설계해도 실제로는 노드 Role로 우회 접근이 가능합니다.
- 원칙적으로 애플리케이션 AWS 권한은 IRSA로만 부여
- 노드 Role은 CNI, EBS CSI, 로그/메트릭 등 노드 애드온에 필요한 최소 권한만
함정 3) S3 권한에서 ListBucket 을 과도하게 열어둠
SDK나 라이브러리가 HeadBucket 또는 GetBucketLocation 을 요구하는 경우가 있어 “일단 s3:*” 같은 처방이 나오기 쉽습니다. S3는 액션별 리소스 스코프가 달라서 정책이 미묘합니다.
- 오브젝트 작업:
arn:aws:s3:::bucket/prefix/* - 버킷 작업:
arn:aws:s3:::bucket
함정 4) 외부에서 공급되는 이미지가 토큰을 읽어갈 수 있는 위험
IRSA는 ServiceAccount 토큰을 사용합니다. 취약한 이미지나 공급망 이슈가 있으면 토큰이 유출될 수 있습니다.
- IRSA가 필요 없는 워크로드는
automountServiceAccountToken: false - 네임스페이스 기본값으로 토큰 마운트를 끄고, 필요한 SA만 켜는 전략 검토
- NetworkPolicy로 메타데이터/외부 통신 최소화
Terraform로 IRSA 구성 자동화 예시
운영에서는 수동 클릭보다 IaC가 안전합니다. 아래는 IRSA Role과 Policy를 만드는 Terraform 예시의 뼈대입니다.
data "aws_eks_cluster" "this" {
name = var.cluster_name
}
data "aws_iam_openid_connect_provider" "this" {
url = data.aws_eks_cluster.this.identity[0].oidc[0].issuer
}
resource "aws_iam_policy" "s3_uploader" {
name = "s3-uploader-policy"
policy = file("policies/s3-uploader.json")
}
resource "aws_iam_role" "s3_uploader" {
name = "s3-uploader-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = data.aws_iam_openid_connect_provider.this.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"${replace(data.aws_eks_cluster.this.identity[0].oidc[0].issuer, "https://", "")}:sub" = "system:serviceaccount:apps:s3-uploader"
"${replace(data.aws_eks_cluster.this.identity[0].oidc[0].issuer, "https://", "")}:aud" = "sts.amazonaws.com"
}
}
}]
})
}
resource "aws_iam_role_policy_attachment" "attach" {
role = aws_iam_role.s3_uploader.name
policy_arn = aws_iam_policy.s3_uploader.arn
}
이후 Kubernetes ServiceAccount 는 Helm이나 Kustomize로 배포하면서 eks.amazonaws.com/role-arn 만 주입하면 됩니다.
체크리스트: “최소 권한 IRSA” 합격 기준
- Trust Policy의
sub가 정확히system:serviceaccount:ns:sa로 고정되어 있는가 aud가sts.amazonaws.com으로 고정되어 있는가- IAM Policy에서
Action과Resource가 최소 범위인가 - Deployment가 의도한
serviceAccountName을 사용하고 있는가 - 노드 Role에 동일 권한이 남아 있지 않은가
- 필요 없는 워크로드의
automountServiceAccountToken을 꺼서 공격 표면을 줄였는가
마무리
IRSA는 “EKS에서 AWS 권한을 주는 또 하나의 방법”이 아니라, 멀티테넌시와 제로트러스트에 가까운 운영을 가능하게 하는 기본기입니다. 특히 사고는 보통 정책을 하나 더 열어서 해결할 때가 아니라, 열어둔 권한이 다른 Pod로 전이될 때 발생합니다.
IRSA를 도입했다면 다음 단계는 반드시 “Role을 잘게 쪼개기”와 “Trust Policy의 sub 고정”입니다. 그리고 AccessDenied나 403을 만났을 때는 권한 정책뿐 아니라 Assume 경로(issuer, aud, sub, SA 매핑)를 함께 점검해야 합니다.
추가로 AWS API 권한 이슈를 체계적으로 디버깅하는 관점은 AWS Bedrock InvokeModel 403·Throttling 해결 - IAM·VPC·쿼터도 참고하면, “정책은 맞는데 왜 막히지” 같은 상황에서 원인 분리가 더 빨라집니다.