ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

K8s Secret实战指南:创建方式、注入姿势与安全加固实践

K8s Secret实战指南:创建方式、注入姿势与安全加固实践 最近在帮团队梳理 Kubernetes简称 K8s集群的配置治理规范发现一个很有意思的现象不少同学对 K8s 的 Secret 对象理解得比较浅要么把它当成万能保险箱以为数据放进去就绝对安全要么干脆不用把数据库密码、API 密钥、证书私钥直接写死在 Pod 的 yaml 里。这两种极端在真正的生产环境里都挺危险的。这篇文章不会泛泛地讲 Secret 是什么而是从我自己的实战经验出发把 Secret 的设计逻辑、创建方式、注入姿势、安全加固措施以及我踩过的坑完整串一遍。无论你是刚把 K8s 集群搭起来、正在规范配置管理方式还是准备面试 K8s 相关岗位这篇文章应该都能给你一些用得上的东西。顺便说一句Secret 是 K8s 原生的资源对象不管你的集群是用 kubeadm 装的、二进制方式搭的还是云厂商托管的使用方式完全一样不存在哪套集群“不支持”的问题。1. 先搞懂 Secret 的本质它不是加密只是不让你一眼看到1.1 K8s 为什么要单独造一个 Secret 对象先回答一个很多新手都会问的问题K8s 里已经有 ConfigMap 了为什么还要再搞一个 Secret它们俩看起来都是把配置数据从 Pod 里抽离出来区别到底在哪最直接的答案Secret 专门用来放“敏感信息”。这个定位决定了它和 ConfigMap 在用途上有本质差异。ConfigMap 里放的是端口号、日志级别、开关配置这类不敏感的数据就算被人看到了也无所谓甚至很多团队会直接把 ConfigMap 提交到 Git 仓库里做版本管理。但 Secret 里放的是数据库密码、第三方 API Token、TLS 私钥、镜像仓库凭证这些东西一旦泄露轻则数据被拖库重则整个集群被攻破。K8s 把 Secret 单独设计成一个 API 对象核心目的有三个一是从机制上引导用户把敏感信息与非敏感信息分开管理二是配合 RBAC基于角色的权限控制体系给 Secret 单独做权限隔离比如普通开发人员可以看 ConfigMap但只有运维人员能看 Secret三是作为后续安全增强的载体比如 K8s 支持对写入 etcd 的 Secret 做加密这类功能如果直接加在 ConfigMap 上影响面就太大了。还要顺带提一点经常有人把 K8s 和 Docker 放在一起比较实际上这俩不是同一个层次的东西。Docker 解决的是“单个容器怎么打包和运行”K8s 解决的是“一堆容器怎么编排、调度、管理”。Secret 是 K8s 这一层的概念它作用于 Pod 和集群层面而不是 Docker 镜像层面。你把镜像推到镜像仓库镜像里的环境变量如果写死了密码那不管用不用 Secret密码都在镜像里躺着这也是很多人容易忽略的泄露途径。1.2 Secret 到底长什么样结构拆解直接看一个最小化的 Secret YAML这样比讲一堆理论更直观。apiVersion: v1 kind: Secret metadata: name: myapp-db-secret namespace: production type: Opaque data: DB_HOST: bXktZGF0YWJhc2UuZGVmYXVsdC5zdmMuY2x1c3Rlci5sb2NhbA DB_USERNAME: YWRtaW4 DB_PASSWORD: c3VwZXItc2VjcmV0LXBhc3N3b3Jk先看type字段Secret 不是只有一种类型Opaque是默认类型表示任意键值对绝大多数场景下你只需要用这种。另外还有两种高频类型kubernetes.io/dockerconfigjson用来存私有镜像仓库凭证kubernetes.io/tls用来存 TLS 证书和私钥。这三种类型覆盖了日常工作中 90% 的诉求后面我会分别演示怎么创建。再看data和stringData这两个字段的区别。data里面的值必须是 Base64 编码后的字符串也就是说你放进去之前要自己先 base64 一下。而stringData允许你直接写明文K8s 的 API Server 会在写入 etcd 之前帮你自动编码成 Base64最后实际保存的时候还是编码后的。我个人建议在 YAML 里写stringData更舒服可读性好很多尤其是在多人协作的仓库里别人 review 代码时不需要先解码才能看懂这个 Key 到底是什么。metadata里的namespace也很关键。Secret 是命名空间级别的资源同一个名字可以同时存在于default、production、staging这些不同的命名空间里互不影响。但这也意味着你在production里创建的 Secret不能直接被staging下的 Pod 引用这是新手最容易踩的坑之一后面排查问题部分我会详细说。2. 创建 Secret 的几种方式总有一种适合你2.1 命令行一键创建适合快速调试和临时验证如果你的集群已经跑起来了想最快速度创建一个 Secret 来验证某个功能那kubectl create secret是最直接的方式不需要写 YAML 文件。最常用的是generic子命令它有几个--from系列参数可以组合使用。# 方式一直接给字面量适合单个或少量键值 kubectl create secret generic app-secret \ --from-literalDB_PASSWORDMyPssw0rd! \ --from-literalAPI_TOKENtoken-xxxxx # 方式二从文件加载整个文件内容会成为某个 Key 的值 kubectl create secret generic app-secret \ --from-fileconfig.yaml./conf/config.yaml # 方式三从目录加载目录下每个文件名会自动成为 Key kubectl create secret generic app-secret \ --from-file./configs/方式一是最直观的--from-literal后面用KeyValue的格式多个键值用多个参数就行。方式二和三适合场景是你有一份现成的配置文件需要完整塞进 Secret 里然后以文件形式挂载到 Pod。这里有一个比较实用的细节--from-fileconfig.yaml./conf/config.yaml这种写法表示“把本地的conf/config.yaml内容放进 SecretKey 为config.yaml”如果省略前面的config.yaml则会用文件名本身作为 Key。命令行创建还有个好处--dry-runclient -o yaml参数可以让你在真正创建之前把最终 YAML 打印出来方便审查。kubectl create secret generic app-secret \ --from-literalDB_PASSWORDMyPssw0rd! \ --dry-runclient -o yaml这样输出出来的 YAML就是 API Server 实际会接受到的内容你可以确认data字段已经被 Base64 编码。2.2 YAML 声明式管理生产环境更推荐命令行创建方便归方便但在生产环境里我更推荐把 Secret 定义放进 Git 仓库走 GitOps 流程统一管理。原因很简单你的 Secret 不能只存在于集群里它需要有版本历史、需要被 review、需要能在故障时快速重建。用 YAML 文件管理时我推荐用stringData而不是data。apiVersion: v1 kind: Secret metadata: name: app-config namespace: production type: Opaque stringData: DB_HOST: my-database.default.svc.cluster.local DB_USERNAME: admin DB_PASSWORD: super-secret-password注意stringData只是创建或更新时的便捷写法当你通过kubectl get secret app-config -o yaml查看实际对象时API Server 返回的仍然是data字段值已经变成 Base64 编码了。这里要特别提醒一个安全习惯尽量不要用kubectl get secret name -o yaml然后把输出直接拷贝到剪贴板或者聊天工具里发送。命令本身没有问题但你的 shell 历史、终端日志、企业内部的即时通讯记录都可能因此留下敏感信息。我一般都会在拿到 YAML 后立即用 sed 或者编辑器把data段替换掉只保留我要关注的 metadata 部分。2.3 私有镜像仓库凭证一行命令解决拉取问题集群要拉取私有镜像仓库里的镜像你就必须给节点提供凭证。这种凭证也是通过 Secret 创建的类型是kubernetes.io/dockerconfigjson。命令行创建非常简单kubectl create secret docker-registry regcred \ --docker-serverregistry.example.com \ --docker-usernamedeploy-user \ --docker-passwordSuperSecret \ --docker-emaildeployexample.com \ --namespaceproduction创建完成之后在 Deployment 的 Pod 模板里通过imagePullSecrets引用Kubernetes 在拉取镜像时就会自动携带这个凭证。spec: template: spec: imagePullSecrets: - name: regcred containers: - name: myapp image: registry.example.com/myapp:latest这里常见的坑是imagePullSecrets是 Pod 级别的字段如果你写在了 Deployment 顶层kubectl apply会直接报错。另外regcred和 Deployment 必须在同一个 namespace否则 Pod 调度到该节点时无法找到对应的 Secret依然会报ImagePullBackOff。2.4 TLS 证书类型给 Ingress 用如果你的集群用 Ingress 对外暴露 HTTPS 服务那 TLS 证书和私钥也需要放到 Secret 里类型是kubernetes.io/tls。kubectl create secret tls myapp-tls \ --certcerts/tls.crt \ --keycerts/tls.key \ --namespaceproduction要注意--cert和--key必须配对证书里面包含的域名信息要和你的 Ingress 规则匹配否则浏览器会报证书错误。Ingress 里引用时只需在tls段指定 secretName 即可。到这里Secret 的创建方式基本覆盖全了。做一个简单对比表格帮大家记Secret 类型典型用途创建命令常见引用位置Opaque数据库密码、API Token、任意键值kubectl create secret generic环境变量、挂载文件dockerconfigjson私有镜像仓库登录凭证kubectl create secret docker-registryPod 的 imagePullSecretstlsTLS 证书和密钥kubectl create secret tlsIngress、Ingress Controller3. 把 Secret 真正用起来三种访问姿势详解3.1 环境变量注入简单但更新不实时把 Secret 的内容注入到 Pod 的环境变量是大多数业务应用最习惯的读取方式因为应用代码不用做任何特殊处理直接os.Getenv(DB_PASSWORD)就行。apiVersion: apps/v1 kind: Deployment metadata: name: myapp namespace: production spec: template: spec: containers: - name: myapp image: myapp:latest env: - name: DB_PASSWORD valueFrom: secretKeyRef: name: app-config key: DB_PASSWORDvalueFrom.secretKeyRef指向集群里已经存在的一个 Secret 对象name是 Secret 名称key是 Secret 数据里的键。你也可以用envFrom把整个 Secret 暴露成环境变量envFrom: - secretRef: name: app-config这样 Secret 里的每个键值都会变成环境变量变量名就是 Key 名。看起来挺省事但我不太建议在大规模生产环境这么干。理由是你失去了对变量名的显式控制如果 Secret 里某个 Key 的命名不符合环境变量规范比如包含中划线K8s 会自动忽略并产生告警事件应用读不到这个变量排错成本就增加了。用env逐条声明虽然啰嗦但每个变量来源都清晰可见谁都能看懂。需要特别记住的是环境变量方式注入的 Secret只在 Pod 启动时生效Pod 运行期间就算你更新了 SecretPod 里的环境变量也不会跟着变。想要新值生效必须重建 Pod。这一点和文件挂载方式不同后面我会展开讲。3.2 文件挂载改动后自动同步但有例外文件挂载的方式是把 Secret 挂载到容器内的一个目录每个键对应一个文件文件名就是键名文件内容就是键值。应用读取指定路径的文件即可。spec: template: spec: containers: - name: myapp image: myapp:latest volumeMounts: - name: app-config mountPath: /etc/myapp readOnly: true volumes: - name: app-config secret: secretName: app-config在这个配置里如果 Secretapp-config里有一个DB_PASSWORD键容器内就会生成/etc/myapp/DB_PASSWORD文件内容就是该键的值。应用只需要读取这个文件就能拿到配置。文件挂载相比环境变量有个很大的优势更新 Secret 后kubelet 会周期性同步这些文件默认大约每 1 分钟同步一次不需要重启 Pod 就能拿到新值。如果你的应用做了文件监听或定时重读就可以实现配置热更新。但是这里有一个非常隐蔽的坑如果volumeMounts里加了subPath那挂载行为就变了Secret 更新后文件不会自动同步。volumeMounts: - name: app-config mountPath: /etc/myapp/DB_PASSWORD subPath: DB_PASSWORD readOnly: truesubPath是把 Secret 里的某个键挂载成单个文件而不是挂载整个目录。这样做的好处是容器内其他文件不会被覆盖但代价是 kubelet 不会对这个文件做自动更新。我见过不止一次事故应用配置文件是 nginx.conf通过 subPath 从 Secret 挂载运维改了 Secretnginx 始终加载的还是旧配置排查了半天才发现是这个机制问题。所以我的建议是需要热更新的场景不要用 subPath必须用 subPath 的场景就接受“改 Secret 之后需要重启 Pod”这个现实别在更新机制上白费劲。3.3 静态 Pod / 控制器自动挂载ServiceAccount 与 Secret 的关系还有一类 Secret 你可能没主动创建过但每个命名空间里都存在那就是 ServiceAccount 对应的 Token。每个 namespace 在创建defaultServiceAccount 时会自动生成一个类型为kubernetes.io/service-account-token的 Secret它会被自动挂载到该命名空间下所有 Pod 的/var/run/secrets/kubernetes.io/serviceaccount/目录。这个目录下包含 CA 证书和 JWT Token供 Pod 内应用与 K8s API Server 做身份认证。这个机制本身没问题但实际运维中我发现有些团队把所有 Pod 都放在同一个 namespace也没有把automountServiceAccountToken显式设为false导致业务容器里白白挂载了一个具备一定权限的 ServiceAccount Token。万一业务容器被入侵攻击者拿到这个 Token 就能尝试调用 API Server。这个点虽然不算 Secret 的核心用法但和 Secret 权限安全强相关我在下一节展开讲 RBAC 时会再提一次。4. 安全加固:这才是生产环境该有的姿势4.1 别让 Secret 裸奔给 etcd 里的数据加一层真正的加密先说一个很多人都会误解的事实Secret 里的值只是 Base64 编码而 Base64 不是加密。你随手写一个字符串然后 base64 一下任何人都能直接解码还原。K8s 默认配置下Secret 的数据是以 Base64 编码的形式存在 etcd 里的这意味着如果备份文件泄露或者 etcd 被拖走里面的密码、Token 等于全裸。我见过很多团队用默认配置跑了一年多直到安全审计才发现这个问题。解决方案是给 API Server 配置EncryptionConfiguration让 Secret 在写入 etcd 之前真正加密。具体操作分三步。第一步生成一个 32 字节的随机密钥并 Base64 编码head -c 32 /dev/urandom | base64第二步写一个加密配置文件/etc/kubernetes/encryption/encryption-config.yamlapiVersion: apiserver.config.k8s.io/v1 kind: EncryptionConfiguration resources: - resources: - secrets providers: - aescbc: keys: - name: key1 secret: 第一步生成的base64字符串 - identity: {}注意 providers 的顺序aescbc排在前面表示新写入的数据都用它加密identity: {}排在最后表示读取数据时对于未加密的存量数据也能兼容解密。第三步修改 kube-apiserver 的启动参数加上--encryption-provider-config/etc/kubernetes/encryption/encryption-config.yaml改完重启 API Server新写入的 Secret 就会以密文形式存到 etcd 了。这一步如果集群是 kubeadm 装的可以直接编辑/etc/kubernetes/manifests/kube-apiserver.yaml添加参数kubelet 会自动检测到并重启 API Server 静态 Pod。要特别提醒的是这个配置不会对已有的存量 Secret 做加密。已经存在 etcd 里的老 Secret 在更新之前仍然是明文编码状态。想要全部加密可以遍历所有 namespace 下的 Secret重新写入一次或者借助开源工具强制轮换。我当时的做法是写了个简单脚本把每个 Secretget -o yaml再apply一遍强制 API Server 走一次加密路径实测有效。4.2 权限收敛Secret 的 RBAC 管控比加密更前置的一道防线是权限控制。如果你的集群任何人都能kubectl get secrets -A那你加密做得再好也没用。K8s 默认的 RBAC 策略是如果你给某个用户绑定了cluster-admin或view权限他都能看到 Secret 内容。注意view这个角色看起来很无害但它对 Secret 是有读取权限的这是 K8s 出于兼容性做的默认设定实际生产里容易被忽略。我的建议是给 Secret 定义一个独立的最小权限 RoleapiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: namespace: production name: production-secret-reader rules: - apiGroups: [] resources: [secrets] verbs: [get, list, watch]然后只给确实需要读取 Secret 的 ServiceAccount 或用户绑定这个 RoleapiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: namespace: production name: prod-deployer-secret-reader subjects: - kind: ServiceAccount name: deploy-robot namespace: production roleRef: kind: Role name: production-secret-reader apiGroup: rbac.authorization.k8s.io这里要给一个更安全的建议普通开发者的只读权限不要给整个 namespace 级的view而是可以单独给他 ConfigMap 和 Pod 的只读权限Secret 一律不给。真需要看的时候走一次性临时授权。同时在 API Server 开启审计日志后谁看了哪个 Secret、什么时候看的都会被记录。排查安全事件时这些日志是重要依据。如果你的集群还没开审计建议尽早研究一下审计策略配置这个投入非常值得。还有一个小细节命名空间里自动挂载的 ServiceAccount Token 也是 Secret。如果你不想让某个 Pod 自动持有集群权限在 Pod 模板里显式关闭spec: template: spec: automountServiceAccountToken: false认认真真做一遍 RBAC 梳理之后你可能会发现集群里长期存在一堆实际用不到的超级权限账号这也是我每次安全巡检必查的一项。4.3 更进一步Sealed Secrets 与 External Secrets就算把 Secret 纳入 Git 仓库管理stringData仍然把明文写在了仓库里这对于私有仓库还好如果是开源项目或者对代码权限管控比较松的团队风险依然很大。有两个工具能解决这个问题。第一个是 Bitnami Sealed Secrets。它的思路是在集群里装一个 ControllerController 持有一对密钥公钥用于加密私钥用于解密。你本地用kubeseal工具把明文 Secret 加密成一个 SealedSecret 对象这个对象可以安全地提交到 Git 仓库。部署时把 SealedSecret apply 到集群Controller 拿到后用私钥解密自动帮你创建真正的 Secret 对象。由于只有 Controller 有私钥即使 SealedSecret YAML 泄露别人也还原不出明文。# 本地用一个普通的 Secret YAML 作为输入 kubeseal --controller-namespace kube-system --controller-name sealed-secrets-controller \ my-secret.yaml my-sealed-secret.yaml生成出来的my-sealed-secret.yaml可以直接提交到 Git。这个工具使用成本很低但安全收益很大强烈推荐给使用 GitOps 流程的团队。第二个是 External Secrets Operator。它的思路更彻底Secret 的明文根本不在 K8s 集群里持久化而是存在第三方密钥管理系统里比如云厂商的 KMS、HashiCorp Vault、AWS Secrets Manager。集群内的 External Secrets Controller 按需从这些系统拉取最新的敏感值然后生成 K8s Secret。这样做的好处是密钥的源头在 KMS 或 Vault 里轮换、审计都在那套成熟体系里完成K8s 只是消费方。如果你的公司已经有密钥管理系统这个方案是长期最优解。这两个工具不冲突可以结合使用。我的经验是敏感程度一般的配置用 Sealed Secrets 解决 Git 明文问题核心数据库密码、支付相关的密钥就直接接 Vault 或云 KMS。另外还有一个技巧K8s 从 1.21 开始支持 Secret 不可变immutable。给 Secret 加上immutable: true后这个 Secret 就不能被修改或删除只能重建。这对于防止误操作、保证运行稳定性很有帮助。代价是不能热更新了需要配合外部系统自动重建 Secret 才能用比如 External Secrets Operator 的轮换模式。对于关键业务的数据库凭证我觉得不可变是一件好事任何人想改都必须显式重建并走一次发布流程。5. 常见问题与排查实录5.1 Secret 更新了Pod 里怎么还是旧值这个问题我在 3.1 和 3.2 里都提到了这里再把判断流程串一下方便你遇到问题时快速定位如果 Secret 通过环境变量注入那 Pod 不会热更新强制删除 Pod 让 Deployment 重新创建新 Pod 即可生效。如果 Secret 通过文件挂载且没有用 subPath那么 kubelet 会在同步周期内更新文件通常最多等 1~2 分钟。如果你等不了手动kubectl rollout restart deployment/myapp也能立即生效。如果用了 subPath文件不会自动更新同样需要重启 Pod。还有一个细节容易被忽略kubectl rollout restart重启 Deployment 后新旧 Pod 会短暂共存这时如果应用启动时有强校验逻辑可能会出现短暂启动失败。建议在业务低峰期操作。5.2 Secret 已经创建了Pod 还报 Secret not found / ImagePullBackOff这个问题我在前面已经提过一次核心原因多半是 namespace 不一致。Secret 和 Pod 必须处于同一个 namespace。排查步骤我总结出来给你参考kubectl get secret name -n namespace确认目标 namespace 下确实存在这个 Secret。kubectl describe pod pod-name -n namespace查看 Events 里的具体报错通常会直接告诉你secret xxx not found。如果确认存在检查 Deployment 里的imagePullSecrets或secretKeyRef拼写有没有大小写、中划线之类的差异这类问题最常见也最隐蔽。顺手检查一下metadata.namespace是不是被前一个命令的历史参数污染了比如你上一个命令在productionnamespace 创建了 Secret下一个 Deployment 写在staging就非常容易出现这种问题。5.3 不小心把 Secret 提交到 Git 仓库了怎么办这个问题我处理过好几次方案其实分两部分。第一部分是把明文从 Git 历史里清掉。老项目的 Git 历史往往已经很长直接用git filter-branch逐条改会很痛苦推荐用git filter-repo或者 BFG Repo-Cleaner可以批量把历史中出现的某个字符串或者某个文件路径全部重写掉。改完历史后强制推送然后所有团队成员重新 clone 新的仓库原来的仓库副本全部作废。但比清理 Git 历史更重要的是第二部分轮换密钥。只要 Secret 内容已经出现在任何人的本地仓库、CI 日志、聊天记录里就应该默认它已经泄露。Git 历史清理只是防止后续继续传播真正的止损是修改所有受影响的密码、Token、证书。这一步不能省也别抱有侥幸心理。我的建议是从这次事件开始给团队配一个 gitleaks 之类的预提交检查工具在代码 push 之前先扫描一遍从源头拦截。5.4 Secret 的 1MiB 大小限制K8s 对单个 Secret 对象的大小上限是 1MB确切说是 1 MiB这个限制其实是继承了 etcd 单对象大小上限。如果你试图往 Secret 里塞一个几十 MB 的配置文件API Server 会拒绝写入。遇到这种情况先想一下是不是设计问题这么大的数据本身不适合放进 Secret可以考虑拆分成多个 Secret 按需挂载或者把大文件放到对象存储在应用启动时拉取。如果只是用于 Kubernetes 集群自身的某些资源比如镜像证书链比较长也可以考虑把证书链拆成多个键分别存储挂载时拼起来用但这要改应用逻辑不是很推荐。总的来说越是接近但这个上限越要审视一下你的方案是否合理。5.5 Pod 启动后看不到 Secret 对应的环境变量如果你的应用容器里printenv查不到某个变量先确认 Deployment 的 env 配置里secretKeyRef的key和 Secret 里的data键名是否完全一致大小写敏感一个字母都不能差。还有一个容易被忽略的点如果 Secret 里的 Key 名不符合 POSIX 环境变量命名规范只能包含字母、数字、下划线不能以数字开头envFrom批量导入时该键会被静默跳过并且会生成一个InvalidEnvironmentVariableNames事件。用kubectl describe deployment就能看到这个事件。所以我在前面建议用逐条的env就是为了避免这种“看起来没生效实际上被规范过滤了”的诡异情况。最后说几句实在话Secret 这个东西本身用法并不复杂复杂的是你对它的安全意识和管理规范。我早年也吃过亏图省事把数据库密码直接写在 ConfigMap 里等出了问题再回头看才发现整个集群的敏感信息防护形同虚设。后来逐步把 Secret 的权限收敛、etcd 加密、Git 安全存储这些事情一项项补起来才真正觉得集群有了点“生产可用”的样子。如果你现在正准备在团队里推行 Secret 规范化管理我的建议是从最小权限做起先把不需要访问 Secret 的默认权限收紧再考虑上 etcd 加密和工具链。步子不用一下迈太大但每一步都要走稳。希望这篇文章能帮你少踩几个坑。
返回列表