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
ロールに結びつくのかと思いきや、違うっぽい。
- RoleRef:
- 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 ノードが繋がるための証明書を取得するだけでも大変なんだなあ。