保护 Kubernetes 中的生产环境调试
在生产环境调试期间,最快的途径通常是广泛的访问权限,
例如 cluster-admin(授予管理员级别访问权限的 ClusterRole)、共享的堡垒机/跳板机,
或者长期存在的 SSH 密钥。这种方法在当时有效,
但存在两个常见问题:审计变得困难,临时例外往往成为惯例。
本文提供了我的建议,适用于现有 Kubernetes 环境的良好实践,只需最小的工具变更:
- 使用 RBAC 实现最小权限原则
- 短期、身份绑定的凭证
- 用于云原生调试的 SSH 风格握手模型
保护生产调试工作流程的良好架构是使用即时安全 shell 网关(通常部署为集群中的按需 Pod)。
它充当 SSH 风格的“前门”,使临时访问真正是临时的。
你可以使用短期、身份绑定的凭证进行身份验证,建立到网关的会话,
网关使用 Kubernetes API 和 RBAC 控制他们可以做什么,
例如 pods/log、pods/exec 和 pods/portforward。
会话会自动过期,网关日志和 Kubernetes 审计日志都会记录谁在何时访问了什么,
而无需共享堡垒机账户或长期存在的密钥。
1) 在 Kubernetes RBAC 之上使用访问代理
RBAC 控制谁可以在 Kubernetes 中做什么。 尽管 Kubernetes 还支持其他授权模式(如 Webhook 授权), 但许多 Kubernetes 环境主要依赖 RBAC 进行授权。 你可以直接使用 Kubernetes RBAC 强制执行访问控制, 或者在集群前面放置一个访问代理,该代理在幕后仍然依赖 Kubernetes 权限。 在任一模型中,Kubernetes RBAC 仍然是 Kubernetes API 允许什么以及在什么范围的事实来源。
访问代理添加了 RBAC 不能很好覆盖的控制。
例如,它可以决定请求是自动批准还是需要手动批准,用户是否可以运行命令,
以及会话中允许哪些命令。它还可以管理组成员身份,以便你向组而不是单个用户授予权限。
Kubernetes RBAC 可以允许诸如 pods/exec 之类的操作,
但它不能限制在 exec 会话中运行哪些命令。
通过该模型,Kubernetes RBAC 定义了用户或组(例如单个命名空间中的值班团队)允许的操作。 我建议你只定义向组或 ServiceAccount 授予权限的访问规则——永远不要授予单个用户。 然后,代理或身份提供商根据需要向该组添加或删除用户。
代理还可以在上面强制执行额外的策略,例如在交互式会话中允许哪些命令, 以及哪些请求可以自动批准而哪些需要手动批准。 该策略可以保存在 JSON 或 XML 文件中,并通过代码审查进行维护, 因此更新会通过正式的拉取请求进行,并像任何其他生产变更一样接受审查。
示例:命名空间范围的值班调试 Role
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: oncall-debug
namespace: <namespace>
rules:
# 运行的内容
- apiGroups: [""]
resources: ["pods", "events"]
verbs: ["get", "list", "watch"]
# 读取日志
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get"]
# 交互式调试操作
- apiGroups: [""]
resources: ["pods/exec", "pods/portforward"]
verbs: ["create"]
# 了解滚动控制器状态
- apiGroups: ["apps"]
resources: ["deployments", "replicasets"]
verbs: ["get", "list", "watch"]
# 允许 kubectl 允许调试临时容器
- apiGroups: [""]
resources: ["pods/ephemeralcontainers"]
verbs: ["update"]
将该 Role 绑定到一个组(而不是单个用户),以便可以通过身份提供商管理成员身份:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: oncall-debug
namespace: <namespace>
subjects:
- kind: Group
name: oncall-<team-name>
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: oncall-debug
apiGroup: rbac.authorization.k8s.io
2) 短期、身份绑定的凭证
目标是使用短期、身份绑定的凭证,这些凭证将会话与真实人物明确绑定并快速过期。 这些凭证可以包含用户的身份以及他们被允许执行的范围。 它们通常使用保存在工程师那里的私钥签名,例如硬件支持的密钥(例如 YubiKey), 因此没有访问该密钥的权限就无法伪造它们。
你可以使用 Kubernetes 原生身份验证(例如客户端证书或基于 OIDC 的流程)实现这一点, 或者让前一节的访问代理代表用户颁发短期凭证。 在许多设置中,Kubernetes 仍然使用 RBAC 基于已验证身份和组/声明来强制执行权限。 如果使用访问代理,它还可以在凭证中编码额外的范围约束,并在会话期间强制执行这些约束, 例如会话适用于哪个集群或命名空间,以及允许对 pod 或节点执行哪些操作(或批准的命令)。 在任一情况下,凭证都应由证书颁发机构(CA)签名,并且该 CA 应定期轮换(例如每季度)以限制长期风险。
选项 A:短期 OIDC 令牌
许多托管 Kubernetes 集群已经提供短期令牌。 主要是确保你的 kubeconfig 自动刷新它们,而不是将长期令牌复制到文件中。
例如:
users:
- name: oncall
user:
exec:
apiVersion: client.authentication.k8s.io/v1
command: cred-helper
args: ["--cluster=prod", "--ttl=30m"]
选项 B:短期客户端证书(X.509)
如果你的 API 服务器(或前一节的访问代理)被设置为信任客户端 CA, 你可以使用短期客户端证书进行调试访问。这个想法是:
- 私钥在工程师的机器上创建并保留(理想情况下是硬件支持的,如 YubiKey/PIV 令牌中不可导出的密钥)
- 颁发短期证书(通常通过 CertificateSigningRequest API, 或前一节的访问代理,具有 TTL)。
- RBAC 将已验证的身份映射到最小化 Role
使用 Kubernetes CertificateSigningRequest API 可以直接实现这一点。
本地生成密钥和 CSR:
# 生成私钥。
# 也可以在硬件令牌中生成私钥;
# OpenSSL 和一些类似的工具都支持这种方式。
openssl genpkey -algorithm Ed25519 -out oncall.key
openssl req -new -key oncall.key -out oncall.csr \
-subj "/CN=user/O=oncall-payments"
创建一个短期过期的 CertificateSigningRequest:
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
name: oncall-<user>-20260218
spec:
request: <base64-encoded oncall.csr>
signerName: kubernetes.io/kube-apiserver-client
expirationSeconds: 1800 # 30 分钟
usages:
- client auth
CSR 被批准并签名后,提取颁发的证书并将其与私钥一起使用进行身份验证, 例如通过 kubectl。
3) 使用即时访问网关运行调试命令
一旦你有了短期凭证,就可以使用它们打开到即时访问网关的安全 shell 会话, 该网关通常通过 SSH 暴露并按需创建。如果网关通过 SSH 暴露, 常见模式是为工程师颁发会话的短期 OpenSSH 用户证书。 网关信任你的 SSH 用户 CA,在连接时对工程师进行身份验证, 然后在代表用户进行 Kubernetes API 调用之前应用批准的会话策略。 OpenSSH 证书与 Kubernetes X.509 客户端证书是分开的,因此这些通常被视为独立的层。
生成的会话也应该有范围限制,以便无法在批准的范围之外重用它。 例如,网关或代理可以将其限制为特定集群和命名空间,还可以限制为更窄的目标, 如 Pod 或节点。这样,即使有人尝试重用访问权限,它也不会在预期范围之外工作。 会话建立后,网关只执行允许的操作并记录发生的事情用于审计。
示例:命名空间范围的角色绑定
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: jit-debug
namespace: <namespace>
annotations:
kubernetes.io/description: >
Colleagues performing semi-privileged debugging, with access provided
just in time and on demand.
rules:
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: jit-debug
namespace: <namespace>
subjects:
- kind: Group
name: jit:oncall:<namespace> # 从短期凭证 (cert/OIDC) 映射而来
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: jit-debug
apiGroup: rbac.authorization.k8s.io
这些 RBAC 对象以及它们定义的规则只允许在指定的命名空间内进行调试;不允许尝试访问其他命名空间。
示例:集群范围的角色绑定
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: jit-cluster-read
rules:
- apiGroups: [""]
resources: ["nodes", "namespaces"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: jit-cluster-read
subjects:
- kind: Group
name: jit:oncall:cluster
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: jit-cluster-read
apiGroup: rbac.authorization.k8s.io
这些 RBAC 规则授予集群范围的读访问权限(例如,对节点和命名空间), 并且只应该用于真正需要集群范围资源的工作流程。
更细粒度的限制,例如“仅这个 pod/node ”或“仅这些命令”, 通常由访问网关/代理在会话期间强制执行,但 Kubernetes 也提供其他选项, 例如用于限制写入的 ValidatingAdmissionPolicy 和用于跨动词的自定义授权的 Webhook 授权。
在具有更严格访问控制的环境中,你可以添加一个额外的短期会话中介层, 将会话建立与特权操作分开。两个层都是临时的,使用身份绑定的过期凭证, 并产生独立的审计轨迹。中介层处理会话设置/转发,而执行层仅执行 RBAC 授权的 Kubernetes 操作。 这种分离可以通过缩小职责范围、为每个步骤限定凭证范围以及强制执行端到端会话过期来减少暴露。
参考资料
免责声明:本文表达的观点仅代表作者个人,不代表作者雇主或任何其他组织的观点。