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.

Sources

Related topics