暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Kubernetes Secret如何安全存储到Git代码仓库

int32bit 2021-06-15
635

一、背景

我们知道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 use k8s auth

我们给应用授权,进行严格权限管控,创建一个只读的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集成的方法也比较多,可以从中选择一项适合企业管理的即可。

参考资料

[1]

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


文章转载自int32bit,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论