flawopen.com/Teardowns/cve-2024-3177-kubernetes-serviceaccount-envfrom-secrets-bypass
CVE-2024-3177: обход ограничения Secret через envFrom в admission-плагине ServiceAccount Kubernetes
CVE-2024-3177 (CVSS 2.7, низкая): admission-плагин ServiceAccount в kube-apiserver сверял тома Secret и ссылки env valueFrom со списком разрешённых секретов сервисного аккаунта, но не проверял envFrom, поэтому под мог загрузить любой Secret своего namespace.
Гостиница выдаёт каждой горничной список номеров, куда ей можно входить. Охранник у служебного входа сверяет с этим списком каждый ключ на связке и каждый отдельный ключ на поясе. В сумочку на молнии, где лежат сразу все ключи этажа, никто никогда не заглядывает. Горничная с допуском только в вестибюль проходит с сумочкой от директорского этажа, и охранник её пропускает: в его инструкции про сумочки ничего нет.
Ключевые понятия и термины
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 во всех трёх местах.
Пошаговый сценарий атаки
Ограниченный сервисный аккаунт
ServiceAccount builder помечен kubernetes.io/enforce-mountable-secrets: "true" и перечисляет в secrets только build-token. В том же namespace лежит и prod-db-credentials.
Спецификация пода с envFrom
Пользователь с правом создавать поды отправляет под, работающий от builder, и задаёт envFrom: [{secretRef: {name: prod-db-credentials}}] у обычного, init- или эфемерного контейнера.
Проверка admission пройдена
limitSecretReferences() не находит ни томов Secret, ни env[].valueFrom.secretKeyRef вне разрешённого списка и допускает под.
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
}
Чек-лист по защите системы для инженеров
- ✓Обновите kube-apiserver до v1.29.4, v1.28.9, v1.27.13 или новее; обходного пути через конфигурацию нет.
- ✓Найдите сервисные аккаунты, опирающиеся на эту политику:
kubectl get serviceaccounts -Aс фильтром по аннотацииkubernetes.io/enforce-mountable-.secrets=true - ✓Ищите в журналах аудита API создание подов и обновления эфемерных контейнеров, где
envFrom[].secretRef.nameотсутствует в спискеsecretsсервисного аккаунта пода. - ✓Считайте право создавать поды в namespace правом чтения его Secret; храните чувствительные Secret в namespace, где создателям подов и так доверен доступ к ним.
- ✓Если admission-проверка ограничивает тип ресурса, перечислите все поля спецификации пода, способные на него сослаться (тома, проецируемые тома,
env,envFrom, init- и эфемерные контейнеры), и напишите тест на каждое поле.