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

● CVE-2024-3177 · CVSS 2.7 · Basse
Recherche · FlawOpen

CVE-2024-3177 : contournement des Secrets via envFrom dans l'admission ServiceAccount de Kubernetes

CVE-2024-3177 (CVSS 2.7, faible) : le plugin d'admission ServiceAccount de kube-apiserver vérifiait les volumes de Secret et les références env valueFrom par rapport à la liste autorisée du compte de service, mais ignorait envFrom, si bien qu'un pod pouvait charger n'importe quel Secret de son namespace.

💡 Explication en Langage Simple (ELI5)

Un hôtel remet à chaque agent d'entretien la liste des chambres où il a le droit d'entrer. Le vigile de l'entrée du personnel compare à cette liste chaque clé du trousseau et chaque clé isolée accrochée à la ceinture. Personne n'ouvre jamais la pochette zippée qui contient d'un coup toutes les clés d'un étage. Un agent autorisé seulement pour le hall passe avec la pochette de l'étage de direction, et le vigile le laisse entrer, car sa liste de contrôle ne parle jamais de pochettes.

Concepts Clés et Termes

ServiceAccount admission plugin
Contrôleur d'admission intégré à kube-apiserver qui renseigne le compte de service du pod et, sur demande, limite les Secrets que le pod peut référencer (plugin/pkg/admission/serviceaccount).
kubernetes.io/enforce-mountable-secrets
Annotation posée sur une ServiceAccount. Avec la valeur "true", les pods exécutés sous ce compte ne peuvent référencer que les Secrets listés dans son champ secrets.
envFrom / secretRef
Champ de conteneur qui importe d'un coup toutes les clés d'un Secret comme variables d'environnement, contrairement à env[].valueFrom.secretKeyRef, qui n'en importe qu'une.
Conteneur éphémère
Conteneur de débogage ajouté à un pod en cours d'exécution via la sous-ressource pods/ephemeralcontainers. Il est admis séparément, par limitEphemeralContainerSecretReferences().

Analyse de Cause Racine

limitSecretReferences() dans plugin/pkg/admission/serviceaccount/admission.go appliquait la liste des Secrets montables en parcourant les volumes de Secret du pod et le env[].valueFrom.secretKeyRef de chaque conteneur. Elle ne parcourait jamais envFrom[].secretRef, qui importe un Secret entier. La même lacune existait pour les conteneurs init et, dans limitEphemeralContainerSecretReferences(), pour les conteneurs éphémères. La PR #124322 a ajouté la boucle envFrom manquante aux trois endroits.

Déroulement de l'Attaque Étape par Étape

Étape 1

Un compte de service restreint

La ServiceAccount builder porte kubernetes.io/enforce-mountable-secrets: "true" et ne liste que build-token dans secrets. Le même namespace contient aussi prod-db-credentials.

Étape 2

Spécification de pod avec envFrom

Un utilisateur autorisé à créer des pods en soumet un qui tourne sous builder et définit envFrom: [{secretRef: {name: prod-db-credentials}}] sur un conteneur, un conteneur init ou un conteneur éphémère.

Étape 3

Le contrôle d'admission passe

limitSecretReferences() ne trouve aucun volume de Secret ni aucun env[].valueFrom.secretKeyRef hors de la liste autorisée : le pod est admis.

Étape 4

Le kubelet injecte le Secret

Le kubelet résout envFrom et définit chaque clé de prod-db-credentials comme variable d'environnement, lisible par le conteneur.

Code Source : Vulnérable vs Sécurisé

IMPLÉMENTATION VULNÉRABLE
// 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
}
PATCH SÉCURISÉ ET ROBUSTE
// 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
}

Liste de Contrôle de Sécurité pour l'Ingénierie

Sources