Amazon EKSの [system:masters] はどこから来たかWebhookトークン認証まで追ってみた

EKSを作ったユーザ、またはロールに紐付く system:masters というグループがどこから来ているのか、自分の目で確かめたくて掘り進めたので記録を残しておきます。

EKSのsystem:mastersって

以下にあるようにEKSを作ったユーザ、またはロールに紐付くグループです。

When you create an Amazon EKS cluster, the IAM entity user or role, such as a federated user that creates the cluster, is automatically granted system:masters permissions in the cluster's RBAC configuration.

https://docs.aws.amazon.com/eks/latest/userguide/add-user-role.html

この system:masters はcluster-adminに紐付けられていてスーパーユーザ的に機能します。

以下のように別のロールやユーザにマッピングもできます。(この場合は eks-admin というIAMロールに管理者権限である system:masters をマッピングしています)

kubectl apply -f  - <<EOF
apiVersion: v1
kind: ConfigMap
metadata:
  name: aws-auth
  namespace: kube-system
data:
  mapRoles: |
    - rolearn: arn:aws:iam::123456789012:role/eks-admin
      groups:
      - system:masters
EOF

system:masters グループ自体は cluster-admin に紐付いています。これで管理者権限があるのも納得です。

$ kubectl describe clusterrolebindings cluster-admin
Name:         cluster-admin
Labels:       kubernetes.io/bootstrapping=rbac-defaults
Annotations:  rbac.authorization.kubernetes.io/autoupdate: true
Role:
  Kind:  ClusterRole
  Name:  cluster-admin
Subjects:
  Kind   Name            Namespace
  ----   ----            ---------
  Group  system:masters  

ただ、少し調べるとわかるのですが、このGroupというものは、ここで言うClusterRoleのようにK8s上にリソースとして定義されたものではありません。

面白いのは上記の configmap/aws-authsystem:masters とクラスタ作成者を紐付ける情報が無いところです。実際にconfigmap/aws-authを削除してしまっても、クラスタを作成したユーザは変わらずに system:masters に紐付いている(つまりcluster-admin にも紐付いている)ことが確認できます。

$ kubectl delete -nkube-system configmap/aws-auth 
configmap "aws-auth" deleted

$ kubectl get clusterrole | head # <-変わらず操作ができる
NAME                                                                   AGE
admin                                                                  14d
aws-node                                                               14d
省略

EKSの認証の仕組み

では system:masters がどこから来たのかを、調べながら追ってみました。

まずEKSの認証の仕組みですが、IAMと深く関わっています。

ユーザ認証を絵の通りIAMに任せています。これはKubernetesが提供している認証(Authentication) の仕組みのうちWebhook Token Authenticationという方式です。並んでいる Static Token File などと違い動的に権限を設定できる方式となります。

f:id:yomon8:20201101170548p:plain

引用元 https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/managing-auth.html

ステップバイステップで追ってみる

このWebhookという方式をキーとして、kubectlからどうやって認証かかっているのか追ってみたいと思います。

kubectlの認証

AWSのドキュメントの手順に則れば、以下のでkubectlの設定ができるのはご存知だと思います。eksctlで作成した場合は自動で設定されてた気もします。

aws eks update-kubeconfig --name yomon8-cluster

ここで設定された内容を見ると以下の通りです。

exec 配下を見るとわかるのですが aws eks get-token コマンドが呼ばれています。

 cat ~/.kube/config
# 省略
users:
- name: arn:aws:eks:ap-northeast-1:123456789012:cluster/yomon8-cluster
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1alpha1
      args:
      - --region
      - ap-northeast-1
      - eks
      - get-token
      - --cluster-name
      - yomon8-cluster
      command: aws

実際に呼んでみます。これはAWSコマンド実行者に紐付くTOKENを取得しています。

aws --region ap-northeast-1 eks get-token --cluster-name yomon8-cluster
{
  "status": {
    "expirationTimestamp": "2020-11-01T11:20:01Z",
    "token": "k8s-aws-v1.TOKENTOKENTOKEN"
  },
  "kind": "ExecCredential",
  "spec": {},
  "apiVersion": "client.authentication.k8s.io/v1alpha1"
}

実際に、上記の TOKENTOKENTOKEN のところをBase64でデコードすると以下のような文字列になります。この記事ここまで読んでいる人なら見たことあるような内容になっていると思います。

ちなみに x-k8s-aws-id とあるとおり、EKS用のものなので、このままcurlに流しても認証できません。

https://sts.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=60&X-Amz-Credential=AKIA....%2Fus-east-1%2Fsts%2Faws4_request&X-Amz-SignedHeaders=host%3Bx-k8s-aws-id&X-Amz-Date=20201101T111553Z&X-Amz-Signature=92598a645f35afe4b70894aee0f487a57afe98449....

ちなみに、以下にあるPythonスクリプトでも同じモノが取得できます。boto3で書かれているので、慣れていれば読みやすいと思います。説明も記載されています。

https://github.com/kubernetes-sigs/aws-iam-authenticator/tree/v0.5.2#api-authorization-from-outside-a-cluster

このトークンをAWSのIDとしてEKSに送り、Webhook認証に流します。

f:id:yomon8:20201101200948p:plain

Webhookの認証

次はWebhook部分を見てみます。

f:id:yomon8:20201101203931p:plain

Webhookの仕様は以下にも記載があります通り、 TokenReview というオブジェクトを利用します。

https://v1-16.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.16/#tokenreview-v1-authentication-k8s-io

下記のリンクを引用します。

https://kubernetes.io/ja/docs/reference/access-authn-authz/authentication/#webhook-token-authentication

以下のようなリクエストを受けて、

{
  "apiVersion": "authentication.k8s.io/v1beta1",
  "kind": "TokenReview",
  "spec": {
    "token": "(Bearerトークン)"
  }
}

こんな返答が返るようです。 groups とありますが、ここに system:masters が入って欲しいです。

{
  "apiVersion": "authentication.k8s.io/v1beta1",
  "kind": "TokenReview",
  "status": {
    "authenticated": true,
    "user": {
      "username": "janedoe@example.com",
      "uid": "42",
      "groups": [
        "developers",
        "qa"
      ],
      "extra": {
        "extrafield1": [
          "extravalue1",
          "extravalue2"
        ]
      }
    }
  }
}

実行してみる

ここまでの情報を受けて、実際に流してみます。

TokenReview にリクエストをPOSTする必要があるのですが、その権限は system:auth-delegator というClusterRoleに付いています。

$ kubectl get clusterrole system:auth-delegator -oyaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  annotations:
    rbac.authorization.kubernetes.io/autoupdate: "true"
  creationTimestamp: "2020-10-18T01:14:30Z"
  labels:
    kubernetes.io/bootstrapping: rbac-defaults
  name: system:auth-delegator
  resourceVersion: "59"
  selfLink: /apis/rbac.authorization.k8s.io/v1/clusterroles/system%3Aauth-delegator
  uid: 514a8baf-ea9e-4745-8957-1258839198af
rules:
- apiGroups:
  - authentication.k8s.io
  resources:
  - tokenreviews
  verbs:
  - create
- apiGroups:
  - authorization.k8s.io
  resources:
  - subjectaccessreviews
  verbs:
  - create

webhook-test-ns という名前空間をテスト用に作成して、 system:auth-delegator を付与した webhook-test というServiceAccountを作成してみます。

kubectl apply -f - << EOF
---
apiVersion: v1
kind: Namespace
metadata:
  name: webhook-test-ns
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: webhook-test
  namespace: webhook-test-ns
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: role-tokenreview-binding
  namespace: webhook-test-ns
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: system:auth-delegator
subjects:
  - kind: ServiceAccount
    name: webhook-test
    namespace: webhook-test-ns
EOF

Webhook認証を実行するためTokenReview オブジェクトの権限のある作成したサービスアカウント webhook-test のトークンを取得します。

$ NS=webhook-test-ns #名前空間を変数に設定
$ kubectl get secrets -n ${NS}
NAME                       TYPE                                  DATA   AGE
default-token-gvhxs        kubernetes.io/service-account-token   3      3h30m
webhook-test-token-cm8nl   kubernetes.io/service-account-token   3      3h30m
$ SA_TOKEN=$(kubectl get secrets -n${NS} webhook-test-token-cm8nl -o jsonpath="{@['data.token']}"|base64 -d) && echo ${SA_TOKEN}
# -> ey....   (ServiceAccoutのトークンが出力される)

次にEKSのクラスタを作成したユーザのトークンを取得します。これをWebhook認証にかけます。

$ CLUSTER_NAME=yomon8-cluster
$ REGION=ap-northeast-1
$ USER_TOKEN=$(aws --region ${REGION} eks get-token --cluster-name ${CLUSTER_NAME}  | jq -r '.status.token') && echo ${USER_TOKEN}
# -> k8s-aws-v1.... (クラスタ作成IAMユーザのトークンが出力される。これがsystems:masterに紐付いている)

エンドポイントの情報を取得します。

$ END_POINT=$(aws eks describe-cluster --name ${CLUSTER_NAME} | jq -r '.cluster.endpoint') && echo ${END_POINT}
# -> https://xxxxx.ap-northeast-1.eks.amazonaws.com

今まで集めた情報を利用してWebhookにTokenReviewのPOSTを実行します。

$ curl -XPOST --insecure ${END_POINT}/apis/authentication.k8s.io/v1/tokenreviews \
-H "Authorization: Bearer ${USER_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"apiVersion\": \"authentication.k8s.io/v1\",\"kind\": \"TokenReview\",\"spec\": {\"token\": \"${USER_TOKEN}\"}}"

accessKeyIdにクラスタ作成時のユーザのアクセスキーが入っていました。 system:masters に紐付いていることが確認できます。

usernamekubernetes-admin と設定されてますね。

{
  "kind": "TokenReview",
  "apiVersion": "authentication.k8s.io/v1",
  "metadata": {
    "creationTimestamp": null
  },
  "spec": {
    "token": "k8s-aws-v1....."
  },
  "status": {
    "authenticated": true,
    "user": {
      "username": "kubernetes-admin",
      "uid": "heptio-authenticator-aws:123456789012:AIDxxxxx",
      "groups": [
        "system:masters",
        "system:authenticated"
      ],
      "extra": {
        "accessKeyId": [
  "AKIA......"
]
      }
    },
    "audiences": [
      "https://kubernetes.default.svc"
    ]
  }
}

ここまで来たら、最後は最初にも書いた以下の定義で system:masterscluster-admin に紐付いていきます。

f:id:yomon8:20201101215816p:plain

$ kubectl describe clusterrolebindings cluster-admin
Name:         cluster-admin
Labels:       kubernetes.io/bootstrapping=rbac-defaults
Annotations:  rbac.authorization.kubernetes.io/autoupdate: true
Role:
  Kind:  ClusterRole
  Name:  cluster-admin
Subjects:
  Kind   Name            Namespace
  ----   ----            ---------
  Group  system:masters  

リソースとして定義の無い system:masters が出てくる場所が見られたので少しスッキリです。

参考

KubernetesのToken Review API - Qiita

kubectlでやってることをcurlでやりたい - Qiita