一、背景
我们知道Kubernetes提供了Secret对象用于管理业务敏感数据,比如用户名密码、私钥等。Secret最终默认是存储在etcd中的,为了保证Secret的安全,需要保证两点:
(1)密钥的传输是安全的,这一点很容易做到,etcd配置使用TLS即可,整个传输过程均使用HTTPS协议。
(2)密钥的存储是安全的,这在Kubernetes v1.7也已经实现了,Kubernetes支持配置Secret使用多种加密方式存储,支持的加密算法如AES-CBC、AES-GCM等,在配置文件中支持自定义加密Key以及轮转,具体可以参考Encrypting Secret Data at Rest[1]。
这看似已经完美解决了,但是对于Kubernetes平台这一侧来说,虽然Secret在etcd是加密存储的,但在平台侧看到的是解密的数据,即众所周知的Base64编码,当然只要控制好Secret的访问权限则可以实现保护数据的安全。
但是如果我们使用类似Git/SVN的版本控制工具来管理这些应用的Manifests yaml文件,或者使用类似GitOps的CD集成工具,这些Base64编码的Secret就很不安全了,总不会有人把Secret直接存储在Git或者SVN上吧。
通常的做法是Secret从Git仓库中分离出来由管理员管理,当应用部署时手动先创建这些Secret实例,这种需要人工干预的方式效率非常低,也违背了应用持续集成的高效率快敏捷的原则。
二、Bitnami SealedSecret方案
Bitnami提供了一种可行方案,其开源的SealedSecrets[2]工具能支持对Kubernetes的Secret使用非对称加密算法加密并转化为新的CRD对象SealedSecret,SealedSecret由于是加密后的数据因此可以安全地push到代码仓库中,当应用部署时Kubernetes创建SealedSecret对象实例,控制器会监听SealedSecret对象实例状态,自动地对SealedSecret实例进行解密并映射为对应的Kubernetes Secret对象,相当于对Secret做了一层中间加密存储过程。
在Kubernetes集群中配置使用SealedSecret也很简单,只需要安装其控制器:
$ kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.16.0/controller.yaml
并安装加密客户端工具即可,该工具用于对Kubernetes原生Secret加密为SealedSecret对象:
wget https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.16.0/kubeseal-linux-amd64 -O kubeseal
创建一个demo secret并通过kubeseal加密:
# kubectl create secret generic demo-secret \
--from-literal=username=int32bit \
--from-literal=password=NoMoreSecret \
-o yaml --dry-run=client \
| kubeseal -o yaml | tee demo-secret.yaml
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: demo-secret
namespace: sealed-secrets
spec:
encryptedData:
password: Ag...9A== # 非常长的加密数据
username: Ag...8C== # 非常长的加密数据
template:
data: null
metadata:
name: demo-secret
namespace: sealed-secrets
如上通过kubeseal加密Secret后生成了新的SealedSecret对象demo-secret.yaml
,可以安全地push到Git仓库中。
当应用部署时直接基于这个新的对象创建SealedSecret:
# kubectl apply -f demo-secret.yaml
sealedsecret.bitnami.com/demo-secret created
SealedSecret Controller会自动创建对应的Secret:
# kubectl get sealedsecrets demo-secret -o yaml
apiVersion: v1
data:
password: Tm9Nb3JlU2VjcmV0
username: aW50MzJiaXQ=
kind: Secret
metadata:
name: demo-secret
namespace: sealed-secrets
ownerReferences:
- apiVersion: bitnami.com/v1alpha1
controller: true
kind: SealedSecret
name: demo-secret
uid: 3273d7bb-9629-4fe2-bbfd-75631ce24a87
resourceVersion: "46585715"
uid: 88eb269b-87f7-48a3-8032-65502648665a
type: Opaque
从ownerReferences
可以看出这个Secret就是由SealedSecret自动创建的,应用不需要做任何修改完全无侵入就可以读取Secret值。
当然你也可以不用kubeseal工具加密,选择自己实现加密,只需要使用对应的公钥即可,公钥可以通过工具或者API获取:
kubeseal --fetch-cert | openssl x509 -text -noout
不过需要注意的是,SealedSecret使用非对称加密算法,我们知道公钥用于加密,私钥用于解密,而私钥是托管在部署SealedSecret的Kubernetes集群的,在另一个Kubernetes集群是没法解密的。这就导致无法实现跨多环境场景下做自动化集成,比如SIT、UAT、DEV环境如果用的是不同的Kubernetes集群,相对应的加密SealedSecret Manifest文件不一样,SIT的环境的SealedSecret无法在UAT环境中解密。
当然你可以把私钥导出来,再导入到另一个集群,即所有环境的Kubernetes集群共享使用相同的密钥对,这样解决了所有的集群都可以解密的问题。不过这样也存在非常大的私钥安全风险,一旦私钥泄露了,所有环境的Secret也会全部泄露。
三、Kamus方案
前面介绍的SealedSecret使用自己的私钥加密,与特定的集群关联在一起。实际生产环境中可能需要外部更安全专业的密钥管理工具,甚至使用硬件加密机。
如果使用Helm工具管理应用,可以考虑helm-secrets[3]插件,支持集成AWS KMS、GCP KMS等密钥托管工具。
不过我更推荐Kamus[4],这也是一款针对Kubernetes Secret加密的开源工具,与SealedSecret不一样的主要有两点:
(1)提供多种Provider支持各种云上的密钥托管工具,比如Azure KeyVault、 Google Cloud KMS、AWS KMS等。 (2)遵循零信任模型,即只有关联了指定的ServiceAccount的应用可以解密,其他应用没有任何办法拿到解密数据,相当于做了一层严格的权限管控。这个和网络总讲的微分段零信任模型原则是一样的:)
如果在公有云场景,Kamus显然是非常适合的,能直接与公有云的KMS服务联动起来统一对密钥进行管理。
四、Vault方案
在自建私有云环境下,可能无法与公有云上的KMS服务交互,因此前面介绍的Kamus无法应用到私有环境中,可以考虑使用Hashicorp公司开源的Vault工具作为替代方案,Vault作为一款企业级密钥信息管理工具,提供了非常完善的工具和方法集成Kubernetes,实现了Kamus相同的功能,原理也是大体相同的。
关于Vault的相关介绍可以参考Vault官方文档[5],这里不对Vault本身做太多的介绍。
如果是测试的话如下命令即可快速部署一个demo环境:
# helm repo add hashicorp https://helm.releases.hashicorp.com
# helm repo update
# cat helm-vault-values.yml
csi:
enabled: true
injector:
enabled: true
server:
dev:
enabled: true
# helm install vault hashicorp/vault --values helm-vault-values.yml
测试时直接使用root token认证,实际生产时应该创建严格管控的用户以及权限Policy策略。
写入测试数据如下:
kubectl exec -it vault-0 -- /bin/sh
vault secrets enable -path=internal kv-v2
vault kv put internal/database/config username="int32bit" password="db-secret-password"
此时可以通过vault CLI或者API获取写入的数据:
/ $ vault kv get internal/database/config
====== Metadata ======
Key Value
--- -----
created_time 2021-06-08T10:20:12.21987623Z
deletion_time n/a
destroyed false
version 2
====== Data ======
Key Value
--- -----
password db-secret-password
username int32bit
此时Kubernetes应用可以通过vault API获取secret数据,建议通过initContainer完成Vault认证并获取数据写入到应用共享的volume中,应用直接从volume中读取数据。
不过这种方式虽然只需要InitContainer拥有Vault凭证,应用起来后InitContainer就销毁了,但还是意味着Vault凭证需要托管在Kubernetes中,这就是先有鸡还是先有蛋的问题,因此这种方式并不推荐在生产上使用,推荐的方法参考如下章节介绍。
4.1 Vault使用Kubernetes认证
前面介绍了直接读取Vault数据的方法有个问题,就是从vault中获取数据需要认证凭证(Vault的用户名密码或者token等),这个认证凭证又是敏感数据,虽然通过initContainer已经大大降低了vault认证信息泄露的风险,因为vault的认证信息只会存在initContainer中,而应用起来后initContainer就自动销毁了。不过存储在Kubernetes中还是不太安全的,万一泄露,则加密的数据自然不攻自破。
好在vault支持Kubernetes认证,这个初步听起来好像有点奇怪,Kubernetes作为vault的认证服务器?
是的,Vault借助了Kubernetes的TokenReviews功能,用于校验Token是否合法。以最简单的ServiceAccount为例,我们知道ServiceAccount是唯一由Kubernetes托管的用户认证实体,当然一般不用做自然人用户使用,而是给运行在Kubernetes的Pod应用使用,通过关联ServiceAccount给Pod授权再熟悉不过了,关于如何使用以及关联rolebingding/clusterrolebingding不再介绍,这里仅简单介绍下Kubernetes ServiceAccount认证的原理,关于Kubernetes认证可以参考我之前的一篇文章浅聊Kubernetes的各种认证策略以及适用场景。
其实ServiceAccount认证本质就是JWT Token认证,这个token会自动以volume的形式挂载到pod的/run/secrets/kubernetes.io/serviceaccount/token
路径上:
kubectl exec -t -i vault-0 -- cat /run/secrets/kubernetes.io/serviceaccount/token
这个JWT Token有三个部分内容构成,分别为头部元数据(加密算法、版本)、主体内容Body以及签名。
通过如下脚本可以解码头部元数据以及主体内容:
#!/bin/bash
decode_base64_url() {
LEN=$((${#1} % 4))
RESULT="$1"
if [ $LEN -eq 2 ]; then
RESULT+='=='
elif [ $LEN -eq 3 ]; then
RESULT+='='
fi
echo "$RESULT" | tr '_-' '/+' | base64 -d
}
# 解码JWT
decode_jwt()
{
JWT_RAW=$1
for line in $(echo "$JWT_RAW" | awk -F '.' '{print $1,$2}'); do
RESULT=$(decode_base64_url "$line")
echo "$RESULT"
done
}
decode_jwt $@
解码后的内容如下:
{"alg":"RS256","kid":"VnxYZVWpj5NIRNkn-mgm3kjPARoCriR6VocXAzINSi8"}
{
"iss": "kubernetes/serviceaccount",
"kubernetes.io/serviceaccount/namespace": "vault",
"kubernetes.io/serviceaccount/secret.name": "vault-token-psh5d",
"kubernetes.io/serviceaccount/service-account.name": "vault",
"kubernetes.io/serviceaccount/service-account.uid": "a0865e8b-798d-4426-a3d2-87c475387e44",
"sub": "system:serviceaccount:vault:vault"
}
这其实和x509证书的数字签名非常相似,头部数据声明了加密算法和版本协议,主体内容包含了用户信息,包括uid、name等。需要注意的是头部元数据和主体内容是完全公开的,并没有加密,仅使用了类似Base64的编码,不需要任何公钥私钥谁都可以完全解码,所以千万不要把敏感数据放到JWT Token中。
而签名部分则是为了确保Token完整性以及真实性,保证两点:一是这个Token确定是可信任的Kubernetes颁发,二是这个Token没有被人篡改过。这个JWT Token是由kube-controller-manager生成颁发的,通过--service-account-private-key-file
参数指定颁发Token的私钥文件,如果Kubernetes使用kubeadm部署则默认路径为/etc/kubernetes/pki/sa.key
,公钥为sa.pub
。
这样校验这个Token是不是合法的只需要拿到私钥对应的公钥sa.pub
即可,如果签名一致则说明Token是合法的,如果内容被篡改过,则必然签名不一致。
ServiceAccount不仅可以用于给Pod做Kubernetes的API认证,还可以用于运行在Kubernetes的微服务应用之间的认证。打个比方假设你的应用A需要调用数据仓库B,但是B尚未实现认证,如何最快速的给B加上认证功能?我觉得最简单的办法就是利用Kubernetes的ServiceAccount,简易步骤如下:
(1)使用Kubernetes创建一个ServiceAccount db_admin
并关联到应用A对应的Deployment上。(2)A应用每次调用B的时候必须读取/run/secrets/kubernetes.io/serviceaccount/token
内容放到Header中并发送到B。(3)B校验这个ServiceAccount,解码JWT Token并校对是不是db_admin
,同时校验Token是否合法,方法有两个,一是使用公钥sa.pub
进行JWT Token校验,二是直接调用Kubernetes的tokenreviews。
learnk8s.io有一个很好的例子可以参考Authentication between microservices using Kubernetes identities[6]
甚至可以不用修改B的代码,通过Pod的Ambassador Pattern模式实现代理认证即可,参考Extending applications on Kubernetes with multi-container pods[7]。
但是特别需要注意的是默认的ServiceAccount Token一旦创建就是永久有效,目前不支持自动轮转,手动轮转只能把ServiceAccount删了再重新创建,并且ServiceAccount不支持自定义audience从而做进一步的权限控制,换句话说只要关联了这个ServiceAccount就具有一样的权限。
社区很早就发现了这个问题,因此设计了ProjectedServiceAccountToken
,在实际生产中推荐使用ProjectedServiceAccountToken
,与ServiceAccount默认的Token最大的不同是支持设置TTL(过期时间)以及audience(颁发给谁)。
如下是一个使用ProjectedServiceAccountToken
的测试Pod,注意需要禁用默认ServiceAccount Token挂载,设置automountServiceAccountToken
为false
:
apiVersion: v1
kind: Pod
metadata:
labels:
run: test
name: test
spec:
serviceAccount: internal-app
automountServiceAccountToken: false
volumes:
- name: api-token
projected:
sources:
- serviceAccountToken:
path: internal-app
expirationSeconds: 600
audience: data-store
containers:
- image: nginx
imagePullPolicy: IfNotPresent
name: test
volumeMounts:
- mountPath: /var/run/secrets/tokens
name: api-token
dnsPolicy: ClusterFirst
restartPolicy: Always
查看Token:
kubectl exec -t -i test -- cat /run/secrets/tokens/internal-app
eyJhbGciOi...
解码后的内容如下:
{
"aud": [
"data-store"
],
"exp": 1623318918,
"iat": 1623318318,
"iss": "https://kubernetes.default.svc.cluster.local",
"kubernetes.io": {
"namespace": "vault",
"pod": {
"name": "test",
"uid": "f3f54b31-cc59-49cc-a080-9214d4c71a8a"
},
"serviceaccount": {
"name": "internal-app",
"uid": "e9faced0-e429-4985-9595-7ff4eb48b1e6"
}
},
"nbf": 1623318318,
"sub": "system:serviceaccount:vault:internal-app"
}
从原来的Token中我们发现明显多了iat、exp以及aud等字段,分别为颁发时间、过期时间以及audience。
在校验Token时除了校验Token的合法性,我们只需要再校验下exp是否过期以及aud字段是否为指定的用户即可。
更多Projected volume可以参考官方文档Kubernetes Volumes[8]。
了解了ServiceAccount的如上认证原理,也就清楚了Vault是如何利用Kubernetes做认证的原理。
Vault开启Kubernetes认证也非常简单:
/ $ vault auth enable kubernetes
Success! Enabled kubernetes auth method at: kubernetes/
/ $ vault write auth/kubernetes/config \
> token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
> kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443" \
> kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
Success! Data written to: auth/kubernetes/config
当然也可以通过Vault Web页面操作:

我们给应用授权,进行严格权限管控,创建一个只读的Vault policy权限:
vault policy write internal-app - <<EOF
path "internal/data/database/config" {
capabilities = ["read"]
}
EOF
关联Kubernetes的ServiceAccount internal-app
,即只允许internal-app
这个ServiceAccount读取如上Vault数据:
$ vault write auth/kubernetes/role/internal-app \
bound_service_account_names=internal-app \
bound_service_account_namespaces=default \
policies=internal-app \
ttl=24h
Success! Data written to: auth/kubernetes/role/internal-app
这种方式不再需要写入静态的Vault用户名密码以及Token,并与Kubernetes认证关联起来,从而解决了Vault认证问题。
解决了认证问题,接下来解决如何读取Vault数据问题,Vault支持如下三种方式:
(1)通过InitContainer Agent注入; (2)通过Injector自动注入; (3)通过Secrets Store CSI挂载。
4.2 通过InitContainer读取数据
这种方式原理比较简单,就是前面介绍的通过一个InitContainer读取ServiceAccount Token向Vault认证,认证后读取数据写入到共享的Volume中。
为了演示我们创建一个Demo应用,我们前面已经创建了Policy并关联internal-app ServiceAccount,这个Policy允许读取internal/data/database/config
数据。因此我们这个Demo Deployment只需要关联internal-app这个ServiceAccount就具有读权限。
这个应用通过initContainer获取用户名和密码并写入到一个emptyDir的Volume中:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: demo-app
name: demo-app
spec:
replicas: 1
selector:
matchLabels:
app: demo-app
template:
metadata:
labels:
app: demo-app
spec:
serviceAccountName: internal-app
initContainers:
- image: curlimages/curl
imagePullPolicy: IfNotPresent
name: get-token
volumeMounts:
- name: secret
mountPath: /secrets
command:
- /bin/sh
- -c
- |
echo "Install jq ..."
apk add jq -X https://mirrors.cloud.tencent.com/alpine/latest-stable/main
TOKEN=$(curl -sSL -X POST \
-d "{\"jwt\": \"$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)\", \"role\": \"internal-app\"}" \
$VAULT_ADDR/v1/auth/kubernetes/login \
| jq -r '.auth.client_token')
echo "Authenticate to vault with kubernetes serviceaccount ..."
curl -H "X-Vault-Token: $TOKEN" $VAULT_ADDR/v1/internal/data/database/config >/tmp/data
echo "Extract data from secret ..."
jq -r '.data.data.username' </tmp/data >/secrets/username
jq -r '.data.data.password' </tmp/data >/secrets/password
echo "Done"
env:
- name: VAULT_ADDR
value: http://vault:8200
securityContext:
runAsUser: 0 # 需要root权限安装jq工具,否则无权限。
containers:
- image: busybox
imagePullPolicy: IfNotPresent
name: demo-app
volumeMounts:
- name: secret
mountPath: /secrets
command:
- /bin/sh
- -c
- 'while true; do echo "The username is $(cat /secrets/username) and the password is $(cat /secrets/password)" && sleep 5;done'
volumes:
- name: secret
emptyDir: {}
状态为Running后查看应用日志:
# kubectl logs demo-app-fb65f6659-r4zxc
The username is int32bit and the password is db-secret-password
如上输出表明Demo应用成功地获取到了用户名和密码。
如上我们是通过initContainer手动写的脚本调用Vault API获取数据,只是为了验证其原理,实际这么操作有点麻烦。其实官方已经提供了现成的Agent InitContainer自动认证并获取数据,并且支持基于模板渲染,推荐使用这种方式而不是自己写一个InitContainer。
首先定义Vault配置以及需要渲染的模板:
kind: ConfigMap
metadata:
name: example-vault-agent-config
namespace: vault
apiVersion: v1
data:
vault-agent-config.hcl: |
exit_after_auth = true
pid_file = "/home/vault/pidfile"
auto_auth {
method "kubernetes" {
mount_path = "auth/kubernetes"
config = {
role = "internal-app"
}
}
sink "file" {
config = {
path = "/home/vault/.vault-token"
}
}
}
template {
destination = "/etc/secrets/index.html"
contents = <<EOT
<html>
<body>
<p>Some secrets:</p>
{{- with secret "internal/data/database/config" }}
<ul>
<li><pre>username: {{ .Data.data.username }}</pre></li>
<li><pre>password: {{ .Data.data.password }}</pre></li>
</ul>
{{ end }}
</body>
</html>
EOT
}
如上首先配置认证的路径以及角色,然后配置了渲染HTML模板,经典的Jinja2。
部署Demo应用如下:
apiVersion: v1
kind: Pod
metadata:
name: vault-agent-example
namespace: vault
spec:
serviceAccountName: internal-app
volumes:
- configMap:
items:
- key: vault-agent-config.hcl
path: vault-agent-config.hcl
name: example-vault-agent-config
name: config
- emptyDir: {}
name: shared-data
initContainers:
- args:
- agent
- -config=/etc/vault/vault-agent-config.hcl
- -log-level=debug
env:
- name: VAULT_ADDR
value: http://vault:8200
image: vault
imagePullPolicy: IfNotPresent
name: vault-agent
volumeMounts:
- mountPath: /etc/vault
name: config
- mountPath: /etc/secrets
name: shared-data
containers:
- image: nginx
imagePullPolicy: IfNotPresent
name: nginx-container
ports:
- containerPort: 80
volumeMounts:
- mountPath: /usr/share/nginx/html
name: shared-data
原理和前面的方式一样,都是通过InitContainer获取数据写入到Volume中,只是这个InitContainer替换使用为官方的镜像。
此时我们访问Demo nginx应用:
# curl $(kubectl get pod vault-agent-example -o jsonpath="{.status.podIP}")
<html>
<body>
<p>Some secrets:</p>
<ul>
<li><pre>username: int32bit</pre></li>
<li><pre>password: db-secret-password</pre></li>
</ul>
</body>
</html>
可见应用成功获取到用户名和密码信息。
4.3 通过injector自动注入
前面介绍了通过InitContainer获取数据,并自动从Vault获取数据渲染模板。
Vault还支持通过injector自动注入Vault值,只需要通过注解配置路径以及模板即可:
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-injector-example
labels:
app: demo-injector-example
spec:
selector:
matchLabels:
app: demo-injector-example
replicas: 1
template:
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "internal-app"
vault.hashicorp.com/agent-inject-secret-database-config.txt: "internal/data/database/config"
vault.hashicorp.com/agent-inject-template-database-config.txt: |
{{- with secret "internal/data/database/config" -}}
username = {{ .Data.data.username}} and password = {{ .Data.data.password}}
{{- end -}}
labels:
app: demo-injector-example
spec:
serviceAccountName: internal-app
containers:
- name: demo-app
image: busybox
imagePullPolicy: IfNotPresent
command:
- /bin/sh
- -c
- 'while true; do cat /vault/secrets/database-config.txt && echo && sleep 5;done'
如上通过注解指定了渲染的模板文件database-config.txt
以及相关配置,并没有手动指定InitContainer以及共享的Volume,但其实原理是一样的,vault-agent-injector
会自动给Pod添加上InitContainer以及共享volume。
通过如下命令可以查看注入的InitContainer:
# kubectl get pod demo-injector-example-65767b48f9-tdzct -o jsonpath='{.spec.initContainers[*].name}'
vault-agent-init
查看注入的Volume如下:
# kubectl get pod demo-injector-example-65767b48f9-tdzct -o jsonpath='{.spec.volumes[*].name}' | tr ' ' '\n'
internal-app-token-q2dxj
home-init
home-sidecar
其中/vault/secrets
就是共享的volume用于存储Vault值。
我们知道InitContainer运行完后就销毁了,这样当Vault数据更新时就无法更新Pod的数据了。
为了解决这个问题,Injector不仅注入了InitContainer还注入了一个SideCar容器,用于监听Vault数据变化并更新Vault值。
# kubectl logs demo-injector-example-65767b48f9-tdzct -f -c demo-app
username = int32bit and password = db-secret-password
username = int32bit and password = db-secret-password
username = int32bit and password = new-password
如上当我们更新password时,demo输出了新的值。
4.4 通过Secrets Store CSI挂载
除了前面介绍的InitContainer以及Injector注入的方法,我们还可以使用Kubernetes社区的Secrets Store CSI驱动,这种方式与使用原生Secret方式比较类似,关于Secrets Store CSI Driver可以参考The Secrets Store CSI Driver[9]。
使用Secret Store CSI方式需要安装CSI驱动:
helm install csi secrets-store-csi-driver/secrets-store-csi-driver
Secret Store CSI支持各种不同的Provider,如AWS、GCP、Azure以及我们要用的Vault,通过SecretProviderClass指定Provider并配置。
apiVersion: secrets-store.csi.x-k8s.io/v1alpha1
kind: SecretProviderClass
metadata:
name: vault-database
spec:
provider: vault
secretObjects:
- data:
- key: password
objectName: "db-password"
secretName: mirror-secret
type: Opaque
parameters:
vaultAddress: "http://vault.vault:8200"
roleName: "internal-app"
objects: |
- objectName: "db-password"
secretPath: "internal/data/database/config"
secretKey: "password"
配置完后我们就能像原生Secret一样,通过volume挂载Secret,只是volume声明参数不一样而已,需要指定前面定义的secretProviderClass
。
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: demo
name: demo
spec:
replicas: 1
selector:
matchLabels:
app: demo
template:
metadata:
labels:
app: demo
spec:
serviceAccountName: internal-app
containers:
- image: busybox
imagePullPolicy: IfNotPresent
name: webapp
command:
- /bin/sh
- -c
- 'while true; do echo $(date "+%Y-%m-%d %H:%M:%S"): The secret is $(cat /mnt/secrets-store/db-password); sleep 1;done'
volumeMounts:
- name: secrets-store-inline
mountPath: "/mnt/secrets-store"
readOnly: true
volumes:
- name: secrets-store-inline
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: "vault-database"
volume中定义了使用secrets-store.csi.k8s.io
CSI驱动并指定了vault-database
,应用起来后我们可以验证是否正常获取数据:
# kubectl logs demo-6ffb6d6f-rfgvq
2021-06-15 01:44:34: The secret is new-password
回到前面SecretProviderClass
定义,我们还声明了mirror-secret,当Secret被引用时,会自动创建一个Kubernetes原生的Secret,这个Secret名为mirror-secret
:
# kubectl get secret mirror-secret -o yaml
apiVersion: v1
data:
password: bmV3LXBhc3N3b3Jk
kind: Secret
metadata:
creationTimestamp: "2021-06-15T01:44:33Z"
labels:
secrets-store.csi.k8s.io/managed: "true"
name: mirror-secret
namespace: vault
ownerReferences:
- apiVersion: apps/v1
kind: ReplicaSet
name: demo-6ffb6d6f
uid: 94f14cc7-6a77-4356-9d80-02972b00ff64
resourceVersion: "51551831"
uid: 5e50a1bd-84b1-4839-99a4-763230400e00
type: Opaque
但是需要注意是这个Secret只是为了给原生的Kubernetes工具所管理,因为我们很多工具如Kubernetes Dashboard并不支持SecretProviderClass资源的管理,因此创建对应的原生Secret方便实现Secret统一纳管。
我们从ownerReferences
看出这个Secret并不是SecretProviderClass创建的,而是ReplicaSet
,换句话说,只有当应用部署时引用这个SecretProviderClass才会创建这个原生Secret,当应用删除时,这个Secret将会同时被移除。
与Injector模式一样,使用Secret Store CSI方式挂载Vault,也支持自动更新,通过--rotation-poll-interval参数可以指定轮询参数变化频率。
五、结论
本文首先介绍了Kubernetes Secret加密存储的必要性,然后分别介绍了Bitnami 的SealedSecret方案、开源的Kamus方案以及开源的Vault方案,其中单集群环境下SealedSecret方案是最简单的,而Kamus适合公有云环境下,与公有云KMS服务联动。Vault非常适合在私有环境下使用,提供与Kubernetes Secret集成的方法也比较多,可以从中选择一项适合企业管理的即可。
参考资料
Encrypting Secret Data at Rest: https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/
[2]SealedSecrets: https://github.com/bitnami-labs/sealed-secrets
[3]helm-secrets: https://github.com/jkroepke/helm-secrets
[4]Kamus: https://github.com/Soluto/kamus
[5]Vault官方文档: https://learn.hashicorp.com/collections/vault/kubernetes
[6]Authentication between microservices using Kubernetes identities: https://learnk8s.io/microservices-authentication-kubernetes#:~:text=The%20Kubernetes%20API%20verifies%20Service%20Account%20identities.%20In,valid%20or%20not%20%E2%80%94%20yes%2C%20it%27s%20that%20simple.
[7]Extending applications on Kubernetes with multi-container pods: https://learnk8s.io/sidecar-containers-patterns
[8]Kubernetes Volumes: https://kubernetes.io/docs/concepts/storage/volumes/#projected
[9]The Secrets Store CSI Driver: https://secrets-store-csi-driver.sigs.k8s.io/getting-started/usage.html




