Rate Limits & Lockout
Docs version v1 (main)Nauthera is pre-1.0, in a design-partner programUpdated
Two separate mechanisms protect the server:
- Rate limiting caps how many requests one client IP may send to the protocol endpoints, the HTTP APIs and the hosted UI's pages, with the few exceptions listed below. The gRPC control plane is not rate-limited.
- Brute-force protection caps sign-in attempts per IP and failed guesses per identifier, soft-locks an account after repeated failures, and asks for a captcha once sign-ins start failing.
Both identify the caller by client IP, read as security.client_ip.mode says: the
connecting peer, or a forwarded address from a peer in security.trusted_proxies. Behind
an ingress or load balancer, list its addresses there, or every request appears to come
from the proxy and one noisy client throttles everybody. The dashboard's overview tells
platform administrators when traffic looks like it arrives through a proxy the
configuration does not name. See
Configuration.
Rate limiting
| Key | Default | Meaning |
|---|---|---|
rate_limit.enabled | true | |
rate_limit.rps | 50 | Requests per second per IP |
rate_limit.burst | 100 | Burst size of the memory bucket, which the redis backend also falls back to |
rate_limit.backend | memory | memory or redis |
rate_limit.window | 1m | Window length (redis backend only) |
The two backends behave differently:
| Backend | Algorithm | Scope |
|---|---|---|
memory | Token bucket: rps per second, bursts up to burst | Per replica. The effective limit across the fleet is rps × replicas. |
redis | Fixed window: rps × window requests per window (3,000 a minute by default). | Shared by every replica. Requires cache.enabled. While Redis cannot answer, each replica falls back to its own memory bucket (rps, burst). |
The production Helm profile uses redis.
One budget per IP. Every rate-limited endpoint draws from the same per-IP budget:
/authorize, /token, the sign-in API, the admin API and the hosted UI's pages all
count together. The main organization's discovery documents and JWKS are not
rate-limited. An organization's own discovery documents and JWKS under /orgs/{slug} are
rate-limited, and so are its discovery documents at the RFC 8414 path-inserted locations
on the server root.
When the limit is hit, the server answers 429 with a problem document and these
headers. OAuth endpoints answer this way too, not with an OAuth error object:
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
X-RateLimit-Limit: 3000
X-RateLimit-Remaining: 0
Retry-After: 12
{"type":"https://nauthera.io/problems/rate_limited","title":"Too Many Requests","status":429,
"code":"rate_limited","detail":"rate limit exceeded","instance":"/token","request_id":"…"}Retry-After is in whole seconds, at least 1. The hosted UI's pages and an
organization's own discovery documents and JWKS send the same status and headers, but
their problem document has type: about:blank and no code. A browser navigation that
hits the limit gets an HTML error page instead of a problem document, with the same
status and rate-limit headers.
When Redis fails. With the redis backend, a request the shared limiter cannot
answer is limited by a per-replica token bucket instead, with the same rps and
burst, so the limit loosens to at most rps × replicas and nothing is refused for the
outage alone. The replica asks Redis again at most once a second and goes back to the
shared window as soon as it answers. Each switch is logged, and
nauthera_ reads 1 while a replica limits on its own.
Brute-force protection
| Key | Default | Meaning |
|---|---|---|
auth. / ip_window | 50 / 15m | Sign-in attempts allowed from one IP per window, successful or not, across all organizations. Self-service registration, password-reset, email-verification and invitation requests count too. |
auth. / identifier_window | 10 / 15m | Failed sign-ins allowed against one email address in one organization per window |
auth. | 5 | Failures that lock the account |
auth. | 15m | How long the first lock lasts |
auth. | true | Double the lock for each further failure |
auth. | 1h | Longest lock |
auth. | false | Whether a locked account is told apart from a throttled one |
auth. | 4 | Password-grade derivations at once per replica, shared by sign-in, setting a password, and redeeming or issuing recovery codes. Each argon2id derivation uses about 64 MiB. |
auth. | 2s | How long a request waits for a free slot before it is refused |
auth. | 24h | How long a run of failures is remembered after its last failure and the lock it set |
The per-IP and per-identifier throttles are held in the cache, so with Redis they are shared by every replica. The account lock is stored on the user record in the database.
Account lockout
After max_failed_attempts failures, the account is locked for lockout_duration. With
backoff on, each failure after that doubles the next lock, up to lockout_max. While the
account is locked, every attempt is refused, including one with the right password, and
refused attempts neither count as failures nor extend the lock. So a third party cannot
keep an account locked longer by guessing at it. An email address no account holds keeps
the same count and lock, so it locks on the same attempt an account would and the answers
do not reveal whether the account exists.
A successful sign-in clears the failure count. Setting the user's password, through the
admin API (PUT /) or from an emailed reset
link, clears the count and the lock at once. To unlock without touching the password,
POST / clears the failure count, the lock and the
sign-in throttle on the user's address. It needs the users.lifecycle permission, a
recent sign-in and a reason, which the audit record keeps; the account's status is left as
it is.
What the sign-in screen sees
| Situation | Response from POST /v1/auth/sign-in |
|---|---|
| The request names no client registered in the organization and does not continue the device flow | 404, code: not_found, before the password is checked |
| A captcha is due and the request carries no valid solution | 400, code: captcha_failed |
| Wrong email or password | 401, code: |
A throttle tripped, every verification slot busy, or the account is locked while reveal_lockout is off | 429, code: too_many_requests |
The account is locked and reveal_lockout is on | 429, code: account_locked; the hosted UI shows /account-locked |
| The right password for an account an administrator suspended or blocked | 401, code:; with reveal_lockout on, 403, code: |
By default a locked account looks the same as a throttled one, so the response does not confirm that the account exists. Not every response is yet identical for registered and unknown identifiers.
Captcha
The hosted UI asks for a captcha where guessing is cheap for a script:
- self-service registration and every request for an emailed link (password reset, resending a verification link) always ask;
- sign-in asks once the address typed, or the caller's IP, has failed
auth.(3) times recently. Until then a person signs in without one.captcha. sign_ in_ after 0never asks at sign-in.
| Key | Default | Meaning |
|---|---|---|
auth.captcha.provider | altcha | altcha, the built-in proof-of-work that needs no third party; a hosted provider, turnstile, hcaptcha or recaptcha (v2); or none |
auth. | 1000 | Difficulty: the browser makes half this many attempts on average. At most 100,000. |
auth. | 5m | How long a challenge can be solved and sent back, 1m to 1h |
auth.captcha.site_key, auth.captcha.secret | empty | A hosted provider's keys, both required by one. The secret has a _FILE form. |
auth. | 3 | Recent failures before sign-in asks |
The UI fetches a challenge from GET /v1/auth/captcha and sends the solution with the
request it guards. A missing, wrong, expired or already used solution answers
400 captcha_failed; each solution is single use. If a hosted provider cannot be asked, the request
answers 503 unavailable. A sign-in the captcha refuses still counts toward the per-IP
throttle. See Configuration.
Second factors
Second factors have their own lockout, independent of the account lock. After
mfa.max_failures (5) wrong codes, TOTP verification is locked for mfa.lock_duration
(15m); wrong recovery codes count toward that limit but never lock TOTP verification.
After as many wrong recovery codes, recovery-code redemption is locked for the same time.
Each lock leaves the other method open.
See Multi-Factor Authentication.
The device flow's code entry allows five wrong user codes per signed-in user, after
which entry is refused with too_many_attempts.
Monitoring
nauthera_counts password sign-in attempts byauth_ logins_ total result:success,invalid,throttled,lockedandunavailable.successmeans the password was right; a second factor may still follow. An attempt against a locked account counts aslockedonly whenreveal_lockoutis on, and asthrottledotherwise.nauthera_israte_ limit_ degraded 1while a replica limits on its own because the shared Redis limiter cannot answer.nauthera_counts soft locks applied, to an account or to an email address no account holds, each re-lock included.auth_ account_ lockouts_ total auth.audit events record every sign-in answeredrate_ limit. triggered too_many_requests: a tripped throttle, every verification slot busy, or a locked account whilereveal_lockoutis off. Registration and email-link requests refused by the per-IP throttle record it too.auth.account.lockedis recorded once for each lock applied, by the failure that set it, withattemptsandlocked_until;limit_scopesays whether an account or an identifier no account holds was locked.