flawopen.com/Teardowns/cve-2024-3177-kubernetes-serviceaccount-envfrom-secrets-bypass
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.
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
secretsdes 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/ephemeralcontainershinzugefügt wird. Er wird separat zugelassen, durchlimitEphemeralContainerSecretReferences().
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
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.
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.
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.
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
// 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
}
Checkliste für Engineering & Systemsicherheit
- ✓kube-apiserver auf v1.29.4, v1.28.9, v1.27.13 oder neuer aktualisieren; einen Konfigurations-Workaround gibt es nicht.
- ✓Service-Accounts finden, die sich auf die Richtlinie verlassen:
kubectl get serviceaccounts -A, gefiltert auf die Annotationkubernetes.io/enforce-mountable-.secrets=true - ✓API-Audit-Logs nach Pod-Erstellungen und Updates ephemerer Container durchsuchen, deren
envFrom[].secretRef.namenicht in dersecrets-Liste des Service-Accounts steht. - ✓Das Recht, in einem Namespace Pods anzulegen, als Lesezugriff auf dessen Secrets behandeln; sensible Secrets nur in Namespaces ablegen, deren Pod-Ersteller ihnen ohnehin vertraut werden.
- ✓Schränkt eine Admission-Prüfung einen Ressourcentyp ein, jedes Feld der Pod-Spezifikation aufzählen, das ihn referenzieren kann (Volumes, projizierte Volumes,
env,envFrom, Init- und ephemere Container), und je Feld einen Test schreiben.