Закройте сервер и поставьте WAF перед приложением

С первого развёртывания erpc worker пускает SSH только с вашего адреса и отбрасывает все остальные входящие соединения. Таблица [waf] ставит фильтрующий прокси перед вашим API.
Эта страница описывает ERPC CLI v0.1.0-rc.4 — пререлиз.

Брандмауэр

Развёртывание настраивает брандмауэр сервера до первого запуска приложения:
  • SSH — только с адресов из firewall.ssh_allow и с адреса, с которого идёт развёртывание;
  • все остальные входящие соединения отбрасываются, кроме run.port для адресов из firewall.app_allow; для приложения за WAF вместо него — порт WAF, и только там, где WAF на сервере уже работает по правилам, которые сейчас задаёт [waf] (ниже).
Каждое изменение выполняется под 120-секундным таймером отката и сохраняется только после того, как проходит новое соединение, поэтому ошибочное изменение не может закрыть вам доступ.
Однако allowlist переживает адрес, с которого он был составлен: если ваш домашний адрес изменится, вернуть доступ можно через поддержку ERPC для сервера ERPC или через консоль провайдера для собственного сервера. Прежде чем полагаться на домашний адрес, добавьте в firewall.ssh_allow второй адрес, с которого можно достучаться до сервера (офис, выход VPN).
Если ваш адрес меняется, добавьте все постоянные адреса, которые используют ваши администраторы:
bash
erpc worker firewall allow --to ssh 203.0.113.10 198.51.100.7
КомандаЧто делает
erpc worker firewall listПоказывает, кто может достучаться до сервера: erpc.toml рядом с таблицей сервера
erpc worker firewall allow --to ssh|app <addresses>Пропускает адреса и применяет изменение. SSH разрешается с отдельных адресов; сеть принимается только с --to app
erpc worker firewall deny --to ssh|app <addresses>Перестаёт их пропускать
erpc worker firewall syncСнова приводит брандмауэр сервера к тому, что указано в erpc.toml

Поставьте WAF перед приложением

Добавьте в erpc.toml одну таблицу:
toml
[waf]
port = 8080
После этого erpc worker deploy запускает на сервере обратный HTTP-прокси в отдельном юните systemd (erpc-waf-<app>-<env>): от имени непривилегированного динамического пользователя без ключа, без секрета и без чего-либо, принадлежащего воркеру, с ограничением в 1 GiB памяти. Он слушает waf.port, читает каждый запрос раньше приложения, отклоняет то, что названо в его правилах, и передаёт остальное приложению на 127.0.0.1:<run.port>.
  • Приложение остаётся на loopback. Его собственный порт никогда не открывается снаружи. erpc worker api и проверка работоспособности обращаются к приложению через loopback, в обход WAF.
  • Кто может подключаться, по-прежнему определяет список брандмауэра, и порт WAF — единственное место, где этот список может разрешить всех:
    sh
    erpc worker firewall allow --to app 0.0.0.0/0     # and ::/0
    Если в списке никого нет, WAF слушает, но никто не пропускается, и развёртывание сообщает об этом.
  • Порт открывается только после того, как WAF начнёт его слушать, и никогда раньше. Развёртывание, при котором WAF не запустился, завершается ошибкой и сообщает причину, а приложение продолжает работать.
  • Когда вы меняете [waf], порт не открывается ни для кого нового, пока развёртывание не установит новые правила.
  • Если убрать [waf] из erpc.toml и выполнить развёртывание, WAF будет удалён с сервера.

Настройки WAF

Обязателен только port. Все ключи со значениями по умолчанию:
toml
[waf]
port = 8080          # the public port, 1024 or above; the app keeps [run] port
methods = ["GET", "POST", "PUT", "DELETE"]
allow_paths = []     # every path
deny_paths = []
max_body_bytes = 1048576
max_header_bytes = 16384
rate = { per_second = 50, burst = 100 }
max_connections_per_address = 32
ban = { after = 100, window_secs = 60, for_secs = 600 }
block_probes = true
header_timeout_secs = 10
body_timeout_secs = 30
upstream_timeout_secs = 30
new_connections = { per_second = 50, burst = 100 }
КлючЧто делаетЧто получает отклонённое
methodsПередаваемые методы, заглавными буквами (не более 16; никогда CONNECT)405, с Allow
allow_pathsПередаваемые пути; если ничего не указано — все пути. Шаблон — это один путь или всё под префиксом, если шаблон заканчивается на *: /api/* — это /api и всё ниже. Сравнивается с путём после percent-декодирования, с учётом регистра404
deny_pathsОтклоняемые пути; записываются так же и проверяются первыми403
max_body_bytesНаибольший размер тела запроса, до 64 MiB413
max_header_bytesНаибольший размер строки запроса и заголовков вместе, от 1024 до 65536431
rateЧисло запросов в секунду с одного адреса и сколько запросов разом допускается, прежде чем начнёт действовать ограничение скорости429, с Retry-After
max_connections_per_addressСоединения, которые один адрес держит одновременноСоединение закрывается без ответа
banПосле after заблокированных запросов в пределах window_secs адрес отклоняется на for_secs (до 30 дней)Его соединения закрываются без ответа
block_probesОтклоняет то, что запрашивают сканеры (/.env, /.git, /wp-login.php и подобное)403
header_timeout_secsСколько времени у клиента есть на отправку заголовочной части запроса, от 1 до 120. Столько же ждут клиента, который остановился посреди тела или перестал принимать ответ408 для заголовочной части; 400 для остановившегося тела; для непринятого ответа соединение закрывается
body_timeout_secsСколько всего времени может занимать тело запроса, от 1 до 600, не считая времени, которое WAF ждёт приложение408
upstream_timeout_secsСколько времени у приложения есть, чтобы принять соединение и начать отвечать, от 1 до 600504; 502, если приложение не слушает
new_connectionsСобственное ограничение брандмауэра на новые TCP-соединения в секунду с одного адреса к порту WAF; лишние отбрасываются nftables до того, как их увидит прокси. Пока набор адресов nftables заполнен, адрес, которого в нём нет, не учитывается, поэтому это ограничение не применяется к нему, пока в наборе снова не появится место; собственные ограничения прокси (rate, max_connections_per_address, ban) по-прежнему действуютПакет отбрасывается
rate, max_connections_per_address, ban и new_connections отключаются значением false (ban = false), а пропущенный ключ принимает значение по умолчанию. erpc worker check ещё до любого развёртывания сообщает, что будет делать таблица [waf] и что в ней не так.

Что WAF делает без всяких настроек

  • Запрос, который не читается как один однозначный запрос HTTP/1, получает 400; то же получает путь, записанный не в нормальной форме.
  • Запрос на смену протокола (Upgrade) получает 501.
  • Каждый отказ засчитывается в бан, и каждый отказ завершает своё соединение.
  • Адресом считается адрес IPv4 или IPv6 /64.
  • WAF не передаёт дальше заголовки соединения клиента с прокси (Connection и названные в нём заголовки, Keep-Alive, Proxy-Authenticate, Proxy-Authorization, Proxy-Connection, TE, Trailer), Expect, заголовки, в имени которых есть подчёркивание, а также собственные Forwarded, X-Real-IP и X-Forwarded-Proto клиента. Он устанавливает в X-Real-IP адрес, с которого пришло соединение, и добавляет этот адрес в конец X-Forwarded-For; только последняя запись X-Forwarded-For исходит от самого прокси.

Как увидеть, что делает WAF

КомандаЧто показывает
erpc worker statusПорт WAF, куда он передаёт запросы, с какого времени и счётчики с того момента
erpc worker firewall listКто может достучаться до порта WAF и что было заблокировано, с разбивкой по правилам
erpc worker logs --wafКаждый заблокированный запрос: правило, статус, адрес клиента, метод и первые 200 байт цели запроса
Переданный запрос не оставляет строки в журнале, а заголовки и тела никогда не записываются в журнал; query заблокированного запроса записывается — насколько хватает этих 200 байт. То, что отправляет клиент, не может ничего написать в вашем терминале: WAF пишет свой журнал печатаемыми символами ASCII.

Чего WAF не делает

  • Нет TLS. На своём порту он работает по обычному HTTP/1.1.
  • Нет HTTP/2: на него он отвечает 400.
  • Нет WebSocket и никакого другого Upgrade: 501.
  • Не заглядывает в тела. Тело измеряется, но никогда не читается. Список запросов сканеров короткий и служит фильтром шума, а не гарантией: приложение по-прежнему само проверяет свои входные данные.
  • Нет аутентификации. Кто может вызывать приложение, решает само приложение и список брандмауэра.
  • Один адрес — один клиент. Клиенты за одним NAT, прокси или CDN делят одно ограничение скорости и один бан; у клиента с несколькими адресами столько же ограничений скорости, сколько адресов.
  • Баны и счётчики забываются при перезапуске WAF.
  • Он запоминает не более 8192 адресов и одновременно держит не более 1024 соединений.
  • Он слушает только IPv4, поэтому запись IPv6 в app_allow пока ни к чему не ведёт.
  • Путь сравнивается как байты, в которые он декодируется. До приложения, которое читает пути менее строго (без учёта регистра, с параметрами ;, с двойным декодированием), можно добраться по имени, которое deny_paths не покрывает. У allow_paths такой бреши нет: то, что не указано, не передаётся.
  • Медленный читатель обслуживается до конца. Клиент, который продолжает принимать ответ, как бы медленно он это ни делал, обслуживается до конца.
  • Наплыв запросов может оборвать большую загрузку. WAF работает в одном потоке, поэтому наплыв запросов может оборвать большую загрузку с кодом 408. Приложение, принимающее большие тела, повышает body_timeout_secs вместе с max_body_bytes.
Руководство по воркерам содержит все правила, включая то, когда изменение app_allow откладывается до следующего развёртывания.