EKS에서 쿠버네티스 포드의 IAM 권한 제어하기: Pod Identity Webhook

용찬호

쿠버네티스 모자를 쓰고 있는 용감한 쿠키
쿠버네티스 모자를 쓰고 있는 용감한 쿠키

데브시스터즈에서는 여러 개의 쿠버네티스 클러스터를 구축해 게임 개발 및 운영, 데이터 분석, 로깅 등을 위한 인프라로서 활용하고 있습니다. 각 클러스터에서는 쿠버네티스의 기능을 확장하기 위해 약 20여개 이상의 애드온을 운영하고 있는데, 이번 글에서는 AWS EKS에서 포드(Pod)의 IAM 권한을 세밀하게 제어할 수 있는 Pod Identity Webhook 애드온의 동작 원리와 사용 방법을 설명해보려 합니다.

AWS에서 쿠버네티스 포드의 IAM 권한 제어

AWS EC2에서 인프라를 운영한다면 EC2 인스턴스 하나에 IAM 역할(Role) 하나를 대응시켜 해당 인스턴스에게 특정 AWS 권한을 부여할 수 있습니다. 하지만 EC2 인스턴스 상에서 쿠버네티스를 운영한다면 하나의 인스턴스에 여러 개의 포드 또는 컨테이너가 실행될 수 있으며, 이는 곧 해당 인스턴스에 부여한 IAM 역할을 인스턴스 상의 모든 포드들이 동일하게 사용할 수 있다는 것을 의미합니다. 각 포드의 권한들이 집중되어 있는 하나의 IAM 역할을 워커 그룹 내의 모든 포드가 공유하는 것은 보안적으로 바람직하지 않습니다.

EC2와 권한을 공유하는 경우와 그렇지 않은 경우
EC2와 권한을 공유하는 경우와 그렇지 않은 경우

포드 별로 IAM 권한을 분리하는 경우와 그렇지 않은 경우

이러한 상황을 방지하기 위해서는 각 포드마다 애플리케이션에 맞는 최소한의 IAM 역할만을 부여하되, 인스턴스의 IAM 역할과는 분리되어 있어야 합니다. 하지만 쿠버네티스가 자체적으로 이러한 기능을 제공하지는 않으며, kube2iam, kiam, Pod Identity Webhook 등의 애드온을 추가적으로 사용해야만 합니다.

kube2iam과 kiam을 통한 포드의 IAM 권한 제어

kube2iamkiam은 포드에 IAM 역할을 부여할 수 있는 쿠버네티스 애드온입니다. 정확히 말하자면, 포드가 특정 IAM 역할을 사용하도록 Assume Role을 대신 수행하는 역할을 담당합니다. 원래대로라면 포드는 인스턴스의 IAM 역할을 그대로 물려받아야 하지만, kube2iam과 kiam을 사용하면 포드가 특정 IAM 역할인 것처럼 변장(Assume)시켜주는 것이 가능합니다.

이 때, kube2iam과 kiam은 다른 포드에 부여될 IAM 역할을 Assume할 수 있어야 하므로 인스턴스에 부여된 IAM 역할을 통해 Assume Role 권한을 사용할 수 있어야 합니다.

kiam과 kube2iam의 동작 원리
kiam과 kube2iam의 동작 원리

kiam과 kube2iam의 동작 원리

kube2iam과 kiam의 동작 원리는 매우 간단합니다.

  1. EC2 인스턴스 메타데이터 API를 호출할 때 사용되는 주소인 169.254.169.254로 향하는 트래픽을 kube2iam이나 kiam으로 라우팅하도록 iptables DNAT 규칙을 워커 호스트에 추가합니다. 이 작업은 보통 애드온이 자동으로 수행합니다.

    169.254.169.254로 향하는 요청을 가로채는 iptables 규칙
    169.254.169.254로 향하는 요청을 가로채는 iptables 규칙

  2. 포드가 사용하는 서비스 어카운트(ServiceAccount)의 주석(Annotation)에 해당 포드가 사용할 IAM 역할을 명시합니다.

    포드가 사용할 IAM 역할을 서비스 어카운트에 명시
    포드가 사용할 IAM 역할을 서비스 어카운트에 명시

  3. 포드에서 AWS API를 호출할 경우, 1번에서 추가된 iptables 규칙에 의해 API 요청은 kube2iam이나 kiam으로 라우팅됩니다.
  4. kube2iam 또는 kiam은 해당 포드의 서비스 어카운트에 설정된 IAM 역할로 Assume Role을 수행해 해당 AWS API 요청을 전송합니다. 따라서 최종적으로 포드는 해당 IAM 역할에 정의된 권한만을 사용할 수 있습니다.
  5. Assume Role을 위한 임시 자격 증명(credential)은 kiam이나 kube2iam이 관리하기 때문에, 포드의 입장에서는 이러한 일련의 과정들이 모두 투명하게 수행됩니다.

kiam과 kube2iam의 핵심 기능이나 동작 원리는 대체로 비슷하지만, 두 애드온의 가장 큰 차이점은 그 구조에 있습니다.

kube2iam은 데몬셋으로 배포하기 때문에 모든 워커 노드의 IAM 역할에 Assume Role 권한을 부여해야 합니다. 이와 반대로 kiam은 서버-에이전트 구조로 되어 있으며, kiam 에이전트는 AWS API 요청을 가로채 kiam 서버로 전달할 뿐입니다. 따라서 실제적인 Assume Role은 kiam 서버에서 발생하기 때문에 kiam-server가 위치한 노드에만 Assume Role 권한을 부여하면 된다는 보안적인 장점도 있습니다.

데브시스터즈는 kubeadm, kops로 관리하는 클러스터에서 두 애드온을 사용했었으며, 간단히 정리해본 장단점은 아래와 같습니다.

  • kube2iam은 모든 워커 인스턴스에 Assume Role을 부여해야 하지만, 설치 및 관리가 상대적으로 간편하다는 장점이 있었습니다.
  • kiam은 kiam 서버가 위치한 워커 인스턴스에만 Assume Role 권한을 부여하면 되지만, 서버-에이전트 간 TLS 통신을 위해 인증서를 별도로 관리하거나 kiam 서버가 위치할 워커 그룹을 nodeSelector로 따로 지정해야 하는 등 별도로 신경써야 할 부분이 있었습니다.

무엇보다도 두 애드온 모두 오픈소스다보니 Helm 차트 및 프로젝트가 빠르게 업데이트되지 않는 등의 애로사항이 있었습니다. 뿐만 아니라 많은 애드온을 함께 운영하고 있다보니, 최대한 외부 의존적인 애드온의 갯수를 줄일 수 있었으면 하는 바람도 없지않아 있었습니다.

EKS Pod Identity Webhook을 통한 포드의 IAM 권한 제어

EKS에서는 kube2iam이나 kiam 대신 Pod Identity Webhook이라는 애드온을 사용해 포드의 IAM 역할을 관리할 수 있습니다. Pod Identity Webhook은 EKS에서만 사용할 수 있는 특별한 애드온으로, 쿠버네티스와 AWS의 자체 기능을 활용해 포드에 IAM 역할을 부여합니다.

데브시스터즈에서는 기존에 운영하던 kubeadm 및 kops 기반의 쿠버네티스 인프라를 EKS로 교체하였는데, 이에 따라 kube2iam와 kiam은 자연스럽게 Pod Identity Webhook으로 대체되었습니다. 하지만 Pod Identity Webhook은 한국어는 물론 영어로 되어 있는 레퍼런스가 그다지 많지 않은 편입니다. 때문에 Pod Identity Webhook의 사용 방법과 자세한 동작 원리 등을 이해하기가 어려울 수 있는데, 이번 글에서는 이를 명확하게 설명해보려 합니다.

Pod Identity Webhook 이미지 빌드 및 설치

Pod Identity Webhook을 사용하기 위해서는 아래의 준비물이 필요합니다.

  • 1.14 버전 이상의 EKS 쿠버네티스 클러스터
  • Pod Identity Webhook 도커 이미지

Pod Identity Webhook의 Github 저장소에서 Dockerfile을 제공하고는 있지만, 도커 이미지 자체가 Public하게 공개되어 있지는 않으므로 직접 이미지를 빌드해 사용해야 합니다. 아래 명령어로 적절히 도커 이미지를 빌드해 Docker Hub, ECR 등의 레지스트리에 Push합니다.

git clone https://github.com/aws/amazon-eks-pod-identity-webhook.git

cd amazon-eks-pod-identity-webhook && \
    docker build . -t <도커 이미지 이름> && \
    docker push <도커 이미지 이름>

공식 문서에서 가이드하는대로 Makefile을 실행하면 설치에 필요한 각종 리소스들이 자동으로 생성됩니다. 단, 쿠버네티스의 root CA 인증서 서명 요청을 위한 certificate 리소스가 Pending 상태일 수 있으므로 직접 certificate를 approve 해주는 작업이 필요할 수 있습니다.

make cluster-up IMAGE=<이미지 이름>

# Generating certs and deploying into active cluster...
# ...

kubectl certificate approve \
  $(kubectl get csr -o jsonpath='{.items[?(@.spec.username=="system:serviceaccount:default:pod-identity-webhook")].metadata.name}')

# certificatesigningrequest.certificates.k8s.io/csr-b6bd2 approved

Pod Identity Webhook의 역할

kube2iam이나 kiam과는 달리 Pod Identity Webhook은 Assume Role과 같은 동작을 수행하지 않습니다. 단지 어드미션 컨트롤러(Admission Controller) 중 mutating webhook이라는 쿠버네티스의 기능을 이용해 특정 IAM 역할을 사용할 포드에게 몇 가지 설정을 추가해줄 뿐입니다. make cluster-up 명령어를 실행했을 때 출력 결과를 유심히 살펴보았다면 mutating webhook도 함께 생성된 것을 알 수 있었을 것입니다.

make cluster-up IMAGE=devsisters/eks-pod-identity-webhook:latest
# Generating certs and deploying into active cluster...
# ...

kubectl apply -f deploy/mutatingwebhook-ca-bundle.yaml
# mutatingwebhookconfiguration.admissionregistration.k8s.io/pod-identity-webhook created

그렇다면 Pod Identity Webhook은 mutating webhook을 통해 어떠한 내용을 포드 스펙에 추가하는걸까요? 이를 직접 확인해보기 위해 간단한 포드를 직접 생성해 보겠습니다. kube2iam이나 kiam과 마찬가지로, 포드가 사용할 IAM 역할의 ARN을 서비스 어카운트의 주석에 추가했다는 점에 유의합니다.

cat << EOF | kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::1234567890:role/my-test-role # 적절한 값으로 수정해 사용합니다.
  name: debug-sa
  namespace: default
EOF

$ cat << EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: debug
spec:
  serviceAccount: debug-sa
  containers:
  - name: debug
    image: quay.io/devsisters/awscli
    args: ["tail", "-f", "/dev/null"]
EOF

kubectl get po debug -o yaml 명령어를 실행해서 포드 스펙을 자세히 들여다보면 아래와 같은 설정이 추가되었음을 알 수 있습니다.

# mutate webhook에 의해 추가된 부분만을 표시하였습니다.
 ...
    env:
    - name: AWS_ROLE_ARN
      value: arn:aws:iam::1234567890:role/my-test-role
    - name: AWS_WEB_IDENTITY_TOKEN_FILE
      value: /var/run/secrets/eks.amazonaws.com/serviceaccount/token
 ...
    volumeMounts:
    - mountPath: /var/run/secrets/eks.amazonaws.com/serviceaccount
      name: aws-iam-token
      readOnly: true
  ...
  volumes:
  - name: aws-iam-token
    projected:
      defaultMode: 420
      sources:
      - serviceAccountToken:
          audience: sts.amazonaws.com
          expirationSeconds: 86400
          path: token

Pod Identity Webhook이 포드에 추가한 설정은 2개의 환경 변수, 1개의 토큰 마운트가 전부입니다. 하지만 위 설정만으로도 포드가 특정 IAM 역할만을 사용하도록 만들기에는 충분합니다.

Token Projection과 EKS OpenID Connect Provider

쿠버네티스에서 서비스 어카운트(ServiceAccount)를 식별하는 가장 대표적인 방법은 아마 JWT 토큰일 것입니다. 서비스 어카운트를 생성하면 그에 대응하는 시크릿(Secret) 리소스가 자동으로 생성되며, 쿠버네티스 API 서버는 이 시크릿의 JWT 토큰을 통해 서비스 어카운트의 인증을 수행합니다. 하지만 이 JWT 토큰은 어디까지나 쿠버네티스 내에서 서비스 어카운트를 식별하기 위해 사용되는 것이기 때문에 OAuth2에서 쓰이는 exp, aud 등과 같은 속성이 들어가있지 않습니다.

서비스 어카운트의 토큰을 jwt.io에서 decode한 데이터
서비스 어카운트의 토큰을 jwt.io에서 decode한 데이터

서비스 어카운트의 토큰을 jwt.io에서 decode한 데이터

하지만 쿠버네티스 버전 1.12 이상부터는 projectedServiceAccountToken이라는 기능을 사용함으로써 토큰에 exp, aud등의 항목을 덧붙힐 수 있는데, Pod Identity Webhook이 mutate webhook을 통해 포드에 마운트한 토큰이 바로 이것입니다. 즉, 포드 내부에 마운트된 aws-iam-token 파일에는 JWT 토큰이 담겨져 있으며, 이 토큰을 jwt.io에서 decode 해보면 exp, aud 등의 속성이 추가로 들어있는 것을 확인할 수 있습니다.

# ...생략...
    volumeMounts:
    - mountPath: /var/run/secrets/eks.amazonaws.com/serviceaccount
      name: aws-iam-token
      readOnly: true

# ...생략...
  volumes:
  - name: aws-iam-token
    projected:
      defaultMode: 420
      sources:
      - serviceAccountToken:
          audience: sts.amazonaws.com
          expirationSeconds: 86400
          path: token

OIDC의 몇 가지 속성이 추가된 쿠버네티스 토큰
OIDC의 몇 가지 속성이 추가된 쿠버네티스 토큰

OIDC의 몇 가지 속성이 추가된 쿠버네티스 토큰

이 토큰에서 눈여겨 보아야할 부분은 iss 속성의 값입니다. https://oidc.eks.. 로 시작하는 이 URL은 EKS가 자체적으로 제공하는 OpenID Connect Provider (이하 EKS IdP) 주소입니다. EKS 쿠버네티스 버전 1.14 이상을 사용하면 자동으로 EKS IdP가 함께 생성되며, 이 EKS IdP를 통해 쿠버네티스가 발급한 토큰이 유효한지를 검증할 수 있습니다. EKS IdP 주소는 EKS 정보 페이지에서도 쉽게 확인할 수 있습니다.

EKS 정보에서 확인할 수 있는 IdP 주소
EKS 정보에서 확인할 수 있는 IdP 주소

EKS 정보에서 확인할 수 있는 IdP 주소

AWS는 이 기능을 이용해 포드에게 IAM 역할을 부여할 수 있는지를 검사합니다. 포드가 특정 IAM 역할으로 Assume 할 때 위 토큰을 AWS로 전송하게 되고, AWS의 입장에서는 이 토큰과 EKS IdP를 통해 포드가 해당 IAM 역할을 사용할 수 있는지를 검증합니다.

projectedServiceAccountToken 기능은 아래의 파라미터를 API 서버에 추가해야 사용할 수 있지만, EKS의 경우에는 자동으로 값이 설정되기 때문에 별도의 작업이 필요하지 않습니다.

--service-account-singing-key-file=<서비스 어카운트 토큰 발급에 사용된 비밀 키. 쿠버네티스의 root CA가 아닙니다>
--service-account-issuer=https://alicek106.devsisters.com
--api-audiences=devsisters

JWT 토큰을 통한 AssumeRoleWithWebIdentity

AWS SDK는 AWS_ROLE_ARNAWS_WEB_IDENTITY_TOKEN_FILE 라는 이름의 환경변수 값이 설정되어 있을 경우 해당 환경변수를 읽어들여 Web Identity 토큰으로 Assume Role을 시도합니다. 즉, $(AWS_ROLE_ARN) 라는 역할을 Assume하기 위한 자격 증명으로서 $(AWS_WEB_IDENTITY_TOKEN_FILE) 경로의 Web Identity 토큰 파일을 사용하겠다는 뜻입니다. 포드 내부의 AWS SDK는 이 토큰을 통해 AssumeRoleWithWebIdentity를 호출함으로써 임시 자격 증명을 획득하게 되고, 비로소 특정 IAM 역할로 변신할 수 있게 됩니다. 이 때, Assume Role 동작을 위한 인증은 AWS가 아닌 외부 Web IdP (이 경우에는 위에서 언급한 EKS IdP) 에 위임하게 됩니다.

${AWS_WEB_IDENTITY_TOKEN_FILE) 경로에 있는 Web Identity 토큰 파일은 바로 앞에서 설명한 aws-iam-token 토큰입니다. 이 토큰의 issuer는 EKS의 IdP임을 잊지 말아야 합니다.

하지만 이렇게 환경 변수를 읽어들여 Assume Role을 수행하는 기능은 특정 버전 이상의 AWS SDK를 필요로 한다는 사실에 유의해야 합니다. 따라서 애플리케이션의 AWS SDK 버전이 낮을 경우 Pod Identity Webhook을 사용할 수 없을수도 있습니다. web identity token을 사용할 수 있는 AWS SDK 버전은 AWS 문서에서 확인할 수 있습니다.

EKS OpenID Connect Provider 등록 및 Trust Relationship 설정

다음 단계는 EKS IdP를 identity provider로서 등록하고, 포드가 Web Identity 토큰을 통해 IAM 역할을 Assume할 수 있도록 Trust Relationship을 추가하는 일만이 남았습니다. 앞서 설명했던 것처럼, 토큰을 전송한 클라이언트(AWS SDK)가 Assume Role을 할 수 있는지 판단하는 주체는 EKS IdP가 됩니다.

Pod Identity Webhook을 사용하기 위한 워크플로우
Pod Identity Webhook을 사용하기 위한 워크플로우

Pod Identity Webhook을 사용하기 위한 워크플로우

EKS IdP를 Identity Provider로 등록하는 작업은 AWS 웹 콘솔에서 진행할 수도 있지만 (IAM 페이지 → 왼쪽 메뉴 중 Identity Provider), 데브시스터즈의 모든 인프라는 테라폼을 통해 코드화되어 있으므로 이 또한 테라폼으로 구성했습니다. 단, thumbprint_list 항목은 예외적으로 직접 입력해줘야 하는 이슈가 있으므로 참고하기 바랍니다.

resource "aws_iam_openid_connect_provider" "my_eks_oidc" {
  # 데브시스터즈는 EKS 테라폼 모듈을 사용하고 있으며, output으로부터 issuer url을 가져올 수 있습니다.
  url = module.cluster.cluster_oidc_issuer_url

  client_id_list = [
    "sts.amazonaws.com"
  ]

  thumbprint_list = [
    "9e99a48a9960b14926bb7f3b02e22da2b0ab7280"
  ]
}

마지막으로, AssumeRoleWithWebIdentity과 EKS IdP를 통해 IAM 역할을 Assume할 수 있도록 Trust Relationship을 추가합니다. Conditions에서는 어떠한 네임스페이스의 서비스 어카운트가 Assume Role을 할 수 있는지 별도로 제한할 수 있습니다. 아래의 예시에서는 StringEquals를 통해 default 네임스페이스의 debug-sa 서비스 어카운트에게만 권한을 Assume Role 권한을 부여했습니다.

data "aws_iam_policy_document" "assume_role_policy" {
  statement {
    actions = ["sts:AssumeRoleWithWebIdentity"]

    principals {
      type        = "federated"
      identifiers = [aws_iam_openid_connect_provider.my_eks_oidc.arn]
    }

    condition {
      test     = "StringEquals"
      variable = "${replace(aws_iam_openid_connect_provider.my_eks_oidc.url, "https://", "")}:sub"
      values = [
        "system:serviceaccount:default:debug-sa""
      ]
    }
  }
}

resource "aws_iam_role" "my-test-role" {
  name = "my-test-role"
  assume_role_policy = data.aws_iam_policy_document.assume_role_policy.json
}

필요하다면 condition에서 StringEqual 대신 StringLike를 사용하고, 권한을 부여할 서비스 어카운트를 system:serviceaccount:default:* 처럼 와일드카드로 명시할 수도 있습니다.

지금까지 Pod Identity Webhook을 사용하는 전체적인 워크플로우를 간단하게 설명했습니다. 위 내용을 문제없이 잘 따라했다면 포드는 특정 IAM 역할로 Assume되어 AWS API를 호출하게 됩니다. 비록 이러한 과정들이 귀찮게 느껴질지라도, 포드에는 가능한 한 최소한의 권한만을 부여하는 것이 바람직하다는 사실을 잊지 말아야 합니다.

데브시스터즈는 최고의 인재를 찾고 있습니다.

데브시스터즈에서는 능력있는 DevOps 엔지니어를 찾고 있습니다.
자세한 내용은 채용 사이트를 확인해주세요!
게임인프라KubernetesDevOps

© 2020 Devsisters Corp. All Rights Reserved.