Не работаем от root:
Если в Dockerfile нет инструкции USER, процесс запускается от root. Внутри контейнера это root с урезанным набором прав, но при побеге из контейнера (такие уязвимости находят регулярно) атакующий получает root на хосте. Плюс банальное: примонтировали каталог хоста через bind mount (глава 5), и контейнер пишет туда файлы от root, которые потом не удалить без sudo.
Лечится парой строк в Dockerfile:
Код: Выделить всё
FROM node:22-alpine
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
COPY --chown=app:app package*.json ./
RUN npm ci --omit=dev
COPY --chown=app:app . .
USER app
CMD ["node", "server.js"]
Урезаем права при запуске:
Docker выдает контейнеру набор capabilities, кусочков root-привилегий. Большинству приложений не нужен ни один. Заодно можно запретить запись в файловую систему контейнера и эскалацию привилегий через setuid-бинарники:
Код: Выделить всё
docker run -d --name api \
--cap-drop ALL \
--read-only \
--tmpfs /tmp \
--security-opt no-new-privileges \
-p 127.0.0.1:8080:8080 \
myapp:1.4.2
В Compose (глава 8) то же самое описывается декларативно:
Код: Выделить всё
services:
api:
image: myapp:1.4.2
user: "1000:1000"
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
Все, что попало в слой образа, остается там навсегда: docker history и любой, кто скачает образ, увидят ваши ENV с паролями и скопированный .env. Передавайте секреты при запуске (глава 7), а если секрет нужен на этапе сборки, используйте механизм секретов BuildKit:
Код: Выделить всё
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
Сканируем образы:
Базовый образ полугодовой давности это десятки известных CVE. Сканер trivy ставится одним бинарником и показывает уязвимости по слоям: trivy image myapp:1.4.2. У Docker есть встроенный docker scout cves. Гоняйте сканер в CI и пересобирайте образы на свежей базе хотя бы раз в месяц. И фиксируйте теги (глава 3): latest сегодня и latest через месяц это разные образы.
Типичные грабли:
Монтирование /var/run/docker.sock внутрь контейнера. Доступ к сокету Docker равен root на хосте, что бы ни стояло в USER. Если контейнеру нужно управлять Docker, это осознанное решение для доверенного кода, а не удобный хак.
--read-only ломает приложения, которые пишут в свои каталоги: nginx хочет /var/cache/nginx, php-fpm пишет сессии на диск. Не откатывайте флаг целиком, добавьте tmpfs на конкретный путь.
После --user 1000:1000 контейнер не сможет писать в volume, который раньше наполнялся от root. Поправьте владельца один раз: docker run --rm -v mydata:/data alpine chown -R 1000:1000 /data.
Итог: не root, минимум прав, секреты вне образа, регулярное сканирование. Этого хватает, чтобы закрыть большинство реальных инцидентов с контейнерами у небольших команд. В последней главе соберем все вместе и поговорим о подготовке к продакшену: политики рестарта, лимиты ресурсов, healthcheck.