IRSA deep dive

公式が懇切丁寧に解説してくれてる
こっちの記事も分かりやすい。補足的に読むと良い

IRSA 雑まとめ
事前設定から動作フローまで
- EKS クラスタごとの ID プロパイダーを AWS IAM に登録
- ID プロパイダーは EKS クラスタ作成時に自動で作られる
- IAM ロールと紐づけた k8s Service Account を作成
- Pod 作成時に Pod Identity Webhook が OIDC 準拠の JWT トークンを Service Account Volume としてマウント
- 環境変数も追加してる
- Pod が
sts:AssumeRoleWithWebIdentity
API を実行- OIDC 準拠の JWT トークンを STS で検証し、 AWS アクセス用のトークンと交換(例:AWS_SECRET_ACCESS_KEY)

ちょっと k8s service account 周りの知識をアップデート

Service Account ごとに Secret 作って Mount する方式は1.24以降は 以前までの話し
v1.24以前のバージョンでは、サービスアカウントごとに永続的なトークンが自動的に作成されていました

1.24以降は Service Account Token Volume Projection という機能を使い、TokenRequest API で発行した有効期間の短いトークンを Projected Volume に配置して Pod に Mount している。
Projected Volume を利用することで有効期間を設定したり、Pod のライフサイクルと合わせることができる。
ちなみに kube-apiserver の起動引数で --service-account-issuer
を渡せるので EKS ではこれを OIDC プロパイダーに設定してると思われる
--service-account-issuer
defines the Identifier of the service account token issuer.

kind で建てた Nginx Pod はこんな感じで token が差し込まれている
apiVersion: v1
kind: Pod
metadata:
name: nginx
namespace: default
spec:
containers:
- image: nginx:1.14.2
imagePullPolicy: IfNotPresent
name: nginx
volumeMounts:
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
name: kube-api-access-2gzms
readOnly: true
volumes:
- name: kube-api-access-2gzms
projected:
defaultMode: 420
sources:
- serviceAccountToken:
expirationSeconds: 3607
path: token
❯ k exec -it nginx -- cat /var/run/secrets/kubernetes.io/serviceaccount/token | jwt decode - --json
{
"header": {
"alg": "RS256",
"kid": "bvG7Yoiq6dptuH1d0QF7yvDZiAK5PX-Kv6iiXL-TVZc"
},
"payload": {
"aud": [
"https://kubernetes.default.svc.cluster.local"
],
"exp": 1772119066,
"iat": 1740583066,
"iss": "https://kubernetes.default.svc.cluster.local",
"jti": "5c1a68da-51fb-4df4-bb1b-ebc2e9287939",
"kubernetes.io": {
"namespace": "default",
"node": {
"name": "kind-worker",
"uid": "cd05f349-b2cf-499d-8fe5-9339030a945f"
},
"pod": {
"name": "nginx",
"uid": "ee5ad02e-f3a4-4b6c-9820-a228e782ff88"
},
"serviceaccount": {
"name": "default",
"uid": "461c19dd-c73f-41c4-977a-bcd6ca5ac3d7"
},
"warnafter": 1740586673
},
"nbf": 1740583066,
"sub": "system:serviceaccount:default:default"
}
}

kube-apiserver 起動時に --service-account-issuer
が渡されていること確認
クラスタ作成
eksctl create cluster --name $CLUSTER_NAME \
--region ap-northeast-1 \
--version 1.31 \
--vpc-private-subnets $SUBNET_IDS \
--without-nodegroup
eksctl utils update-cluster-logging \
--enable-types all \
--cluster $CLUSTER_NAME \
--approve
ログ確認
❯ aws logs get-log-events \
--log-group-name $LOG_GROUP_NAME \
--log-stream-name $LOG_STREAM_NAME \
| jq -r '.events[].message' | grep 'service-account-issuer'
I0215 06:22:43.106402 10 flags.go:64] FLAG: --service-account-issuer="[https://oidc.eks.ap-northeast-1.amazonaws.com/id/CAB144FE897D2D29F1A4C5DFCA6A00D5]"
I0215 06:22:46.618908 10 storage_rbac.go:226] created clusterrole.rbac.authorization.k8s.io/system:service-account-issuer-discovery
I0215 06:22:47.310575 10 storage_rbac.go:256] created clusterrolebinding.rbac.authorization.k8s.io/system:service-account-issuer-discovery

IRSA で使うトークンの中身
$ IAM_TOKEN=$(kubectl exec -it eks-iam-test3 -- cat /var/run/secrets/eks.amazonaws.com/serviceaccount/token)
$ jwt decode $IAM_TOKEN --json --iso8601
{
"header": {
"alg": "RS256",
"kid": "689de1734321bcfdfbef825503a5ead235981e7a"
},
"payload": {
"aud": [
"sts.amazonaws.com"
],
"exp": "2022-02-19T16:43:55+00:00",
"iat": "2022-02-18T16:43:55+00:00",
"iss": "https://oidc.eks.us-east-2.amazonaws.com/id/xxxx",
"kubernetes.io": {
"namespace": "default",
"pod": {
"name": "eks-iam-test3",
"uid": "6fd2f65f-4554-4317-9343-c8e5d28029c3"
},
"serviceaccount": {
"name": "my-sa",
"uid": "2c935d89-3ff0-425d-85c2-8236a6d626aa"
}
},
"nbf": "2022-02-18T16:43:55+00:00",
"sub": "system:serviceaccount:default:my-sa"
}
}

Pod が作成されるとまず eks-pod-identity-webhook(Mutating Webhook)が以下を podSpec に差し込む
サンプル(環境変数)
$ kubectl get pod eks-iam-test3 -o json | jq -r '.spec.containers | .[].env'
[
{
"name": "AWS_DEFAULT_REGION",
"value": "us-east-2"
},
{
"name": "AWS_REGION",
"value": "us-east-2"
},
{
"name": "AWS_ROLE_ARN",
"value": "arn:aws:iam::111122223333:role/eksctl-eks-oidc-demo-addon-iamserviceaccount-Role1-1SJZ3F7H39X72"
},
{
"name": "AWS_WEB_IDENTITY_TOKEN_FILE",
"value": "/var/run/secrets/eks.amazonaws.com/serviceaccount/token"
}
]
サンプル(volumeMounts)
$ kubectl get pod eks-iam-test3 -o json | jq -r '.spec.containers | .[].volumeMounts'
[
{
"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
"name": "kube-api-access-p2dlk",
"readOnly": true
},
{
"mountPath": "/var/run/secrets/eks.amazonaws.com/serviceaccount",
"name": "aws-iam-token",
"readOnly": true
}
]
サンプル(volumes)
$ kubectl get pod eks-iam-test3 -o json | jq -r '.spec.volumes[] | select(.name=="aws-iam-token")'
{
"name": "aws-iam-token",
"projected": {
"defaultMode": 420,
"sources": [
{
"serviceAccountToken": {
"audience": "sts.amazonaws.com",
"expirationSeconds": 86400,
"path": "token"
}
}
]
}
}

MutatingWebhookConfiguration はこれで /mutate
がエンドポイント

annotations[].eks.amazonaws.com/role-arn
が設定されている場合にのみ mutate される実装になっている
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-serviceaccount
namespace: my-namespace
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/MyIamRole

eks-pod-identity-webhook によって書き換えられた PodSpec は 一旦 etcd に保存される。その後 kube-scheduler によってノードに Pod がスケジュールされる。
ノード上の kubelet が PodSpec に設定された volume を検出し、TokenRequest API を発行する。この時、projected volume に設定された serviceAccountToken から audience を抜いてリクエストに含める
TokenRequest API は kube-apiserver によって処理される。トークンを発行する際にリクエストに audience が含まれている場合は JWT クレームの aud として設定する

OIDC トークンが Pod にマウントされるまでのフロー
- Deployment 作成を開始
- kube-apiserver は受け取ったリクエストを mutating webhook にフォワード
- mutating webhook は環境変数、volumeMounts、volumes を pod spec に追加
- kube-apiserver は書き換えられた pod spec を含む Deployment を etcd に登録
- Pod がノードにスケジュールされた後、kubelet が TokenRequest API を発行
- kube-apiserver は aud: sts.amazon.com を含んだトークン(OIDC トークン)を作成して返却
- kubelet が受け取ったトークンを Pod にマウント

OIDC トークンと AWS の一時的な認証情報を交換するフロー
- Pod から AssumeRoleWithWebIdentity API がコールされる(厳密にはアプリ内の aws-sdk から)
- IAM は OIDC プロパイダーからトークンの署名検証用の公開鍵(JWKS)を取得する
- 公開鍵を使って Pod から送られてきた OIDC トークンの署名検証
- AWS API を実行できる一時的な認証情報を Pod に返却