# 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.
