Published on

EKS ExternalSecret 미동작 - IRSA·KMS·권한 10분 진단

Authors

서버리스든 마이크로서비스든, EKS에서 External Secrets Operator(ESO)를 붙여 AWS Secrets Manager/SSM Parameter Store 값을 Kubernetes Secret으로 동기화하는 패턴은 거의 표준이 됐습니다. 그런데 운영에서 자주 겪는 문제가 하나 있습니다. ExternalSecret 리소스는 적용됐는데 Secret이 안 생기거나, 계속 Error/NotReady로 남는 현상입니다.

대부분의 경우 원인은 세 갈래로 수렴합니다.

  1. IRSA(OIDC/TrustPolicy/ServiceAccount) 불일치
  2. KMS 복호화 권한(kms:Decrypt) 또는 키 정책 문제
  3. Secrets Manager/SSM IAM 권한/리소스 ARN 스코프 오류

이 글은 “10분 안에” 원인을 좁히는 순서로 구성했습니다. (깊게 파기 전에, 먼저 어디서 깨지는지 빠르게 분기)

관련해서 IRSA에서 AccessDenied가 날 때의 더 상세한 체크리스트는 다음 글도 함께 보면 좋습니다: EKS IRSA인데 AccessDenied? OIDC·TrustPolicy·SA 점검


0) 먼저 용어 정리: 어디가 실패할 수 있나

ESO를 기준으로 보면 동기화는 대략 이렇게 흐릅니다.

  1. ExternalSecret(또는 PushSecret)이 SecretStore/ClusterSecretStore를 참조
  2. ESO 컨트롤러 Pod가 ServiceAccount로 실행
  3. Pod는 IRSA로 AWS STS AssumeRoleWithWebIdentity
  4. 얻은 Role로 Secrets Manager/SSM 읽기
  5. (해당 시) KMS로 복호화
  6. Kubernetes API로 Secret 생성/업데이트

따라서 진단도 (A) 쿠버네티스 리소스 참조 문제 → (B) IRSA → (C) AWS 권한(Secrets/SSM) → (D) KMS 순으로 가면 빠릅니다.


1) 1분 컷: ExternalSecret/Store 상태와 이벤트로 1차 분기

가장 먼저 “어디서” 실패하는지 확인합니다.

# ExternalSecret 상태/이벤트
kubectl -n <ns> describe externalsecret <name>

# SecretStore/ClusterSecretStore 상태
kubectl -n <ns> get secretstore
kubectl -n <ns> describe secretstore <store-name>
# 또는
kubectl describe clustersecretstore <store-name>

여기서 흔히 보이는 힌트:

  • could not get secret data from provider: AWS 호출(권한/네트워크/리전)
  • AccessDenied / Not authorized: IRSA 또는 IAM 정책
  • InvalidIdentityToken: OIDC/TrustPolicy/SA 매칭
  • kms:Decrypt 관련 메시지: KMS 권한/키 정책
  • SecretSyncedError인데 K8s 이벤트에 forbidden이 뜨면: Kubernetes RBAC 문제(ESO가 Secret 생성 권한이 없음)

> 참고: RBAC 문제는 IRSA/KMS/IAM과 별개로 “마지막 단계”에서 터집니다. 이벤트에 secrets is forbidden 류가 보이면 AWS가 아니라 K8s 권한부터 고치세요.


2) 3분 컷: ESO 컨트롤러 로그에서 에러 문자열로 원인 고정

컨트롤러 로그는 거의 정답지를 줍니다.

# 설치 방식에 따라 deployment 이름이 다를 수 있음
kubectl -n external-secrets get pods
kubectl -n external-secrets logs deploy/external-secrets -f --tail=200

# 특정 ExternalSecret 관련 키워드로 필터
kubectl -n external-secrets logs deploy/external-secrets --tail=500 | \
  egrep -i "accessdenied|invalididentitytoken|assumerole|kms|decrypt|secretsmanager|ssm|throttl|timeout"

로그에서 자주 나오는 패턴과 의미:

  • AccessDenied: Not authorized to perform sts:AssumeRoleWithWebIdentity
    • IRSA TrustPolicy/OIDC/SA annotation 문제
  • InvalidIdentityToken: No OpenIDConnect provider found in your account
    • 클러스터 OIDC provider 미생성 또는 잘못된 issuer
  • AccessDeniedException: User is not authorized to perform: secretsmanager:GetSecretValue
    • IAM 정책(리소스 ARN/액션) 문제
  • AccessDeniedException: ... kms:Decrypt
    • KMS 권한 또는 키 정책 문제

3) 5분 컷: IRSA(서비스어카운트 ↔ IAM Role)부터 검증

ExternalSecret이 안 붙는 케이스의 절반 이상은 IRSA입니다. 아래 3가지만 빠르게 확인해도 대부분 잡힙니다.

3-1) ESO가 사용하는 ServiceAccount 확인

# ESO 컨트롤러가 어떤 SA로 뜨는지
kubectl -n external-secrets get deploy external-secrets -o jsonpath='{.spec.template.spec.serviceAccountName}{"\n"}'

# SA에 role-arn annotation이 있는지
kubectl -n external-secrets get sa <sa-name> -o yaml | sed -n '/annotations:/,/^$/p'

정상이라면 보통 아래가 있어야 합니다.

annotations:
  eks.amazonaws.com/role-arn: arn:aws:iam::<ACCOUNT_ID>:role/<ESO_ROLE>

3-2) Pod에 WebIdentity 토큰/환경변수가 주입됐는지

POD=$(kubectl -n external-secrets get pod -l app.kubernetes.io/name=external-secrets -o name | head -n1)

kubectl -n external-secrets exec -it ${POD} -- sh -lc '
  env | egrep "AWS_ROLE_ARN|AWS_WEB_IDENTITY_TOKEN_FILE|AWS_REGION|AWS_DEFAULT_REGION"; 
  ls -l $AWS_WEB_IDENTITY_TOKEN_FILE
'
  • AWS_ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE가 없으면: SA annotation 미적용, 또는 Pod가 해당 SA를 안 씀.

3-3) IAM Role TrustPolicy의 sub/aud 조건이 SA와 일치하는지

TrustPolicy에서 가장 자주 틀리는 건 sub(namespace/name)입니다.

aws iam get-role --role-name <ESO_ROLE> --query 'Role.AssumeRolePolicyDocument' --output json

예시(핵심만):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::<ACCOUNT_ID>:oidc-provider/oidc.eks.<region>.amazonaws.com/id/<OIDC_ID>"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "oidc.eks.<region>.amazonaws.com/id/<OIDC_ID>:sub": "system:serviceaccount:external-secrets:<sa-name>",
          "oidc.eks.<region>.amazonaws.com/id/<OIDC_ID>:aud": "sts.amazonaws.com"
        }
      }
    }
  ]
}
  • namespace가 다르거나 SA 이름이 다르면 100% 실패합니다.
  • aud가 누락/불일치여도 실패할 수 있습니다.

IRSA에서 더 자주 터지는 케이스(클러스터 OIDC provider 미생성, issuer mismatch, 조건 키 오타 등)는 위 내부 링크 글을 참고하세요: EKS IRSA인데 AccessDenied? OIDC·TrustPolicy·SA 점검


4) 7분 컷: Secrets Manager/SSM 권한(리소스 ARN 스코프) 확인

IRSA가 통과하면 다음은 “무엇을 읽을 권한이 있나”입니다.

4-1) 필요한 최소 액션

  • Secrets Manager 사용 시(일반적인 경우)
    • secretsmanager:GetSecretValue
    • secretsmanager:DescribeSecret
    • (옵션) secretsmanager:ListSecrets는 보통 불필요(운영 최소권한에선 제외)
  • SSM Parameter Store 사용 시
    • ssm:GetParameter, ssm:GetParameters, ssm:GetParametersByPath

4-2) 가장 흔한 실수: Secret ARN 형식/와일드카드

Secrets Manager의 ARN은 종종 뒤에 랜덤 suffix가 붙습니다.

  • 예: arn:aws:secretsmanager:ap-northeast-2:123456789012:secret:prod/db-password-AbCdEf

정책에서 리소스를 ...:secret:prod/db-password로만 잡으면 매칭이 안 됩니다. 보통은 suffix를 고려해 *를 붙입니다.

{
  "Effect": "Allow",
  "Action": [
    "secretsmanager:GetSecretValue",
    "secretsmanager:DescribeSecret"
  ],
  "Resource": [
    "arn:aws:secretsmanager:ap-northeast-2:123456789012:secret:prod/*"
  ]
}

4-3) 리전 불일치도 의외로 자주 발생

  • EKS는 ap-northeast-2인데 Secret은 us-east-1에 있음
  • ESO SecretStore에 region을 안 적어서 기본값이 다르게 잡힘

SecretStore 예시(Secrets Manager):

apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: aws-secrets
  namespace: app
spec:
  provider:
    aws:
      service: SecretsManager
      region: ap-northeast-2
      auth:
        jwt:
          serviceAccountRef:
            name: external-secrets-sa

5) 9분 컷: KMS 복호화(kms:Decrypt)와 Key Policy 함정

Secrets Manager/SSM이 **KMS CMK(고객 관리형 키)**로 암호화된 경우, IAM에 Secrets 권한만 있어서는 부족합니다. 최종적으로 KMS에서 복호화가 막힙니다.

5-1) IAM 정책에 kms:Decrypt 추가

{
  "Effect": "Allow",
  "Action": [
    "kms:Decrypt",
    "kms:DescribeKey"
  ],
  "Resource": "arn:aws:kms:ap-northeast-2:123456789012:key/<KEY_ID>"
}
  • SSM SecureString이면 kms:Decrypt가 거의 필수입니다.
  • Secrets Manager도 CMK를 쓰면 필요합니다.

5-2) Key Policy가 더 강하게 막는 케이스

KMS는 IAM 정책 + Key policy 둘 다 통과해야 합니다.

  • IAM에 kms:Decrypt가 있어도 Key policy에서 Role을 허용하지 않으면 실패
  • 특히 보안팀이 키 정책을 “특정 principal만” 허용하도록 잠가둔 환경에서 자주 발생

점검 방법:

aws kms get-key-policy --key-id <KEY_ID> --policy-name default --output text

Key policy에 최소한 해당 Role(또는 계정 루트에 대한 적절한 위임)이 들어가야 합니다.

5-3) 조건(EncryptionContext) 때문에 막히는 케이스

조직에서 KMS에 kms:EncryptionContext:* 조건을 걸어두면, 서비스가 넣는 컨텍스트와 맞지 않아 AccessDenied가 납니다. 이 경우는 로그에 컨텍스트 관련 단서가 나오며, 키 정책/조건 설계를 다시 봐야 합니다.


6) 마지막 1분: Kubernetes RBAC(Secret 생성 권한) 확인

AWS 쪽이 다 맞아도, ESO가 K8s Secret을 만들 권한이 없으면 최종 결과는 “ExternalSecret은 있는데 Secret이 없음”입니다.

# external-secrets 네임스페이스에서 컨트롤러 SA가 secrets를 만들 수 있는지
kubectl auth can-i create secrets \
  --as system:serviceaccount:external-secrets:<sa-name> \
  -n <target-namespace>

kubectl auth can-i update secrets \
  --as system:serviceaccount:external-secrets:<sa-name> \
  -n <target-namespace>

no가 나오면 설치된 ClusterRole/RoleBinding 범위가 부족한 것입니다.


7) 현장에서 바로 쓰는 “10분 진단” 체크리스트

아래 순서대로 보면, 보통 10분 안에 원인 범위를 고정할 수 있습니다.

  1. kubectl describe externalsecret에서 이벤트 확인 (에러 문자열 확보)
  2. ESO 컨트롤러 로그에서 AccessDenied / InvalidIdentityToken / kms:Decrypt 키워드 확인
  3. ESO Pod의 SA 확인 → SA annotation(eks.amazonaws.com/role-arn) 확인
  4. Pod 내부에 AWS_WEB_IDENTITY_TOKEN_FILE, AWS_ROLE_ARN 주입 확인
  5. IAM Role TrustPolicy의 sub=system:serviceaccount:<ns>:<sa> 일치 확인
  6. IAM 정책에서 secretsmanager:GetSecretValue 또는 ssm:GetParameter* 리소스 ARN 스코프 확인(특히 Secrets ARN suffix)
  7. CMK 사용 시 kms:Decrypt + Key policy에서 Role 허용 확인
  8. 마지막으로 kubectl auth can-i create secrets로 K8s RBAC 확인

8) (부록) 재현 가능한 예시: ExternalSecret + IRSA 최소 구성

8-1) ServiceAccount

apiVersion: v1
kind: ServiceAccount
metadata:
  name: external-secrets-sa
  namespace: app
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/eso-app-role

8-2) SecretStore

apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: aws-secrets
  namespace: app
spec:
  provider:
    aws:
      service: SecretsManager
      region: ap-northeast-2
      auth:
        jwt:
          serviceAccountRef:
            name: external-secrets-sa

8-3) ExternalSecret

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: db-credentials
  namespace: app
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets
    kind: SecretStore
  target:
    name: db-credentials
    creationPolicy: Owner
  data:
    - secretKey: password
      remoteRef:
        key: prod/db-password

8-4) IAM 정책(예시)

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "secretsmanager:GetSecretValue",
        "secretsmanager:DescribeSecret"
      ],
      "Resource": "arn:aws:secretsmanager:ap-northeast-2:123456789012:secret:prod/*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "kms:Decrypt",
        "kms:DescribeKey"
      ],
      "Resource": "arn:aws:kms:ap-northeast-2:123456789012:key/<KEY_ID>"
    }
  ]
}

마무리

ExternalSecret이 “안 붙는다”는 증상은 같아도, 실제 원인은 IRSA(신원) → IAM(권한) → KMS(복호화) → K8s RBAC(생성) 중 한 군데에서 끊긴 결과입니다. 핵심은 감으로 고치지 말고, describe 이벤트와 컨트롤러 로그에서 에러 문자열을 먼저 확보한 뒤 위 순서대로 분기하는 것입니다.

특히 sts:AssumeRoleWithWebIdentity류의 에러가 보이면 IRSA 쪽이 거의 확정이며, 이 경우 더 자세한 점검 포인트는 다음 글에서 확장해서 확인할 수 있습니다: EKS IRSA인데 AccessDenied? OIDC·TrustPolicy·SA 점검