RC-2 0.5.2
release-tag / release-image (push) Successful in 2m3s
release-main / release-images (push) Failing after 3m9s

This commit is contained in:
2026-08-24 05:46:42 +02:00
parent 934b013d18
commit 7c69432097
34 changed files with 2393 additions and 52 deletions
+26
View File
@@ -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
+133
View File
@@ -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
}
}
}
+74
View File
@@ -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.
+7
View File
@@ -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:
+77
View File
@@ -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