secret


weight: 76 title: Secret配置 date: “2022-05-21T00:00:00+08:00” type: book

Secret 对象类型用来保存敏感信息,例如密码、OAuth令牌和ssh key。将这些信息放在 secret 中比放在 pod 的定义中或者docker镜像中来说更加安全和灵活。

Secret概览

Secret是一种包含少量敏感信息例如密码、tokenkey的对象。这样的信息可能会被放在Pod spec中或者镜像中;将其放在一个secret对象中可以更好地控制它的用途,并降低意外暴露的风险。

用户可以创建secret,同时系统也创建了一些secret

要使用secretpod需要引用secretPod可以用两种方式使用secret:作为 volume 中的文件被挂载到pod中的一个或者多个容器里,或者当kubeletpod拉取镜像时使用。

内置secret

Service Account使用API凭证自动创建和附加secret

Kubernetes自动创建包含访问API凭据的secret,并自动修改您的pod以使用此类型的secret

如果需要,可以禁用或覆盖自动创建和使用API凭据。但是,如果您需要的只是安全地访问apiserver,我们推荐这样的工作流程。

参阅 Service Account 文档获取关于Service Account如何工作的更多信息。

创建您自己的Secret

使用kubectl创建Secret

假设有些pod需要访问数据库。这些pod需要使用的用户名和密码在您本地机器的 ./username.txt./password.txt 文件里。

# Create files needed for rest of example.
$ echo -n "admin" > ./username.txt
$ echo -n "1f2d1e2e67df" > ./password.txt

kubectl create secret 命令将这些文件打包到一个Secret中并在API server中创建了一个对象。

$ kubectl create secret generic db-user-pass --from-file=./username.txt --from-file=./password.txt
secret "db-user-pass" created

您可以这样检查刚创建的secret

$ kubectl get secrets
NAME                  TYPE                                  DATA      AGE
db-user-pass          Opaque                                2         51s

$ kubectl describe secrets/db-user-pass
Name:            db-user-pass
Namespace:       default
Labels:          <none>
Annotations:     <none>

Type:            Opaque

Data
====
password.txt:    12 bytes
username.txt:    5 bytes

请注意,默认情况下,getdescribe 命令都不会显示文件的内容。这是为了防止将secret中的内容被意外暴露给从终端日志记录中刻意寻找它们的人。

请参阅 解码secret 了解如何查看它们的内容。

手动创建Secret

您也可以先以jsonyaml格式在文件中创建一个secret对象,然后创建该对象。

每一项必须是base64编码:

$ echo -n "admin" | base64
YWRtaW4=
$ echo -n "1f2d1e2e67df" | base64
MWYyZDFlMmU2N2Rm

现在可以像这样写一个secret对象:

apiVersion: v1
kind: Secret
metadata:
  name: mysecret
type: Opaque
data:
  username: YWRtaW4=
  password: MWYyZDFlMmU2N2Rm

数据字段是一个映射。它的键必须匹配DNS_SUBDOMAIN,前导点也是可以的。这些值可以是任意数据,使用base64进行编码。

使用 kubectl create创建secret

$ kubectl create -f ./secret.yaml
secret "mysecret" created

编码注意: secret数据的序列化JSONYAML值使用base64编码成字符串。换行符在这些字符串中无效,必须省略。当在Darwin/OS X上使用 base64 实用程序时,用户应避免使用 -b 选项来拆分长行。另外,对于Linux用户如果 -w 选项不可用的话,应该添加选项 -w 0base64 命令或管道 base64 | tr -d '\n'

解码Secret

可以使用 kubectl get secret 命令获取secret。例如,获取在上一节中创建的secret

$ kubectl get secret mysecret -o yaml
apiVersion: v1
data:
  username: YWRtaW4=
  password: MWYyZDFlMmU2N2Rm
kind: Secret
metadata:
  creationTimestamp: 2016-01-22T18:41:56Z
  name: mysecret
  namespace: default
  resourceVersion: "164619"
  selfLink: /api/v1/namespaces/default/secrets/mysecret
  uid: cfee02d6-c137-11e5-8d73-42010af00002
type: Opaque

解码密码字段:

$ echo "MWYyZDFlMmU2N2Rm" | base64 --decode
1f2d1e2e67df

使用Secret

Secret可以作为数据卷被挂载,或作为环境变量暴露出来以供pod中的容器使用。它们也可以被系统的其他部分使用,而不直接暴露在pod内。例如,它们可以保存凭据,系统的其他部分应该用它来代表您与外部系统进行交互。

Pod中使用Secret文件

Pod中的volume里使用Secret

  1. 创建一个secret或者使用已有的secret。多个pod可以引用同一个secret
  2. 修改您的pod的定义在 spec.volumes[] 下增加一个volume。可以给这个volume随意命名,它的 spec.volumes[].secret.secretName 必须等于secret对象的名字。
  3. spec.containers[].volumeMounts[] 加到需要用到该secret的容器中。指定 spec.containers[].volumeMounts[].readOnly = truespec.containers[].volumeMounts[].mountPath 为您想要该secret出现的尚未使用的目录。
  4. 修改您的镜像并且/或者命令行让程序从该目录下寻找文件。Secretdata 映射中的每一个键都成为了 mountPath 下的一个文件名。

这是一个在pod中使用volume挂在secret的例子:

apiVersion: v1
kind: Pod
metadata:
  name: mypod
spec:
  containers:
    - name: mypod
      image: redis
      volumeMounts:
        - name: foo
          mountPath: "/etc/foo"
          readOnly: true
  volumes:
    - name: foo
      secret:
        secretName: mysecret

您想要用的每个secret都需要在 spec.volumes 中指明。

如果pod中有多个容器,每个容器都需要自己的 volumeMounts 配置块,但是每个secret只需要一个 spec.volumes

您可以打包多个文件到一个secret中,或者使用的多个secret,怎样方便就怎样来。

向特性路径映射secret密钥

我们还可以控制Secret key映射在volume中的路径。您可以使用 spec.volumes[].secret.items 字段修改每个key的目标路径:

apiVersion: v1
kind: Pod
metadata:
  name: mypod
spec:
  containers:
    - name: mypod
      image: redis
      volumeMounts:
        - name: foo
          mountPath: "/etc/foo"
          readOnly: true
  volumes:
    - name: foo
      secret:
        secretName: mysecret
        items:
          - key: username
            path: my-group/my-username

将会发生什么呢:

  • username secret存储在 /etc/foo/my-group/my-username 文件中而不是 /etc/foo/username 中。
  • password secret没有被影射

如果使用了 spec.volumes[].secret.items,只有在 items 中指定的key被影射。要使用secret中所有的key,所有这些都必须列在 items 字段中。所有列出的密钥必须存在于相应的secret中。否则,不会创建卷。

Secret文件权限

您还可以指定secret将拥有的权限模式位文件。如果不指定,默认使用 0644。您可以为整个保密卷指定默认模式,如果需要,可以覆盖每个密钥。

apiVersion: v1
kind: Pod
metadata:
  name: mypod
spec:
  containers:
    - name: mypod
      image: redis
      volumeMounts:
        - name: foo
          mountPath: "/etc/foo"
  volumes:
    - name: foo
      secret:
        secretName: mysecret
        defaultMode: 256

然后,secret将被挂载到 /etc/foo 目录,所有通过该secret volume挂载创建的文件的权限都是 0400

请注意,JSON规范不支持八进制符号,因此使用256值作为0400权限。如果您使用yaml而不是json作为pod,则可以使用八进制符号以更自然的方式指定权限。

您还可以是用映射,如上一个示例,并为不同的文件指定不同的权限,如下所示:

apiVersion: v1
kind: Pod
metadata:
  name: mypod
spec:
  containers:
    - name: mypod
      image: redis
      volumeMounts:
        - name: foo
          mountPath: "/etc/foo"
  volumes:
    - name: foo
      secret:
        secretName: mysecret
        items:
          - key: username
            path: my-group/my-username
            mode: 511

在这种情况下,导致 /etc/foo/my-group/my-username 的文件的权限值为 0777。由于JSON限制,必须以十进制格式指定模式。

请注意,如果稍后阅读此权限值可能会以十进制格式显示。

Volume中消费secret

在挂载的secret volume的容器内,secret key将作为文件,并且secret的值使用base-64解码并存储在这些文件中。这是在上面的示例容器内执行的命令的结果:

$ ls /etc/foo/
username
password
$ cat /etc/foo/username
admin
$ cat /etc/foo/password
1f2d1e2e67df

容器中的程序负责从文件中读取secret

挂载的secret被自动更新

当已经在volume中消被消费的secret被更新时,被映射的key也将被更新。

Kubelet在周期性同步时检查被挂载的secret是不是最新的。但是,它正在使用其基于本地ttl的缓存来获取当前的secret值。结果是,当secret被更新的时刻到将新的secret映射到pod的时刻的总延迟可以与kubelet中的secret缓存的kubelet sync period + ttl一样长。

Secret作为环境变量

secret作为pod中的环境变量使用:

  1. 创建一个secret或者使用一个已存在的secret。多个pod可以引用同一个secret
  2. 在每个容器中修改您想要使用secret keyPod定义,为要使用的每个secret key添加一个环境变量。消费secret key的环境变量应填充secret的名称,并键入 env[x].valueFrom.secretKeyRef
  3. 修改镜像并/或者命令行,以便程序在指定的环境变量中查找值。
apiVersion: v1
kind: Pod
metadata:
  name: secret-env-pod
spec:
  containers:
    - name: mycontainer
      image: redis
      env:
        - name: SECRET_USERNAME
          valueFrom:
            secretKeyRef:
              name: mysecret
              key: username
        - name: SECRET_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysecret
              key: password
  restartPolicy: Never

消费环境变量里的Secret

在一个消耗环境变量secret的容器中,secret key作为包含secret数据的base-64解码值的常规环境变量。这是从上面的示例在容器内执行的命令的结果:

$ echo $SECRET_USERNAME
admin
$ echo $SECRET_PASSWORD
1f2d1e2e67df

使用imagePullSecret

imagePullSecret是将包含Docker(或其他)镜像注册表密码的secret传递给Kubelet的一种方式,因此可以代表您的pod拉取私有镜像。

手动指定imagePullSecret

imagePullSecret的使用在 镜像文档 中说明。

安排imagePullSecrets自动附加

您可以手动创建imagePullSecret,并从serviceAccount引用它。使用该serviceAccount创建的任何pod和默认使用该serviceAccountpod将会将其的imagePullSecret字段设置为服务帐户的imagePullSecret字段。有关该过程的详细说明,请参阅 ImagePullSecrets添加到服务帐户

自动挂载手动创建的Secret

手动创建的secret(例如包含用于访问github帐户的令牌)可以根据其服务帐户自动附加到pod

详细

限制

验证secret volume来源确保指定的对象引用实际上指向一个类型为Secret的对象。因此,需要在依赖于它的任何pod之前创建一个secret

Secret API对象驻留在命名空间中。它们只能由同一命名空间中的pod引用。

每个secret的大小限制为1MB。这是为了防止创建非常大的secret会耗尽apiserverkubelet的内存。然而,创建许多较小的secret也可能耗尽内存。更全面得限制secret对内存使用的更全面的限制是计划中的功能。

Kubelet仅支持从API server获取的Pod使用secret。这包括使用kubectl创建的任何pod,或间接通过replication controller创建的pod。它不包括通过kubelet--manifest-url标志,其 --config 标志或其REST API创建的pod(这些不是创建pod的常用方法

必须先创建secret,除非将它们标记为可选项,否则必须在将其作为环境变量在pod中使用之前创建secret。对不存在的secret的引用将阻止其启动。

通过 secretKeyRef 对不存在于命名的key中的key进行引用将阻止该启动。

用于通过 envFrom 填充环境变量的secret,这些环境变量具有被认为是无效环境变量名称的key将跳过这些键。该pod将被允许启动。将会有一个事件,其原因是 InvalidVariableNames,该消息将包含被跳过的无效键的列表。该示例显示一个pod,它指的是包含2个无效键,1badkey2alsobad的默认/mysecret ConfigMap

$ kubectl get events
LASTSEEN   FIRSTSEEN   COUNT     NAME            KIND      SUBOBJECT                         TYPE      REASON
0s         0s          1         dapi-test-pod   Pod                                         Warning   InvalidEnvironmentVariableNames   kubelet, 127.0.0.1      Keys [1badkey, 2alsobad] from the EnvFrom secret default/mysecret were skipped since they are considered invalid environment variable names.

SecretPod生命周期的联系

通过API创建的Pod时,不会检查应用的secret是否存在。一旦Pod被调度,kubelet就会尝试获取该secret的值。如果获取不到该secret,或者暂时无法与API server建立连接,kubelet将会定期重试。Kubelet将会报告关于pod的事件,并解释它无法启动的原因。一旦获取的secretkubelet将创建并装载一个包含它的卷。在安装所有pod的卷之前,都不会启动pod的容器。

使用案例

使用案例:包含ssh密钥的pod

创建一个包含ssh keysecret

$ kubectl create secret generic ssh-key-secret --from-file=ssh-privatekey=/path/to/.ssh/id_rsa --from-file=ssh-publickey=/path/to/.ssh/id_rsa.pub

安全性注意事项:发送自己的ssh密钥之前要仔细思考:集群的其他用户可能有权访问该密钥。使用您想要共享Kubernetes集群的所有用户可以访问的服务帐户,如果它们遭到入侵,可以撤销。

现在我们可以创建一个使用ssh密钥引用secretpod,并在一个卷中使用它:

kind: Pod
apiVersion: v1
metadata:
  name: secret-test-pod
  labels:
    name: secret-test
spec:
  volumes:
    - name: secret-volume
      secret:
        secretName: ssh-key-secret
  containers:
    - name: ssh-test-container
      image: mySshImage
      volumeMounts:
        - name: secret-volume
          readOnly: true
          mountPath: "/etc/secret-volume"

当容器中的命令运行时,密钥的片段将可在以下目录:

/etc/secret-volume/ssh-publickey
/etc/secret-volume/ssh-privatekey

然后容器可以自由使用密钥数据建立一个ssh连接。

使用案例:包含prod/test凭据的pod

下面的例子说明一个pod消费一个包含prod凭据的secret,另一个pod使用测试环境凭据消费secret

创建secret

$ kubectl create secret generic prod-db-secret --from-literal=username=produser --from-literal=password=Y4nys7f11
secret "prod-db-secret" created
$ kubectl create secret generic test-db-secret --from-literal=username=testuser --from-literal=password=iluvtests
secret "test-db-secret" created

创建pod

apiVersion: v1
kind: List
items:
  - kind: Pod
    apiVersion: v1
    metadata:
      name: prod-db-client-pod
      labels:
        name: prod-db-client
    spec:
      volumes:
        - name: secret-volume
          secret:
            secretName: prod-db-secret
      containers:
        - name: db-client-container
          image: myClientImage
          volumeMounts:
            - name: secret-volume
              readOnly: true
              mountPath: "/etc/secret-volume"
  - kind: Pod
    apiVersion: v1
    metadata:
      name: test-db-client-pod
      labels:
        name: test-db-client
    spec:
      volumes:
        - name: secret-volume
          secret:
            secretName: test-db-secret
      containers:
        - name: db-client-container
          image: myClientImage
          volumeMounts:
            - name: secret-volume
              readOnly: true
              mountPath: "/etc/secret-volume"

这两个容器将在其文件系统上显示以下文件,其中包含每个容器环境的值:

/etc/secret-volume/username
/etc/secret-volume/password

请注意,两个podspec配置中仅有一个字段有所不同;这有助于使用普通的pod配置模板创建具有不同功能的pod。您可以使用两个service account进一步简化基本pod spec:一个名为 prod-user 拥有 prod-db-secret ,另一个称为 test-user 拥有 test-db-secret 。然后,pod spec可以缩短为,例如:

kind: Pod
apiVersion: v1
metadata:
  name: prod-db-client-pod
  labels:
    name: prod-db-client
spec:
  serviceAccount: prod-db-client
  containers:
    - name: db-client-container
      image: myClientImage

使用案例:secret卷中以点号开头的文件

为了将数据“隐藏”起来(即文件名以点号开头的文件,简单地说让该键以一个点开始。例如,当如下secret被挂载到卷中:

kind: Secret
apiVersion: v1
metadata:
  name: dotfile-secret
data:
  .secret-file: dmFsdWUtMg0KDQo=
---
kind: Pod
apiVersion: v1
metadata:
  name: secret-dotfiles-pod
spec:
  volumes:
    - name: secret-volume
      secret:
        secretName: dotfile-secret
  containers:
    - name: dotfile-test-container
      image: gcr.io/google_containers/busybox
      command:
        - ls
        - "-l"
        - "/etc/secret-volume"
      volumeMounts:
        - name: secret-volume
          readOnly: true
          mountPath: "/etc/secret-volume"

Secret-volume 将包含一个单独的文件,叫做 .secret-filedotfile-test-container/etc/secret-volume/.secret-file路径下将有该文件。

注意

以点号开头的文件在 ls -l 的输出中被隐藏起来了;列出目录内容时,必须使用 ls -la 才能查看它们。

使用案例:Secret仅对pod中的一个容器可见

考虑以下一个需要处理HTTP请求的程序,执行一些复杂的业务逻辑,然后使用HMAC签署一些消息。因为它具有复杂的应用程序逻辑,所以在服务器中可能会出现一个未被注意的远程文件读取漏洞,这可能会将私钥暴露给攻击者。

这可以在两个容器中分为两个进程:前端容器,用于处理用户交互和业务逻辑,但无法看到私钥;以及可以看到私钥的签名者容器,并且响应来自前端的简单签名请求(例如通过本地主机网络

使用这种分割方法,攻击者现在必须欺骗应用程序服务器才能进行任意的操作,这可能比使其读取文件更难。

最佳实践

客户端使用secret API

当部署与secret API交互的应用程序时,应使用诸如 RBAC 之类的 授权策略 来限制访问。

Secret的重要性通常不尽相同,其中许多可能只对Kubernetes集群内(例如service account令牌)和对外部系统造成影响。即使一个应用程序可以理解其期望的与之交互的secret的权力,但是同一命名空间中的其他应用程序也可以使这些假设无效。

由于这些原因,在命名空间中 watchlistsecret的请求是非常强大的功能,应该避免这样的行为,因为列出secret可以让客户端检查所有secret是否在该命名空间中。在集群中watchlist 所有secret的能力应该只保留给最有特权的系统级组件。

需要访问secrets API的应用程序应该根据他们需要的secret执行 get 请求。这允许管理员限制对所有secret的访问,同时设置 白名单访问 应用程序需要的各个实例。

为了提高循环获取的性能,客户端可以设计引用secret的资源,然后 watch 资源,在引用更改时重新请求secret

安全属性

保护

因为 secret 对象可以独立于使用它们的 pod 而创建,所以在创建、查看和编辑pod的流程中secret被暴露的风险较小。系统还可以对 secret 对象采取额外的预防措施,例如避免将其写入到磁盘中可能的位置。

只有当节点上的pod需要用到该secret时,该secret才会被发送到该节点上。它不会被写入磁盘,而是存储在tmpfs中。一旦依赖于它的pod被删除,它就被删除。

在大多数Kubernetes项目维护的发行版中,用户与API server之间的通信以及从API serverkubelet的通信都受到SSL/TLS的保护。通过这些通道传输时,secret受到保护。

节点上的secret数据存储在tmpfs卷中,因此不会传到节点上的其他磁盘。

同一节点上的很多个pod可能拥有多个secret。但是,只有pod请求的secret在其容器中才是可见的。因此,一个pod不能访问另一个Podsecret

Pod中有多个容器。但是,pod中的每个容器必须请求其挂载卷中的secret卷才能在容器内可见。这可以用于 Pod级别构建安全分区

风险

  • API serversecret数据以纯文本的方式存储在etcd中;因此:
    • 管理员应该限制admin用户访问etcd
    • API server中的secret数据位于etcd使用的磁盘上;管理员可能希望在不再使用时擦除/粉碎etcd使用的磁盘
  • 如果您将secret数据编码为base64的清单(JSONYAML)文件,共享该文件或将其检入代码库,这样的话该密码将会被泄露。Base64编码不是一种加密方式,一样也是纯文本。
  • 应用程序在从卷中读取secret后仍然需要保护secret的值,例如不会意外记录或发送给不信任方。
  • 可以创建和使用secretpod的用户也可以看到该secret的值。即使API server策略不允许用户读取secret对象,用户也可以运行暴露secretpod
  • 如果运行了多个副本,那么这些secret将在它们之间共享。默认情况下,etcd不能保证与SSL/TLS的对等通信,尽管可以进行配置。
  • 目前,任何节点的root用户都可以通过模拟kubelet来读取API server中的任何secret。只有向实际需要它们的节点发送secret才能限制单个节点的根漏洞的影响,该功能还在计划中。
上一页
下一页