Что на самом деле даёт скрытие origin
Три вещи, и в них стоит быть точным. Во-первых, флуд перестаёт долетать до машины, которая не способна его выдержать: у одного VPS конечный аплинк, и когда адрес, принимающий на себя пакеты, — это специально выделенный edge, а не машина с вашей базой данных, волюметрическая атака становится чужой инженерной проблемой. Во-вторых, приложение перестаёт быть доступным в обход собственной защиты — ограничения по частоте запросов, антибот-правила, WAF и геоблокировка тривиально обходятся любым, кто способен подключиться напрямую к origin, а большинство тех, кто их настраивает, никогда не проверяют, остаётся ли это возможным. В-третьих, адрес, который отдаёт контент, перестаёт быть адресом, хранящим ваши данные, — именно это разделение и приводит к тому, что жалоба на злоупотребление, скан или целевой зонд попадают куда-то дешёвое и легко заменяемое.
А теперь честная половина. Скрытие origin — не анонимность: оно прячет адрес, а не человека, а платёжный след, регистрация домена и аккаунт за ними — отдельная проблема, у которой есть своё отдельное руководство. Оно не чинит ваше приложение: origin, которого никто не может найти, всё равно уязвим в тот момент, когда его находят, — а находят их регулярно. Оно ничего не скрывает от вашего провайдера, который по определению знает, какая машина отвечает на каком адресе. Считайте это одним слоем, повышающим цену атаки, поверх машины, которую сначала как следует защитили, — а не заменой того или другого.
SP·02Все способы, которыми утекает IP-адрес origin
Причина, по которой скрытый origin так часто оказывается вовсе не скрыт, в том, что люди закрывают один канал утечки и считают, будто остальные закрылись сами собой. Это не так. Вот список, который мы прогоняем каждый раз, примерно в порядке того, как часто именно этот пункт кого-то и подводил:
- Историческая запись DNS. Сборщики пассивного DNS фиксируют вашу A-запись задолго до того, как вы встали за прокси. Адрес, которым вы пользовались в прошлом году, — это постоянный, бесплатный и доступный для поиска результат.
- Прозрачность сертификатов. Каждый публично доверенный сертификат публикуется в открытые append-only-журналы вместе с каждым именем хоста, которое он покрывает. Сертификат, выпущенный для
origin.example.com, объявляет это имя всему миру, а его A-запись довершает остальное. - Поддомены, которые никуда не переехали.
mail,ftp,webmail,cpanel,dev,staging,vpn,monitor— корневой домен ушёл за CDN, а эти так и остались указывать прямо на машину. - Почта. MX-запись на origin выдаёт адрес напрямую; то же самое делает заголовок
Received:в письме, которое отправило ваше приложение, — а вызвать такое письмо может кто угодно через форму сброса пароля. - Исходящие запросы. Webhook'и, загрузка аватарок, RSS-подписки, превью ссылок, проверки обновлений, OAuth-коллбэки. Каждый из них раскрывает адрес origin тому, кто управляет другим концом соединения, — а функция предпросмотра ссылок позволяет атакующему самому выбрать этот «другой конец».
- Ответ на голый адрес. Если origin по-прежнему отдаёт ваш сайт в ответ на запрос без совпадающего заголовка
Host, сканеры, обходящие весь интернет, уже проиндексировали его: хеш favicon, заголовок страницы, отпечаток сертификата и порядок HTTP-заголовков — всё это ищется без труда. - Забытая запись IPv6. A-запись переехала на прокси, а AAAA-запись по-прежнему указывает домой.
- Приложение, которое рассказывает о себе само. Абсолютные URL в конфиге CMS, редиректы на внутреннее имя хоста, трассировки стека, баннеры
Server, source maps, неаутентифицированный статус-эндпоинт.
Обратите внимание, что объединяет большинство этих пунктов: они постоянны. Журналы сертификатов только пополняются и ничего не удаляют, а пассивный DNS — это архив. Опубликованный адрес нельзя отозвать — можно только перестать им пользоваться, и именно поэтому важен порядок действий, изложенный ниже.
SP·03Два вида edge: CDN или собственная машина
Коммерческий CDN даёт anycast-мощность, с которой не сравнится ни один отдельный сервер, — в десятках городов, часто на бесплатном тарифе. Плата за это в том, что TLS терминируется на инфраструктуре, которую вы не контролируете: оператор видит ваш открытый трафик, знает, какому аккаунту он принадлежит, и может быть принуждён действовать исходя из этого знания — или просто однажды утром решить, что ваш контент здесь нежелателен. Есть и более тонкая проблема, характерная именно для общих фронтов: если firewall на origin разрешает опубликованные диапазоны адресов CDN, то любой другой обладатель аккаунта на этом же CDN оказывается внутри вашего списка разрешённых и может направить собственное имя хоста на ваш origin. Это реальный обход, а не теоретический, и именно поэтому существует authenticated origin pull.
Edge, который вы держите сами, — обратная сделка. Закрытый ключ есть только у вас, машина стоит в юрисдикции, которую вы выбрали осознанно, а стоит это $8.00/мес за младший тариф — по сути, статистическая погрешность на фоне того, что она защищает. Чего вы не получаете — так это anycast: флуд на 200 Gbps насытит аплинк edge вне зависимости от того, насколько элегантно настроен ваш nginx, так что поглощение на сетевом уровне должно приходить откуда-то ещё. В нашем случае это 1.5 Tbps апстрим-защиты перед каждой машиной парка — именно это и делает самостоятельно управляемый edge жизнеспособным, а не единой точкой отказа. Оба вида к тому же комбинируются: CDN спереди — ради охвата и объёма, ваш собственный узел за ним — ради той части, которую вы отказываетесь кому-либо передавать. Выбирайте исходя из того, какой отказ вам будет проще потом объяснить.
SP·04Туннель — это то, что чаще всего делают неправильно
Обычная схема — это origin, слушающий 0.0.0.0:443, с firewall'ом, вносящим адреса прокси в список разрешённых. Это работает, и это же самое слабое звено во всей конструкции. Списки разрешённых устаревают — опубликованные диапазоны меняются, а обновление так и не применяется; они общие, так что на публичном CDN в них допущен и любой другой клиент; и они отказывают открытым способом именно там, где это важнее всего, — потому что всё это время origin остаётся действующим публичным слушателем, ожидая одной ошибки конфигурации или одного ufw disable во время отладочной сессии.
Версия, которая держится, переворачивает эту схему: у origin вообще нет ни одного публичного слушателя. Между edge и origin поднимается туннель WireGuard, веб-сервер привязывается только к адресу туннеля, а на публичном интерфейсе действует политика default-deny для обоих семейств IP без единого исключения для 80 или 443. Тогда доступность — это не правило, которое кто-то может забыть продлить, а отсутствие маршрута как таковое. WireGuard здесь правильный инструмент, потому что это модуль ядра с крошечной поверхностью атаки, он молчит в ответ на неаутентифицированные сканеры (неаутентифицированный пакет вообще не получает ответа, так что UDP-порт как будто и не существует), и стоит считанные микросекунды на пакет. Если вы никогда раньше его не поднимали, руководство по WireGuard разбирает основы; здесь же нужна лишь простая связь точка-точка между двумя пирами.
Одно правило перед началом — то самое, которое сэкономит вам целый день: держите вторую SSH-сессию открытой всё время. Запереть SSH за туннелем, который вы ещё продолжаете настраивать, — обычный способ потерять машину, а на хосте, где нет личности в базе, некому выдать вам консольную сессию, и подниматься по лестнице поддержки попросту некуда. Путь назад — это заново развёрнутая машина и восстановление из бэкапа: быстрое, если бэкапы свежие, и окончательное, если нет.
SP·05Сертификаты и журнал, который публикует ваши имена хостов
Прозрачность сертификатов — вещь по-настоящему полезная, которая с удовольствием испортит вам всю неделю. Каждый сертификат, выпущенный публичным CA, отправляется в append-only-журналы, доступные для поиска кому угодно, и запись содержит каждое имя, указанное в сертификате. Выпустите сертификат для origin.example.com или direct.example.com — и вы навсегда, в структурированном виде, опубликуете именно то имя хоста, которое пытались не афишировать. Хуже того, привычка включать имена staging- и admin-хостов в один и тот же список SAN превращает одно неосторожное продление сертификата в карту всей вашей инфраструктуры.
Дисциплина здесь простая. Публичные сертификаты живут только на edge и покрывают только те имена, которыми реально пользуются посетители. Origin получает самоподписанный сертификат или сертификат от небольшого частного CA, закреплённый в конфигурации прокси через proxy_ssl_trusted_certificate, — на канале, по которому говорит только ваш собственный прокси, не нужно ничего публично доверенного, а выпуск публичного сертификата для него не даёт ничего, кроме лишней записи в журнале. Если вам нужно много публичных поддоменов, wildcard-сертификат публикует одно имя вместо тридцати. И переключите ACME на проверку DNS-01: HTTP-01 требует, чтобы что-то отвечало на 80-м порту для проверяемого имени хоста, — а это именно тот публичный слушатель, который вы только что убрали. Наконец, примите асимметрию: журналы работают в режиме append-only, так что уже опубликованное имя хоста нельзя отозвать. Если оно указывало на origin, origin нужен новый адрес.
Почта и другие сервисы, отвечающие не на том адресе
Почта — классический обход защиты, потому что она обязана быть доступной по определению. Если MX-запись вашего домена указывает на origin, всё упражнение заканчивается раньше, чем успевает начаться, — запись публична, и один dig ставит точку в поисках. Даже если MX указывает в другое место, приложение, отправляющее почту напрямую с origin, впечатывает адрес хоста-отправителя в цепочку Received: каждого письма, а любая форма, рассылающая письмо пользователю по требованию, превращает это в поиск в режиме самообслуживания. Решение — сделать origin клиентом и никогда сервером: пропускайте исходящую почту через сервис отправки (submission) или отдельную машину, держите MX на машине, которой разрешено быть найденной, и прочитайте полные заголовки тестового письма, прежде чем считать задачу выполненной. Держать свою почту в промышленном масштабе — отдельный проект сам по себе, и ему не место на машине, которую вы прячете.
Затем пройдитесь по всему остальному, что тихо слушает порты. Агенты мониторинга, дашборды контейнеров, порты БД, открытые «временно», эндпоинт метрик на 9100, панель управления на нестандартном порту, демон SSH на публичном интерфейсе. Каждый из них — это сервис, который отвечает на адресе, который вы пытаетесь сохранить приватным, а сканеры находят нестандартные порты так же легко, как и стандартные. Аудит — это одна команда, ss -tulpn, и правильный результат — список, в котором ничто не привязано к публичному адресу. Именно третий шаг ниже добивается такого результата и следит, чтобы он таким и оставался.
Egress (исходящий трафик): соединения, которые инициирует ваш origin
Origin, который ничего не принимает, всё равно способен себя выдать, потому что он не только принимает соединения — он их ещё и открывает. Зеркала пакетов, NTP, webhook платёжному процессору, API бота, изображение, загруженное ради превью ссылки, проверка лицензии, исходящая SMTP-сессия, Git-remote, репортер ошибок. Для того, кто сидит на другом конце любого из этих соединений, публичный адрес origin — это просто адрес источника соединения. В большинстве случаев это безобидно, потому что вы сами выбрали этот «другой конец» и доверяете ему. Проблема — в горстке точек, которые выбирает уже атакующий: вставьте ссылку в любую функцию, которая рендерит превью, зарегистрируйте webhook или найдите подделку запроса на стороне сервера в модуле импорта изображений — и origin сам обратится и подключится к хосту, за которым наблюдает атакующий. Это деанонимизация за две минуты без единого эксплойта.
Есть два защитимых ответа. Строгий вариант заворачивает весь egress в туннель и отдаёт NAT на откуп edge, так что исходящий адрес источника у origin становится адресом edge, — это одна настройка в конфиге пира плюс forwarding и правило маскарадинга на другом конце. wg-quick сам решает проблему петли маршрутизации: при маршруте 0.0.0.0/0 он устанавливает правило fwmark, благодаря которому собственные пакеты туннеля всё равно доходят до эндпоинта напрямую, — и именно это чаще всего ломают, когда прописывают маршруты вручную. Прагматичный вариант оставляет прямой egress для трафика, который вы контролируете, и ставит прокси перед всем, что забирает URL, заданный пользователем. Что не имеет защиты — так это незнание, какой из двух вариантов у вас на самом деле. Решите это осознанно, а затем проверьте запросом на хост, которым владеете сами, и взглядом на адрес источника в его логах.
Сколько это стоит и как доказать, что это работает
Строка в бюджете — это один дополнительный VPS. Младший тариф за $8.00/мес терминирует TLS и проксирует небольшой сайт, даже не заметив нагрузки, — reverse-proxy — это по большей части копирование сокета, а 2 vCPU и 4 ГБ RAM с запасом хватает далеко за той точкой, где узким местом становится уже origin позади него. Разместите его в регионе, отличном от региона origin, чтобы один и тот же юридический инструмент или одна авария в дата-центре не задевали сразу обоих, и не забывайте про задержку: лишний хоп добавляет вполне реальные миллисекунды, так что edge в Амстердаме перед origin в Куала-Лумпуре — это осознанное архитектурное решение, а не случайность. Пары внутри одного континента обычно обходятся в считанные миллисекунды, а переиспользование TLS-сессии, которое вы получаете на edge, часто отыгрывает эту задержку обратно уже на реальной загрузке страницы.
Доказать, что это работает, — это то, что отличает настройку от контроля, и это повторяющаяся задача, а не разовая: каждый новый поддомен, каждый новый сертификат, каждая новая интеграция — это свежий шанс заново опубликовать адрес. Батарея проверок из седьмого шага занимает около десяти минут: попробуйте достучаться до сайта напрямую по адресу origin, выгрузите все имена хостов, для которых вы когда-либо выпускали сертификат, переберите очевидные имена поддоменов, проверьте забытую AAAA-запись и отправьте письмо самому себе. Прогоняйте это после каждого изменения инфраструктуры. И держите в голове вывод, пока делаете это: если origin отвечает, правильное исправление — не ещё одно правило firewall, а новый адрес, потому что старый уже лежит в чьём-то архиве. В общей схеме этот слой стоит после защиты сервера и рядом с бэкапами вне хоста: защита определяет, насколько сложно сломать машину, бэкапы определяют, насколько быстро вы восстановитесь, а это определяет, насколько сложно вообще найти машину.
SP·09Шаг за шагом
-
01
Разверните edge и дайте ему ровно одну задачу
Разверните второй VPS в регионе, который не совпадает с регионом origin, и относитесь к нему как к устройству с одной-единственной задачей: терминация TLS, reverse-proxy — и больше ничего. Никакой базы данных, никакого кода приложения, никаких shell-скриптов, исчезновение которых кто-то заметит. Прогоните на нём чек-лист первого часа — SSH только по ключу, firewall с default-deny на обоих семействах IP, автоматические обновления безопасности, — а затем откройте ровно три порта.
ssh root@198.51.100.20 apt update && apt full-upgrade -y hostnamectl set-hostname edge-01 apt install -y nginx wireguard-tools ufw unattended-upgrades ufw default deny incoming && ufw default allow outgoing ufw limit 22/tcp ufw allow 80,443/tcp ufw allow 51820/udp ufw --force enable
-
02
Поднимите туннель прежде, чем трогать DNS
Два пира, один канал. Сгенерируйте пару ключей на каждой машине и выделите туннелю собственную небольшую подсеть — в итоге origin будет доступен по адресу
10.66.0.2и больше нигде. Origin сам дозванивается до edge (это та сторона, у которой не будет ни одного открытого порта), поэтому именно он несётEndpointи keepalive; edge только слушает.# on both machines umask 077; wg genkey | tee privkey | wg pubkey > pubkey # edge-01 — /etc/wireguard/wg0.conf [Interface] Address = 10.66.0.1/24 ListenPort = 51820 PrivateKey = <edge-privkey> [Peer] PublicKey = <origin-pubkey> AllowedIPs = 10.66.0.2/32 # origin-01 — /etc/wireguard/wg0.conf [Interface] Address = 10.66.0.2/24 PrivateKey = <origin-privkey> [Peer] PublicKey = <edge-pubkey> Endpoint = 198.51.100.20:51820 AllowedIPs = 10.66.0.1/32 PersistentKeepalive = 25
Включите его с обеих сторон и подтвердите рукопожатие, прежде чем двигаться дальше, — туннель, который работает только до следующей перезагрузки, хуже, чем его отсутствие.
systemctl enable --now wg-quick@wg0 wg show # expect a recent handshake and non-zero transfer ping -c3 10.66.0.1 # from the origin
-
03
Сделайте origin недоступным из публичного интернета
Это шаг, который выполняет всю настоящую работу, и тот самый шаг, на котором люди сами себя запирают снаружи. Откройте вторую SSH-сессию и оставьте её подключённой прежде, чем запускать что-либо из написанного ниже, — здесь нет консоли поддержки, на которую можно опереться. Затем привяжите веб-сервер к адресу туннеля, сбросьте всё на публичном интерфейсе и разрешите только туннель и сам эндпоинт WireGuard.
# /etc/nginx/sites-available/app — listen on the tunnel only listen 10.66.0.2:8080; ufw --force reset ufw default deny incoming ufw default allow outgoing ufw allow in on wg0 to any port 8080 proto tcp ufw allow in on wg0 to any port 22 proto tcp ufw allow from 198.51.100.20 to any port 51820 proto udp ufw --force enable # the audit: nothing may be bound to a public address ss -tulpn | grep -Ev '10\.66\.0\.2|127\.0\.0|\[::1\]'
Если последняя команда что-то выводит — это утечка: исправляйте адрес привязки, а не накручивайте вокруг него ещё одно правило firewall. Выжить разрешено только двум слушателям: WireGuard на 51820 и sshd, если вы ещё не перенесли его в туннель.
-
04
Терминируйте TLS на edge и проксируйте через туннель
Выпустите публичный сертификат на edge для тех имён, которыми реально пользуются посетители, и проксируйте выше по потоку на адрес туннеля. Второй блок server — не необязательное украшение: именно он не даёт edge отдавать ваш сайт сканеру, который подключается по IP без заголовка
Host, — а именно так и снимают отпечаток с парадной двери.# edge-01 — /etc/nginx/sites-available/example.com server { listen 443 ssl; http2 on; server_name example.com www.example.com; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; add_header Strict-Transport-Security "max-age=63072000" always; location / { proxy_pass http://10.66.0.2:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_http_version 1.1; } } # anything that is not a known hostname gets nothing at all server { listen 80 default_server; listen 443 ssl default_server; ssl_reject_handshake on; return 444; } -
05
Верните origin настоящий IP клиента
За прокси каждый запрос приходит с адреса
10.66.0.1. Если ничего не исправить, ваши логи доступа станут бесполезны, rate limiting по IP начнёт душить туннель вместо атакующего, аfail2banрано или поздно забанит сам edge и положит сайт — по-настоящему популярный способ устроить себе простой в процессе укрепления защиты. Доверяйте заголовку с прокинутым адресом, но только от адреса туннеля, и никогда — от всего остального мира.# origin-01 — /etc/nginx/conf.d/realip.conf set_real_ip_from 10.66.0.1; real_ip_header X-Forwarded-For; real_ip_recursive off;
Сделайте то же самое в приложении —
ProxyFixво Flask,TRUSTED_PROXIESв Laravel,set_real_ip_fromплюс собственный список доверенных прокси у фреймворка, — а rate limiting поставьте на edge, где настоящий адрес клиента присутствует нативно:# edge-01 — /etc/nginx/nginx.conf (http block) limit_req_zone $binary_remote_addr zone=front:10m rate=20r/s; # then, inside the location block limit_req zone=front burst=40 nodelay;
-
06
Уберите почту и egress с адреса origin
Направьте MX на машину, которой разрешено быть найденной, отправляйте исходящую почту через релей, а не напрямую с origin, и переключите продление сертификатов на проверку DNS-01, чтобы ничему не приходилось отвечать на 80-м порту. Затем решите, что делать с остальным исходящим трафиком. Чтобы завернуть его целиком через edge, расширьте
AllowedIPsу origin и позвольте edge выполнять маскарадинг —wg-quickсам устанавливает правило fwmark, которое сохраняет доступность самого туннеля, так что маршрут для эндпоинта не придётся прописывать вручную.# origin-01 — /etc/wireguard/wg0.conf, in [Peer] AllowedIPs = 0.0.0.0/0, ::/0 # edge-01 — forward and NAT the tunnel echo 'net.ipv4.ip_forward=1' > /etc/sysctl.d/99-fwd.conf && sysctl --system ufw route allow in on wg0 out on eth0 # in /etc/ufw/before.rules, above the *filter block: # *nat # :POSTROUTING ACCEPT [0:0] # -A POSTROUTING -s 10.66.0.0/24 -o eth0 -j MASQUERADE # COMMIT systemctl restart ufw && wg-quick down wg0 && wg-quick up wg0
Затем проверьте это с origin:
curl -s https://ifconfig.coдолжен вернуть адрес edge, а не собственный. -
07
Поохотьтесь на собственный origin, затем напишите runbook
Атакуйте его так, как это сделал бы кто-то другой. Первая команда — самая важная: если origin по-прежнему отдаёт ваш сайт при обращении напрямую, ничего из написанного выше пока не работает.
# does the origin answer for your hostname? curl -sk --resolve example.com:443:203.0.113.10 https://example.com/ \ -o /dev/null -w '%{http_code}\n' # want: a timeout, not 200 # every hostname you have ever certified, from the public logs curl -s 'https://crt.sh/?q=%25.example.com&output=json' \ | grep -o '"name_value":"[^"]*"' | sort -u # records that never moved dig +short example.com A; dig +short example.com AAAA; dig +short example.com MX for h in www mail ftp webmail cpanel dev staging vpn monitor origin direct; do printf '%-9s %s\n' "$h" "$(dig +short $h.example.com | tr '\n' ' ')" doneЗатем отправьте письмо самому себе из приложения и прочитайте полную цепочку
Received:, а также вставьте ссылку на подконтрольный вам хост в любую функцию, которая рендерит превью, и проверьте, какой адрес её загрузил. Запишите, как выглядит правильный результат для каждой проверки, и прогоняйте весь набор заново после каждого изменения DNS, каждого нового сертификата и каждой новой интеграции. Если хоть одна из них вскроет origin, пересоберите его на новом адресе — опубликованный уже лежит в чьём-то архиве, и никакое правило firewall его оттуда не вернёт.


