RC-2 0.5.2
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
# Public VPS / edge
|
||||
NETBIRD_VERSION=latest
|
||||
CADDY_VERSION=2.11.4
|
||||
EDGEGUARD_VERSION=0.5.2
|
||||
NETBIRD_PEER_NAME=public-proxy
|
||||
NETBIRD_MANAGEMENT_URL=https://netbird.example.org
|
||||
NETBIRD_SETUP_KEY=REPLACE_ME
|
||||
NETBIRD_LOG_LEVEL=info
|
||||
|
||||
# Public DNS names. Both A/AAAA records point to this VPS.
|
||||
GUAC_HOST=ts.hilden.info
|
||||
SESSIONGUARD_HOST=sessionguard.hilden.info
|
||||
ACME_EMAIL=admin@example.org
|
||||
|
||||
# Fixed private Docker addresses advertised through NetBird Networks.
|
||||
GUAC01_IP=10.201.1.10
|
||||
GUAC02_IP=10.201.2.10
|
||||
GUAC03_IP=10.201.3.10
|
||||
SESSIONGUARD_IP=10.202.0.10
|
||||
|
||||
# Generate e.g. with: openssl rand -hex 32
|
||||
GUAC_LB_SECRET=REPLACE_WITH_RANDOM_SECRET
|
||||
|
||||
# Base image name built by this repository's release workflow.
|
||||
# If your repository name differs, adjust accordingly.
|
||||
EDGEGUARD_IMAGE=git.send.nrw/sendnrw/sessionguard-edgeguard
|
||||
@@ -0,0 +1,133 @@
|
||||
{
|
||||
email {$ACME_EMAIL}
|
||||
admin off
|
||||
|
||||
# Reduce protocol/parser attack surface without interfering with Guacamole
|
||||
# WebSockets. HTTP/3 can be enabled later if there is a concrete need.
|
||||
servers {
|
||||
protocols h1 h2
|
||||
strict_sni_host on
|
||||
max_header_size 64KB
|
||||
timeouts {
|
||||
read_header 10s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(edge_security_headers) {
|
||||
header {
|
||||
-Server
|
||||
Strict-Transport-Security "max-age=31536000"
|
||||
X-Content-Type-Options "nosniff"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()"
|
||||
}
|
||||
}
|
||||
|
||||
(edgeguard_check) {
|
||||
# EdgeGuard only listens on 127.0.0.1. These headers are overwritten by
|
||||
# Caddy and therefore cannot be forged by an Internet client.
|
||||
forward_auth 127.0.0.1:9081 {
|
||||
uri /check
|
||||
header_up X-Edge-Client-IP {client_ip}
|
||||
header_up X-Edge-Original-Host {host}
|
||||
header_up X-Edge-Original-Method {method}
|
||||
header_up X-Edge-Original-URI {uri}
|
||||
}
|
||||
}
|
||||
|
||||
# Guacamole public endpoint + SessionGuard Access Auth
|
||||
{$GUAC_HOST} {
|
||||
encode zstd gzip
|
||||
import edge_security_headers
|
||||
|
||||
route {
|
||||
# Security pre-check occurs before OIDC/Auth and before any backend.
|
||||
import edgeguard_check
|
||||
|
||||
# These endpoints belong to SessionGuard, but intentionally live on
|
||||
# the Guacamole hostname so the Access-Auth cookie remains host-bound.
|
||||
handle_path /_sessionguard/* {
|
||||
reverse_proxy {$SESSIONGUARD_IP}:8080
|
||||
}
|
||||
|
||||
handle {
|
||||
route {
|
||||
# Never trust identity headers supplied by an Internet client.
|
||||
request_header -X-Guacamole-User
|
||||
request_header -X-SessionGuard-User
|
||||
request_header -X-SessionGuard-Email
|
||||
request_header -X-SessionGuard-Groups
|
||||
request_header -X-Forwarded-User
|
||||
request_header -X-Authenticated-User
|
||||
|
||||
# SessionGuard is the single OIDC/ForwardAuth authority.
|
||||
forward_auth {$SESSIONGUARD_IP}:8080 {
|
||||
uri /auth/verify
|
||||
copy_headers {
|
||||
X-Guacamole-User
|
||||
X-SessionGuard-User
|
||||
X-SessionGuard-Email
|
||||
X-SessionGuard-Groups
|
||||
}
|
||||
}
|
||||
|
||||
# Sticky sessions are important because Guacamole keeps runtime
|
||||
# authentication/session state in the selected webapp process.
|
||||
reverse_proxy {$GUAC01_IP}:8080 {$GUAC02_IP}:8080 {$GUAC03_IP}:8080 {
|
||||
lb_policy cookie guac_node {$GUAC_LB_SECRET}
|
||||
lb_try_duration 5s
|
||||
lb_try_interval 250ms
|
||||
health_uri /
|
||||
health_interval 10s
|
||||
health_timeout 3s
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log {
|
||||
output file /var/log/caddy/guacamole-access.log {
|
||||
roll_size 100MiB
|
||||
roll_keep 5
|
||||
roll_keep_for 168h
|
||||
}
|
||||
format json
|
||||
sampling {
|
||||
interval 1s
|
||||
first 200
|
||||
thereafter 20
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# SessionGuard administration UI, Agent endpoint and APIs.
|
||||
{$SESSIONGUARD_HOST} {
|
||||
encode zstd gzip
|
||||
import edge_security_headers
|
||||
|
||||
route {
|
||||
import edgeguard_check
|
||||
|
||||
# These endpoints are required internally only. Broker requests from
|
||||
# Guacamole workers go directly over NetBird, never via public Caddy.
|
||||
respond /metrics 404
|
||||
respond /api/v1/broker/* 404
|
||||
|
||||
reverse_proxy {$SESSIONGUARD_IP}:8080
|
||||
}
|
||||
|
||||
log {
|
||||
output file /var/log/caddy/sessionguard-access.log {
|
||||
roll_size 100MiB
|
||||
roll_keep 5
|
||||
roll_keep_for 168h
|
||||
}
|
||||
format json
|
||||
sampling {
|
||||
interval 1s
|
||||
first 200
|
||||
thereafter 20
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
# SessionGuard Public VPS: Caddy + NetBird + EdgeGuard
|
||||
|
||||
This stack keeps public TLS termination and Guacamole load balancing on Caddy,
|
||||
uses NetBird only as the encrypted backend transport, and inserts SessionGuard
|
||||
EdgeGuard as a localhost-only request pre-check.
|
||||
|
||||
## Data path
|
||||
|
||||
```text
|
||||
Internet
|
||||
-> Caddy :443 (TLS, SNI, security headers, sticky load balancing)
|
||||
-> EdgeGuard 127.0.0.1:9081 /check
|
||||
-> SessionGuard Access Auth over NetBird
|
||||
-> Guacamole worker over NetBird
|
||||
```
|
||||
|
||||
EdgeGuard is not in the Guacamole tunnel after the WebSocket connection has
|
||||
been established. It evaluates normal HTTP requests and the WebSocket handshake.
|
||||
|
||||
## What EdgeGuard enforces
|
||||
|
||||
- static IPv4/IPv6 IP/CIDR blacklist;
|
||||
- global request rate limit to protect backends during distributed HTTP floods;
|
||||
- high per-IP request rate limit (NAT-friendly defaults);
|
||||
- tighter, configurable limits for OIDC/login/callback paths;
|
||||
- scanner-path blocking (`/.env`, `/.git`, WordPress/phpMyAdmin probes, etc.);
|
||||
- blocks CONNECT/TRACE/TRACK;
|
||||
- temporary persistent bans for clearly hostile scanner/method behavior;
|
||||
- host allowlist and URI-length validation;
|
||||
- localhost-only health and Prometheus-style metrics endpoints;
|
||||
- automatic config/list reload (15 seconds by default);
|
||||
- bounded per-IP state (100,000 entries by default) to avoid memory exhaustion from rotating source addresses.
|
||||
|
||||
The default `rate_limit_weight` for auto-ban is zero on purpose: legitimate
|
||||
users behind a shared NAT should receive 429 during extreme bursts, but should
|
||||
not cause the whole NAT address to be banned. Scanner probes are weighted much
|
||||
more strongly and are auto-banned after repeated hits.
|
||||
|
||||
## Caddy hardening
|
||||
|
||||
The supplied Caddyfile additionally enables:
|
||||
|
||||
- strict SNI/Host matching;
|
||||
- 10 second request-header timeout;
|
||||
- 64 KiB maximum request headers;
|
||||
- HTTP/1.1 + HTTP/2 only (HTTP/3 disabled to reduce exposed protocol surface);
|
||||
- HSTS and conservative security headers;
|
||||
- public blocking of SessionGuard `/metrics` and `/api/v1/broker/*`;
|
||||
- rotated JSON access logs with sampling during request floods;
|
||||
- Caddy admin API disabled (`admin off`); configuration changes use a container restart.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Copy `.env.example` to `.env` and set all values.
|
||||
2. Adjust `edgeguard.json` host names if needed.
|
||||
3. Add permanent abusive IPs/CIDRs to `blacklist.txt`.
|
||||
4. Keep `rate-exempt.txt` empty unless you have a known large NAT that really
|
||||
needs exemption from per-IP limits.
|
||||
5. Start with `docker compose up -d`.
|
||||
6. Verify:
|
||||
|
||||
```bash
|
||||
curl -fsS http://127.0.0.1:9081/healthz -o /dev/null
|
||||
curl -fsS http://127.0.0.1:9081/metrics
|
||||
curl -I https://ts.hilden.info/
|
||||
```
|
||||
|
||||
## Important DDoS boundary
|
||||
|
||||
EdgeGuard protects the application/backends against HTTP request floods and
|
||||
common low-cost scanners. It cannot protect a single VPS if the Internet link
|
||||
or provider edge is saturated. Keep the VPS provider's network firewall and
|
||||
DDoS protection enabled. For a volumetric attack, filtering must happen before
|
||||
traffic reaches the VPS.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Static IP/CIDR blacklist. One IPv4/IPv6 address or CIDR per line.
|
||||
# Changes are picked up automatically (default: within 15 seconds).
|
||||
#
|
||||
# Examples:
|
||||
# 203.0.113.44
|
||||
# 198.51.100.0/24
|
||||
# 2001:db8:1234::/48
|
||||
@@ -0,0 +1,81 @@
|
||||
services:
|
||||
netbird:
|
||||
image: netbirdio/netbird:${NETBIRD_VERSION:-latest}
|
||||
container_name: netbird-public-proxy
|
||||
hostname: ${NETBIRD_PEER_NAME:-public-proxy}
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
- SYS_ADMIN
|
||||
- SYS_RESOURCE
|
||||
devices:
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
environment:
|
||||
NB_SETUP_KEY: ${NETBIRD_SETUP_KEY:?NETBIRD_SETUP_KEY is required}
|
||||
NB_MANAGEMENT_URL: ${NETBIRD_MANAGEMENT_URL:?NETBIRD_MANAGEMENT_URL is required}
|
||||
NB_LOG_LEVEL: ${NETBIRD_LOG_LEVEL:-info}
|
||||
volumes:
|
||||
- netbird-client:/var/lib/netbird
|
||||
|
||||
edgeguard:
|
||||
image: ${EDGEGUARD_IMAGE:?EDGEGUARD_IMAGE is required}:${EDGEGUARD_VERSION:-latest}
|
||||
container_name: sessionguard-edgeguard
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
depends_on:
|
||||
- netbird
|
||||
command:
|
||||
- -config
|
||||
- /etc/sessionguard-edgeguard/edgeguard.json
|
||||
read_only: true
|
||||
cap_drop:
|
||||
- ALL
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
tmpfs:
|
||||
- /tmp:size=16m,noexec,nosuid,nodev
|
||||
volumes:
|
||||
- ./edgeguard.json:/etc/sessionguard-edgeguard/edgeguard.json:ro
|
||||
- ./blacklist.txt:/etc/sessionguard-edgeguard/blacklist.txt:ro
|
||||
- ./rate-exempt.txt:/etc/sessionguard-edgeguard/rate-exempt.txt:ro
|
||||
- edgeguard-state:/var/lib/sessionguard-edgeguard
|
||||
|
||||
caddy:
|
||||
image: caddy:${CADDY_VERSION:-2.11.4}
|
||||
container_name: caddy-public
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
depends_on:
|
||||
- netbird
|
||||
- edgeguard
|
||||
environment:
|
||||
ACME_EMAIL: ${ACME_EMAIL:?ACME_EMAIL is required}
|
||||
GUAC_HOST: ${GUAC_HOST:-ts.hilden.info}
|
||||
SESSIONGUARD_HOST: ${SESSIONGUARD_HOST:-sessionguard.hilden.info}
|
||||
GUAC01_IP: ${GUAC01_IP:?GUAC01_IP is required}
|
||||
GUAC02_IP: ${GUAC02_IP:?GUAC02_IP is required}
|
||||
GUAC03_IP: ${GUAC03_IP:?GUAC03_IP is required}
|
||||
SESSIONGUARD_IP: ${SESSIONGUARD_IP:?SESSIONGUARD_IP is required}
|
||||
GUAC_LB_SECRET: ${GUAC_LB_SECRET:?GUAC_LB_SECRET is required}
|
||||
read_only: true
|
||||
cap_drop:
|
||||
- ALL
|
||||
cap_add:
|
||||
- NET_BIND_SERVICE
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
tmpfs:
|
||||
- /tmp:size=64m,noexec,nosuid,nodev
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy-data:/data
|
||||
- caddy-config:/config
|
||||
- caddy-logs:/var/log/caddy
|
||||
|
||||
volumes:
|
||||
netbird-client:
|
||||
edgeguard-state:
|
||||
caddy-data:
|
||||
caddy-config:
|
||||
caddy-logs:
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"listen": "127.0.0.1:9081",
|
||||
"allowed_hosts": [
|
||||
"ts.hilden.info",
|
||||
"sessionguard.hilden.info"
|
||||
],
|
||||
"blacklist_file": "/etc/sessionguard-edgeguard/blacklist.txt",
|
||||
"rate_exempt_file": "/etc/sessionguard-edgeguard/rate-exempt.txt",
|
||||
"state_file": "/var/lib/sessionguard-edgeguard/state.json",
|
||||
"reload_seconds": 15,
|
||||
"max_uri_length": 8192,
|
||||
"global_limit": {
|
||||
"rate_per_second": 2500,
|
||||
"burst": 5000
|
||||
},
|
||||
"per_ip_limit": {
|
||||
"rate_per_second": 200,
|
||||
"burst": 500
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"name": "guac-access-login",
|
||||
"host": "ts.hilden.info",
|
||||
"path_prefix": "/_sessionguard/auth/login",
|
||||
"rate_per_second": 5,
|
||||
"burst": 100
|
||||
},
|
||||
{
|
||||
"name": "guac-access-callback",
|
||||
"host": "ts.hilden.info",
|
||||
"path_prefix": "/_sessionguard/auth/oidc/callback",
|
||||
"rate_per_second": 10,
|
||||
"burst": 100
|
||||
},
|
||||
{
|
||||
"name": "sessionguard-oidc",
|
||||
"host": "sessionguard.hilden.info",
|
||||
"path_prefix": "/oidc/",
|
||||
"rate_per_second": 5,
|
||||
"burst": 50
|
||||
}
|
||||
],
|
||||
"blocked_methods": [
|
||||
"CONNECT",
|
||||
"TRACE",
|
||||
"TRACK"
|
||||
],
|
||||
"scanner_path_prefixes": [
|
||||
"/.env",
|
||||
"/.git",
|
||||
"/.svn",
|
||||
"/.hg",
|
||||
"/wp-admin",
|
||||
"/wp-login.php",
|
||||
"/phpmyadmin",
|
||||
"/pma",
|
||||
"/cgi-bin",
|
||||
"/server-status",
|
||||
"/actuator",
|
||||
"/vendor/phpunit",
|
||||
"/boaform",
|
||||
"/HNAP1",
|
||||
"/solr/",
|
||||
"/jenkins/"
|
||||
],
|
||||
"auto_ban": {
|
||||
"enabled": true,
|
||||
"threshold": 10,
|
||||
"window_seconds": 120,
|
||||
"ban_seconds": 900,
|
||||
"scanner_weight": 5,
|
||||
"method_weight": 3,
|
||||
"rate_limit_weight": 0,
|
||||
"invalid_uri_weight": 3
|
||||
},
|
||||
"max_tracked_ips": 100000
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
# Optional CIDRs which bypass per-IP and endpoint rate limits.
|
||||
# They are NOT exempt from the static blacklist, host/method/scanner checks,
|
||||
# or the global overload limit.
|
||||
#
|
||||
# Use this only for known NAT gateways/monitoring systems if necessary.
|
||||
# 192.0.2.10
|
||||
Reference in New Issue
Block a user