Skip to content

基于 Kubernetes 的云原生 DevOps 第 10 章 配置和机密数据

🏷️ Kubernetes 《基于 Kubernetes 的云原生 DevOps》


If you want to keep a secret, you must also hide it from yourself.

-- George Orwell, 1984


Kubernetes 提供了几种不同的方法来帮助管理配置:

  1. 通过 Pod 规范中的环境变量将值传递给应用程序;
  2. 使用 ConfigMapSecret 对象将配置数据直接存储在 Kubernetes 中;

10.1 ConfigMap

ConfigMap 是 Kubernetes 中存储配置数据的主要对象。你可以视其为存储配置数据的一组命名键值对

创建 ConfigMap

假设你需要在 Pod 的文件系统中创建一个名为 config.yaml 的 YAML 配置文件:

yaml
autoSaveInterval: 60
batchSize: 18
protocols:
  - http
  - https

可以直接将文件中的内容原封不动的嵌入到 ConfigMap 配置中,书中示例的格式如下:

yaml
apiVersion: v1
data:
  config.yaml: |
    autoSaveInterval: 60
    batchSize: 18
    protocols:
      - http
      - https
kind: ConfigMap
metadata:
  name: demo-config
  namespace: demo

注意: YAML 中的 | 表示后面是原始数据块。

JiaJia:

现在项目使用的是阿里云,其 ConfigMap 格式如下:

yaml
apiVersion: v1
data:
  autoSaveInterval: '60'
  batchSize: '18'
kind: ConfigMap
metadata:
  name: demo-config
  namespace: demo

纯数字的话需要用单引号括起来,数组方式貌似不支持。

还有一种更简单的方法,可以通过 kubectl 利用 YAML 文件创建 ConfigMap

powershell
PS C:\k8s> kubectl create configmap demo-config --namespace=demo --from-file=config.yaml
configmap/demo-config created

创建的配置文件内容如下:

yaml
kind: ConfigMap
apiVersion: v1
metadata:
  name: demo-config
  namespace: demo
  uid: 7f34afba-b490-47d3-8828-e4d635391358
  resourceVersion: '899820'
  creationTimestamp: '2021-12-30T07:41:03Z'
  managedFields:
    - manager: kubectl-create
      operation: Update
      apiVersion: v1
      time: '2021-12-30T07:41:03Z'
      fieldsType: FieldsV1
      fieldsV1:
        f:data:
          .: {}
          f:config.yaml: {}
data:
  config.yaml: "autoSaveInterval: 60\r\nbatchSize: 18\r\nprotocols:\r\n  - http\r\n  - https"

应该是和上面书中的示例是等价的。

导出 ConfigMap 对应的清单文件:

powershell
kubectl get configmap/demo-config --namespace=demo -o yaml > demo-config.yaml

导出文件 demo-config.yaml 内容如下:

yaml
apiVersion: v1
data:
  config.yaml: "autoSaveInterval: 60\r\nbatchSize: 18\r\nprotocols:\r\n  - http\r\n
    \ - https"
kind: ConfigMap
metadata:
  creationTimestamp: "2021-12-30T07:41:03Z"
  name: demo-config
  namespace: demo
  resourceVersion: "899820"
  uid: 7f34afba-b490-47d3-8828-e4d635391358

利用 ConfigMap 设置环境变量

main.go:

go
package main

import (
  "fmt"
  "log"
  "net/http"
  "os"
)

func handler(w http.ResponseWriter, r *http.Request) {
  greeting := os.Getenv("GREETING")
  fmt.Fprintf(w, "%s, 世界\n", greeting)
}

func main() {
  http.HandleFunc("/", handler)
  log.Fatal(http.ListenAndServe(":8888", nil))
}

configmap.yaml:

这里的格式和我这边现行项目的格式一致,直接在 data 中配置的键值对。

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: demo-config
data:
  greeting: Hola

deployment.yaml:

在部署的规格中,通过 env 指定环境变量,这里的区别是用 valueFrom 替代了 value,然后通过 configMapKeyRef 中的配置标识环境变量的值来自哪个 ConfigMap 中的哪个 key

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
    spec:
      containers:
        - name:  demo
          image: cloudnatived/demo:hello-config-env
          ports:
            - containerPort: 8888
          env:
            - name: GREETING
              valueFrom:
                configMapKeyRef:
                  name: demo-config
                  key: greeting
powershell
kubectl apply -f .\hello-config-env\k8s\
configmap/demo-config created
deployment.apps/demo created
powershell
kubectl port-forward deploy/demo 9999:8888
http
GET http://localhost:9999/

---

HTTP/1.1 200 OK
Date: Thu, 30 Dec 2021 08:28:29 GMT
Content-Length: 13
Content-Type: text/plain; charset=utf-8
Connection: close

Hola, 世界

利用 ConfigMap 设置整个环境

使用 envFrom 获取 ConfigMap 中所有的键,并将其转换为环境变量。
环境变量的 key 就是 ConfigMap 中指定的 key

yaml
spec:
  containers:
    - name:  demo
      image: cloudnatived/demo:hello-config-env
      ports:
        - containerPort: 8888
      envFrom:
      - configMapRef:
          name: demo-config

使用 envFrom 的同时仍然可以使用 env 指定环境变量,并且同名时 env 中的优先级更高。

在命令参数中指定环境变量

可以使用 Kubernetes 的特殊语法 $(VARIABLE) 在命令行(容器入口)中引用环境变量。

args 中指定的参数会传送到容器的默认入口点。

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
    spec:
      containers:
        - name:  demo
          image: cloudnatived/demo:hello-config-args
          args:
            - "-greeting"
            - "$(GREETING)"
          ports:
            - containerPort: 8888
          env:
            - name: GREETING
              valueFrom:
                configMapKeyRef:
                  name: demo-config
                  key: greeting

利用 ConfigMap 创建配置文件

首先需要在 ConfigMap 中存储完整的配置文件(而不是一个个键),之后使用 ConfigMap 创建一个卷,最后将这个卷挂载到容器。

configmap.yaml

注意: YAML 中的 | 表示后面是原始数据块。

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: demo-config
data:
  config: |
    greeting: Buongiorno

这里其内容为:

yaml
greeting: Buongiorno

部署模板的内容如下:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
    spec:
      containers:
        - name:  demo
          image: cloudnatived/demo:hello-config-file
          ports:
            - containerPort: 8888
          volumeMounts:
          - mountPath: /config/
            name: demo-config-volume
            readOnly: true
      volumes:
      - name: demo-config-volume
        configMap:
          name: demo-config
          items:
          - key: config
            path: demo.yaml
  • volumes 中利用配置文件创建了一个名为 demo-config-volume 的卷,文件名为 demo.yaml
  • containers.volumeMounts 中将这个卷挂载到容器的 /config/ 路径。
  • 最后将在容器中创建一个 /config/demo.yaml 文件。

查看集群中的 ConfigMap 数据:

yaml
PS C:\k8s> kubectl describe configmap/demo-config
Name:         demo-config
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
config:
----
greeting: Buongiorno


BinaryData
====

Events:  <none>

如果更新 ConfigMap 并修改它的值,则相应的文件也会自动更新。
有些应用程序会自动监测配置文件的更新,有些则不会。
可通过重新部署应用程序来读取修改。

配置发生变化后更新 Pod

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
  name: demo

JiaJia:

书中说放在注释中,可以通过 helm 发布来触发自动重新部署,通过测试发现,更新后 checksum/config 注释的值确实变了,但是并没有触发重新部署。

powershell
helm install demo-config .\hello-config-file\
helm upgrade demo-config .\hello-config-file\
powershell
kubectl port-forward deploy/demo 9999:8888

ConfigMap 更新前后的 deployment 的注解如下:

yaml
metadata:
  annotations:
    checksum/config: 16c967a4e6e489350734c14176d0db0db7fca24b38dcc3c5277d2a0d184cdb5c
yaml
metadata:
  annotations:
    checksum/config: fc80ec715dcecbce099238327a24c33c46cdf46e3f3dbc17b7aea7b14ca03caf

但 Pod 并没有更新。

将 ConfigMap 文件的 sha256 值移到环境变量中后,可以正常的触发 Pod 的更新。

yaml
spec:
  ...
  template:
    ...
    spec:
      containers:
        - name:  demo
          ...
          env:
            - name: checksum.config
              value: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}

10.2 Kubernetes Secret

Kubernetes 提供了一种专门存储机密数据的特殊对象: Secret 。其清单示例如下:

yaml
apiVersion: v1
kind: Secret
metadata:
  name: demo-secret
stringData:
  magicWord: xyzzy

利用机密数据设置环境变量

部署文件:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
    spec:
      containers:
        - name: demo
          image: cloudnatived/demo:hello-secret-env
          ports:
            - containerPort: 8888
          env:
            - name: MAGIC_WORD
              valueFrom:
                secretKeyRef:
                  name: demo-secret
                  key: magicWord

配置 env.valueFrom.secretKeyRef 来从 Secret 中读取数据到环境变量(和 configMapKeyRef 类似)。
这里环境变量为 MAGIC_WORD ,其值来自 Secret 文件( demo-secret )中的 magicWord

镜像 cloudnatived/demo:hello-secret-env 中的代码如下(Go 语言):

go
package main

import (
  "fmt"
  "log"
  "net/http"
  "os"
)

func handler(w http.ResponseWriter, r *http.Request) {
  magicWord := os.Getenv("MAGIC_WORD")
  fmt.Fprintf(w, "The magic word is %q", magicWord)
}

func main() {
  http.HandleFunc("/", handler)
  log.Fatal(http.ListenAndServe(":8888", nil))
}

MAGIC_WORD 环境变量的值读取到 magicWord 变量(magicWord := os.Getenv("MAGIC_WORD"))并输出到响应。

应用部署文件:

powershell
PS C:\projects\github\cloudnativedevops\demo> kubectl apply -f .\hello-secret-env\k8s\
deployment.apps/demo created
secret/demo-secret created

将本地端口 9999 转发到 demo 部署的 8888 端口。

powershell
PS C:\projects\github\cloudnativedevops\demo> kubectl port-forward deploy/demo 9999:8888
Forwarding from 127.0.0.1:9999 -> 8888
Forwarding from [::1]:9999 -> 8888
Handling connection for 9999

访问 http://localhost:9999/ 地址:

http
GET http://localhost:9999/

---

HTTP/1.1 200 OK
Date: Mon, 16 May 2022 08:59:50 GMT
Content-Length: 25
Content-Type: text/plain; charset=utf-8
Connection: close

The magic word is "xyzzy"

之后可以通过如下方式清除资源:

powershell
PS C:\projects\github\cloudnativedevops\demo> kubectl delete -f .\hello-secret-env\k8s\
deployment.apps "demo" deleted
secret "demo-secret" deleted

将 Secret 写入文件

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
    spec:
      containers:
        - name: demo
          image: cloudnatived/demo:hello-secret-file
          ports:
            - containerPort: 8888
          volumeMounts:
            - name: demo-secret-volume
              mountPath: "/secrets/"
              readOnly: true
      volumes:
        - name: demo-secret-volume
          secret:
            secretName: demo-secret

镜像 cloudnatived/demo:hello-secret-file 对应的代码(Go 语言):

go
package main

import (
  "fmt"
  "io/ioutil"
  "log"
  "net/http"
)

const secretPath = "/secrets/magicWord"

var magicWord []byte

func handler(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "The magic word is %q", magicWord)
}

func main() {
  var err error
  magicWord, err = ioutil.ReadFile(secretPath)
  if err != nil {
    log.Fatal(err)
  }
  http.HandleFunc("/", handler)
  log.Fatal(http.ListenAndServe(":8888", nil))
}

可以看到是从 /secrets/magicWord 文件中读取文件内容到变量 magicWordmagicWord, err = ioutil.ReadFile(secretPath))并将其打印到响应。而 magicWord 正是上面 Secret 文件中键值对的键的名称,读取到的内容是键对应的值。

实际操作及控制台的输出内容如下:

应用部署:

powershell
PS C:\projects\github\cloudnativedevops\demo> kubectl apply -f .\hello-secret-file\k8s\
Warning: resource deployments/demo is missing the kubectl.kubernetes.io/last-applied-configuration annotation which is required by kubectl apply. kubectl apply should only be used on resources created declaratively by either kubectl create --save-config or kubectl apply. The missing annotation will be patched automatically.
deployment.apps/demo configured
secret/demo-secret created

将本地端口 9999 转发到 demo 部署的 8888 端口。

powershell
PS C:\projects\github\cloudnativedevops\demo> kubectl port-forward deploy/demo 9999:8888
Forwarding from 127.0.0.1:9999 -> 8888
Forwarding from [::1]:9999 -> 8888

访问 http://localhost:9999/ 地址:

http
GET http://localhost:9999/

---

HTTP/1.1 200 OK
Date: Mon, 16 May 2022 08:00:43 GMT
Content-Length: 25
Content-Type: text/plain; charset=utf-8
Connection: close

The magic word is "xyzzy"

之后可以通过如下方式清除资源:

powershell
PS C:\projects\github\cloudnativedevops\demo> kubectl delete -f .\hello-secret-file\k8s\
deployment.apps "demo" deleted
secret "demo-secret" deleted

读取 Secret

通过 kubectl describe 命令查看 Secret 内容:

powershell
PS C:\k8s> kubectl describe secret/demo-secret
Name:         demo-secret
Namespace:    default
Labels:       <none>
Annotations:  <none>

Type:  Opaque

Data
====
magicWord:  5 bytes

这次没有显示实际数据。Kubernetes Secret 的类型是 Opaque ,这意味着它们不会在 kubectl describe 的输出、日志消息或终端中显示。这样可以防止意外泄漏机密数据。

使用 kubectl get 查看混淆后的机密数据:

powershell
PS C:\k8s> kubectl get secret/demo-secret -o yaml
apiVersion: v1
data:
  magicWord: eHl6enk=
kind: Secret
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"v1","kind":"Secret","metadata":{"annotations":{},"name":"demo-secret","namespace":"default"},"stringData":{"magicWord":"xyzzy"}}
  creationTimestamp: "2022-05-16T09:09:11Z"
  name: demo-secret
  namespace: default
  resourceVersion: "1750142"
  uid: dd8ad1f8-6a76-4da0-83a5-50762a97cdca
type: Opaque

由于机密数据可以是不可打印的二进制数据(比如 TLS 加密密钥),因此 Kubernetes 的 Secret 始终以 base64 格式存储。

解码 base64 数据:

bash
echo "eHl6enk=" | base64 --decode

JiaJia:

PowerShell 下调用如下函数解码:

powershell
PS C:\k8s> [System.Text.Encoding]::UTF8.GetString([System.Text.Encoding]::UTF8.GetBytes("eHl6enk="))
eHl6enk=

也可以将其包装成如下函数,然后再调用:

powershell
function ConvertFrom-Base64($string) {
   $bytes  = [System.Convert]::FromBase64String($string);
   $decoded = [System.Text.Encoding]::UTF8.GetString($bytes);
   return $decoded;
}

运行效果:

powershell
PS C:\k8s> function ConvertFrom-Base64($string) {
>>    $bytes  = [System.Convert]::FromBase64String($string);
>>    $decoded = [System.Text.Encoding]::UTF8.GetString($bytes);
>>    return $decoded;
>> }
PS C:\k8s> ConvertFrom-Base64("eHl6enk=")
xyzzy

也可以使用 base64 命令对文本进行编码:

bash
echo -n xyzzy | base64

JiaJia:

PowerShell 可以使用如下函数将文本转化为 base64 格式:

powershell
function ConvertTo-Base64($string) {
   $bytes  = [System.Text.Encoding]::UTF8.GetBytes($string);
   $encoded = [System.Convert]::ToBase64String($bytes);
   return $encoded;
}

运行效果:

powershell
PS C:\k8s> function ConvertTo-Base64($string) {
>>    $bytes  = [System.Text.Encoding]::UTF8.GetBytes($string);
>>    $encoded = [System.Convert]::ToBase64String($bytes);
>>    return $encoded;
>> }
PS C:\k8s> ConvertTo-Base64("xyzzy")
eHl6enk=

访问 Secret

随可以访问或编辑 Secret 是由 Kubernetes 访问控制机制 RBAC 控制的,将在 11.1 节中详细讨论 RBAC。
如果你使用的集群不支持 RBAC 或 未启用 RBAC,则所有用户或任何容器都可以访问所有 Secret(千万不要在没有 RBAC 的情况下在生产环境中运行任何集群)。

静态加密

Kubernetes 从 1.7 版开始支持静态加密。这意味着 etcd 数据库中的机密数据实际上是经过加密后存储在磁盘上的,即使可以直接访问数据库的人也无法读取。这样 Kubernetes API 服务器拥有加密此数据的密钥。正确配置的集群应当启用静态加密。

bash
kubectl describe pod -n kube-system -l component=kube-apiserver | grep encryption

JiaJia:

PowerShell 中将 grep 改为 findstr

powershell
PS C:\k8s> kubectl describe pod -n kube-system -l component=kube-apiserver | findstr encryption

如果没有看到 experimental-encryption-provider-config 标志,则表明密钥启用静态加密。
如果你使用的是 GKE 或其它托管的 Kubernetes 服务,则可以放心,你的数据已经通过其它机制进行了加密,因此看不到这个标志。

防止 Secret 被删

可以使用 Helm 专用的注解,防止资源被删除。

yaml
apiVersion: v1
kind: Secret
metadata:
  annotations:
    "helm.sh/resource-policy": keep
  name: demo-secret
stringData:
  magicWord: xyzzy

10.3 Secret 管理策略

上一节的清单文件中的数据是明文保存的。永远不要在源代码管理的文件中公开这样的机密数据。

在选择工具或策略来管理应用程序中的机密信息时,你需要考虑以下几个该问题:

  1. 将机密存储在何处才能保证高可用性?
  2. 运行中的应用程序应当如何使用机密?
  3. 在轮换或改变机密时,运行中的应用程序需要做些什么?

在版本控制中加密机密

管理机密的第一种方式是将机密数据直接存储到版本控制代码库的代码中,切记以加密形式存储,并在部署时进行解密。

你应该严格限制加密密钥的访问,只允许某些特定的人访问,而且绝不能公开给开发人员。

缺点是同样的机密可能会存在于多个项目中,修改的工作量会比较大。

对于只有非关键数据的小型组织而言,在源代码库中保存加密的机密数据是一个不错的起点。

远程存储 Secret

另一种管理机密的方法是将它们保存到一个(或多个)文件中,然后把文件保存在异地的安全文件存储库中,例如 AWS S3 存储桶或 Google 云存储。在部署应用时,下载这些文件,经过解密后提供给应用程序。

跟第一种方式比较类似,但解决了同一个机密在多个代码库中重复存储的问题。

使用专业的机密管理工具

当规模非常大时,可能需要考虑使用专业的机密管理工具,例如 Hashicorp VaultSquare KeywhizAWS Secrets ManagerAzure Key Vault

这些工具可以将所有应用程序的机密数据集中存储在安全的地方,不仅可以提供高可用性,而且还可以控制哪些用户和服务账号有权添加、删除、更改或查看机密数据。

上述工具中最受欢迎的是 Hashicorp Vault

推荐

建议使用 Sops 之类的轻量级加密工具,直接在源代码中对机密数据进行加密。因为一般你没有那么多机密需要管理。除非你的基础设施非常复杂且相互依赖。

10.4 使用 Sops 加密机密数据

Mozilla 项目开发的 Sopssecrets operations 的缩写 )是一种加密/解密工具,能够处理 YAML、JSON 和二进制文件,而且还支持多个加密后端,包括 PGP/GnuPG、Azure Key Vault、AWS 的密钥管理服务(KMS)以及 Google 的云密钥管理服务。

Sops 简介

Sops 不会加密整个文件,它只加密各个机密数据的值。

有关 Sops 的安装请参照项目主页:https://github.com/mozilla/sops

使用 Sops 加密文件

Sops 本身并不会处理加密。它将加密的工作委托给后端,比如 GnuPGPretty Good Privacy,即 PGP 协议的一种流行的开源实现)。

PGP 加密与 SSH 和 TLS 一样,是一个公钥加密系统。

首先,使用 gpg --gen-key 生成一对密钥。中间需要输入 real nameemail address 和密码(密码后面解密时会使用到)。

bash
gpg --gen-key

JiaJia:

powershell
PS C:\k8s> gpg --gen-key
gpg (GnuPG) 2.3.6; Copyright (C) 2021 g10 Code GmbH
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Note: Use "gpg --full-generate-key" for a full featured key generation dialog.

GnuPG needs to construct a user ID to identify your key.

Real name: JiaJia
Email address: jiajia@gmail.com
You selected this USER-ID:
    "JiaJia <jiajia@gmail.com>"

Change (N)ame, (E)mail, or (O)kay/(Q)uit? O
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
gpg: directory 'C:\\Users\\ljj\\AppData\\Roaming\\gnupg\\openpgp-revocs.d' created
gpg: revocation certificate stored as 'C:\\Users\\ljj\\AppData\\Roaming\\gnupg\\openpgp-revocs.d\\IHT6OR9022D5WUE5SC8D4498GNP7L7MOJ7PH3Z0M.rev'
public and secret key created and signed.

pub   ed25519 2022-05-17 [SC] [expires: 2024-05-16]
      IHT6OR9022D5WUE5SC8D4498GNP7L7MOJ7PH3Z0M
uid                      JiaJia <jiajia@gmail.com>
sub   cv25519 2022-05-17 [E] [expires: 2024-05-16]

其中 IHT6OR9022D5WUE5SC8D4498GNP7L7MOJ7PH3Z0M 就是密钥指纹,后面加密时会使用到。

同时还会生成一个对应的撤销证书 IHT6OR9022D5WUE5SC8D4498GNP7L7MOJ7PH3Z0M.rev 。其内容如下:

txt
This is a revocation certificate for the OpenPGP key:

pub   ed25519 2022-05-17 [SC] [expires: 2024-05-16]
      IHT6OR9022D5WUE5SC8D4498GNP7L7MOJ7PH3Z0M
uid          JiaJia <jiajia@gmail.com>

A revocation certificate is a kind of "kill switch" to publicly
declare that a key shall not anymore be used.  It is not possible
to retract such a revocation certificate once it has been published.

Use it to revoke this key in case of a compromise or loss of
the secret key.  However, if the secret key is still accessible,
it is better to generate a new revocation certificate and give
a reason for the revocation.  For details see the description of
of the gpg command "--generate-revocation" in the GnuPG manual.

To avoid an accidental use of this file, a colon has been inserted
before the 5 dashes below.  Remove this colon with a text editor
before importing and publishing this revocation certificate.

:-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: This is a revocation certificate

iHgEIBYKACAWiQTvMEp0xEU1an6MQIpAGnJA7Je1aAUCYoM9bQIdBBBKCRBAGnJA
7Je1aLV+AQC8v236150pD51Ln0be0WLaGDzOdHgaylK1KdX+SD3PDQEAmwb/BUOm
fcCMAq2MgNyjTa/qf3epBbmft3gxhgoEcQo=
=4LG3
-----END PGP PUBLIC KEY BLOCK-----

安装 Sops

可以从项目仓库 https://github.com/mozilla/sops 下载,也可以通过 Go 来安装:

shell
go get -u go.mozilla.org/sops/cmd/sops
sops -v

JiaJia:

我这里使用 Go 安装没成功,直接从 项目仓库 下载的 sops-v3.7.3.exe 文件,将其改名为 sops.exe ,然后将目录加入 Path 环境变量(需要重启命令行控制台)。

下载不下来的可以使用我共享的文件:【金山文档】sops-v3.7.3

powershell
PS C:\k8s> sops -v
sops 3.7.3
[warning] failed to retrieve latest version from upstream: Get "https://raw.githubusercontent.com/mozilla/sops/master/version/version.go": dial tcp: lookup raw.githubusercontent.com: getaddrinfow: The requested name is valid, but no data of the requested type was found.

[warning] failed to compare current version with latest: Version string empty
 (latest)

对机密文件进行加密

加密前( secret.yaml ):

yaml
apiVersion: v1
kind: Secret
metadata:
  name: demo-secret
stringData:
  magicWord: xyzzy

对清单文件进行加密:

powershell
sops --encrypt --in-place --pgp IHT6OR9022D5WUE5SC8D4498GNP7L7MOJ7PH3Z0M .\secret.yaml

JiaJia:

powershell
PS C:\k8s> sops --encrypt --in-place --pgp IHT6OR9022D5WUE5SC8D4498GNP7L7MOJ7PH3Z0M .\secret.yaml
[PGP]    time="2022-05-17T14:30:48+08:00" level=warning msg="Deprecation Warning: GPG key fetching from a keyserver within sops will be removed in a future version of sops. See https://github.com/mozilla/sops/issues/727 for more information."

执行时有个弃用警告,貌似以后将移除从密钥服务器获取密钥的功能,不知道支不支持在加密时指定密钥文件。
但是,暂时好像也没有提供直接从文件读取密钥的功能,而且在之前通过 gpg --gen-key 生成密钥时也没看到哪个是公钥/私钥。
Windows 版安装时会附带安装一个 Kleopatra 软件,这里会列出已经创建的密钥,也可以通过这个软件里的菜单创建密钥、导出密钥、吊销认证、更改到期日期。
导出的文件为 .asc 后缀,可以通过 gpg --import filename.asc 命令来导入密钥。

加密后文件:

yaml
apiVersion: ENC[AES256_GCM,data:kw0=,iv:ODCJHP7UfhcOIylfenNiI2PGBbUQPDfKrfmvJF3ZJNQ=,tag:2OfdVDlizaPFuyBohqw8gA==,type:str]
kind: ENC[AES256_GCM,data:h+JSJpi2,iv:gbzeMdyBhxOeHfq2k90k1QAXKbM0Y8IBGokF7AUX668=,tag:cX2xWAiV9yqImA+mvEICVQ==,type:str]
metadata:
    name: ENC[AES256_GCM,data:tX8Tvtf74ZL4EE4=,iv:OltIC2JO48M8OxUweNkhsyfpFVO3SwyndU2B9LrdQy4=,tag:01WfuucFxJQPrtNG6WYYfQ==,type:str]
stringData:
    magicWord: ENC[AES256_GCM,data:cCupDhg=,iv:rUDkh51WFa+Jqu5i7QJbPgsVdC5wuqr83Krmm6P6hzU=,tag:MbTIKdIyn9UdwT5yCdlxfg==,type:str]
sops:
    kms: []
    gcp_kms: []
    azure_kv: []
    hc_vault: []
    age: []
    lastmodified: "2022-05-17T06:30:50Z"
    mac: ENC[AES256_GCM,data:EgcWojzhy9LIMUlmamIlbDzh2wLyBYzhw7vG8Yuwj6+IpfHDPhH5XeREGdSP2xDuQDYggZ0jo+SZsgGmOAD6iW2fR/9hKP/XRWqbkDvRhTQEV162iQSwGG1F4fq4OErScZsEetFTGSi3z6Ly2ek2vMjSJnrHCn8QRPv+gg9eE1s=,iv:askdsJ2DFI2prvEiiPBlGVd3Gfk18ZJbmtVDtidnBbE=,tag:VQ10Vl7MmLFd7e4YalI5dQ==,type:str]
    pgp:
        - created_at: "2022-05-17T06:30:47Z"
          enc: "-----BEGIN PGP MESSAGE-----\r\n\r\nhF4DKbfoNF76ZNwSAQdAkAJ8kPHs9fS+Gw4Llp00Kh7iYuJ4hd4g5KB5g0REFjEw\r\nC0o5syW5J29Gw4YeyUgZUo3Iq5tJk5y9IGAjNA3ADRQN8e8G2wObpfboaiyU/zt7\r\n1GYBCQIQjP8BjGfbdCJf2jvaIiRQ8W1GCWzmDiQcAuz2oc6CYS94bs4APo9z1KqD\r\naWiLGjuSSY5AC+gW/JCfNOGZY9v+C+4MgjF2plBbPKxMG0EB/1qBUfp6QZMZEzV2\r\ng8TsYpm4qR0=\r\n=CsXe\r\n-----END PGP MESSAGE-----\r\n"
          fp: IHT6OR9022D5WUE5SC8D4498GNP7L7MOJ7PH3Z0M
    unencrypted_suffix: _unencrypted
    version: 3.7.3

解密文件:

powershell
sops --decrypt .\secret.yaml

输入命令后会有弹窗要求输入创建密钥时输入的密码。解密成功后只会将解密的结果打印在控制台,不会修改源文件。

JiaJia:

powershell
PS C:\k8s> sops --decrypt .\secret.yaml
apiVersion: v1
kind: Secret
metadata:
    name: demo-secret
stringData:
    magicWord: xyzzy

可以通过添加 --in-place 标志,直接将解密结果写入原文件。

powershell
sops --decrypt --in-place .\secret.yaml

使用 KMS 后端

如果你使用 Amazon KMS 或 Google Cloud KMS 在云中管理密钥,则可以结合 Sops 一起使用。使用 KMS 密钥的方式与 PGP 示例完全相同,只不过文件中的元数据会有所不同。

10.5 小结

  • 分离配置数据与应用程序代码,然后使用 Kubernetes ConfigMap 和 Secret 进行部署。这样就无需在每次更改密码时重新部署应用程序。
  • 为了将数据放入 ConfigMap,你可以直接写入 Kubernetes 清单文件,或使用 kubectl 将现有的 YAML 文件转换为 ConfigMap 规范。
  • 在数据进入 ConfigMap 之后,就可以将其插入到容器的环境中,或添加到入口点命令行的参数中。或者,你也可以将数据写入挂载到容器的文件中。
  • Secret 的工作方式与 ConfigMap 相同,除了数据是静态加密的, kubectl 的输出会显示混淆的数据。
  • 一种简单又灵活的管理机密数据的方法是将机密数据直接存储在源代码库中,但一定要使用 Sops 或其它文本加密工具对其进行加密。
  • 不要过度考虑机密管理,尤其是刚开始的时候。从简单且方便开发人员设置的方式入手。
  • 如果许多应用程序共享机密,则可以将它们(加密后)存储在云存储桶中,并在部署时再获取。
  • 企业级的机密管理需要专业的服务,例如 Vault。但不要优先考虑 Vault,因为你可能并不需要。而且,你可以随时切换到 Vault。
  • Sops 是一种加密工具,可以处理 YAML 和 JSON 等键值文件。它可以从本地 GnuPG 密钥环或云密钥管理服务(比如 Amazon KMS 和 Google Cloud KMS)获取加密密钥。