fix(tradein/auth): лимит попыток входа 5 → 20 на адрес — офис за одним NAT запирал сам себя
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI / changes (pull_request) Successful in 14s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m55s

27.08, инцидент с пилотом «Практика». После починки доступа к базе auth
сотрудники начали получать «Слишком много попыток» — при том, что многие
ещё вообще не пробовали войти.

Причина в форме лимита, а не в его величине. Пилот — ОФИС: все выходят
из-под одного NAT, и пять попыток за пять минут делились на всю компанию
сразу. Одного человека, перепутавшего пароль, хватало, чтобы запереть
остальных.

Защита от перебора не ослабевает. Настоящий предохранитель — счёт по
ЛОГИНУ (login_username_fail_threshold, 20 за час), он не тронут. Лимит по
адресу существует против всплеска с одной машины, а не против офиса, и
двадцать попыток за пять минут эту роль выполняют.
This commit is contained in:
bot-backend 2026-08-27 13:26:44 +03:00
parent 51c779c092
commit b9654d01c0

View file

@ -105,7 +105,16 @@ class Settings(BaseSettings):
auth_mode: Literal["dual", "db_only"] = Field(default="dual", validation_alias="AUTH_MODE")
# Rate-limit на /login: не более login_rate_limit попыток за
# login_rate_limit_window_s секунд на ключ (обычно IP или username).
login_rate_limit: int = Field(default=5, validation_alias="LOGIN_RATE_LIMIT")
#
# 27.08: было 5. Пилот «Практика» — ОФИС, все сотрудники выходят из-под
# одного NAT, то есть пять попыток делились на всю компанию сразу. Хватило
# одного человека, перепутавшего пароль, чтобы запереть остальных: «Слишком
# много попыток» получали те, кто вообще ещё не пробовал войти.
#
# Защита от перебора при этом не ослабевает: настоящий предохранитель —
# login_username_fail_threshold (счёт по ЛОГИНУ, 20 за час), и он не тронут.
# Лимит по адресу нужен против всплеска с одной машины, а не против офиса.
login_rate_limit: int = Field(default=20, validation_alias="LOGIN_RATE_LIMIT")
login_rate_limit_window_s: int = Field(
default=300, validation_alias="LOGIN_RATE_LIMIT_WINDOW_S"
)