DockerMiddleCoding
Почему запускать контейнер от root — плохо? Как задать non-root user в Dockerfile?
Root в контейнере опасен: при уязвимости атакующий получает привилегированный доступ к хосту и volumes. Задаётся через USER 1001 в Dockerfile после создания пользователя через useradd/adduser с фиксированным UID.
Запуск контейнера от non-root пользователя
По умолчанию процессы в контейнере запускаются от root (UID 0). Даже если user namespace не настроен, root внутри контейнера = root снаружи при определённых условиях. Это основная причина многих container escape уязвимостей.
Почему root в контейнере опасен
- Примонтированные volumes наследуют права: файлы, созданные контейнером, принадлежат root на хосте.
- Уязвимость в приложении даёт атакующему root-права в контейнере, что существенно упрощает побег.
- Без User Namespace root в контейнере = root на хосте при доступе к shared ресурсам (procfs, shared volumes).
- Многие compliance-стандарты (PCI DSS, SOC 2, CIS Docker Benchmark) запрещают root в контейнерах.
Задать non-root user в Dockerfile
# Вариант 1: создать пользователя в образе
FROM python:3.13-slim
# Создаём группу и пользователя с фиксированным UID/GID
RUN groupadd --gid 1001 appgroup && \
useradd --uid 1001 --gid appgroup --shell /bin/sh --create-home appuser
WORKDIR /app
COPY --chown=appuser:appgroup requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY --chown=appuser:appgroup . .
# Переключиться на non-root пользователя
USER appuser
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
# Вариант 2: distroless образ (Google)
FROM gcr.io/distroless/python3-debian12
COPY --from=builder /app /app
WORKDIR /app
# distroless запускает от nonroot (UID 65532) по умолчанию
CMD ["main.py"]
Node.js пример
FROM node:20-alpine
# node-образы уже содержат пользователя 'node' (UID 1000)
WORKDIR /app
COPY --chown=node:node package*.json .
RUN npm ci --only=production
COPY --chown=node:node . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]
Go с минимальным scratch образом
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o server .
FROM scratch
# В scratch нет useradd — копируем passwd файл
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /app/server /server
USER nobody
CMD ["/server"]
Docker Compose: проверка пользователя
services:
api:
image: my-api:latest
user: "1001:1001" # можно переопределить здесь
# или через переменную:
# user: "${UID}:${GID}"
Проверка
# Проверить, от кого запущен процесс
docker exec my-container whoami
docker exec my-container id
# Проверить образ без запуска
docker inspect my-image --format '{{.Config.User}}'
Capabilities вместо root
Если приложению нужны специфические привилегии (например, слушать порт <80 или управлять сетью), можно добавить конкретные capabilities вместо полного root:
docker run -d \
--user 1001 \
--cap-add NET_BIND_SERVICE \
my-app:latest
Подводные камни
- Файлы, скопированные через
COPYбез--chown, принадлежат root — non-root пользователь не сможет их прочитать, если права 640. - Монтированные volumes создаются с правами хоста. Если volume принадлежит root, контейнерный non-root пользователь не получит доступ. Решение: инициализировать volume через entrypoint или задать
chownв entrypoint.sh. - Некоторые базовые образы (Alpine) не содержат
useradd— используйтеadduser -D appuser. - Если UID внутри контейнера не совпадает с UID владельца файла на хосте, могут возникнуть проблемы с правами при работе с bind mount.
USERв Dockerfile не препятствуетdocker run --user root ...— IMAGE-инструкция легко переопределяется при запуске.- Distroless-образы не содержат shell —
docker exec ... bashне работает. Для отладки нужен debug-вариант:gcr.io/distroless/python3-debian12:debug. - Запуск от non-root не помогает, если контейнер запущен с
--privileged— это полностью отключает ограничения безопасности. - Фиксируйте UID числом (1001), а не именем — в разных базовых образах одно имя может иметь разный UID, что приводит к непредсказуемым правам.
Common mistakes
- Добавить USER, но оставить файлы приложения root-owned и получить permission denied.
- Запускать контейнер с Docker socket mount, считая non-root достаточной защитой.
- Использовать root ради записи в произвольные директории вместо настройки writable paths.
What the interviewer is testing
- Объясняет blast radius root in container.
- Пишет Dockerfile with user/group and ownership.
- Учитывает volumes, ports, writable directories and capabilities.