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

● CVE-2024-3177 · CVSS 2.7 · Niedrig
Sicherheitsforschung · FlawOpen

CVE-2024-3177: Umgehung der Secret-Beschränkung per envFrom in der ServiceAccount-Admission von Kubernetes

CVE-2024-3177 (CVSS 2.7, niedrig): Das ServiceAccount-Admission-Plugin des kube-apiserver prüfte Secret-Volumes und env-valueFrom-Referenzen gegen die Freigabeliste des Service-Accounts, ignorierte aber envFrom – ein Pod konnte so jedes Secret seines Namespace laden.

💡 Einfache Erklärung (ELI5)

Ein Hotel gibt jeder Reinigungskraft eine Liste der Zimmer, die sie betreten darf. Der Wachmann am Personaleingang gleicht jeden Schlüssel am Schlüsselbund und jeden einzelnen Schlüssel am Gürtel mit dieser Liste ab. In die Reißverschlusstasche mit allen Schlüsseln eines ganzen Stockwerks schaut nie jemand. Wer nur für die Lobby freigegeben ist, geht mit der Tasche für die Chefetage hinein, und der Wachmann winkt durch, weil seine Checkliste Taschen nicht kennt.

Kernkonzepte & Begriffe

ServiceAccount admission plugin
Eingebauter Admission-Controller des kube-apiserver, der den Service-Account eines Pods setzt und auf Wunsch einschränkt, welche Secrets der Pod referenzieren darf (plugin/pkg/admission/serviceaccount).
kubernetes.io/enforce-mountable-secrets
Annotation an einem ServiceAccount. Mit dem Wert "true" dürfen Pods unter diesem Account nur die Secrets referenzieren, die im Feld secrets des Accounts stehen.
envFrom / secretRef
Container-Feld, das alle Schlüssel eines Secrets auf einmal als Umgebungsvariablen importiert – anders als env[].valueFrom.secretKeyRef, das nur einen Schlüssel holt.
Ephemerer Container
Debug-Container, der einem laufenden Pod über die Subressource pods/ephemeralcontainers hinzugefügt wird. Er wird separat zugelassen, durch limitEphemeralContainerSecretReferences().

Ursachenanalyse

limitSecretReferences() in plugin/pkg/admission/serviceaccount/admission.go setzte die Freigabeliste mountbarer Secrets durch, indem es die Secret-Volumes des Pods und das env[].valueFrom.secretKeyRef jedes Containers durchlief. envFrom[].secretRef, das ein ganzes Secret importiert, durchlief es nie. Dieselbe Lücke bestand für Init-Container und – in limitEphemeralContainerSecretReferences() – für ephemere Container. PR #124322 ergänzte die fehlende envFrom-Schleife an allen drei Stellen.

Schritt-für-Schritt Angriffsablauf

Schritt 1

Ein eingeschränkter Service-Account

Der ServiceAccount builder trägt kubernetes.io/enforce-mountable-secrets: "true" und listet in secrets nur build-token. Im selben Namespace liegt auch prod-db-credentials.

Schritt 2

Pod-Spezifikation mit envFrom

Ein Benutzer mit Recht zum Anlegen von Pods reicht einen Pod ein, der als builder läuft und an einem Container, Init-Container oder ephemeren Container envFrom: [{secretRef: {name: prod-db-credentials}}] setzt.

Schritt 3

Die Admission-Prüfung besteht

limitSecretReferences() findet weder ein Secret-Volume noch ein env[].valueFrom.secretKeyRef außerhalb der Freigabeliste und lässt den Pod zu.

Schritt 4

Das Kubelet injiziert das Secret

Das Kubelet löst envFrom auf und setzt jeden Schlüssel von prod-db-credentials als Umgebungsvariable, die der Container lesen kann.

Quellcode: Verwundbar vs. Sicher

VERWUNDBARE IMPLEMENTIERUNG
// 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
}
GEHÄRTETER SICHERHEITS-PATCH
// 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
}

Checkliste für Engineering & Systemsicherheit

Quellen