feat: generate XFF_HMAC_KEY when none is passed
Build docker image and push to registry.bitdeals.org / main-build-job (push) Successful in 37s
Build docker image and push to registry.bitdeals.org / main-build-job (push) Successful in 37s
An empty key switches the pseudonym off: no X-Client-Id is sent, and a rate limit downstream falls back to one bucket shared by every visitor. That is the one state nobody chooses on purpose and the easiest to reach by forgetting a line in a .env — so the entrypoint now fills the key in with `openssl rand -base64 32` when nothing else did. Nobody picks this value, nothing outside the container needs to know it, and no two deployments need the same one, which is what makes generating it the right default rather than a convenience. A fresh key per container start costs a reset of the downstream rate-limit buckets — invisible against a one-minute window — and makes pseudonyms from before and after unlinkable, which is the property the key exists for rather than a loss. Passing one explicitly still wins, for whoever wants pseudonyms stable across restarts or identical on two proxies. base64 and not hex, deliberately: HAProxy's hmac() decodes the key as base64, and hex would be accepted and silently decoded into something else — a usable key, but it would quietly cost the guarantee that a malformed one stops the container at configuration parsing. The base image's entrypoint is the haproxy binary with no shell in between, so the wrapper is the whole chain and execs the same binary with the same arguments. CMD is restated rather than inherited. Verified by building the image and checking the config in all three states: unset (wrapper reports it generated one, config parses), set and valid (wrapper silent, config parses), set and not base64 (`[ALERT] invalid args in converter 'hmac' : failed to parse key`, container refuses to start). READMEs updated in both languages, and "address" is spelled "IP address" throughout — it was never anything else.
This commit is contained in:
+35
-25
@@ -19,9 +19,10 @@ HAProxy, работающий в docker-контейнере с конфигур
|
||||
имеет уровень `admin` и не защищён аутентификацией, поэтому кто может его
|
||||
открыть, определяют права на файл, — см. «Замечания».
|
||||
|
||||
Переменная окружения одна — `XFF_HMAC_KEY`, и она необязательна. Всё остальное
|
||||
задано в `docker/haproxy.cfg`, который копируется в образ при сборке, поэтому
|
||||
изменение маршрутизации означает пересборку и повторное развёртывание.
|
||||
Переменная окружения одна — `XFF_HMAC_KEY`, и задавать её не нужно: если она не
|
||||
задана, точка входа генерирует ключ сама. Всё остальное задано в
|
||||
`docker/haproxy.cfg`, который копируется в образ при сборке, поэтому изменение
|
||||
маршрутизации означает пересборку и повторное развёртывание.
|
||||
|
||||
Сертификат читается из `/usr/local/etc/haproxy/certificates/site.pem`, том
|
||||
подключён **только на чтение** и разделяется с certbot. Файл обязан
|
||||
@@ -100,7 +101,7 @@ docker push registry.bitdeals.org/haproxy
|
||||
|-p 443|HTTPS. Требует наличия `site.pem` в томе сертификатов до старта контейнера|
|
||||
|-v /usr/local/etc/haproxy/certificates|Каталог сертификатов, только на чтение. Читается лишь `site.pem` и лишь при связывании портов. certbot пишет его через тот же том, подключённый на запись как `/etc/certificates`|
|
||||
|-v /var/lib/haproxy|Сокет runtime API (`admin.sock`, уровень `admin`, **без аутентификации**). Подключайте этот том к certbot и больше никуда — см. «Замечания»|
|
||||
|-e XFF_HMAC_KEY|Необязательная, base64. Задана — адрес посетителя заменяется на HMAC от него в `X-Client-Id` и дальше не передаётся; пуста — такой заголовок не отправляется. Создать: `openssl rand -base64 32`, см. «Замечания»|
|
||||
|-e XFF_HMAC_KEY|Необязательная, base64. IP посетителя заменяется на HMAC от него в `X-Client-Id` и дальше не передаётся. Не задавай — точка входа сгенерирует ключ на каждый запуск контейнера; задавай, только если псевдонимы должны пережить перезапуск или совпадать на двух прокси, см. «Замечания»|
|
||||
|
||||
Маршрутизация, тайм-ауты и настройки TLS параметрами не являются: они находятся
|
||||
в `docker/haproxy.cfg` и поставляются внутри образа.
|
||||
@@ -167,10 +168,10 @@ docker push registry.bitdeals.org/haproxy
|
||||
забирает `docker logs`. Успешный запрос не пишется ничем; 503, бэкенд без
|
||||
сервера, отклонённое рукопожатие — пишутся. Убирайте `dontlog-normal`
|
||||
осознанно, если нужен полный журнал обращений: он же удерживает объём.
|
||||
- **Логгеров два, и забытый второй сдаёт адреса.** `option httplog` здесь не
|
||||
используется: его формат по умолчанию начинается с `%ci:%cp`, то есть адреса
|
||||
посетителей попали бы в `docker logs` и свели бы на нет псевдоним, который
|
||||
выставляют фронтенды. Собственный `log-format` ставит в это первое поле
|
||||
- **Логгеров два, и забытый второй сдаёт IP-адреса.** `option httplog` здесь не
|
||||
используется: его формат по умолчанию начинается с `%ci:%cp`, то есть
|
||||
IP-адреса посетителей попали бы в `docker logs` и свели бы на нет псевдоним,
|
||||
который выставляют фронтенды. Собственный `log-format` ставит в это первое поле
|
||||
псевдоним. Ловушка — `error-log-format`: он покрывает то, что происходит *до*
|
||||
появления транзакции (отклонённое TLS-рукопожатие, а нижняя граница теперь
|
||||
TLS 1.2), и его умолчание начинается так же. Здесь заданы оба. Поэтому
|
||||
@@ -180,32 +181,41 @@ docker push registry.bitdeals.org/haproxy
|
||||
- **В журнал идут только метод и путь, никогда не строка запроса.** `%{+Q}r`
|
||||
унёс бы и её, и токен, однажды оказавшийся в URL, был бы записан на всё время
|
||||
хранения журнала.
|
||||
- **Адрес посетителя дальше не идёт.** `option forwardfor` не задан:
|
||||
- **IP-адрес посетителя дальше не идёт.** `option forwardfor` не задан:
|
||||
`X-Forwarded-For` в обоих фронтендах удаляется и никогда не заполняется,
|
||||
поэтому ничто за этим прокси не может записать в журнал адрес, которого ему не
|
||||
давали. Вместо адреса передаётся псевдоним в `X-Client-Id` — HMAC-SHA256 от
|
||||
адреса на ключе `XFF_HMAC_KEY`. Он взаимно однозначен с адресом, то есть как
|
||||
поэтому ничто за этим прокси не может записать в журнал IP, которого ему не
|
||||
давали. Вместо IP-адреса передаётся псевдоним в `X-Client-Id` — HMAC-SHA256 от
|
||||
IP-адреса на ключе `XFF_HMAC_KEY`. Он взаимно однозначен с IP, то есть как
|
||||
ключ ограничения частоты ничем не хуже, и без ключа необратим. Именно HMAC, а
|
||||
не просто хеш: IPv4 — это 2³² значений, и хеш адреса без ключа перебирается за
|
||||
секунды.
|
||||
- **`XFF_HMAC_KEY` необязателен, и пустой ключ выключает функцию, а не
|
||||
ослабляет её.** Не задан — `X-Client-Id` не отправляется вовсе, и ограничение
|
||||
частоты ниже по цепочке вырождается в одну корзину на всех посетителей;
|
||||
задан — у каждого посетителя своя. Чего не происходит никогда, так это
|
||||
псевдонима, выведенного на пустом ключе. Значение, не являющееся корректным
|
||||
base64, останавливает контейнер при разборе конфигурации — тихо испортиться
|
||||
оно не может. Ротация ключа сбрасывает корзины (пользователь этого не видит) и
|
||||
меняет все псевдонимы, поэтому активность до и после ротации связать нельзя.
|
||||
не просто хеш: IPv4 — это 2³² значений, и хеш IP-адреса без ключа перебирается
|
||||
за секунды.
|
||||
- **`XFF_HMAC_KEY` не оставляется пустым, а генерируется.** Пустой ключ
|
||||
выключает функцию: `X-Client-Id` не отправляется вовсе, и ограничение частоты
|
||||
ниже по цепочке вырождается в одну корзину на всех посетителей — состояние,
|
||||
которого никто не выбирает нарочно и в которое проще всего попасть, забыв
|
||||
строку в `.env`. Поэтому точка входа подставляет `openssl rand -base64 32`,
|
||||
если ключа нет. Его значение никто не выбирает, снаружи контейнера оно никому
|
||||
не нужно, и двум развёртываниям не требуется одинаковое.
|
||||
Новый ключ на каждый запуск контейнера стоит сброса корзин ниже по цепочке —
|
||||
на минутном окне это незаметно — и делает несвязываемыми псевдонимы до и
|
||||
после, а это свойство, ради которого ключ и существует, а не потеря. Задавать
|
||||
значение явно стоит лишь тогда, когда псевдонимы должны пережить перезапуск
|
||||
или совпадать на двух прокси.
|
||||
Конфигурация по-прежнему умеет работать с пустым ключом: `haproxy.cfg` можно
|
||||
запустить и вне этого образа. Значение, не являющееся корректным base64,
|
||||
по-прежнему останавливает контейнер при разборе — тихо испортиться оно не
|
||||
может, и поэтому же генерируется base64, а не hex: hex здесь был бы принят и
|
||||
молча раскодирован как base64 во что-то другое.
|
||||
- **Оба удаления безусловны.** `X-Forwarded-For` и `X-Client-Id` удаляются
|
||||
независимо от того, задан ключ или нет, — чтобы присланный клиентом заголовок
|
||||
ниже по цепочке нельзя было принять за выставленный этим прокси. То же с
|
||||
`X-Forwarded-Proto`: каждый фронтенд выставляет собственную схему, а не
|
||||
передаёт дальше клиентское утверждение.
|
||||
- **Потребителя всё равно нужно научить этим пользоваться.** Ограничение
|
||||
частоты, построенное на адресе сокета — `$binary_remote_addr` у nginx,
|
||||
`request.client.host` у ДС, — видит этот прокси на каждом запросе и
|
||||
частоты, построенное на IP-адресе сокета — `$binary_remote_addr` у nginx,
|
||||
`request.client.host` у ДС, — видит IP этого прокси на каждом запросе и
|
||||
вырождается в общую корзину. Ключом должен быть `X-Client-Id`, и доверять ему
|
||||
следует только с адреса этого прокси; готовый пример —
|
||||
следует только с IP этого прокси; готовый пример —
|
||||
`frontend/docker/rate-limit.conf` в репозитории bitdeals-ng.
|
||||
- **TLS задан в `global`, а не отдан на усмотрение OpenSSL.** Нижняя граница —
|
||||
TLS 1.2, список шифров только ECDHE и в вариантах ECDSA и RSA (certbot
|
||||
|
||||
Reference in New Issue
Block a user