flawopen.com/Teardowns/cve-2024-3177-kubernetes-serviceaccount-envfrom-secrets-bypass

● CVE-2024-3177 · CVSS 2.7 · Baja
Investigación · FlawOpen

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.

💡 Explicación en Lenguaje Sencillo (ELI5)

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, en limitEphemeralContainerSecretReferences().

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

Paso 1

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.

Paso 2

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.

Paso 3

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.

Paso 4

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

IMPLEMENTACIÓN VULNERABLE
// 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
}
PARCHE SEGURO Y ROBUSTO
// 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

Fuentes