众所周知,kubernetes中提供了一种名为secrets的对象,用于存放集群内部使用的各类敏感数据,比如数据库用户名、密码、各种token、证书等等,从而使得敏感信息和普通配置文件有效解耦。但是默认情况下secrets信息在etcd中是以base64编码形式保存的明文,本篇文章说明如何通过插件加密存储机密数据。
加密插件配置
总体来说配置比较简单,跟着官网的说明做就ok。官方连接:https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/。这里面值得注意的是,kube-apisever的加密插件配置参数为--encryption-provider-config
,在1.13版本之前是--experimental-encryption-provider-config
,该参数在1.14版本之后已经被正式废弃。
配置文件示例:
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- identity: {}
- aesgcm:
keys:
- name: key1
secret: c2VjcmV0IGlzIHNlY3VyZQ==
- name: key2
secret: dGhpcyBpcyBwYXNzd29yZA==
- aescbc:
keys:
- name: key1
secret: c2VjcmV0IGlzIHNlY3VyZQ==
- name: key2
secret: dGhpcyBpcyBwYXNzd29yZA==
- secretbox:
keys:
- name: key1
secret: YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY=
其中resources
可以是多组独立的配置,每组配置下定义了该组资源的加解密的策略,比如这个配置文件定义了secrets资源的加解密策略。providers
定义了加解密的实际提供者,目前k8支持的provider如下所示:
名称 | 加密类型 | 强度 | 速度 | 密钥长度 | 其它事项 |
---|---|---|---|---|---|
identity |
无 | N/A | N/A | N/A | 不加密写入的资源。当设置为第一个 provider 时,资源将在新值写入时被解密。 |
aescbc |
填充 PKCS#7 的 AES-CBC | 最强 | 快 | 32字节 | 建议使用的加密项,但可能比 secretbox 稍微慢一些。 |
secretbox |
XSalsa20 和 Poly1305 | 强 | 更快 | 32字节 | 较新的标准,在需要高度评审的环境中可能不被接受。 |
aesgcm |
带有随机数的 AES-GCM | 必须每 200k 写入一次 | 最快 | 16, 24, 或者 32字节 | 建议不要使用,除非实施了自动密钥循环方案。 |
kms |
使用信封加密方案:数据使用带有 PKCS#7 填充的 AES-CBC 通过 data encryption keys(DEK)加密,DEK 根据 Key Management Service(KMS)中的配置通过 key encryption keys(KEK)加密 | 最强 | 快 | 32字节 | 建议使用第三方工具进行密钥管理。为每个加密生成新的 DEK,并由用户控制 KEK 轮换来简化密钥轮换。配置 KMS 提供程序 |
其中identity就是明文,不加密。其余就是各类加解密算法,建议使用aescbc
,足够用了,其实就是使用CBC模式、PKCS#7填充的aes256加密。这里要注意的是,providers中可以设置多个加密provider
,每个provider
可以设置多个加密的密钥。
加解密规则
- 加密:kube-apiserver默认会使用第一个provider的第一个key进行加密(上面这个例子里面就是明文不加密了)
- 解密:会依次尝试所有的解密算法,每个算法中会依次尝试所有的key,如果全部尝试失败,则会返回一个错误,以阻止客户端访问该资源。这么设置的原因,当然也是因为一旦你更换了加密密钥(或者加密算法),还能保证你原来用老的密钥(算法)加密的数据还可以正常的访问(加密插件会按照有序列表中的定义挨个尝试)。
kube-apiserver配置
到这里按照常规流程你一定想kubectl create -f
来创建这个资源了,如果你这么做了,不出意外的话会看到如下的报错:
error: unable to recognize "encrypt.conf": no matches for kind "EncryptionConfiguration" in version "apiserver.config.k8s.io/v1"
这是因为,kube-apiserver的相关资源,是不能通过kubectl命令来创建的,官方文档并没有明确说明,其实也很好理解,自己怎么创建自己嘛!这个资源,只能是通过配置启动参数在kube-apisever启动的时候来加载。这边我使用kubeadm安装的集群,配置文件位置在/etc/kubernetes/manifests
,找到kube-apiserver.yaml
,这个就是kube-apiserver启动用的配置文件(用其他方式安装的也类似,只要找到这个配置文件就可以)。所以说这个加密插件的启动,目前来说貌似只能在私有集群中实现,如果你用的是gke、ake、tke这样的云服务商提供的集群就不行了。
tips: kube-apiserver这个pod和普通pod不同,是一个静态pod(static pod),也就是直接启动在特定的node上,由该宿主机的kubelet直接控制。pod不会漂移,配置文件也在宿主机上面,kubelet在启动时,通过读取
/etc/kubernetes/manifests
里面的配置信息,直接拉起pod。事实上如果你使用kubeadm,则会安装4个静态pod,分别是etcd、kube-apiserver、kube-scheduler、kube-controller-manager。不难看出这些就是保障k8集群正常运作的核心组件,其他插件诸如core-dns、kube-proxy、calico等都是以daemonset或者deployment形式运行的普通pod。在修改了/etc/kubernetes/manifests
里的配置信息后,kubelet会自动重启该静态pod。
我们在这个配置文件中加入如下信息:
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
component: kube-apiserver
tier: control-plane
name: kube-apiserver
namespace: kube-system
spec:
containers:
- command:
- kube-apiserver
- --encryption-provider-config=/etc/kubernetes/pki/encrypt.conf
- --advertise-address=192.168.31.241
- --allow-privileged=true
- --authorization-mode=Node,RBAC
.....
其中encryption-provider-config
就是配置插件插件启动时读取的配置文件所在位置,这边我们把前面写的配置文件命名为encrypt.conf
,放在/etc/kubernetes/pki
目录下,通过阅读配置文件,我们可以看到kube-apiserver在启动的时候会挂载三个宿主机目录,其中就有/etc/kubernetes/pki
。所以你把配置文件放在这里,kube-apiserver启动的时候就能正确找到这个配置文件了。
加密功能验证
kube-apisever重启后,我们就可以尝试一下,看看加密功能是否生效。这里我们用到的配置文件如下:
kind: EncryptionConfiguration
apiVersion: apiserver.config.k8s.io/v1
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: SSM5rRRrQ9+8MsA2cHeRfb7KG9rvF/wsqHOgoQAv5bM=
- identity: {}
这个配置保证了我们新建的secrets资源都会默认使用aescbc
加密算法,并且使用key1中定义的这个密钥来加密数据。注意identity
这个参数必须要设置,否则所有我们之前建立的secrets都会无法访问。具体原因其实上面已经讲过,大家可以想一下为什么。
现在我们来新建一个serctes资源来验证一下
kubectl create secret generic secret1 -n default --from-literal=mykey=mydata
之后登陆入容器etcd中
kubectl exec -it etcd-miwifi-r1cm-srv -n kube-system /bin/sh
执行etcdctl命令查看刚才建立的secret1密钥的内容,这里要注意的是etcd默认的api版本是v2,k8默认使用的版本是v3,两者互不兼容,所以在执行的时候需要在命令前显式的加上ETCDCTL_API=3
来告诉etcdctl我要调用的是v3 api,或者使用环境变量export指定也可以。由于v3默认开启了ssl认证,所以在调用的时候还需要加上连接认证信息,这部分内容可以在etcd.yaml
的livenessProbe
这个配置中查看到,把这条命令复制出来,后面加上secret1的路径,就可以查看到secret内容了
/ # ETCDCTL_API=3 etcdctl --endpoints=https://[127.0.0.1]:2379 --cacert=/etc/kubernetes/pki/etcd/ca.crt --cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt --key=/etc/kubernetes/pki/etcd/healthcheck-client.key get /registry/secrets/default/secret1 | hexdump -C
00000000 2f 72 65 67 69 73 74 72 79 2f 73 65 63 72 65 74 |/registry/secret|
00000010 73 2f 64 65 66 61 75 6c 74 2f 73 65 63 72 65 74 |s/default/secret|
00000020 31 0a 6b 38 73 3a 65 6e 63 3a 61 65 73 63 62 63 |1.k8s:enc:aescbc|
00000030 3a 76 31 3a 6b 65 79 31 3a d5 62 9c a5 45 d3 76 |:v1:key1:.b..E.v|
00000040 50 b5 4f 44 66 22 a6 37 2d 95 87 e9 93 65 72 a4 |P.ODf".7-....er.|
00000050 2d 97 b1 b6 44 b0 8e 7c 27 ba 99 61 86 56 a7 97 |-...D..|'..a.V..|
00000060 21 03 eb 46 93 a9 ba f7 c1 63 fe 5c 34 12 9d 54 |!..F.....c.\4..T|
00000070 ba 3e 73 d5 71 b4 b9 28 ac 0e 66 6e a2 09 44 48 |.>s.q..(..fn..DH|
00000080 cf c6 da 4a 24 6d 49 06 dd f4 e6 85 ff ab e0 e3 |...J$mI.........|
00000090 ed 59 07 98 c2 3e 33 9e 91 f7 9a 9e d1 7f db 65 |.Y...>3........e|
000000a0 f8 60 40 2d 7c 86 1a f2 8b 37 67 c8 83 d3 5e 7b |.`@-|....7g...^{|
000000b0 fa 51 35 f1 ee d7 51 28 81 a3 9b bd 6d 80 bb e7 |.Q5...Q(....m...|
000000c0 b8 0e 4b 85 0e 90 f3 50 41 0a |..K....PA.|
注意在数据头部出现k8s:enc:aescbc:v1:
,说明数据已经被正确加密,使用的是aescbc
算法,使用的密钥为key1
。
接下来我们看下kube-apiserver在读取的时候是否正确解密了,执行下面的命令
[root@MiWiFi-R1CM-srv manifests]# kubectl get secrets secret1 -o yaml
apiVersion: v1
data:
mykey: bXlkYXRh
kind: Secret
metadata:
creationTimestamp: "2019-08-16T15:12:14Z"
name: secret1
namespace: default
resourceVersion: "549592"
selfLink: /api/v1/namespaces/default/secrets/secret1
uid: 3a74e0fd-c038-11e9-95ca-0800279f163b
type: Opaque
得到mykey
的base64编码数据bXlkYXRh
,将其decode一下
[root@MiWiFi-R1CM-srv manifests]# echo -n "bXlkYXRh" | base64 --decode
mydata
没错,正是我们设置的sercets机密数据,试验成功!
总结
通过kubernetes提供的加密插件,使得etcd中存放的secrets数据都以密文的形式存放,这无异大大提高了数据安全性。但是要明确一点,加密插件只是加密了etcd中保存的数据,这意味着你执行kubectl get secrets mysecret -o yaml
这样的命令看到的仍然是明文,在容器内部注入的secrets文件或者环境变量看到的也是明文,原因当然是kube-apiserver在从etcd中取出数据的时候已经帮你自动解密了。如果你有全程加密的需求(比如说想在容器内看到的也是密文),这显然是kubernetes这种平台层的工具做不到的,因为这已经涉及到了应用的改造。
其实就目前的实际使用场景看,如果你有将etcd直接暴露给集群内第三方服务使用或者直接暴露给外部服务使用的需求(一般非常少),那么你最好使用加密插件,否则会面临机密数据泄漏的风险。而如果etcd仅供k8s的系统组件来使用的话,由于kubernetes本身已经有比较完善的rbac机制,那么你只要做好kube-apiserver的权限管理即可,例如:
- etcd启用安全连接机制,严格禁止非系统组件的直接访问
- kubectl客户端工具只能在master节点上由系统管理员操作
- 各服务内的serviceaccount做好权限管理,禁止直接访问secrets资源
那么其实也未必需要加密,k8s默认提供的secrets策略已经完全能够满足要求(毕竟你即使在etcd中加密了,有kubectl权限的和有访问secrets权限的账号还是可以看到明文)。