- Published on
EKS Pod에서 SSM 세션 403 실패 원인과 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스처럼 보이는 Pod 안에서 aws ssm start-session을 호출해 운영 접근(점프 호스트 대체, RDS/EC2 접근 터널링 등)을 하려다 보면, 유독 403(AccessDenied) 로 막히는 경우가 많습니다. 특히 EKS에서는 “Pod의 IAM(=IRSA)”, “노드 IAM”, “VPC 엔드포인트/라우팅”, “SSM 대상 리소스의 정책/상태”가 얽히면서 원인이 한 번에 안 보입니다.
이 글은 EKS Pod에서 SSM 세션 시작이 403으로 실패할 때를 전제로, 로그/에러 메시지별로 원인을 쪼개고 재현 가능한 설정 예제(Policy/IRSA/Endpoint)를 함께 제공합니다.
> 참고로 Pod에서 AWS 자격증명 자체를 못 찾는 문제라면 403이 아니라 Unable to locate credentials가 흔합니다. 그 케이스는 별도로 정리한 글(EKS Pod에서 AWS SDK 자격증명 못찾음 해결 가이드)도 함께 확인하면 진단 속도가 빨라집니다.
1) 403의 정체: “누가” 거부했는지부터 분리
SSM 세션은 크게 두 덩어리 권한이 필요합니다.
- 세션을 여는 호출자(=Pod의 IAM 주체)
ssm:StartSession(대상 리소스에 대해)- (옵션)
ssm:DescribeInstanceInformation,ec2:DescribeInstances - (포트포워딩/터널링)
ssm:StartSession+ 문서(SSM Document) 접근 권한
- 세션의 대상(대부분 EC2, 또는 ECS/온프렘 SSM Managed Instance)
- SSM Agent가 정상 등록되어 있어야 함
- 대상 인스턴스 프로파일에
AmazonSSMManagedInstanceCore등 필요 - (KMS 사용 시) 관련 KMS 권한
403은 보통 1번(호출자)에서 터지지만, “대상 리소스 정책/조건” 때문에 403이 나기도 합니다.
대표적인 403 메시지 패턴
AccessDeniedException: User: arn:aws:sts::...:assumed-role/... is not authorized to perform: ssm:StartSession on resource: ...- 호출자 정책 부족 또는 리소스/조건 불일치
AccessDeniedException: not authorized to perform: ssm:StartSession on resource: arn:aws:ssm:REGION:ACCOUNT:document/AWS-StartPortForwardingSessionToRemoteHost- SSM Document 권한 부족(포트 포워딩 문서)
AccessDeniedException: ... because no identity-based policy allows the ssm:StartSession action- 거의 항상 IRSA/Role 정책 문제
2) 먼저 “Pod가 어떤 IAM으로 호출 중인지” 확인
EKS에서 Pod가 AWS API를 호출하는 경로는 보통 두 가지입니다.
- IRSA(권장): ServiceAccount ↔ IAM Role 연동, Pod 단위 최소권한
- 노드 Role(비권장): IRSA가 없으면 노드 인스턴스 프로파일로 호출
403을 해결하려면 우선 Pod가 실제로 어떤 주체로 AWS API를 치는지 확인해야 합니다.
Pod 내부에서 Caller Identity 확인
kubectl exec -it deploy/myapp -- sh
# (컨테이너에 awscli가 없다면 디버그용 이미지를 붙이거나 ephemeral container 사용)
aws sts get-caller-identity
출력이 예를 들어 아래처럼 나오면 IRSA로 잘 붙은 것입니다.
{
"Account": "123456789012",
"Arn": "arn:aws:sts::123456789012:assumed-role/eks-ssm-session-role/1699999999999",
"UserId": "..."
}
만약 ...:assumed-role/<node-instance-role>/i-... 처럼 노드 Role이 보이면 IRSA 미적용(또는 깨짐) 상태일 가능성이 큽니다.
IRSA가 깨지는 흔한 원인
- ServiceAccount에
eks.amazonaws.com/role-arnannotation 누락 - Pod가 다른 ServiceAccount를 쓰고 있음(Deployment 수정 누락)
- OIDC Provider 미연결/삭제
- Trust policy의
sub조건이 namespace/serviceaccount와 불일치
3) IRSA 설정 예제(Trust Policy 포함)
아래는 default 네임스페이스의 ssm-client ServiceAccount가 eks-ssm-session-role을 Assume 하도록 하는 최소 예시입니다.
(1) ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
name: ssm-client
namespace: default
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/eks-ssm-session-role
(2) IAM Role Trust Policy
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.ap-northeast-2.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.ap-northeast-2.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E:sub": "system:serviceaccount:default:ssm-client",
"oidc.eks.ap-northeast-2.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E:aud": "sts.amazonaws.com"
}
}
}
]
}
여기서 403이 계속 나면 sub 값이 실제 SA와 일치하는지(네임스페이스 포함), 클러스터 OIDC issuer가 맞는지부터 다시 보세요.
4) SSM StartSession에 필요한 IAM Policy(세션/문서/대상)
SSM 세션은 “대상 인스턴스”와 “세션 문서(PortForwarding 등)”가 함께 등장합니다. 403이 나는 가장 흔한 이유는 ssm:StartSession만 주고 문서 권한을 빼먹는 것입니다.
(A) EC2 인스턴스에 셸 세션만(기본)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "StartSessionToSpecificInstances",
"Effect": "Allow",
"Action": [
"ssm:StartSession",
"ssm:TerminateSession",
"ssm:ResumeSession"
],
"Resource": [
"arn:aws:ec2:ap-northeast-2:123456789012:instance/i-0123456789abcdef0",
"arn:aws:ssm:ap-northeast-2:123456789012:session/*"
]
},
{
"Sid": "DescribeForUX",
"Effect": "Allow",
"Action": [
"ssm:DescribeInstanceInformation",
"ec2:DescribeInstances"
],
"Resource": "*"
}
]
}
> 운영에서는 인스턴스 ARN을 태그 기반으로 제한하는 편이 안전합니다(예: Condition에 ec2:ResourceTag/SSMAccess=true).
(B) 포트포워딩(문서 권한 필수)
RDS/사설 서비스 접근을 위해 가장 많이 쓰는 문서는 아래 중 하나입니다.
AWS-StartPortForwardingSessionAWS-StartPortForwardingSessionToRemoteHost
이때 403이 ...document/AWS-StartPortForwardingSessionToRemoteHost로 뜨면 문서 리소스 권한이 빠진 것입니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowStartSession",
"Effect": "Allow",
"Action": [
"ssm:StartSession",
"ssm:TerminateSession",
"ssm:ResumeSession"
],
"Resource": [
"arn:aws:ec2:ap-northeast-2:123456789012:instance/*",
"arn:aws:ssm:ap-northeast-2:123456789012:session/*"
]
},
{
"Sid": "AllowSSMDocumentsForPortForward",
"Effect": "Allow",
"Action": "ssm:StartSession",
"Resource": [
"arn:aws:ssm:ap-northeast-2::document/AWS-StartPortForwardingSession",
"arn:aws:ssm:ap-northeast-2::document/AWS-StartPortForwardingSessionToRemoteHost"
]
},
{
"Sid": "Describe",
"Effect": "Allow",
"Action": [
"ssm:DescribeInstanceInformation",
"ec2:DescribeInstances"
],
"Resource": "*"
}
]
}
포인트는 AWS 관리형 문서의 ARN은 계정이 아니라 ::document/... 형태라는 점입니다(리전은 들어감).
5) “권한은 맞는데도 403”인 경우: Organizations/SCP, Permission Boundary, Session Policy
IRSA Role에 정책을 붙였는데도 403이면 아래를 의심합니다.
- **SCP(Organizations Service Control Policy)**가
ssm:StartSession을 차단 - Role에 Permissions boundary가 걸려 상위에서 제한
- AssumeRole 시 session policy로 권한이 깎임(드뭄)
이 경우 CloudTrail에서 StartSession 이벤트를 보고 errorCode와 userIdentity를 확인하면 빠릅니다.
6) 네트워크/VPC 엔드포인트 이슈인데 403처럼 보이는 케이스
엄밀히 말해 네트워크가 막히면 403보다는 타임아웃/5xx가 많습니다. 하지만 프록시/중간 장비, 잘못된 엔드포인트(리전 불일치)로 “권한 문제처럼 보이는” 응답이 나오는 케이스가 있습니다.
체크 포인트:
- Pod가 호출하는 리전이 대상 리전과 동일한가? (
AWS_REGION,AWS_DEFAULT_REGION) - 프라이빗 서브넷에서 NAT 없이 SSM을 쓰는가? 그렇다면 VPC Interface Endpoint가 필요
SSM 세션에 필요한 엔드포인트는 보통 아래 3개입니다.
com.amazonaws.<region>.ssmcom.amazonaws.<region>.ssmmessagescom.amazonaws.<region>.ec2messages
STS도 IRSA/AssumeRoleWithWebIdentity에 필요할 수 있습니다(환경/SDK에 따라). STS 경로가 막히면 자격증명 갱신 실패로 연쇄 문제가 납니다. 이 주제는 EKS STS 엔드포인트 타임아웃 - VPC·NAT·DNS 해결에서 더 깊게 다뤘습니다.
VPC 엔드포인트 생성 예시(Terraform)
resource "aws_vpc_endpoint" "ssm" {
vpc_id = var.vpc_id
service_name = "com.amazonaws.${var.region}.ssm"
vpc_endpoint_type = "Interface"
subnet_ids = var.private_subnet_ids
security_group_ids = [aws_security_group.vpce.id]
private_dns_enabled = true
}
resource "aws_vpc_endpoint" "ssmmessages" {
vpc_id = var.vpc_id
service_name = "com.amazonaws.${var.region}.ssmmessages"
vpc_endpoint_type = "Interface"
subnet_ids = var.private_subnet_ids
security_group_ids = [aws_security_group.vpce.id]
private_dns_enabled = true
}
resource "aws_vpc_endpoint" "ec2messages" {
vpc_id = var.vpc_id
service_name = "com.amazonaws.${var.region}.ec2messages"
vpc_endpoint_type = "Interface"
subnet_ids = var.private_subnet_ids
security_group_ids = [aws_security_group.vpce.id]
private_dns_enabled = true
}
Security Group(vpce.id) 인바운드는 보통 “VPC 내부에서 443 허용”이면 됩니다. 아웃바운드는 기본 허용 또는 필요한 범위로 제한합니다.
7) 실제 실행 예제: Pod에서 포트포워딩 세션 열기
Pod가 awscli를 포함하지 않는 경우가 많으니, 운영에선 별도 디버그 Pod를 띄우는 편이 안전합니다.
디버그 Pod(awscli 포함) 예시
apiVersion: v1
kind: Pod
metadata:
name: ssm-debug
namespace: default
spec:
serviceAccountName: ssm-client
containers:
- name: awscli
image: amazon/aws-cli:2.15.0
command: ["sh", "-c", "sleep 36000"]
접속 후:
kubectl exec -it ssm-debug -- sh
aws sts get-caller-identity
# 예: EC2 인스턴스(i-...)를 경유해 RDS(5432)에 로컬 15432로 포워딩
aws ssm start-session \
--target i-0123456789abcdef0 \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters '{"host":["mydb.cluster-xxxx.ap-northeast-2.rds.amazonaws.com"],"portNumber":["5432"],"localPortNumber":["15432"]}'
여기서 403이 나면 에러 메시지에 등장하는 ARN을 그대로 보고 정책의 Resource에 반영하세요.
...instance/...→ 대상 인스턴스 범위...document/...→ 문서 ARN 추가...session/...→ 세션 리소스(일부 정책/조건에서 필요)
8) 빠른 체크리스트(10분 컷)
1) Pod IAM 주체 확인
aws sts get-caller-identity가 기대한 IRSA Role인가?
2) IRSA 바인딩 확인
- ServiceAccount annotation 존재?
- Trust policy의
sub/aud정확?
3) 최소 권한 정책 확인
ssm:StartSession대상 인스턴스 리소스 포함?- 포트포워딩이면
document/AWS-StartPortForwardingSession*리소스 포함?
4) 상위 정책 차단 여부
- SCP/permission boundary 확인
5) 프라이빗 네트워크라면 엔드포인트 확인
ssm,ssmmessages,ec2messagesVPC Endpoint + Private DNS- STS 경로(필요 시) 점검
DNS가 간헐적으로 흔들리면 SSM/ST S 호출이 불규칙하게 실패하기도 합니다. 클러스터 DNS 안정화는 EKS NodeLocal DNSCache로 DNS 간헐 실패 잡기도 참고할 만합니다.
9) 마무리: 403은 “정책 리소스”와 “문서 권한”에서 끝나는 경우가 많다
EKS Pod에서 SSM 세션이 403으로 실패하면, 대부분은 아래 둘 중 하나로 수렴합니다.
- IRSA가 기대한 Role로 붙지 않았거나(노드 Role로 호출 중)
ssm:StartSession은 줬지만 문서(AWS-StartPortForwardingSessionToRemoteHost) 리소스 권한을 안 줬거나
그 다음으로는 SCP/permission boundary 같은 상위 거부, 그리고 프라이빗 네트워크에서의 엔드포인트/리전 불일치가 뒤따릅니다.
운영 환경에서는 CloudTrail로 StartSession 이벤트를 확인하고(누가/어떤 ARN에/왜 거부됐는지), 그 ARN을 기반으로 정책을 좁혀가는 방식이 가장 빠르고 안전합니다.