Доступ к серверу только через WebShield
Как закрыть свой сервер от прямых обращений по секретному заголовку WebShield — примеры для nginx, Apache, IIS, Caddy и других, плюс автобан через fail2ban.
Домен вы спрятали за WebShield, а IP-адрес сервера остался прежним. Кто его знает — старые DNS-записи, утёкший заголовок письма, скан подсети, — тот обращается к сайту напрямую и проходит мимо защиты целиком.
Лечится это так: сервер перестаёт отвечать всем, кроме нас.
Как это работает
Заголовок раздела «Как это работает»К каждому запросу, который мы отправляем на ваш сервер, добавляется заголовок с секретом вашего хоста:
X-WebShield-Origin-Token: wso_…Ваш сервер проверяет заголовок и отвечает только на запросы с ним. Всё остальное — отказ. Заголовок посетителя с тем же именем до вас не дойдёт никогда: мы подставляем своё значение поверх.
Почему не список наших адресов: он меняется. Узлы добавляются под нагрузкой и выводятся из работы, и ваш файрвол пришлось бы править вслед за нашей сетью. Секрет от топологии не зависит — прописали один раз и забыли.
Токен — в личном кабинете: Хосты → нужный хост → Доступ только через WebShield. Значение у каждого хоста своё.
Перед тем как включать
Заголовок раздела «Перед тем как включать»- Весь трафик уже идёт через нас. В DNS запись сайта проксируется, прямых
A-записей на сервер не осталось. Иначе вы закроете сайт от собственных посетителей. - У вас есть запасной вход. SSH, панель хостинга — что угодно, чем откатить правку, не открывая сайт.
Про сертификат беспокоиться не нужно: проверка Let’s Encrypt приходит на ваш домен, то есть к нам, и мы передаём её на сервер тем же путём и с тем же заголовком. Отдельное исключение для /.well-known/acme-challenge/ не требуется — а если его сделать, вы оставите приоткрытой ровно ту дверь, которую пришли закрывать.
Проверка
Заголовок раздела «Проверка»Прямой запрос должен получить отказ, запрос через нас — работать:
# Напрямую по адресу сервера — ожидаем 403.curl -sk -o /dev/null -w '%{http_code}\n' https://203.0.113.10/ -H 'Host: example.com'
# Через WebShield — ожидаем нормальный ответ.curl -s -o /dev/null -w '%{http_code}\n' https://example.com/Код ответа выбираете вы: 403 честнее 401 (никакой авторизацией тут не пахнет), а 444 в nginx вообще обрывает соединение молча. Для сайта разницы нет — до посетителей эти ответы не доходят, их видят только те, кто пришёл мимо нас.
server { server_name example.com;
# Не от WebShield — дальше не идём. if ($http_x_webshield_origin_token != "wso_ВАШ_ТОКЕН") { return 403; }
# ... ваша обычная конфигурация: proxy_pass, root, fastcgi_pass ...}Проверить и применить: nginx -t && systemctl reload nginx.
Apache 2.4, в конфигурации сайта или в .htaccess — дополнительных модулей не нужно:
<If "%{HTTP:X-WebShield-Origin-Token} != 'wso_ВАШ_ТОКЕН'"> Require all denied</If>Проверить и применить: apachectl configtest && systemctl reload apache2.
Тот же синтаксис подходит для LiteSpeed и OpenLiteSpeed с включённой совместимостью с .htaccess.
example.com { @notWebShield not header X-WebShield-Origin-Token "wso_ВАШ_ТОКЕН" respond @notWebShield 403
reverse_proxy localhost:3000}Применить: caddy reload --config /etc/caddy/Caddyfile.
Нужен модуль URL Rewrite. В web.config сайта:
<configuration> <system.webServer> <rewrite> <rules> <rule name="WebShield only" stopProcessing="true"> <match url=".*" /> <conditions> <add input="{HTTP_X_WEBSHIELD_ORIGIN_TOKEN}" pattern="^wso_ВАШ_ТОКЕН$" negate="true" /> </conditions> <action type="CustomResponse" statusCode="403" statusReason="Forbidden" statusDescription="Forbidden" /> </rule> </rules> </rewrite> </system.webServer></configuration>Traefik
Заголовок раздела «Traefik»Токен добавляется прямо в правило роутера:
http: routers: site: rule: "Host(`example.com`) && Header(`X-WebShield-Origin-Token`, `wso_ВАШ_ТОКЕН`)" service: siteЗапрос без токена под правило не подходит, маршрута для него нет — Traefik отвечает 404.
HAProxy
Заголовок раздела «HAProxy»frontend https-in bind *:443 ssl crt /etc/haproxy/certs/example.com.pem
acl from_webshield req.hdr(X-WebShield-Origin-Token) -m str wso_ВАШ_ТОКЕН http-request deny deny_status 403 unless from_webshield
default_backend siteNode.js (Express)
Заголовок раздела «Node.js (Express)»const TOKEN = process.env.WEBSHIELD_ORIGIN_TOKEN;
app.use((req, res, next) => { if (req.get("x-webshield-origin-token") === TOKEN) return next(); res.status(403).end();});Токен держите в переменной окружения, а не в репозитории.
В самом начале точки входа (index.php), до подключения фреймворка:
$token = getenv('WEBSHIELD_ORIGIN_TOKEN');$sent = $_SERVER['HTTP_X_WEBSHIELD_ORIGIN_TOKEN'] ?? '';if (!hash_equals($token, $sent)) { http_response_code(403); exit;}Для WordPress, Битрикса и прочих готовых систем правку лучше делать не в коде, а на уровне веб-сервера — обновление CMS её не затрёт.
Python (Django, Flask, FastAPI)
Заголовок раздела «Python (Django, Flask, FastAPI)»Прослойкой WSGI/ASGI — до маршрутизации приложения:
import osfrom hmac import compare_digest
TOKEN = os.environ["WEBSHIELD_ORIGIN_TOKEN"]
class WebShieldOnly: def __init__(self, app): self.app = app
def __call__(self, environ, start_response): sent = environ.get("HTTP_X_WEBSHIELD_ORIGIN_TOKEN", "") if not compare_digest(sent, TOKEN): start_response("403 Forbidden", [("Content-Type", "text/plain")]) return [b"Forbidden"] return self.app(environ, start_response)Автобан отказов через fail2ban
Заголовок раздела «Автобан отказов через fail2ban»Отказ — это уже не ошибка, а улика: до вашего сервера напрямую не достучится ни один живой посетитель, все они приходят через нас. Значит, адрес в таком логе принадлежит тому, кто ищет ваш origin. Его можно банить, не разбираясь, — и сразу надолго.
Пишите отказы в отдельный файл, чтобы не вылавливать их из общего лога.
Отдельный лог отказов
Заголовок раздела «Отдельный лог отказов»nginx. Формат объявляется рядом с остальными (контекст http), проверка переезжает внутрь location:
log_format ws_denied '$remote_addr $host "$request" $status';
server { server_name example.com;
location / { if ($http_x_webshield_origin_token != "wso_ВАШ_ТОКЕН") { access_log /var/log/nginx/origin-denied.log ws_denied; return 403; }
# ... ваша обычная конфигурация ... }}Apache. Условное логирование по той же проверке:
SetEnvIfExpr "%{HTTP:X-WebShield-Origin-Token} != 'wso_ВАШ_ТОКЕН'" ws_deniedCustomLog /var/log/apache2/origin-denied.log common env=ws_denied
<If "%{HTTP:X-WebShield-Origin-Token} != 'wso_ВАШ_ТОКЕН'"> Require all denied</If>Фильтр — /etc/fail2ban/filter.d/webshield-origin.conf. В файл попадают только отказы, поэтому ловить достаточно адрес в начале строки:
[Definition]failregex = ^<HOST>ignoreregex =Правило — /etc/fail2ban/jail.d/webshield-origin.conf:
[webshield-origin]enabled = truefilter = webshield-originlogpath = /var/log/nginx/origin-denied.logmaxretry = 1findtime = 1dbantime = 30dbanaction = nftables-allportsmaxretry = 1 здесь не опечатка: второго шанса такому адресу давать незачем. banaction выберите под свой файрвол — nftables-allports, iptables-multiport или ufw.
Проверить, что правило подхватилось: fail2ban-client status webshield-origin.
Если отдельного файла нет
Заголовок раздела «Если отдельного файла нет»Отказы попадают в общий access.log — там они неотличимы от чужих 403 вашего приложения, и фильтр ^<HOST> забанит всех подряд, включая живых посетителей. Нужна примета, по которой строку опознать.
nginx — отвечать 444. Соединение обрывается молча, а в лог идёт статус, которого не вернёт ни одно приложение:
if ($http_x_webshield_origin_token != "wso_ВАШ_ТОКЕН") { return 444; }[Definition]failregex = ^<HOST> -.*" 444 \d+ignoreregex =[webshield-origin]enabled = truefilter = webshield-originlogpath = /var/log/nginx/access.logmaxretry = 1findtime = 1dbantime = 30dbanaction = nftables-allportsnginx — оставить 403 и дописать метку в формат лога. Пригодится, если 444 уже занят другими правилами:
log_format ws_combined '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" "$http_user_agent" $ws_denied';
server { server_name example.com; access_log /var/log/nginx/access.log ws_combined;
# Объявление обязательно: иначе nginx ругается на неизвестную переменную. set $ws_denied "-"; if ($http_x_webshield_origin_token != "wso_ВАШ_ТОКЕН") { set $ws_denied "ws-denied"; return 403; }
# ... ваша обычная конфигурация ...}Apache — та же метка через переменную окружения:
SetEnvIfExpr "%{HTTP:X-WebShield-Origin-Token} != 'wso_ВАШ_ТОКЕН'" ws_denied=ws-deniedLogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %{ws_denied}e" ws_combinedCustomLog ${APACHE_LOG_DIR}/access.log ws_combinedВ обоих случаях строка отказа заканчивается словом ws-denied, у остальных там прочерк. Фильтр ловит именно её:
[Definition]failregex = ^<HOST> .* ws-denied$ignoreregex =Правило то же, только logpath указывает на общий лог.
Перевыпуск токена
Заголовок раздела «Перевыпуск токена»Кнопка Перевыпустить в карточке хоста выдаёт новое значение, старое перестаёт работать в течение минуты.
Порядок при перевыпуске обратный включению: сначала новое значение уходит к нам (кнопка), потом на ваш сервер. Промежуток между этими действиями сайт отвечает ошибкой, поэтому держите конфигурацию наготове. На серверах, где правку применить мгновенно нельзя, разрешите на время оба значения — старое и новое.
Перевыпускайте, если значение попало в общий репозиторий, в скриншот или в переписку.
Если сайт ответил ошибкой
Заголовок раздела «Если сайт ответил ошибкой»- 403 у всех посетителей. Токен на сервере не совпадает с тем, что в личном кабинете, — сравните значения целиком, включая префикс
wso_. - Не работает часть путей. Правило применили не ко всему сайту: у вложенных
location,<Directory>или маршрутов бывает своя проверка. - Файлы в CDN отдаются, а страницы нет (или наоборот). Некоторые панели обслуживают статику отдельным сервером — правило нужно и там.
Токен защищает от обращений мимо нас, но не заменяет файрвол: закрытые от интернета порты базы данных, панели и SSH остаются вашей задачей.