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

● CVE-2024-3177 · CVSS 2.7 · Baixa
Pesquisa · FlawOpen

CVE-2024-3177: bypass de Secrets via envFrom na admissão de ServiceAccount do Kubernetes

CVE-2024-3177 (CVSS 2.7, baixa): o plugin de admissão ServiceAccount do kube-apiserver conferia volumes de Secret e referências env valueFrom contra a lista permitida da conta de serviço, mas nunca olhava envFrom, então um pod podia carregar qualquer Secret do seu namespace.

💡 Explicação em Linguagem Simples (ELI5)

Um hotel dá a cada camareira a lista dos quartos em que ela pode entrar. O segurança da porta de serviço confere cada chave do chaveiro e cada chave avulsa presa ao cinto com essa lista. Ninguém nunca abre a bolsinha com zíper que guarda todas as chaves de um andar de uma vez. Uma camareira liberada só para o saguão passa com a bolsinha do andar executivo, e o segurança deixa entrar, porque a lista de verificação nunca fala de bolsinhas.

Conceitos Centrais e Termos

ServiceAccount admission plugin
Controlador de admissão embutido no kube-apiserver que preenche a conta de serviço do pod e, quando solicitado, restringe quais Secrets o pod pode referenciar (plugin/pkg/admission/serviceaccount).
kubernetes.io/enforce-mountable-secrets
Anotação em uma ServiceAccount. Com o valor "true", pods que rodam com essa conta só podem referenciar os Secrets listados no campo secrets da conta.
envFrom / secretRef
Campo do contêiner que importa todas as chaves de um Secret como variáveis de ambiente de uma só vez, ao contrário de env[].valueFrom.secretKeyRef, que importa uma única chave.
Contêiner efêmero
Contêiner de depuração adicionado a um pod em execução pelo subrecurso pods/ephemeralcontainers. Ele é admitido separadamente, por limitEphemeralContainerSecretReferences().

Análise de Causa Raiz

limitSecretReferences() em plugin/pkg/admission/serviceaccount/admission.go aplicava a lista de Secrets montáveis percorrendo os volumes de Secret do pod e o env[].valueFrom.secretKeyRef de cada contêiner. Nunca percorria envFrom[].secretRef, que importa um Secret inteiro. A mesma lacuna existia para contêineres init e, em limitEphemeralContainerSecretReferences(), para contêineres efêmeros. O PR #124322 acrescentou o laço envFrom que faltava nos três lugares.

Fluxo de Ataque Passo a Paso

Passo 1

Uma conta de serviço restrita

A ServiceAccount builder tem kubernetes.io/enforce-mountable-secrets: "true" e lista apenas build-token em secrets. O mesmo namespace também guarda prod-db-credentials.

Passo 2

Especificação de pod com envFrom

Um usuário que pode criar pods envia um que roda como builder e define envFrom: [{secretRef: {name: prod-db-credentials}}] em um contêiner, contêiner init ou contêiner efêmero.

Passo 3

A verificação de admissão passa

limitSecretReferences() não encontra volume de Secret nem env[].valueFrom.secretKeyRef fora da lista permitida, então o pod é admitido.

Passo 4

O kubelet injeta o Secret

O kubelet resolve envFrom e define cada chave de prod-db-credentials como variável de ambiente, onde o contêiner pode lê-la.

Código-Fonte: Vulnerável vs. Seguro

IMPLEMENTAÇÃO VULNERÁVEL
// 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 SEGURO E 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 Verificação de Segurança para Engenharia

Fontes