flawopen.com/Teardowns/cve-2024-3177-kubernetes-serviceaccount-envfrom-secrets-bypass
CVE-2024-3177:Kubernetes ServiceAccount 准入插件 envFrom 绕过 Secret 限制
CVE-2024-3177(CVSS 2.7,低危):kube-apiserver 的 ServiceAccount 准入插件会用服务账号的允许列表检查 Secret 卷和 env valueFrom 引用,却从不检查 envFrom,因此 Pod 可以加载所在命名空间中的任意 Secret。
酒店给每位保洁员一张允许进入的房间清单。员工通道的保安会拿清单核对钥匙圈上的每一把钥匙,以及挂在腰带上的每一把单独的钥匙。但没人打开过那只装着整层楼全部钥匙的拉链小包。一个只被允许进大堂的保洁员拎着行政楼层的钥匙包走进去,保安照样放行,因为他的检查清单里根本没有“钥匙包”这一项。
核心概念与专有名词
ServiceAccount admission plugin- kube-apiserver 内置的准入控制器,负责为 Pod 填入服务账号,并可按要求限制 Pod 能引用哪些 Secret(
plugin/pkg/admission/serviceaccount)。 kubernetes.io/enforce-mountable-secrets- ServiceAccount 上的注解。设为 "true" 时,以该账号运行的 Pod 只能引用账号
secrets字段中列出的 Secret。 envFrom / secretRef- 容器字段,一次性把某个 Secret 的所有键导入为环境变量;而
env[].valueFrom.secretKeyRef只导入单个键。 临时容器- 通过
pods/ephemeralcontainers子资源加入运行中 Pod 的调试容器,由limitEphemeralContainerSecretReferences()单独做准入检查。
根本原因剖析 (Root Cause)
plugin/pkg/admission/serviceaccount/admission.go 中的 limitSecretReferences() 通过遍历 Pod 的 Secret 卷和每个容器的 env[].valueFrom.secretKeyRef 来执行可挂载 Secret 允许列表,却从不遍历会导入整个 Secret 的 envFrom[].secretRef。init 容器,以及 limitEphemeralContainerSecretReferences() 中的临时容器,也存在同样的缺口。PR #124322 在这三处补上了缺失的 envFrom 循环。
攻击执行流程分解
受限的服务账号
ServiceAccount builder 带有 kubernetes.io/enforce-mountable-secrets: "true",其 secrets 中只列出 build-token。同一命名空间里还有 prod-db-credentials。
带 envFrom 的 Pod 规约
有权创建 Pod 的用户提交一个以 builder 运行的 Pod,并在普通容器、init 容器或临时容器上设置 envFrom: [{secretRef: {name: prod-db-credentials}}]。
准入检查通过
limitSecretReferences() 没有发现允许列表之外的 Secret 卷或 env[].valueFrom.secretKeyRef,于是放行该 Pod。
kubelet 注入 Secret
kubelet 解析 envFrom,把 prod-db-credentials 的每个键都设为环境变量,容器即可读取。
源代码对比:漏洞与安全实现
// plugin/pkg/admission/serviceaccount/admission.go (kube-apiserver v1.29.3)
func (s *Plugin) limitSecretReferences(serviceAccount *corev1.ServiceAccount, pod *api.Pod) error {
// Only allow Secrets that the service account lists in its "secrets" field.
mountableSecrets := sets.NewString()
for _, ref := range serviceAccount.Secrets {
mountableSecrets.Insert(ref.Name)
}
for _, volume := range pod.Spec.Volumes {
source := volume.VolumeSource
if source.Secret != nil && !mountableSecrets.Has(source.Secret.SecretName) {
return fmt.Errorf("volume with secret.secretName=%q is not allowed because service account %s does not reference that secret", source.Secret.SecretName, serviceAccount.Name)
}
}
for _, container := range pod.Spec.Containers {
for _, env := range container.Env {
if env.ValueFrom != nil && env.ValueFrom.SecretKeyRef != nil {
if !mountableSecrets.Has(env.ValueFrom.SecretKeyRef.Name) {
return fmt.Errorf("container %s with envVar %s referencing secret.secretName=%q is not allowed because service account %s does not reference that secret", container.Name, env.Name, env.ValueFrom.SecretKeyRef.Name, serviceAccount.Name)
}
}
}
// BUG: container.EnvFrom is never inspected. envFrom[].secretRef imports
// every key of any Secret in the namespace and still passes admission.
}
return nil
}
// plugin/pkg/admission/serviceaccount/admission.go (fixed in v1.29.4, PR #124322)
func (s *Plugin) limitSecretReferences(serviceAccount *corev1.ServiceAccount, pod *api.Pod) error {
// Only allow Secrets that the service account lists in its "secrets" field.
mountableSecrets := sets.NewString()
for _, ref := range serviceAccount.Secrets {
mountableSecrets.Insert(ref.Name)
}
for _, volume := range pod.Spec.Volumes {
source := volume.VolumeSource
if source.Secret != nil && !mountableSecrets.Has(source.Secret.SecretName) {
return fmt.Errorf("volume with secret.secretName=%q is not allowed because service account %s does not reference that secret", source.Secret.SecretName, serviceAccount.Name)
}
}
for _, container := range pod.Spec.Containers {
for _, env := range container.Env {
if env.ValueFrom != nil && env.ValueFrom.SecretKeyRef != nil {
if !mountableSecrets.Has(env.ValueFrom.SecretKeyRef.Name) {
return fmt.Errorf("container %s with envVar %s referencing secret.secretName=%q is not allowed because service account %s does not reference that secret", container.Name, env.Name, env.ValueFrom.SecretKeyRef.Name, serviceAccount.Name)
}
}
}
// FIX: envFrom can import a whole Secret, so it gets the same allow-list check.
// The patch adds this loop for init and ephemeral containers too.
for _, envFrom := range container.EnvFrom {
if envFrom.SecretRef != nil && !mountableSecrets.Has(envFrom.SecretRef.Name) {
return fmt.Errorf("container %s with envFrom referencing secret.secretName=%q is not allowed because service account %s does not reference that secret", container.Name, envFrom.SecretRef.Name, serviceAccount.Name)
}
}
}
return nil
}
工程与系统安全加固清单
- ✓将 kube-apiserver 升级到 v1.29.4、v1.28.9、v1.27.13 或更高版本;没有可替代的配置缓解手段。
- ✓找出依赖该策略的服务账号:用
kubectl get serviceaccounts -A并按注解kubernetes.io/enforce-mountable-过滤。secrets=true - ✓在 API 审计日志中查找 Pod 创建和临时容器更新请求,其
envFrom[].secretRef.name不在该 Pod 服务账号的secrets列表中。 - ✓把在某命名空间创建 Pod 的权限视为读取该命名空间 Secret 的权限;敏感 Secret 只放在其 Pod 创建者本就可信的命名空间中。
- ✓准入检查限制某类资源时,列举 Pod 规约中所有可能引用它的字段(卷、投射卷、
env、envFrom、init 容器和临时容器),并为每个字段各写一个测试。