Linux Capabilities в контейнерах
В этой заметке мы разберём, как запускать контейнеры от пользователя без root-прав: настроим Dockerfile, разрулим права доступа и затронем проблемы, с которыми сталкиваются реальные приложения.
Чтобы запустить контейнер не от root, достаточно создать пользователя и указать его через инструкцию USER.
RUN groupadd -g 1001 flaskgroup && \
useradd -u 1001 -g flaskgroup -m flaskuser
USER flaskuser
Но в реальных проектах всё не ограничивается только этим.
Нужно понимать, как устроено приложение и его конфигурация. Кроме того, важно убедиться, что non-root пользователь способен удовлетворить все требования приложения. Например, читать и писать файлы или получать доступ к конфигурациям.
Host root vs. container root
Host root и container root - это не одно и то же, если говорить о доступе к ресурсам хоста.
Давайте разберёмся на примерах, чтобы разница была понятнее.
Root-пользователь на хосте
Root на хостовой системе имеет полный контроль над всеми ресурсами и процессами. Хостовый root может читать любые файлы, выполнять любые команды, устанавливать и удалять ПО, менять системные настройки.
По сути, это абсолютный администратор хоста.
Root-пользователь в контейнере
Root внутри контейнера обладает похожими правами, но только в пределах контейнера.
Хотя контейнерный root внешне выглядит как обычный root, он во многом изолирован.
Примечание: container root (UID 0) - это тот же UID 0, что и на хосте, если явно не настроены user namespaces.
Это означает, что, несмотря на root-права внутри контейнера, прямого доступа к ресурсам хоста у него нет. Этому мешают namespaces, control groups и профили безопасности вроде seccomp.
Контейнерный runtime просто убирает часть особых привилегий (capabilities) и блокирует определённые системные вызовы (через seccomp), делая окружение контейнера более безопасным, даже если UID внутри контейнера и на хосте совпадает.
Например:
$ docker run -it ubuntu
root@0295ecbcec17:/# mount -t tmpfs none /mnt
mount: /mnt: permission denied.
dmesg(1) may have more information after failed mount system call.
root@0295ecbcec17:/# dmesg
dmesg: read kernel buffer failed: Operation not permitted
Но если явно разрешить привилегии - через capabilities или флаг --privileged - контейнерный root получает доступ к хосту (например, через примонтированные тома или сетевые настройки).
В таком режиме можно напрямую монтировать файловые системы хоста, менять параметры ядра, писать в системные файлы и читать их.
Например:
# Явно разрешаем доступ к файловой системе хоста $ docker run --privileged -it ubuntu root@ff566015378b:/# mount /dev/sda1 /mnt root@ff566015378b:/# cd /mnt/ root@ff566015378b:/mnt# ls EFI root@ff566015378b:/# sysctl -w net.ipv4.ip_forward=1 net.ipv4.ip_forward = 1 root@ff566015378b:/# echo 1 > /proc/sys/net/ipv4/ip_forward
Теперь представим, что контейнер запущен без --privileged.
Даже в этом случае риск остаётся: если контейнер, работающий от root, будет скомпрометирован, уязвимости могут позволить атакующему вырваться за пределы контейнера и получить доступ на уровне хоста.
Именно поэтому запуск контейнеров от root настоятельно не рекомендуется.
Запуск контейнеров от non-root пользователя
Теперь давайте посмотрим, что нам важно учитывать при запуске контейнеров от пользователя без root-прав.
Создание non-root пользователя
Первый шаг (очевидно) - создать non-root пользователя.
Команды groupadd и useradd создают нового пользователя (flaskuser) и группу (flaskgroup) с фиксированными UID и GID (например, 1001 - часто используемое значение для non-root пользователей).
Примечание: UID 1000 и выше обычно выделяются для non-root пользователей (обычных пользователей). Во многих Linux-дистрибутивах UID для таких пользователей по умолчанию начинается с 1000 или 1001. UID 0 зарезервирован для root. Конкретные диапазоны для вашей системы можно посмотреть в файле /etc/login.defs.Например:
RUN groupadd -g 1001 flaskgroup && \
useradd -u 1001 -g flaskgroup -m flaskuser
Права на файлы
При запуске приложений они часто ожидают, что смогут писать в определённые директории - будь то лог-файлы или runtime-конфигурация.
Нужно заранее позаботиться о правильных владельцах и правах доступа ещё на этапе сборки образа.
Допустим, приложению нужно писать логи в /app/logs. Тогда конфигурация для non-root пользователя может выглядеть так:
RUN mkdir -p /app/logs && \
chown flaskuser:flaskgroup /app/logs && \
chmod 755 /app/logs
Привязка к портам
Non-root пользователи не могут биндиться к привилегированным портам (порты < 1024).
Такие сервисы, как Nginx, по умолчанию принимают соединения на порту 80. Более того, если посмотреть стандартные образы Nginx, они сконфигурированы для запуска от root.
Например, если запустить контейнер с Nginx и посмотреть процессы, видно, что master-процесс работает от root.
$ docker run -d --name nginx nginx
f5896b56bbbdcca7d8....
$ docker top nginx | awk '{print $1, $8, $9}'
UID CMD
root nginx: master
systemd+ nginx: worker
Если вы хотите запускать такие приложения от non-root пользователя, нужно поменять порт на другой, например 8080, ещё во время сборки образа.
Например:
RUN sed -i 's,listen 80;,listen 8080;,' /etc/nginx/conf.d/default.conf
Примечание: если всё же нужно использовать низкие порты (порты < 1024) - что крайне не рекомендуется - придётся использовать capabilities вроде CAP_NET_BIND_SERVICE (и делать это очень аккуратно), не выдавая полный root-доступ и не запуская весь контейнер от root.Зависимости сервисов
Представим, что вы контейнеризируете приложение, которое предполагает запись логов в системную директорию /var/log/ (а это требует root-прав).
Кроме того, оно общается с другим процессом через /var/run/ для межпроцессного взаимодействия (IPC), что тоже обычно требует root.
В таких случаях лучше перенастроить приложение или немного его переработать, чтобы оно использовало другие пути. Например:
- логи пишутся в
/app/logsвместо/var/log; - IPC-файлы создаются в
/app/runвместо/var/run.
В Dockerfile приложение запускается от пользователя appuser - non-root пользователя, созданного во время сборки. Владение директориями /app/logs и /app/run передаётся этому пользователю.
Например:
ENV APP_USER=appuser
ENV LOG_DIR=/app/logs
ENV RUN_DIR=/app/run
# Create a non-root user
RUN useradd -ms /bin/bash $APP_USER
# Create alternative directories for logs and runtime files
RUN mkdir -p $LOG_DIR $RUN_DIR \
&& chown -R $APP_USER:$APP_USER $LOG_DIR $RUN_DIR
Конфигурационные файлы и runtime-директории
Есть приложения, которые изначально предполагают наличие root-доступа и ожидают, что смогут создавать или менять директории в местах, где требуются root-права.
Для таких приложений обычно приходится делать одно или несколько из следующих действий:
- разрешить non-root пользователю доступ к нужным директориям или файлам;
- заранее создать директории с корректными правами;
- настроить альтернативные пути для файлов и директорий.
Классический пример - Nginx Unprivileged Dockerfile. В нём Nginx изначально настроен на запуск от non-root пользователя.
Пример: Flask-приложение с non-root Dockerfile
Ниже приведён пример Dockerfile, который использует non-root пользователя.
В нём меняется владелец директории /app на пользователя с:
- UID: 1001
- GID: 1001
Так директория подготавливается для работы non-root пользователя.
После этого происходит переключение на non-root пользователя с UID 1001 для всех последующих команд.
FROM python:3.11-slim
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*
# Create a non-root user with UID 1001 and GID 1001, and set permissions
RUN mkdir -p /app && \
chown -R 1001:1001 /app
# Switch to the non-root user with UID 1001
USER 1001:1001
COPY --chown=1001:1001 requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY --chown=1001:1001 . .
EXPOSE 5000
CMD ["python", "app.py"]
Container non-root vs Kubernetes non-root user
Теперь представим, что в контейнере уже настроен non-root пользователь.
Что произойдёт, когда вы задеплоите этот контейнер в Kubernetes-под?
Как Kubernetes будет относиться к non-root пользователю, заданному внутри контейнера?
Это пожалуй будет тема для отдельной небольшой статьи.