1クール続けるブログ

とりあえず1クール続けるソフトウェアエンジニアの備忘録

EKSにおけるPodレベルでのIAMロール割当

記事一覧はこちら

NodeレベルでのIAMロール割当しちゃってる…

という私用クラスタを使ってしまっている僕がPodレベルでの権限割当について学んだので、その備忘録です。ALB Controllerの利用をするときに、Pod単位での権限割当を行う導入フローになっているのを見て、そろそろちゃんと勉強せねばと思った次第です。
学ぶときに利用したリソースは下記です。

Kubernetesの中での権限制御

これはRole Based Access Control(RBAC)を利用することになると思います。
Podに割り当てるServiceAccount リソースと「どのリソースに対してどの操作を許すか」という宣言を行ったRoleもしくはClusterRoleを用意し、RoleBindingClusterRoleBindingで紐付けることで制御します。ArgoCDなどのCDツールでは様々なk8sリソースの参照/変更権限が必要になるため、デフォルトのServiceAccountでは権限が足らず、必要な権限を付与したServiceAccountを用意することになりました。

では、podがk8sリソースではなくAWSのリソースを操作するときには、どのような仕組みで制御されているか(例えばS3からデータを取得するなど)。

Kubernetesからawsリソースを操作する時の権限制御

ノード単位での権限制御

EKSローンチ当初はノード単位でのIAMロール割当しか出来ませんでした。
なので、least-privilegedを守るためには、権限の大きいノードグループを別で作成し、podのnodeSelectorでスケジューリングされるノードグループを別々にする必要がありました。
podはスケジューリングされているノードのインスタンスプロファイルを利用してAWSリソースにアクセスします。EC2メタデータサービスを利用して、アクセスキーとシークレットアクセスキーを取得できる例のやつです(instance-metadata-security-credentials)。

pod単位での権限制御

EKSのv1.13から利用できるようになった、podごとにIAMロールを割り当てる方法です。
といっても公式で出たのがそのタイミングというだけで、記事にもありますが、3rd Partyでは kube2iam などがEKSローンチ当初から利用されていたように感じます(kopsで利用されてて実績があったんですかね)。
それらの3rd Partyは「EC2 メタデータ API へのリクエストをインターセプトし、STS API を呼び出して一時クレデンシャルを取得する」という手法を取っていたようです。EC2メタデータのエンドポイントである 169.254.169.254 へのアクセスをノードのiptablesでポート8181で待ち構えているdaemonsetにルーティングさせて、podのiam.amazonaws.com/roleアノテーションを見に行って指定したRoleにAssumeRoleさせるようです。

AWS公式の方法は、IAM Roles for Service Accounts (IRSA) という方法で上記の方法とはアプローチが違うようです。
前述の方法だとiptableをいじることにコストとか、KubernetesのRBACがIAMロールと結びつかないことによって直感的でないことがもしかしていまいちだったのかもしれません(多分もっとちゃんとした理由がある)。

IAM Roles for Service Accounts

OIDC (OpenID Connect)

AWSがpodに権限を付与するために用いた手段では、IAM OIDCプロバイダを利用することが前提になっています。恥ずかしながら、認証周りに疎いのでOIDC知りませんでした。

OpenID Connect 1.0 is a simple identity layer on top of the OAuth 2.0 protocol. - OpenID Connect | OpenID

OAuth2.0 の上にあるシンプルなidentityレイヤなんですね(分からん)。identityというのはset of attributes related to an entity を指すそうで、ここでのentityは人や機械、サービスのことのようです。identityレイヤがもたらすのはIDトークンとユーザ情報のAPIの仕様ということなんだそうな(参考)。OAuth 2.0はUser IDの取得方法やProfile APIの仕様については定めてなかったのか!

そして、AWSではIAMでウェブIDフェデレーションをサポートしています。IDトークンを AWS アカウントのリソースを使用するためのアクセス許可を持つ IAM ロールにマッピングすることが可能です(参考)。APIでいうと、AssumeRoleWithWebIdentityが呼ばれるそうです。
OIDC互換のプロバイダといえば、FacebookGoogleなどがそれにあたりますが、そのプロバイダのIAMエンティティが「IAM OIDC identity provider」です。これにIdP とその設定について埋め込むことで、AWS アカウントと IdP の間の「信頼」が確立されます。

Amazon EKS now hosts a public OIDC discovery endpoint per cluster containing the signing keys for the ProjectedServiceAccountToken JSON web tokens so external systems, like IAM, can validate and accept the OIDC tokens issued by Kubernetes.
- Technical overview - Amazon EKS

このPodに権限を付与するという文脈で用いられるIdPは、EKSクラスタのことになります。
なので、EKSが公開するOIDCディスカバリエンドポイントを設定した「IAM OIDC identity provider」作成する作業がまず必要になります。そうすることで、OIDC プロバイダーを使った認証が有効化され、IAM ロールの引き受けを可能とする JSON Web Token (JWT) の取得ができるようになります。

f:id:jrywm121:20210129220241p:plain

AssumeRoleWebIdentityではJWTトークンを渡すことになります(参考)。IDトークンに含まれるフィールドissでプロバイダのエンドポイントが分かるので、それがIAM OIDCプロバイダに存在するのかを確認するはず。もし存在すれば、STSからEKSにTokenの検証リクエストを投げます。検証し問題なければ、AssumeRoleの際にリクエストしたRoleを被ることができるユーザかどうかの確認が走るはず(この辺はドキュメントベースでないのでかなり怪しい)。エンドユーザの識別子はIDトークンのsubフィールドに含まれるそうな。
AssumeRole先のIAMロールには下記のようなポリシーをアタッチすることでk8sのサービスアカウントがどのIAMロールをAssumeできるかを制限します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::<AWS_ACCOUNT_ID>:oidc-provider/<OIDC_PROVIDER>"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "<OIDC_PROVIDER>:sub": "system:serviceaccount:<SERVICE_ACCOUNT_NAMESPACE>:<SERVICE_ACCOUNT_NAME>"
        }
      }
    }
  ]
}

最終的には、STSからAssumeしたRoleのACCESS_KEYSECRET_ACCESS_KEYをpodに返却し、podはその認証情報を利用してAWSリソースの操作を行います。

Service Account Token Volume Projection

では、そもそもEKSからpodにOIDC互換のIDトークンをどう渡しているのでしょうか?
Kubernetes v1.12からサポートされたProjectedVolumeをつかって受け渡しているそうです。ServiceAccountのアノテーションに、eks.amazonaws.com/role-arnで引き受けるIAMロールの名称を宣言するわけですが、(おそらく)そのアノテーションが付与されていると、Mutating Admission Controllerが、環境変数projectedVolumepodSpecに付与してくれます。なので、運用者がk8s環境で行うことはサービスアカウントをアノテートするだけですね。

以下、参考ドキュメントから引用

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
secrets:
  - name: my-serviceaccount-token-m5msn
---
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

ミドルウェアもどんどんpod単位の権限制御に

自分は気づけてなかったのですが、external-dnsaws-load-balancer-controllerなどのリソースは既にpod単位の権限制御前提で導入手順とか記載されていますね。
セキュリティ的にも重要なのでしっかり理解しておきたいです。以上!