もうずっといなかぐらし

かたいなかのブログ

EKSでの認証認可 〜aws-iam-authenticatorとIRSAのしくみ〜

こちらはAmazon EKS #1 Advent Calendar 2019 7日目の記事です。

EKSでIAM RoleをUserAccountに紐付けたり、ServiceAccountをIAM Roleに紐付けたりする際、AWSのドキュメントに従って設定してはいるものの、その設定によって実際にどんな処理が行われているかを具体的に知らない方も多いのではないでしょうか?(私も今回の記事のために調べるまではそうでした。)

そこで今回の記事では、Kubernetesの認証認可の仕組みを解説したあと、AWSのIAMの認証情報をKubernetes内のUserAccountに紐付けるaws-iam-authenticatorの動作の仕組みとKubernetesのService AccountにIAM Roleを紐づける仕組みについて設定方法のレベルから一段掘り下げて実際の動作に焦点を当てながら説明していきます。

目次

Kubernetesの認証認可・AdmissionControl

まずは事前知識として、Kubernetesの認証認可およびAdmission Controlの仕組みについておさらいしていきましょう。KubernetesでのAPIサーバへのリクエストは、実際にその内容に従って処理を行う前に以下の各stageで順番に処理されます。

  1. Authentication(認証)
  2. Authorization(認可)
  3. Admission Control (リクエストのバリデーション・変更等)

それぞれ順番に見ていきましょう。

Authentication(認証)

認証方法

KubernetesではBasic認証を使用した方法やクライアント証明書を使用する方法など、様々な認証方法が提供されています。

EKSでは主に以下の2つの認証方法が使用されています。

  • Service Account Tokens
    • ServiceAccountに紐付いたトークンをAPIサーバに送信し、APIサーバがそのトークンを検証します。
    • 後のServiceAccountの節で詳しく説明します。
  • Webhook Token Authentication
    • クライアントからAPIサーバに送られたトークンをさらに別のプロセスにWebhookで投げて検証を委譲します。
    • aws-iam-authenticatorによるIAMとUserAccountのリンクはこれにより実現されています。

認証主体

Kubernetesの認証主体は以下の2種類です。

  • ServiceAccount
  • UserAccount
ServiceAccount

ServiceAccountはKubernetesAPIによって管理される名前空間に紐付いたリソースです。

ServiceAccountごとに、Kubernetesクラスタが発行したトークンをクラスタ内のSecretとして保持しています。Podのコンテナにこのトークンを埋め込み、Pod内のプロセスにこのトークンを使用させることでkube-apiserver から認証されます。

ServiceAccountは以下のようなYAMLで定義します。

apiVersion: v1
kind: ServiceAccount
metadata:
  name: test
  namespace: default
secrets: # ServiceAccount作成後のSecret作成に伴って追加される
- name: test-token-mfb7n

ServiceAccountを作成すると、TokenControllerによってトークンを保持する以下のようなSecretが作成されます。

# 見やすいよう項目の順番を入れ替えた
apiVersion: v1
kind: Secret
type: kubernetes.io/service-account-token
metadata:
  # 省略
  name: test-token-xxxxx
  namespace: default
  # 省略
data:
  ca.crt: LS0...(省略)...= # APIサーバのCA証明書がBase64エンコードされたもの
  namespace: ZGVmYXVsdA== # 名前空間名がBase64エンコードされたもの
  token: ZXI...(省略)...= # JWT形式のトークンがBase64エンコードされたもの

このようなSecretはServiceAccountを指定してPodを作成すると、自動でマウントされます。具体的には、後述するAdmission Controllerの仕組みによってPodに以下のような設定が追加されます。

volumeMounts:
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
  name: test-token-xxxx
  readOnly: true
  volumes:
  - name: test-token-xxxx
    secret:
      defaultMode: 420
      secretName: test-token-xxxxx

これにより、コンテナ内の/var/run/secrets/kubernetes.io/に以下のような3つのファイルが作成されます。

なお、ServiceAccountを指定せずにPodを作成すると、Podと同じnamepaceのdefault という名前のServiceAccountの指定が追加され、defaultのServiceAccountに紐付いたトークンが埋め込まれます。これも後述するAdmission Controlによるものです。

KubernetesのGo ClientではPodにトークンが埋め込まれた状態で以下のような設定をすることで、APIサーバへのアクセス時にAuthorization: Bearerヘッダでトークンを送信するようになり、認証された状態で操作を行うことができるようになります。(エラーハンドリング処理は省略)

config, err := rest.InClusterConfig()
clientset, err := kubernetes.NewForConfig(config)
UserAccount

UserAccountはServiceAccountとは違い、Kubernetesの外部で管理されているユーザです(=kubectlでUserAccountを作成するようなことはしない)。外部の認証情報と連携して使用されることが意図されており、例えばGKEではGCPのアカウントとリンクしていたりします。

EKSの場合はaws-iam-authenticatorという仕組みを使用し、IAMの認証情報から得たトークンを使用しKubernetes内のユーザに紐付けることができます。こちらは後ほど詳しく解説します。

Authorization(認可)

Kubernetesでの認可はRBAC(Role Based Access Control)と呼ばれる仕組みです。RBACではRole or ClusterRoleで許可する操作を定義し、RoleBinding or ClusterRoleBindingでServiceAccount等に紐付けます。Role or ClusterRoleはAWS IAMロールと名前は似ていますが認証される主体ではなく、むしろ権限を定義するIAMポリシーに近いものです。

以下はClusterRoleとClusterRoleBindingでServiceAccountにPodに対する操作を許可する場合の例です。

まず、ClusterRoleで許可する操作を定義します。

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  namespace: default
  name: pod-reader
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "watch", "list"]

これをClusterRoleBindingでServiceAccountに紐付けます。

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: read-pods-test
subjects:
- kind: ServiceAccount
  name: test
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

これにより、ServiceAccountが紐付いたPod内からClusterRoleで許可された操作が行えるようになります。

Admission Control

Kubernetesでは、認証及び認可の処理が終わったリクエストを実行する前に、さらにAdmission Controlという仕組みで内容のバリデーションと修正を行います。

様々なAdmissionControllerが用意されており、基本的にはkube-apiserverと一緒にコンパイルされ同じバイナリにまとめられています。ただし、MutatingAdmissionWebhookValidatingAdmissionWebhook を使用することで Webhookで外部のプロセスにバリデーションおよび修正の処理を委譲できます。このようなWebhoookの設定はKubernetesのリソースとして管理されており、EKSではデフォルトで以下のような設定が生えています

$ kubectl get mutatingwebhookconfiguration
NAME                   CREATED AT
pod-identity-webhook   2019-11-22T12:59:57Z

先程のServiceAccountの解説の中でPod作成時に自動的にトークンがマウントされたり、ServiceAccountが指定されていないときにはdefaultのServiceAccountの指定が追加されていましたが、これはTokenControllerによりPodの作成のリクエストが修正されたためです。

また、後で説明するServiceAccountにIAMロールを紐づける仕組みの中でもトークンや環境変数の設定を MutatingAdmissionWebhook でPodに追加しています。

EKSのUserAccount(aws-iam-authenticator)

EKSではaws-iam-authenticatorによりIAMのエンティティとKubernetesのUserAccount/Groupを紐付けます。

ここでは、紐付けに必要な設定を見たあとで、実際にどのような処理が行われているかを掘り下げてみていきます。

設定内容

aws-iam-authenticatorでIAMのエンティティとKubernetesのUserAccount/Groupを紐づけを定義するのは以下のような aws-auth ConfigMapです。

apiVersion: v1
data:
  mapRoles: |
    - rolearn: arn:aws:iam::XXXXXXXXXXXX:role/<ワーカーノードのロール名>
      username: system:node:{{EC2PrivateDNSName}}
      groups:
        - system:bootstrappers
        - system:nodes
  mapUsers: |
    - userarn: arn:aws:iam::XXXXXXXXXXXX:user/admin
      username: admin
      groups:
        - system:masters
kind: ConfigMap
metadata:
  # 省略
  name: aws-auth
  namespace: kube-system
  # 省略

EKSのkube-apiserverにIAMの認証情報からトークンを作成してアクセスするにはaws eks update-kubeconfigコマンド等で以下のようなkubeconfigファイルを作成します。

# 省略
users:
- name: arn:aws:eks:ap-northeast-1:<アカウントID>:cluster/<クラスタ名>
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1alpha1
      command: aws
      args:
      - --region
      - ap-northeast-1
      - eks
      - get-token
      - --cluster-name
      - <クラスタ名>

これらの設定により、クライアント側のIAMエンティティとaws-auth ConfigMapで定義した紐付けに従ってKubernetesのUserAccount/Groupとして認証されます。

余談ですが、将来のaws-iam-authenticatorのバージョンでは、このようなCustomResourceにより管理するようになるようです。これにより、1つの大きなConfigMapを使ってすべての紐付けを管理せず、IAMエンティティごとに別々に紐付けを定義するようになるようです。(https://github.com/kubernetes-sigs/aws-iam-authenticator/pull/116)

実際の処理

では、実際には前節の設定でどんな処理が行われているのでしょうか?

aws-iam-authenticator による認証の流れは以下のようになります。

f:id:katainaka0503:20191126011641p:plain

まず、kubeconfigの設定により、kube-apiserverへのアクセス前に、トークンを取得するためaws eks get-tokenコマンドが実行されます。これにより、STSGetCallerIdentityの署名付きURLが発行され、これをもとにトークンが作成されます。

トークンの検証時にはこのトークンから抜き出した署名付きURLでGetCallerIdentityを実行します。GetCallerIdentity では署名付きURLを発行したIAMエンティティの情報が得られます。これにより、クライアントが特定のIAMのエンティティであることが確かめられるというわけです。

クライアントがAPIサーバにリクエストする際には、Authorization: Bearerヘッダでトークンをいっしょに送信します。

ここから先はEKSのコントロールプレーン内での処理です。

トークンを受け取ったkube-apiserverはWebhook Token Authenticationの仕組みでaws-iam-authenticatorサーバにトークンの検証を委譲します。

aws-iam-authenticatorサーバではトークンから抜き出した署名付きURLでGetCallerIdentityを実行します。結果として得られたIAMのエンティティの情報とaws-auth ConfigMapの情報を突き合わせてIAMのエンティティに紐づくUserAccount/Groupを取得します。そして、kube-apiserverにUserAccount/Groupの情報を返します。

このような流れにより、リクエストが特定のグループの特定のユーザからのものとして認証されます。

ServiceAccountのIAM ロール(IRSA)

先程のaws-iam-authenticatorの仕組みは、IAMのエンティティをKubernetesのユーザアカウントに紐付ける仕組みでした。

逆に、Kubernetes内部にいるPodのService Accountの認証情報を使用してIAM Roleを引き受けるためにはいくつかの方法があります。

  • Service Accountのトークンを使用してIAM Roleを引き受ける(IAM Role for Service Account 以下 IRSA)
  • ノードのiptablesをいじくって、メタデータエンドポイントへのアクセスを横取りしていい感じにAssumeRoleできるようにする(KIAM/kube2iam)
  • AWSの認証情報をSecretにして埋め込む

ここでは、IRSAによりService AccountにAWSのロールを紐付ける方法をみていきます。

実際にはeksctlの便利なコマンドが用意されているため、実際にどのような設定が行われるのか深く意識せずとも使えますが、ここでもあえて一段掘り下げて何をやっているのかを順番に見ていきます。

概要

実際の動作のフローとしては以下のようになります。

  1. EKSクラスタが発行したトークンを信頼させるため、IAMのOIDCプロバイダーというエンティティを作成

  2. IAMロールがアノテートされたServiceAccountを準備

  3. ServiceAccountのトークンでのAssumeRoleを許可したIAMロールを準備

  4. このServiceAccountを指定したPod作成時、AdmissionControllerによりSTS向けのServiceAccountのトークンの埋め込みと必要な環境変数の設定が行われる

  5. 以下に示すバージョンより新しいAWS SDK動作時にはコンテナに埋め込まれているトークンを元にAssumeRoleWithWebIdenityを実行し、IAM Roleを引き受ける

図にすると以下です(Amazon Web Services ブログの記事より転載させていただきました)

https://d2908q01vomqb2.cloudfront.net/ca3512f4dfa95a03169c5a670a4c91a19b3077b4/2019/08/12/irp-eks-setup-1024x1015.png

設定

では実際の設定を見ていきながらそれぞれ確認していきましょう。

AWSでOIDCプロバイダーを作成

まず、IAMのOIDC providerというエンティティを作成します。これを作成することで、EKSクラスタから発行されたトークンを信頼するようになり、AssumeRoleWithWebIdentityによるWebフェデレーションが行えるようになります。

eksctlでは以下のようなコマンドで実行しますが、

eksctl utils associate-iam-oidc-provider \
            --name floral-mongoose-1574427060 \
            --approve

これは以下のようなコマンドと同等です。

EKSクラスタから発行されるIDトークンのISSUERのURLを取得し、IAMにOIDCのプロバイダとして設定することで、EKSクラスタが発行したIDトークンを信頼させています。

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

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

ServiceAccountとIAMRoleの作成

次にServiceAccountおよびそこからAsssumeRoleできるIAM Roleを作成していきます。eksctlを使用すると以下のようなコマンドで実行できます。

eksctl create iamserviceaccount --cluster floral-mongoose-1574427060 --name serviceaccount-iamrole-test --attach-policy-arn arn:aws:iam::aws:policy/PowerUserAccess --approve

このコマンドは、実際には以下のような信頼ポリシーを持つIAMロールを作成し、

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "$PROVIDER_ARN"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "${ISSUER_HOSTPATH}:sub": "system:serviceaccount:default:<service account>"
        }
      }
    }
  ]
}

以下のようなアノテーションがついたServiceAccountを作成しています。

eks.amazonaws.com/role-arn=<IAMロールのArn>

信頼ポリシーを見ると、Service Accountから発行されたトークンを使用してAssumeRoleできるように設定されていることがわかります。

PodでServiceAccountを指定

最後にPodでこのServiceAccountを指定し、Pod内のプロセスがIAMロールを引き受けられるようにします。

AssumeRole先のIAM Roleに関するアノテーションがついたServiceAccountを指定してPodを作成すると、 MutatingAdmissionWebhookで処理が委譲されたamazon-eks-pod-identity-webhookにより、Podにいくつか設定が追加されます。

まず、KubernetesのService Account Token Volume projectionという仕組みでaudsts.amazonaws.comとなっているIDトークンが発行され、/var/run/secrets/eks.amazonaws.com/serviceaccount/tokenに埋め込むように設定が追加されます。

https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#service-account-token-volume-projection

    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

そして、トークンの場所及び引き受けるIAMロールを表す環境変数が追加されます。

  containers:
  - env:
    - name: AWS_ROLE_ARN
      value: arn:aws:iam::795113267886:role/eksctl-floral-mongoose-1574427060-addon-iams-Role1-1BHQ40Q5V2FWF
    - name: AWS_WEB_IDENTITY_TOKEN_FILE
      value: /var/run/secrets/eks.amazonaws.com/serviceaccount/token

このように環境変数が設定されトークンが埋め込まれている場合、ある一定のバージョン以上のAWS SDKであれば勝手にトークンを使用してAssumeRoleWithWebIdentityでIAMロールを引き受けてくれます。

このように、IRSAは、Kubernetesの外部audience向けにService AccountのIDトークンを発行する仕組み(Service Account Token Volume Projection)とAWSのウェブ ID フェデレーションの仕組みがうまく噛み合うことで実現されています。

まとめ

aws-iam-authenticatorおよびIRSAの仕組みを解説しました。

aws-iam-authenticatorは署名付きURLを用いてクライアントが実際にIAMで認証されていることを検証する巧妙な方法で実現されていました。またIRSAの仕組みはOIDCというオープンな仕様の上でKubernetesAWSの実装がうまく噛み合って実現されていました。

個人的に気になりつつも調べきれていなかったところではあったのですが、アドベントカレンダーという機会で半強制的に自分にプレッシャーを掛けて勉強できたので良かったなと感じています。

これからもEKSやっていくぞ!

参考資料

全体

aws-iam-authenticator

IRSA