feat: add per-IP rate limiting to login endpoint
All checks were successful
check / check (push) Successful in 59s

Add a token-bucket rate limiter (golang.org/x/time/rate) that limits
login attempts per client IP on POST /api/v1/login. Returns 429 Too
Many Requests with a Retry-After header when the limit is exceeded.

Configurable via LOGIN_RATE_LIMIT (requests/sec, default 1) and
LOGIN_RATE_BURST (burst size, default 5). Stale per-IP entries are
automatically cleaned up every 10 minutes.

Only the login endpoint is rate-limited per sneak's instruction —
session creation and registration use hashcash proof-of-work instead.
This commit is contained in:
user
2026-03-17 02:26:59 -07:00
committed by clawbot
parent db3d23c224
commit 0bcc711a92
9 changed files with 443 additions and 5 deletions

View File

@@ -2203,6 +2203,8 @@ directory is also loaded automatically via
| `NEOIRC_HASHCASH_BITS` | int | `20` | Required hashcash proof-of-work difficulty (leading zero bits in SHA-256) for session creation. Set to `0` to disable. |
| `NEOIRC_OPER_NAME` | string | `""` | Server operator (o-line) username. Both name and password must be set to enable OPER. |
| `NEOIRC_OPER_PASSWORD` | string | `""` | Server operator (o-line) password. Both name and password must be set to enable OPER. |
| `LOGIN_RATE_LIMIT` | float | `1` | Allowed login attempts per second per IP address. |
| `LOGIN_RATE_BURST` | int | `5` | Maximum burst of login attempts per IP before rate limiting kicks in. |
| `MAINTENANCE_MODE` | bool | `false` | Maintenance mode flag (reserved) |
### Example `.env` file
@@ -2607,6 +2609,49 @@ creating one session pays once and keeps their session.
- **Language-agnostic**: SHA-256 is available in every programming language.
The proof computation is trivially implementable in any client.
### Login Rate Limiting
The login endpoint (`POST /api/v1/login`) has per-IP rate limiting to prevent
brute-force password attacks. This uses a token-bucket algorithm
(`golang.org/x/time/rate`) with configurable rate and burst.
| Environment Variable | Default | Description |
|---------------------|---------|-------------|
| `LOGIN_RATE_LIMIT` | `1` | Allowed login attempts per second per IP |
| `LOGIN_RATE_BURST` | `5` | Maximum burst of login attempts per IP |
When the limit is exceeded, the server returns **429 Too Many Requests** with a
`Retry-After: 1` header. Stale per-IP entries are automatically cleaned up
every 10 minutes.
> **⚠️ Security: Reverse Proxy Required for Production Use**
>
> The rate limiter extracts the client IP by checking the `X-Forwarded-For`
> header first, then `X-Real-IP`, and finally falling back to the TCP
> `RemoteAddr`. Both `X-Forwarded-For` and `X-Real-IP` are **client-controlled
> request headers** — any client can set them to arbitrary values.
>
> Without a properly configured reverse proxy in front of this server:
>
> - An attacker can **bypass rate limiting entirely** by rotating
> `X-Forwarded-For` values on each request (each value is treated as a
> distinct IP).
> - An attacker can **deny service to a specific user** by spoofing that user's
> IP in the `X-Forwarded-For` header, exhausting their rate limit bucket.
>
> **Recommendation:** Always deploy behind a reverse proxy (e.g. nginx, Caddy,
> Traefik) that strips or overwrites incoming `X-Forwarded-For` and `X-Real-IP`
> headers with the actual client IP. If running without a reverse proxy, be
> aware that the rate limiting provides no meaningful protection against a
> targeted attack.
**Why rate limits here but not on session creation?** Session creation is
protected by hashcash proof-of-work (stateless, no IP tracking needed). Login
involves bcrypt password verification against a registered account — a
fundamentally different threat model where an attacker targets a specific
account. Per-IP rate limiting is appropriate here because the cost of a wrong
guess is borne by the server (bcrypt), not the client.
---
## Roadmap