Bans and rate limits
An example
[sftp.ban]
max_failures = 5
window_secs = 300
ban_duration_secs = 600
whitelist_ips = ["10.0.0.0/8"]
[admin]
listen = "0.0.0.0:8080"
[admin.ban]
max_failures = 10
trusted_proxies = ["10.0.0.5"] # the reverse proxy in front of the API
[api]
enabled = true
[api.ban]
max_failures = 5
trusted_proxies = ["10.0.0.5"] # the same one: a single listener
Without the section, the door bans nothing.
One list per door
| List | Section | Doors |
|---|---|---|
sftp | [sftp.ban] | SFTP |
api | [api.ban] | File API, download tickets, file explorer |
admin | [admin.ban] | Admin API and admin console |
The three lists are independent: an address banned on the file API still opens the admin console, and the other way around.
The keys of a ban
The same under [sftp.ban], [api.ban] and [admin.ban], read at startup.
All the keys: Reference [sftp.ban],
[api.ban],
[admin.ban].
What counts as a failure
A failure is a credential presented and judged wrong, whatever the account tried.
| Door | Counts |
|---|---|
| SFTP | wrong password or unknown name; JWT token judged wrong; key signature refused; a connection that ends without authenticating after declined keys (one failure per connection, not per key) |
| File API | wrong or malformed Basic; empty Bearer or token judged wrong |
| Admin console and admin API | wrong static token; empty Bearer, JWT or session token judged wrong; POST /admin/login: wrong password, unknown name or account without an admin role |
These do not count: a disabled method, an expired or not yet valid token, a refused download ticket, whatever it is, a refusal after an accepted credential (no role, session limit), a refusal by the rate limiter, the refusal of an address already banned. A successful connection does not reset the counter.
With [api] cors_origins = ["*"], any web page can make a visitor’s
browser send wrong credentials, which count toward the ban of the
visitor’s address: list the origins of your applications.
The life of a ban
| Moment | Effect |
|---|---|
max_failures failures within window_secs | the address is banned for ban_duration_secs; an ip_banned audit line |
with [cluster] | the failures the other instances hold in their window add up toward the threshold, read every second (≈ 1 s of delay); an unreachable instance stops counting after 5 s (2 × peer_timeout_ms + 1 s if longer) |
| on SFTP, at the verdict | the open SFTP sessions of the address are cut, transfers included |
| one more failure during the ban | the end moves back to ban_duration_secs after it |
| the end | the ban ends, with no line |
DELETE /admin/bans/{protocol}/{ip} | the ban is lifted |
An IPv4-mapped address (::ffff:10.1.2.3) is the same as 10.1.2.3, for
the ban, the whitelist and the proxies.
Lifting a ban
GET /admin/bans (Bans tab of the admin console) lists them; lifting one
requires the unban permission, {protocol} is sftp, api or admin:
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
http://localhost:8080/admin/bans/sftp/203.0.113.7
A lift is a dated decision: it cancels the verdicts decided before it, never a later verdict. It does not reset the counter.
Sharing bans between instances
| Backend | Keys | Sharing |
|---|---|---|
| memory | none | each instance has its own bans, lost on restart; with [cluster], a lift is relayed to every instance, as with a file |
| file | persist_file | read at startup, published every 30 s, read again on a change and every reread_interval_secs; written atomically; on a shared volume (NFS, EFS) reading again is enough |
| ConfigMap | backend = "configmap", ban_configmap_name | watched through the Kubernetes API (feature k8s, a Role): Kubernetes |
The three lists can share a file or a ConfigMap. The merge keeps, per
address, the longest ban and the most recent verdict; a ban learned from a
peer cuts the SFTP sessions of the address, with no ip_banned line (the
one from the instance that decided it is enough). On shutdown, each list
is published, 5 s at most each.
The address of an HTTP client: trusted_proxies
The HTTP doors take the TCP peer as the address. When this peer is in the
trusted_proxies of their list ([api.ban] for the file API,
[admin.ban] for the admin API), they take the rightmost address of
X-Forwarded-For that is not a trusted proxy, failing that
X-Real-IP. The ban, the limiter, the share of the hashing queue and the
audit use this address. The SFTP door reads only the TCP peer.
Without [admin] control_listen, the file API and the admin API share a
listener: [api.ban] and [admin.ban] then carry the same
trusted_proxies.
Rate limits
A token bucket per address: burst connections or requests in a row,
then the rate per minute. Without the section, no limit.
[sftp.rate_limit]
connections_per_minute = 30
burst = 10
[api.rate_limit]
requests_per_minute = 600
All the keys: Reference [sftp.rate_limit],
[api.rate_limit].
The admin API has no rate limit. A rate refusal does not count toward the ban.
What you will see
| When | Line |
|---|---|
| a ban decided | audit ip_banned; INFO IP banned after auth failures |
| sessions cut by a ban | INFO cut the sessions of a banned address; audit session_end reason=banned |
| a peer publishes bans | INFO merged persisted bans from file |