Files
oakazanin/content/posts/ubuntu-24-docker-hardening/index.md
T

797 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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