182 lines
5.9 KiB
Markdown
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.
|