Как безопасно выставить домашний сервис в интернет и не открыть ни одного порта наружу
Зачем «козе» баян ?
Представьте ситуацию: дома стоит сервер, на нём — фотоархив, документы, NAS, камеры и, возможно, ещё что-то. При этом хочется открыть наружу один сервис — например, дашборд или Immich, чтобы заходить с телефона, с работы или из поездки.
Самый очевидный вариант — просто пробросить порт на домашнем роутере. Работать будет, но сразу появляются две проблемы.
-
Открытый порт дома. Если у вас белый IP и наружу смотрит сервис, этот адрес будут постоянно сканировать боты. Сам по себе открытый порт не означает взлом, но это дополнительная публичная точка атаки.
-
Если публичный сервис всё-таки взломают, атакующий оказывается внутри домашней инфраструктуры — рядом с NAS, документами, камерами и другими системами. Один скомпрометированный контейнер может стать началом гораздо более неприятной истории.
Я для себя решил это через две идеи: бастион и DMZ.
Бастион — маленький дешёвый VPS с публичным IP. Весь внешний трафик приходит сначала туда. Дома при этом нет открытых входящих портов.
DMZ — отдельная изолированная сеть, в которой живут публичные сервисы. Из неё нет доступа к основной домашней LAN.
Две границы безопасности
Вся схема держится на двух независимых барьерах. Если публичное приложение будет скомпрометировано, этого ещё недостаточно, чтобы попасть к домашним данным.
Граница 1. Сеть гипервизора
Дома работает гипервизор. В моём случае это Proxmox, но принцип тот же для любого гипервизора.
Обычные виртуальные машины с чувствительными данными находятся в основной локальной сети LAN. Публичные сервисы туда не помещаются: для них создаётся отдельная виртуальная сеть DMZ.
Например:
DMZ: 10.20.0.0/24
Шлюз DMZ: 10.20.0.1
VM в DMZ: 10.20.0.X
На уровне гипервизора действует жёсткое правило: из DMZ в LAN доступа нет. Трафик DMZ → LAN блокируется файрволом.
При этом сама DMZ может выходить в интернет через NAT. Виртуальная машина может скачивать обновления, Docker-образы и обращаться к внешним сервисам, но не должна иметь доступа к NAS, камерам и другим системам домашней сети.
Если публичный сервис взломают, атакующий остаётся внутри DMZ.
Граница 2. VPS-бастион
Вторая граница находится снаружи дома.
Для этого используется небольшой VPS с белым IP. Больших ресурсов ему не требуется: он принимает HTTPS, терминирует TLS, выполняет авторизацию и передаёт трафик по WireGuard.
На VPS наружу открываются только действительно нужные порты:
443/TCP — HTTPS;
SSH — для администрирования;
UDP-порты WireGuard.
Всё остальное закрывается файрволом.
Никакого прямого доступа из интернета к домашним виртуальным машинам нет.
Почему туннель инициируется из дома
Это ключевой момент всей схемы.
Чтобы VPS передал запрос домашнему сервису, совсем не обязательно открывать входящий порт на домашнем роутере.
WireGuard-соединение инициирует домашняя виртуальная машина. Она сама устанавливает исходящее соединение с VPS.
Исходящие соединения нормально проходят через NAT и CGNAT, поэтому дома может быть серый IPv4, динамический адрес и полностью закрытые входящие соединения.
После установки WireGuard между VPS и домашней VM существует постоянный зашифрованный туннель. VPS передаёт запросы к приложению через уже существующий канал.
Домашний роутер при этом остаётся закрытым. Снаружи виден только VPS.
Аналогия простая: вместо того чтобы оставлять входную дверь открытой в ожидании курьера, мы сами протягиваем защищённый канал до пункта выдачи. Снаружи в дом никто напрямую не входит.
Как выглядит путь запроса
Пользователь
↓
DNS
только резолвинг имени
↓
VPS-бастион
Caddy или другой reverse proxy
TLS
авторизация
↓
WireGuard
↓
DMZ-виртуалка
10.30.0.2:порт
↓
Docker-приложение
Ответ возвращается тем же путём.
Приложение внутри DMZ желательно привязывать только к туннельному IP, например:
10.30.0.2:8080
а не к:
0.0.0.0:8080
Это означает, что сервис не слушает на всех интерфейсах машины.
Но здесь важно не создать ложного чувства безопасности: bind на туннельный IP — это дополнительный слой, а не замена файрволу.
Основная гарантия изоляции — файрвол: запрет DMZ → LAN и отсутствие ненужных маршрутов. Bind лишь усиливает схему по принципу защиты в глубину.
Что понадобится
Домашний гипервизор с поддержкой изолированных виртуальных сетей. В моём случае Proxmox.
Отдельный сетевой мост DMZ, например 10.20.0.0/24, отрезанный от LAN.
Дешёвый VPS с публичным IP под роль бастиона.
Домен и DNS. Удобно использовать wildcard-запись *.example.ru, указывающую на VPS.
Шаблон виртуальной машины, например Debian 13, чтобы быстро клонировать новые DMZ-машины.
На VPS — Caddy или другой reverse proxy, WireGuard и файрвол.
Обозначения
VPS_IP — публичный IP VPS.
example.ru — домен.
app.example.ru — поддомен приложения.
PVE_LAN_IP — адрес Proxmox в домашней LAN.
10.20.0.0/24 — подсеть DMZ.
10.20.0.1 — шлюз DMZ.
10.20.0.X — адрес VM в DMZ.
10.30.0.0/30 — подсеть WireGuard.
10.30.0.1 — WireGuard-адрес VPS.
10.30.0.2 — WireGuard-адрес VM.
51820/UDP — пример порта WireGuard.
Технически можно делать отдельный туннель на каждую VM или на каждый публикуемый сервис. Я предпочитаю разделять их, чтобы одна ошибка не давала лишнего доступа к соседним сервисам.
Этап 1. Клон виртуалки и сеть
Клонируем шаблон в новую VM и сразу подключаем её сетевой интерфейс к DMZ-мосту, а не к LAN.
Это важный момент: если по ошибке подключить машину к обычной домашней сети, вся идея изоляции теряется.
Хорошая практика — при первом старте держать виртуальный сетевой кабель отключённым, пока не задан статический адрес. После настройки назначаем, например:
IP: 10.20.0.X
Gateway: 10.20.0.1
После этого поднимаем интерфейс и проверяем изоляцию.
Проверка:
ping 10.20.0.1
Шлюз DMZ должен отвечать.
ping 1.1.1.1
Интернет должен работать.
ping <LAN_IP>
Доступа к LAN быть не должно.
Для более надёжной проверки лучше использовать не только ping, но и nc, curl или nmap на конкретные сервисы LAN.
Если из DMZ получается подключиться к LAN — изоляция настроена неправильно и дальше идти нельзя.
Этап 2. Базовая настройка системы
Доводим новую VM до нормального рабочего состояния: задаём hostname, DNS, устанавливаем обновления, WireGuard и необходимые системные пакеты.
Типовые вещи вроде DNS, локали и Docker лучше один раз правильно настроить в шаблоне, чтобы потом каждый новый клон начинался с чистого состояния.
Этап 3. SSH-доступ
SSH наружу для DMZ-машин не открываем.
Администрирование выполняется из домашней сети через гипервизор как jump host.
Пример:
ssh -J user@PVE_LAN_IP user@10.20.0.X
После первого входа добавляем публичный SSH-ключ и отключаем вход по паролю.
При любых изменениях SSH и файрвола лучше не закрывать текущую рабочую сессию, пока новая конфигурация не проверена из второго терминала.
Этап 4. WireGuard до бастиона
Создаём отдельный туннель между VPS и DMZ-машиной.
Ключи генерируются отдельно на каждой стороне. Приватные ключи никогда не передаются между машинами — наружу передаются только публичные.
На VPS, например:
WireGuard IP: 10.30.0.1/30
ListenPort: 51820
В peer указывается публичный ключ VM.
На VM:
WireGuard IP: 10.30.0.2/30
Endpoint: VPS_IP:51820
PersistentKeepalive: 25
PersistentKeepalive нужен, чтобы NAT-состояние не исчезало при долгом отсутствии трафика.
В AllowedIPs на стороне VM можно указать только адрес бастиона:
10.30.0.1/32
Не обязательно отправлять через VPS весь трафик машины с помощью 0.0.0.0/0. Туннель в этой схеме нужен именно для связи с бастионом.
После настройки проверяем handshake WireGuard и доступность адресов 10.30.0.1 и 10.30.0.2.
Этап 5. Docker
Устанавливаем Docker Engine и Docker Compose.
Есть одна тонкость: если контейнер должен слушать на IP WireGuard, при загрузке системы Docker может стартовать раньше, чем поднимется WireGuard-интерфейс. Тогда контейнер не сможет привязаться к ещё не существующему адресу.
Это решается systemd drop-in для docker.service.
Создаём каталог:
mkdir -p /etc/systemd/system/docker.service.d
Создаём файл:
/etc/systemd/system/docker.service.d/10-wait-wg.conf
Содержимое:
[Unit]
After=wg-quick@wg0.service
Wants=wg-quick@wg0.service
После этого:
systemctl daemon-reload
Проверяем зависимость:
systemctl show docker --property=After
Почему именно drop-in, а не правка штатного docker.service?
Потому что штатный unit принадлежит пакету Docker и может быть заменён при обновлении. Файлы в /etc/systemd/system/docker.service.d/ сохраняются и дополняют штатную конфигурацию systemd.
Этап 6. Приложения
Приложения разворачиваются через Docker Compose.
Публичный сервис желательно публиковать только на адресе WireGuard:
10.30.0.2:8080
а не:
0.0.0.0:8080
Для нескольких приложений в одной VM есть два основных варианта.
Вариант A. Отдельный порт на приложение.
Каждый контейнер слушает свой порт WireGuard-адреса. На VPS для каждого приложения создаётся своё правило reverse proxy.
Это просто и прозрачно. Для нескольких сервисов обычно вполне достаточно.
Вариант B. Внутренний reverse proxy.
В самой DMZ-VM запускается Caddy или Nginx, который слушает один порт WireGuard и дальше распределяет запросы по контейнерам.
Этот вариант удобнее, если приложений становится много: туннель и конфигурацию бастиона не приходится постоянно усложнять.
Этап 7. Публикация на бастионе
На VPS настраиваем Caddy или другой reverse proxy.
Например, запросы к:
передаются через WireGuard на:
10.30.0.2:8080
TLS завершается на бастионе.
Там же можно выполнять авторизацию. Для простого закрытого сервиса подойдёт Basic Auth, а для более серьёзной схемы — Authentik, Authelia или другой SSO.
DNS-запись:
app.example.ru → VPS_IP
Если уже используется wildcard:
*.example.ru → VPS_IP
то отдельную DNS-запись для каждого нового приложения создавать не обязательно.
Перед применением конфигурации reverse proxy обязательно проверяем её синтаксис. После этого лучше делать reload, а не restart, чтобы не ронять уже работающие соединения.
Проверка снаружи
С авторизацией:
curl -u user:pass https://app.example.ru/ -o /dev/null -w “%{http_code}\n”
Без авторизации:
curl https://app.example.ru/ -o /dev/null -w “%{http_code}\n”
В зависимости от конфигурации первый запрос должен вернуть нормальный ответ или редирект, а второй — 401 либо отправить пользователя на SSO.
Правила гигиены DMZ
Изоляцию обеспечивает файрвол, bind только усиливает её.
Главное правило — DMZ не должна иметь доступа к LAN. Привязка приложения к WireGuard-IP полезна, но она не заменяет сетевой барьер.
SSH в DMZ-машины — только по ключам. Публичный SSH к ним не нужен.
Изоляцию DMZ → LAN нужно перепроверять после изменений сети и файрвола.
TLS удобно терминировать на бастионе. Трафик между бастионом и DMZ идёт через зашифрованный WireGuard-туннель.
Приватные ключи никогда не покидают машину, на которой были созданы.
Пароли и секреты лучше хранить в менеджере секретов или парольном менеджере, а не разбрасывать по конфигам открытым текстом.
На бастионе полезны CrowdSec или fail2ban. Они не заменяют нормальный файрвол и безопасную конфигурацию сервисов, но помогают автоматически блокировать перебор и очевидный шум из интернета.
Что получается в итоге
Дома нет открытых входящих портов.
Публичный IP находится на VPS, а не на домашнем сервере.
Публичные приложения вынесены в DMZ и отделены от домашней LAN.
Весь внешний веб-трафик проходит через одну контролируемую точку — бастион.
TLS, авторизация, логи и защита от перебора сосредоточены на VPS.
Новый сервис добавляется без изменения основной домашней сети: создаётся VM или контейнер в DMZ, поднимается туннель и добавляется правило reverse proxy.
Сама схема не является чем-то революционным. Это обычное разделение ролей: публичная точка входа отдельно, публичные сервисы отдельно, домашние данные отдельно.
Смысл не в том, чтобы сделать сервер «невзламываемым». Таких серверов не бывает.
Смысл в другом: если один публичный сервис всё-таки сломают, последствия должны закончиться на этом сервисе, а не распространяться на весь домашний сервер, NAS, камеры, документы и резервные копии.
P.S. Если где-то возникают сложности с конкретной конфигурацией WireGuard, Caddy, Proxmox или Docker, детали уже можно разбирать отдельно под конкретную сеть и конкретные адреса.