Published on

EKS IRSA 웹아이덴티티 토큰 만료·403 해결

Authors

서버리스나 노드 IAM Role 대신 IRSA(IAM Roles for Service Accounts)를 쓰면, 파드가 sts:AssumeRoleWithWebIdentity로 임시 자격 증명을 받아 AWS API를 호출합니다. 그런데 운영에서 자주 보는 증상이 있습니다.

  • 일정 시간 잘 되다가 갑자기 ExpiredToken 또는 WebIdentityErr 류 에러
  • AccessDenied 403, 특히 AssumeRoleWithWebIdentity 단계에서 막힘
  • 재시작하면 잠깐 살아나지만 다시 실패

이 글은 “웹아이덴티티 토큰 만료·403”을 원인별로 분류하고, 어떤 로그를 어디서 봐야 하는지, 그리고 정확히 무엇을 고치면 되는지를 실전 위주로 정리합니다.

OIDC Thumbprint 변경으로 인한 IRSA 403은 별도 케이스로 자주 발생합니다. 아래 글도 함께 참고하세요.

IRSA 동작 방식: 만료가 “어디서” 나는지부터 나누기

IRSA는 크게 두 단계입니다.

  1. 쿠버네티스가 파드에 ServiceAccount 토큰(JWT) 을 마운트
  2. AWS SDK 또는 AWS CLI가 그 토큰을 읽어 AssumeRoleWithWebIdentity 호출

여기서 만료는 두 층에서 발생합니다.

  • K8s 토큰(JWT) 만료/갱신 문제: 토큰 파일이 갱신되지 않거나 잘못 마운트됨
  • STS 임시 자격증명 만료: SDK가 갱신을 못 하거나 캐시가 깨짐

따라서 증상을 보면 먼저 “STS 호출 자체가 403인가”, “STS는 성공했는데 이후 API가 403인가”를 구분해야 합니다.

가장 흔한 403: Trust Policy 조건 불일치

IRSA 403의 다수는 Role의 Trust Policy에서 sub 또는 aud 조건이 실제 토큰과 맞지 않아 발생합니다. 에러는 보통 아래처럼 나옵니다.

  • AccessDenied: Not authorized to perform sts:AssumeRoleWithWebIdentity

1) ServiceAccount 어노테이션 확인

kubectl -n <namespace> get sa <serviceaccount> -o yaml

아래가 있어야 합니다.

metadata:
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::<account-id>:role/<role-name>

<namespace> 또는 <serviceaccount>가 다르면 Trust Policy의 sub가 어긋납니다.

2) Role Trust Policy의 sub / aud 점검

IRSA용 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:my-ns:my-sa"
        }
      }
    }
  ]
}

여기서 자주 틀리는 지점:

  • sub의 네임스페이스 또는 SA 이름 오타
  • audsts.amazonaws.com으로 안 맞춤
  • OIDC provider ARN이 다른 클러스터의 것으로 남아 있음

토큰 만료처럼 보이지만 실제론 “토큰 파일/환경변수” 문제

IRSA는 AWS SDK가 다음 두 값을 기반으로 동작합니다.

  • AWS_ROLE_ARN
  • AWS_WEB_IDENTITY_TOKEN_FILE

EKS는 보통 파드에 이를 자동 주입합니다. 그러나 다음 케이스에서 깨집니다.

  • Helm 템플릿에서 환경변수를 덮어씀
  • 사이드카/Init 컨테이너가 /var/run/secrets/eks.amazonaws.com/serviceaccount/token 경로를 가정하고 하드코딩
  • automountServiceAccountToken: false를 켜서 토큰이 아예 마운트되지 않음

파드 내부에서 즉시 확인하는 명령

kubectl -n <namespace> exec -it <pod> -- sh -lc 'env | grep -E "AWS_ROLE_ARN|AWS_WEB_IDENTITY_TOKEN_FILE"'
kubectl -n <namespace> exec -it <pod> -- sh -lc 'ls -al /var/run/secrets/eks.amazonaws.com/serviceaccount || true'
kubectl -n <namespace> exec -it <pod> -- sh -lc 'test -f "$AWS_WEB_IDENTITY_TOKEN_FILE" && echo OK || echo MISSING'

AWS_WEB_IDENTITY_TOKEN_FILE이 비어 있거나 파일이 없으면, SDK는 IRSA를 못 쓰고 다른 크리덴셜 소스(예: 노드 role, 빈 값)로 떨어지며 403이 발생할 수 있습니다.

“만료 후부터 403” 패턴: SDK 갱신 실패 또는 캐시/파일 시스템 이슈

정상이라면 SDK는 STS 임시 자격증명 만료 전에 자동으로 새로 발급받습니다. 그런데 다음 상황에서 갱신이 실패합니다.

  • 컨테이너가 STS 엔드포인트로 나갈 수 없음(네트워크/프록시/DNS)
  • AWS_STS_REGIONAL_ENDPOINTS 또는 리전 설정이 꼬여 특정 리전 STS로만 가다 실패
  • 오래된 SDK 버전이 WebIdentity 로테이션에 버그가 있음
  • 자격증명 캐시 파일을 볼륨에 쓰는 앱 로직이 꼬여 오래된 값만 계속 사용

CloudTrail로 “AssumeRoleWithWebIdentity” 자체가 성공했는지 확인

CloudTrail EventName이 AssumeRoleWithWebIdentity인 이벤트를 찾습니다.

  • 이벤트가 없다: 파드가 STS 호출을 못 하고 있거나, SDK가 IRSA를 아예 안 쓰는 상태
  • 이벤트가 AccessDenied: Trust Policy, OIDC provider, sub/aud 불일치
  • 이벤트는 성공인데 이후 API가 403: Role Permission Policy가 부족하거나 다른 Role을 쓰고 있음

OIDC Provider 관련 문제: Thumbprint, Issuer, Provider ARN

EKS IRSA는 IAM OIDC Provider에 의존합니다. 인증서 체인이 바뀌거나(Thumbprint 변경), 클러스터를 재생성했는데 Role의 Federated가 예전 Provider를 가리키면 403이 납니다.

이 케이스는 증상이 매우 전형적이라 별도 글로 정리해두었습니다.

운영 팁은 하나입니다. “파드가 가진 토큰의 issuer”와 “IAM OIDC provider URL”이 일치하는지 항상 맞춰보세요.

쿠버네티스 토큰 자체가 문제인 경우: BoundServiceAccountTokenVolume

최근 쿠버네티스는 ServiceAccount 토큰을 “짧은 TTL + 자동 로테이션” 형태로 제공합니다. 일반적으로는 문제 없지만, 다음 조건에서 만료처럼 보일 수 있습니다.

  • 매우 오래된 EKS AMI/쿠버네티스 버전 조합
  • 특이한 CSI/보안 에이전트가 토큰 파일을 가로채거나 읽기 권한을 깨뜨림
  • automountServiceAccountToken 설정이 워크로드/SA 수준에서 엇갈림

점검 포인트:

kubectl -n <namespace> get pod <pod> -o jsonpath='{.spec.automountServiceAccountToken}'
kubectl -n <namespace> get sa <serviceaccount> -o jsonpath='{.automountServiceAccountToken}'

둘 중 하나라도 false면 토큰이 마운트되지 않을 수 있습니다.

디버깅을 빠르게 만드는 재현용 Pod

문제 파드가 복잡하면, 동일한 ServiceAccount로 최소 Pod를 띄워 STS 호출만 검증하는 게 가장 빠릅니다.

apiVersion: v1
kind: Pod
metadata:
  name: irsa-debug
  namespace: my-ns
spec:
  serviceAccountName: my-sa
  containers:
    - name: awscli
      image: amazon/aws-cli:2.15.0
      command: ["sh", "-lc"]
      args:
        - |
          echo "ROLE=$AWS_ROLE_ARN"
          echo "TOKEN_FILE=$AWS_WEB_IDENTITY_TOKEN_FILE"
          aws sts get-caller-identity --region ap-northeast-2
  restartPolicy: Never

실행 후:

kubectl -n my-ns logs irsa-debug

여기서 get-caller-identity가 실패하면 IRSA 레벨 문제(Trust/OIDC/토큰/네트워크)로 확정할 수 있습니다.

애플리케이션 코드에서 자주 만드는 실수: 자격증명 고정

IRSA는 “임시 자격증명”입니다. 즉, 앱이 아래처럼 자격증명을 한 번만 읽어서 전역 변수로 고정해버리면, 만료 이후 403이 날 수 있습니다.

  • 프로세스 시작 시 STS 호출 결과를 파일로 저장하고 계속 재사용
  • SDK가 제공하는 Provider Chain을 우회해서 AccessKey/SecretKey/SessionToken을 직접 주입

(예시) Go AWS SDK v2에서 권장 패턴

아래처럼 config.LoadDefaultConfig에 맡기면 WebIdentity를 포함한 기본 체인이 알아서 로테이션합니다.

package main

import (
  "context"
  "fmt"
  "log"

  "github.com/aws/aws-sdk-go-v2/config"
  "github.com/aws/aws-sdk-go-v2/service/sts"
)

func main() {
  ctx := context.Background()

  cfg, err := config.LoadDefaultConfig(ctx)
  if err != nil {
    log.Fatal(err)
  }

  client := sts.NewFromConfig(cfg)
  out, err := client.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{})
  if err != nil {
    log.Fatal(err)
  }

  fmt.Println(*out.Arn)
}

만약 커스텀 Credential Provider를 쓰고 있다면, “만료 갱신 로직”이 있는지부터 의심해야 합니다.

네트워크/프록시로 STS만 막히는 케이스

EKS에서 Private 서브넷 + 제한된 egress 환경이면, STS로의 HTTPS 통신이 막혀서 갱신이 실패할 수 있습니다.

체크리스트:

  • NAT Gateway 또는 egress 경로 존재 여부
  • VPC Endpoint 사용 시 com.amazonaws.<region>.sts 인터페이스 엔드포인트 연결
  • 프록시 사용 시 HTTPS_PROXY 설정과 NO_PROXY에 AWS 도메인 포함 여부

파드에서 간단 확인:

kubectl -n <namespace> exec -it <pod> -- sh -lc 'apk add --no-cache curl >/dev/null 2>&1 || true; curl -sS https://sts.ap-northeast-2.amazonaws.com/ -o /dev/null -w "%{http_code}\n"'

403은 “접근 거부”일 수도 있지만, 여기서는 단순히 STS가 살아있음을 보는 용도입니다. 타임아웃/리졸브 실패가 나오면 네트워크 문제로 좁혀집니다.

해결 절차를 한 장으로 정리

운영에서 시간을 아끼는 순서대로 정리하면 아래입니다.

  1. 재현용 디버그 Podaws sts get-caller-identity 확인
  2. 파드 내부에서 AWS_ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE 및 토큰 파일 존재 확인
  3. IAM Role Trust Policy의 sub, aud, Federated(OIDC provider ARN) 확인
  4. CloudTrail에서 AssumeRoleWithWebIdentity 이벤트의 성공/실패 확인
  5. OIDC provider(Thumbprint/Issuer) 변경 여부 확인
  6. STS 네트워크 경로(NAT/VPC Endpoint/프록시) 확인
  7. 앱 코드에서 자격증명 고정/캐시 여부 확인 및 SDK 업그레이드

같이 보면 좋은 글: “토큰/JWT 디버깅” 관점

IRSA의 토큰 문제는 결국 “JWT 기반 인증이 예상대로 갱신/검증되는가”의 문제로 수렴합니다. JWT 디버깅 감각을 잡는 데는 아래 글도 도움이 됩니다.

마무리

EKS IRSA의 ExpiredToken과 403은 대부분 “정책이 틀렸다”라기보다, 토큰이 어떤 경로로 발급되고 소비되는지를 놓쳐서 생깁니다. 특히 AssumeRoleWithWebIdentity가 성공하는지부터 확인하면, 문제 영역이 Trust/OIDC인지, 네트워크인지, 애플리케이션 갱신 로직인지가 빠르게 갈립니다.

다음 장애를 대비해 권장하는 최소 안전장치는 두 가지입니다.

  • CloudTrail에서 AssumeRoleWithWebIdentity 실패 알람(AccessDenied 급증)
  • 재현용 irsa-debug Pod 매니페스트를 레포에 저장해 즉시 검증 가능하게 유지