Linux Capabilities в контейнерах

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 пользователю, заданному внутри контейнера?

Это пожалуй будет тема для отдельной небольшой статьи.



Report Page