От переводчика: существуют разные контексты безопасности (security contexts) в Kubernetes. Они позволяют настраивать параметры безопасности на уровне пода или контейнера. Некоторые из них вполне очевидны, другие — не совсем ясны и сбивают с толку. В этой статье автор Кристоф Тафани-Дерепер развенчивает мифы вокруг allowPrivilegeEscalation.

TL;DR: allowPrivilegeEscalation — настройка для усиления безопасности и ничего более. Если у вас есть возможность легко и быстро отключить её для рабочих нагрузок — делайте это. Если нет, то эта опция не станет причиной взлома. Даже если явно её не отключить, скорее всего, всё будет в порядке.

Что такое allowPrivilegeEscalation?

Спросите любого инженера по безопасности, стоит ли разрешать приложениям «повышать привилегии». Есть большая вероятность, что в ответ на вас посмотрят с недоумением, растерянностью, а то и усомнятся в здравом уме.

К счастью, тут просто недопонимание. Вы же спрашиваете: «А это вообще страшно, что я не поставил флаг allowPrivilegeEscalation в false

А ваш безопасник представляет себе: «Ничего, если моё кривое Java-приложение сбежит из контейнера и будет куролесить по кластеру, как в старые добрые времена?»

Отличная новость: вы оба не знаете, что означает флаг allowPrivilegeEscalation, — да и кто вас за это упрекнёт?

Распространённые заблуждения об allowPrivilegeEscalation

Сразу проясним: хотя отключение allowPrivilegeEscalation и может пригодиться, прежде всего это настройка для повышения безопасности в контейнерных окружениях.

Если оставить allowPrivilegeEscalation установленным в true (значение по умолчанию):

  • непривилегированный процесс в контейнере не сможет каким-то магическим образом повысить свои привилегии до root;

  • процессы, запущенные в контейнере, не смогут внезапно покинуть его;

  • под не сможет повысить свои привилегии в кластере.

«Но Кристоф, — спросите вы, — так что же она тогда вообще делает?» Давайте сначала рассмотрим пример атаки, которую эта настройка действительно предотвращает. А затем разберёмся с тем, как именно среды исполнения контейнеров (runtime) её реализуют.

allowPrivilegeEscalation в действии

Воспроизведём сценарий, в котором уязвимость позволяет непривилегированному процессу осуществить эскалацию привилегий до уровня root внутри контейнера. Это может произойти из-за наличия уязвимостей на уровне ядра, таких как DirtyCow, DirtyPipe или CVE-2023-0386 в OverlayFS. Также возможен более простой, но не менее реалистичный способ: эксплуатация исполняемого файла, принадлежащего суперпользователю, с установленным атрибутом setuid

Сначала воспроизведём сценарий. Затем посмотрим, каким образом деактивация allowPrivilegeEscalation не даст такой уязвимости сработать.

Воспользуемся программой ниже. Она использует функции setreuid (расшифровывается как «set real and effective user id») и setregid, чтобы получить права root. Это сработает, только если сам исполняемый файл принадлежит root и у него установлен бит setuid

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(void) {
    // Эскалируем до root.
    setreuid(0, 0);
    setregid(0, 0);
    // Запустить командную оболочку.
    char* const argv[] = {"/bin/bash", NULL};
    char* const environ[] = {NULL};
    execve("/bin/bash", argv, environ);
}
gcc escalate.c -Wall -o /tmp/escalate
sudo chown root:root /tmp/escalate
sudo chmod +s /tmp/escalate

Теперь можно взять обычного пользователя и проверить, что эта уязвимая программа действительно позволяет эскалировать привилегии до root:

Dockerfile ниже описывает Alpine-контейнер, в котором приложение работает под обычным пользователем, а внутри лежит уязвимый исполняемый файл:

Dockerfile
FROM alpine:3.20 AS builder
WORKDIR /build
RUN cat > escalate.c <<EOF
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
int main(void) {
    // Escalate to root
    setreuid(0, 0); 
    setregid(0, 0);
    // Spawn a shell
    char* const argv[] = {"/bin/bash", NULL};
    char* const environ[] = {"PATH=/bin:/sbin:/usr/bin:/usr/sbin", NULL};
    if (-1 == execve("/bin/bash", argv, environ)) {
        printf("Unable to execve /bin/bash, errno %d\n", errno);
    }
}
EOF
RUN cat /build/escalate.c
RUN apk add --no-cache gcc musl-dev
RUN gcc escalate.c -Wall -o escalate
FROM alpine:3.20 AS runner
WORKDIR /app
COPY --from=builder /build/escalate ./escalate
RUN chown root:root ./escalate && chmod +s ./escalate
RUN adduser app-user --uid 1000 --system --disabled-password --no-create-home
RUN apk add bash
USER app-user
ENTRYPOINT ["sh", "-c", "echo Application running && sleep infinity"]

Давайте соберём его и запустим в кластере Kubernetes, явно включив параметр allowPrivilegeEscalation (хотя он и так по умолчанию включён):

# Собираем образ.
docker build . -t my-app:0.1
# Создаем kind-кластер и запускаем в нём образ.
kind create cluster
kind load docker-image my-app:0.1
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: my-app
spec:
  securityContext:
    runAsUser: 1000
    runAsGroup: 1000
  containers:
  name: my-app
    image: my-app:0.1
    securityContext:
      allowPrivilegeEscalation: true
EOF

Как и ожидалось, получается использовать уязвимость, чтобы повысить себе права до root:

Если же запустить наш под с allowPrivilegeEscalation=false, то получим:

Что же произошло? Вызовы setreuid и setregid закончились неудачей. Ошибки станут более очевидны, если в код «эксплойта» добавить обработку ошибок:

// Эскалируем до root.
if (setreuid(0, 0) != 0) {
    printf("setreuid(0, 0) failed: %s\n", strerror(errno));
}
if (setregid(0, 0) != 0) {
    printf("setregid(0, 0) failed: %s\n", strerror(errno));
}

Вывод:

Как работает allowPrivilegeEscalation

Цитирую документацию Kubernetes:

Параметр allowPrivilegeEscalation определяет, может ли процесс получить больше привилегий, чем его родительский процесс. Это булево значение напрямую управляет установкой флага no_new_privs для процесса контейнера.

Флаг no_new_privs появился в версии 3.5 ядра Linux, вышедшей в 2012 году. При активации он гарантирует, что ни один дочерний процесс не сможет получить больше разрешений, чем его родитель.

Это поведение можно проверить, вручную установив no_new_privs перед тем, как пытаться повысить привилегии. Для этого воспользуемся небольшой утилитой, которая:

  1. Использует системный вызов prctl для установки no_new_privs.

  2. Создаёт новый процесс sh, который будет «защищён» от повышения привилегий.

Второй шаг необходим, поскольку установленный флаг не применяется к уже работающим процессам:

#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/prctl.h>
int main(void) {
    // Устанавливаем no_new_privs.
    if (-1 == prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
        printf("Could not set prctl: %s\n", strerror(errno));
    }
    // Запускаем командную оболочку.
    char* const argv[] = {"/bin/sh", NULL};
    char* const environ[] = {"PATH=/bin:/sbin:/usr/bin:/usr/sbin", NULL};
    if (-1 == execve("/bin/sh", argv, environ)) {
        printf("Unable to execve /bin/sh, errno %d\n", errno);
    }
}

Скомпилируем и запустим эту утилиту. Видим, что она благополучно устанавливает флаг no_new_privs для нового shell-процесса (чтобы убедиться в этом, достаточно прочитать /proc/self/status):

Если теперь ещё раз попытаться повысить привилегии, у нас ничего не выйдет — попытка заблокируется так же, как когда allowPrivilegeEscalation был установлен в false:

Именно эту последовательность действий выполняет среда исполнения контейнеров (runtime) при создании новых контейнеризованных процессов. Например, ниже представлен код инициализации контейнера из runc, используемый многими средами исполнения контейнеров, такими как containerd, CRI-O и Docker: 

// Если параметр NoNewPrivileges равен true (напрямую контролируется allowPrivilegeEscalation), вызвать prctl(PR_SET_NO_NEW_PRIVES, 1, 0, 0, 0).
if l.config.NoNewPrivileges {
  if err := unix.Prctl(unix.PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); err != nil {
    return &os.SyscallError{Syscall: "prctl(SET_NO_NEW_PRIVS)", Err: err}
  }
}

Как видите, он делает то же, что и мы:

  1. Проверяет, стоит ли NoNewPrivileges в true (это значение напрямую берётся из поля allowPrivilegeEscalation контекста безопасности Kubernetes).

  2. Если это так, устанавливает no_new_privs перед тем, как создать процесс контейнера.

Так зачем всё это нужно?

Безопасность — как и большинство областей, которые борются с системными проблемами, — сводится к созданию различных уровней защиты, чтобы одна ошибка не превратилась в утечку данных.

В этом контексте: да, явное отключение allowPrivilegeEscalation — это разумная и хорошая мера по усилению безопасности. Отключение этого параметра значительно укрепляет уверенность в том, что злоумышленник, скомпрометировав непривилегированное приложение, не сможет повысить свои права до root-пользователя внутри контейнера. Это, в свою очередь, снижает риск эксплуатации других уязвимостей, требующих прав суперпользователя.

Опасно ли оставлять этот параметр включённым? Скорее всего, нет. Это просто ещё один механизм защиты, который пока не задействован. Он не станет причиной взлома. Если у вас не суперпродвинутая команда безопасников, то лучше заняться более важными аспектами защиты контейнеров (кстати, об этом я рассказывал на KubeCon EU 2024 — посмотрите мой доклад и статью, там есть основанные на реальных угрозах идеи, с чего начать).

Впрочем, это не означает, что этот параметр можно игнорировать. Обязательно включите его в свой план по безопасности контейнеров.

Часто задаваемые вопросы

Каково значение по умолчанию для параметра allowPrivilegeEscalation?

По умолчанию установлено значение true. Подробности смотрите в документации, соответствующем исходном коде и связанном Issue.

Есть ли смысл отключать allowPrivilegeEscalation, если рабочие нагрузки выполняются под root внутри контейнеров?

Нет, смысла нет. Если рабочие нагрузки работают под root, дальнейшее повышение привилегий внутри контейнера для них невозможно.

Есть ли смысл отключать allowPrivilegeEscalation, если нагрузки работают как privileged или имеют CAP_SYS_ADMIN?

Нет смысла. На самом деле это даже не получится сделать — API-сервер отклонит ваш запрос (смотрите код валидации):

The Pod "my-app" is invalid: spec.containers[0].securityContext: Invalid value: cannot set `allowPrivilegeEscalation` to false and `privileged` to true

Защищает ли отключение allowPrivilegeEscalation от всех видов повышения привилегий внутри контейнера?

Нет. Например, это не поможет, если злоумышленник использует уязвимость ядра, позволяющую ему повысить свои привилегии. Тем не менее этот параметр должен блокировать все способы повышения привилегий, основанные на эксплуатации setuid/setgid

Существует ли связь между allowPrivilegeEscalation и privileged?

Нет. Отключение allowPrivilegeEscalation является механизмом усиления безопасности. Если оставить значение по умолчанию, процессы внутри контейнера всё равно не смогут тривиально повысить свои привилегии или выйти за пределы контейнера.

Запуск рабочих нагрузок с включённым параметром privileged приводит к тому, что они выполняются так, как если бы были процессами непосредственно на хосте, что по своей сути делает выход из контейнера тривиальной задачей.

Если злоумышленнику удастся повысить привилегии до root внутри контейнера, кластер обречен?

Это ещё одно заблуждение, с готовностью тиражируемое атмосферой страха, неопределённости и сомнений (FUD — Fear, Uncertainty, and Doubt. — Прим. пер.), которая иногда движет индустрией безопасности. Процесс, запущенный с правами root внутри контейнера, не может тривиальным образом выйти за его пределы. Для этого ему придётся воспользоваться уязвимостью или ошибкой в конфигурации.

Заключение

Надеюсь, статья помогла разобраться в том, что такое allowPrivilegeEscalation, чем этот параметр не является и каковы явные преимущества его использования. Я сам поначалу запутался, когда впервые узнал о нём. Похоже, он нередко выступает поводом для заблуждений — возможно, из-за своего не слишком удачного названия.

Спасибо, что дочитали до конца. Давайте продолжим обсуждение на Hacker News, в Twitter или Mastodon.

Благодарю моего коллегу Рори МакКьюна (Rory McCune) за рецензирование этой статьи.

P. S. 

Читайте также в нашем блоге:

Комментарии (0)