1. 这不是“加个防火墙”就能搞定的安全——初创团队在 DigitalOcean Kubernetes 上的真实安全水位线

你刚用 kubekey 在 DigitalOcean 上三分钟拉起一个三节点集群, kubectl get nodes 全绿,服务跑起来了,前端能访问,后端日志在刷,团队群里一片“成了!”。这时候如果有人问:“安全吗?”——90% 的初创技术负责人会下意识回一句:“应该……没问题吧?我们没开什么高危端口。”

这不是敷衍,是真实困境。DigitalOcean 的 Kubernetes 服务(DOKS)把底层基础设施的复杂度削平了,但 安全责任的边界反而更模糊了 。它不替你管 Pod 里跑的是不是带漏洞的旧版 Nginx,不检查你的 Helm Chart 里有没有硬编码的数据库密码,更不会阻止开发人员用 --privileged=true 启动一个调试容器。它只保证:你的控制平面是隔离的、API Server 的 TLS 是有效的、节点 OS 的基础补丁是打的。剩下的,全是你的战场。

我见过太多案例:一家做 SaaS 工具的团队,用 DOKS 托管核心 API,上线三个月后被扫描出 /etc/passwd 可读漏洞——根源不是 DigitalOcean 的问题,而是他们用了一个社区 Helm Chart,Chart 里默认把 ConfigMap 挂载进了容器的 / 根目录,而 ConfigMap 里恰好存了测试环境的数据库连接串。另一个团队更典型:为快速验证功能,直接在生产命名空间里部署了一个带 hostNetwork: true hostPID: true 的临时诊断 Pod,结果这个 Pod 被上游供应链里的一个恶意依赖反向 SSH 出去,成了整个集群的跳板。

这些都不是理论风险。它们发生在真实的、资源紧张、节奏飞快的初创场景里。而“Zero Trust”和“Least Privilege”这两个词,在 DigitalOcean 的文档里可能只占一页纸,但在你每天写的 YAML 文件里,它们得落实成每一行 securityContext 、每一个 RoleBinding 、每一次 kubectl auth can-i 的验证。这篇内容不讲大道理,不堆概念,只拆解你在 DOKS 上真正要动手改、要写、要反复验证的六个关键动作。它来自我们帮 17 家不同阶段的初创公司做 Kubernetes 安全加固的实操记录,每一步都标好了“为什么必须做”和“不做会怎样”。

2. 控制平面不是黑箱:理解 DOKS 安全模型的“已托管”与“自担责”分界点

很多团队对 DOKS 的安全认知存在一个根本性错觉:以为“托管 Kubernetes”等于“托管全部安全”。这就像租了一套精装修公寓,物业负责楼体结构、电梯和消防系统,但你家里的门锁、保险柜、甚至是否给保姆留备用钥匙,物业概不负责。DOKS 的安全责任模型,正是如此清晰的划分。

2.1 DigitalOcean 明确托管的部分:你无需操心的“基座”

DigitalOcean 对 DOKS 控制平面(Control Plane)承担 100% 的安全责任。这包括:

  • API Server 的 TLS 终止与证书管理 :每个新集群创建时,DOKS 自动为你签发并轮换一套由 Let’s Encrypt 或其私有 CA 签发的证书。你不需要生成 ca.crt server.key ,也不需要配置 --tls-cert-file 。所有 kubectl 请求都通过 HTTPS 加密通道抵达 API Server,且证书链是可信的。这是最基础也是最关键的加密层,DOKS 做得很稳。

  • etcd 数据库的静态加密(At-Rest Encryption) :DOKS 默认启用 AES-256-GCM 加密算法,对存储在 DigitalOcean 块存储(Block Storage)上的 etcd 数据进行加密。这意味着即使物理磁盘被非法获取,没有密钥也无法解密其中的 Secret、ConfigMap 等敏感数据。这个密钥由 DigitalOcean 的密钥管理系统(KMS)托管,你无法访问或导出它——这恰恰是好事,避免了密钥管理的额外负担。

  • 控制平面组件的高可用与补丁更新 :DOKS 将 API Server、etcd、Controller Manager、Scheduler 等组件以多副本形式部署在 DigitalOcean 的专用、隔离网络中。当上游 Kubernetes 社区发布 CVE 补丁(例如 CVE-2023-2431,一个影响 kube-apiserver 的拒绝服务漏洞),DigitalOcean 会在 72 小时内完成热补丁或滚动升级,你无需手动干预。这是托管服务的核心价值,省去了你维护一个高可用、合规的控制平面的巨大成本。

提示:你可以通过 doctl kubernetes cluster get <cluster-name> 命令查看集群的 kubernetes_version updated_at 字段,确认当前版本及上次更新时间。不要试图自己去 SSH 到 master 节点——DOKS 不提供对控制平面节点的 SSH 访问权限,这是设计使然,也是安全边界。

2.2 你必须 100% 自行负责的部分:安全的“最后一公里”

一旦请求穿过 API Server,进入你的工作负载(Workload)层面,安全责任就完全移交给你。DOKS 不会、也不能替你做以下任何事:

  • Pod 内部的安全上下文(Security Context) runAsUser runAsGroup fsGroup readOnlyRootFilesystem allowPrivilegeEscalation 这些字段,全靠你在 Deployment 或 Pod 的 YAML 中显式声明。DOKS 不会帮你把 runAsUser: 0 (即 root)自动降权为 1001 。如果你的镜像默认以 root 运行,且你没覆盖它,那它就是以 root 运行。

  • 网络策略(NetworkPolicy)的定义与执行 :DOKS 集群默认启用 calico CNI 插件,它完全支持 Kubernetes NetworkPolicy API。但 DOKS 不会为你生成一条 deny-all 的默认策略,也不会自动限制 frontend 命名空间只能访问 backend 命名空间的 8080 端口。这一切,从策略编写、到 kubectl apply -f network-policy.yaml ,再到验证其生效,都是你的任务。

  • Secret 的生命周期管理 :DOKS 提供 kubectl create secret 命令,但它只是把 Base64 编码后的字符串存进 etcd。它不检查你存的是不是明文密码,不阻止你把 admin_password db_password 存在同一个 Secret 里,更不会在 Secret 被挂载进 Pod 后,自动清理 Pod 容器内的内存副本。Secret 的安全性,完全取决于你如何创建、如何使用、以及你的应用是否会在日志中无意打印它。

  • 节点(Worker Node)的操作系统与运行时安全 :DOKS 为你预装了 Ubuntu 22.04 LTS 或 Debian 11,并定期推送安全更新(如 apt update && apt upgrade )。但 DOKS 不会替你禁用 root 用户的 SSH 登录,不会帮你卸载集群里根本用不到的 docker CLI(因为 DOKS 使用 containerd),更不会阻止你在节点上手动安装一个未经审计的监控代理。节点是你自己的计算资源,它的操作系统安全,是你自己的责任。

2.3 一个被严重低估的“灰色地带”:Ingress Controller 与 Load Balancer

这里藏着一个初创团队最容易栽跟头的坑。DOKS 提供了官方的 digitalocean-cloud-controller-manager ,它能自动为你创建 DigitalOcean 的 Load Balancer(DO LB)来暴露 Ingress。但 DO LB 本身是一个独立的、位于 Kubernetes 集群之外的网络设备。它的安全配置, 既不属于 DOKS 托管的控制平面,也不属于你管理的 Worker Node

  • SSL/TLS 终止位置 :你可以选择让 DO LB 终止 TLS(即把证书上传到 DO LB),然后以 HTTP 流量转发给你的 Ingress Controller(如 Nginx Ingress)。这很常见,但意味着从 DO LB 到 Ingress Controller 的这段流量是明文的。如果攻击者已经渗透进你的 VPC(例如通过一个被攻破的 Pod),他就能嗅探这段流量。更安全的做法是,让 DO LB 透传 TLS(TCP 模式),由 Ingress Controller 自己处理证书。但这要求你把证书和私钥作为 Kubernetes Secret 存入集群,并在 Ingress 资源中引用。DOKS 不会替你做这个决策,也不会帮你生成那个 Secret。

  • WAF(Web 应用防火墙)集成 :DigitalOcean 的 Load Balancer 目前不原生集成 WAF 功能。如果你需要防 SQL 注入、XSS 攻击,你有两个选择:一是在集群内部署一个开源 WAF(如 ModSecurity + Nginx),这增加了你的运维复杂度;二是购买第三方 WAF 服务(如 Cloudflare),并将 DO LB 的后端指向它。无论哪种,配置、规则调优、日志分析,全是你的活。

理解这个分界点,是制定有效安全策略的第一步。它决定了你的精力应该投向哪里:把时间花在研究如何自动化轮换 etcd 加密密钥上,是徒劳的;而花时间写一个脚本,每天扫描所有 Pod 的 securityContext 并报告 allowPrivilegeEscalation: true 的实例,则是刀刀见血的务实之举。

3. 从“能跑”到“可信”的第一步:强制实施最小权限原则(Least Privilege)的落地清单

在 DOKS 上,“最小权限”不是一句口号,它是一张必须逐项打钩的、覆盖整个请求生命周期的检查清单。这张清单的起点,不是你的应用代码,而是 kubectl 这个命令行工具本身。因为它是所有操作的源头。

3.1 Kubectl 的身份:别再用 doctl kubernetes cluster kubeconfig save 生成的“上帝令牌”

当你执行 doctl kubernetes cluster kubeconfig save --cluster-name my-cluster 时,DigitalOcean 会为你生成一个 kubeconfig 文件。这个文件里包含的 token ,是一个拥有 cluster-admin 角色的 ServiceAccount Token。它等同于 Linux 系统里的 root 用户,可以 get list create delete watch 集群内的一切资源,包括 secrets nodes clusterroles

初创团队早期图省事,把这个 kubeconfig 文件直接交给所有开发、测试、甚至产品经理,让他们自己 kubectl port-forward 查看日志。这无异于把公司保险柜的万能钥匙,发给了前台接待员。一次误操作 kubectl delete namespace production ,或者一个被钓鱼邮件诱导执行的恶意脚本,后果不堪设想。

正确做法:为每个角色创建专属的、受限的 ServiceAccount

我们为一家客户设计了一套基于角色的访问控制(RBAC)体系,仅用了 4 个 YAML 文件,就将权限收束得非常干净:

# 1. dev-sa.yaml: 开发人员 ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
  name: dev-sa
  namespace: default
---
# 2. dev-role.yaml: 开发人员 Role (命名空间级别)
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: dev-role
  namespace: default
rules:
- apiGroups: [""]
  resources: ["pods", "pods/log", "pods/exec", "services", "endpoints"]
  verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
  resources: ["deployments", "statefulsets"]
  verbs: ["get", "list", "watch", "patch"] # patch 允许更新镜像 tag,但不允许 delete 或 create
---
# 3. dev-rolebinding.yaml: 将 Role 绑定到 ServiceAccount
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: dev-rolebinding
  namespace: default
subjects:
- kind: ServiceAccount
  name: dev-sa
  namespace: default
roleRef:
  kind: Role
  name: dev-role
  apiGroup: rbac.authorization.k8s.io
---
# 4. dev-kubeconfig.yaml: 为 dev-sa 生成专属 kubeconfig
# 此文件需通过安全渠道(如 HashiCorp Vault)分发给开发者
# 它只包含 dev-sa 的 token 和指向集群的 server 地址,不含任何其他信息

这个方案的关键在于: 权限是按“动词”(verbs)而非“名词”(resources)授予的 dev-role 允许 patch Deployment,但不允许 create delete 。这意味着开发者可以执行 kubectl set image deployment/my-app container-name=new-image:v1.2 来更新镜像,但无法执行 kubectl delete deployment my-app 来删除整个应用。这是一个微小但至关重要的区别,它把“变更”和“摧毁”彻底隔离开。

注意: kubectl auth can-i --list --as=system:serviceaccount:default:dev-sa 是你的最佳朋友。在部署任何 RBAC 配置后,务必用这条命令验证该账号的实际权限。它会列出所有 yes no 的结果,比凭空想象可靠一万倍。

3.2 Pod 的“身份证”:SecurityContext 的七项硬性约束

一个 Pod 的 securityContext ,就是它的“宪法”。它规定了这个进程在操作系统层面能做什么。在 DOKS 上,我们强制所有生产环境的 Deployment 必须包含以下七项约束,缺一不可:

  1. runAsNonRoot: true :这是底线。它强制 Pod 内的主进程不能以 UID 0(root)启动。如果镜像的 Dockerfile 里写了 USER root ,这个 Pod 就会启动失败。这迫使你去修改镜像,用 USER 1001 这样的非特权用户。这是防御“容器逃逸”(Container Escape)的第一道物理屏障。

  2. runAsUser: 1001 :明确指定 UID。不能只写 runAsNonRoot: true 就完事。因为有些镜像会以 UID 65534(nobody)启动,而 nobody 在某些发行版里可能被赋予了意外的组权限。固定一个 UID(如 1001),并在你的应用 Dockerfile 中 useradd -u 1001 appuser ,能确保行为可预测。

  3. runAsGroup: 1001 :同理,固定 GID。这能防止应用因组权限混乱而无法读写挂载的卷(Volume)。

  4. fsGroup: 1001 :这个字段常被忽略,但它至关重要。它告诉 kubelet,当一个 PersistentVolumeClaim (PVC)被挂载进 Pod 时,kubelet 应该递归地将该卷内所有文件的 group ID 修改为 1001 ,并赋予 g+rwx 权限。这样,你的应用进程(UID 1001, GID 1001)就能顺利读写卷里的文件,而无需在容器启动脚本里写 chown -R 1001:1001 /data 这种危险操作。

  5. readOnlyRootFilesystem: true :将容器的根文件系统设为只读。这意味着 /bin /usr /lib 等目录无法被写入。这能有效阻止恶意软件在运行时向 /bin/sh 注入后门,或篡改系统二进制文件。当然,你需要确保应用的所有写操作都指向 /tmp 或一个显式声明的 emptyDir volumeMount

  6. allowPrivilegeEscalation: false :这是 runAsNonRoot 的加强版。它禁止进程通过 setuid setgid 二进制文件(如 /usr/bin/ping )来提升自身权限。即使你的镜像里不小心包含了 ping ,这个设置也能让它失效。

  7. capabilities.drop: ["ALL"] :Linux Capabilities 是对 root 权限的精细化拆分。 ALL 表示丢弃所有能力。一个典型的、安全的最小集合是:

    capabilities:
      drop:
      - ALL
      add:
      - NET_BIND_SERVICE # 如果你的应用需要监听 80/443 端口
    

这七项加起来,就是一个 Pod 的“安全基线”。我们把它封装成一个 kustomize bases 目录,所有新项目都 kustomize build 这个基线,再叠加自己的业务配置。这确保了安全不是靠人肉记忆,而是靠工程化流程。

3.3 服务间通信:NetworkPolicy 的“默认拒绝”哲学

在 DOKS 上, calico 默认是启用的,但它的默认行为是“允许所有”。这就像一栋大楼,所有房间的门都是敞开的。 NetworkPolicy 就是你的门禁系统。

我们的实践是: 每个命名空间(Namespace)创建之初,第一件事就是部署一条 default-deny 策略

# default-deny.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
  namespace: my-app
spec:
  podSelector: {} # 选择该命名空间下的所有 Pod
  policyTypes:
  - Ingress
  - Egress

这条策略本身不定义任何允许规则,它只做一件事: 拒绝所有入站(Ingress)和出站(Egress)流量 。它像一张白纸,你必须在上面亲手画出每一条“允许”的线。

然后,你才开始添加具体的策略。例如, frontend 命名空间的 Pod 只能访问 backend 命名空间的 8080 端口:

# frontend-to-backend.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: frontend-to-backend
  namespace: frontend
spec:
  podSelector:
    matchLabels:
      app: frontend
  policyTypes:
  - Egress
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          name: backend
      podSelector:
        matchLabels:
          app: backend
    ports:
    - protocol: TCP
      port: 8080

这个过程强迫你去思考:“我的服务到底需要和谁通信?需要什么协议?需要哪个端口?”而不是“反正都开着,有需要再说”。我们曾帮一家客户审计,发现他们的 monitoring 命名空间里的 Prometheus,竟然能 egress 到互联网上的任意 IP 和端口。这完全是不必要的风险,一条 NetworkPolicy 就能把它锁死在 metrics 命名空间内部。

4. 零信任(Zero Trust)不是玄学:在 DOKS 上构建“永不信任,始终验证”的数据流

“Zero Trust”这个词被用滥了,但在 Kubernetes 的语境下,它有一个极其具体、可落地的含义: 任何两个 Pod 之间的通信,都不能基于“它们在同一个集群里”这个事实而被默认信任。每一次连接,都必须经过显式的、可审计的身份认证和授权。 在 DOKS 上,这主要通过 Service Mesh(服务网格)来实现,而 Istio 是目前最成熟的选择。

4.1 为什么是 Istio?为什么不是 Linkerd 或 Consul?

初创团队常纠结于选型。Linkerd 更轻量,Consul 更通用。但我们坚定推荐 Istio,原因有三:

  • 与 DOKS 的兼容性经过充分验证 :DigitalOcean 官方文档和 GitHub 示例仓库中,Istio 是唯一被完整演示的 Service Mesh。它的 istioctl install 命令能完美适配 DOKS 的 containerd 运行时和 calico CNI,几乎零配置即可完成安装。而 Linkerd 的 linkerd check 在某些 DOKS 版本上会报 CNI plugin not found 的警告,需要额外调试。

  • mTLS(双向 TLS)是开箱即用的默认行为 :Istio 的 istiod 控制平面,会自动为每个注入了 Sidecar( istio-proxy )的 Pod 生成短期证书(默认 24 小时有效期),并强制所有 Pod 间的通信使用 mTLS 加密。这意味着, frontend Pod 发送给 backend Pod 的 HTTP 请求,在网络层面上是被加密的 TLS 流量,中间人(Man-in-the-Middle)无法窃听或篡改。这解决了“服务间通信明文传输”的老大难问题。

  • 细粒度的授权策略(AuthorizationPolicy) :Istio 的 AuthorizationPolicy ,比原生 Kubernetes 的 NetworkPolicy 更进一步。 NetworkPolicy 只能控制“IP+端口”,而 AuthorizationPolicy 可以控制“HTTP 方法+路径+Header+JWT Claim”。例如,你可以写一条策略:

    apiVersion: security.istio.io/v1beta1
    kind: AuthorizationPolicy
    metadata:
      name: api-read-only
      namespace: backend
    spec:
      selector:
        matchLabels:
          app: api-server
      rules:
      - from:
        - source:
            principals: ["cluster.local/ns/frontend/sa/frontend-sa"]
        to:
        - operation:
            methods: ["GET", "HEAD"]
            paths: ["/v1/users/*"]
    

    这条策略的意思是:“只有 frontend 命名空间里,名为 frontend-sa 的 ServiceAccount 启动的 Pod,才能以 GET HEAD 方法,访问 /v1/users/* 这个路径”。它把权限控制从网络层,精准地下沉到了应用层。

4.2 Istio 的“渐进式”落地:从自动注入到全链路追踪

在资源紧张的初创团队,我们反对一次性全量部署 Istio。我们采用三步走的渐进策略:

第一步:启用 Sidecar 自动注入(Auto-Injection)

在目标命名空间(如 production )上打一个标签:

kubectl label namespace production istio-injection=enabled

然后,所有新创建的 Pod,都会被 istiod 自动注入一个 istio-proxy 容器。这个 istio-proxy 是一个轻量级的 Envoy 代理,它会劫持 Pod 的所有入站(Inbound)和出站(Outbound)流量。此时,mTLS 已经在后台静默工作,但你的应用代码完全无感,零改造。

第二步:定义 PeerAuthentication 策略,强制 mTLS

创建一个 PeerAuthentication 资源,作用域为整个集群:

apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: istio-system
spec:
  mtls:
    mode: STRICT # 强制所有流量必须使用 mTLS

这条策略生效后,任何未注入 Sidecar 的 Pod,将无法与注入了 Sidecar 的 Pod 通信。这会立刻暴露出那些“裸奔”的、未被网格化的服务,逼着你去处理它们。

第三步:引入 RequestAuthentication AuthorizationPolicy

当 mTLS 稳定运行一周后,再引入应用层的认证与授权。 RequestAuthentication 用于验证 JWT(JSON Web Token)令牌,通常由你的 API Gateway(如 Kong 或 Traefik)在入口处签发。 AuthorizationPolicy 则基于 JWT 中的 sub (Subject)或 groups 字段,决定该请求是否有权访问后端服务。

这个过程会产生大量可观测性数据。 istioctl dashboard kiali 会为你展示一张实时的服务拓扑图,上面清晰地标出了每个服务的请求成功率、延迟、错误率。你会发现,一个看似健康的 GET /healthz 接口,其成功率可能只有 99.2%,背后是 0.8% 的超时——这往往是网络抖动或下游服务过载的早期信号。这种“全链路追踪”能力,是 Zero Trust 的眼睛和耳朵。

提示:Istio 的资源对象( VirtualService , DestinationRule , Gateway )本身也是 Kubernetes 的 CRD(Custom Resource Definition)。这意味着,你可以像管理 Deployment 一样,用 GitOps(如 Argo CD)来管理它们。把所有的 Istio 配置都放进 Git 仓库,每次 kubectl apply 都是一次可追溯、可回滚的变更。这是 Zero Trust 文化在工程实践上的终极体现:一切皆代码,一切皆可审计。

5. 秘密(Secret)管理:告别 kubectl create secret generic 的原始时代

在 DOKS 上, kubectl create secret generic my-db-secret --from-literal=username=admin --from-literal=password=123456 是最便捷的方式,也是最危险的习惯。它把明文密码塞进了 etcd,而 etcd 的加密密钥由 DigitalOcean 托管,你无法控制其轮换周期。更糟的是,这个 Secret 一旦被创建,就永远躺在那里,直到你手动 delete 它。而现实中,密码轮换是常态。

5.1 Secret 的生命周期:从“创建”到“销毁”的四个阶段

一个安全的 Secret 管理流程,必须覆盖其完整的生命周期:

  1. 生成(Generation) :密码不应由人手动生成。应使用 openssl rand -base64 32 或 HashiCorp Vault 的 kv 引擎来生成高强度、随机的密钥。DOKS 本身不提供密钥生成服务,所以你需要一个外部的、可信的密钥管理服务(KMS)。

  2. 存储(Storage) :生成的密钥,不应以明文形式存入 Git 仓库。应将其存入 Vault 的 kv-v2 路径下,例如 secret/data/myapp/db-credentials 。Vault 会对它进行二次加密,并记录每一次读取的日志。

  3. 注入(Injection) :当你的应用 Pod 启动时,它需要从 Vault 获取密钥。这里有两种主流模式:

    • Sidecar 模式(Vault Agent Injector) :在 Pod 的 YAML 中,添加一个 vault.hashicorp.com/agent-inject: 'true' 的 annotation。Vault Agent Injector 会自动为 Pod 注入一个 vault-agent 容器,并将密钥以文件形式挂载到 /vault/secrets/ 下。你的应用只需读取这个文件。
    • Init Container 模式(Vault Agent) :在 Pod 的 initContainers 中,启动一个 vault-agent ,让它先从 Vault 拉取密钥,写入一个 emptyDir 卷,然后主容器再从这个卷里读取。这种方式更轻量,但需要你管理 Init Container 的镜像和配置。
  4. 轮换(Rotation) :这是最常被忽视的一环。Vault 的 kv-v2 引擎支持版本化。当你在 Vault 中更新 secret/data/myapp/db-credentials 时,它会创建一个新版本(v2),而旧版本(v1)依然存在。你的应用 Pod 不会自动感知到新版本。你需要触发一次滚动更新(Rolling Update),让新的 Pod 启动时拉取 v2 的密钥。这可以通过一个简单的 kubectl rollout restart deployment/my-app 来完成,前提是你的 Deployment 的 spec.template.spec.containers 中, env volumeMounts 的配置,是动态绑定到 Vault 的路径的。

5.2 一个可落地的 Vault + DOKS 集成方案

我们为一家客户搭建的方案,只用了 3 个核心组件:

  • HashiCorp Vault(托管在 DigitalOcean Droplet 上) :我们选择在一台独立的、最小规格($5/month)的 Ubuntu Droplet 上安装 Vault。它与你的 DOKS 集群在同一个 VPC 内,通过内网通信,延迟极低。我们禁用了 Vault 的 UI,只通过 vault CLI 和 API 进行管理。

  • Vault Agent Injector(作为 Kubernetes DaemonSet 部署) :这个组件监听 Kubernetes API,当它发现一个带有 vault.hashicorp.com/agent-inject: 'true' annotation 的 Pod 创建请求时,它会拦截该请求,修改 Pod 的 YAML,注入 vault-agent 容器和相应的 volumeMounts

  • 一个标准化的 Helm Chart( myapp-chart :这个 Chart 的 values.yaml 中,有一个 vault 区块:

    vault:
      enabled: true
      address: "http://vault.vault.svc.cluster.local:8200" # Vault Service 的 ClusterIP
      role: "myapp-role" # Vault 中为该应用定义的 Role
      secrets:
      - path: "secret/data/myapp/db-credentials"
        type: "kv-v2"
        keys: ["username", "password"]
    

    当你执行 helm install myapp ./myapp-chart --set vault.enabled=true 时,Helm 会根据 values.yaml ,自动生成带有正确 annotation 和 volumeMount 的 Deployment。

这个方案的好处是: 密钥的生命周期完全脱离了 Kubernetes 的 Secret 对象 。它不再是一个静态的、可能被 kubectl get secret -o yaml 导出的资源,而是一个动态的、按需拉取的、有审计日志的、可版本化的凭证。它把“秘密管理”从一个运维操作,变成了一个标准的、可复用的、可测试的软件交付环节。

6. 持续验证:建立你的 DOKS 安全健康度仪表盘

安全不是一次性的“加固项目”,而是一个持续的、数据驱动的“健康监测”过程。在 DOKS 上,你需要一个仪表盘,它能告诉你:今天,我的集群比昨天更安全,还是更脆弱?

6.1 三个必建的、自动化的安全检查脚本

我们为所有客户部署了三个核心脚本,它们每天凌晨 2 点,通过 CronJob 在集群内自动运行,并将结果推送到 Slack 频道:

脚本一: check-pod-security.sh —— 扫描所有 Pod 的 SecurityContext

#!/bin/bash
# 这个脚本会遍历所有命名空间下的所有 Pod
# 报告任何违反“七项硬性约束”的实例
echo "=== POD SECURITY CHECK ==="
for ns in $(kubectl get namespaces -o jsonpath='{.items[*].metadata.name}'); do
  echo "Checking namespace: $ns"
  # 检查 runAsNonRoot
  kubectl get pods -n $ns -o json | jq -r '.items[] | select(.spec.securityContext.runAsNonRoot != true) | .metadata.name' | while read pod; do
    echo "❌ [${ns}] Pod ${pod} missing runAsNonRoot: true"
  done
  # 检查 allowPrivilegeEscalation
  kubectl get pods -n $ns -o json | jq -r '.items[] | select(.spec.securityContext.allowPrivilegeEscalation == true) | .metadata.name' | while read pod; do
    echo "❌ [${ns}] Pod ${pod} has allowPrivilegeEscalation: true"
  done
done

脚本二: check-network-policy.sh —— 验证 NetworkPolicy 的覆盖率

#!/bin/bash
echo "=== NETWORK POLICY COVERAGE CHECK ==="
for ns in $(kubectl get namespaces -o jsonpath='{.items[*].metadata.name}'); do
  # 检查该命名空间下是否存在 default-deny 策略
  if ! kubectl get networkpolicy -n $ns default-deny >/dev/null 2>&1; then
    echo "⚠️  [${ns}] Missing default-deny NetworkPolicy!"
  fi
  # 检查该命名空间下是否有 Pod 没有被任何 NetworkPolicy 选中
  # (即,没有任何 NetworkPolicy 的 podSelector 能匹配到它)
  for pod in $(kubectl get pods -n $ns -o jsonpath='{.items[*].metadata.name}'); do
    # 这里用一个简化的逻辑:如果该 Pod 有 label,且没有 NetworkPolicy 的 podSelector 匹配它,则报警
    # 实际生产中,我们会用更精确的匹配逻辑
    labels=$(kubectl get pod -n $ns $pod -o jsonpath='{.metadata.labels}')
    if [[ -z "$labels" ]]; then
      echo "⚠️  [${ns}] Pod ${pod} has no labels, cannot be targeted by NetworkPolicy"
    fi
  done
done

脚本三: check-secret-rotation.sh —— 审计 Secret 的年龄

#!/bin/bash
echo "=== SECRET ROTATION AUDIT ==="
# 列出所有 Secret,并计算其创建时间距今多少天
kubectl get secrets --all-namespaces -o json | \
  jq -r '.items[] | select(.metadata.creationTimestamp != null) | 
         "\(.metadata.namespace)/\(.metadata.name) \(.metadata.creationTimestamp)"' | \
  while read line; do
    ns_name=$(echo $line | awk '{print $1}')
    created=$(echo $line | awk '{print $2}')
    # 将 ISO8601 时间戳转换为 Unix 时间戳,并计算天数差
    age_days=$(( ($(date -d "$created" +%s 2>/dev/null) - $(date -d "now" +%s)) / 86400 ))
    if [[ $age_days -gt 90 ]]; then
      echo "❌ [${ns_name}] Secret is ${age_days} days old (>90 days)"
    fi
  done

这三个脚本,构成了你的安全基线。它们不追求“100% 安全”,而是追求“100% 可见”。当某天 Slack 里弹出一条 ❌ [production] Pod api-server-7b8c9d0e-fgh12 has allowPrivilegeEscalation: true 的消息时,它不是一个故障告警,而是一个明确的、可立即行动的工单。它告诉你,某个新上线的、未经安全审查的 Helm Chart,绕过了你们的 CI/CD 流水线中的安全检查。

6.2 将安全指标融入你的 SLO(服务等级目标)

最后,把安全从一个“后台运维事项”,提升为一个“前台业务指标”。我们建议,将以下两个指标,写入你的工程团队的季度 OKR(Objectives and Key Results)中:

  • **Key Result 1:所有生产命名空间的 default-deny NetworkPolicy 覆盖

更多推荐