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

● CVE-2024-3177 · CVSS 2.7 · Низкий
Исследования · FlawOpen

CVE-2024-3177: обход ограничения Secret через envFrom в admission-плагине ServiceAccount Kubernetes

CVE-2024-3177 (CVSS 2.7, низкая): admission-плагин ServiceAccount в kube-apiserver сверял тома Secret и ссылки env valueFrom со списком разрешённых секретов сервисного аккаунта, но не проверял envFrom, поэтому под мог загрузить любой Secret своего namespace.

💡 Простыми словами (ELI5)

Гостиница выдаёт каждой горничной список номеров, куда ей можно входить. Охранник у служебного входа сверяет с этим списком каждый ключ на связке и каждый отдельный ключ на поясе. В сумочку на молнии, где лежат сразу все ключи этажа, никто никогда не заглядывает. Горничная с допуском только в вестибюль проходит с сумочкой от директорского этажа, и охранник её пропускает: в его инструкции про сумочки ничего нет.

Ключевые понятия и термины

ServiceAccount admission plugin
Встроенный admission-контроллер kube-apiserver: подставляет поду сервисный аккаунт и при необходимости ограничивает, на какие Secret под может ссылаться (plugin/pkg/admission/serviceaccount).
kubernetes.io/enforce-mountable-secrets
Аннотация ServiceAccount. При значении "true" поды с этим аккаунтом могут ссылаться только на Secret из поля secrets аккаунта.
envFrom / secretRef
Поле контейнера, которое за один раз импортирует все ключи Secret как переменные окружения, в отличие от env[].valueFrom.secretKeyRef, импортирующего один ключ.
Эфемерный контейнер
Отладочный контейнер, добавляемый в работающий под через подресурс pods/ephemeralcontainers. Он проходит admission отдельно, в limitEphemeralContainerSecretReferences().

Анализ первопричины

limitSecretReferences() в plugin/pkg/admission/serviceaccount/admission.go применяла список монтируемых Secret, перебирая тома Secret пода и env[].valueFrom.secretKeyRef каждого контейнера. Поле envFrom[].secretRef, импортирующее Secret целиком, она не перебирала никогда. Та же дыра была у init-контейнеров и — в limitEphemeralContainerSecretReferences() — у эфемерных контейнеров. PR #124322 добавил недостающий цикл по envFrom во всех трёх местах.

Пошаговый сценарий атаки

Шаг 1

Ограниченный сервисный аккаунт

ServiceAccount builder помечен kubernetes.io/enforce-mountable-secrets: "true" и перечисляет в secrets только build-token. В том же namespace лежит и prod-db-credentials.

Шаг 2

Спецификация пода с envFrom

Пользователь с правом создавать поды отправляет под, работающий от builder, и задаёт envFrom: [{secretRef: {name: prod-db-credentials}}] у обычного, init- или эфемерного контейнера.

Шаг 3

Проверка admission пройдена

limitSecretReferences() не находит ни томов Secret, ни env[].valueFrom.secretKeyRef вне разрешённого списка и допускает под.

Шаг 4

Kubelet подставляет Secret

Kubelet разворачивает envFrom и выставляет каждый ключ prod-db-credentials как переменную окружения, доступную контейнеру.

Исходный код: Уязвимый vs Защищённый вариант

УЯЗВИМАЯ РЕАЛИЗАЦИЯ
// 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
}

Чек-лист по защите системы для инженеров

Источники