diff --git a/content/posts/tips-debian-12-hardening/featured.png b/content/posts/debian-12-base-hardening/featured.png similarity index 100% rename from content/posts/tips-debian-12-hardening/featured.png rename to content/posts/debian-12-base-hardening/featured.png diff --git a/content/posts/debian-12-base-hardening/index.md b/content/posts/debian-12-base-hardening/index.md new file mode 100644 index 0000000..469eb82 --- /dev/null +++ b/content/posts/debian-12-base-hardening/index.md @@ -0,0 +1,837 @@ +--- +title: "Debian 12: базовая настройка и hardening для production" +date: 2026-05-22 +draft: false +description: "Пошаговая настройка чистого Debian 12 (Bookworm) для production: SSH, firewall, fail2ban, swap, sysctl, автообновления, логи, бэкап конфигов." +tags: ["debian 12", "security", "hardening", "ssh", "ufw", "fail2ban", "sysctl", "production", "linux"] +categories: ["Системное администрирование"] +series: ["Linux Hardening"] +series_order: 1 +--- + +# Debian 12: базовая настройка и hardening для production + +Поставил чистый Debian 12 и думаешь сразу накатывать приложения? Не торопись. Час на базовую настройку сейчас - это недели сэкономленного времени на разгребание последствий взлома или падения. + +Статья покрывает всё что нужно для production-сервера без контейнеров. Пройдёшь по шагам и получишь сервер с закрытым SSH, firewall, защитой от брутфорса, ограничением ресурсов ядра и автоматическими обновлениями безопасности. Docker - в следующей статье серии. + +## Исходные данные + +- Чистый Debian 12 (Bookworm) +- Root доступ по SSH +- Статический IP + +Проверь версию: + +```bash +lsb_release -a +``` + +Ожидаемый вывод: +``` +Distributor ID: Debian +Description: Debian GNU/Linux 12 (bookworm) +Release: 12 +Codename: bookworm +``` + +Все команды выполняются от `root` если не указано иное. + +## Обновление системы + +```bash +apt update +apt upgrade -y +apt dist-upgrade -y +apt autoremove -y +``` + +Проверь нужна ли перезагрузка: + +```bash +[ -f /var/run/reboot-required ] && echo "Reboot needed" || echo "No reboot needed" +``` + +Если нужна - перезагрузись: + +```bash +reboot +``` + +## Базовые утилиты + +```bash +apt install -y \ + sudo curl wget git vim htop iotop iftop \ + net-tools dnsutils tcpdump screen tmux rsync \ + util-linux unzip ca-certificates gnupg2 lsb-release \ + smartmontools +``` + +## Настройка системы + +### Hostname + +```bash +hostnamectl set-hostname srv01.example.com +``` + +Добавь в `/etc/hosts`: + +```bash +nano /etc/hosts +``` + +``` +127.0.0.1 localhost +203.0.113.10 srv01.example.com srv01 + +::1 localhost ip6-localhost ip6-loopback +ff02::1 ip6-allnodes +ff02::2 ip6-allrouters +``` + +Замени `203.0.113.10` на реальный IP сервера, `srv01.example.com` - на своё имя. + +Проверь FQDN: + +```bash +hostname -f +# srv01.example.com +``` + +### Timezone + +```bash +timedatectl set-timezone Europe/Moscow +``` + +Проверь: + +```bash +timedatectl +``` + +### Локаль + +```bash +apt install -y locales +dpkg-reconfigure locales +``` + +Выбери `en_US.UTF-8` как основную. Добавь `ru_RU.UTF-8` если нужна кириллица в системных сообщениях. + +Проверь: + +```bash +locale +# LANG=en_US.UTF-8 +``` + +## Пользователь и SSH + +### Создание пользователя + +Root сессия - одна ошибка без возможности откатиться. `sudo` даёт контроль и лог действий. Создай непривилегированного пользователя: + +```bash +adduser admin +``` + +Добавь в sudo: + +```bash +usermod -aG sudo admin +``` + +Проверь: + +```bash +groups admin +# admin : admin sudo +``` + +### SSH ключи + +На своей рабочей машине сгенерируй ключ: + +```bash +# Linux/macOS +ssh-keygen -t ed25519 -C "admin@srv01" +``` + +```powershell +# Windows +ssh-keygen -t ed25519 +``` + +Скопируй публичный ключ на сервер: + +```bash +# Linux/macOS +ssh-copy-id admin@203.0.113.10 +``` + +```powershell +# Windows +type $env:userprofile\.ssh\id_ed25519.pub | ssh admin@203.0.113.10 "mkdir -m 700 -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys" +``` + +Проверь вход по ключу - не должен спрашивать пароль: + +```bash +ssh admin@203.0.113.10 +``` + +### SSH hardening + +Залогинься под `admin` и открой конфиг: + +```bash +sudo nano /etc/ssh/sshd_config +``` + +Замени содержимое (или найди и измени каждый параметр): + +``` +# Нестандартный порт - отсекает 90% ботов +Port 2222 + +# Только ключи +PubkeyAuthentication yes +AuthorizedKeysFile .ssh/authorized_keys + +# Запретить root логин +PermitRootLogin no + +# Только ключи, без паролей +PasswordAuthentication no +PermitEmptyPasswords no +KbdInteractiveAuthentication no + +# Ограничения сессий +MaxAuthTries 3 +MaxSessions 3 +LoginGraceTime 30 + +# Таймауты - отключать неактивные сессии +ClientAliveInterval 300 +ClientAliveCountMax 2 + +# Отключить лишнее +X11Forwarding no +PrintMotd no +UsePAM yes +AcceptEnv LANG LC_* + +# Только IPv4 (убери если используешь IPv6) +AddressFamily inet +``` + +**КРИТИЧНО:** Перед перезапуском SSH открой вторую сессию и не закрывай её - страховка если что-то пойдёт не так. + +Проверь конфиг на ошибки: + +```bash +sudo sshd -t +``` + +Нет вывода - нет ошибок. Перезапусти: + +```bash +sudo systemctl restart sshd +``` + +В новом терминале проверь подключение на новом порту: + +```bash +ssh -p 2222 admin@203.0.113.10 +``` + +Если работает - старые сессии можно закрывать. + +### SSH banner (опционально) + +Создай предупреждение при входе: + +```bash +sudo nano /etc/ssh/banner +``` + +``` +############################################################################### +# Доступ только для авторизованных пользователей. # +# Все действия логируются. # +# Несанкционированный доступ преследуется по закону. # +############################################################################### +``` + +Добавь в `sshd_config`: + +``` +Banner /etc/ssh/banner +``` + +Перезапусти SSH: + +```bash +sudo systemctl restart sshd +``` + +## Firewall (UFW) + +**Важно:** сначала настраиваем правила, потом включаем. Иначе заблокируешь себя. + +```bash +sudo apt install -y ufw +``` + +Политика по умолчанию: + +```bash +# Блокировать все входящие +sudo ufw default deny incoming + +# Разрешить все исходящие +sudo ufw default allow outgoing +``` + +Разрешаем SSH на новом порту: + +```bash +# Замени 2222 на свой порт если менял +sudo ufw allow 2222/tcp comment 'SSH' +``` + +Включаем firewall: + +```bash +sudo ufw enable +``` + +Проверь статус: + +```bash +sudo ufw status verbose +``` + +Ожидаемый вывод: +``` +Status: active +Logging: on (low) +Default: deny (incoming), allow (outgoing), disabled (routed) +New profiles: skip + +To Action From +-- ------ ---- +2222/tcp ALLOW IN Anywhere # SSH +2222/tcp (v6) ALLOW IN Anywhere (v6) # SSH +``` + +### Rate limiting на SSH + +```bash +# Блокировать IP после 6 подключений за 30 секунд +sudo ufw limit 2222/tcp comment 'SSH rate limit' +``` + +### Открытие дополнительных портов + +Когда будешь ставить сервисы - открывай только нужные порты: + +```bash +# HTTP/HTTPS +sudo ufw allow 80/tcp comment 'HTTP' +sudo ufw allow 443/tcp comment 'HTTPS' + +# PostgreSQL только с конкретного IP +sudo ufw allow from 192.168.1.10 to any port 5432 comment 'PostgreSQL' +``` + +Проверь что слушает на хосте: + +```bash +ss -tulnp +``` + +Всё что слушает - должно быть либо закрыто firewall, либо намеренно открыто. + +## Fail2ban + +```bash +sudo apt install -y fail2ban +``` + +Создай локальный конфиг - не редактируй `jail.conf`, он перезаписывается при обновлении: + +```bash +sudo nano /etc/fail2ban/jail.local +``` + +```ini +[DEFAULT] +# Бан на 1 час +bantime = 3600 +# За 10 минут +findtime = 600 +# После 3 неудачных попыток +maxretry = 3 +# Не банить свои IP +ignoreip = 127.0.0.1/8 ::1 +# Явно указываем backend - важно для совместимости в серии Ubuntu/Rocky +backend = systemd + +[sshd] +enabled = true +port = 2222 +filter = sshd +maxretry = 3 +bantime = 3600 +``` + +Замени `2222` на свой SSH порт. + +Запусти: + +```bash +sudo systemctl enable fail2ban +sudo systemctl start fail2ban +``` + +Проверь статус: + +```bash +sudo fail2ban-client status +``` + +``` +Status +|- Number of jail: 1 +`- Jail list: sshd +``` + +Проверь статус конкретного jail: + +```bash +sudo fail2ban-client status sshd +``` + +## Swap + +Swap в виде файла гибче раздела: можно увеличить или уменьшить без переразбивки диска. + +### Создание swap файла + +Проверь текущий swap: + +```bash +free -h +sudo swapon --show +``` + +Создай файл (пример - 2GB): + +```bash +# Создать файл нужного размера +sudo fallocate -l 2G /swapfile +``` + +Если `fallocate` не поддерживается файловой системой: + +```bash +sudo dd if=/dev/zero of=/swapfile bs=1M count=2048 status=progress +``` + +Установи права - swap файл не должен читаться другими пользователями: + +```bash +sudo chmod 600 /swapfile +``` + +Инициализируй и активируй: + +```bash +# Разметить как swap +sudo mkswap /swapfile + +# Включить +sudo swapon /swapfile +``` + +Проверь: + +```bash +free -h +sudo swapon --show +``` + +Добавь в `/etc/fstab` для автомонтирования после перезагрузки: + +```bash +echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab +``` + +### Настройка swappiness + +`swappiness` определяет агрессивность использования swap. Для production снижаем - swap только когда RAM заполнена на 90%+: + +```bash +# Проверить текущее значение (по умолчанию 60) +cat /proc/sys/vm/swappiness +``` + +Установи постоянно: + +```bash +echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.d/99-swap.conf +sudo sysctl -p /etc/sysctl.d/99-swap.conf +``` + +### Изменение размера swap (когда потребуется) + +```bash +# Отключить текущий swap +sudo swapoff /swapfile + +# Изменить размер (пример - увеличить до 4GB) +sudo fallocate -l 4G /swapfile + +# Переинициализировать +sudo mkswap /swapfile + +# Включить +sudo swapon /swapfile +``` + +Проверь результат: + +```bash +free -h +``` + +Запись в `/etc/fstab` менять не нужно - путь тот же. + +### Удаление swap (если потребуется) + +```bash +sudo swapoff /swapfile +sudo rm /swapfile +``` + +Удали строку из `/etc/fstab`: + +```bash +sudo sed -i '/\/swapfile/d' /etc/fstab +``` + +## Sysctl - защита ядра + +Создай отдельный файл - чище чем писать в `/etc/sysctl.conf`: + +```bash +sudo nano /etc/sysctl.d/99-hardening.conf +``` + +``` +# Защита от SYN flood +net.ipv4.tcp_syncookies = 1 +net.ipv4.tcp_max_syn_backlog = 2048 +net.ipv4.tcp_synack_retries = 2 + +# Игнорировать ICMP redirects (защита от MITM) +net.ipv4.conf.all.accept_redirects = 0 +net.ipv4.conf.default.accept_redirects = 0 +net.ipv6.conf.all.accept_redirects = 0 +net.ipv6.conf.default.accept_redirects = 0 + +# Не отправлять ICMP redirects +net.ipv4.conf.all.send_redirects = 0 +net.ipv4.conf.default.send_redirects = 0 + +# Игнорировать source routed packets +net.ipv4.conf.all.accept_source_route = 0 +net.ipv6.conf.all.accept_source_route = 0 + +# Защита от IP spoofing +net.ipv4.conf.all.rp_filter = 1 +net.ipv4.conf.default.rp_filter = 1 + +# Логировать подозрительные пакеты +net.ipv4.conf.all.log_martians = 1 + +# Защита от time-wait assassination +net.ipv4.tcp_rfc1337 = 1 + +# Отключить IPv6 если не используешь +# net.ipv6.conf.all.disable_ipv6 = 1 +# net.ipv6.conf.default.disable_ipv6 = 1 +``` + +Примени: + +```bash +sudo sysctl -p /etc/sysctl.d/99-hardening.conf +``` + +Проверь что применилось: + +```bash +sudo sysctl net.ipv4.tcp_syncookies +# net.ipv4.tcp_syncookies = 1 +``` + +## Автообновления безопасности + +```bash +sudo apt install -y unattended-upgrades apt-listchanges +``` + +```bash +sudo nano /etc/apt/apt.conf.d/50unattended-upgrades +``` + +Раскомментируй/измени: + +``` +Unattended-Upgrade::Mail "root"; +Unattended-Upgrade::Remove-Unused-Kernel-Packages "true"; +Unattended-Upgrade::Remove-Unused-Dependencies "true"; +Unattended-Upgrade::Automatic-Reboot "false"; +``` + +`Automatic-Reboot "false"` - важно для production. Перезагрузку после обновления ядра делать вручную в удобное время. + +Включи: + +```bash +sudo dpkg-reconfigure -plow unattended-upgrades +``` + +Выбери `Yes`. + +Настрой расписание: + +```bash +sudo nano /etc/apt/apt.conf.d/10periodic +``` + +``` +APT::Periodic::Update-Package-Lists "1"; +APT::Periodic::Download-Upgradeable-Packages "1"; +APT::Periodic::AutocleanInterval "7"; +APT::Periodic::Unattended-Upgrade "1"; +``` + +Проверь что сервис работает: + +```bash +sudo systemctl status unattended-upgrades +``` + +## Отключение ненужных сервисов (при наличии) + +Посмотри что запущено: + +```bash +systemctl list-units --type=service --state=running +``` + +Стандартные кандидаты на отключение - проверь что не используешь перед отключением: + +```bash +# Bluetooth - на сервере не нужен +sudo systemctl disable --now bluetooth.service + +# avahi-daemon - mDNS/DNS-SD, нужен редко +sudo systemctl disable --now avahi-daemon.service + +# ModemManager - управление модемами +sudo systemctl disable --now ModemManager.service +``` + +Проверь что слушает на сетевых портах: + +```bash +ss -tulnp +``` + +Всё незнакомое - выясни что это и реши нужно ли. + +## Логи и ротация + +### Ограничение journald + +```bash +sudo nano /etc/systemd/journald.conf +``` + +Раскомментируй/измени: + +``` +SystemMaxUse=500M +SystemMaxFileSize=100M +MaxRetentionSec=1month +``` + +Перезапусти: + +```bash +sudo systemctl restart systemd-journald +``` + +Проверь размер: + +```bash +sudo journalctl --disk-usage +``` + +На чистом Debian 12 rsyslog не установлен - логи пишутся только в journald. Настройки выше достаточно. +Если устанавливаешь rsyslog отдельно - создай /etc/logrotate.d/rsyslog с правилами ротации вручную. + +## Мониторинг диска + +На VPS имя устройства зависит от гипервизора - не всегда `/dev/sda`. Определи актуальное: + +```bash +# Найти дисковые устройства +lsblk -d -o NAME,TYPE | grep disk +# NAME TYPE +# vda disk +# +# Результат зависит от гипервизора и провайдера: +# /dev/sda - KVM у некоторых провайдеров, Xen +# /dev/vda - KVM (наиболее распространённый вариант) +# /dev/xvda - Xen +``` + +Проверь состояние: + +```bash +# Замени vda на своё устройство +sudo smartctl -H /dev/vda +``` + +Ожидаемый вывод: +``` +# ATA диски: +# SMART overall-health self-assessment test result: PASSED +# +# SCSI/NVMe диски (типично для VPS): +# SMART Health Status: OK +# +# Оба варианта означают одно: диск здоров +``` + +Если `FAILED` - диск умирает. Срочно бэкап и замена. + +Детали SMART: + +```bash +sudo smartctl -a /dev/vda +``` + +Обращай внимание на `Reallocated_Sector_Ct`, `Pending_Sector_Count`, `Offline_Uncorrectable` - если ненулевые, диск проблемный. + +Примечание: +На виртуальных дисках (VMware, KVM) SMART может быть недоступен: `SMART support is: Unavailable - device lacks SMART capability.` +В этом случае smartctl полезен только для физических серверов. +На VPS ориентируйся на мониторинг от провайдера. + +## Бэкап конфигов + +Конфиги на диске - не бэкап. Скрипт архивирует `/etc/` и SSH ключи. + +```bash +nano ~/backup-configs.sh +``` + +```bash +#!/bin/bash + +BACKUP_DIR="$HOME/backups" +DATE=$(date +%Y%m%d_%H%M%S) +BACKUP_FILE="$BACKUP_DIR/config_backup_$DATE.tar.gz" + +mkdir -p "$BACKUP_DIR" + +# Сохранить список установленных пакетов +dpkg --get-selections > "$HOME/installed-packages.txt" + +# Архивировать конфиги и SSH ключи +sudo tar -czf "$BACKUP_FILE" \ + /etc/ \ + /root/.ssh/ \ + /home/*/.ssh/ \ + "$HOME/installed-packages.txt" \ + 2>/dev/null + +echo "Backup: $BACKUP_FILE" +ls -lh "$BACKUP_FILE" + +# Удалить бэкапы старше 30 дней +find "$BACKUP_DIR" -name "config_backup_*.tar.gz" -mtime +30 -delete +``` + +Права и первый запуск: + +```bash +chmod +x ~/backup-configs.sh +~/backup-configs.sh +``` + +Ожидаемый вывод: +``` +Backup: /home/admin/backups/config_backup_20260522_030000.tar.gz +-rw-r--r-- 1 root root 1.2M May 22 03:00 /home/admin/backups/config_backup_20260522_030000.tar.gz +``` + +Автоматизация через cron (каждое воскресенье в 3:00): + +```bash +crontab -e +``` + +``` +0 3 * * 0 /home/admin/backup-configs.sh >> /var/log/backup-configs.log 2>&1 +``` + +### Восстановление + +Восстановить конфиги: + +```bash +sudo tar -xzf ~/backups/config_backup_ДАТА.tar.gz -C / +``` + +Восстановить список пакетов: + +```bash +sudo dpkg --set-selections < ~/installed-packages.txt +sudo apt-get dselect-upgrade +``` + +## Финальная проверка +```bash +# Открытые порты +ss -tulnp + +# Статус firewall +sudo ufw status verbose + +# Статус fail2ban +sudo fail2ban-client status sshd + +# Состояние диска +sudo smartctl -H /dev/vda + +# Swap +free -h +``` + +## Что дальше + +Сервер готов к установке приложений. База одинакова для любой нагрузки - веб, база данных, почта. + +Если следующий шаг - контейнеры, читай продолжение серии: [Debian 12: настройка Docker-хоста для production](/posts/debian-12-docker-hardening/). + +**Стек этой статьи:** Debian 12 (Bookworm) · UFW · Fail2ban · OpenSSH · unattended-upgrades · systemd · smartmontools diff --git a/content/posts/tips-debian-12-hardening/index.md b/content/posts/debian-12-base-hardening/old_index.md similarity index 100% rename from content/posts/tips-debian-12-hardening/index.md rename to content/posts/debian-12-base-hardening/old_index.md diff --git a/content/posts/debian-12-docker-hardening/featured.png b/content/posts/debian-12-docker-hardening/featured.png new file mode 100644 index 0000000..9d1c913 Binary files /dev/null and b/content/posts/debian-12-docker-hardening/featured.png differ diff --git a/content/posts/debian-12-docker-hardening/index.md b/content/posts/debian-12-docker-hardening/index.md new file mode 100644 index 0000000..1e9940c --- /dev/null +++ b/content/posts/debian-12-docker-hardening/index.md @@ -0,0 +1,869 @@ +--- +title: "Debian 12: настройка Docker-хоста для production" +date: 2026-05-22 +draft: true +description: "Пошаговая настройка Debian 12 как Docker-хоста для production: установка Docker, настройка daemon, UFW + Docker интеграция, изоляция контейнеров, docker.sock, ресурсные ограничения." +tags: ["debian 12", "docker", "security", "hardening", "ufw", "production", "containers", "linux"] +categories: ["Системное администрирование"] +series: ["Linux Hardening"] +series_order: 2 +--- + +# Debian 12: настройка Docker-хоста для production + +Статья предполагает что базовая настройка уже выполнена: SSH на порту 2222, UFW включён, fail2ban настроен, sysctl применён. +Если нет - сначала: +{{< article link="/posts/debian-12-base-hardening/">}} + +Здесь ставим Docker и закрываем типичные дыры: daemon без избыточных привилегий, UFW который не обходится Docker, изоляция контейнеров по сетям. Плюс разбираем docker.sock - самое опасное место на Docker-хосте. + +## Исходные данные + +- Debian 12 с выполненным базовым hardening +- Минимум 2GB RAM, 20GB диска + +Проверь версию: + +```bash +lsb_release -a +``` + +``` +Distributor ID: Debian +Description: Debian GNU/Linux 12 (bookworm) +Release: 12 +Codename: bookworm +``` + +Все команды выполняются с `sudo` если не указано иное. + +## Установка Docker + +### Удаление старых версий + +Если был установлен Docker из репозиториев Debian - удали. Версия там устаревшая: + +```bash +sudo apt remove -y docker docker-engine docker.io containerd runc docker-compose docker-doc podman-docker +``` + +### Установка из официального репозитория + +>Команды установки актуальны на момент написания статьи. Docker меняет процедуру добавления репозитория - перед установкой сверяйся с официальной документацией: docs.docker.com/engine/install/debian + +```bash +# Установить зависимости +sudo apt update +sudo apt install -y ca-certificates curl + +# Создать директорию для ключей +sudo install -m 0755 -d /etc/apt/keyrings + +# Добавить GPG ключ Docker +sudo curl -fsSL https://download.docker.com/linux/debian/gpg \ + -o /etc/apt/keyrings/docker.asc +sudo chmod a+r /etc/apt/keyrings/docker.asc + +# Добавить репозиторий +sudo tee /etc/apt/sources.list.d/docker.sources <Этот раздел - справочный. Конкретные настройки применяются при разворачивании каждого сервиса. + +Docker создаёт свои iptables правила и **обходит UFW**. Порт опубликованный через `-p 80:80` становится доступен снаружи даже если UFW его не разрешал. Это не баг - это архитектурное решение Docker. + +Два подхода к решению: + +### Подход 1 - рекомендуемый: порты на localhost + +Публикуй порты только на localhost, снаружи открывай через reverse proxy (nginx, Traefik): + +```bash +# Плохо - порт открыт на всех интерфейсах, обходит UFW +docker run -p 80:80 nginx + +# Хорошо - порт только на localhost +docker run -p 127.0.0.1:80:80 nginx +``` + +В docker-compose.yml: + +```yaml +ports: + - "127.0.0.1:8080:80" +``` + +Reverse proxy слушает на `0.0.0.0:443` и проксирует на `127.0.0.1:8080`. UFW разрешает только 443. Docker UFW не обходит - нечего обходить. + +Для большинства production сетапов достаточно этого подхода - он проще и надёжнее. + +### Подход 2: DOCKER-USER chain + +Docker оставляет пустую цепочку `DOCKER-USER` для пользовательских правил. Правила в ней применяются до правил Docker. Нужен когда по каким-то причинам нельзя использовать reverse proxy: + +```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 + +# Применить правила UFW к трафику идущему в контейнеры +-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 +``` + +Перезагрузи UFW: + +```bash +sudo ufw reload +``` + +## Сетевая изоляция контейнеров + +>Этот раздел - справочный. Сети создаются при разворачивании каждого сервиса. + +Вместо глобального `icc: false` - изоляция через сети. + +По умолчанию все контейнеры запущенные без явной сети попадают в `bridge` сеть и видят друг друга. Создавай отдельную сеть для каждого приложения: + +```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 +``` + +Контейнеры из разных сетей не могут общаться - это и есть правильная изоляция без глобального `icc: false`. + +В docker-compose.yml сети создаются автоматически для каждого стека. Явно задавай сети если нужна дополнительная изоляция внутри стека. + +## Безопасность образов + +### Сканирование уязвимостей - Trivy + +```bash +# Добавить репозиторий Trivy +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 +admin@vps01:~$ trivy image nginx:alpine +2026-06-09T12:22:41+03:00 INFO [vulndb] Need to update DB +2026-06-09T12:22:41+03:00 INFO [vulndb] Downloading vulnerability DB... +2026-06-09T12:22:41+03:00 INFO [vulndb] Downloading artifact... repo="mirror.gcr.io/aquasec/trivy-db:2" +95.58 MiB / 95.58 MiB [---------------------------------------------------------------------] 100.00% 763.08 KiB p/s 2m8s +2026-06-09T12:24:52+03:00 INFO [vulndb] Artifact successfully downloaded repo="mirror.gcr.io/aquasec/trivy-db:2" +2026-06-09T12:24:52+03:00 INFO [vuln] Vulnerability scanning is enabled +2026-06-09T12:24:52+03:00 INFO [secret] Secret scanning is enabled +2026-06-09T12:24:52+03:00 INFO [secret] If your scanning is slow, please try '--scanners vuln' to disable secret scanning +2026-06-09T12:24:52+03:00 INFO [secret] Please see https://trivy.dev/docs/v0.71/guide/scanner/secret#recommendation for faster secret detection +2026-06-09T12:25:11+03:00 INFO Detected OS family="alpine" version="3.23.4" +2026-06-09T12:25:11+03:00 INFO [alpine] Detecting vulnerabilities... os_version="3.23" repository="3.23" pkg_num=71 +2026-06-09T12:25:11+03:00 INFO Number of language-specific files num=0 + +Report Summary + +┌──────────────────────────────┬────────┬─────────────────┬─────────┐ +│ Target │ Type │ Vulnerabilities │ Secrets │ +├──────────────────────────────┼────────┼─────────────────┼─────────┤ +│ nginx:alpine (alpine 3.23.4) │ alpine │ 1 │ - │ +└──────────────────────────────┴────────┴─────────────────┴─────────┘ +Legend: +- '-': Not scanned +- '0': Clean (no security findings detected) + + +nginx:alpine (alpine 3.23.4) + +Total: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 1, CRITICAL: 0) + +┌─────────┬───────────────┬──────────┬────────┬───────────────────┬───────────────┬─────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │ +├─────────┼───────────────┼──────────┼────────┼───────────────────┼───────────────┼─────────────────────────────────────────────────┤ +│ libxml2 │ CVE-2026-6732 │ HIGH │ fixed │ 2.13.9-r0 │ 2.13.9-r1 │ libxml2: libxml2: Denial of Service via crafted │ +│ │ │ │ │ │ │ XSD-validated document │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-6732 │ +└─────────┴───────────────┴──────────┴────────┴───────────────────┴───────────────┴─────────────────────────────────────────────────┘ +``` + +>Status `fixed` - исправление уже есть, обнови образ +>Status `affected` - исправления нет, принимай решение осознанно + +Правило для production: образы с CRITICAL уязвимостями не идут в production. HIGH - анализировать и принимать решение. + +Сканировать перед каждым деплоем: + +```bash +# Выйти с ненулевым кодом если есть CRITICAL/HIGH - удобно для CI +trivy image --exit-code 1 --severity CRITICAL,HIGH nginx:alpine +``` + +### Аудит событий Docker + +Auditd фиксирует все обращения к Docker на уровне ядра: запуск контейнеров, изменения конфигурации, доступ к docker.sock. Когда что-то пойдёт не так - это первое место где искать что произошло и когда. + +```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 +sudo systemctl restart auditd +``` + +Просматривай события: + +```bash +sudo ausearch -k docker | tail -20 +``` + +### Правила выбора образов + +>Этот раздел - справочный. Применяй при выборе базового образа для своих Dockerfile. + +Alpine-based или distroless - меньше attack surface: + +```dockerfile +# 200MB лишних пакетов и уязвимостей +FROM ubuntu:24.04 + +# 5MB, минимум уязвимостей +FROM alpine:3.20 + +# Ещё лучше - distroless (нет shell, нет пакетного менеджера) +FROM gcr.io/distroless/static-debian12 +``` + +Всегда указывай конкретный тег - никогда `latest`: + +```bash +# Плохо - непредсказуемо, сломается при следующем релизе образа +docker pull nginx:latest + +# Хорошо - предсказуемо +docker pull nginx:1.27-alpine +``` + +## Изоляция контейнеров + +>Этот раздел - справочный. + +### Capabilities + +Контейнер по умолчанию имеет набор Linux capabilities которые ему не нужны. Убирай всё лишнее: + +```bash +# Убрать все capabilities, добавить только нужные +docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx +``` + +Минимальный набор для большинства веб-сервисов: `NET_BIND_SERVICE` (порты < 1024). Если сервис не биндит привилегированные порты - вообще ничего добавлять не нужно, только `--cap-drop=ALL`. + +### Read-only filesystem + +```bash +# Контейнер не может писать в FS кроме явно указанных точек +docker run --read-only --tmpfs /tmp --tmpfs /var/run nginx +``` + +В docker-compose.yml: + +```yaml +read_only: true +tmpfs: + - /tmp + - /var/run +``` + +Не все образы поддерживают read-only без дополнительной настройки - смотри документацию образа. + +### Seccomp + +Docker применяет seccomp профиль по умолчанию - блокирует ~44 опасных syscall. Проверь что профиль применяется: + +```bash +docker info | grep seccomp +# Security Options: ... seccomp +``` + +### Не запускай контейнеры с --privileged + +```bash +# НИКОГДА в production +docker run --privileged nginx +``` + +`--privileged` даёт контейнеру доступ ко всем устройствам хоста и отключает все механизмы изоляции. Если видишь `--privileged` в чужом docker-compose - это красный флаг. + +## Ресурсные ограничения + +>Этот раздел - справочный. + +Без ограничений один контейнер может съесть всю память или CPU хоста и положить остальные сервисы. + +>OOM (Out Of Memory) killer - механизм ядра Linux: когда процесс превышает лимит памяти, ядро принудительно его завершает. Код завершения контейнера 137 (128 + сигнал 9) означает что сработал OOM killer, а не штатное завершение. + +```bash +# Максимум 512MB RAM - если превысит, OOM (Out Of Memory) killer завершит контейнер +docker run -m 512m --memory-reservation 256m nginx + +# Максимум 0.5 ядра +docker run --cpus="0.5" nginx + +# Защита от fork bomb - контейнер не создает бесконечное количество процессов +docker run --pids-limit 200 nginx +``` + +Если контейнер неожиданно останавливается - первым делом проверяй OOM (Out Of Memory): + +```bash +dmesg | grep -i "oom\|killed process" +docker inspect CONTAINER --format='{{.State.OOMKilled}}' +``` + +## Docker Compose с настройками безопасности + +>Этот раздел - справочный. + +Шаблон `docker-compose.yml` для production: сети, лимиты, изоляция: + +```yaml +services: + web: + image: nginx:1.27-alpine + container_name: web + restart: unless-stopped + + # Порт только на localhost - reverse proxy снаружи + ports: + - "127.0.0.1:8080:80" + + # Ресурсные ограничения + 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" + + # Только backend сеть - не доступен снаружи + networks: + - backend + + app: + image: myapp:1.2.3 + container_name: app + restart: unless-stopped + + # Подключена к обеим сетям - мост между web и db + 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 # backend сеть без доступа к интернету + +volumes: + db_data: + +secrets: + db_password: + file: ./secrets/db_password.txt +``` + +`internal: true` для backend сети - контейнеры в ней не имеют доступа к внешним сетям. База данных не должна ходить в интернет. + +## Регулярные задачи + +>Этот раздел - справочный. + +### Проверка контейнеров с docker.sock + +Проверь запущенные контейнеры у которых docker.sock смонтирован: + +```bash +# Контейнеры с docker.sock = контейнеры с root-доступом к хосту +docker ps --format '{{.Names}}' | 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 +``` +Запускай после добавления каждого нового сервиса. Пустой вывод - норма. + +Никогда не монтируй docker.sock в контейнер без явной необходимости. Если сервис требует docker.sock (Portainer, Watchtower, Traefik с автообнаружением) - осознавай что это сервис с root-доступом к хосту. + +### Очистка + +Docker накапливает мусор: остановленные контейнеры, неиспользуемые образы, dangling volumes: + +```bash +# Показать что занимает место +docker system df + +# Удалить всё неиспользуемое (остановленные контейнеры, dangling образы, неиспользуемые сети) +docker system prune -f + +# Удалить всё включая неиспользуемые образы +docker system prune -a -f + +# Удалить неиспользуемые volumes (ОСТОРОЖНО - данные удалятся безвозвратно) +docker volume prune -f +``` + +Автоматизируй через cron: + +```bash +sudo crontab -e +``` + +``` +# Еженедельная очистка - каждое воскресенье в 4:00 +0 4 * * 0 docker system prune -f >> /var/log/docker-prune.log 2>&1 +``` + +### Мониторинг ресурсов + +```bash +# Статистика всех запущенных контейнеров в реальном времени +docker stats + +# Один снимок без потока +docker stats --no-stream + +# Конкретный контейнер +docker stats web --no-stream +``` + +## Troubleshooting + +### Контейнер не видит DNS + +**Симптом:** `curl: Could not resolve host: example.com` внутри контейнера. + +**Причина:** DNS не передан контейнеру. + +**Решение:** Добавь DNS в daemon.json: + +```json +{ + "dns": ["8.8.8.8", "8.8.4.4"] +} +``` + +```bash +sudo systemctl restart docker +``` + +--- + +### Docker обходит UFW + +**Симптом:** Порт опубликованный через `-p 80:80` доступен снаружи несмотря на UFW deny. + +**Причина:** Docker пишет напрямую в iptables, обходя UFW. + +**Решение:** Использовать `127.0.0.1:80:80` вместо `80:80` - подход 1 из раздела UFW + Docker выше. + +--- + +### Permission denied на bind mount + +**Симптом:** Контейнер падает с `Permission denied` при обращении к примонтированной директории. + +**Причина:** UID процесса в контейнере не совпадает с владельцем файлов на хосте. + +**Решение:** Проверь под каким UID работает процесс в контейнере: + +```bash +docker run --rm IMAGE id +``` + +Установи правильного владельца на хосте: + +```bash +sudo chown -R UID:GID /path/to/mount +``` + +--- + +### OOM killer убивает контейнер + +**Симптом:** Контейнер неожиданно останавливается, `docker ps` показывает `Exited (137)`. + +**Причина:** Превышен memory limit, ядро завершило процесс. + +**Диагностика:** + +```bash +# Проверить OOM события +dmesg | grep -i "oom\|killed process" + +# Статус контейнера +docker inspect CONTAINER --format='{{.State.OOMKilled}}' +``` + +**Решение:** Увеличить `memory` limit или оптимизировать приложение. + +--- + +### Диск забит логами контейнеров + +**Симптом:** `df -h` показывает 100% на `/var/lib/docker`. + +**Диагностика:** + +```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 +# Статус daemon +sudo systemctl status docker + +# Конфигурация +docker info | grep -E "Storage Driver|Logging Driver|Live Restore|no-new-privileges" + +# Открытые порты +ss -tulnp | grep docker + +# Размер данных Docker +docker system df + +# Контейнеры с docker.sock +docker ps -q | xargs docker inspect --format '{{.Name}}: {{range .Mounts}}{{if eq .Source "/var/run/docker.sock"}}SOCK{{end}}{{end}}' 2>/dev/null | grep SOCK +``` + +## Что дальше + +Docker-хост готов к production нагрузке. Следующий шаг - разворачиваем сервисы поверх этой базы. + +- Peertube на VPS - следующая статья серии: собственный видеохостинг на этом Docker-хосте +- Reverse proxy - Traefik или nginx перед контейнерами +- Мониторинг - Prometheus + cAdvisor + Grafana +- Централизованные логи - Loki + Promtail + +**Стек этой статьи:** Debian 12 (Bookworm) · Docker CE · UFW · auditd · Trivy · docker-compose diff --git a/content/posts/mailserver-part-2-postfix-dovecot/index.md b/content/posts/mailserver-part-2-postfix-dovecot/index.md index bf05995..6b03897 100644 --- a/content/posts/mailserver-part-2-postfix-dovecot/index.md +++ b/content/posts/mailserver-part-2-postfix-dovecot/index.md @@ -24,7 +24,7 @@ series_order: 2 > Если еще не настроил - сделай это сейчас: -{{< article link="/posts/tips-debian-12-hardening/">}} +{{< article link="/posts/debian-12-base-hardening/">}} Дальше считаем, что база готова. diff --git a/content/posts/mailserver-part-3-security/index.md b/content/posts/mailserver-part-3-security/index.md index 81ba580..a3dd1f3 100644 --- a/content/posts/mailserver-part-3-security/index.md +++ b/content/posts/mailserver-part-3-security/index.md @@ -729,7 +729,7 @@ SpamAssassin автоматически учится на письмах с че Fail2ban должен быть установлен из статьи по базовой настройке сервера: -{{< article link="/posts/tips-debian-12-hardening/">}} +{{< article link="/posts/debian-12-base-hardening/">}} Проверь: