Усиление установки
Базовый чек-лист продакшена — в Установке, таблица переменных HSTS и их взаимные ограничения — в Конфигурации (секция Security). Эта страница — свод: что из защиты периметра закрывает обратный прокси перед приложением, что закрывает само приложение, и как проверить обе половины после деплоя.
Граница ответственности
Обратный прокси закрывает то, до чего приложению снизу не дотянуться: терминирует TLS,
подменяет собой дефолтные страницы ошибок веб-сервера/панели хостинга (в которых обычно
светится версия nginx или Traefik), ограничивает набор HTTP-методов и решает, какие пути
вообще видны снаружи. Приложение закрывает то, что живёт внутри самого ответа: security-заголовки
на каждой странице, Strict-Transport-Security при HTTPS-GOTCHA_BASE_URL, отсутствие
обслуживания TRACE и страницы ошибок без номера версии и стека вызовов. Ни одна половина не
подменяет другую — прокси без этих настроек оставляет дыры, которые приложение закрыть не
может в принципе (версия nginx уходит до того, как запрос вообще доедет до Go-процесса), а
голое приложение без прокси перед ним — это HTTP без TLS и открытые наружу служебные пути.
Обратный прокси
Три настройки, которые стоит выставить в любом прокси перед Gotcha: server_tokens off,
чтобы не светить версию самого прокси в заголовках и дефолтных страницах ошибок; собственный
error_page вместо страницы nginx/Traefik/панели хостинга по умолчанию; ограничение методов
до тех, что реально нужны, — GET, POST, HEAD.
server_tokens off;
location / {
limit_except GET POST HEAD { deny all; }
proxy_intercept_errors on;
error_page 404 500 502 503 504 /error.html;
proxy_pass http://127.0.0.1:59080;
}
proxy_intercept_errors on обязателен вместе с error_page — без него nginx проксирует
готовую страницу ошибки от бэкенда как есть, а не подменяет её своей.
Что не выставлять наружу
/metrics, /version, /healthz, /readyz — служебные ручки, нужные изнутри (оркестратору,
системе мониторинга, вам самим по SSH-туннелю), а не публике. Ни одна не требует
аутентификации. /version, /healthz и /readyz анонимно отдают точную версию сборки
(поля version/commit в теле ответа) — а точная версия сужает атакующему поиск
уязвимостей до тех, что закрыты именно в этом релизе, вместо перебора вслепую. Подробнее об
этих ручках и о том, что именно они отдают, — Мониторинг самого gotcha.
location ~ ^/(metrics|version|healthz|readyz)$ {
allow 10.0.0.0/8;
allow 127.0.0.1;
deny all;
proxy_pass http://127.0.0.1:59080;
}
@internal path /metrics /version /healthz /readyz
handle @internal {
@allowed remote_ip 10.0.0.0/8 127.0.0.1
handle @allowed { reverse_proxy localhost:59080 }
respond 403
}
Замените 10.0.0.0/8 на диапазон, из которого реально приходят ваши пробы (оркестратор,
Prometheus, ваша сеть) — открытый по умолчанию диапазон бессмысленен как ограничение.
То же относится к базам. Штатный docker-compose.yml не публикует порты PostgreSQL и
ClickHouse на хост — до них добираются только контейнеры той же docker-сети, — но пароль
у обеих по умолчанию gotcha / gotcha, и он одинаков у каждой установки в мире. Смените
его через GOTCHA_COMPOSE_PG_PASSWORD / GOTCHA_COMPOSE_CH_PASSWORD до первого старта
(после инициализации тома переменная сама по себе уже ничего не меняет), а на живой
установке — сначала ALTER USER в самой базе, потом переменная; команды — в
Конфигурации. И не
добавляйте базам ports: «для удобства»: с дефолтным паролем это открытая база на публичном
адресе.
TLS и HSTS
TLS — не ниже версии 1.2, с редиректом с голого HTTP на HTTPS. HSTS настраивается ровно в
ОДНОМ месте — либо на прокси, либо в приложении, никогда в обоих сразу: два источника
заголовка на одном ответе не складываются, а просто маскируют друг друга. Если HSTS уже
шлёт прокси, приложению задайте GOTCHA_HSTS_ENABLED=false.
Приложение собирает заголовок из четырёх переменных:
| Переменная | Дефолт | Смысл |
|---|---|---|
GOTCHA_HSTS_ENABLED | true | Отправлять ли Strict-Transport-Security вообще (только на https-ответах). |
GOTCHA_HSTS_MAX_AGE_SECONDS | 31536000 | На сколько секунд браузеру запомнить требование HTTPS (дефолт — год); 0 — не «выключено», а осознанный аварийный откат, см. ниже. |
GOTCHA_HSTS_INCLUDE_SUBDOMAINS | false | Распространять требование HTTPS на все поддомены хоста из GOTCHA_BASE_URL. |
GOTCHA_HSTS_PRELOAD | false | Помечать инстанс кандидатом на списки предзагрузки браузеров. |
Точные правила отказа старта, поведение при MAX_AGE_SECONDS=0 и почему выключение HSTS не
снимает уже выданный браузером пин — в Конфигурации.
Включайте includeSubDomains, только если контролируете (или уже проверили HTTPS на) весь
родительский домен целиком: gotcha.example.com с этим флагом требует HTTPS не только от
себя, а от каждого сервиса на example.com, включая те, что вы не администрируете и которые
могут быть не готовы к HTTPS.
Preload — билет в один конец: попав в список предзагрузки, домен зашивается в релизы
браузеров на месяцы вперёд, и снять его оттуда — вопрос месяцев, а не минут. Выход при
аварии — строго в этом порядке, иначе приложение откажется стартовать (валидация конфига
требует max-age не меньше года, пока PRELOAD=true, — см.
Конфигурацию):
GOTCHA_HSTS_PRELOAD=false— снять требование годовогоmax-age, которое иначе не даст уйти в шаг 2.GOTCHA_HSTS_MAX_AGE_SECONDS=0, оставивGOTCHA_HSTS_ENABLED=true— заголовок с нулевым max-age реально уходит клиентам и снимает пин.- Дождаться, пока пин истечёт у уже посетивших инстанс клиентов.
- Только теперь
GOTCHA_HSTS_ENABLED=false, если заголовок больше не нужен вовсе.
Выключенный HSTS пин сам по себе не снимает — он лишь перестаёт продлеваться, поэтому шаг 4 без шагов 1–3 не отменяет аварию, а замораживает её на срок ранее выданного max-age.
security.txt
Приложение не отдаёт /.well-known/security.txt само — это осознанное решение, не
недоработка. Контакт для сообщений об уязвимостях — свойство ДОМЕНА, а не конкретного
приложения на нём: значительная часть установок Gotcha живёт на поддомене чужого домена
(общий хостинг, корпоративный портал), и владеет контактом безопасности владелец домена, а
не разработчик Gotcha. Положите файл на прокси:
location = /.well-known/security.txt {
default_type text/plain;
return 200 "Contact: mailto:security@example.com\nExpires: 2027-01-01T00:00:00.000Z\n";
}
Самопроверка
После деплоя проверьте обе половины периметра — прокси и приложение — набором curl:
# security-заголовки приложения на странице входа
curl -sI https://gotcha.example/login | grep -Ei 'content-security-policy|x-frame-options|strict-transport'
# HSTS есть на https-инстансе...
curl -sI https://gotcha.example/login | grep -i strict-transport
# ...и его нет на голом http-деплое, независимо от конфига
# (пусто, если только ваш прокси сам не вешает HSTS на редиректе http->https —
# рекомендуемая топология выше это допускает; тогда заголовок на 301 ожидаем)
curl -sI http://gotcha.example/login | grep -i strict-transport # ожидается: пусто
# служебные пути закрыты прокси
curl -s -o /dev/null -w '%{http_code}\n' https://gotcha.example/metrics # ожидается 403
curl -s -o /dev/null -w '%{http_code}\n' https://gotcha.example/version # ожидается 403
# TRACE не обслуживается
curl -s -o /dev/null -w '%{http_code}\n' -X TRACE https://gotcha.example/login # ожидается 404
Про TRACE отдельно: ожидается именно 404, а не 405. Веб-слой перехватывает любой метод на этом пути общим catch-all’ом и всегда отвечает стилизованной страницей 404 — отсутствие 405 здесь не признак того, что метод TRACE чем-то обслуживается, это признак того, что маршрут вообще не различает методы на уровне mux.