眾所周知,kubernetes中提供了一種名為secrets的對象,用于存放集群內(nèi)部使用的各類敏感數(shù)據(jù),比如數(shù)據(jù)庫用戶名、密碼、各種token、證書等等,從而使得敏感信息和普通配置文件有效解耦。但是默認(rèn)情況下secrets信息在etcd中是以base64編碼形式保存的明文,本篇文章說明如何通過插件加密存儲機密數(shù)據(jù)。
加密插件配置
總體來說配置比較簡單,跟著官網(wǎng)的說明做就ok。官方連接:https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/。這里面值得注意的是,kube-apisever的加密插件配置參數(shù)為--encryption-provider-config
,在1.13版本之前是--experimental-encryption-provider-config
,該參數(shù)在1.14版本之后已經(jīng)被正式廢棄。
配置文件示例:
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 | 不加密寫入的資源。當(dāng)設(shè)置為第一個 provider 時,資源將在新值寫入時被解密。 |
aescbc |
填充 PKCS#7 的 AES-CBC | 最強 | 快 | 32字節(jié) | 建議使用的加密項,但可能比 secretbox 稍微慢一些。 |
secretbox |
XSalsa20 和 Poly1305 | 強 | 更快 | 32字節(jié) | 較新的標(biāo)準(zhǔn),在需要高度評審的環(huán)境中可能不被接受。 |
aesgcm |
帶有隨機數(shù)的 AES-GCM | 必須每 200k 寫入一次 | 最快 | 16, 24, 或者 32字節(jié) | 建議不要使用,除非實施了自動密鑰循環(huán)方案。 |
kms |
使用信封加密方案:數(shù)據(jù)使用帶有 PKCS#7 填充的 AES-CBC 通過 data encryption keys(DEK)加密,DEK 根據(jù) Key Management Service(KMS)中的配置通過 key encryption keys(KEK)加密 | 最強 | 快 | 32字節(jié) | 建議使用第三方工具進行密鑰管理。為每個加密生成新的 DEK,并由用戶控制 KEK 輪換來簡化密鑰輪換。配置 KMS 提供程序 |
其中identity就是明文,不加密。其余就是各類加解密算法,建議使用aescbc
,足夠用了,其實就是使用CBC模式、PKCS#7填充的aes256加密。這里要注意的是,providers中可以設(shè)置多個加密provider
,每個provider
可以設(shè)置多個加密的密鑰。
加解密規(guī)則
- 加密:kube-apiserver默認(rèn)會使用第一個provider的第一個key進行加密(上面這個例子里面就是明文不加密了)
- 解密:會依次嘗試所有的解密算法,每個算法中會依次嘗試所有的key,如果全部嘗試失敗,則會返回一個錯誤,以阻止客戶端訪問該資源。這么設(shè)置的原因,當(dāng)然也是因為一旦你更換了加密密鑰(或者加密算法),還能保證你原來用老的密鑰(算法)加密的數(shù)據(jù)還可以正常的訪問(加密插件會按照有序列表中的定義挨個嘗試)。
kube-apiserver配置
到這里按照常規(guī)流程你一定想kubectl create -f
來創(chuàng)建這個資源了,如果你這么做了,不出意外的話會看到如下的報錯:
error: unable to recognize "encrypt.conf": no matches for kind "EncryptionConfiguration" in version "apiserver.config.k8s.io/v1"
這是因為,kube-apiserver的相關(guān)資源,是不能通過kubectl命令來創(chuàng)建的,官方文檔并沒有明確說明,其實也很好理解,自己怎么創(chuàng)建自己嘛!這個資源,只能是通過配置啟動參數(shù)在kube-apisever啟動的時候來加載。這邊我使用kubeadm安裝的集群,配置文件位置在/etc/kubernetes/manifests
,找到kube-apiserver.yaml
,這個就是kube-apiserver啟動用的配置文件(用其他方式安裝的也類似,只要找到這個配置文件就可以)。所以說這個加密插件的啟動,目前來說貌似只能在私有集群中實現(xiàn),如果你用的是gke、ake、tke這樣的云服務(wù)商提供的集群就不行了。
tips: kube-apiserver這個pod和普通pod不同,是一個靜態(tài)pod(static pod),也就是直接啟動在特定的node上,由該宿主機的kubelet直接控制。pod不會漂移,配置文件也在宿主機上面,kubelet在啟動時,通過讀取
/etc/kubernetes/manifests
里面的配置信息,直接拉起pod。事實上如果你使用kubeadm,則會安裝4個靜態(tài)pod,分別是etcd、kube-apiserver、kube-scheduler、kube-controller-manager。不難看出這些就是保障k8集群正常運作的核心組件,其他插件諸如core-dns、kube-proxy、calico等都是以daemonset或者deployment形式運行的普通pod。在修改了/etc/kubernetes/manifests
里的配置信息后,kubelet會自動重啟該靜態(tài)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資源都會默認(rèn)使用aescbc
加密算法,并且使用key1中定義的這個密鑰來加密數(shù)據(jù)。注意identity
這個參數(shù)必須要設(shè)置,否則所有我們之前建立的secrets都會無法訪問。具體原因其實上面已經(jīng)講過,大家可以想一下為什么。
現(xiàn)在我們來新建一個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
執(zhí)行etcdctl命令查看剛才建立的secret1密鑰的內(nèi)容,這里要注意的是etcd默認(rèn)的api版本是v2,k8默認(rèn)使用的版本是v3,兩者互不兼容,所以在執(zhí)行的時候需要在命令前顯式的加上ETCDCTL_API=3
來告訴etcdctl我要調(diào)用的是v3 api,或者使用環(huán)境變量export指定也可以。由于v3默認(rèn)開啟了ssl認(rèn)證,所以在調(diào)用的時候還需要加上連接認(rèn)證信息,這部分內(nèi)容可以在etcd.yaml
的livenessProbe
這個配置中查看到,把這條命令復(fù)制出來,后面加上secret1的路徑,就可以查看到secret內(nèi)容了
/ # 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.|
注意在數(shù)據(jù)頭部出現(xiàn)k8s:enc:aescbc:v1:
,說明數(shù)據(jù)已經(jīng)被正確加密,使用的是aescbc
算法,使用的密鑰為key1
。
接下來我們看下kube-apiserver在讀取的時候是否正確解密了,執(zhí)行下面的命令
[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編碼數(shù)據(jù)bXlkYXRh
,將其decode一下
[root@MiWiFi-R1CM-srv manifests]# echo -n "bXlkYXRh" | base64 --decode
mydata
沒錯,正是我們設(shè)置的sercets機密數(shù)據(jù),試驗成功!
總結(jié)
通過kubernetes提供的加密插件,使得etcd中存放的secrets數(shù)據(jù)都以密文的形式存放,這無異大大提高了數(shù)據(jù)安全性。但是要明確一點,加密插件只是加密了etcd中保存的數(shù)據(jù),這意味著你執(zhí)行kubectl get secrets mysecret -o yaml
這樣的命令看到的仍然是明文,在容器內(nèi)部注入的secrets文件或者環(huán)境變量看到的也是明文,原因當(dāng)然是kube-apiserver在從etcd中取出數(shù)據(jù)的時候已經(jīng)幫你自動解密了。如果你有全程加密的需求(比如說想在容器內(nèi)看到的也是密文),這顯然是kubernetes這種平臺層的工具做不到的,因為這已經(jīng)涉及到了應(yīng)用的改造。
其實就目前的實際使用場景看,如果你有將etcd直接暴露給集群內(nèi)第三方服務(wù)使用或者直接暴露給外部服務(wù)使用的需求(一般非常少),那么你最好使用加密插件,否則會面臨機密數(shù)據(jù)泄漏的風(fēng)險。而如果etcd僅供k8s的系統(tǒng)組件來使用的話,由于kubernetes本身已經(jīng)有比較完善的rbac機制,那么你只要做好kube-apiserver的權(quán)限管理即可,例如:
- etcd啟用安全連接機制,嚴(yán)格禁止非系統(tǒng)組件的直接訪問
- kubectl客戶端工具只能在master節(jié)點上由系統(tǒng)管理員操作
- 各服務(wù)內(nèi)的serviceaccount做好權(quán)限管理,禁止直接訪問secrets資源
那么其實也未必需要加密,k8s默認(rèn)提供的secrets策略已經(jīng)完全能夠滿足要求(畢竟你即使在etcd中加密了,有kubectl權(quán)限的和有訪問secrets權(quán)限的賬號還是可以看到明文)。