Login Rate Limiting & Brute-Force Protection
Athena guards the sign-in page with several independent layers of throttling so that automated
password guessing is slowed to a crawl and repeat offenders are cut off. This page explains each
layer — the connection-level request limiter on the login endpoint, the per-IP request limit with
auto-ban, the per-account lockout and hard lock, and the growing delay between attempts — the
order they fire in, the appsettings.json keys and defaults that tune them, how they
behave when Athena sits behind a reverse proxy, and how to watch for an attack in the audit trail.
Every protection below is active out of the box with sensible defaults. You only need to touch the settings on this page if you want to make them stricter (or, rarely, looser). The two account-lockout values are also editable from the console — see Security Settings. The remaining values live in the server configuration file described here.
The layers, in order#
A single sign-in request passes through the checks below from the outside in. The first one that trips stops the request, so an attacker never gets to spend attempts against the layers further down.
| # | Protection | Scoped to | Trigger (default) | Effect |
|---|---|---|---|---|
| 1 | Connection request limiter | The login endpoint, per source connection | 20 requests / 60 s | Excess requests are refused with 429 Too Many Requests and a
Retry-After: 60 header, before any credentials are checked. |
| 2 | Per-IP request limit | Each client IP address | 10 sign-in requests / minute | 429 Too Many Requests, and the IP is auto-banned for 30 minutes. |
| 3 | Progressive delay | Each account | Every failed sign-in | A growing pause before the next attempt is accepted — 1s, 2s, 4s, 8s, capped at 16s. |
| 4 | Temporary lockout | Each account | 5 failed sign-ins | The account is locked for 15 minutes, then clears itself automatically. |
| 5 | Hard lock | Each account | 30 failed sign-ins | The account stays locked until an operator unlocks it — it does not clear on its own. |
Layers 1 and 2 are request-rate limits: they care about how many requests arrive, regardless of whether the password was right. Layers 3–5 are account counters: they respond to how many of those attempts failed. Together they blunt both a fast flood from one source and a slow, patient guess against one account.
1. Connection request limiter#
The outermost guard is a fixed-window request limiter that applies only to the login endpoint
(POST api/auth/login). It counts requests per source connection and, once the limit
is exceeded within the window, immediately answers 429 Too Many Requests with a
Retry-After: 60 header — the request never reaches the credential check. Because it
runs at the very front of the request pipeline, it is the cheapest possible way to shed a flood
of login traffic.
Key (under Security:ConnectionRateLimit) | Environment variable | Default | What it does |
|---|---|---|---|
PermitLimit | Security__ConnectionRateLimit__PermitLimit | 20 | Login requests allowed from one connection per window. |
WindowSeconds | Security__ConnectionRateLimit__WindowSeconds | 60 | Length of the fixed window, in seconds. |
// appsettings.json — allow at most 20 login requests per minute per connection
"Security": {
"ConnectionRateLimit": {
"PermitLimit": 20,
"WindowSeconds": 60
}
}
This limiter is scoped to POST api/auth/login only; every other API and console
request passes through untouched. Changing PermitLimit or
WindowSeconds is read from the configuration file, so a change takes effect the
next time the server starts.
2. Per-IP request limit & auto-ban#
Behind the connection limiter, Athena also tracks sign-in requests per client IP address. When a
single IP sends more than the allowed number of sign-in requests in a minute, further requests
are refused with 429 Too Many Requests and that IP is automatically banned
for 30 minutes. The ban is recorded in the audit trail.
Key (under Security) | Environment variable | Default | What it does |
|---|---|---|---|
MaxRequestsPerMinutePerIp | Security__MaxRequestsPerMinutePerIp | 10 | Sign-in requests allowed from one IP per minute before the IP is rate-limited and auto-banned. |
An operator can also ban an IP address on demand, or lift a ban, using the localhost-only recovery endpoints described on the Emergency Recovery page. Bans are held in the server's memory, so restarting the server clears every active IP ban and every account lockout at once.
3. Progressive delay#
Each time a sign-in fails for an account, the next attempt for that account is held back by a short, growing delay before it is even evaluated: roughly 1 second after the first failure, then 2, 4, 8, and capped at 16 seconds. A legitimate user who mistypes a password once or twice barely notices, but an automated guesser is throttled to a handful of tries per minute.
4. Temporary account lockout#
After a run of consecutive failures, the account is locked for a fixed period and then unlocks itself. These are the two values you can also set from Settings → Security in the console; see Security Settings → Account lockout.
Key (under Security) | Console setting | Default | What it does |
|---|---|---|---|
MaxFailedLoginAttempts | Max Failed Attempts | 5 | Consecutive failed sign-ins before the account is temporarily locked. |
LockoutDurationMinutes | Lockout Duration (minutes) | 15 | How long the account stays locked before it unlocks automatically. |
Whether a password was simply wrong, the account is temporarily locked, or the account is hard locked, the sign-in page returns the same generic error. This is deliberate: it stops an attacker from learning whether a username exists or which accounts are worth targeting. The specific reason is recorded in the audit trail, where you can see it.
5. Hard lock#
If failures against one account keep piling up well past the temporary-lockout threshold, the account is hard locked. Unlike a temporary lockout, a hard lock does not clear on its own — it stays in place until an operator unlocks the account. This is the case the Emergency Recovery tools exist for.
Key (under Security) | Environment variable | Default | What it does |
|---|---|---|---|
HardLockThreshold | Security__HardLockThreshold | 30 | Failed sign-ins for one account before it is hard locked and requires a manual unlock. |
To clear a hard lock, an Admin can unlock the account from the console, or — if every admin is locked out — use the localhost-only break-glass unlock from the server host. Both paths, along with the manual IP ban, are documented on the Emergency Recovery page.
Behind a reverse proxy#
If you publish Athena through a reverse proxy or load balancer (for example HAProxy or nginx), the two request-rate limiters see client addresses differently, and it is worth understanding why:
- The connection request limiter (layer 1) runs at the very front of
the pipeline, before any
X-Forwarded-Forheader is honoured. It therefore always counts by the real transport peer — the proxy — and cannot be fooled by a spoofed forwarded header. Behind a single proxy this effectively becomes a limit on total login traffic arriving through that proxy, so sizePermitLimitfor your expected peak sign-in rate. - The per-IP limit and the account counters run after the forwarded
headers are applied, so they can attribute requests to the true client IP — but only once you
have told Athena which proxies to trust. Configure your proxy addresses with
Security:TrustedProxies/Security:TrustedProxyNetworksas described under Server TLS → Behind a reverse proxy or load balancer. Until a proxy is trusted, forwarded client addresses are ignored and the real client IP is not visible to the per-IP checks.
Watching for an attack#
Every sign-in outcome is written to the audit trail. A failed attempt
is recorded as a LoginFailure event at Warning severity, capturing the
username tried and the source IP address; a rate-limited or auto-banned request is recorded too.
A successful sign-in is a LoginSuccess event at Information severity. See
the Audit Event Types Reference for the full list.
A sudden burst of LoginFailure events — especially many usernames from one IP, or
one username from many IPs — is the signature of a brute-force attempt. You can pull them from
the PowerShell module:
# Connect as an Admin
Connect-Athena -Server "athena.contoso.com"
# Failed sign-ins in the last 24 hours, newest first
Get-AthenaAudit -EventType "LoginFailure" |
Sort-Object Timestamp -Descending |
Select-Object Timestamp, Username, IpAddress, Description
If you forward the audit trail to a SIEM, alert on a spike of LoginFailure events —
see Audit Forwarding to SIEM. When you spot an offending
address, ban it or clear an affected account from the
Emergency Recovery page.
Tuning notes#
- The two lockout values are live; the rest need a restart. Max Failed
Attempts and Lockout Duration are read each time a sign-in is evaluated, so console or API
changes apply immediately. The
ConnectionRateLimitkeys,MaxRequestsPerMinutePerIp, andHardLockThresholdare read fromappsettings.jsonand take effect the next time the server starts. - Keep the request limits above your real peak. During a mass password change
or a scripted client that signs in often, set
PermitLimitandMaxRequestsPerMinutePerIpcomfortably above the legitimate rate so real users are never caught. - A restart is the blunt reset. Lockouts and IP bans are held in memory, so a server restart releases all of them at once — useful as a last resort, heavy-handed for routine use. Prefer the targeted unlock and un-ban actions instead.
Related#
- Security Settings — set Max Failed Attempts and Lockout Duration from the console, plus session expiry and the password policy.
- Emergency Recovery — unlock an account, reset a password, and ban or un-ban an IP from the server host.
- Configuration — where the
Securitykeys live inappsettings.json. - Server TLS Certificate — trusting a reverse proxy so the per-IP checks see the real client address.
- Audit Event Types Reference and Audit Forwarding to SIEM — observing and alerting on sign-in activity.