Published on

EKS에서 AWS SDK 403 MissingAuthenticationToken 해결

Authors

서버리스/컨테이너 환경에서 AWS SDK를 쓰다 보면 403 MissingAuthenticationToken은 꽤 당황스럽습니다. 특히 EKS에서는 Pod가 어떤 방식으로 AWS 자격 증명(credential)을 얻는지가 여러 갈래(IRSA, 노드 IAM, 정적 키, 웹 아이덴티티 토큰)로 나뉘어 있어, 같은 403이라도 원인이 다릅니다.

이 글은 “EKS에서 AWS SDK 호출이 403 MissingAuthenticationToken으로 실패한다”는 상황을 기준으로, 재현 가능한 진단 순서가장 흔한 원인별 해결법을 정리합니다. (IRSA 자체는 되는데 특정 API만 403인 경우는 원인이 달라질 수 있어, 필요하면 EKS IRSA는 되는데 KMS Decrypt 403 해결법도 함께 확인하세요.)

1) MissingAuthenticationToken이 의미하는 것

MissingAuthenticationToken은 대체로 다음 중 하나를 뜻합니다.

  • 요청이 서명(Signature V4)되지 않았거나, 서명에 필요한 키를 못 구함
  • 잘못된 엔드포인트/리전으로 호출해서 AWS가 “이건 인증 토큰이 없는 요청”으로 판단
  • 프록시/미들웨어가 Authorization 헤더를 제거
  • (EKS에서 흔함) IRSA 환경변수/토큰 파일이 없거나, SDK가 이를 읽지 못함

중요 포인트는, 이 에러가 “권한 부족(AccessDenied)”과는 결이 다르다는 점입니다.

  • AccessDenied: 인증은 됐는데 권한이 없음
  • MissingAuthenticationToken: 인증 자체가 성립하지 않음(서명/토큰/헤더/엔드포인트 문제)

2) 가장 빠른 1차 분기: Pod에서 어떤 Credential Provider를 쓰는가

EKS Pod의 AWS 인증 경로는 보통 다음 중 하나입니다.

  1. IRSA(WebIdentity): ServiceAccount에 role annotation + projected token + AWS_ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE
  2. 노드 IAM(EC2 IMDS): Pod가 노드의 인스턴스 프로파일을 그대로 사용(권장 X)
  3. 정적 키: AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY를 Secret로 주입(권장 X)

먼저 Pod 안에서 아래를 확인합니다.

kubectl exec -it deploy/myapp -- sh -lc '
  echo "AWS_ROLE_ARN=$AWS_ROLE_ARN";
  echo "AWS_WEB_IDENTITY_TOKEN_FILE=$AWS_WEB_IDENTITY_TOKEN_FILE";
  env | egrep "^AWS_|^HTTP" | sort
'
  • IRSA를 의도했다면 AWS_ROLE_ARN / AWS_WEB_IDENTITY_TOKEN_FILE가 보여야 합니다.
  • 반대로 정적 키가 들어가 있으면(AWS_ACCESS_KEY_ID 등) IRSA보다 우선될 수 있어, 잘못된 키로 서명되어 403이 날 수도 있습니다.

3) IRSA 환경에서 가장 흔한 원인 5가지

3.1 ServiceAccount에 role annotation이 없거나 다른 SA를 쓰는 경우

Deployment가 실제로 어떤 ServiceAccount를 쓰는지부터 확인합니다.

kubectl get deploy myapp -o jsonpath='{.spec.template.spec.serviceAccountName}{"\n"}'
kubectl get sa myapp-sa -o yaml

ServiceAccount에 다음 형태가 있어야 합니다.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: myapp-sa
  namespace: default
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/myapp-irsa-role
  • Deployment는 serviceAccountName: myapp-sa를 가리켜야 합니다.
  • Helm/Kustomize에서 SA 이름이 바뀌거나, 기본 SA를 쓰게 되면 IRSA가 적용되지 않습니다.

3.2 OIDC Provider 미설정/불일치

IRSA는 EKS 클러스터의 OIDC issuer와 IAM OIDC provider가 매칭되어야 합니다.

확인:

aws eks describe-cluster --name my-cluster --query 'cluster.identity.oidc.issuer' --output text
aws iam list-open-id-connect-providers --query 'OpenIDConnectProviderList[*].Arn' --output text

OIDC provider가 없거나 issuer가 다르면, SDK가 STS AssumeRoleWithWebIdentity 단계에서 실패할 수 있고, 애플리케이션 레벨에서는 “결국 서명 없는 요청”으로 이어져 MissingAuthenticationToken처럼 보이기도 합니다(라이브러리/예외 처리 방식에 따라 메시지가 달라짐).

3.3 Trust policy의 sub/aud 조건 불일치

IRSA Role의 trust policy에서 sub(namespace/sa) 조건이 실제 SA와 일치해야 합니다.

예시(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:aud": "sts.amazonaws.com",
          "oidc.eks.ap-northeast-2.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E:sub": "system:serviceaccount:default:myapp-sa"
        }
      }
    }
  ]
}

여기서 namespace나 serviceaccount 이름이 하나라도 다르면 AssumeRole이 안 됩니다.

3.4 Pod에 projected token이 없거나 경로가 다름

IRSA는 Pod에 토큰 파일이 마운트되어야 하고, SDK는 AWS_WEB_IDENTITY_TOKEN_FILE 경로를 읽습니다.

kubectl exec -it deploy/myapp -- sh -lc '
  ls -al $AWS_WEB_IDENTITY_TOKEN_FILE;
  head -c 20 $AWS_WEB_IDENTITY_TOKEN_FILE; echo
'
  • 파일이 없으면 IRSA가 아닌 상태입니다.
  • 보안 하드닝/PSA/OPA 정책으로 projected volume이 막힌 경우도 있습니다.

3.5 SDK 버전/로딩 순서 문제(특히 오래된 SDK)

일부 오래된 SDK/런타임은 web identity provider를 기본 체인에 포함하지 않거나, 환경변수 우선순위가 달라 문제를 만듭니다.

  • Java: AWS SDK v2 권장, v1은 설정에 따라 web identity 인식이 애매할 수 있음
  • Node.js: v3 사용 시 기본 credential chain이 잘 동작
  • Python(boto3): 비교적 안정적이나, 컨테이너 이미지에 CA/시간 동기 문제까지 겹치면 다른 에러로 변질

가능하면 SDK를 최신으로 올리고, “명시적으로 credential provider를 지정”하는 것도 방법입니다.

4) 노드 IAM(IMDS)로 떨어지는 케이스: IMDS 차단/홉 제한

IRSA가 적용되지 않으면 SDK는 다음 후보로 **EC2 Instance Metadata Service(IMDS)**를 시도합니다. 그런데 EKS에서는 아래 이유로 IMDS가 막혀 있을 수 있습니다.

  • 노드가 IMDSv2 강제 + hop limit(1) 설정
  • CNI/네트워크 정책/iptables로 169.254.169.254 차단

이 경우 SDK는 credential을 못 얻고, 결과적으로 서명 없는 요청 → MissingAuthenticationToken이 됩니다.

Pod에서 IMDS 접근 여부를 확인:

kubectl exec -it deploy/myapp -- sh -lc '
  apk add --no-cache curl >/dev/null 2>&1 || true
  curl -sS -m 1 http://169.254.169.254/latest/meta-data/ || echo "IMDS blocked"
'
  • IRSA를 쓸 거라면 IMDS 접근이 되든 안 되든 상관없지만, IRSA가 깨졌을 때 IMDS로 fallback되는지 확인하는 지표가 됩니다.

5) 리전/엔드포인트/서비스 URL 실수로 인한 403

의외로 많습니다. 예:

  • STS를 https://sts.amazonaws.com이 아닌 다른 URL로 호출
  • 리전이 비어 있거나, AWS_REGION/AWS_DEFAULT_REGION이 엉뚱함
  • S3를 path-style/virtual-hosted-style로 잘못 구성 + 프록시가 Host 헤더 변경

진단 체크:

kubectl exec -it deploy/myapp -- sh -lc '
  echo "AWS_REGION=$AWS_REGION";
  echo "AWS_DEFAULT_REGION=$AWS_DEFAULT_REGION";
'

애플리케이션에서 AWS SDK 클라이언트 생성 시 리전을 명시하는 것도 안정적입니다.

(예) Node.js AWS SDK v3에서 리전/자격 자동 체인 사용

import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";

const client = new STSClient({
  region: process.env.AWS_REGION || "ap-northeast-2",
});

const out = await client.send(new GetCallerIdentityCommand({}));
console.log(out);

GetCallerIdentity는 “내가 어떤 자격으로 호출 중인지” 확인하는 데 가장 좋습니다.

6) 프록시/서비스메시가 Authorization 헤더를 제거하는 케이스

사내 프록시, Envoy, Nginx, 또는 일부 서비스메시 설정에서 hop-by-hop 헤더 처리나 보안 정책으로 Authorization 헤더가 제거/변경되면 SigV4가 깨집니다.

  • 특히 “애플리케이션 → 프록시 → AWS 서비스” 구조에서 발생
  • 외부로 나가는 egress proxy를 강제하는 환경에서 흔함

확인 포인트:

  • 프록시가 Authorization, X-Amz-Date, X-Amz-Security-Token, Host 헤더를 그대로 전달하는지
  • HTTP/2 변환 과정에서 헤더 정규화/삭제가 없는지

이 케이스는 애플리케이션 로그에 “서명 생성은 했는데 AWS에서 토큰이 없다고 함”처럼 보일 수 있습니다.

7) 실전 진단 런북: 10분 안에 결론 내기

아래 순서대로 보면 대부분 원인이 좁혀집니다.

7.1 Pod 내부에서 현재 CallerIdentity 확인

가능하면 애플리케이션에 진단용 엔드포인트를 추가하거나, 임시로 AWS CLI Pod를 띄웁니다.

kubectl run -it --rm awscli --image=amazon/aws-cli:2.15.0 --restart=Never -- \
  sh -lc 'aws sts get-caller-identity && aws configure list'
  • 여기서도 실패하면 클러스터/SA/IRSA 레벨 문제
  • 여기서는 성공하고 앱만 실패하면 SDK 버전/환경변수/프록시/리전/엔드포인트 문제

7.2 ServiceAccount ↔ Role 매핑 확인

kubectl get sa myapp-sa -o jsonpath='{.metadata.annotations.eks\.amazonaws\.com/role-arn}{"\n"}'

7.3 토큰 파일 존재/권한 확인

kubectl exec -it deploy/myapp -- sh -lc 'test -r "$AWS_WEB_IDENTITY_TOKEN_FILE" && echo OK || echo NO'

7.4 Trust policy 조건(sub/aud) 확인

IAM 콘솔 또는 CLI로 role trust policy를 점검합니다.

8) 해결 패턴별 처방전

패턴 A: IRSA 의도했는데 환경변수/토큰이 없다

  • Deployment의 serviceAccountName 수정
  • ServiceAccount에 eks.amazonaws.com/role-arn annotation 추가
  • OIDC provider 생성/연결

패턴 B: 토큰은 있는데 AssumeRole이 안 된다

  • IAM Role trust policy의 sub, aud 조건 수정
  • namespace/SA 이름 변경이 있었는지 확인

패턴 C: 앱에서만 MissingAuthenticationToken

  • SDK 업그레이드
  • 리전 명시
  • 프록시가 Authorization 계열 헤더를 제거하는지 확인
  • 컨테이너에 AWS_EC2_METADATA_DISABLED=true를 줘서 IMDS fallback을 차단하고, IRSA만 쓰도록 강제(원인 규명에 도움)
env:
  - name: AWS_EC2_METADATA_DISABLED
    value: "true"
  - name: AWS_REGION
    value: "ap-northeast-2"

패턴 D: IRSA는 되는데 특정 서비스만 403

이건 MissingAuthenticationToken이 아니라 AccessDenied로 보이는 경우가 더 많지만, KMS/S3/ECR 등 서비스별 정책/키 정책 문제로 갈라집니다. 예를 들어 KMS는 키 정책이 추가로 관여합니다. 이 경우는 EKS IRSA는 되는데 KMS Decrypt 403 해결법처럼 서비스별로 접근해야 합니다.

또 ECR pull 자체가 막힌 케이스는 증상이 다르지만(이미지 pull 단계), 운영 중 함께 터지는 경우가 많아 EKS Pod에서 AWS ECR 403 AccessDenied 해결도 참고가 됩니다.

9) (부록) Terraform로 IRSA 구성 예시

IRSA를 “대충 콘솔에서 클릭”으로 만들면 나중에 누락/불일치가 잦습니다. IaC로 고정해두면 재현성이 좋아집니다.

아래는 핵심 개념만 담은 예시입니다(환경에 맞게 수정 필요).

data "aws_eks_cluster" "this" {
  name = var.cluster_name
}

data "aws_eks_cluster_auth" "this" {
  name = var.cluster_name
}

# OIDC issuer URL에서 https:// 제거한 host/path가 IAM OIDC provider의 URL에 해당
locals {
  oidc_issuer = replace(data.aws_eks_cluster.this.identity[0].oidc[0].issuer, "https://", "")
}

resource "aws_iam_role" "irsa" {
  name = "myapp-irsa-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Principal = {
        Federated = aws_iam_openid_connect_provider.this.arn
      }
      Action = "sts:AssumeRoleWithWebIdentity"
      Condition = {
        StringEquals = {
          "${local.oidc_issuer}:aud" = "sts.amazonaws.com"
          "${local.oidc_issuer}:sub" = "system:serviceaccount:${var.namespace}:${var.serviceaccount_name}"
        }
      }
    }]
  })
}

resource "aws_iam_role_policy_attachment" "attach" {
  role       = aws_iam_role.irsa.name
  policy_arn  = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess" # 예시
}

이후 Kubernetes ServiceAccount에 annotation을 붙입니다.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: myapp-sa
  namespace: default
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/myapp-irsa-role

10) 마무리: MissingAuthenticationToken을 “인증 경로 문제”로 단순화하자

EKS에서 403 MissingAuthenticationToken은 대부분 (1) Pod가 어떤 credential을 쓸지 결정되는 구간(IRSA/IMDS/정적키)에서 꼬이거나, (2) 서명 헤더가 전송 중 훼손되거나, (3) 리전/엔드포인트가 틀린 문제입니다.

정리하면 다음 3가지만 먼저 확인하면 해결 속도가 빨라집니다.

  1. Pod에 AWS_ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE가 있는가(=IRSA 적용 여부)
  2. aws sts get-caller-identity가 Pod에서 되는가(=자격 획득 여부)
  3. 프록시/메시가 Authorization/Host/X-Amz-* 헤더를 건드리지 않는가(=서명 보존 여부)

여기까지 확인했는데도 원인이 애매하면, 실패하는 AWS API(서비스/리전/엔드포인트)와 SDK 언어/버전, Pod의 환경변수 목록을 기준으로 더 좁혀 들어가면 됩니다.