Amazon Web Services ブログ

Kubernetes サービスアカウントに対するきめ細やかな IAM ロール割り当ての紹介

本投稿は Micah Hausler と Michael Hausenblas による記事を翻訳したものです

AWS ではお客様のニーズに最優先にフォーカスしています。Amazon EKS におけるアクセス権制御に関して、みなさまは「パブリックコンテナロードマップ」の Issue #23 にて EKS でのきめ細かい IAM ロールの利用方法 を求められていました。このニーズに応えるため、コミュニティでは kube2iamkiamZalando’s IAM controller といったいくつかのオープンソースソリューションが登場しました。これらのソリューションは素晴らしいプロダクトであるだけでなく、それぞれのアプローチの要件及び制約は何なのかについて多くの方の理解を促すことを可能にしました。

そして今、柔軟かつ簡単に利用可能なソリューションがやってきました。私たちの重要なゴールとして、粒度の高いロールを Node レベルではなくPod レベルでの提供がありました。私たちが今回考え出したソリューションもオープンソースとして公開されているため、eksctl での Amazon EKS クラスター作成時にも利用できますし、DIY アプローチでの Kubernetes クラスターとしてポピュラーな kops によって作成されたようなクラスタにおいてもご利用いただくことが可能です。

アクセスコントロール: IAM と RBAC

Kubernetes on AWS では、補完しあう2つのアクセスコントロール手法が動作します。AWS Identity and Access Management (IAM) は AWS サービスへのアクセス許可、例えばあるアプリケーションが S3 にアクセス可能、といった設定を可能にします。Kubernetes の文脈では、Kubernetes リソースに対するアクセスパーミッションを定義するのは Kubernetes Role-based Access Control (RBAC) です。これらが連携動作する例は以下のようになります(以前 Fluent Bit のアウトプットプラグインを紹介した記事 “Fluent Bit による集中コンテナロギング” でもこのあたりをカバーしています)。

 

NOTE 知識のブラッシュアップには the IAM and RBAC terminology resource ページもおススメです。

pod-log-reader ロールの Kubernetes RBAC 設定を通して、Fluent Bit プラグインは NGINX pods のログを読む権限を付与されます。インラインポリシーがアタッチされた、AWS IAM ロール eksctl-fluent-bit-demo-nodegroup-ng-2fb6f1a-NodeInstanceRole-P6QXJ5EYS6 を持つ EC2 インスタンスで作動しているため、Kinesis Data Firehose のデリバリーストリームにログエントリを書き込むことができます。

上図から分かるように、次に出てくる問題はKubernetesノードがすべて同じ権限を持ってしまうことです。本設計は最小権限の原則に違反しており、攻撃者に必要以上に広範な攻撃対象領域を与えてしまいます。

より良い方法はないでしょうか?はい、あります。この問題に対処するためにコミュニティは kiamkube2iam といったツールを開発しましたが、私たちの「IAM Roles for Service Accounts (IRSA)」では異なるアプローチを取りました:PodIAM 上の一級市民にしたのです。これまで見られた、「EC2 メタデータ API へのリクエストをインターセプトし、STS API を呼び出して一時クレデンシャルを取得する」方法ではなく、AWS のアイデンティティ API 群が Kubernetes の Pod を認識できるように API を変更しました。OpenID Connect (OIDC) アイデンティティプロバイダーと Kubernetes サービスアカウントのアノテーションを組み合わせることによって、Pod レベルでの IAM ロールを使用できるようになったのです。

私たちのソリューションをさらに詳しく掘り下げていきましょう。OIDC フェデレーションアクセスを使用すると、OIDC プロバイダーを使った認証が有効化され、IAM ロールの引き受けを可能とする JSON Web Token (JWT) の取得ができるため、Secure Token Service (STS) を介して IAM ロールを引き受けることができます。一方、Kubernetesは ProjectedVolume を通したサービスアカウントトークン を発行でき、 これが Pod にとって有効な OIDC JWT という位置づけになります。私たちのセットアップでは、各 Pod に暗号化署名されたトークンを付与します。Pod のアイデンティティの確認は、任意の OIDC プロバイダーに対してこのトークンを STS を介して検証することができます。さらに、私たちは AWS SDK のクレデンシャルプロバイダーから sts:AssumeRoleWithWebIdentity API を呼び出せるように SDK を更新し、Kubernetes が発行した OIDC トークンを、AWS ロールのクレデンシャルに交換できるようにしました。

このソリューションは現在 EKS で利用可能です。EKS では、AWS がコントロールプレーンを管理し、必要とされる環境変数及び ProjectedVolume をインジェクトする webhook を実行します。本ソリューションは DIY な Kubernetes on EC2 構成でも利用できます。詳細については後述します。

新しい IRSA 機能のメリットを享受するために必要となる大まかな手順は、次の通りです:

  1. eksctl を利用し、OIDC プロバイダーセットアップが有効化されたクラスタを作成します。本機能は EKS クラスター 1.13 以上で利用可能です。
  2. S3 などの対象となる AWS サービスへのアクセスを定義する IAM ロールを作成し、その IAM ロールでサービスアカウントにアノテーションを付与します。
  3. 最後に、前の手順で作成したサービスアカウントを Pod に設定し、IAM ロールを引き受けられるようにします。

では、上記の手順が EKS 上どのように見えるか、詳しく見てみましょう。ここでは、IRSA の有効化や必要なトークンの Pod へのインジェクション等の処理は既に終わっている前提となります。

eksctl を利用した Amazon EKS の設定

EKS で IAM Roles for Service Accounts (IRSA) を使用するにはどうすれば良いのでしょうか?下記は、ウォークスルーに沿って進めることができるよう、できる限りシンプルにしてあります。さらに下に進むと、S3 に書き込みをするアプリを使用した具体的かつ完全な手順があります。

1. クラスターと OIDC ID プロバイダー作成

まず、下記のコマンドを使って新しい v1.13 の EKS クラスターを作成します:

Bash
$ eksctl create cluster irptest --approve
[]  using region us-west-2
...

次に OIDC ID プロバイダー (IdP) を AWS 上に設定します:

注: eksctl はバージョン 0.5.0 以降である必要が有ります

Bash
$ eksctl utils associate-iam-oidc-provider \
            --name irptest \
            --approve
[]  using region us-west-2
[]  will create IAM Open ID Connect provider for cluster "irptest" in "us-west-2"
[]  created IAM Open ID Connect provider for cluster "irptest" in "us-west-2"

eksctl を使用している場合、これ以上の手順は必要ないのでここで完了です。手順2にお進みください。

もし EKS クラスターの作成に CloudFormation 等を使用されている場合、ご自身で OIDC IdP を作成する必要があります。Terraform のような一部のツールは本トピックに関するファーストクラスのサポートを提供していますが、その他の場合は次のように IdP を手動で作成する必要があります:

最初に、下記を実行してクラスターのアイデンティティ発行者 URL を取得:

Bash
$ ISSUER_URL=$(aws eks describe-cluster \
                    --name irptest \
                    --query cluster.identity.oidc.issuer \
                    --output text)

上記の手順で取得した URL は次のステップで必要となるため ISSUER_URL 環境変数にセットしました。次に、OIDC プロバイダーを作成します。(後述するように独自のプロバイダーを使用することもできます):

Bash
$ aws iam create-open-id-connect-provider \
        --url $ISSUER_URL \
        --thumbprint-list $ROOT_CA_FINGERPRINT \
        --client-id-list sts.amazonaws.com

NOTE ROOT_CA_FINGERPRINT の取得方法は OIDC プロバイダー次第です。AWS における方法については OpenID Connect ID プロバイダーのルート CA サムプリントの取得 をご参照ください。

2. Kubernetes サービスアカウントと IAM ロールの設定

次に、Kubernetes サービスアカウントを作成し、S3 や DynamoDB などの対象となるサービスへのアクセスを定義する IAM ロールを設定します。このためには指定された Kubernetes サービスアカウントが IAM ロールを引き受けることができるように、IAM 信頼ポリシーも設定する必要があります。次のコマンドを利用するとこれらのステップをすべて一度に実行することができるため便利です:

Bash
$ eksctl create iamserviceaccount \
            --name my-serviceaccount \
            --namespace default \
            --cluster irptest \
            --attach-policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess \
            --approve
[]  1 task: { 2 sequential sub-tasks: { create addon stack "eksctl-irptest-addon-iamsa-default-my-serviceaccount", create ServiceAccount:default/my-serviceaccount } }
[]  deploying stack "eksctl-irptest-addon-iamsa-default-my-serviceaccount"
[]  create all roles and service account

内部的には、上記のコマンドは2つのことを実行しています:

  1. IAM ロールを作成します。この例では eksctl-irptest-addon-iamsa-default-my-serviceaccount-Role1-U1Y90I1RCZWB という形のロールを作成し、指定されたポリシー、本例では arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess をアタッチします。
  2. Kubernetes のサービスアカウント (本例では my-serviceaccount )を作成し、サービスアカウントに上記 IAM ロールのアノテーションを付与します。

次の CLI コマンドは eksctl create iamserviceaccount が内部的に処理するステップと同じものです:

Bash
# STEP 1: create IAM role and attach the target policy:
$ ISSUER_HOSTPATH=$(echo $ISSUER_URL | cut -f 3- -d'/')
$ ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
$ PROVIDER_ARN="arn:aws:iam::$ACCOUNT_ID:oidc-provider/$ISSUER_HOSTPATH"
$ cat > irp-trust-policy.json << EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "$PROVIDER_ARN"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "${ISSUER_HOSTPATH}:sub": "system:serviceaccount:default:my-serviceaccount"
        }
      }
    }
  ]
}
EOF
$ ROLE_NAME=s3-reader
$ aws iam create-role \
        --role-name $ROLE_NAME 
        --assume-role-policy-document file://irp-trust-policy.json
$ aws iam update-assume-role-policy \
        --role-name $ROLE_NAME \
        --policy-document file://irp-trust-policy.json
$ aws iam attach-role-policy \
        --role-name $ROLE_NAME \
        --policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
$ S3_ROLE_ARN=$(aws iam get-role \
                    --role-name $ROLE_NAME \
                    --query Role.Arn --output text)
# STEP 2: create Kubernetes service account and annotate it with the IAM role:
$ kubectl create sa my-serviceaccount
$ kubectl annotate sa my-serviceaccount eks.amazonaws.com/role-arn=$S3_ROLE_ARN

アイデンティティに関する説明、サービスアカウントと IAM ロールの設定が完了したので、Pod に関する設定に進みます。

3. Pod の設定

サービスアカウントは Kubernetes API サーバに対するアプリケーションの ID であり、アプリケーションをホストする Pod はこのサービスアカウントを使用することを覚えておきましょう。

前の手順で my-serviceaccount というサービスアカウントを作成したので、これを Pod spec でも使用しましょう。サービスアカウントは次のように見えるはずです(読みやすくするために編集しています):

Bash
$ kubectl get sa my-serviceaccount -o yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/eksctl-irptest-addon-iamsa-default-my-serviceaccount-Role1-UCGG6NDYZ3UE
  name: my-serviceaccount
  namespace: default
secrets:
  - name: my-serviceaccount-token-m5msn

Deployment のマニフェストで serviceAccountName: my-serviceaccount を使用しつつ、対象の Pod に上記で定義したサービスアカウントを使用させることができます。Deployment は次のようになるはずです(読みやすくするために編集しています):

YAML
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    run: myapp
  name: myapp
spec:
  ...
  template:
    ...
    spec:
      serviceAccountName: my-serviceaccount
      containers:
      - image: myapp:1.2
        name: myapp
        ...

いよいよ kubectl apply を使用して Deployment を作成することができます。結果、Pod は次のようになります(こちらも読みやすくするために編集しています):

YAML
apiVersion: apps/v1
kind: Pod
metadata:
  name: myapp
spec:
  serviceAccountName: my-serviceaccount
  containers:
  - name: myapp
    image: myapp:1.2
    env:
    - name: AWS_ROLE_ARN
      value: arn:aws:iam::123456789012:role/eksctl-irptest-addon-iamsa-default-my-serviceaccount-Role1-UCGG6NDYZ3UE
    - 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

上記では、EKSで(webhook を介して)実行する Mutating Admission Controller が、環境変数 AWS_IAM_ROLE_ARN および AWS_WEB_IDENTITY_TOKEN_FILE と、aws-iam-token ボリュームを自動的にインジェクトしたことがわかります。あなたが行う必要があったのはサービスアカウント my-serviceaccount をアノテーションとして付与するだけでした。さらに、STS からの一時的なクレデンシャルはデフォルトで86,400秒(24時間)有効であることがわかります。

Admission Controller に Pod を変更されたくない場合は、環境変数 AWS_WEB_IDENTITY_TOKEN_FILE および AWS_ROLE_ARN に、ProjectedVolume に射影されるサービスアカウントトークンのパスとロールを手動で指定することができます。Pod に対する volume および volumeMounts パラメーターの指定も必要です。詳細については Kubernetes ドキュメントも参照してください。

最後に必要な手順は、Pod がそのサービスアカウントを介して IAM ロールを引き受けることです。これは次のように動作します: OIDC フェデレーションにより、OAuth2 フローを通して OIDC プロバイダーから JSON Web Token (JWT) を取得し、この JWT を用いて Secure Token Service (STS) から IAM ロールを引き受けることができます。Kubernetes では、各 Pod に ProjectedVolume を通して与えられたサービスアカウントトークン(これは有効な OIDC JWT であり、STS によって検証可能)を用いて Pod のアイデンティティを確認します。AWS SDKは sts:AssumeRoleWithWebIdentity API を呼び出す新しいクレデンシャルプロバイダーが利用できるようにアップデートされており、Kubernetes が発行した OIDC トークンを AWS ロールクレデンシャルに交換します。よって本機能を正しく動作させるためには、下記のバージョン以降の SDK を使用する必要があります:

上記の SDK バージョンのいずれも(まだ)使用していない場合、または移行する立場に(まだ)ない場合は、次のレシピを使用して Pod 上のアプリケーションを IRSA-aware にすることができます。前提条件として、下記のように AWS CLI 及び jq がインストールされている必要があります。

Bash
$ JQ=/usr/bin/jq && curl https://stedolan.github.io/jq/download/linux64/jq > $JQ && chmod +x $JQ
$ curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && python get-pip.py
$ pip install awscli --upgrade

これで、sts:AssumeRoleWithWebIdentity API を手動で呼び出すことができます:

Bash
$ aws sts assume-role-with-web-identity \
--role-arn $AWS_ROLE_ARN \
--role-session-name mh9test \
--web-identity-token file://$AWS_WEB_IDENTITY_TOKEN_FILE \
--duration-seconds 1000 > /tmp/irp-cred.txt
$ export AWS_ACCESS_KEY_ID="$(cat /tmp/irp-cred.txt | jq -r ".Credentials.AccessKeyId")"
$ export AWS_SECRET_ACCESS_KEY="$(cat /tmp/irp-cred.txt | jq -r ".Credentials.SecretAccessKey")"
$ export AWS_SESSION_TOKEN="$(cat /tmp/irp-cred.txt | jq -r ".Credentials.SessionToken")"
$ rm /tmp/irp-cred.txt

NOTE 上記の場合、一時 STS クレデンシャルは、--duration-seconds で指定された1000秒間有効であり、その後ご自身で更新する必要があります。また、セッション名は任意であり、各セッションはステートレスで独立していることに注意してください。 つまり、トークンにはすべての関連データが含まれます。

一般的な設定が完了したところで、IRSA が動いている具体的な例を見てみましょう。

使用例のウォークスルー

このウォークスルーでは、stdin から入力を取得し、作成時間をキーとして S3 バケットにデータを書き込むアプリケーションで IRSA (IAM Roles for Service Accounts) を使用する方法をお見せします。

S3 Echoer デモアプリ を EKS で動かすためには、IRSA が有効化されたクラスターをセットアップし、S3 バケットを作成し、アプリケーションが実行される Pod の IRSA を有効にする必要があります。

デモアプリケーションのリポジトリをローカルディレクトリに git clone するところから始めましょう。

Bash
$ git clone https://github.com/mhausenblas/s3-echoer.git && cd s3-echoer

次に、EKS クラスターを作成し、IRSA を有効にします:

Bash
$ eksctl create cluster --approve

$ eksctl utils associate-iam-oidc-provider --name s3echotest --approve

次に、IAM ロールを作成し、Pod が使用するサービスアカウントにアノテーションを付与し、アプリケーションに必要なアクセス権限を付与します:

Bash
$ eksctl create iamserviceaccount \
            --name s3-echoer \
            --cluster s3echotest \
            --attach-policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess 
            --approve

この時点で、必要なパーツがすべて揃いました。次に、書き込み対象の S3 バケットを作成し、S3 Echoer アプリケーションを1回限りの Kubernetes Job として起動します:

Bash
$ TARGET_BUCKET=irp-test-2019

$ aws s3api create-bucket \
        --bucket $TARGET_BUCKET \
        --create-bucket-configuration LocationConstraint=$(aws configure get region) \
        --region $(aws configure get region)

$ sed -e "s/TARGET_BUCKET/${TARGET_BUCKET}/g" s3-echoer-job.yaml.template > s3-echoer-job.yaml

$ kubectl apply -f s3-echoer-job.yaml

NOTE S3 バケット名はグローバルに一意である必要があるため、$TARGET_BUCKET にはここに示されている値とは異なる値を使用してください。

最後に、S3 バケットへの書き込みが成功したかどうかを確認するには、次のコマンドを実行します:

Bash
$ aws s3api list-objects \
        --bucket $TARGET_BUCKET \
        --query 'Contents[].{Key: Key, Size: Size}'
[
    {
        "Key": "s3echoer-1565024447",
        "Size": 27
    }
]

以下に、いかにして AWS IAM と Kubernetes のようなさまざまな異なる要素が連携し、EKS で IRSA を実現しているかを示します(点線はアクション、実線はプロパティまたはリレーション):

上図をステップごとに見ていきましょう:

  1. kubectl apply -f s3-echoer-job.yaml コマンドの実行によって S3 Echoer アプリケーションを起動すると、YAML マニフェストが Amazon EKS Pod Identity webhook 設定がなされた API サーバーへ送信されます。これは、Mutating Admission ステップ内で呼び出されます。
  2. Kubernetes Job は、serviceAccountName で設定されたサービスアカウント s3-echoer を使用します。
  3. サービスアカウントには eks.amazonaws.com/role-arn アノテーションがあるため、webhook は必要な環境変数 (AWS_ROLE_ARNAWS_WEB_IDENTITY_TOKEN_FILE) をインジェクトし、Pod に ProjectedVolume aws-iam-token を設定します。
  4. Echoer アプリケーションが S3 を呼び出してバケットにデータを書き込もうとすると、ここで使用する IRSA 対応の Go SDK は sts:assume-role-with-web-identity API を呼び出し、arn:aws:iam::aws:policy/AmazonS3FullAccess マネージドポリシーが付与された IAM ロールを引き受け、S3 バケットに書き込み操作をするため一時的なクレデンシャルを取得します。

IAM ロールやサービスアカウントなどがどのように連携しているのかを学び、これらのアクセス制御についてを深く知りたい場合は、rbIAM を使用できます。rbIAM は、体系的に IAM / RBACの領域を学んでいただくことを目的に私たちが書いたツールです。たとえば、S3 Echoer デモの場合、rbIAM の動作の抜粋は次のようになります。

以上です!S3 Echoer アプリケーションを使用して、EKS で IRSA を使用する方法を示し、IAM と Kubernetes の異なるエンティティが IRSA を実現するためにどのように連携するかをお見せしました。kubectl delete job/s3-echoer を使用してクリーンアップすることを忘れないようにしましょう。

オープンソース: DIY Kubernetes on AWS での利用方法

EKS で IRSA を使用する方法がわかったところで、たとえば、kops で Kubernetes クラスターを管理している場合など、DIY Kubernetes on AWS に IRSA を使用できるかどうか知りたい方もいらっしゃると思います。私たちはこのソリューションをオープンソースとして公開しているため、EKS での利用に加えて、独自のセットアップでも利用可能です。Mutating Admission フェーズで API サーバから呼び出される Amazon EKS Pod Identity webhook (aws/amazon-eks-pod-identity-webhook) をご確認ください。

GitHub リポジトリにてお持ちの Kubernetes on AWS 環境でのセットアップおよび構成方法を学習できます:

Amazon EKS Pod Identity Webhook GitHub リポジトリの手順に従って webhook をセットアップすることでお持ちの Kubernetes 環境で IRSA の利用を始められます。GitHub issues を利用してぜひフィードバックをお寄せください。

次のステップ

本リリースへの期待、また、その実現に必要なコンポーネントをオープンソース化できたこともあり、これらを皆様と共有できることを大変嬉しく思います。ご自身のクラスターに是非ご活用ください。これからも IRSA の改善を続け、コミュニティから寄せられている(これだけには限りませんが)次のようなリクエストに応えていきます: クロスアカウントロールのサポート、複数プロファイルのサポート、トークンを使用した AWS サービスではないシステムとの通信 (例えば EKS 上で稼働している Jenkins や Vault へのアクセスなど)。

期待された動作ではない場合、機能しないような場合は是非お知らせください。また、GitHub 上の AWS Containers Roadmap プロジェクトにてフィードバックやコメントをお寄せいただければ幸いです。

翻訳はコンテナサービス事業開発マネージャーの勝間田が担当しました。原文はこちらです。