k8s与安全相关的内容相当多,各种类型的证书、jwt token,相关的概念还有非对称加密、数字签名、数字证书各种内容,本篇的目的就是将这些一并学习。
非对称加密的基本原理是使用一些加密算法生成一对公钥和密钥,使用公钥对数据加密只能用与之相对应的密钥解密,反之则亦然。ssh、https都是非对称加密的使用场景。在https通信开始时,用户的浏览器需要获取对方服务器的公钥,而公钥就是存在对方的https证书里面;在ssh的场景中,把公钥复制到服务器的.ssh/authorized_keys
就可以实现免密登录。如果没有手动复制公钥,如何交换公钥也是一个关键问题,这个问题由DH算法得到了解决。
下面使用openssl来演示下非对称加密过程:
shell# 生成一对公钥和密钥
openssl genpkey -algorithm RSA -outform PEM -out private_key.pem
openssl rsa -in private_key.pem -outform PEM -pubout -out public_key.pem
# 生成一个文件用于加密
echo "hello world" > hello.txt
# 用公钥加密
openssl rsautl -encrypt -inkey public_key.pem -pubin -in plain_text -out encrypted_text
# 查看加密内容,会发现是无法阅读的内容
cat encrypted_text
# 用密钥解密
openssl rsautl -decrypt -inkey private_key.pem -in encrypted_text -out decrypted_text
# 查看解密后的内容
cat decrypted_text
Hello world
假设,A与B之间需要通信,他们使用对方的各自公钥加密内容,接受适在使用自己的密钥解,但是因为非对称加密当中公钥都是公开的,所以此时中间C可以拦截B->A的消息(反之也行),用A的公钥重新加密一段内容发送给A,这样A就会误以为这是B发的,示意图如下。C将信息篡改,让A误以为B给他发送了一条内容为bad的信息。
所以这一环节的问题出现在,非对称加密只是中间人无法解密出消息的内容,但是不能保证中间人不会伪造数据,也就是说非对称加密无法解决消息的发送方是不是自己所期望的。类比到生活中,解决该问题的办法就是要求通信双方在它们的信件上都加上自己的签名来表明身份,并且该签名也只能保密只能让对方解密。
数字签名的基本原理类似,A->B发送信息,A用B的公钥加密一段内容,但是用A的私钥结合消息内容生成数字签名连同加密的内容发送给B。B接收到消息后,用B的秘钥解密出内容,再用A的公钥验证数字签名。具体的签名过程为:
要对比摘要是否相等,所以必须要保证加密双方所用的摘要生成算法是完全一致的,目前使用较多的就是md5和sha1。有了数字签名后,如果中间人C需要伪造消息发送给B,B就可以通过签名来验证数据是否来自A,示意图如下,来自wiki:
实际中的过程不是直接对消息的内容生成签名,而是先对消息内容进行Hash(也就是所谓摘要,digest)以后在生成签名。这里主要考虑是,摘要经过Hash后数据量相比消息内容已经短了很多,生成摘要更加高效。除此以外,Hash值还保证了数据是否被篡改,被篡改后的数据所生成的Hash肯定是不相同的。下面是一张相类似的图,但是内容相对详细一些,图来自。
前面对于数字签名的讨论的前提是通信的双方已经知道对方的公钥,那么在通信的时候如何去确定所使用的公钥一定是对方的而不是别人的伪造的呢?这就需要第三方机构,确保通信所拿到的公钥一定是真的,这个机构就是CA(certification authority)机构。当一个服务期望通过https为外界提供时,就需要找ca机构申请数字证书。目前大部分的数字证书都是由RFC 5280定义的x.509标准,里边就包含着公钥信息,示意图如下,图来自:
上图的Key指代的就是证书的公钥,在https场景下就是对方服务器的公钥,浏览器就可以用服务器提供的公钥对内容加密消息内容。当我们访问一个https网站的时候,浏览器首先会对服务器的证书进行验证。那么如何确保证书本身是否篡改呢?这还是利用到了数字签名的作用,即上图证书的signature字段。如同前面描述过的数字签名流程一样,浏览器通过签名即可校验证书的完整性,不过实际比这个过程要复杂,因为大部分的https网站使用的都是二级证书。当我们往一个https网站发起链接时,如果CA证书有问题,浏览器就会提示形如证书不安全或者是该网站的证书不是由可靠的机构颁发的证书的提示信息,至于HTTPS链接建立的过程网上相关文章很多不在过多赘述。
在chrome上任意点开一个https加密的网站,点击地址栏的锁团,可以看到它的ca证书相关信息,如下:
上面的证书表示根证书是来自Let's enctypt(ISRG Root X1),R3是一个中间证书(Intermediate Certificates),这就涉及到证书链的概念。新的问题是,如何验证二级证书本身是不是被篡改?还是数字签名的功劳,不过二级证书的签名是由根证书签署的,所以浏览器先需要从二级证书中反推找到根证书,用根证书(一般已经直接内嵌在浏览器或者操作系统)的签名来验证二级证书。在这个整个流程中可以发现根证书是十分重要的,一旦根证书坏了大家就不会认为它是不可靠的。这也是多级证书的原因,如果一个二级证书出现了问题,吊销该二级证书也不会影响到根证书,对于多级证书的内容可以参考这里和这里。
既然每一份证书都有来自CA机构的签名,那么根证书的签名从哪儿来呢?根证书的是自签证书,意思是它的签名是CA机构用自己的私钥生成的。使用OpenSSL来生成ca证书的样例可以参考:https://www.zhaohuabing.com/post/2020-03-19-pki/。
目前的大多数浏览器-服务器之间的交互都是单项认证的,也就是浏览器需要认证服务器的ca证书,但是服务器却不需要校验客户端。与之相对的就是双向认证客户端与服务端需要完成各自认证以后才可以通信,认证的流程同单向认证是一样的。双向认证的内容可以参考:The magic of TLS, X509 and mutual authentication explained,下图就来自这篇medium文章描述了双向认证的流程,虽然在内容上有些简化,不过总体意思是正确的。
kubernetes内各种组件都有各种证书,经过上面的基本知识的理解,也就理解了k8s组件各种证书的使用。先查看etcd所使用的证书:
shell$ kubectl get pods -n kube-system etcd-k8svm -o yaml | grep "cet\|key"
- --cert-file=/etc/kubernetes/pki/etcd/server.crt
- --key-file=/etc/kubernetes/pki/etcd/server.key
- --peer-cert-file=/etc/kubernetes/pki/etcd/peer.crt
- --peer-key-file=/etc/kubernetes/pki/etcd/peer.key
- --peer-trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt
- --trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt
首先需要明确的是k8s集群当中的关键组件都是需要双向认证的,在etcd节点中一个etcd服务即会做客户端也会作为server,有这样的概念以后各个证书的就很好解释了:
接下来看看apiserver的证书,apiserver是整个k8s集群当中唯一与etcd直接通信的组件,很明显的是apiserver至少具备了与用于etcd访问的相关证书,直接去/etc/kubernetes/manifest
查看apiserver的manifest,如下:
shell$ cat kube-apiserver.yaml | grep --color "crt\|pem\|key"
- --client-ca-file=/etc/kubernetes/pki/ca.crt # 用于验证访问kube-apiserver的client的根证书
- --etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt # 用于验证来自etcd的服务端证书的根证书
- --etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt # apiserver作为客户端访问etcd的证书
- --etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key # 访问etcd的客户端证书所对应的密钥
- --kubelet-client-certificate=/etc/kubernetes/pki/apiserver-kubelet-client.crt # apiserver作为客户端访问kubelet的证书
- --kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet-client.key # 访问kubelet的客户端证书的密钥
- --proxy-client-cert-file=/etc/kubernetes/pki/front-proxy-client.crt # 不是特别了解
- --proxy-client-key-file=/etc/kubernetes/pki/front-proxy-client.key
- --requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt
- --service-account-key-file=/etc/kubernetes/pki/sa.pub # 留到service count 小节再描述
- --service-account-signing-key-file=/etc/kubernetes/pki/sa.key
- --tls-cert-file=/etc/kubernetes/pki/apiserver.crt # apiserver作为服务端对外提供服务的证书
- --tls-private-key-file=/etc/kubernetes/pki/apiserver.key # apiserver的服务端证书对应的密钥
从参数列表来看,总结一下它们的作用: 因为双向验证,apiserver需要传入客户端证书(如apiserver作为etcd的客户端)和服务端证书(让其他组件访问,普通pod,kubectl等),每一份证书都需要一份数字签名用于证书数据的验证。
front-proxy-client.crt
是用于k8s聚合层的,目前没有了解过,跳过。接下来看一下kube-controller-manager的启动命令:
yamlspec:
containers:
- command:
- kube-controller-manager
- --authentication-kubeconfig=/etc/kubernetes/controller-manager.conf
- --authorization-kubeconfig=/etc/kubernetes/controller-manager.conf
- --bind-address= 0.0.0.0
- --client-ca-file=/etc/kubernetes/pki/ca.crt # 验证client证书的根证书
- --cluster-name=kubernetes
- --cluster-signing-cert-file=/etc/kubernetes/pki/ca.crt # 用于签发用户证书的根证书
- --cluster-signing-key-file=/etc/kubernetes/pki/ca.key #用户签发用户证书的签名
- --controllers=*,bootstrapsigner,tokencleaner
- --kubeconfig=/etc/kubernetes/controller-manager.conf
- --leader-elect=true
- --requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt
- --root-ca-file=/etc/kubernetes/pki/ca.crt # secret内证书,将会给pod用于访问apiserver
- --service-account-private-key-file=/etc/kubernetes/pki/sa.key # 下面会讲解
- --use-service-account-credentials=true
--cluster-signing-cert-file
和--cluster-signing-key-file
的的作用是签发用户证书,k8s提供了certificates.k8s.io
API目的就是用集群的根证书来签发用户证书。不过与重点还是关注controller-manager.conf
的内容,如下:
yamlapiVersion: v1
clusters:
- cluster:
# 用于验证 kube-apiserver 服务器证书的 CA 根证书,因为组件之间是双向认证的,所以apiserver也会往
# controller manager 发送一个证书,controller manager 就用下面这个配置项验证apiserver的证书
certificate-authority-data: xxxx....
kind: Config
preferences: {}
users:
- name: system:kube-controller-manager
user:
# controller-manager 用于访问api-server的证书
client-certificate-data: xxxx....
# controller-manager 证书所对应的密钥
client-key-data: xxxxxx...
cluster:certificate-authority-data
的内容实际上就/etc/kubernetes/pki/ca.crt
,只不过在controller-manager.conf以base64编码了,作用是验证apiserver的服务端证书。另外一个角度来说,还是因为双向认证的原因, controller-manager访问apisever也需要提供一份证书和签名,这也就是user:client-certificate-data
的内容。
Note: 从文档找到对于--client-ca-file
的描述如下:
--client-ca-file
: When a request arrives to the Kubernetes apiserver, if this option is enabled, the Kubernetes apiserver checks the certificate of the request. If it is signed by one of the CA certificates in the file referenced by--client-ca-file
, then the request is treated as a legitimate request, and the user is the value of the common nameCN=
, while the group is the organizationO=
. See the documentation on TLS authentication.
kubelet的启动没有manifest,是以system.service形式运行,直接使用ps
查看kubelet启动参数:
shell$ ps -aux | grep kubelet
/usr/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf --config=/var/lib/kubelet/config.yaml --network-plugin=cni --pod-infra-container-image=k8s.gcr.io/pause:3.6
可以看到,比较关键的就是几个配置文件。kubelet.conf内容如下:
yamlapiVersion: v1
clusters:
- cluster:
certificate-authority-data: xxxxx...
server: https://10.246.204.219:6443
# 省略了一些不相关内容
users:
- name: system:node:ygj-k8s-master
user:
client-certificate: /var/lib/kubelet/pki/kubelet-client-current.pem
client-key: /var/lib/kubelet/pki/kubelet-client-current.pem
配置项server
指明了要交互的apiserver地址,certificate-authority-data
是集群的ca经过base64以后的内容,可以与master节点的ca证书内容进行对比确认。kubelet 需要与apiserver交互,所用的证书和私钥都位于 /var/lib/kubelet/pki/kubelet-client-current.pem
,也就是说证书和私钥位于一份.pem
文件,直接查看文件的内容就可以确认了。从这里引申出来的一项内容是k8s的TLS-boostrapping机制,目的是简化worker节点证书生成的过程,bootstrapping机制以利于bootstrap-kubelet.conf
这个配置文件。
目录/var/lib/kubelet/pki
还有kubelet.crt
是kubelet的服务端证书,是kubelet对外提供服务所用的证书,因此与kubelet的基本功能无关,对于这个证书的使用参考: https://github.com/kubernetes/kubeadm/issues/2186
kube-proxy不同于其他系统组件使用.conf
文件,kube在证书的使用上和普通的pod没有差别,通过secret将集群的ca挂载到集群内部,它与apiserver之间是单向认证的,即只用集群ca校验apiserver的证书。下面是一个示例:
shell$ k describe pod kube-proxy-ps9hf -n kube-system
# 省略了一些内容
Mounts:
/lib/modules from lib-modules (ro)
/run/xtables.lock from xtables-lock (rw)
/var/lib/kube-proxy from kube-proxy (rw)
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-dbdgd (ro)
#... 省略了一些内容
kube-api-access-dbdgd:
Type: Projected (a volume that contains injected data from multiple sources)
TokenExpirationSeconds: 3607
ConfigMapName: kube-root-ca.crt
ConfigMapOptional: <nil>
DownwardAPI: true
所以在排查证书相关的问题时不需要关注kube-proxy。
controller-manager与api-server有与service account相关的参数分别是:
shell# controller manager
- kube-controller-manager
- --service-account-private-key-file=/etc/kubernetes/pki/sa.key
# apiserver
- --service-account-key-file=/etc/kubernetes/pki/sa.pub
- --service-account-signing-key-file=/etc/kubernetes/pki/sa.key
按照文档描述,controller-magager的--service-account-private-key-file
用于对service account token的签名。
Filename containing a PEM-encoded private RSA or ECDSA key used to sign service account tokens.
apiserver的--service-account-key-file
用于对签名的校验
File containing PEM-encoded x509 RSA or ECDSA private or public keys, used to verify ServiceAccount tokens. The specified file can contain multiple keys, and the flag can be specified multiple times with different files. If unspecified, --tls-private-key-file is used. Must be specified when --service-account-signing-key-file is provided
按照我个人的理解,这就是数字签名的过程,确保信息没有被修改。新的问题是: service account token到底什么? 从这里引申出service account、secret的内容。
service account的作用类似于账号密码,将会作为访问API server的凭证。每个namespace下都会有一个默认的default
service accuont,当一个pod被创建时如果没有指定service account,那么就使用这个默认的service account,它只具有默认的权限。每个service account都有一个与之相对应的secret,下面是一个nfs provisioner示例:
shell$ kubectl get serviceaccount nfs-client-provisioner -o yaml
apiVersion: v1
kind: ServiceAccount
metadata:
creationTimestamp: "2023-08-07T06:06:23Z"
name: nfs-client-provisioner
namespace: default
resourceVersion: "470582"
uid: 9d89b220-1b2c-4820-8e38-bc60488ae8ee
# 该service account所用secret
secrets:
- name: nfs-client-provisioner-token-b2vcn
而每个pod在启动时绑定一个secret volume,如果没有指定service account它绑定的是默认的secert。在老一点的版本中,k8s直接以 secret volume绑定到pod内,比较新的版本(1.2.x 具体未考证)使用了projected volume,作用是将多种类型的数据挂载到同一个目录,kubectl describe pod
查看任意pod就可以看到。概括来说,虽然在pod的manifest中我们配置的是service account,但是它实际起效的是通过 secret挂载到了pod内部。再来看看secret,打开任意一个secret的内容:
shell$ kubectl describe secret default-token-mg4kv
Name: default-token-mg4kv
Namespace: default
Labels: <none>
Annotations: kubernetes.io/service-account.name: default
kubernetes.io/service-account.uid: 5ff4a69f-654a-48f2-b284-57f50ee07e7c
Type: kubernetes.io/service-account-token
Data
====
ca.crt: 1099 bytes
namespace: 7 bytes
token: eyJhbGciOiJSUzI1N.... # 省略了一些数据
如果直接用kubectl get
得到的是被base64 编码后的数据,这些数据的是:
ca.crt: 用于校验服务端所用的证书。比如pod要访问apiserver的数据就需要用该证书去验证apiserve所提供的server证书,也就是apiserver的--tls-cert-file
参数所指定的证书。实际上ca.crt的内容就是根证书ca.crt,而apiserver.crt又是由ca.crt签发的,所以用ca.crt来验证是显而易见的。
namespace: 该secret所处的namespace
token: 是一个jwt token,使用base64解码以后可以去jwt官网解码,其中的信息如下图,可以看到serviceaccount的信息尽在其中,apiserver就可以用这些信息来判断一个 pod 是否有权限访问一些资源,token在生成的时候会用/etc/kubernetes/pki/sa.key
生成签名。
引用文档的相关描述:
A
kubernetes.io/service-account-token
type of Secret is used to store a token credential that identifies a service account.
第一个问题是,ca.crt
的内容是从哪儿来的?**ca.crt的内容就是来自controller-manager参数中的--root-ca-file
**也就是集群的ca。这一切都顺理成章了,secret里的ca.crt
通过volume挂载到pod内部,pod用它来校验apiserver的https证书,apiserver的https证书又是从这个ca.crt
签发 。
token是jwt token,下面是经过解码后的JWT token示例。所以回到最初的问题,service account token就是这个jwt token。By the way,我们日常使用kubectl
访问apiserver总体的流程也是与上面描述的流程没什么区别,查看~/.kube/config
内容也是类似,无非是证书和秘钥的内容。
最后以一张图概括下上面的所有提到的内容,图来自:
本篇草草的过了下与kubernetes证书相关的内容,无论是从证书与加密本身还是从k8s内容都不够详尽,并且是自学的所以在内容上可能有些纰漏和错误,希望大家指出。
https://qingwave.github.io/k8s-tls
https://pandaychen.github.io/2020/04/09/A-KUBERNETES-CERTIFICATE-PRINCIPLE/
https://www.zhaohuabing.com/post/2020-05-19-k8s-certificate/
本文作者:strickland
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!