flawopen.com/Teardowns/cve-2024-3177-kubernetes-serviceaccount-envfrom-secrets-bypass
CVE-2024-3177: evasión de Secrets mediante envFrom en la admisión de ServiceAccount de Kubernetes
CVE-2024-3177 (CVSS 2.7, baja): el plugin de admisión ServiceAccount de kube-apiserver comprobaba los volúmenes de Secret y las referencias env valueFrom contra la lista permitida de la cuenta de servicio, pero nunca miraba envFrom, así que un pod podía cargar cualquier Secret de su namespace.
Un hotel da a cada persona de limpieza la lista de habitaciones en las que puede entrar. El guardia de la puerta de personal compara con esa lista cada llave del llavero y cada llave suelta enganchada al cinturón. Nadie abre nunca el estuche con cremallera que guarda todas las llaves de una planta a la vez. Alguien autorizado solo para el vestíbulo entra con el estuche de la planta ejecutiva y el guardia le deja pasar, porque su lista de control nunca menciona estuches.
Conceptos Clave y Términos
ServiceAccount admission plugin- Controlador de admisión integrado en kube-apiserver que asigna la cuenta de servicio del pod y, si se le pide, limita qué Secrets puede referenciar el pod (
plugin/pkg/admission/serviceaccount). kubernetes.io/enforce-mountable-secrets- Anotación de una ServiceAccount. Con el valor "true", los pods que se ejecutan con esa cuenta solo pueden referenciar los Secrets listados en su campo
secrets. envFrom / secretRef- Campo del contenedor que importa todas las claves de un Secret como variables de entorno de una vez, a diferencia de
env[].valueFrom.secretKeyRef, que importa una sola clave. Contenedor efímero- Contenedor de depuración que se añade a un pod en ejecución mediante el subrecurso
pods/ephemeralcontainers. Se admite por separado, enlimitEphemeralContainerSecretReferences().
Análisis de Causa Raíz
limitSecretReferences() en plugin/pkg/admission/serviceaccount/admission.go aplicaba la lista de Secrets montables recorriendo los volúmenes de Secret del pod y el env[].valueFrom.secretKeyRef de cada contenedor. Nunca recorría envFrom[].secretRef, que importa un Secret completo. El mismo hueco existía para los contenedores init y, en limitEphemeralContainerSecretReferences(), para los contenedores efímeros. El PR #124322 añadió el bucle envFrom que faltaba en los tres sitios.
Flujo de Ataque Paso a Paso
Una cuenta de servicio restringida
La ServiceAccount builder lleva kubernetes.io/enforce-mountable-secrets: "true" y solo lista build-token en secrets. El mismo namespace contiene también prod-db-credentials.
Especificación de pod con envFrom
Un usuario que puede crear pods envía uno que se ejecuta como builder y define envFrom: [{secretRef: {name: prod-db-credentials}}] en un contenedor, contenedor init o contenedor efímero.
La comprobación de admisión pasa
limitSecretReferences() no encuentra ningún volumen de Secret ni ningún env[].valueFrom.secretKeyRef fuera de la lista permitida, así que admite el pod.
El kubelet inyecta el Secret
El kubelet resuelve envFrom y define cada clave de prod-db-credentials como variable de entorno, donde el contenedor puede leerla.
Código Fuente: Vulnerable vs. Seguro
// 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
}
Lista de Verificación de Seguridad para Ingeniería
- ✓Actualice kube-apiserver a v1.29.4, v1.28.9, v1.27.13 o posterior; no hay solución alternativa por configuración.
- ✓Localice las cuentas de servicio que dependen de la política:
kubectl get serviceaccounts -Afiltrado por la anotaciónkubernetes.io/enforce-mountable-.secrets=true - ✓Busque en los registros de auditoría de la API creaciones de pods y actualizaciones de contenedores efímeros cuyo
envFrom[].secretRef.nameno figure en la listasecretsde la cuenta de servicio del pod. - ✓Trate el permiso para crear pods en un namespace como acceso de lectura a los Secrets de ese namespace; guarde los Secrets sensibles en namespaces cuyos creadores de pods ya sean de confianza para ellos.
- ✓Cuando una comprobación de admisión restrinja un tipo de recurso, enumere cada campo de la especificación del pod que pueda referenciarlo (volúmenes, volúmenes proyectados,
env,envFrom, contenedores init y efímeros) y escriba una prueba por campo.