Kubernetes之Secret
1.使用Secret给容器传递敏感数据
在k8s中,有环境变量和configmap给容器传递非敏感数据,但是通常会包含一些敏感数据,如证书和私钥,需要保证安全性。
1.1 介绍secret
为了存储与分发此类信息,Kubernetes提供了一种称为Secret的单独资源对象。Secret结构与ConfigMap类似,均是键/值对的映射。Secret的使用方法也与ConfigMap相同,可以
-
- 将Secret条目作为环境变量传递给容器
- 将Secret条目暴露为卷中的文件
Kubernetes通过仅仅将Secret分发到需要访问Secret的pod所在的机器节点来保障其安全性。另外,Secret只会存储在节点的内存中,永不写入物理存储,这样从节点上删除Secret时就不需要擦除磁盘了。
对于主节点本身(尤其是etcd),Secret通常以非加密形式存储,这就需要保障主节点的安全从而确保存储在Secret中的敏感数据的安全性。这种保障不仅仅是对etcd存储的安全性保障,同样包括防止未授权用户对API服务器的访问,这是因为任何人都能通过创建pod并将Secret挂载来获得此类敏感数据。从Kubernetes1.7开始,etcd会以加密形式存储Secret,某种程度提高了系统的安全性。正因为如此,从Secret与ConfigMap中做出正确选择是势在必行的,选择依据相对简单:
-
- 采用ConfigMap存储非敏感的文本配置数据。
- 采用Secret存储天生敏感的数据,通过键来引用。如果一个配置文件同时包含敏感与非敏感数据,该文件应该被存储在Secret中。
1.2 默认令牌Secret介绍
首先分析一种默认被挂载至所有容器的Secret,对任意一个pod使用命令kubectl describe pod,输出往往包含如下信息:
Volumes: default-token-cfee9: Type: Secret (a volume populated by a Secret) SecretName: default-token-cfee9
每个pod都会被自动挂载上一个secret卷,这个卷引用的是前面kubectl describe输出中的一个叫作default-token-cfee9的Secret。由于Secret也是资源对象,因此可以通过kubectl get secrets命令从Secret列表中找到这个default-token Secret:
$ kubectl get secrets
NAME TYPE DATA AGE
default-token-cfee9 kubernetes.io/service-account-token 3 39d
同样可以使用kubectl describe多了解一下这个Secret,如下面的代码所示。
#代码7.20 描述一个Secret $ kubectl describe secrets Name: default-token-cfee9 Namespace: default Labels: <none> Annotations: kubernetes.io/service-account.name=default kubernetes.io/service-account.uid=cc04bb39-b53f-42010af00237 Type: kubernetes.io/service-account-token Data ==== ca.crt: 1139 bytes #包含下面3个条目 namespace: 7 bytes token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
可以看出这个Secret包含三个条目---ca.crt、namespace与token,包含了从pod内部安全访问KubernetesAPI服务器所需的全部信息。尽管你希望做到应用程序对Kubernetes的完全无感知,然而在除了直连Kubernetes别无他法的情况下,你将会使用到secret卷提供的文件。
kubectl describe pod命令会显示secret卷被挂载的位置:
Mounts: /var/run/secrets/kubernetes.io/serviceaccount from default-token-cfee9
注意:default-tokenSecret默认会被挂载至每个容器。可以通过设置pod定义中的automountServiceAccountToken字段为false,或者设置pod使用的服务账户中的相同字段为false来关闭这种默认行为(服务账户章节会做详细讲解)。
己经说过Secret类似于ConfigMap,由于该Secret包含三个条目,可通过kubectl exec观察到被secret卷挂载的文件夹下包含三个文件:
$ kubectl exec mypod ls /var/run/secrets/kubernetes.io/serviceaccount/ ca.crt namespace token
下一小节中将会讲解应用程序是如何使用这些文件来访问API服务器的。
1.3 创建Secret
现在创建一个小型Secret。改进fortune-serving的Nginx容器的配置,使其能够服务于HTTPS流量。你需要创建私钥和证书,由于需要确保私钥的安全性,可将其与证书同时存入Secret。
首先在本地机器上生成证书与私钥文件。
$ openssl genrsa -out https.key 2048 $ openssl req -new -x509 -key https.key -out https.cert -days 3650 -subj /CN=www.kubia-example.com
现在为了更好地理解Secret,额外创建一个内容为字符串bar的虚拟文件foo。过会儿就会理解为何要这样做:
$ echo bar > foo
现在使用kubectl create secret命令由这三个文件创建Secret:
$ kubectl create secret generic fortune-https --from-file=https.key --from-file=https.cert --from-file=foo secret "fortune-https" created
与创建ConfigMap的过程类似,这里创建了一个名为fortune-https的generic Secret,它包含有两个条目:https.key和https.cert,分别对应于两个同名文件的内容。如前所述,同样可以用--from-file=fortune-https囊括整个文件夹中的所有文件,替代单独指定每个文件的创建方式。
1.4 对比 ConfigMap 与 Secret
Secret与ConfigMap仍有比较大的差别,这也是为何kubernetes开发者们在支持了Secret—段时间之后仍会选择创建ConfigMap。创建的Secret的YAML格式定义如下面的代码清单所示。
#代码7.21 Secret的YAML格式定义 $ kubectl get secret fortune-https -o yaml apiVersion: v1 data: foo: YmFyCg== https.cert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCekNDQ... https.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcE... kind: Secret ...
将其与之前创建的ConfigMap的YAML格式定义做对比:
#代码7.22 Config的YAML格式定义 $ kubectl get configmap fortune-config -o yaml apiVersion: v1 data: my-nginx-config.conf: | server { ... } sleep-interval: | 25 kind: ConfigMap ...
注意到两者的区别了吗? Secre条目的内容会被以Base64格式编码,而ConfigMap直接以纯文本展示。这种区别导致在处理YAML和JSON格式的Secret时会稍许有些麻烦,需要在设置和读取相关条目时对内容进行编解码。
为二进制数据创建Secret
采用Base64编码的原因很简单Secret的条目可以涵盖二进制数据,而不仅仅是纯文本。Base64编码可以将二进制数据转换为纯文本以YAML或JSON格式展示。
提示:Secret甚至可以被用来存储非敏感二进制数据。不过值得注意的是Secret限于1MB。
stringData字段介绍
由于并非所有的敏感数据都是二进制形式,Kubernetes允许通过Secret的stringData字段设置条目的纯文本值,如下面的代码清单所示。
#代码7.23 通过stringdata字段向Secret添加纯文本条目值 kind: Secret apiVersion: v1 stringData: #stringData可用来社会资非二进制数据 foo: plain text #值为被Base64编码 data: https.cert: LSOtLSlCRUdJTiBDRVJUSUZJQOFURSOtLSOtCklJSURCekNDQ… https.key: LSOtLSlCRUdJTiBSUOEgUFJJVkFURSBLRVktLSOtLQpNSUlFcE....
stringData字段是只写的(注意:是只写,非只读),可以被用来设置条目值。通过kubectl get -o yaml获取Secret的YAML格式定义时,不会显示stringData字段。相反,stringData字段中的所有条目(如上面示例中的foo条目)会被Base64编码之展示在data字段下。
在pod中读取Secret条目
通过secret卷将Secret暴露给容器之后,Secret条目的值会被解码并真实形式(纯文本或二进制)写入对应的文件。通过环境变量暴露Secret条目亦是如此。在这两种情况下,应用程序均无须主动解码,可直接读取文件内容或者查找环境变量。
1.5 在pod中使用Secret
fortune-https已经包含了证书与密钥文件,接下来需要做的是配置Nginx服务器去使用它们。
修改fortune-config ConfigMap以开启HTTPS
为了开启HTTPS,需要再次修改这个ConfigMap对应的配置条目:
$ kubectl edit configmap fortune-config
文本编辑器打开后,修改条目my-nginx-config.conf的内容,如下面的代码清单所示。
#代码7.24 修改fortune-config ConfigMap的数据 ... data: my-nginx-config.conf: | server { listen 80; listen 443 ssl; server_name www.kubia-example.com; ssl_certificate certs/https.cert; ssl_certificate_key certs/https.key; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers HIGH:!aNULL:!MD5; location / { root /usr/share/nginx/html; index index.html index.htm; } } sleep-interval: | ...
上面配置了服务器从/etc/nginx/certs中读取证书与密钥文件,因此之后需要将secret卷挂载于此。
挂载fortune-secret至pod
接下来需要创建一个新的fortune-https pod,将含有证书与密钥的secret卷挂载至pod中的web-server容器,如下面的代码清单所示。
#代码7.25 fortune-https的pod的YAML格式定义:fortune-pod-https.yaml apiVersion: v1 kind: Pod metadata: name: fortune-https spec: containers: - image: luksa/fortune:env name: html-generator env: - name: INTERVAL valueFrom: configMapKeyRef: name: fortune-config key: sleep-interval volumeMounts: - name: html mountPath: /var/htdocs - image: nginx:alpine name: web-server volumeMounts: - name: html mountPath: /usr/share/nginx/html readOnly: true - name: config mountPath: /etc/nginx/conf.d readOnly: true - name: certs #配置Nginx从/etc/nginx/certs/中读取证书和密钥文件,需将secert卷挂载于此 mountPath: /etc/nginx/certs/ readOnly: true ports: - containerPort: 80 - containerPort: 443 volumes: - name: html emptyDir: {} - name: config configMap: name: fortune-config items: - key: my-nginx-config.conf path: https.conf - name: certs #这里引用fortune-https Secret来定义secret卷 secret: secretName: fortune-https
图7.12形象化地展示了上述YAML格式定义中的各组件及其相互关系。Secret default-token以及卷、卷挂载并不包含在这一定义中,因为这些组件被自动加入pod定义,图中不予展示。
注意:与configMap卷相同,secret卷同样支持通过defaultModes属性指定卷中文件的默认权限。
测试Nginx是否正使用Secret中的证书与密钥
pod运行之后,开启端口转发隧道将HTTPS流量转发至pod的443端口,并用curl向服务器发送请求:
$ kubectl port-forward fortune-https 8443:443 & Forwarding from 127.0.0.1:8443 -> 443 Forwarding from [::1]:8443 -> 443 $ curl https://localhost:8443 -k
若服务器配置正确,会得到一个响应,检查响应中服务器证书是否与之前生成的证书匹配。curl命令添加选项-v开启详细日志,如下面的代码清单所示。
#显示Nginx发送的服务器证书 $ curl https://localhost:8443 -k -v * About to connect() to localhost port 8443 (#0) * Trying ::1... * Connected to localhost (::1) port 8443 (#0) * Initializing NSS with certpath: sql:/etc/pki/nssdb * skipping SSL peer certificate verification * SSL connection using TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 * Server certificate: * subject: CN=www.kubia-example.com #证书与之前创建并存储于Secret中的证书匹配 * start date: aug 16 18:43:13 2016 GMT * expire date: aug 14 18:43:13 2026 GMT * common name: www.kubia-example.com * issuer: CN=www.kubia-example.com
Secret卷存储于内存
通过挂载secret卷至文件夹/etc/nginx/certs将证书与私钥成功传递给容器。secret卷采用内存文件系统列出容器的挂载点,如下面的代码清单所示。
$ kubectl exec fortune-https -c web-server -- mount | grep certs tmpfs on /etc/nginx/certs type tmpfs (ro,relatime)
由于使用的是tmpfs,存储在Secret中的数据不会写入磁盘,这样就无法被窃取。
通过环境变量暴露Secret条目
除卷之外,Secret的独立条目可作为环境变量被暴露,就像ConfigMap中sleep-interval条目做的那样。举个例子,若想将Secret中的键foo暴露为环境变量FOO_SECRET,需要在容器定义中添加如下片段。
#代码7.27 Secret条目暴露为环境变量 env: - name: FOO_SECRET valueFrom: secretKeyRef: #通过Secret条目设置环境变量 name: fortune-https #Secret的键 key: foo #Secret的名称
上面片段与设置INTERVAL环境变量的基本一致,除了这里是使用secretKeyRef字段来引用Secret,而非configMapKeyRef,后者用以引用ConfigMap。
Kubernetes允许通过环境变量暴露Secret,然而此特性的使用往往不是一个好主意。应用程序通常会在错误报告时转储环境变量,或者是启动时打印在应用日志中,无意中暴露了Secret信息。另外,子进程会继承父进程的所有环境变量,如果是通过第三方二进制程序启动应用,你并不知道它使用敏感数据做了什么。
提示:由于敏感数据可能在无意中被暴露,通过环境变量暴露Secret给容器之前。请再三思考。为了确保安全性,请始终采用secret卷的方式暴露Secret。
了解镜像拉取Secret
现在学会了如何传递Secret给应用程序并使用它们包含的数据。Kubernetes自身在有些时候希望我们能够传递证书给它,比如从某个私有镜像仓库拉取镜像时。这一点同样需通过Secret来做到。
到目前为止所使用的容器镜像均存储在公共仓库,从上面拉取镜像时无须任何特殊的证书。不过大部分组织机构不希望它们的镜像开放给所有人,因此会使用私有镜像仓库。部署一个pod时,如果容器镜像位于私有仓库,Kubernetes需拥有拉取镜像所需的证书。让我们看一下该怎么做。
在Docker Hub上使用私有镜像仓库
Docker Hub除了是一个公共镜像仓库,还支持在上面创建私有仓库。通过浏览器登录https://hub.docker.com。
运行一个镜像来源于私有仓库的pod时,需要做以下两件事:
-
- 创建包含Docker镜像仓库证书的Secret。
- pod定义中的imagePullSecrets字段引用该Secret。
创建用于Docker镜像仓库鉴权的Secret
使用kubectl create secret命令创建
$ kubectl create secret docker-registry mydockerhubsecret \ --docker-username=myusername --docker-password=mypassword \ --docker-emai1=my.email@provider.com
这里创建了一个docker-registry类型的mydockerhubsecret Secret, 创建时需指定Docker Hub的用户名、密码以及邮箱。通过kubectl describe观察新建Secret的内容时会发现仅有一个条目.dockercfg,相当于用户主目录下的.dockercfg文件。该文件通常在运行docker login命令时由Docker自动创建。
在pod定义中使用docker-registry Secret
为了Kubernetes从私有镜像仓库拉取镜像时能够使用Secret,需要在pod定义中指定docker-registry Secret的名称,如下面的代码。
#代码 7.28 指定镜像拉取 Secret 的 pod 定义:pod-with-private-image.yaml apiVersion: v1 kind: Pod metadata: name: private-pod spec: imagePullSecrets: #能够从私有镜像仓库中拉取镜像 - name: mydockerhubsecret containers: - image: username/private:tag name: main
不需要为每个pod指定镜像拉取Secret
假设某系统中通常运行大量pod,你可能会好奇是否需要为每个pod都添加相同的镜像拉取Secret。幸运的是,情况并非如此。有一种通过添加Secret至ServiceAccount使所有pod都能自动添加上镜像拉取Secret。这里不做解释。