flawopen.com/Teardowns/cve-2024-3177-kubernetes-serviceaccount-envfrom-secrets-bypass
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.
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, parlimitEphemeralContainerSecretReferences().
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
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.
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.
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.
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é
// 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
}
Liste de Contrôle de Sécurité pour l'Ingénierie
- ✓Mettez kube-apiserver à jour en v1.29.4, v1.28.9, v1.27.13 ou ultérieure ; aucun contournement par configuration n'existe.
- ✓Repérez les comptes de service qui s'appuient sur cette politique :
kubectl get serviceaccounts -Afiltré sur l'annotationkubernetes.io/enforce-mountable-.secrets=true - ✓Cherchez dans les journaux d'audit de l'API les créations de pods et les mises à jour de conteneurs éphémères dont
envFrom[].secretRef.namene figure pas dans la listesecretsdu compte de service du pod. - ✓Considérez le droit de créer des pods dans un namespace comme un accès en lecture à ses Secrets ; placez les Secrets sensibles dans des namespaces dont les créateurs de pods y ont déjà légitimement accès.
- ✓Quand un contrôle d'admission restreint un type de ressource, énumérez chaque champ de la spécification du pod qui peut y faire référence (volumes, volumes projetés,
env,envFrom, conteneurs init et éphémères) et écrivez un test par champ.