Compare commits
2 Commits
c9bbffc3bf
...
40e42fd8c6
| Author | SHA1 | Date | |
|---|---|---|---|
| 40e42fd8c6 | |||
| 05e5ffdad4 |
|
Before Width: | Height: | Size: 611 KiB After Width: | Height: | Size: 611 KiB |
@@ -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
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 611 KiB |
@@ -0,0 +1,869 @@
|
|||||||
|
---
|
||||||
|
title: "Debian 12: настройка Docker-хоста для production"
|
||||||
|
date: 2026-05-22
|
||||||
|
draft: false
|
||||||
|
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 <<EOF
|
||||||
|
Types: deb
|
||||||
|
URIs: https://download.docker.com/linux/debian
|
||||||
|
Suites: $(. /etc/os-release && echo "$VERSION_CODENAME")
|
||||||
|
Components: stable
|
||||||
|
Architectures: $(dpkg --print-architecture)
|
||||||
|
Signed-By: /etc/apt/keyrings/docker.asc
|
||||||
|
EOF
|
||||||
|
|
||||||
|
sudo apt update
|
||||||
|
|
||||||
|
# Установить Docker
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
### Добавление пользователя в группу docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo usermod -aG docker $USER
|
||||||
|
# Активировать группу без перелогина
|
||||||
|
newgrp docker
|
||||||
|
```
|
||||||
|
|
||||||
|
**Важно:** членство в группе `docker` эквивалентно sudo без пароля - через docker.sock можно получить полный root на хосте. Добавляй только пользователей которым полностью доверяешь. Подробнее - в разделе про docker.sock ниже.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm hello-world
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидаемый вывод: `Hello from Docker!`
|
||||||
|
|
||||||
|
## docker.sock - главный риск
|
||||||
|
|
||||||
|
Прежде чем настраивать daemon - разберёмся с самым опасным местом.
|
||||||
|
|
||||||
|
`/var/run/docker.sock` - Unix сокет Docker daemon. Любой процесс с доступом к нему имеет **полный root на хосте**. Не "почти root" - именно root. Через один вызов API можно запустить контейнер с bind mount `/:/host` и получить доступ ко всей файловой системе хоста:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Пример атаки - одна команда и ты root на хосте (выполнение не требуется)
|
||||||
|
docker run --rm -v /:/host alpine chroot /host
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверь кто имеет доступ к сокету:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Права на сокет
|
||||||
|
ls -la /var/run/docker.sock
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
srw-rw---- 1 root docker 0 May 22 10:00 /var/run/docker.sock
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверь кто в группе `docker`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
getent group docker
|
||||||
|
```
|
||||||
|
|
||||||
|
Только пользователи которым доверяешь полный root должны быть в этой группе.
|
||||||
|
|
||||||
|
## Настройка 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: json-file` + `max-size/max-file` - ограничение логов контейнеров. Без этого `/var/lib/docker/containers/` забивает весь диск за несколько дней на активном сервисе. 50MB × 3 файла = 150MB максимум на контейнер.
|
||||||
|
|
||||||
|
`live-restore: true` - контейнеры продолжают работать пока daemon перезапускается. Без этого `systemctl restart docker` роняет все контейнеры.
|
||||||
|
|
||||||
|
`userland-proxy: false` - iptables вместо userland proxy для проброса портов. Быстрее и меньше процессов.
|
||||||
|
|
||||||
|
`no-new-privileges: true` - контейнеры не могут поднять привилегии через setuid/setgid бинарники.
|
||||||
|
|
||||||
|
`storage-driver: overlay2` - современный и быстрый драйвер. На Debian 12 используется по умолчанию, но лучше зафиксировать явно.
|
||||||
|
|
||||||
|
### Почему нет userns-remap
|
||||||
|
|
||||||
|
`userns-remap` - user namespace remapping. Root в контейнере (UID 0) маппится на непривилегированный UID на хосте (например 100000). Звучит безопасно.
|
||||||
|
|
||||||
|
На практике: ломает все bind mounts - файлы на хосте принадлежат UID 1000, а контейнер видит UID 100000 и получает `Permission denied`. Redis, PostgreSQL, nginx - у каждого свой UID, каждый нужно пересчитывать вручную через `chown -R 100000:100000`. Для production с несколькими сервисами это постоянная головная боль. Включай только если чётко понимаешь все последствия и готов управлять UID маппингом вручную.
|
||||||
|
|
||||||
|
### Почему нет icc:false
|
||||||
|
|
||||||
|
`icc: false` (inter-container communication) - глобальный запрет общения между контейнерами на уровне daemon.
|
||||||
|
|
||||||
|
Проблема: Docker Compose создаёт изолированную bridge сеть для каждого стека. Контейнеры одного стека (`app` + `postgres` + `redis`) общаются через эту сеть. С `icc: false` на уровне daemon они перестают видеть друг друга - стек перестаёт работать. Правильный подход - изоляция через сети Docker Compose. Описано ниже.
|
||||||
|
|
||||||
|
Примени конфиг:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl restart docker
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверь что daemon запустился с нужными параметрами:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker info | grep -E "Storage Driver|Logging Driver|Live Restore"
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Storage Driver: overlay2
|
||||||
|
Logging Driver: json-file
|
||||||
|
Live Restore Enabled: true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sysctl для Docker-хоста
|
||||||
|
|
||||||
|
Docker требует дополнительных параметров ядра помимо базового hardening:
|
||||||
|
|
||||||
|
```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
|
||||||
|
|
||||||
|
# Для Elasticsearch и других Java приложений в контейнерах
|
||||||
|
vm.max_map_count = 262144
|
||||||
|
```
|
||||||
|
|
||||||
|
Примени:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo sysctl -p /etc/sysctl.d/99-docker.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверь forwarding:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo sysctl net.ipv4.ip_forward
|
||||||
|
# net.ipv4.ip_forward = 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## UFW + Docker
|
||||||
|
|
||||||
|
>Этот раздел - справочный. Конкретные настройки применяются при разворачивании каждого сервиса.
|
||||||
|
|
||||||
|
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
|
||||||
@@ -24,7 +24,7 @@ series_order: 2
|
|||||||
|
|
||||||
> Если еще не настроил - сделай это сейчас:
|
> Если еще не настроил - сделай это сейчас:
|
||||||
|
|
||||||
{{< article link="/posts/tips-debian-12-hardening/">}}
|
{{< article link="/posts/debian-12-base-hardening/">}}
|
||||||
|
|
||||||
Дальше считаем, что база готова.
|
Дальше считаем, что база готова.
|
||||||
|
|
||||||
|
|||||||
@@ -729,7 +729,7 @@ SpamAssassin автоматически учится на письмах с че
|
|||||||
|
|
||||||
Fail2ban должен быть установлен из статьи по базовой настройке сервера:
|
Fail2ban должен быть установлен из статьи по базовой настройке сервера:
|
||||||
|
|
||||||
{{< article link="/posts/tips-debian-12-hardening/">}}
|
{{< article link="/posts/debian-12-base-hardening/">}}
|
||||||
|
|
||||||
Проверь:
|
Проверь:
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user