Intro

Service Account token을 통해 이미지를 가져오는 것은 kubernetes 1.33에서 알파 릴리즈되고 1.34에 베타로 올라간 기능이다. 실제 EKS 환경에서 Pod에서 이미지를 가져오기까지 과정을 살펴보고, 이러한 기능이 왜 추가 됐는지 확인해보자.

Image pull 인증

보통 이미지를 가져올떄 인증 방법은 노드에 할당된 role을 기반으로 registry에서 이미지를 가져오는 것으로 이해한다.

공식 kuberntes 문서에서 소개하는 방법은 다음 문서에서 확인할 수 있고, 다음과 같은 예시가 있다.

  • Pod에 imagePullSecrets 명시하기
  • Node가 Private Registry에 접근할 수 있도록 설정하기
  • kubelet credential provider plugin 사용하기
  • pre-pulled image 사용하기

EKS에서는 어떻게 이미지를 가져올까?

  1. sandbox 이미지 AWS ecr에서 가져온다. 그리고 pinning까지 해주는 것을 볼 수 있다.

간혹 docker image prune 과 같은 명령어로 노드에서 이미지를 정리하는 경우가 있다. 이때, sandbox image를 의도치 않게 삭제되기 떄문에 pinning 해버리는 것으로 로직이 변경됐다.

# pull the sandbox using aws credentials and tag it under a different name
PULL_ARGS=""
if [[ "${PAUSE_CONTAINER_IMAGE}" == *"dkr.ecr"* ]]; then
  PULL_ARGS="${PULL_ARGS} --user AWS:$(aws ecr get-login-password)"
fi

sudo ctr --namespace k8s.io image label ${TAG} io.cri-containerd.pinned=pinned
  1. Pod 이미지 Kubernetes 1.27 이상 부터 kubelet이 ecr-credential-provider 플러그인을 통해 ECR credential을 가져오고, nodeadm에서 해당 설정을 초기화한다. 1

실제 로직을 보면 ecr 이미지만 ecr-credential-provider 플러그인을 통해 이미지를 가져오도록 설정된 것을 볼 수 있다.

이러한 플러그인 통해 가져오는 실제 동작은 kubernetes 문서에서 잘 설명되어있다. 2 kubelet이 플러그인 binary 파일을 직접 실행하고 표준 입출력으로 통신해서 credential을 가져온다.

The kubelet and the exec plugin communicate through stdio (stdin, stdout, and stderr) using Kubernetes versioned APIs. These plugins allow the kubelet to request credentials for a container registry dynamically as opposed to storing static credentials on disk.

표준 입/출력으로 golang에 전달받는 예시는 아래와 같다.

type CredentialProviderRequest struct {
        Image string
}
 
 
type CredentialProviderResponse struct {
	metav1.TypeMeta
	CacheKeyType PluginCacheKeyType
	CacheDuration *metav1.Duration
	Auth map[string]AuthConfig
}

ecr의 예시로 보면, 아래와 같이 kubelet이 Pod 생성중 이미지 pull을 요청하면 ecr Plugin이 GetAuthorizeToken으로 이미지를 가져온다. 해당 인증 정보를 메모리에 캐싱해두게된다.

[그림1. Image credential Provider 동작 방식]

이렇게 credential provider는 kubelet 레벨에서 실행되므로, 노드내 모든 Pod는 같은 credential으로 모든 이미지에 접근이 가능하다. 이는 최소권한 정책에 위배되기 때문에 Pod 단위에서 어떤 이미지를 가져올 수 있을지 이미지 보안을 강화하기 위해서는 적절하지 않을 수 있다.

imagePullSecret을 사용하는 경우, long-lived 인증 + service account나 pod에 명시적으로 attach 되어있어 탈취될 위험도 있다.

따라서 EKS에서는 Pod에 마운트된 service account token을 통해 이미지를 가져올 수 있도록 설정할 수 있다.

Info

이는 kubernetes의 기능으로, ecr Plugin뿐만 아니라 표준I/O로 들어오는 CredentialProviderRequest/CredentialProviderResponse type의 인증 처리 binary만 따로 구현하면 기타 service account token을 활용해서도 사용할 수 있다.

Image pull with service account token

pod에 bound된 service account token을 사용하여 이미지를 가져올 수 있다.

사용 방법은 간단한데, kubelet에 --image-credential-provider-config 에 경로를 전달해주면 되고, config spec은 다음과 같다. ( 3 문서에서, 주석만 제거해왔다.)

apiVersion: kubelet.config.k8s.io/v1
kind: CredentialProviderConfig
providers:
  - name: ecr-credential-provider
    matchImages:
      - "*.dkr.ecr.*.amazonaws.com"
      - "*.dkr.ecr.*.amazonaws.com.cn"
      - "*.dkr.ecr-fips.*.amazonaws.com"
      - "*.dkr.ecr.us-iso-east-1.c2s.ic.gov"
      - "*.dkr.ecr.us-isob-east-1.sc2s.sgov.gov"
    defaultCacheDuration: "12h"
    apiVersion: credentialprovider.kubelet.k8s.io/v1
    env:
      - name: AWS_PROFILE
        value: example_profile
    tokenAttributes:
      # +required
      serviceAccountTokenAudience: "<audience for the token>"
      # +required
      cacheType: "<Token or ServiceAccount>"
      # +required
      requireServiceAccount: true
      # +optional
      requiredServiceAccountAnnotationKeys:
      - "example.com/required-annotation-key-1"
      - "example.com/required-annotation-key-2"
      # +optional
      optionalServiceAccountAnnotationKeys:
      - "example.com/optional-annotation-key-1"
      - "example.com/optional-annotation-key-2"

tokenAttributes 이하부터, service account token을 이용하여 image를 가져오기 위한 설정으로 각 설정은 다음과 같다.

  • serviceAccountTokenAudience: 인증에 사용된 audience이다. 요 값은 필수이고 설정하게 되면 kubelet에서 여기에 기입된 audience용 service account token을 생성하고 plugin으로 넘긴다. EKS의 경우 sts.amazonaws.com 으로 설정할 수 있다.
  • cacheType: Pod에 연결된 token을 기반으로 cache할지, ServiceAccount 기반으로 cache할지 결정한다. 같은 ServiceAccount 를 기반으로cache를 하기 위해서는 ServiceAccount 타입으로 쓰면된다.
  • requireServiceAccount: 토큰 인증을 강제하기 위한 설정이다. false로 설정시, CredentialProviderRequest에 토큰 정보가 들어가지 않는다.
  • requiredServiceAccountAnnotationKeys/optionalServiceAccountAnnotationKeys: serviceaccount에 필요한/추가 annotation key를 설정하는 부분이다. 요기에 annotation key를 추가하면 플러그인의 입력 CredentialProviderRequest으로 요청이 넘어간다.

사용방법

1. RBAC 설정 참고 예시 clusterrole/clusterrolebinding을 설정한다. 위에서 설명한 것처럼 kubelet이 service account token을 생성할 수 있도록 권한을 추가해주어야한다.

Node Authrization과 비슷하게 kubelet이 service account token을 요청하기 떄문에 system:nodes에 RBAC을 추가한다.

# RBAC for audience restriction
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: registry-audience-access
rules:
- verbs: ["request-serviceaccounts-token-audience"]
  apiGroups: [""]
  resources: ["my-registry-audience"]
  resourceNames: ["registry-access-sa"]  # Optional: specific ServiceAccount
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: kubelet-registry-audience
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: registry-audience-access
subjects:
- kind: Group
  name: system:nodes
  apiGroup: rbac.authorization.k8s.io

2. image-credential-provider 설정 추가 해당 yaml 파일을 노드에 저장하고, kubelet 설정 --image-credential-provider-config 으로 넘기면 된다.

EKS 사용하는 경우, 아래와 같이 userdata를 수정할 수 있다.

EKS의 노드 join을 위한 nodeadm 명령어는 userdata가 전부 끝나는, cloud-init-final 이후에 실행되기 때문에, Content-Type으로 scripts와 nodeadm을 분리해서 설정하면 따로 race condition 문제없이 잘 적용된다.

MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="BOUNDARY"

--BOUNDARY
Content-Type: application/node.eks.aws

---
apiVersion: node.eks.aws/v1alpha1
kind: NodeConfig
spec:
  cluster:
    apiServerEndpoint: API_SERVER_ENDPOINT
    certificateAuthority: CERTIFICATE
    cidr: SERVICE_IPv4_RANGE
    name: CLUSTER_NAME
  kubelet:
    config:
      maxPods: 17 
    flags:
    - "--image-credential-provider-config=/etc/eks/image-credential-provider/custom-config.json" 
...

--BOUNDARY
Content-Type: text/x-shellscript;

#!/bin/bash
cat <<EOF > /etc/eks/image-credential-provider/custom-config.json
apiVersion: kubelet.config.k8s.io/v1
kind: CredentialProviderConfig
providers:
  - name: ecr-credential-provider
    matchImages:
      - "*.dkr.ecr.*.amazonaws.com"
      - "*.dkr.ecr.*.amazonaws.com.cn"
      - "*.dkr.ecr-fips.*.amazonaws.com"
      - "*.dkr.ecr.us-iso-east-1.c2s.ic.gov"
      - "*.dkr.ecr.us-isob-east-1.sc2s.sgov.gov"
    defaultCacheDuration: "12h"
    apiVersion: credentialprovider.kubelet.k8s.io/v1
    tokenAttributes:
      # +required
      serviceAccountTokenAudience: "sts.amazonaws.com"
      # +required
      cacheType: "Token"
      # +required
      requireServiceAccount: true
      # +optional
      requiredServiceAccountAnnotationKeys:
      - "eks.amazonaws.com/ecr-role-arn"
      - "eks.amazonaws.com/role-arn"
EOF
--BOUNDARY--
  • 현재 EKS join에 사용되는 nodeadm에서 기본적은 config를 생성하는 것을 확인할 수 있다. 4

3. serviceaccount에 annotation 추가 아래와 같이 ServiceAccount에 `eks.amazonaws.com/ecr-role-arn`을 기입해서 EKS 환경에서 사용할 수 있다

apiVersion: v1
kind: ServiceAccount
metadata:
  name: registry-access-sa
  namespace: default
  annotations:
	eks.amazonaws.com/ecr-role-arn: <ecr-role-arn>
	eks.amazonaws.com/role-arn: <role-arn>

Info

ecr-role-arn은 ecr 연결시 사용하는 IAM role이고, kubelet이 서비스 계정 토큰을 생성하여 CredentialProvider로 넘긴다 role-arn은 IRSA을 위해 사용되는 role이고 ecr-role-arn을 수임하기 위해 사용된다.

cloudtrail을 통해 호출되는 정보를 보면 아래와 같이, IRSA role에 의해 ecr-credential-provider가 role을 수임하는 것을 볼 수 있다.

"requestParameters": {
	"roleArn": "arn:aws:iam::xx:role/ecr-no-policy-role",
	"roleSessionName": "ecr-credential-provider"
},
"responseElements": {
	"credentials": {
		"accessKeyId": "",
		"sessionToken": "",
		"expiration": "
},
	"subjectFromWebIdentityToken": "system:serviceaccount:default:demo-app-sa",
	"assumedRoleUser": {
		"arn": "<IRSA-ROLE>"
	},
	"provider": "<eks-oidc-provider>",
	"audience": "sts.amazonaws.com"
},

그 외 audience등이 잘못되어서 AssumeRoleWithWebIdentity이 되지 않으면 아래와 같이 오류가 발생할 수 있다.

Jan 13 06:00:38 ip-10-0-129-245.ap-northeast-2.compute.internal kubelet[2030]: E0113 06:00:38.834786    3738 main.go:352] Error running credential provider plugin: operation error ECR: GetAuthorizationToken, get identity: get credentials: failed to assume role: operation error STS: AssumeRoleWithWebIdentity, https response error StatusCode: 400, RequestID: 85887559-2a68-42a1-bc9f-7851ca7a148e, InvalidIdentityToken: Incorrect token audience

플러그인 이름은 1개만 존재할 수 있고, 중복되는 경우 아래와 같이 오류가 발생한다.

Dec 30 03:20:24 ip-10-0-149-175.ap-northeast-2.compute.internal kubelet[2342]: E1230 03:20:24.587135    2342 kuberuntime_manager.go:302] "Failedto register CRI auth plugins" err="duplicate provider name \"ecr-credential-provider\" found in configuration file(s)"

참고자료

Footnotes

  1. https://awslabs.github.io/amazon-eks-ami/usage/overview/#image-credential-provider-plugins

  2. https://kubernetes.io/docs/tasks/administer-cluster/kubelet-credential-provider/

  3. https://kubernetes.io/docs/tasks/administer-cluster/kubelet-credential-provider/#service-account-token-for-image-pulls

  4. https://github.com/awslabs/amazon-eks-ami/blob/379c4c592236dfcd3243eafea6e9370342c7aa73/nodeadm/internal/kubelet/image-credential-provider.go#L33-L96