Published on

EKS Pod에서 SSM 세션 403 실패 원인과 해결

Authors

서버리스처럼 보이는 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 세션은 크게 두 덩어리 권한이 필요합니다.

  1. 세션을 여는 호출자(=Pod의 IAM 주체)
    • ssm:StartSession (대상 리소스에 대해)
    • (옵션) ssm:DescribeInstanceInformation, ec2:DescribeInstances
    • (포트포워딩/터널링) ssm:StartSession + 문서(SSM Document) 접근 권한
  2. 세션의 대상(대부분 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-arn annotation 누락
  • 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을 태그 기반으로 제한하는 편이 안전합니다(예: Conditionec2:ResourceTag/SSMAccess=true).

(B) 포트포워딩(문서 권한 필수)

RDS/사설 서비스 접근을 위해 가장 많이 쓰는 문서는 아래 중 하나입니다.

  • AWS-StartPortForwardingSession
  • AWS-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 이벤트를 보고 errorCodeuserIdentity를 확인하면 빠릅니다.

6) 네트워크/VPC 엔드포인트 이슈인데 403처럼 보이는 케이스

엄밀히 말해 네트워크가 막히면 403보다는 타임아웃/5xx가 많습니다. 하지만 프록시/중간 장비, 잘못된 엔드포인트(리전 불일치)로 “권한 문제처럼 보이는” 응답이 나오는 케이스가 있습니다.

체크 포인트:

  • Pod가 호출하는 리전이 대상 리전과 동일한가? (AWS_REGION, AWS_DEFAULT_REGION)
  • 프라이빗 서브넷에서 NAT 없이 SSM을 쓰는가? 그렇다면 VPC Interface Endpoint가 필요

SSM 세션에 필요한 엔드포인트는 보통 아래 3개입니다.

  • com.amazonaws.<region>.ssm
  • com.amazonaws.<region>.ssmmessages
  • com.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, ec2messages VPC 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을 기반으로 정책을 좁혀가는 방식이 가장 빠르고 안전합니다.