Immich наружу из дома или Изолированная DMZ-виртуалка с публикацией через VPS

Как безопасно выставить домашний сервис в интернет и не открыть ни одного порта наружу

Зачем «козе» баян ?

Представьте ситуацию: дома стоит сервер, на нём — фотоархив, документы, NAS, камеры и, возможно, ещё что-то. При этом хочется открыть наружу один сервис — например, дашборд или Immich, чтобы заходить с телефона, с работы или из поездки.

Самый очевидный вариант — просто пробросить порт на домашнем роутере. Работать будет, но сразу появляются две проблемы.

  1. Открытый порт дома. Если у вас белый IP и наружу смотрит сервис, этот адрес будут постоянно сканировать боты. Сам по себе открытый порт не означает взлом, но это дополнительная публичная точка атаки.

  2. Если публичный сервис всё-таки взломают, атакующий оказывается внутри домашней инфраструктуры — рядом с 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.

Аналогия простая: вместо того чтобы оставлять входную дверь открытой в ожидании курьера, мы сами протягиваем защищённый канал до пункта выдачи. Снаружи в дом никто напрямую не входит.

Как выглядит путь запроса

Пользователь

https://app.example.ru


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.

Например, запросы к:

app.example.ru

передаются через 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, детали уже можно разбирать отдельно под конкретную сеть и конкретные адреса.

может я слишком упрощаю, но достаточно обратного прокси, (сам использую Traefik)

если нужен доступ к сервисам, которым не присвоил домены, по IP, прокинут wireguard туннель.. подключаешься хоть с телефона, хоть с ноута в нужный момент
p.s. , да, дополню, после замечания, если у тебя “белый” IP

Несколько замечаний:

  1. Поправьте, пожалуйста, форматирование, сейчас не очень удобно читать лонгрид
  2. DMZ это несколько больше, нежели слушать туннельный интерфейс, я бы даже сказал, что совсем не слушать туннельный интерфейс, это про NAT со стороны внешних по отношению к DMZ сетей, ну далее об этом написано
  3. Если убрать DMZ, то бастион неплохо реализуется готовыми решениями типа Pangolin.

DMZ и бастион — разные вещи, и Pangolin заменяет только второе - Вполне согласен…
DMZ есть изоляция на гипервизоре это по сути=ограничение ущерба на случай если публичный сервис взломают и атакующий будет заперт в загоне и не дотянется до LAN
Мой взгляд: Pangolin — сильный кандидат **+ DMZ…

Интересно вот что (FABLE5) на эту тему “галюцинировала”:

Сильная сторона Pangolin** — аутентификация из коробки: SSO, TOTP-2FA, организации и роли (RBAC), share-ссылки, PIN на ресурс, browser-based доступ к SSH/RDP. Это то, ради чего сейчас пришлось бы городить Authentik/Authelia. Для , например - юрфирмы с разграничением доступа к клиентским данным — весомо.

Риск Pangolin — меняет модель угроз VPS в обратную сторону. Изоляция (AllowedIPs /32, ip_forward=0, тупой бастион, единственный порт 8080) заменяется на «умный» бастион: центр identity-логики + БД пользователей + управляемая из дашборда маршрутизация, потенциально в целые сетевые диапазоны, а не в один host:port. Компрометация такого VPS = компрометация центра управления доступом. Плюс шире поверхность атаки на периметре (Traefik + Gerbil + БД + дашборд против минималистичного Caddy).

Итог: это не «лучше/хуже», а конфликт двух философий — сильная аутентификация против сильной изоляции периметра.

(Traefik + WG по IP) упрощает личный доступ, но не даёт ни изоляции, ни полноценной публикации для внешних потребителей…страшно :slight_smile: ну или если бы данные были некритичны совершенно… но и так наверное тоже можно

Тоже застал этот выбор, когда переезжал с обычного хостинга на VPS. Только у меня был куда более простой кейс — один WordPress, никакого Immich и NAS за спиной, поэтому я пошёл по пути наименьшего сопротивления: VPS с Nginx, домен на Cloudflare, всё наружу. Изоляции дома нет вообще, весь сайт лежит на VPS.

Но схема с бастионом и DMZ, как у автора, мне зашла именно как архитектурная идея. У себя дома планирую скоро поднять сервисы, которые хочу отдавать наружу (личный файловый обменник, пара API), и вот как раз раздумываю: либо повторять связку WireGuard + Caddy на VPS, либо смотреть в сторону Pangolin, который упоминал KRom — когда надо быстро и без боли закрыть доступ аутентификацией.

Один момент, с которым я столкнулся на практике: если у вас дома серый IP (у меня так), то входящих портов не будет вообще, и вариант LamerDeath с чистым Traefik отпадает — только исходящий туннель. Так что для «серых» это схема автора или Pangolin-подобные решения, выбор реально между аутентификацией и изоляцией.

Домашний Сервер —> WireGuard —> VPS (белый IP) - это все ради портов собственно…
Pangolin TailScale … это все обертки - внутри то WireGuard
Тоже пробую сейчас TailScale (на VPS свой HeadScale поднят) это ради SSH по сути… и уменьшение угроз для Бастиона (VPS) + Pangolin для выпуска наружу приложений типа Immich, … etc. с хорошей аутентификацией и обратным прокси из коробки… порпобуем…

Отличная комбинация: Headscale для SSH — это как раз снимает вопрос «тупого бастиона», о котором говорил KRom: вместо открытого 22 на VPS снаружи остаётся только WireGuard-интерфейс, и поверх туннеля можно смело оставить sshd. А Pangolin перед Immich закрывает аутентификацию, ради которой иначе пришлось бы городить Authentik/Authelia.

Единственное, за чем стоит последить — вес самого Pangolin: Traefik + Gerbil + БД + дашборд, это уже не минималистичный Caddy с одним конфигом. Для домашнего контура нормально, но если VPS совсем дохлый, разница в RAM заметна.

Потом напишите, пожалуйста, как оно в проде пойдёт — очень интересно сравнить с чистой связкой WG + Caddy на практике.

У меня тоже реализовано все по похожей схеме, только туннель WG поднимает CHR на виртулизации, домашняя сеть про него даже не знает, для домашней сети использую туннель с основного роутера.

Простите, прочитал все достаточно бегло, может уже писали, но если мы говорим о доступе к Immich с внешней сети используя обратный прокси, то почему очень редко пишут про mTLS? В приложении реализована поддержка клиентских сертификатов, работает отлично, как дополнительный метод защиты.