使用 Bootstrap Token 完成 TLS Bootstrapping

Posted by 漠然 on August 28, 2018

最近在测试 Kubernetes 1.11.2 新版本的相关东西,发现新版本的 Bootstrap Token 功能已经进入 Beta 阶段,索性便尝试了一下;虽说目前是为 kubeadm 设计的,不过手动挡用起来也不错,这里记录一下使用方式

一、环境准备

首先需要有一个运行状态正常的 Master 节点,目前我测试的是版本是 1.11.2,低版本我没测试;其次本文默认 Node 节点 Docker、kubelet 二进制文件、systemd service 配置等都已经处理好,更具体的环境如下:

Master 节点 IP 为 192.168.1.61,Node 节点 IP 为 192.168.1.64

docker1.node ➜  ~ kubectl version
Client Version: version.Info{Major:"1", Minor:"11", GitVersion:"v1.11.2", GitCommit:"bb9ffb1654d4a729bb4cec18ff088eacc153c239", GitTreeState:"clean", BuildDate:"2018-08-07T23:08:19Z", GoVersion:"go1.10.3", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"11", GitVersion:"v1.11.2", GitCommit:"bb9ffb1654d4a729bb4cec18ff088eacc153c239", GitTreeState:"clean", BuildDate:"2018-08-07T23:08:19Z", GoVersion:"go1.10.3", Compiler:"gc", Platform:"linux/amd64"}

docker1.node ➜  ~ docker info
Containers: 0
 Running: 0
 Paused: 0
 Stopped: 0
Images: 0
Server Version: 18.06.1-ce
Storage Driver: overlay2
 Backing Filesystem: xfs
 Supports d_type: true
 Native Overlay Diff: true
Logging Driver: json-file
Cgroup Driver: cgroupfs
Plugins:
 Volume: local
 Network: bridge host macvlan null overlay
 Log: awslogs fluentd gcplogs gelf journald json-file logentries splunk syslog
Swarm: inactive
Runtimes: runc
Default Runtime: runc
Init Binary: docker-init
containerd version: 468a545b9edcd5932818eb9de8e72413e616e86e
runc version: 69663f0bd4b60df09991c08812a60108003fa340
init version: fec3683
Security Options:
 apparmor
 seccomp
  Profile: default
Kernel Version: 4.15.0-33-generic
Operating System: Ubuntu 18.04.1 LTS
OSType: linux
Architecture: x86_64
CPUs: 2
Total Memory: 3.847GiB
Name: docker1.node
ID: AJOD:RBJZ:YP3G:HCGV:KT4R:D4AF:SBDN:5B76:JM4M:OCJA:YJMJ:OCYQ
Docker Root Dir: /data/docker
Debug Mode (client): false
Debug Mode (server): false
Registry: https://index.docker.io/v1/
Labels:
Experimental: false
Insecure Registries:
 127.0.0.0/8
Live Restore Enabled: false

二、TLS Bootstrapping 回顾

在正式进行 TLS Bootstrapping 操作之前,如果对 TLS Bootstrapping 完全没接触过的请先阅读 Kubernetes TLS bootstrapping 那点事;我想这里有必要简单说明下使用 Token 时整个启动引导过程:

  • 在集群内创建特定的 Bootstrap Token Secret,该 Secret 将替代以前的 token.csv 内置用户声明文件
  • 在集群内创建首次 TLS Bootstrap 申请证书的 ClusterRole、后续 renew Kubelet client/server 的 ClusterRole,以及其相关对应的 ClusterRoleBinding;并绑定到对应的组或用户
  • 调整 Controller Manager 配置,以使其能自动签署相关证书和自动清理过期的 TLS Bootstrapping Token
  • 生成特定的包含 TLS Bootstrapping Token 的 bootstrap.kubeconfig 以供 kubelet 启动时使用
  • 调整 Kubelet 配置,使其首次启动加载 bootstrap.kubeconfig 并使用其中的 TLS Bootstrapping Token 完成首次证书申请
  • 证书被 Controller Manager 签署,成功下发,Kubelet 自动重载完成引导流程
  • 后续 Kubelet 自动 renew 相关证书
  • 可选的: 集群搭建成功后立即清除 Bootstrap Token Secret,或等待 Controller Manager 待其过期后删除,以防止被恶意利用

三、使用 Bootstrap Token

第二部分算作大纲了,这部分将会按照第二部分的总体流程来走,同时会对一些细节进行详细说明

3.1、创建 Bootstrap Token

既然整个功能都时刻强调这个 Token,那么第一步肯定是生成一个 token,生成方式如下:

➜  ~ echo "$(head -c 6 /dev/urandom | md5sum | head -c 6)"."$(head -c 16 /dev/urandom | md5sum | head -c 16)"
47f392.d22d04e89a65eb22

这个 47f392.d22d04e89a65eb22 就是生成的 Bootstrap Token,保存好 token,因为后续要用;关于这个 token 解释如下:

Token 必须满足 [a-z0-9]{6}\.[a-z0-9]{16} 格式;以 . 分割,前面的部分被称作 Token IDToken ID 并不是 “机密信息”,它可以暴露出去;相对的后面的部分称为 Token Secret,它应该是保密的

本部分官方文档地址 Token Format

3.2、创建 Bootstrap Token Secret

对于 Kubernetes 来说 Bootstrap Token Secret 也仅仅是一个特殊的 Secret 而已;对于这个特殊的 Secret 样例 yaml 配置如下:

apiVersion: v1
kind: Secret
metadata:
  # Name MUST be of form "bootstrap-token-<token id>"
  name: bootstrap-token-07401b
  namespace: kube-system

# Type MUST be 'bootstrap.kubernetes.io/token'
type: bootstrap.kubernetes.io/token
stringData:
  # Human readable description. Optional.
  description: "The default bootstrap token generated by 'kubeadm init'."

  # Token ID and secret. Required.
  token-id: 47f392
  token-secret: d22d04e89a65eb22

  # Expiration. Optional.
  expiration: 2018-09-10T00:00:11Z

  # Allowed usages.
  usage-bootstrap-authentication: "true"
  usage-bootstrap-signing: "true"

  # Extra groups to authenticate the token as. Must start with "system:bootstrappers:"
  auth-extra-groups: system:bootstrappers:worker,system:bootstrappers:ingress

需要注意几点:

  • 作为 Bootstrap Token Secret 的 type 必须为 bootstrap.kubernetes.io/token,name 必须为 bootstrap-token-<token id> (Token ID 就是上一步创建的 Token 前一部分)
  • usage-bootstrap-authenticationusage-bootstrap-signing 必须存才且设置为 true (我个人感觉 usage-bootstrap-signing 可以没有,具体见文章最后部分)
  • expiration 字段是可选的,如果设置则 Secret 到期后将由 Controller Manager 中的 tokencleaner 自动清理
  • auth-extra-groups 也是可选的,令牌的扩展认证组,组必须以 system:bootstrappers: 开头

最后使用 kubectl create -f bootstrap.secret.yaml 创建即可

本部分官方文档地址 Bootstrap Token Secret Format

3.3、创建 ClusterRole 和 ClusterRoleBinding

具体都有哪些 ClusterRoleClusterRoleBinding,以及其作用请参考上一篇的 Kubernetes TLS bootstrapping 那点事,不想在这里重复了

在 1.8 以后三个 ClusterRole 中有两个已经有了,我们只需要创建剩下的一个即可:

# A ClusterRole which instructs the CSR approver to approve a node requesting a
# serving cert matching its client cert.
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: system:certificates.k8s.io:certificatesigningrequests:selfnodeserver
rules:
- apiGroups: ["certificates.k8s.io"]
  resources: ["certificatesigningrequests/selfnodeserver"]
  verbs: ["create"]

然后是三个 ClusterRole 对应的 ClusterRoleBinding;需要注意的是 在使用 Bootstrap Token 进行引导时,Kubelet 组件使用 Token 发起的请求其用户名为 system:bootstrap:<token id>,用户组为 system:bootstrappers;so 我们在创建 ClusterRoleBinding 时要绑定到这个用户或者组上;当然我选择懒一点,全部绑定到组上

# 允许 system:bootstrappers 组用户创建 CSR 请求
kubectl create clusterrolebinding kubelet-bootstrap --clusterrole=system:node-bootstrapper --group=system:bootstrappers

# 自动批准 system:bootstrappers 组用户 TLS bootstrapping 首次申请证书的 CSR 请求
kubectl create clusterrolebinding node-client-auto-approve-csr --clusterrole=system:certificates.k8s.io:certificatesigningrequests:nodeclient --group=system:bootstrappers

# 自动批准 system:nodes 组用户更新 kubelet 自身与 apiserver 通讯证书的 CSR 请求
kubectl create clusterrolebinding node-client-auto-renew-crt --clusterrole=system:certificates.k8s.io:certificatesigningrequests:selfnodeclient --group=system:nodes

# 自动批准 system:nodes 组用户更新 kubelet 10250 api 端口证书的 CSR 请求
kubectl create clusterrolebinding node-server-auto-renew-crt --clusterrole=system:certificates.k8s.io:certificatesigningrequests:selfnodeserver --group=system:nodes

关于本部分首次请求用户名变为 system:bootstrap:<token id> 官方文档原文如下:

Tokens authenticate as the username system:bootstrap: and are members of the group system:bootstrappers. Additional groups may be specified in the token’s Secret.

3.4、调整 Controller Manager

根据官方文档描述,Controller Manager 需要启用 tokencleanerbootstrapsigner (目测这个 bootstrapsigner 实际上并不需要,顺便加着吧),完整配置如下(为什么贴完整配置? 文章凑数啊…):

KUBE_CONTROLLER_MANAGER_ARGS="  --address=127.0.0.1 \
                                --bind-address=192.168.1.61 \
                                --port=10252 \
                                --secure-port=10258 \
                                --cluster-name=kubernetes \
                                --cluster-signing-cert-file=/etc/kubernetes/ssl/k8s-root-ca.pem \
                                --cluster-signing-key-file=/etc/kubernetes/ssl/k8s-root-ca-key.pem \
                                --controllers=*,bootstrapsigner,tokencleaner \
                                --deployment-controller-sync-period=10s \
                                --experimental-cluster-signing-duration=86700h0m0s \
                                --enable-garbage-collector=true \
                                --leader-elect=true \
                                --master=http://127.0.0.1:8080 \
                                --node-monitor-grace-period=40s \
                                --node-monitor-period=5s \
                                --pod-eviction-timeout=5m0s \
                                --terminated-pod-gc-threshold=50 \
                                --root-ca-file=/etc/kubernetes/ssl/k8s-root-ca.pem \
                                --service-account-private-key-file=/etc/kubernetes/ssl/k8s-root-ca-key.pem \
                                --feature-gates=RotateKubeletServerCertificate=true"

3.5、生成 bootstrap.kubeconfig

前面所有步骤实际上都是在处理 Api Server、Controller Manager 这一块,为的就是 “老子启动后 TLS Bootstarpping 发证书申请你两个要立马允许,不能拒绝老子”;接下来就是比较重要的 bootstrap.kubeconfig 配置生成,这个 bootstrap.kubeconfig 是最终被 Kubelet 使用的,里面包含了相关的 Token,以帮助 Kubelet 在第一次通讯时能成功沟通 Api Server;生成方式如下:

# 设置集群参数
kubectl config set-cluster kubernetes \
  --certificate-authority=/etc/kubernetes/ssl/k8s-root-ca.pem \
  --embed-certs=true \
  --server=https://127.0.0.1:6443 \
  --kubeconfig=bootstrap.kubeconfig
# 设置客户端认证参数
kubectl config set-credentials system:bootstrap:47f392 \
  --token=47f392.d22d04e89a65eb22 \
  --kubeconfig=bootstrap.kubeconfig
# 设置上下文参数
kubectl config set-context default \
  --cluster=kubernetes \
  --user=system:bootstrap:47f392 \
  --kubeconfig=bootstrap.kubeconfig
# 设置默认上下文
kubectl config use-context default --kubeconfig=bootstrap.kubeconfig

3.6、调整 Kubelet

Kubelet 启动参数需要做一些相应调整,以使其能正确的使用 Bootstartp Token,完整配置如下(与使用 token.csv 配置没什么变化,因为主要变更在 bootstrap.kubeconfig 中):

KUBELET_ARGS="  --address=192.168.1.64 \
                --allow-privileged=true \
                --alsologtostderr \
                --anonymous-auth=true \
                --bootstrap-kubeconfig=/etc/kubernetes/bootstrap.kubeconfig \
                --cert-dir=/etc/kubernetes/ssl \
                --cgroup-driver=cgroupfs \
                --cluster-dns=10.254.0.2 \
                --cluster-domain=cluster.local. \
                --fail-swap-on=false \
                --healthz-port=10248 \
                --healthz-bind-address=192.168.1.64 \
                --feature-gates=RotateKubeletClientCertificate=true,RotateKubeletServerCertificate=true \
                --node-labels=node-role.kubernetes.io/k8s-master=true \
                --image-gc-high-threshold=70 \
                --image-gc-low-threshold=50 \
                --kube-reserved=cpu=500m,memory=512Mi,ephemeral-storage=1Gi \
                --kubeconfig=/etc/kubernetes/kubelet.kubeconfig \
                --system-reserved=cpu=1000m,memory=1024Mi,ephemeral-storage=1Gi \
                --serialize-image-pulls=false \
                --sync-frequency=30s \
                --pod-infra-container-image=k8s.gcr.io/pause:3.1 \
                --resolv-conf=/etc/resolv.conf \
                --rotate-certificates"

一切准备就绪后,执行 systemctl daemon-reload && systemctl start kubelet 启动即可

四、其他说明

可能有人已经注意到,在官方文档中最后部分有关于 ConfigMap Signing 的相关描述,同时要求了启用 bootstrapsigner 这个 controller,而且在上文创建 Bootstrap Token Secret 中我也说 usage-bootstrap-signing 这个可以不设置;其中官方文档上的描述我们能看到的大致只说了这么两段稍微有点用的话:

In addition to authentication, the tokens can be used to sign a ConfigMap. This is used early in a cluster bootstrap process before the client trusts the API server. The signed ConfigMap can be authenticated by the shared token.

The ConfigMap that is signed is cluster-info in the kube-public namespace. The typical flow is that a client reads this ConfigMap while unauthenticated and ignoring TLS errors. It then validates the payload of the ConfigMap by looking at a signature embedded in the ConfigMap.

从这两段话中我们只能得出两个结论:

  • Bootstrap Token 能对 ConfigMap 签名
  • 可以签名一个 kube-public NameSpace 下的名字叫 cluster-info 的 ConfigMap,并且这个 ConfigMap 可以在没进行引导之前强行读取

说实话这两段话搞得我百思不得骑姐其解,最终我在 kubeadm 的相关文档中找到了真正的说明及作用:

  • 在使用 kubeadm init 时创建 cluster-info 这个 ConfigMap,ConfigMap 中包含了集群基本信息
  • 在使用 kubeadm join 时目标节点强行读取 ConfigMap 以得知集群基本信息,然后进行 join

综上所述,我个人认为手动部署下,在仅仅使用 Bootstrap Token 进行 TLS Bootstrapping 时,bootstrapsigner 这个 controller 和 Bootstrap Token Secret 中的 usage-bootstrap-signing 选项是没有必要的,当然我还没测试(胡吹谁不会)…

最后附上 kubeadm 的文档说明: Create the public cluster-info ConfigMapDiscovery cluster-info

转载请注明出处,本文采用 CC4.0 协议授权