opened image

Docker security: лучшие практики безопасности контейнеров

 

Запуск контейнера от root с полным доступом к хостовой файловой системе - одна из самых распространённых ошибок в Docker-окружениях. Эта статья описывает конкретные меры: non-root пользователь, read-only filesystem, ограничение capabilities, сканирование образов через Trivy и изоляция сетей.

 

Среда: Ubuntu 22.04 LTS, Docker 26.1, containerd 1.7.

 

 

Non-root пользователь внутри контейнера

 

По умолчанию процессы внутри контейнера запускаются от root (UID 0). Если приложение скомпрометировано и злоумышленник выйдет из контейнера, он окажется root на хосте. Это нужно устранить на уровне Dockerfile.

 

FROM node:20-alpine

# Создаём непривилегированного пользователя
RUN addgroup -g 1001 appgroup && \
    adduser -u 1001 -G appgroup -D appuser

WORKDIR /app

# Копируем файлы и меняем владельца
COPY --chown=appuser:appgroup package*.json ./
RUN npm ci --only=production

COPY --chown=appuser:appgroup . .

# Переключаемся на непривилегированного пользователя
USER appuser

EXPOSE 3000
CMD ["node", "server.js"]

 

Проверяем, от кого запущен процесс:

docker exec <container_id> whoami
# appuser

docker exec <container_id> id
# uid=1001(appuser) gid=1001(appgroup) groups=1001(appgroup)

 

Если образ уже собран без non-root пользователя, можно указать пользователя при запуске:

docker run --user 1001:1001 nginx:1.26

 

 

 

 

Read-only filesystem

 

Большинству приложений не нужна запись в файловую систему контейнера. Монтируем её как read-only и явно разрешаем запись только в нужные директории:

 

docker run -d \
  --name my_app \
  --read-only \
  --tmpfs /tmp:rw,size=50m \
  --tmpfs /var/run:rw,size=10m \
  -v /app/logs:/app/logs:rw \
  my_image:1.0

 

Флаг --read-only делает корневую файловую систему контейнера read-only. --tmpfs монтирует tmpfs в оперативную память для директорий, куда нужна запись (временные файлы, сокеты). Постоянные данные монтируются через -v.

 

В Docker Compose:

services:
  app:
    image: my_app:1.0
    read_only: true
    tmpfs:
      - /tmp:size=50m
      - /var/run:size=10m
    volumes:
      - ./logs:/app/logs

 

 

 

Ограничение Linux capabilities

 

 

Docker по умолчанию предоставляет контейнеру ~14 capabilities из 40+ существующих в Linux. Большинство приложений не нуждаются даже в них.

 

Хорошая практика - сбросить все capabilities и добавить только необходимые:

docker run -d \
  --cap-drop ALL \
  --cap-add NET_BIND_SERVICE \
  --name nginx_secure \
  nginx:1.26

 

NET_BIND_SERVICE нужен только для прослушивания портов ниже 1024. Если ваше приложение слушает на порту выше 1024, можно убрать и его.

 

В Docker Compose:

services:
  nginx:
    image: nginx:1.26
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE

 

Проверяем, какие capabilities есть у контейнера:

docker inspect <container_id> | grep -A 10 "CapAdd\|CapDrop"

# Или через утилиту capsh внутри контейнера (если доступна)
docker exec <container_id> cat /proc/1/status | grep Cap

 

 

 

 

Сканирование образов через Trivy

 

Trivy - опенсорсный сканер уязвимостей от Aqua Security. Находит CVE в пакетах ОС, библиотеках языков программирования и конфигурационных файлах.

 

Установка Trivy

 

# Через apt (Ubuntu/Debian)
sudo apt install wget apt-transport-https gnupg -y
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | \
  gpg --dearmor | sudo tee /usr/share/keyrings/trivy.gpg > /dev/null
echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb generic main" | \
  sudo tee /etc/apt/sources.list.d/trivy.list
sudo apt update && sudo apt install trivy -y

trivy --version
# Version: 0.52.2

 

Сканирование образа

 

# Сканируем образ до запуска
trivy image nginx:1.26

# Только CRITICAL и HIGH уязвимости
trivy image --severity CRITICAL,HIGH nginx:1.26

# Сканируем запущенный контейнер
trivy image --input <(docker save my_app:1.0)

# Вывод в JSON для CI/CD
trivy image --format json --output trivy-results.json nginx:1.26

 

Пример вывода:

nginx:1.26 (debian 12.6)
Total: 23 (CRITICAL: 2, HIGH: 8, MEDIUM: 13)

 

Интеграция в GitLab CI:

trivy_scan:
  stage: security
  image: aquasec/trivy:0.52.2
  script:
    - trivy image --exit-code 1 --severity CRITICAL $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

 

--exit-code 1 прерывает пайплайн при обнаружении CRITICAL-уязвимостей.

 

 

 

Изоляция сетей Docker

 

По умолчанию все контейнеры попадают в сеть bridge и могут общаться друг с другом без ограничений. Для изоляции создаём отдельные сети и подключаем к ним только те контейнеры, которым нужна связь.

 

# Создаём изолированные сети
docker network create --internal db_net
docker network create frontend_net

# запускаем БД только в db_net (без доступа к интернету - флаг --internal)
docker run -d \
  --name postgres \
  --network db_net \
  postgres:16

# Приложение подключается к обеим сетям: к БД и к фронтенду
docker run -d \
  --name app \
  --network db_net \
  my_app:1.0

docker network connect frontend_net app

# Nginx - только фронтенд сеть
docker run -d \
  --name nginx \
  --network frontend_net \
  -p 80:80 \
  nginx:1.26

 

 

В Docker Compose изоляция сетей выглядит так:

 

services:
  nginx:
    networks: [frontend]

  app:
    networks: [frontend, backend]

  db:
    networks: [backend]

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true

 

Флаг internal: true запрещает контейнерам в этой сети выходить в интернет. База данных изолирована полностью.

 

 

 

Дополнительные меры

 

No new privileges - запрещает процессу внутри контейнера повышать привилегии через setuid/setgid бинарники:

 

docker run --security-opt no-new-privileges:true nginx:1.26

 

Seccomp-профили ограничивают системные вызовы. Docker применяет профиль по умолчанию (~300 разрешённых из 400+), но можно усилить:

docker run --security-opt seccomp=/path/to/profile.json nginx:1.26

 

 

 

Обновляйте базовые образы. Большинство уязвимостей, которые находит Trivy, закрыты в более новых версиях пакетов. FROM node:20-alpine более безопасен, чем FROM node:20-bullseye - Alpine содержит значительно меньше пакетов.

 

 

Итог

 

Безопасность Docker-контейнеров строится на нескольких независимых уровнях. Non-root пользователь снижает последствия компрометации приложения. Read-only filesystem исключает изменение бинарных файлов через уязвимость. Ограничение capabilities убирает ненужные привилегии ядра. Trivy находит известные CVE до деплоя. Изоляция сетей ограничивает lateral movement при взломе одного контейнера. Ни одна из мер не абсолютна, но их совокупность создаёт реальный барьер.