diff --git a/content/posts/ubuntu-24-base-hardening/index.md b/content/posts/ubuntu-24-base-hardening/index.md index c08cb50..97e6eb5 100644 --- a/content/posts/ubuntu-24-base-hardening/index.md +++ b/content/posts/ubuntu-24-base-hardening/index.md @@ -46,7 +46,8 @@ Codename: noble **Про пользователя:** Ubuntu при установке создаёт непривилегированного пользователя с sudo. Если устанавливал с таким пользователем - используй его. Если получил root-only сервер (облако, VPS) - создадим пользователя в разделе SSH. -Все команды выполняются с `sudo` если не указано иное. Если работаешь от root - `sudo` можно опустить. +> [!info] ВАЖНО +> Все команды выполняются с `sudo` если не указано иное. Если работаешь от root - `sudo` можно опустить. ## Обновление системы diff --git a/content/posts/ubuntu-24-docker-hardening/index.md b/content/posts/ubuntu-24-docker-hardening/index.md new file mode 100644 index 0000000..e4601d7 --- /dev/null +++ b/content/posts/ubuntu-24-docker-hardening/index.md @@ -0,0 +1,796 @@ +--- +title: "Ubuntu 24.04: настройка Docker-хоста для production" +date: 2026-07-29 +draft: true +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