上一篇聊了ConfigMap,把配置和镜像解耦的基本玩法过了一遍。今天这篇接着K8S存储系列往下走,核心是Secret——一句话概括就是:凡是密码、Token、证书、私钥这类不能明文躺平在YAML里的东西,都归它管。文中从创建开始,覆盖挂载文件、路径映射、环境变量注入和私有仓库认证,大部分是生产环境里每天都要用的操作,适合刚看完ConfigMap准备进阶的初级运维,也适合想把Secret这块彻底补齐的K8S使用者。
1. Secret是什么,为什么单独拎出来讲
1.1 Secret与ConfigMap的核心差异
K8S里Secret和ConfigMap长得几乎一模一样,都是把数据从镜像里抽出来,再通过挂载或环境变量的方式注入Pod。但两者定位完全不同:ConfigMap放的是配置,比如配置文件、URL、开关项,就算被不小心看到也没多大事;Secret放的是敏感凭证,比如数据库密码、API Token、TLS私钥,一旦落到不该看的人手里,就是直接的安全事故。
K8S对Secret的照顾体现在几个层面:首先,它从形态上强制分离敏感数据,让你在提交YAML到Git仓库之前,至少有一个“不能明文”的意识门槛;其次,Secret支持更细粒度的访问控制,可以通过RBAC限制谁能读取、谁能修改;最后,Secret在节点上可以选择加密存储,配合KMS这类插件做到落盘加密。实操中我见过不少团队把密码直接塞ConfigMap里,项目一多就到处裸奔,真出了事才回过来补课,其实一开始选对类型就省掉后面一堆麻烦。
1.2 大多数人都会踩的认知坑:base64不等于加密
刚上手的人最容易把Secret当成“加密存储”,这是最大的误区。用kubectl get secret xxx -o yaml看一眼就知道,里面的值只是做了Base64编码,拿命令解一下明文立刻出来,本质和明文没区别。
它真正的作用是帮你避开“在YAML和Git里直接写明文”这个最粗的坑。想要真正的加密,得靠K8S对Secret的ETCD加密配置,或者外部KMS集成。所以公司内部如果审计严格,YAML文件本身也要做加密管理,不能因为用了Secret就掉以轻心。
1.3 Secret的主要类型一览
K8S内置了多种Secret类型,不同场景用不同type,不只是默认的Opaque。我把常用的整理一下:
| 类型 | 典型用途 | Kubernetes类型键 |
|---|---|---|
| Opaque | 任意自定义键值,密码、Token等 | generic |
| kubernetes.io/dockerconfigjson | 私有镜像仓库认证 | docker-registry |
| kubernetes.io/tls | TLS证书与私钥 | tls |
| kubernetes.io/service-account-token | ServiceAccount令牌 | 系统自动管理 |
实战中遇到最多的是Opaque和dockerconfigjson,TLS类型创建标准证书Secret时也很常用。不同类型只是data里的字段格式约束不同,底层机制完全一致。
2. 创建Secret的几种主流方式
2.1 用kubectl命令行从字面量创建
最快速的创建方式是用kubectl create secret generic,适合临时验证或写初始化脚本时直接用。比如要给应用配置数据库密码和API Token:
kubectl create secret generic app-secret \ --from-literal=DB_PASSWORD=P@ssw0rd \ --from-literal=API_TOKEN=tok_xxxxx创建完可以用kubectl get secret app-secret -o yaml查看,里面多了data字段,键和值都是Base64编码。有一点要注意:--from-literal的命令行方式会出现在Shell历史里,如果是多人在同一台机器上操作,建议要么用文件方式,要么用声明式YAML管理,避免敏感信息留在历史记录中。
如果希望覆盖已有Secret,可以在命令末尾加--dry-run=client -o yaml导YAML,再kubectl apply -f -,这样既保留了可读性,又能走GitOps流程。我生产环境里经常用这个组合拳:
kubectl create secret generic app-secret \ --from-literal=DB_PASSWORD='新密码' \ --dry-run=client -o yaml | kubectl apply -f -2.2 从文件创建:证书私钥的最好出处
当数据本身是文件时,比如TLS证书、私钥、配置文件,使用--from-file要方便得多。它会自动把文件内容塞进Secret对应的键里,键名默认是文件名:
kubectl create secret generic my-cert \ --from-file=tls.crt=server.crt \ --from-file=tls.key=server.key上面的写法把server.crt作为值,键名改成tls.crt。如果不加=,键名会直接用原始文件名。还有一种用法是--from-file=config/,把目录下所有文件都变成Secret的键,适合一次打包多个证书。
如果用--from-file读取过大的文件,要注意Secret整体体积限制。ETCD默认支持的单对象大小是1.5MB左右,单个Secret最大也就是1MB,不适用于大文件存储,这是设计定位决定的。
2.3 声明式YAML创建
团队协作时最推荐的还是声明式方式,把Secret定义写进Git仓库,走Review和CI流程。Opaque类型的YAML长这样:
apiVersion: v1 kind: Secret metadata: name: app-secret namespace: default type: Opaque data: DB_PASSWORD: UEAzc3cwcmQ= API_TOKEN: dG9rX3h4eHh4注意data里的值必须是Base64。如果你不想自己敲Base64,可以用stringData字段,K8S在创建时会自动帮你编码,YAML文件里直接写明文,然后kubectl apply即可。这种写法审阅时能直接看到值,安全性存疑,但如果配合加密Git仓库使用,体验非常好。
我还遇到过直接把kubectl create secret的dry-run结果落盘管理的场景,这种方式UI上看起来最直观,不过时间久了YAML会变长,不如用Kustomize的generatorOptions统一管理。
2.4 创建后校验:不只是get看一下
创建完Secret,别急着走,建议先验证一下数据是否正确解出:
kubectl get secret app-secret -o jsonpath='{.data.DB_PASSWORD}' | base64 -d如果值是中文或特殊字符,直接base64 -d可能因为换行符出问题,用echo -n包裹更稳妥。生产环境我习惯把这段写成一个小函数,想检查哪个键就传参进去,效率高很多。
3. 把Secret挂载成文件:默认行为、路径映射和subPath
3.1 默认挂载会整个覆盖目录
把Secret挂载成文件是日志采集类应用、传统Java应用读取配置文件最常见的场景。默认写法示意如下:
apiVersion: apps/v1 kind: Deployment metadata: name: app spec: template: spec: containers: - name: app image: myapp:1.0 volumeMounts: - name: secret-volume mountPath: /etc/app-secret readOnly: true volumes: - name: secret-volume secret: secretName: app-secret这样做会把Secret里每个键变成一个文件,文件名为键名,内容为解码后的值。许多第一次上手的人会在这踩坑:mountPath目录下的原有内容全被覆盖了。比如容器镜像里/etc/app目录本身有东西,挂载后全被Secret内容顶掉。
我给出的建议是:尽量把Secret挂载到专门的空目录里,比如/etc/secrets,或者用subPath只挂载单个文件。千万别偷懒直接挂到一个系统目录上,否则容器可能直接起不来。
3.2 items路径映射:改键名、挑字段、映射路径
Secret里有多个键,但你可能只想挂载其中一部分;或者你想把某个键挂到特定路径,比如把DB_PASSWORD挂载成/etc/secrets/password。这时需要items配置:
volumes: - name: secret-volume secret: secretName: app-secret items: - key: DB_PASSWORD path: db/password - key: API_TOKEN path: api/token挂载后容器内会出现/etc/app-secret/db/password和/etc/app-secret/api/token,没有被items列出的键就不会挂载。路径支持子目录,自动创建。这种用法最大的优势是把键名和文件名解耦,也能保持容器内路径规范统一。
3.3 subPath挂载:单文件覆盖,不碰目录里其他内容
如果你只想把Secret里的某一个键挂到已有文件上,比如替换/etc/nginx/nginx.conf,volumeMounts里加subPath就行:
volumeMounts: - name: secret-volume mountPath: /etc/app/config.yaml subPath: config.yaml readOnly: true这里subPath的值是Secret里的键名,挂载后/etc/app/config.yaml就会被替换成Secret里config.yaml键的内容,而/etc/app目录下其他原有文件原封不动。
需要注意subPath挂载会失去K8S对Secret文件更新的自动联动能力,后续Secret更新后subPath挂载的文件不会自动刷新。要更新就得重新创建Pod或手动触发滚动。所以生产上如果依赖配置热更新,优先别用subPath。
3.4 挂载权限与只读策略
Secret文件默认权限是0444,也就是所有用户可读、不可写。这符合安全诉求,但确实有特殊场景需要放开写权限,比如某些应用启动时要写配置文件。可以通过defaultMode修改:
volumes: - name: secret-volume secret: secretName: app-secret defaultMode: 0600我一般会建议:即使应用需要写,也别把整个Secret目录挂成可写,改成单个文件挂载加0644,把影响面降到最小。另一个经验是把readOnly: true显式写上,因为Secret本来就是敏感内容,防止容器内进程意外篡改Secret文件内容。
4. 通过环境变量注入Secret
4.1 单键注入:valueFrom加secretKeyRef
有些应用不读文件,只认环境变量。比如Spring Boot里常见的数据库密码、第三方服务Token,环境变量方式更直接。单个键注入写法如下:
env: - name: DB_PASSWORD valueFrom: secretKeyRef: name: app-secret key: DB_PASSWORD这样容器里的DB_PASSWORD环境变量会带上Secret里对应键的值。注意如果用kubectl exec进容器查看环境变量,值是明文可见的,但至少不会躺在镜像或YAML里。
4.2 批量注入:envFrom省掉一大堆重复代码
Secret里键很多时,一个个写valueFrom很折磨人。用envFrom可以一次性把所有键变成环境变量:
envFrom: - secretRef: name: app-secret前提是Secret里的键本身符合环境变量命名规则,像my.key、app-config这类含点号或横杠的键会被忽略掉,不会注入。实测中不止一次遇到这类坑:Secret创建没问题,但Pod起来后环境变量就是少几个,排查半天发现是键名不合法。
如果需要同时注入多个Secret,可以写多个secretRef,也可以和env混用,后者优先级更高。注意如果两个Secret的键重名,会有覆盖情况,执行时要先确认键不冲突。
4.3 环境变量方式的短板要提前知道
环境变量注入最大的问题是无法热更新。当Secret修改后,文件挂载方式会在kubelet周期内自动更新(通常几十秒内),但环境变量一旦注入,除非Pod重启,否则进程拿到的一直是旧值。如果应用对配置变更要求实时性比较高,建议优先走文件挂载。
另外有个小技巧:很多ConfigMap或Secret的键是下划线风格,但应用希望环境变量名是全大写风格。这种情况下可以写两个Secret,一个存原始键,一个专门映射成环境变量名,或者直接用env逐个映射,看具体需求权衡。
5. 私有镜像仓库认证:imagePullSecrets实战
5.1 创建docker-registry类型的Secret
从私有仓库拉镜像时,kubelet需要一套认证信息。手动创建的命令如下:
kubectl create secret docker-registry my-registry \ --docker-server=registry.example.com \ --docker-username=readonly_user \ --docker-password=readonly_pass \ --docker-email=ops@example.com这里的docker-email虽然是必填参数,但在现代Docker Registry认证中作用不大,大多数Registry(只要兼容Docker Registry v2协议)都不校验邮箱。如果公司私有仓库用的是自建Registry,只要用户名密码对得上就行。
执行后查看一下Secret的类型,会看到kubernetes.io/dockerconfigjson,data里是.dockerconfigjson字段,Base64解码后是标准的Docker配置JSON结构。
5.2 在Pod中指定imagePullSecrets
创建完Secret,需要让它生效。最直接的方式是在Pod模板里声明:
spec: imagePullSecrets: - name: my-registry这个字段写在Pod的spec层级,不是container层级。如果使用Deployment、StatefulSet,就在spec.template.spec.imagePullSecrets下写。一个Pod可以配置多个imagePullSecrets,kubelet会按顺序尝试,直到拉取成功。
我踩过的一个坑是:有些云厂商提供的内置镜像加速器,会覆盖用户的Registry配置,导致指定了imagePullSecrets也拉不到镜像。这种情况要么走公司统一镜像仓库,要么在节点上配置containerd的registry mirror,别在Pod层做过多的绕路。
5.3 更省心的方式:给ServiceAccount绑定imagePullSecrets
如果一个命名空间下大部分Pod都要从同一私有仓库拉镜像,一个个指定imagePullSecrets太啰嗦。K8S允许在ServiceAccount上预置Secret,这样只要Pod使用对应ServiceAccount,就会自动带上认证。
给默认ServiceAccount绑定可以省掉所有Pod的声明:
apiVersion: v1 kind: ServiceAccount metadata: name: default imagePullSecrets: - name: my-registry执行完kubectl apply后,后续创建的Pod都会自动附加my-registry这个Secret,除非Pod里显式覆盖了imagePullSecrets。这是个很实用的最佳实践,在测试环境和多团队共享集群时能省大量重复配置。
如果某个Pod不希望使用默认自动附加的Secret,也可以显式指定imagePullSecrets: []来清空。不过生产环境里,建议对敏感命名空间的默认ServiceAccount做严格管控,避免权限扩散。
5.4 私有Registry自签证书的坑
自建的私有仓库如果用了自签TLS证书,光有imagePullSecrets还不够,kubelet跟Registry握手时依然会报证书错误。具体解决方式是在节点或containerd层面配置自签CA证书,这块和Secret关系不大,但出问题时经常被误以为是imagePullSecrets写错了。
另一个相关问题是HTTPS与HTTP。如果Registry只支持HTTP,需要在containerd配置里给该地址加skip_verify或insecure。这里提醒一下,安全扫描严格的团队不建议连裸HTTP的私有仓库,至少得挂一层TLS。
6. Secret的更新、轮转与排错实录
6.1 文件挂载的Secret更新后会不会自动生效
Secret挂载成文件的场景下,kubelet会周期性地同步Secret内容到节点本地,然后更新挂载的文件。实测下来,修改Secret后大约30秒到1分钟,容器里读到的文件内容会变。
但这里有个隐含条件:应用得能感知文件变化。很多应用只在启动时读一次配置文件,文件内容改了它也不知道。解决方法有几种:一是应用自己监听文件变化,二是把更新流程做成滚动发布,让新版Pod用新Secret,三是通过sidecar做配置重载。生产里比较推荐的是第二种:把Secret的版本号或数据hash打进Pod注解里,作为滚动发布的触发条件。
有些团队会写一个工具,把Secret内容做MD5,在Deployment环境变量里注入这个MD5,Secret一变,Deployment的Template哈希跟着变,控制器自动滚动重启。这是最省事且可靠的做法。
6.2 环境变量注入的Secret为什么重启也没更新
环境变量注入后,Pod不重启就永远用旧值。就算你把Secret改了,然后kubectl delete pod手动重启,有些Pod(尤其是节点资源紧张时)会调度到旧Pod所在节点,kubelet可能用了缓存,导致还是旧值。
遇到这种问题,最稳的办法是先滚动更新Deployment,用自身机制保证新Pod拉到新Secret。如果希望避免这种来回折腾,直接选择文件挂载方式。另外所有环境变量相关的Secret变更,都应该纳入部署流程,比如走GitOps自动触发滚动,而不只是手动改Secret。
6.3 从PodPending到ImagePullBackOff:一条完整排查链
私有仓库认证问题排查时,建议按顺序检查:先看事件,再查Secret,再查权限,最后查仓库侧。
kubectl describe pod my-pod如果事件里出现ImagePullBackOff,先看具体错误信息。unauthorized或no basic auth credentials说明认证信息不匹配或没传;manifest unknown可能是镜像标签不对;tls handshake timeout大概率是网络或证书问题。
接着用kubectl get secret确认Secret存在,并且命名空间和Pod一致。Secret是有命名空间隔离的,写错命名空间会导致Pod完全找不到认证信息。
然后手动验证一下Secret里的账号密码能不能直接登录仓库:
kubectl get secret my-registry -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d解出来的内容结构是{"auths":{"registry.example.com":{"auth":"Base64编码的用户名密码"}}},如果这一串能解出正确的用户名密码,那Secret本身没问题,问题就在Pod引用或仓库侧。
6.4 常见问题速查表
| 问题现象 | 可能原因 | 解决建议 |
|---|---|---|
| 挂载目录变成Secret文件,原内容丢失 | 没有使用subPath | 改为subPath挂载单个文件 |
| Secret修改后环境变量不更新 | 环境变量机制天然不支持热更新 | 切换文件挂载或滚动发布 |
| items指定后部分键没挂载 | 键名拼写错误或items写错 | 检查Secret实际键名并对照items |
| 私有仓库拉取报unauthorized | imagePullSecrets名字错误或账号没权限 | 核对Secret内容、账号权限 |
| Pod里看到的Secret权限不对 | defaultMode设置错误 | 按需求调整defaultMode |
| 容器读的文件内容是Base64 | 挂载源写成了data字段直接挂 | 确认挂载的是解码后的值,不要自造卷 |
Secret用的时间久了,我个人的体会是:它的设计重点不是加密,而是提供一套强制分离敏感信息的机制,把安全和配置管理从“自觉”变成“规范”。真正生产上最好的实践,是配合外部密钥管理服务做加密和轮转,把Secret当作运行时引用,而不是把秘密直接塞进Secret就算完事。文件挂载优先于环境变量注入,ServiceAccount绑定imagePullSecrets优先于每个Pod单独声明,这两条原则能帮你避开大部分不必要的问题。如果项目规模再大一些,建议给Secret加一层版本管理和自动轮转的流水线,这比在YAML里手写一堆Base64要省心得多。