Published on

EKS IRSA는 되는데 KMS Decrypt 403 해결법

Authors

서론

EKS에서 IRSA(IAM Roles for Service Accounts) 구성까지는 잘 됐는데, 애플리케이션이 kms:Decrypt에서만 403(대부분 AccessDeniedException)을 뿜는 상황을 종종 만납니다. 이때 흔히 “IRSA가 깨졌나?”라고 의심하지만, 실제로는 **IRSA는 정상(=STS로 역할을 잘 받아옴)**이고 KMS 쪽 권한/정책/컨텍스트 조건에서 막히는 경우가 압도적으로 많습니다.

이 글은 “IRSA로 AssumeRoleWithWebIdentity는 성공”을 전제로, KMS Decrypt 403을 재현 가능한 체크리스트로 분해해 해결하는 방법을 정리합니다. (IRSA 자체가 타임아웃/0초로 실패한다면 먼저 EKS IRSA에서 AssumeRoleWithWebIdentity 0s 타임아웃 해결을 확인하는 게 빠릅니다.)


1) 증상 정리: IRSA는 되는데 KMS만 403인 전형적 로그

애플리케이션 로그는 보통 아래 형태 중 하나입니다.

  • AccessDeniedException: User: arn:aws:sts::<acct>:assumed-role/<role>/<session> is not authorized to perform: kms:Decrypt on resource: arn:aws:kms:...
  • KMSInvalidStateException이 아니라면, 거의 항상 정책/조건 문제입니다.

중요한 포인트는 에러의 Userassumed-role로 찍히는지입니다. 이게 찍힌다면 IRSA로 역할 assume 자체는 성공한 것입니다.


2) 먼저 “정말 IRSA 역할로 호출 중인지”를 Pod 안에서 확정하기

가끔은 IRSA를 붙였다고 생각했는데 실제로는 노드 IAM(Instance Profile)로 호출하거나, 다른 자격증명이 섞여서 호출됩니다. Pod 내부에서 아래를 실행해 호출 주체를 확인하세요.

kubectl exec -it deploy/myapp -- sh

# AWS SDK가 어떤 자격증명을 쓰는지 확인하기 좋음
aws sts get-caller-identity

# IRSA 환경변수 존재 여부(웹 아이덴티티 토큰)
env | egrep 'AWS_ROLE_ARN|AWS_WEB_IDENTITY_TOKEN_FILE|AWS_REGION|AWS_DEFAULT_REGION'

# 토큰 파일 존재 확인
ls -al $AWS_WEB_IDENTITY_TOKEN_FILE

기대 결과:

  • get-caller-identity의 ARN이 assumed-role/<IRSA_ROLE_NAME>/... 형태
  • AWS_WEB_IDENTITY_TOKEN_FILE/var/run/secrets/eks.amazonaws.com/serviceaccount/token 비슷한 경로

여기까지가 맞다면 “IRSA는 됨”이 확정이고, 이제부터는 KMS 영역입니다.


3) KMS Decrypt 403의 80%: 키 정책(Key policy)이 역할을 안 믿는다

KMS는 일반 IAM 서비스와 다르게 키 정책이 최종 관문인 경우가 많습니다.

3.1 키 정책에 역할(또는 계정 루트)이 포함돼야 한다

가장 흔한 실패 패턴은:

  • IAM Role에 kms:Decrypt 권한은 줬는데
  • KMS Key policy에 그 Role(또는 계정 루트에 대한 위임)이 없어서 403

키 정책에서 다음 중 하나가 성립해야 합니다.

  1. 키 정책에 해당 Role이 직접 kms:Decrypt 허용으로 들어가 있거나
  2. 키 정책에 계정 루트(arn:aws:iam::<acct>:root)가 충분히 위임되어 있고, IAM 정책으로 제어하는 모델을 쓰고 있거나

실무에서 안전하게 가려면 “키 정책 + IAM 정책”을 둘 다 명시하는 편이 디버깅이 쉽습니다.

3.2 예시: IRSA Role에 Decrypt를 허용하는 키 정책(발췌)

{
  "Sid": "AllowDecryptForIrsaRole",
  "Effect": "Allow",
  "Principal": {
    "AWS": "arn:aws:iam::123456789012:role/myapp-irsa-role"
  },
  "Action": [
    "kms:Decrypt",
    "kms:DescribeKey"
  ],
  "Resource": "*"
}

주의:

  • KMS Key policy에서 Resource는 항상 "*"로 두는 형태가 일반적입니다(키 정책 문법 특성).
  • 키가 다른 계정(크로스 어카운트)이면, 키 정책에 외부 계정 Role을 Principal로 명시해야 합니다.

4) IAM 정책은 붙였는데도 403: 조건(Condition) 때문에 막히는 케이스

kms:Decrypt는 보안 강화를 위해 조건을 많이 겁니다. 특히 아래 조건이 들어가면 “IRSA는 되는데 KMS만 403”이 자주 발생합니다.

4.1 kms:ViaService 조건 (예: S3 통해서만 복호화 허용)

보안팀이 키를 “S3에서 SSE-KMS로만 사용”하게 묶기 위해 다음 같은 조건을 넣곤 합니다.

{
  "Effect": "Allow",
  "Action": "kms:Decrypt",
  "Resource": "*",
  "Condition": {
    "StringEquals": {
      "kms:ViaService": "s3.ap-northeast-2.amazonaws.com"
    }
  }
}

이 경우 애플리케이션이 직접 KMS Decrypt API를 호출하면 403이 납니다. 해결은 둘 중 하나입니다.

  • 정말 S3 경유가 의도라면: 직접 Decrypt 호출을 없애고 S3 GetObject로 처리
  • 직접 Decrypt가 필요하다면: kms:ViaService 조건을 제거하거나 별도 키/별도 statement로 분리

4.2 kms:EncryptionContext:* 조건 불일치

KMS는 암호화 시점에 EncryptionContext를 넣을 수 있고, 복호화 시에도 동일 컨텍스트를 요구하도록 정책을 걸 수 있습니다.

예: Secrets Manager, EKS Envelope Encryption, 앱 자체 암복호화 라이브러리 등에서 컨텍스트가 자동으로 붙습니다. 정책이 아래처럼 되어 있으면:

{
  "Effect": "Allow",
  "Action": "kms:Decrypt",
  "Resource": "*",
  "Condition": {
    "StringEquals": {
      "kms:EncryptionContext:app": "myapp"
    }
  }
}

복호화 요청에 EncryptionContext가 없거나 값이 다르면 403입니다.

SDK 예시 (Python/boto3)

import boto3

kms = boto3.client("kms", region_name="ap-northeast-2")

resp = kms.decrypt(
    CiphertextBlob=ciphertext_blob,
    EncryptionContext={"app": "myapp"}
)
plaintext = resp["Plaintext"]

디버깅 팁:

  • 암호문이 어디서 생성됐는지(누가 어떤 컨텍스트로 encrypt했는지)부터 추적해야 합니다.
  • 같은 키라도 컨텍스트 정책이 걸리면 “키 ARN과 권한이 맞아도” 실패합니다.

4.3 kms:CallerAccount, kms:PrincipalArn 조건

조직에서 “특정 Role ARN만 허용” 같은 조건을 넣는 경우, IRSA Role ARN이 생각과 다르면 막힙니다.

  • aws sts get-caller-identity로 나온 assumed-role ARN과
  • 정책에서 기대하는 PrincipalArn이 일치하는지 확인하세요.

5) 리전/키 ARN/엔드포인트 불일치: 멀티리전에서 자주 터진다

EKS는 AWS_REGION/AWS_DEFAULT_REGION이 애매하게 설정되는 경우가 있습니다. KMS 키가 ap-northeast-2에 있는데, SDK가 us-east-1로 호출하면 다른 키를 찾거나 권한이 어긋나 403/NotFound류가 섞여 나옵니다.

체크:

# Pod 내부
aws configure list

# 명시적으로 리전 지정해서 KMS 호출
aws kms describe-key --key-id arn:aws:kms:ap-northeast-2:123456789012:key/.... --region ap-northeast-2

권장:

  • 애플리케이션에 AWS_REGION을 명시
  • KMS client 생성 시 region_name을 명시

6) “키는 맞는데 403”: KMS Grant/서비스 통합 사용 여부 확인

EKS 워크로드가 직접 KMS를 때리는 게 아니라, 중간 서비스(예: EBS CSI, Secrets Manager, S3, SSM Parameter Store)가 KMS를 호출하는 구조면 권한 주체가 달라집니다.

  • 예: 앱이 Secrets Manager에서 시크릿을 가져오면, 복호화는 보통 Secrets Manager 서비스가 수행(또는 서비스가 KMS를 호출)하며, 키 정책에 서비스 프린시펄이 필요할 수 있습니다.
  • 예: EBS 암호화는 EC2/EBS 서비스가 KMS를 사용합니다.

이때는 키 정책에 아래처럼 서비스 프린시펄을 허용해야 하는 케이스가 있습니다(환경에 따라 다름).

{
  "Sid": "AllowSecretsManagerUse",
  "Effect": "Allow",
  "Principal": {"Service": "secretsmanager.amazonaws.com"},
  "Action": ["kms:Decrypt", "kms:GenerateDataKey"],
  "Resource": "*"
}

핵심은 “누가 KMS를 호출하는가”입니다. CloudTrail에서 KMS 이벤트를 보면 실제 호출 주체가 바로 드러납니다.


7) 가장 빠른 정답지: CloudTrail로 KMS 이벤트 한 방에 좁히기

403의 원인은 정책/조건이 대부분이라, 추측보다 CloudTrail의 KMS Decrypt 이벤트를 보는 게 빠릅니다.

확인할 필드:

  • userIdentity.arn / principalId: 실제 호출 주체
  • eventSource: kms.amazonaws.com
  • errorCode, errorMessage: 어떤 조건/정책에서 막혔는지 힌트
  • requestParameters.encryptionContext: 컨텍스트가 들어왔는지
  • resources: 어떤 키를 대상으로 했는지

CloudTrail Lake/Athena를 쓰면 쿼리로 빠르게 찾을 수 있습니다.


8) 재현 가능한 최소 권한 예시(권장 베이스라인)

8.1 IRSA Role에 붙일 IAM 정책(Decrypt만 필요한 경우)

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowDecryptSpecificKey",
      "Effect": "Allow",
      "Action": [
        "kms:Decrypt",
        "kms:DescribeKey"
      ],
      "Resource": "arn:aws:kms:ap-northeast-2:123456789012:key/abcd-efgh-..."
    }
  ]
}

8.2 KMS Key policy에 Role 허용(동일 계정 가정)

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EnableRootPermissions",
      "Effect": "Allow",
      "Principal": {"AWS": "arn:aws:iam::123456789012:root"},
      "Action": "kms:*",
      "Resource": "*"
    },
    {
      "Sid": "AllowDecryptForMyAppIrsaRole",
      "Effect": "Allow",
      "Principal": {"AWS": "arn:aws:iam::123456789012:role/myapp-irsa-role"},
      "Action": ["kms:Decrypt", "kms:DescribeKey"],
      "Resource": "*"
    }
  ]
}

운영에서는 EnableRootPermissions를 더 제한하는 조직도 많지만, 디버깅 단계에서는 이 구조가 원인 분리에 유리합니다.


9) 실전 트러블슈팅 체크리스트(위에서 아래로)

  1. Pod 내부에서 aws sts get-caller-identityassumed-role이 맞는지 확인
  2. 호출 리전이 키 리전과 같은지 확인 (AWS_REGION, SDK client region)
  3. IAM Role에 kms:Decrypt정확한 Key ARN으로 허용되어 있는지 확인
  4. KMS Key policy에 그 Role이 Principal로 허용되어 있는지 확인
  5. 키 정책/IAM 정책에 kms:ViaService, kms:EncryptionContext:*, kms:PrincipalArn 같은 조건이 있는지 확인
  6. CloudTrail에서 KMS Decrypt 이벤트를 찾아 실제 호출 주체/컨텍스트/키를 확인
  7. 서비스 통합(Secrets Manager, S3, EBS 등)이라면 서비스 프린시펄 허용 여부 확인

IRSA 자체 문제로 보이지만 실제로는 “KMS의 정책 모델(키 정책 + 조건)”에서 틀어지는 경우가 대부분입니다.


10) 마무리: ‘IRSA 정상 + KMS 403’은 KMS가 정답이다

정리하면, IRSA가 정상적으로 역할을 받아오는 상황에서 KMS Decrypt만 403이면 원인은 거의 아래 셋 중 하나로 수렴합니다.

  • KMS Key policy에 Role(또는 위임)이 없다
  • 조건(EncryptionContext/ViaService/PrincipalArn 등)이 불일치
  • 리전/키/호출 주체(서비스 통합)가 생각과 다르다

특히 CloudTrail로 KMS 이벤트를 보면 “누가/어떤 키에/어떤 컨텍스트로 요청했는지”가 바로 보여서, 삽질 시간을 크게 줄일 수 있습니다.

추가로, IRSA 자체가 간헐적으로 실패하거나 AssumeRoleWithWebIdentity에서 타임아웃이 난다면 아래 글을 먼저 점검해 두면 전체 인증 경로를 안정화하는 데 도움이 됩니다.