Files
sessiongurad/docs/EDGE-SECURITY.md
jbergner 7c69432097
Some checks failed
release-tag / release-image (push) Successful in 2m3s
release-main / release-images (push) Failing after 3m9s
RC-2 0.5.2
2026-08-24 05:46:42 +02:00

182 lines
5.9 KiB
Markdown

# Public Edge Security (Caddy + SessionGuard EdgeGuard)
SessionGuard EdgeGuard is a small Go service for the public reverse-proxy host.
It is **not** another login and it is not a WAF replacement. It runs locally on
`127.0.0.1:9081` and is called by Caddy using `forward_auth` before traffic is
sent to SessionGuard or a Guacamole worker.
## Threat model
EdgeGuard is intended to reduce the cost of common Internet abuse:
- repeated scanners and exploit probes;
- excessive requests from one source;
- request floods intended to exhaust SessionGuard/Guacamole rather than the
physical Internet link;
- malformed/oversized URIs and unexpected HTTP methods;
- accidental exposure of backend-only SessionGuard endpoints.
It cannot stop a volumetric attack that saturates the VPS uplink. Provider-side
DDoS filtering/firewalling remains necessary for that class of attack.
## Request flow
```text
Browser
-> Caddy TLS
-> EdgeGuard /check (localhost only)
-> 204: continue
-> 403/404/405/421/429: stop at edge
-> SessionGuard Access Auth
-> Guacamole worker
```
Caddy's `forward_auth` sends a lightweight GET subrequest. EdgeGuard therefore
checks the initial WebSocket handshake, but it is not in the byte path of the
established Guacamole WebSocket/RDP stream.
## Controls
### Static blacklist
`blacklist.txt` accepts one IPv4/IPv6 address or CIDR per line. Comments start
with `#`. Changes are automatically picked up.
```text
203.0.113.44
198.51.100.0/24
2001:db8:1234::/48
```
### Global overload limit
The global token bucket is intentionally high and protects the application
backends when a distributed HTTP flood reaches the VPS. It does not prevent
network saturation because TLS and the incoming packets already reached Caddy.
Default production example:
```json
"global_limit": {
"rate_per_second": 2500,
"burst": 5000
}
```
### Per-IP limit
The default is deliberately NAT-friendly:
```json
"per_ip_limit": {
"rate_per_second": 200,
"burst": 500
}
```
For environments where many staff share one public NAT IP, do not aggressively
lower this value. If a known source really needs exemption, put its address in
`rate-exempt.txt`. Exempt sources still pass blacklist, scanner, method, host
and global-overload checks.
### Endpoint-specific limits
The example uses tighter limits for OIDC login/callback paths. These endpoints
do not contain the PocketID password check itself; the limits are intended to
protect state/session allocation and redirect processing from floods.
### Bounded per-IP memory
`max_tracked_ips` bounds the in-memory table used for per-IP token buckets and
offense state. The production example allows 100,000 active source addresses;
when the table is full, previously unseen sources receive HTTP 429 instead of
causing unbounded memory growth. Stale entries are cleaned up automatically.
### Scanner detection and temporary bans
Known irrelevant exploit/scanner paths are denied before they reach the
backends. The defaults include `/.env`, `/.git`, WordPress, phpMyAdmin, CGI,
Spring Actuator and several common automated exploit probes.
The example auto-ban weights are:
- scanner path: 5 points;
- blocked method: 3 points;
- malformed URI: 3 points;
- ordinary rate-limit violation: 0 points.
At 10 points within 120 seconds, the IP is banned for 900 seconds. Bans are
persisted in `/var/lib/sessionguard-edgeguard/state.json` with a short write
debounce, so a container restart does not normally remove them while scanner
floods cannot force one synchronous disk write per request.
Rate-limit violations deliberately have weight 0 by default to avoid banning a
whole corporate NAT during a legitimate burst.
### Blocked HTTP methods
`CONNECT`, `TRACE` and `TRACK` are rejected. SessionGuard/Guacamole continue to
use their normal GET/POST/PUT/PATCH/DELETE/OPTIONS behavior.
### Host allowlist
Only configured public host names are accepted by EdgeGuard. Caddy additionally
uses `strict_sni_host on`, requiring the TLS SNI host and HTTP Host header to
match.
## Caddy hardening
The supplied public Caddyfile also configures:
```caddyfile
servers {
protocols h1 h2
strict_sni_host on
max_header_size 64KB
timeouts {
read_header 10s
}
}
```
The short header timeout and smaller header ceiling reduce slow-header/resource
exhaustion risk. HTTP/3 is intentionally disabled in the example to reduce the
public protocol surface; Guacamole works with HTTP/1.1/WebSocket and HTTP/2.
Access logs are written to size-limited rotating files. Sampling keeps all normal
traffic but reduces log amplification once a single logger exceeds 200 entries
per second.
The public SessionGuard host denies `/metrics` and `/api/v1/broker/*` at Caddy.
The Caddy admin API is disabled (`admin off`) on this dedicated public edge;
configuration changes are applied by restarting the Caddy container.
Guacamole workers call broker APIs directly over NetBird instead.
## Metrics
EdgeGuard exposes Prometheus text metrics only on localhost:
```text
http://127.0.0.1:9081/metrics
```
Counters include total checks, allows, static-blacklist denies, temporary-ban
denies, rate-limit denies, scanner denies and auto-bans.
## Configuration reload
`edgeguard.json`, `blacklist.txt` and `rate-exempt.txt` are re-read on the
configured interval (15 seconds in the example). An invalid replacement config
is logged and the previous working configuration remains active.
## Operational recommendations
1. Keep provider/network DDoS protection enabled.
2. Expose only TCP 80/443 publicly; keep EdgeGuard on localhost.
3. Keep NetBird peer/backend ports private.
4. Pin Caddy to a currently patched release rather than an old major-only image
during controlled production rollouts.
5. Monitor HTTP 429 and EdgeGuard auto-ban counters before tightening limits.
6. Do not put broad office/country CIDRs on the static blacklist without first
checking whether legitimate remote users may originate there.