--- title: "Ubuntu 24.04: настройка Docker-хоста для production" date: 2026-07-29 draft: false description: "Пошаговая настройка Ubuntu 24.04 LTS как Docker-хоста для production: установка Docker, hardening daemon, UFW + Docker интеграция, systemd-resolved и DNS, изоляция контейнеров, docker.sock." summary: "Пошаговая настройка Ubuntu 24.04 LTS как Docker-хоста для production: установка Docker, hardening daemon, UFW + Docker интеграция, systemd-resolved и DNS, изоляция контейнеров, docker.sock." tags: ["ubuntu 24.04", "docker", "security", "hardening", "ufw", "production", "containers", "linux"] categories: ["Безопасность"] series: ["Linux Hardening"] series_order: 4 --- # Ubuntu 24.04: настройка Docker-хоста для production Статья предполагает что базовый hardening уже сделан по статье: SSH на нестандартном порту, UFW включён, fail2ban настроен. Если нет - сначала туда. {{< article link="/posts/ubuntu-24-base-hardening/">}} В этой статье - Docker и всё что нужно чтобы контейнеры не стали дырой в безопасности хоста. **Что получишь на выходе:** - Docker установлен из официального репозитория (не snap) - daemon.json настроен безопасно - без userns-remap и icc:false по умолчанию - UFW и Docker работают вместе - systemd-resolved и DNS в контейнерах - рабочее решение - docker.sock - понимание риска и контроль доступа - Контейнеры изолированы по сетям, ограничены по ресурсам - Образы проверяются на уязвимости ## Исходные данные - Ubuntu 24.04 LTS с выполненным базовым hardening - Минимум 2GB RAM, 20GB диска - Пользователь с sudo Проверь версию: ```bash lsb_release -a ``` ``` Distributor ID: Ubuntu Description: Ubuntu 24.04.4 LTS Release: 24.04 Codename: noble ``` > [!info] ВАЖНО! > Все команды выполняются с `sudo` если не указано иное. > Если работаешь от root - `sudo` можно опустить. ## Установка Docker > [!danger] ОЧЕНЬ ВАЖНО! > Не используй snap версию в production! В Ubuntu можно поставить Docker через snap (`sudo snap install docker`). Snap-версия Docker работает в собственном confinement-окружении со своими путями (`/var/snap/docker/`), хуже интегрируется с системным `iptables`/`UFW`, и обновляется по графику snap, а не когда нужно тебе. Используй официальный репозиторий Docker - там стандартный путь `/var/lib/docker`, предсказуемое поведение, полный контроль над версией. Проверь что snap-версии нет: ```bash snap list | grep docker ``` Если есть - удали: ```bash sudo snap remove docker ``` ### Удаление старых версий ```bash sudo apt remove -y docker docker-engine docker.io containerd runc ``` ### Установка из официального репозитория Зависимости ```bash sudo apt install -y ca-certificates curl gnupg lsb-release ``` Создать директорию для ключей ```bash sudo install -m 0755 -d /etc/apt/keyrings ``` Добавить GPG ключ Docker ```bash curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \ sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg ``` ```bash sudo chmod a+r /etc/apt/keyrings/docker.gpg ``` Добавить репозиторий ```bash echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null ``` Обновить индекс и установить ```bash sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin ``` Проверь версию: ```bash docker --version ``` Ожидаем: ``` # Docker version 29.x.x, build xxxxxxx ``` Проверь daemon: ```bash sudo systemctl status docker ``` Ожидаем: ```bash ● docker.service - Docker Application Container Engine Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; preset: enabled) Active: active (running) since Wed 2026-07-29 MSK; 3h 38min ago TriggeredBy: ● docker.socket Docs: https://docs.docker.com Main PID: 182611 (dockerd) Tasks: 8 Memory: 23.4M (peak: 24.0M) CPU: 2.661s CGroup: /system.slice/docker.service └─182611 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock ``` ### Добавление пользователя в группу docker ```bash sudo usermod -aG docker $USER ``` Выйди и зайди снова (Выполни `newgrp docker` для текущей сессии без перелогина). Проверь: ```bash docker run --rm hello-world ``` ## docker.sock - главный риск Прежде чем настраивать daemon - разберёмся с самым опасным местом. `/var/run/docker.sock` - Unix сокет Docker daemon. Любой процесс с доступом к нему имеет **полный root на хосте**. Через один вызов API можно смонтировать корень хоста и получить туда доступ: ```bash # Пример атаки - одна команда и ты root на хосте docker run --rm -v /:/host alpine chroot /host ``` **Правила работы с docker.sock:** > [!info] ВАЖНО > Никогда не монтируй docker.sock в контейнер без явной необходимости. Если сервис требует его (Portainer, Watchtower, Traefik с автообнаружением) - этот сервис имеет root-доступ к хосту. Проверь права на сокет: ```bash ls -la /var/run/docker.sock ``` ``` srw-rw---- 1 root docker 0 Jul 29 11:38 /var/run/docker.sock ``` Проверь кто в группе docker: ```bash getent group docker ``` Только доверенные пользователи должны быть в этой группе - фактически это равно правам root. Проверь какие контейнеры монтируют сокет: ```bash docker ps -q | xargs -I {} docker inspect {} \ --format '{{.Name}}: {{range .Mounts}}{{if eq .Source "/var/run/docker.sock"}}DOCKER_SOCK_MOUNTED{{end}}{{end}}' \ 2>/dev/null | grep DOCKER_SOCK_MOUNTED ``` ## Hardening daemon.json ```bash sudo nano /etc/docker/daemon.json ``` ```json { "log-driver": "json-file", "log-opts": { "max-size": "50m", "max-file": "3" }, "live-restore": true, "userland-proxy": false, "no-new-privileges": true, "storage-driver": "overlay2", "default-ulimits": { "nofile": { "Name": "nofile", "Hard": 65536, "Soft": 65536 } } } ``` **Что и почему:** `log-driver` + лимиты - без них логи контейнеров забивают `/var/lib/docker/containers/` за дни на активном сервисе. `live-restore: true` - контейнеры продолжают работать пока daemon перезапускается. `userland-proxy: false` - iptables вместо userland proxy, быстрее, меньше процессов. `no-new-privileges: true` - контейнеры не поднимают привилегии через setuid/setgid бинарники. `storage-driver: overlay2` - зафиксирован явно, хотя на Ubuntu 24.04 он и так используется по умолчанию. ### Почему нет userns-remap Root в контейнере мапится на непривилегированный UID на хосте (например 100000). Звучит безопасно, но ломает bind mounts: файлы на хосте принадлежат UID 1000, контейнер видит UID 100000 и получает `Permission denied`. Каждый сервис с записью на хост требует `chown -R 100000:100000`, и пересчёт нужен для каждого нового сервиса. > [!info] ВАЖНО > Включай только если осознанно готов управлять UID маппингом для каждого тома. ### Почему нет icc:false Глобальный `icc: false` ломает общение контейнеров внутри одного docker-compose стека - Compose создаёт свою bridge сеть, и без `icc` контейнеры в ней не видят друг друга без явных `--link`. Правильная изоляция - через отдельные сети Docker, не через глобальный флаг daemon. Раздел ниже. Примени: ```bash sudo systemctl restart docker ``` Проверь: ```bash docker info | grep -E "Storage Driver|Logging Driver|live-restore" ``` ## sysctl для Docker-хоста ```bash sudo nano /etc/sysctl.d/99-docker.conf ``` ``` # IP forwarding - обязательно для Docker net.ipv4.ip_forward = 1 # IPv6 forwarding - только если используешь IPv6 в контейнерах # net.ipv6.conf.all.forwarding = 1 # Лимиты inotify fs.inotify.max_user_instances = 512 fs.inotify.max_user_watches = 524288 # Для Java приложений (Elasticsearch и подобных) в контейнерах vm.max_map_count = 262144 ``` ```bash sudo sysctl -p /etc/sysctl.d/99-docker.conf ``` ## systemd-resolved и DNS в контейнерах Это самая частая проблема на Ubuntu специфично. Ubuntu 24.04 использует `systemd-resolved` - DNS резолвер слушает на `127.0.0.53`. Контейнер не может достать этот адрес - `127.0.0.53` это loopback хоста, контейнер находится в своём network namespace и видит свой собственный loopback, не хостовый. Симптом: ```bash docker run --rm alpine nslookup google.com # ;; connection timed out; no servers could be reached ``` **Решение 1 (быстрое, для отдельных контейнеров):** ```bash docker run --dns 8.8.8.8 --dns 8.8.4.4 alpine nslookup google.com ``` **Решение 2 (для всех контейнеров через daemon.json):** ```bash sudo nano /etc/docker/daemon.json ``` Добавь: ```json { "dns": ["8.8.8.8", "8.8.4.4"] } ``` ```bash sudo systemctl restart docker ``` **Решение 3 (если нужен внутренний DNS, не публичный):** Узнай реальный upstream DNS на хосте: ```bash resolvectl status | grep "DNS Server" ``` Используй именно его IP (не `127.0.0.53`) в `daemon.json`. Docker Compose поддерживает то же самое на уровне сервиса: ```yaml services: app: dns: - 8.8.8.8 - 8.8.4.4 ``` ## UFW + Docker интеграция Та же проблема что и на любом дистрибутиве - Docker пишет свои iptables правила напрямую, обходя UFW. **Подход 1 (рекомендуемый): порты только на localhost** ```bash # Плохо - обходит UFW docker run -p 80:80 nginx # Хорошо docker run -p 127.0.0.1:80:80 nginx ``` Reverse proxy на хосте слушает 0.0.0.0:443, UFW разрешает только 443, контейнеры недоступны напрямую. **Подход 2: DOCKER-USER chain** ```bash sudo nano /etc/ufw/after.rules ``` Добавь в конец: ``` # BEGIN UFW AND DOCKER *filter :DOCKER-USER - [0:0] -A DOCKER-USER -s 10.0.0.0/8 -j RETURN -A DOCKER-USER -s 172.16.0.0/12 -j RETURN -A DOCKER-USER -s 192.168.0.0/16 -j RETURN -A DOCKER-USER -j ufw-user-forward -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16 -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8 -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12 -A DOCKER-USER -j RETURN COMMIT # END UFW AND DOCKER ``` ```bash sudo ufw reload ``` Для большинства случаев достаточно **подхода 1**. ## Сетевая изоляция контейнеров Вместо глобального `icc: false` - отдельные сети под каждое приложение: ```bash docker network create --driver bridge app_network docker run -d --name app --network app_network myapp docker run -d --name db --network app_network postgres ``` Контейнеры из разных сетей не видят друг друга. В docker-compose сети создаются автоматически для каждого стека - это и есть рабочая изоляция. ## Безопасность образов ### Trivy ```bash 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 \ $(lsb_release -sc) main" | \ sudo tee /etc/apt/sources.list.d/trivy.list > /dev/null sudo apt update sudo apt install -y trivy ``` ```bash trivy image nginx:alpine ``` Перед деплоем: ```bash trivy image --exit-code 1 --severity CRITICAL,HIGH nginx:alpine ``` Правило: CRITICAL уязвимости - образ не идёт в production. ### Выбор образов ```dockerfile # Большой attack surface FROM ubuntu:24.04 # Минимальный FROM alpine:3.20 # Ещё меньше - нет shell, нет пакетного менеджера FROM gcr.io/distroless/static-debian12 ``` Конкретные теги, не `latest`: ```bash docker pull nginx:1.27-alpine ``` ## Изоляция контейнеров ### Capabilities ```bash docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx ``` ### Read-only filesystem ```bash docker run --read-only --tmpfs /tmp --tmpfs /var/run nginx ``` ### Seccomp Docker применяет дефолтный seccomp профиль автоматически: ```bash docker info | grep seccomp ``` ### Никогда не использовать `--privileged` ```bash # НИКОГДА в production - отключает всю изоляцию docker run --privileged nginx ``` ## Ресурсные ограничения ```bash # Memory с soft limit docker run -m 512m --memory-reservation 256m nginx # CPU docker run --cpus="0.5" nginx # PID limit - защита от fork bomb docker run --pids-limit 200 nginx ``` Проверка OOM: ```bash dmesg | grep -i "oom\|killed process" docker inspect CONTAINER --format='{{.State.OOMKilled}}' ``` ## Docker Compose с hardening ```yaml services: web: image: nginx:1.27-alpine container_name: web restart: unless-stopped ports: - "127.0.0.1:8080:80" dns: - 8.8.8.8 - 8.8.4.4 deploy: resources: limits: cpus: "0.5" memory: 256M reservations: memory: 128M cap_drop: - ALL cap_add: - NET_BIND_SERVICE read_only: true tmpfs: - /tmp - /var/cache/nginx - /var/run security_opt: - no-new-privileges:true pids_limit: 100 logging: driver: "json-file" options: max-size: "50m" max-file: "3" networks: - frontend db: image: postgres:16-alpine container_name: db restart: unless-stopped environment: POSTGRES_PASSWORD_FILE: /run/secrets/db_password volumes: - db_data:/var/lib/postgresql/data deploy: resources: limits: cpus: "1.0" memory: 512M cap_drop: - ALL cap_add: - SETUID - SETGID - DAC_OVERRIDE - CHOWN security_opt: - no-new-privileges:true pids_limit: 200 logging: driver: "json-file" options: max-size: "50m" max-file: "3" networks: - backend app: image: myapp:1.2.3 container_name: app restart: unless-stopped networks: - frontend - backend depends_on: - db cap_drop: - ALL security_opt: - no-new-privileges:true pids_limit: 100 logging: driver: "json-file" options: max-size: "50m" max-file: "3" networks: frontend: driver: bridge backend: driver: bridge internal: true volumes: db_data: secrets: db_password: file: ./secrets/db_password.txt ``` `internal: true` для backend - база данных без доступа к интернету. ## Регулярные задачи обслуживания ```bash # Что занимает место docker system df # Очистка неиспользуемого docker system prune -f # Полная очистка включая неиспользуемые образы docker system prune -a -f # Volumes - ОСТОРОЖНО, удаляет данные безвозвратно docker volume prune -f ``` Cron: ```bash sudo crontab -e ``` ``` 0 4 * * 0 docker system prune -f >> /var/log/docker-prune.log 2>&1 ``` Метрики: ```bash docker stats --no-stream ``` ### Аудит через auditd ```bash sudo apt install -y auditd ``` ```bash sudo nano /etc/audit/rules.d/docker.rules ``` ``` -w /usr/bin/docker -p rwxa -k docker -w /var/lib/docker -p rwxa -k docker -w /etc/docker -p rwxa -k docker -w /var/run/docker.sock -p rwxa -k docker ``` ```bash sudo systemctl enable auditd ``` ```bash sudo systemctl restart auditd ``` ```bash sudo ausearch -k docker | tail -20 ``` ## Troubleshooting ### DNS не работает в контейнере См. раздел "systemd-resolved и DNS в контейнерах" выше - это специфика Ubuntu, самая частая проблема на этом дистрибутиве. --- ### Docker обходит UFW **Причина:** Docker пишет напрямую в iptables. **Решение:** `127.0.0.1:port:port` или DOCKER-USER chain. --- ### Permission denied на bind mount **Причина:** UID процесса в контейнере не совпадает с владельцем файлов на хосте. ```bash docker run --rm IMAGE id ``` ```bash sudo chown -R UID:GID /path/to/mount ``` --- ### OOM killer убивает контейнер ```bash dmesg | grep -i "oom\|killed process" ``` ```bash docker inspect CONTAINER --format='{{.State.OOMKilled}}' ``` Увеличить `memory` limit или оптимизировать приложение. --- ### newgrp docker не сработал после usermod **Симптом:** `docker: permission denied` несмотря на `usermod -aG docker $USER`. **Причина:** Группа применяется только к новым сессиям. Текущая сессия не знает о изменении. **Решение:** ```bash newgrp docker ``` Или просто выйти и зайти заново по SSH. --- ### Диск забит логами ```bash sudo find /var/lib/docker/containers/ -name "*.log" -exec du -sh {} \; | sort -rh | head -10 ``` Экстренно: ```bash sudo truncate -s 0 /var/lib/docker/containers/CONTAINER_ID/CONTAINER_ID-json.log ``` Постоянно - `max-size`/`max-file` в daemon.json (уже настроено). ### Финальная проверка ```bash sudo systemctl status docker docker info | grep -E "Storage Driver|Logging Driver|live-restore" docker run --rm alpine nslookup google.com ss -tulnp | grep docker docker system df ``` ## Отличия от Debian 12 | Аспект | Debian 12 | Ubuntu 24.04 | |--------|-----------|--------------| | Snap версия Docker | Не существует | Существует, избегать | | DNS resolver | Обычно прямой `/etc/resolv.conf` | systemd-resolved (127.0.0.53), требует явной настройки DNS в Docker | | Группа docker | `newgrp docker` работает | `newgrp docker` работает, та же команда | | UFW | Нужно устанавливать | Предустановлен | | GPG ключ репозитория Docker | `download.docker.com/linux/debian/gpg` | `download.docker.com/linux/ubuntu/gpg` | ## Что дальше Docker-хост готов к production нагрузке. - Reverse proxy - Traefik или nginx - Мониторинг - Prometheus + cAdvisor + Grafana - Централизованные логи - Loki + Promtail - Секреты - HashiCorp Vault или Docker Secrets **Стек этой статьи:** Ubuntu 24.04 LTS (Noble Numbat) · Docker CE · UFW · systemd-resolved · auditd · Trivy · docker-compose