日記マン

動画広告プロダクトしてます。Go, Kubernetesが好きです。

KubernetesのServiceAccount/RBACによる認証・認可

ユーザではなくプログラムからKubernetesのリソースを操作する際、認証はServiceAccount, 認可はRBACの方法が一般的になる。
ServiceAccountの挙動周りを知りたかったのでまとめ。

Kubernetesの認証・認可

Kubernetesのアカウントは人間によるユーザアカウントと、サービスアカウントの二種類がある。
ユーザアカウントは、Kubernetesの認証に利用されるもののKubernetesの管理下ではない。
従って kubectl などで作成・削除することはできない。
対しサービスアカウントは、Kubernetesによって作成され管理される、Kubernetes内のコンポーネントやPodなどで利用されるアカウント。

Kubernetesに対する全ての問い合わせは kube-apiserver へHTTPリクエストする必要がある。
KubernetesAPI Server kube-apiserver は、リクエストを受け取ると認めたアカウントかどうか認証(authentication)を行い、
次にそのリクエストの種類がそのアカウントに許可しているか認可(authorization)を行い、
最後に条件に満たさないリクエストのブロック(validating)や修正(mutating)を行う入力制御(admission control)を行う。
これらのフェーズを通ったリクエストのみ続行する。

 | client | --> | Authentication | --> | Authorization | --> | Admission Control | --> | Do request |

ServiceAccountによる認証

認証方式はモジュールという形で複数提供されている。
その中でServiceAccountによる認証がある。

https://kubernetes.io/docs/reference/access-authn-authz/authentication/

ServiceAccountによる認証は、人間のユーザーではない、PodやKubernetes Componentなども kube-apiserver と対話する形をとるため利用される。

ServiceAccountはシステム内では
system:serviceaccount:<namespace>:<ServiceAccount name>
というクラスタ内で一意に名前で認識される。
namespace作成時に必ずサービスアカウント名 default が作成される。
つまりクラスタ作成時に作られている default namespaceに、
system:serviceaccount:default:default というサービスアカウントが必ず存在することになる。
(GCPもネットワーキングで最初に自動生成されているエンティティに default と名前が付与されるし、Googleの中の人たちはそういう命名が好みみたい。)

$ kubectl get sa --all-namespaces=true # 全てのサービスアカウントを確認する
$ kubectl describe sa default # default:defaultの情報を確認する
Name:                default
Namespace:           default
Labels:              <none>
Annotations:         <none>
Image pull secrets:  <none>
Mountable secrets:   default-token-dn7qb
Tokens:              default-token-dn7qb
Events:              <none>

ServiceAccountが作成されるとSecretリソースに証明書とトークンが作成される。
そのTokenを Authorization: Bearer <トークンの文字列> というリクエストヘッダをつけてAPI Serverにリクエストを送ることで認証が可能になる。

$ kubectl get secret $(kubectl describe sa default | grep Tokens: | awk '{print $2}')
$ kubectl describe secret $(kubectl describe sa default | grep Tokens: | awk '{print $2}')
Name:         default-token-dn7qb
Namespace:    default
Labels:       <none>
Annotations:  kubernetes.io/service-account.name: default
              kubernetes.io/service-account.uid: b8e0678d-64c5-11e9-8617-42010a920131
 
Type:  kubernetes.io/service-account-token
 
Data
====
ca.crt:     1115 bytes
namespace:  7 bytes
token:      <トークンの文字列>

$ kubectl describe secret $(kubectl describe sa default | grep Tokens: | awk '{print $2}') | grep token: | awk '{print $2}' # トークン文字列のみ出力

grepawkコマンドをパイプしてトークン文字列を抜き出してみて、適当にリクエストしてみる。

$ kubectl cluster-info | grep master | awk '{print $6}' # API Serverのアドレスを確認する
$ DEFAULT_TOKEN=$(kubectl describe secret $(kubectl describe sa default | grep Tokens: | awk '{print $2}') | grep token: | awk '{print $2}')
$ curl -k https://<クラスタのIP:Port>/api/v1/namespaces/default/pods -H "Authorization: Bearer $DEFAULT_TOKEN"
{
  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {

  },
  "status": "Failure",
  "message": "pods is forbidden: User \"system:serviceaccount:default:default\" cannot list resource \"pods\" in API group \"\" in the namespace \"default\"",
  "reason": "Forbidden",
  "details": {
    "kind": "pods"
  },
  "code": 403

データの取得には失敗しているが、 User \"system:serviceaccount:default:default\" cannot list とレスポンス内容があるように、 default:defalut サービスアカウントだと認識しているのがわかる。

RBACによる認可

ではPodのListを取得可能であるサービスアカウントを作成してみる。
「PodのListを取得可能」という認可の1つを、リクエストするサービスアカウントが満たしている場合、PodのListを取得できる。

認可も認証と同様、複数の方式のモジュールが提供されている。
その中に、RBAC(役割ベースアクセス制御)がある。

https://kubernetes.io/docs/reference/access-authn-authz/authorization/#authorization-modules

任意のnamespaceに限定した制御の場合はRole, クラスタ全体に有効な制御の場合は ClusterRole というKubernetesリソースを利用する。

ClusterRoleリソースは .rules[].resources に対象リソースを記述し、 .rules[].verbs に許可する操作を記述する。
例えばPodリソースを取得できるClusterRoleは以下の記述になる。

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

$ kubectl apply -f clusterrole.yaml
clusterrole.rbac.authorization.k8s.io/pod-reader created
$ kubectl get clusterrole | grep pod-reader

Role系とServiceAccountを紐づけるRoleBinding系のリソースがある。
Roleの場合はRoleBinding, ClusterRoleの場合はClusterRoleBindingがある。

適当にサービスアカウントを作成し、さきほどのClusterRole pod-reader をバインドしてみる。

$ kubectl create ns sandbox # 事前に sandbox というnamespaceを作成する
namespace/sandbox created
$ cat serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: sa-pod-reader
  namespace: sandbox
$ kubectl apply -f serviceaccount.yaml
serviceaccount/sa-pod-reader created
$ cat clusterrolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: read-pods
subjects:
- kind: ServiceAccount
  name: sa-pod-reader
  namespace: sandbox
roleRef:
  kind: ClusterRole
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io
$ kubectl apply -f clusterrolebinding.yaml
clusterrolebinding.rbac.authorization.k8s.io/read-pods created

ServiceAccount sandbox:sa-pod-readerトークンを利用して、Kubernetes API ServerからPodの取得をしてみる。

$ kubectl run nginx --image=nginx -n sandbox # 検証用にサクッとnginxをデプロイする。
$ SANDBOX_TOKEN=$(kubectl describe secret $(kubectl describe sa sa-pod-reader -n sandbox | grep Tokens: | awk '{print $2}') -n sandbox | grep token: | awk '{print $2}')
$ curl -k https://<クラスタのIP:Port>/api/v1/namespaces/default/pods -H "Authorization: Bearer $SANDBOX_TOKEN"

Podの情報が取得できたのを確認できる。

PodはServiceAccountが紐づいてる

Podには必ずServiceAccountが紐づいている。

$ cat busybox-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: busybox
  namespace: sandbox
spec:
  containers:
  - image: busybox
    name: busybox
    command:
    - sleep
    - "3600"
    imagePullPolicy: IfNotPresent
  restartPolicy: Always

$ kubectl apply -f busybox-pod.yaml
pod/busybox created

立ち上げたPodの情報を一部抜粋すると、勝手にSecretが /var/run/secrets/kubernetes.io/serviceaccount にマウントされていることがわかる。
ServiceAccountを明示的に宣言しなかったPodリソースは、そのnamespaceのdefaultサービスアカウントが紐づけられる。

$ kubectl describe get po busybox -n sandbox
Containers:
  busybox:
    Container ID:  docker://c2a281d1ba122163346596d96bf7ed3d0dffcc86012fc516a3090362f45a8632
    Image:         busybox
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-8jn77 (ro)
Volumes:
  default-token-8jn77:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  default-token-8jn77
    Optional:    false

コンテナの中に入ってみてマウントされていることも確認できる。

$ kubectl exec -it busybox -n sandbox /bin/sh
# ls /var/run/secrets/kubernetes.io/serviceaccount/
ca.crt     namespace  token

コードリーディング

ServiceAccountもKubernetesにおけるリソースであり、つまりそれに反応し仕事をするコントローラが存在する。
ServiceAccountリソースが作成されると、加えて自動的に証明書・トークンをデータとするSecretリソースが作成し紐づけられた。
これはControllerのひとつ TokensController が処理を担っている。

https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/serviceaccount/tokens_controller.go

type TokensControllerOptions struct {
    // TokenGenerator is the generator to use to create new tokens
    TokenGenerator serviceaccount.TokenGenerator
  // ServiceAccountResync is the time.Duration at which to fully re-list service accounts.
  // If zero, re-list will be delayed as long as possible
  ServiceAccountResync time.Duration
  // SecretResync is the time.Duration at which to fully re-list secrets.
  // If zero, re-list will be delayed as long as possible
  SecretResync time.Duration
  // This CA will be added in the secrets of service accounts
  RootCA []byte
 
  // MaxRetries controls the maximum number of times a particular key is retried before giving up
  // If zero, a default max is used
  MaxRetries int
}
 
func (e *TokensController) ensureReferencedToken(serviceAccount *v1.ServiceAccount) ( /* retry */ bool, error) {  
  // Build the secret
  secret := &v1.Secret{
      ObjectMeta: metav1.ObjectMeta{
          Name:      secret.Strategy.GenerateName(fmt.Sprintf("%s-token-", serviceAccount.Name)),
          Namespace: serviceAccount.Namespace,
          Annotations: map[string]string{
              v1.ServiceAccountNameKey: serviceAccount.Name,
              v1.ServiceAccountUIDKey:  string(serviceAccount.UID),
          },
      },
      Type: v1.SecretTypeServiceAccountToken,
      Data: map[string][]byte{},
  }
     
      // Generate the token
      token, err := e.token.GenerateToken(serviceaccount.LegacyClaims(*serviceAccount, *secret))
     
      secret.Data[v1.ServiceAccountTokenKey] = []byte(token)
      secret.Data[v1.ServiceAccountNamespaceKey] = []byte(serviceAccount.Namespace)
      if e.rootCA != nil && len(e.rootCA) > 0 {
          secret.Data[v1.ServiceAccountRootCAKey] = e.rootCA
      }
  
      // Save the secret
      createdToken, err := e.client.CoreV1().Secrets(serviceAccount.Namespace).Create(secret)
     
      // Manually add the new token to the cache store.
      // This prevents the service account update (below) triggering another token creation, if the referenced token couldn't be found in the store
    e.updatedSecrets.Mutation(createdToken)

TokenGenerator はファクトリ呼び出し時に JWTTokenGenerator が注入されていることがわかる。

https://github.com/kubernetes/kubernetes/blob/master/cmd/kube-controller-manager/app/controllermanager.go

証明書をもとにJWTトークンを生成し、Secretに保存している一連の仕事が行われていた。

まとめ

  • ServiceAccount/RBACによる認証・認可について理解した
  • ServiceAccountに紐づくSecretがTokensControllerによって作成される
  • Tokenを Authorization: Bearer <Token> リクエストヘッダに乗せればAPIServerと認証できる

参考文献

knowledge.sakura.ad.jp