社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Git

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

Linux编程 • 2 年前 • 233 次点击  
👇👇关注后回复 “进群” ,拉你进程序员交流群👇👇

作者丨int32bit

来源丨int32bit


一、背景

我们知道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认证的原理

其实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挂载,设置automountServiceAccountTokenfalse:

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 - <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' /secrets/username
           jq -r '.data.data.password' /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 = <    
    
    

Some secrets:


    {{- with secret "internal/data/database/config" }}
    

        
  • username: {{ .Data.data.username }}

  •     
  • password: {{ .Data.data.password }}

  •     

    {{ end }}
    
    
    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: int32bitpre>li>
<li><pre>password: db-secret-passwordpre>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.ioCSI驱动并指定了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

-End-

最近有一些小伙伴,让我帮忙找一些 面试题 资料,于是我翻遍了收藏的 5T 资料后,汇总整理出来,可以说是程序员面试必备!所有资料都整理到网盘了,欢迎下载!

点击👆卡片,关注后回复【面试题】即可获取

在看点这里好文分享给更多人↓↓

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/115802
 
233 次点击