Closed14

IRSA deep dive

ishii1648ishii1648

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)
ishii1648ishii1648

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

ishii1648ishii1648

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.

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

ishii1648ishii1648

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"
  }
}
ishii1648ishii1648

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
ishii1648ishii1648

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"
  }
}

https://aws.amazon.com/jp/blogs/news/diving-into-iam-roles-for-service-accounts/

ishii1648ishii1648

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"
        }
      }
    ]
  }
}
ishii1648ishii1648

eks-pod-identity-webhook によって書き換えられた PodSpec は 一旦 etcd に保存される。その後 kube-scheduler によってノードに Pod がスケジュールされる。

ノード上の kubelet が PodSpec に設定された volume を検出し、TokenRequest API を発行する。この時、projected volume に設定された serviceAccountToken から audience を抜いてリクエストに含める
https://github.com/kubernetes/kubernetes/blob/6b3560758b37680cb713dfc71da03c04cadd657c/pkg/volume/projected/projected.go#L327-L342

TokenRequest API は kube-apiserver によって処理される。トークンを発行する際にリクエストに audience が含まれている場合は JWT クレームの aud として設定する
https://github.com/kubernetes/kubernetes/blob/6b3560758b37680cb713dfc71da03c04cadd657c/pkg/registry/core/serviceaccount/storage/token.go#L222-L226

ishii1648ishii1648

OIDC トークンが Pod にマウントされるまでのフロー

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

OIDC トークンと AWS の一時的な認証情報を交換するフロー

  1. Pod から AssumeRoleWithWebIdentity API がコールされる(厳密にはアプリ内の aws-sdk から)
  2. IAM は OIDC プロパイダーからトークンの署名検証用の公開鍵(JWKS)を取得する
  3. 公開鍵を使って Pod から送られてきた OIDC トークンの署名検証
  4. AWS API を実行できる一時的な認証情報を Pod に返却
このスクラップは2025/03/15にクローズされました