: O. Yuanying

kubeadm join Deep Dive

KubeCon 参加中に Kubernetes Advent Calendarの7日目の記事投稿!

注: ほぼドキュメントベースで記事を書いているので、バージョン混在してるかも。

背景

もともと、OpenStack Magnum というプロジェクトで OpenStack 上に Kubernetes をデプロイするサービスの開発を行っていたこともあって、 Kubernetes のデプロイ周りは結構気になることが多い。

最近だと、 自宅クラスター用ベアメタル上に Kubernetes をデプロイする Remora というツールを細々と開発しているのだけれども、 worker ノードをクラスタに繋げる際に worker ノード用の TLS 証明書を個別に用意するのはめんどくさいなあと思い始めて来たこともあって、 kubeadm join が具体的にどんなことをしているのかが気になって来た。

で、調べて見たところ kubeadm join は kubelet TLS bootstrapping という仕組みを使っていることがわかった。

kubelet TLS bootstrapping

kubelet TLS bootstrapping とは、 kubeadm が新しい worker ノード (kubelet) を安全にクラスターにつなげる際に使っている仕組み。 以下の二つのパートに分けられる。

  • Discovery
    • Join するクラスターへの接続情報をどうやって Worker ノードに伝えるか?
  • Certificate
    • Worker ノードがクラスターへ接続する際の TLS 証明書発行の仕組み。

TLS クライアント認証に詳しい人には以下のように言うと簡単かもしれない。 Discovery はクライアント認証に利用する CA 証明書を取得するパート、 Certificate はクライアント証明書を取得するパート。

具体的には以下のフロー。

$ sudo kubeadm join --token 43a25d.420ff2e06336e4c1 172.31.7.230:6443 --skip-preflight-checks
[kubeadm] WARNING: kubeadm is in beta, please do not use it for production clusters.
[preflight] Skipping pre-flight checks
[discovery] Trying to connect to API Server "172.31.7.230:6443"
[discovery] Created cluster-info discovery client, requesting info from "https://172.31.7.230:6443"
[discovery] Cluster info signature and contents are valid, will use API Server "https://172.31.7.230:6443"
[discovery] Successfully established connection with API Server "172.31.7.230:6443"
[bootstrap] Detected server version: v1.7.6
[bootstrap] The server supports the Certificates API (certificates.k8s.io/v1beta1)
[csr] Created API client to obtain unique certificate for this node, generating keys and certificate signing request
[csr] Received signed certificate from the API server, generating KubeConfig...
[kubeconfig] Wrote KubeConfig file to disk: "/etc/kubernetes/kubelet.conf"

Node join complete:
* Certificate signing request sent to master and response
  received.
* Kubelet informed of new secure connection details.

Run 'kubectl get nodes' on the master to see this machine join.

この、二つのパートを裏側で支えているもう一つの重要なパーツが、Bootstrap Token である。 (以降、Token/Bootsrap Token)。

Bootstrap Token

Bootstrap Token は主に以下の二つの用途で利用される Token。 詳しい仕組みは公式ドキュメント に書かれており、以下の文章は公式文章を要約したものとなる。

  • Discovery で見つかった Kubernetes Cluster の検証。
    • 本当に Discovery で見つけたクラスターは、自分が繋がりたかったクラスターなのか?
  • Kubernetes の API に CSR を送るつける際の認証。

この二つの用途は本来は別の用途であるので、それぞれ別の Token を利用することも可能。

Token Format

Bootstrap Token は正規表現で [a-z0-9]{6}\.[a-z0-9]{16} で表すことができる。 例えば、abcdef.0123456789abcdef。 ID 部分と Secret 部分がピリオドで繋がっている構造となる。

Token の作成・登録

kubeadm を利用していると、 kubeadm join で利用する Token は kubeadm init を実行した際に表示されているものを使うことが多いと思われるが、 実際のところこの Token は上記のフォーマットに則っていればユーザが作ったものでも良い。

以下のフォーマットのマニフェストを作成し、Kubernetes に登録する。

apiVersion: v1
kind: Secret
metadata:
  name: bootstrap-token-07401b
  namespace: kube-system
type: bootstrap.kubernetes.io/token
data:
  description: base64(The default bootstrap token generated by 'kubeadm init'.)
  token-id: base64(07401b)
  token-secret: base64(f395accd246ae52d)
  expiration: base64(2017-03-10T03:22:11Z)
  usage-bootstrap-authentication: base64(true)
  usage-bootstrap-signing: base64(true)
  auth-extra-groups: base64(system:bootstrappers:group1,system:bootstrappers:group2)

usage-bootstrap-authentication と usage-bootstrap-signing は上記で述べた、「Kubernetes の API に CSR を送るつける際の認証」に利用するのか、 それとも「Discovery で見つかった Kubernetes Cluster の検証」に利用するのか、のフラグ。 普通はどっちも true に設定しておくものではなかろうか。

ちなみに、controller-manager に --controllers=*,tokencleaner と設定しておくと expiration を超えた Token を自動で削除してもらえる。

Token による認証

この Token は Kubernetes の API へのリクエスト時の認証に Bearler Token として利用することができる。 (現時点ではまだ alpha feature なので api-server 起動時に --experimental-bootstrap-token-auth フラグを立てる必要がある) この Token で api-server により、 system:bootstrappers グループに属している system:bootstrap:<Token ID> ユーザとして認証される。

Discovery

通常、kubeadm join する際にはクラスターの IP アドレスおよび Port、そして Token を指定することになるが、今回はその際の Discovery 手順について。Join するクラスターへの接続情報をどうやって Worker ノードに伝えるか?について述べる。

ClusterInfo

まず、kubeadm join する前に、管理者は cluster-info と言う名前の ConfigMap を kube-public 名前空間に登録しておく必要がある。 kubeadm を使った場合には kubeadm init した際に自動的に登録されている(と思う)。

apiVersion: v1
kind: ConfigMap
metadata:
  name: cluster-info
  namespace: kube-public
data:
  jws-kubeconfig-07401b: eyJhbGciOiJIUzI1NiIsImtpZCI6IjA3NDAxYiJ9..tYEfbo6zDNo40MQE07aZcQX2m3EB2rO3NuXtxVMYm9U
  kubeconfig: |
    apiVersion: v1
    clusters:
    - cluster:
        certificate-authority-data: 
        server: https://10.138.0.2:6443
      name: ""
    contexts: []
    current-context: ""
    kind: Config
    preferences: {}
    users: []

登録する ConfigMap はこんな感じ。重要な点は、

  • 通常、オレオレ CA に sign されているエンドポイントなため、insecure である。
  • kube-public に登録されているリソースであり、リソースの名前もわかっているので以下のコマンドで取得できる。デザインプロポーザルには認証なしで取得できるとある。
    • curl -k https://${IP_ADDRESS}:${PORT}/api/v1/namespaces/kube-public/configmaps/cluster-info
  • クラスターに API リクエストを送る際に必須な情報である、API エンドポイント、および CA 証明書の情報が含まれた kubeconfig を含んでいる。

これがセキュリティとか関係なく、kubernetes の API に繋がりたい、と言うだけだったら話はこれでおしまいなのだけれども、そもそもオレオレ証明書で署名された insecure な通信で得られた情報なので、この kubeconfig に含まれている情報が本物なのかどうかは定かではない。 そこで Discovery API のデザインプロポーザルには、 この kubeconfig をどう検証するのか?についての説明がある。

ClusterInfo の検証

kubeconfig は標準的な JWS で、"detached content" として署名されており、 ClusterInfo の中には bootstrap-token-${TOKEN_ID} と言う名前で Token ごとに利用する JWS が登録されている。(この bootstrap-token-${TOKEN_ID} は bootstrapsigner と言うコントローラが Bootstrap Token と ClusterInfo を常時監視して生成している。)

JWS のヘッダをデコードしてみると、使われている暗号化のアルゴリズムがわかる。

$ echo "eyJhbGciOiJIUzI1NiIsImtpZCI6IjA3NDAxYiJ9" | base64 -D
{"alg":"HS256","kid":"07401b"}%

デザインプロポーザルには、 JWS の仕様に則って kubeconfig を base64 でエンコードしたものを Bootstrap Token を共通鍵として HS256 scheme を利用して検証できる、とある。

コードにするとこんな感じ。

require 'openssl'

secret_key = 'Bootstrap Token'
message = 'Base64 encoded kubeconfig without trailing `=`'

puts OpenSSL::HMAC::hexdigest(
  OpenSSL::Digest::SHA256.new,
  secret_key,
  message
)
# => bcb5a198056e36cbb27c499528e0e1345ee25c7b5d7b67508c96122b1e38e904

ここで出力された結果が、JWS の後半部分と一緒ならば、検証成功。 ここで得られた kubeconfig が概ね正しいと言うことになる。

CA pinning

上記で 概ね正しい と述べたのは、 例えばネットワークや一部のノードのセキュリティが脆弱で、 Bootstrap Token が盗まれてしまった場合、 「中間者攻撃が可能になってしまう」と言う問題があるから。 (複数のノードで使う前提の秘密鍵が漏れてしまったら、攻撃者は偽のkubeconfig を署名できてしまうため。) それを回避するため、kubeadm の v1.8 からはデフォルトで、 --discovery-token-ca-cert-hash と言うオプションが使われるようになった。

この hash は以下のコマンドで求められる。

$ openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | \
  openssl rsa -pubin -outform der 2>/dev/null | \
  openssl dgst -sha256 -hex | \
  sed 's/^.* //'

この値を一緒に使うことで取得された CA 証明書が正しいと信頼することができる。

(微妙によくわかってないのが、kubeadm join 時の IP アドレスと Port から Kubernetes の API エンドポイントはわかるんだから、あとは CA 証明書を取得できればいいだけで、、kubeconfig 自体の検証は必要なのか?と言うこと。誰か知ってたら教えてください。)

Certificate

Worker ノードと Kubernetes は TLS クライアント認証を使って認証を行なっている。 この Certificate のパートでは Worker ノード用の TLS 証明書を、どう取得するのか? について述べる。

Certificate API

Kubernetes はクラスターレベルでの TLS 認証を行うための証明書を取得するために、 ACME に似た Certificate API を用意してる。 ユーザまたは Worker ノードは API に対して CSR リソースを作成してクライアント証明書を取得する。 コードを読んだところ最新の kubelet は bootstrap 時に

  • kubeconfig ファイルが作成されておらず、
  • bootstrap kubeconfg が設定されていた場合、

自分で CSR を作成して証明書を取得しに行くらしい。賢い。

CSR の種類

多分 csrapproving コントローラが、 API に登録された CSR を以下の三つのサブリソースに分類する。

  • nodeclient
    • 新しく O=system:nodes かつ CN=system:node:(node name) な証明書のための CSR。
  • selfnodeclient
    • クライアント証明書を更新するために、同じ O (グループ) かつ CN (ユーザ名) を持っているユーザが発行した CSR。
  • selfnodeserver
    • kubelet 自身のサーバ証明書を更新/登録する際の CSR。(alpha feature)

ちょっとここら辺からコードおよびドキュメントベースで調べていて間違いが含まれているかもしれないけど、 とりあえず書くだけ書いて後で確認後に修正する。

Certificate API 利用時の認証・認可

そもそも、bootstrap 時に Worker ノードが CSR リソースを作成できねばならないのだが、 どのような RBAC リソースによって制御されているかと言うと、

  • ClusterRole: system:node-bootstrapper
    • certificatesigningrequests の create/get/list/watch を許可。
    • Kubernetes がデフォルトで作成している。
  • ClusterRoleBinding: kubeadm:kubelet-bootstrap
    • RoleRef: system:node-bootstrapper
    • Subject Group: system:bootstrappers:kubeadm:default-node-token
    • kubeadm が作成している。
    • ドキュメントでは Bootstrap Token で認証されたユーザは自動的に system:boostrappers グループに属すことになると書いてあったのでそれが system:node-bootstrapper ロールに結びつくのかと思いきや、違うっぽい。
  • Group: system:bootstrappers:kubeadm:default-node-token
    • Bootstrap Token 自体は kubeadm が登録するのだが、Token登録時に extraGroups が設定でき、その際に Token で認証されたユーザはこのグループに所属されるように設定される。

以上、三つのリソースで認可制御されている。

CSR は登録されても自動で approve され証明書が発行されるわけではなく、 管理者が手動で、kubectl certificates approve してやる必要がある。

が、以下のサブリソースの作成権限を与えておくと自動で approve されるらしい。

  • certificatesigningrequests/nodeclient
  • certificatesigningrequests/selfnodeclient

kubeadm は上記のサブリソース作成権限を以下の ClusterRole および CluserRoleBinding を作成することで、 証明書の発行および更新を自動で行われるようにしている。

  • ClusterRole: system:certificates.k8s.io:certificatesigningrequests:nodeclient
    • v1.8 から Kubernetes 本体で自動で作成されるようになった。
    • certificatesigningrequests/nodeclient の作成権限を持つ Role
  • ClusterRole: system:certificates.k8s.io:certificatesigningrequests:selfnodeclient
    • v1.8 から Kubernetes 本体で自動で作成されるようになった。
    • certificatesigningrequests/selfnodeclient の作成権限を持つ Role
  • ClusterRoleBinding: kubeadm:node-autoapprove-bootstrap
    • system:bootstrappers:kubeadm:default-node-token グループ (Bootstrap Token で認証されると自動で入るグループ) に上記 system:certificates.k8s.io:certificatesigningrequests:nodeclient を付与する。
  • ClusterRoleBinding: kubeadm:node-autoapprove-certificate-rotation
    • system:nodes グループに自動で system:certificates.k8s.io:certificatesigningrequests:selfnodeclient を付与する。

なので、kubeadm でクラスター作成したけど、 Token 使って勝手にワーカーノードを追加されるのは困るよ!と言う人は kubeadm:node-autoapprove-bootstrap を 削除すると良い。

クライアント証明書の取得

一旦 approve された CSR は、そのリソースエンドポイントから証明書を取得することができる。

まとめ

と、言うことで以上で kubeadm を使って Kubelet がクラスターに繋がるためのエンドポイント情報と証明書をどうセットアップするのか、と言うことがわかった。

たかだか kubeadm join コマンドだろ、と思っていたが、 API のエンドポイントを発見して、 クラスターに Worker ノードが繋がるための証明書を取得するだけでも大変なんだなあ。