infra · advanced · 5d
Защищённый домашний стек
Разверни self-hosted стек медиасервера и домашнего сервера на nas01.example, где пять сервисов делят сетевое пространство имён одного VPN-контейнера — kill-switch обрывает весь трафик в момент разрыва туннеля, список split-tunnel сохраняет LAN-доступ, а три кольца доступа (localhost / LAN 10.0.0.0/24 / mesh-VPN 100.64.0.30) держат нужные двери открытыми для нужных людей. Усиль хост: SSH только по ключу, fail2ban, автоматические обновления безопасности; напиши скрипт ротирующего резервного копирования с шифрованием age; и уходи зная, что стек уходит в тень при любом сбое.
Результат
Работающий Docker Compose стек на nas01.example, где все пять сервисов маршрутизируют трафик через VPN-контейнер с kill-switch и split-tunnel; хостовый ufw-файрвол, переживающий перезагрузки без блокировки доступа; SSH, ограниченный авторизацией по ключу без рутового логина и с fail2ban, блокирующим брутфорс; unattended-upgrades для автоматических патчей безопасности; и cron-скрипт резервного копирования с ротацией старых архивов, шифрованием каждого через age перед загрузкой и логированием каждого запуска.
Этапы
0/5 · 0%- 01Сбери VPN-гейтованный стек
Напиши Docker Compose файл, где WireGuard VPN-контейнер является сетевым шлюзом для всех остальных сервисов. Каждый сайдкар подключается к сетевому пространству имён VPN-контейнера через `network_mode: "service:vpn"`, чтобы они делили один сетевой интерфейс и одну таблицу маршрутизации — WireGuard-контейнера. Добавь healthcheck к VPN-сервису, который пингует DNS провайдера или известный IP через туннель; сделай каждый зависимый сервис `depends_on` VPN с `condition: service_healthy`. Убедись, что curl изнутри любого сайдкара идёт через VPN-интерфейс (проверь exit IP), и что остановка VPN-контейнера немедленно обрывает сеть для всех сайдкаров.
Критерии готовности- Все пять сервисов объявляют `network_mode: "service:vpn"` и VPN-сервис имеет работающий healthcheck; `docker compose ps` показывает все сервисы здоровыми.
- `curl ifconfig.me` изнутри любого сайдкара возвращает VPN exit IP, а не публичный IP хоста.
- Остановка VPN-контейнера (`docker compose stop vpn`) приводит к потере сетевого доступа всеми сайдкар-сервисами в течение секунд.
- 02Запри сеть: kill-switch, split-tunnel и кольца ufw
Добавь kill-switch в WireGuard-контейнер, чтобы при разрыве туннеля контейнер не мог маршрутизировать трафик никуда — реализуй это правилами iptables внутри контейнера (или хуками PostDown/PreUp в конфигурации WireGuard), которые сбрасывают весь forwarded-трафик при отсутствии интерфейса wg0. Вырежи split-tunnel-исключение для 10.0.0.0/24, чтобы LAN-клиенты могли напрямую достигать сервисов без VPN. Настрой ufw на хосте для трёх колец доступа: localhost (127.0.0.1) для инструментов на том же хосте, LAN (10.0.0.0/24) для локальных клиентов, и mesh-VPN-пир (100.64.0.30) для удалённого доступа. Помни, что Docker по умолчанию обходит FORWARD-правила ufw — используй записи цепочки `DOCKER-USER` или `--iptables=false` с ручными правилами, чтобы закрыть этот пробел.
Критерии готовности- Отключение интерфейса wg0 внутри VPN-контейнера (`ip link set wg0 down`) немедленно обрывает весь исходящий трафик от сайдкаров; LAN-трафик на 10.0.0.0/24 продолжает идти.
- Сканирование портов с хоста вне всех трёх колец (не localhost, не 10.0.0.0/24, не 100.64.0.30) возвращает все порты как filtered; mesh-VPN-пир по адресу 100.64.0.30 может достигать сервисов.
- 03Усиль хост: SSH, fail2ban и автоматические патчи
Заблокируй хост nas01.example так, чтобы SSH был единственной точкой удалённого входа и был защищён от брутфорса и кражи учётных данных. Отключи парольную аутентификацию и root-логин в `/etc/ssh/sshd_config`; установи публичный ключ пользователя operator; задай `MaxAuthTries 3` и `LoginGraceTime 30`. Установи fail2ban и настрой джейл, банящий IP после 5 неудачных SSH-попыток за 5 минут на 1 час. Включи unattended-upgrades для автоматических патчей безопасности — ограничь только security-pocket, чтобы он никогда не обновлял пакеты, которые могут сломать сервисы. Убедись, что попытка SSH с паролем с тестовой машины отклоняется, а 6 быстрых неудачных попыток вызывают бан, видимый в `fail2ban-client status sshd`.
Критерии готовности- `ssh operator@nas01.example` работает с приватным ключом; `ssh -o PubkeyAuthentication=no operator@nas01.example` отклоняется с "Permission denied (publickey)".
- После 6 быстрых неудачных попыток входа с тестового IP `fail2ban-client status sshd` показывает этот IP в списке заблокированных.
- `unattended-upgrade --dry-run` сообщает, что обновления безопасности eligible, а non-security-пакеты пропускаются.
- 04Удалённый доступ без публичной поверхности
Обеспечь себе удалённый доступ ко всем пяти сервисам за пределами LAN без открытия единого порта на роутере. Используй mesh-VPN или overlay-туннель (например, Tailscale, Headscale или self-hosted WireGuard mesh), чтобы твоё удалённое устройство получало адрес 100.64.0.30 и могло достигать каждого сервиса напрямую по этому адресу. Хост nas01.example не должен иметь публичных TCP или UDP портов, проброшенных с роутера — проверь это внешним сканированием портов. Контроль доступа должен применяться на уровне mesh: только enrolled-устройства с валидными ключами могут присоединиться к mesh; все остальные молча отбрасываются.
Критерии готовности- Внешнее сканирование портов публичного IP nas01.example не показывает открытых портов (все filtered); с удалённого устройства по адресу 100.64.0.30 все пять сервисов доступны по их mesh-IP-адресам.
- Устройство с невалидным или отсутствующим mesh-ключом не может достичь ни одного сервиса (connection refused или timeout на уровне mesh).
Опирается на - 05Ротирующийся офсайт-бэкап с шифрованием age
Напиши shell-скрипт, который делает резервную копию всех дата-директорий сервисов на nas01.example, хранит только последние N архивов на диске (удаляет самый старый, когда количество превышает N), шифрует каждый tarball через `age` перед загрузкой на удалённый сервер, и записывает отметку времени в лог для каждого запуска — успех или ошибка. Публичный ключ age — единственный секрет, нужный во время выполнения; приватный ключ хранится офсайт. Подключи скрипт к cron-заданию, запускающемуся ночью; проверь, запустив его вручную дважды, и убедись, что на диске остаётся только N архивов и каждый архив расшифровывается корректно с соответствующим приватным ключом.
Критерии готовности- Двойной запуск скрипта резервного копирования оставляет ровно N архивов в директории резервных копий; самый старый удалён.
- Каждый архив является age-зашифрованным файлом; `age --decrypt -i private-key.txt backup.tar.gz.age` извлекает оригинальный tarball без ошибок.
- Лог-файл содержит отметку времени для каждого запуска с результатом успеха или причиной ошибки, а cron-задание отображается в `crontab -l`.
Опирается на
Рубрика
| Джуниор | Миддл | Сеньор | |
|---|---|---|---|
| Kill-switch VPN и изоляция сетевого пространства имён | Сервисы работают за VPN-контейнером, исходящий IP — это VPN exit. Kill-switch существует концептуально (отмечен в конфиге), но не протестирован: остановка VPN-контейнера может по-прежнему позволять сайдкар-сервисам выходить в интернет через маршрут по умолчанию хоста. | Все пять сервисов делят сетевое пространство имён VPN-контейнера через `network_mode: "service:vpn"`. Отключение VPN или интерфейса wg0 наглядно обрывает исходящий интернет для всех сайдкаров в течение секунд. Split-tunnel-исключение для 10.0.0.0/24 действует, так что LAN-трафик идёт без VPN. | Ты объясняешь, почему разделённое сетевое пространство имён надёжнее пользовательской Docker-сети с NAT-правилами: сервисы наследуют таблицу маршрутизации и iptables VPN-контейнера и не могут достичь интернета через любой другой интерфейс. Ты определяешь режим отказа, при котором плохо упорядоченное правило iptables или взаимодействие с Docker --iptables=true может открыть путь для побега, и проверяешь, что цепочка DOCKER-USER или её аналог его закрывает. |
| Закаливание хоста и применение колец доступа | Парольная аутентификация SSH отключена, ключ установлен. ufw включён. Fail2ban работает. Три кольца доступа (localhost, LAN, mesh-VPN) концептуально описаны, но не все применяются на уровне файрвола — LAN-кольцо может быть открыто для 0.0.0.0/24, а не для конкретного /24. | ufw разрешает SSH, LAN (10.0.0.0/24) и mesh-VPN пир (100.64.0.30) и по умолчанию запрещает весь другой входящий трафик. Fail2ban банит IP после 5 неудачных SSH-попыток, что проверено срабатыванием бана и проверкой `fail2ban-client status sshd`. Правила ufw переживают перезагрузку. SSH разрешён на порту 22 до запуска `ufw enable` — порядок задокументирован. | Ты рассуждаешь о радиусе поражения при компрометации адреса mesh-VPN пира (100.64.0.30): у него доступ ко всем пяти сервисам, что может быть избыточным. Ты предлагаешь, как правила ACL на уровне mesh (конкретные порты по сервисам, а не полный mesh-доступ) снизили бы этот радиус поражения. Ты определяешь, что Docker по умолчанию обходит FORWARD-правила ufw, и документируешь конкретную цепочку (DOCKER-USER), закрывающую пробел. |
| Удалённый доступ без публичной экспозиции | Удалённый доступ работает — сервисы достижимы за пределами LAN. Для этого с роутера проброшен порт, открывающий хотя бы один порт сервиса в публичный интернет. | Удалённый доступ обеспечен через mesh-VPN или overlay-туннель (Tailscale, Headscale, WireGuard mesh). Внешнее сканирование портов публичного IP хоста показывает все порты как filtered. Только enrolled-устройства с валидными ключами могут достигать сервисов через mesh. | Ты объясняешь, почему отсутствие открытых портов — более сильная позиция, чем проброс портов с fail2ban: атакующий, не знающий о существовании mesh, не имеет TCP-поверхности для зондирования. Ты описываешь сценарий отказа, когда управляющий плоскость mesh (например, DERP-ретранслятор Tailscale) недоступна: продолжает ли стек работать для LAN-клиентов? Деградирует ли удалённый доступ мягко или отказывает жёстко? И ты документируешь путь восстановления. |
| Шифрование резервных копий, ротация и восстановимость | Скрипт резервного копирования работает и создаёт архивы на диске. Архивы не шифруются перед загрузкой; или приватный ключ и зашифрованный архив хранятся в одном месте. Логика ротации отсутствует — старые архивы накапливаются. | Каждый архив шифруется через age перед загрузкой; приватный ключ хранится офсайт отдельно. Ротация оставляет ровно N архивов. Cron-задание запускает скрипт ночью, запись в лог подтверждает каждый запуск. Дешифрование реального архива из хранилища резервных копий протестировано и успешно. | Ты рассуждаешь о цели времени восстановления: учитывая, что архив офсайт, сколько займёт полное восстановление и какие данные могут быть потеряны в худшем случае (окно с момента последней успешной резервной копии)? Ты проверяешь, что скрипт корректно обрабатывает ошибку загрузки (логирует ошибку, не удаляет локальные архивы). Ты определяешь, что публичный ключ age не является секретом, но приватный ключ age — единственная точка отказа дешифрования, и документируешь, как восстановиться при его потере. |
Эталонный разбор (спойлер)
Разделённое сетевое пространство имён против роутинга на контейнер: когда все сайдкары присоединяются к пространству имён VPN-контейнера через `network_mode: "service:vpn"`, они наследуют его таблицу маршрутизации и правила iptables. Kill-switch (сброс всего трафика при отсутствии wg0) применяется к каждому сервису автоматически. Режим отказа раздельного VPN-роутинга на контейнер — утечка маршрута через дефолтный шлюз хоста при разрыве туннеля — невозможен, поскольку сайдкары никогда не имели доступа к этому шлюзу.
Порядок правил ufw и обход Docker: `ufw enable` применяет существующие правила, поэтому правило allow для SSH должно существовать до его запуска — ошибка здесь навсегда блокирует доступ к удалённому хосту. Docker добавляет собственные правила iptables в цепочку FORWARD, обходящие дефолты OUTPUT/INPUT ufw; цепочка DOCKER-USER является официальным хуком для добавления пользовательских ограничений, которые Docker соблюдает.
Нулевые публичные порты через mesh-VPN: проброс порта к сервису открывает его для каждого IP в интернете; поверхность атаки — это сам сервис плюс любая CVE в том, что терминирует соединение. Mesh-VPN, использующий исходящие HTTPS-туннели (входящие порты не нужны), сжимает внешнюю поверхность атаки хоста до нуля — атакующий, не знающий о существовании mesh, не имеет TCP-эндпоинта для сканирования.
Шифрование age для резервных копий: age — это современный инструмент шифрования файлов (Curve25519 + ChaCha20-Poly1305), спроектированный именно для этого случая: публичный ключ шифрует во время резервного копирования, приватный ключ расшифровывает во время восстановления, и приватному ключу никогда не нужно касаться сервера резервных копий. Классический режим отказа — хранение и зашифрованного архива, и приватного ключа в одном месте, что превращает шифрование в театр.
Сделай по-сеньорски
- Разверни Suricata или Zeek как пассивную IDS на LAN bridge-интерфейсе и подключи её алерты к локальному агрегатору логов; настрой набор правил для устранения ложных срабатываний на нормальный Docker bridge-трафик.
- Добавь Prometheus + Grafana в стек (также за VPN-шлюзом) и построй дашборды для uptime VPN-туннеля, частоты сбросов ufw, событий бана fail2ban и успеха/неудачи задания резервного копирования.
- Храни приватный ключ age и приватный ключ SSH в self-hosted менеджере секретов (HashiCorp Vault или аналог) и измени скрипт резервного копирования для получения ключа age-получателя в runtime через Vault API, а не из файла на диске.
- Напиши CI-пайплайн (GitHub Actions или Gitea Actions), который поднимает compose-стек в Docker-in-Docker раннере, выполняет тест kill-switch (отключает wg0 и проверяет отсутствие исходящего трафика) и блокирует merge при неудаче теста.