RC-1
All checks were successful
release-tag / release-image (push) Successful in 2m1s
release-main / release-images (push) Successful in 3m33s

This commit is contained in:
2026-08-24 05:18:43 +02:00
parent a3ffb996ab
commit 934b013d18
15 changed files with 795 additions and 1 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,4 @@
/bin/
*.exe
*.log
state.json
master.json

View File

@@ -0,0 +1,105 @@
# NetBird Networks / Routing-Peer setup
Use **Networks** (the newer NetBird model), not legacy Network Routes.
The NetBird client runs with `network_mode: host` on each Docker host. Each Docker
host is a routing peer for only the private Docker resource(s) that must be
reachable remotely. None of the application containers publishes a host port.
## Peer groups
Create these peer groups:
- `public-proxy`: PUBLIC01
- `guac-workers`: GUAC01, GUAC02, GUAC03
- `sessionguard-host`: SESSIONGUARD01
## Network: guac01-docker
Routing peer: `GUAC01`
Resource:
- Name: `guac01-web`
- Address: `10.201.1.10`
- Type: IP
- Resource group: `guacamole-web`
Policy:
- Source: `public-proxy`
- Destination/resource: `guac01-web`
- Protocol: TCP
- Port: 8080
Repeat with:
- GUAC02 -> `10.201.2.10:8080`
- GUAC03 -> `10.201.3.10:8080`
Do **not** advertise the guacd addresses (`.11`). guacd has no authentication and
must remain reachable only inside its local Docker bridge.
## Network: sessionguard-docker
Routing peer: `SESSIONGUARD01`
Resources:
1. `sessionguard-master`
- Address: `10.202.0.10`
- Type: IP
2. `guacamole-db`
- Address: `10.202.0.20`
- Type: IP
Policies:
- `public-proxy` -> `sessionguard-master`, TCP 8080
- `guac-workers` -> `sessionguard-master`, TCP 8080
- `guac-workers` -> `guacamole-db`, TCP 5432
Do **not** create a resource for `10.202.0.11` (SessionGuard PostgreSQL).
That database remains private to the SessionGuard Docker bridge.
## IP forwarding
Linux routing peers normally enable forwarding automatically through NetBird.
If forwarding is blocked by host policy, persist it explicitly:
```bash
sudo sysctl -w net.ipv4.ip_forward=1
printf 'net.ipv4.ip_forward=1\n' | sudo tee /etc/sysctl.d/99-netbird-routing.conf
```
## Verify
On each routing peer:
```bash
netbird status -d
ip route
```
From PUBLIC01, after the Networks are distributed:
```bash
curl -I http://10.201.1.10:8080/
curl -I http://10.201.2.10:8080/
curl -I http://10.201.3.10:8080/
curl -I http://10.202.0.10:8080/healthz
```
From a Guacamole worker container:
```bash
# SessionGuard broker/control plane
wget -S -O- http://10.202.0.10:8080/healthz
# Shared Guacamole PostgreSQL
# use pg_isready/psql from a temporary postgres client container if needed
```
For production, check that PUBLIC01 <-> routing peers are **P2P/direct**, not
permanently relayed, using `netbird status -d`.

137
production/README.md Normal file
View File

@@ -0,0 +1,137 @@
# SessionGuard + 3x Guacamole + NetBird + public Caddy
Target design for about 200-250 concurrent Guacamole/RDS sessions:
```text
Internet
|
v
PUBLIC01 (Caddy + NetBird)
| HTTPS termination / SessionGuard ForwardAuth / sticky LB
| NetBird/WireGuard
+--> GUAC01 Docker subnet --> Guacamole01 --> guacd01 --> RDS farm
+--> GUAC02 Docker subnet --> Guacamole02 --> guacd02 --> RDS farm
+--> GUAC03 Docker subnet --> Guacamole03 --> guacd03 --> RDS farm
+--> SESSIONGUARD01 Docker subnet --> SessionGuard Master
--> shared Guacamole PostgreSQL
```
## Important: Guacamole is configured once, not three times
All three workers connect to the **same Guacamole PostgreSQL database** at
`10.202.0.20:5432`.
Therefore these are shared automatically:
- Guacamole users
- connection definitions
- connection groups
- permissions
- user preferences stored in PostgreSQL
- the Guacamole connection IDs used by SessionGuard resources
Every worker must use the same:
- SessionGuard-enabled Guacamole image/version
- PostgreSQL credentials/database
- `SESSIONGUARD_BROKER_API_KEY`
- Header-Auth configuration
There is **no PocketID/OIDC configuration on the individual Guacamole workers**.
## PocketID flow
PocketID is configured once through SessionGuard:
```text
Browser -> https://ts.hilden.info
-> public Caddy
-> SessionGuard /auth/verify
-> PocketID if no valid access session
-> SessionGuard OIDC callback
-> Caddy injects X-Guacamole-User
-> selected Guacamole worker
```
Use one PocketID confidential client for Guacamole access:
- Redirect URI: `https://ts.hilden.info/_sessionguard/auth/oidc/callback`
- Back-channel logout callback: `https://ts.hilden.info/_sessionguard/auth/backchannel-logout`
- Post logout redirect: `https://ts.hilden.info/`
- Scopes: `openid profile email groups`
Use a second PocketID client for the SessionGuard administration UI:
- Redirect URI: `https://sessionguard.hilden.info/oidc/callback`
## DNS
Only the public VPS needs public DNS:
```text
ts.hilden.info -> PUBLIC01 public IP
sessionguard.hilden.info -> PUBLIC01 public IP
```
`auth.hilden.info` remains wherever PocketID currently runs. It only needs to be
moved behind PUBLIC01 if you intentionally want that architecture too.
The Guacamole workers and SessionGuard host need no public A records.
## Docker subnet plan
The example intentionally uses unique, fixed subnets:
```text
GUAC01 10.201.1.0/24 Guacamole 10.201.1.10
GUAC02 10.201.2.0/24 Guacamole 10.201.2.10
GUAC03 10.201.3.0/24 Guacamole 10.201.3.10
SESSIONGUARD01 10.202.0.0/24 Master 10.202.0.10
SG DB 10.202.0.11 (NOT routed)
Guac DB 10.202.0.20
```
Change them if they overlap any existing LAN, Docker, VPN or NetBird routes.
## Deployment order
1. Deploy SessionGuard stack.
2. Deploy GUAC01/02/03 using the same `guac-worker/docker-compose.yml` but the
matching `.env` values.
3. Register all four on-prem hosts as NetBird peers.
4. Configure the NetBird Networks described in `NETBIRD-NETWORKS.md`.
5. Verify PUBLIC01 can reach the three Guacamole IPs and SessionGuard health.
6. Deploy PUBLIC01 Caddy stack.
7. Point public DNS for `ts.hilden.info` and `sessionguard.hilden.info` to PUBLIC01.
8. Configure the PocketID callback URLs.
9. Test `/ _sessionguard/auth/status` (without the space) and then Guacamole.
## Guacamole N+1 behavior
Caddy uses a signed `guac_node` load-balancer cookie. A browser stays on the same
Guacamole webapp while that worker is healthy. If a worker fails, new requests can
move to another worker, but an already active remote desktop tunnel on the failed
worker must reconnect. The RDS session itself can survive and SessionGuard can
broker the reconnect back to its existing RDS host.
For a 250-session peak, three workers imply about 83 sessions/worker normally and
125 sessions/worker with one worker down. Validate that target with your actual
applications and display settings.
## RDP target DNS
Your Guacamole connection currently uses:
```text
${SESSIONGUARD_HOST}.stadt-hilden.de
```
Ensure the Guacamole containers can resolve the internal `stadt-hilden.de` zone.
If NetBird DNS overrides resolver settings on the hosts, configure a NetBird DNS
nameserver/match-domain for your AD/internal DNS zone.
## Updating a worker
Because the three workers are stateless apart from the shared `drive` volume and
shared PostgreSQL state, drain one worker at the Caddy/NetBird level, update it,
then return it to service. Never create three independent Guacamole databases.

View File

@@ -0,0 +1,23 @@
WORKER_NAME=guac01
NETBIRD_VERSION=latest
NETBIRD_MANAGEMENT_URL=https://netbird.example.org
NETBIRD_SETUP_KEY=REPLACE_ME_GUAC01
NETBIRD_LOG_LEVEL=info
GUACAMOLE_VERSION=1.6.0
GUACD_LOG_LEVEL=info
SESSIONGUARD_GUAC_IMAGE=git.send.nrw/sendnrw/sessiongurad-guacamole:latest
# Unique Docker subnet for this host. It MUST NOT overlap any LAN/VPN/Docker subnet.
GUAC_DOCKER_SUBNET=10.201.1.0/24
GUAC_APP_IP=10.201.1.10
GUACD_IP=10.201.1.11
# Routed through the SessionGuard routing peer.
SESSIONGUARD_IP=10.202.0.10
GUAC_DB_IP=10.202.0.20
SESSIONGUARD_BROKER_API_KEY=REPLACE_ME
GUAC_DB_NAME=guacamole_db
GUAC_DB_USER=guacamole_user
GUAC_DB_PASSWORD=REPLACE_ME

View File

@@ -0,0 +1,21 @@
WORKER_NAME=guac02
NETBIRD_VERSION=latest
NETBIRD_MANAGEMENT_URL=https://netbird.example.org
NETBIRD_SETUP_KEY=REPLACE_ME_GUAC02
NETBIRD_LOG_LEVEL=info
GUACAMOLE_VERSION=1.6.0
GUACD_LOG_LEVEL=info
SESSIONGUARD_GUAC_IMAGE=git.send.nrw/sendnrw/sessiongurad-guacamole:latest
GUAC_DOCKER_SUBNET=10.201.2.0/24
GUAC_APP_IP=10.201.2.10
GUACD_IP=10.201.2.11
SESSIONGUARD_IP=10.202.0.10
GUAC_DB_IP=10.202.0.20
SESSIONGUARD_BROKER_API_KEY=REPLACE_ME
GUAC_DB_NAME=guacamole_db
GUAC_DB_USER=guacamole_user
GUAC_DB_PASSWORD=REPLACE_ME

View File

@@ -0,0 +1,21 @@
WORKER_NAME=guac03
NETBIRD_VERSION=latest
NETBIRD_MANAGEMENT_URL=https://netbird.example.org
NETBIRD_SETUP_KEY=REPLACE_ME_GUAC03
NETBIRD_LOG_LEVEL=info
GUACAMOLE_VERSION=1.6.0
GUACD_LOG_LEVEL=info
SESSIONGUARD_GUAC_IMAGE=git.send.nrw/sendnrw/sessiongurad-guacamole:latest
GUAC_DOCKER_SUBNET=10.201.3.0/24
GUAC_APP_IP=10.201.3.10
GUACD_IP=10.201.3.11
SESSIONGUARD_IP=10.202.0.10
GUAC_DB_IP=10.202.0.20
SESSIONGUARD_BROKER_API_KEY=REPLACE_ME
GUAC_DB_NAME=guacamole_db
GUAC_DB_USER=guacamole_user
GUAC_DB_PASSWORD=REPLACE_ME

View File

@@ -0,0 +1,82 @@
services:
# Official NetBird Docker client pattern. Because this peer runs on the
# Docker host network, it can act as a routing peer for GUAC_DOCKER_SUBNET.
netbird:
image: netbirdio/netbird:${NETBIRD_VERSION:-latest}
container_name: netbird-${WORKER_NAME}
hostname: ${WORKER_NAME}
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
guacd:
image: guacamole/guacd:${GUACAMOLE_VERSION:-1.6.0}
container_name: ${WORKER_NAME}-guacd
restart: unless-stopped
environment:
LOG_LEVEL: ${GUACD_LOG_LEVEL:-info}
networks:
guac_internal:
ipv4_address: ${GUACD_IP}
volumes:
- drive:/drive
guacamole:
# Must contain the SessionGuard Broker + Access-Auth JS extension.
image: ${SESSIONGUARD_GUAC_IMAGE:?SESSIONGUARD_GUAC_IMAGE is required}
container_name: ${WORKER_NAME}-guacamole
restart: unless-stopped
depends_on:
guacd:
condition: service_started
environment:
WEBAPP_CONTEXT: ROOT
GUACD_HOSTNAME: ${GUACD_IP}
GUACD_PORT: "4822"
# SessionGuard Broker over the routed private SessionGuard Docker subnet.
SESSIONGUARD_MASTER_URL: http://${SESSIONGUARD_IP}:8080
SESSIONGUARD_BROKER_API_KEY: ${SESSIONGUARD_BROKER_API_KEY:?SESSIONGUARD_BROKER_API_KEY is required}
SESSIONGUARD_BROKER_TIMEOUT_MS: "2500"
# ALL Guacamole workers use the SAME PostgreSQL database.
POSTGRESQL_ENABLED: "true"
POSTGRESQL_HOSTNAME: ${GUAC_DB_IP}
POSTGRESQL_PORT: "5432"
POSTGRESQL_DATABASE: ${GUAC_DB_NAME:-guacamole_db}
POSTGRESQL_USERNAME: ${GUAC_DB_USER:-guacamole_user}
POSTGRESQL_PASSWORD: ${GUAC_DB_PASSWORD:?GUAC_DB_PASSWORD is required}
POSTGRESQL_AUTO_CREATE_ACCOUNTS: "true"
# PocketID is NOT configured in Guacamole. The public Caddy calls
# SessionGuard ForwardAuth and injects this trusted identity header.
HTTP_AUTH_ENABLED: "true"
HTTP_AUTH_HEADER: X-Guacamole-User
REMOTE_IP_VALVE_ENABLED: "true"
networks:
guac_internal:
ipv4_address: ${GUAC_APP_IP}
volumes:
- drive:/drive
networks:
guac_internal:
driver: bridge
ipam:
config:
- subnet: ${GUAC_DOCKER_SUBNET}
volumes:
netbird-client:
drive:

View File

@@ -0,0 +1,47 @@
param(
[string]$Binary = ".\bin\sessionguard-agent.exe",
[string]$Config = ".\configs\agent.json",
[string]$GmsaAccount = ""
)
$ErrorActionPreference = 'Stop'
$service = 'SessionGuardAgent'
$dest = 'C:\Program Files\SessionGuard'
$data = 'C:\ProgramData\SessionGuard'
New-Item -ItemType Directory -Force -Path $dest,$data | Out-Null
$existing = Get-Service -Name $service -ErrorAction SilentlyContinue
if ($existing -and $existing.Status -ne 'Stopped') {
Stop-Service -Name $service -Force
$existing.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(20))
}
Copy-Item $Binary "$dest\sessionguard-agent.exe" -Force
if (Test-Path $Config) {
Copy-Item $Config "$data\agent.json" -Force
} elseif (-not (Test-Path "$data\agent.json")) {
throw "Config '$Config' does not exist and no existing $data\agent.json was found."
}
# Protect credentials, enrollment material and the persisted per-agent token/state.
icacls $data /inheritance:r /grant:r 'SYSTEM:(OI)(CI)(F)' 'Administrators:(OI)(CI)(F)' | Out-Null
if ($GmsaAccount) {
icacls $data /grant "$GmsaAccount`:(OI)(CI)(F)" | Out-Null
}
if (-not $existing) {
& "$dest\sessionguard-agent.exe" -config "$data\agent.json" -service install
}
# Enterprise service defaults: delayed start and automatic recovery after crashes.
& sc.exe config $service start= delayed-auto | Out-Null
& sc.exe failure $service reset= 86400 actions= restart/5000/restart/15000/restart/60000 | Out-Null
& sc.exe failureflag $service 1 | Out-Null
if ($GmsaAccount) {
# gMSA accounts are specified as DOMAIN\name$ and use an empty service password.
& sc.exe config $service obj= $GmsaAccount password= "" | Out-Null
}
Start-Service -Name $service
Write-Host "SessionGuard Agent installed/updated and started."
if ($GmsaAccount) { Write-Host "Service identity: $GmsaAccount" } else { Write-Host "Service identity: LocalSystem" }

View File

@@ -0,0 +1,21 @@
# Public VPS / edge
NETBIRD_VERSION=latest
CADDY_VERSION=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

View File

@@ -0,0 +1,67 @@
{
email {$ACME_EMAIL}
}
# Guacamole public endpoint + SessionGuard Access Auth
{$GUAC_HOST} {
encode zstd gzip
# These endpoints belong to SessionGuard, but intentionally live on the
# Guacamole hostname so the Access-Auth cookie remains host-bound.
# handle_path removes /_sessionguard before proxying.
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
# Guacamole with WEBAPP_CONTEXT=ROOT serves / as its frontend.
health_uri /
health_interval 10s
health_timeout 3s
}
}
}
log {
output stdout
format console
}
}
# SessionGuard administration UI, Agent endpoint and APIs.
{$SESSIONGUARD_HOST} {
encode zstd gzip
reverse_proxy {$SESSIONGUARD_IP}:8080
log {
output stdout
format console
}
}

View File

@@ -0,0 +1,45 @@
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
caddy:
image: caddy:${CADDY_VERSION:-2}
container_name: caddy-public
restart: unless-stopped
network_mode: host
depends_on:
- netbird
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}
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
volumes:
netbird-client:
caddy-data:
caddy-config:

Binary file not shown.

View File

@@ -0,0 +1,31 @@
NETBIRD_VERSION=latest
NETBIRD_PEER_NAME=sessionguard01
NETBIRD_MANAGEMENT_URL=https://netbird.example.org
NETBIRD_SETUP_KEY=REPLACE_ME_SESSIONGUARD01
NETBIRD_LOG_LEVEL=info
POSTGRES_VERSION=17
GUACAMOLE_VERSION=1.6.0
# Preserve your actual registry spelling. Change sessiongurad -> sessionguard if your repo uses the corrected name.
SESSIONGUARD_IMAGE=git.send.nrw/sendnrw/sessiongurad:latest
# Unique Docker subnet advertised through this NetBird routing peer.
SESSIONGUARD_DOCKER_SUBNET=10.202.0.0/24
SESSIONGUARD_APP_IP=10.202.0.10
SESSIONGUARD_DB_IP=10.202.0.11
GUAC_DB_IP=10.202.0.20
SESSIONGUARD_DB_NAME=sessionguard
SESSIONGUARD_DB_USER=sessionguard
SESSIONGUARD_DB_PASSWORD=REPLACE_ME
GUAC_DB_NAME=guacamole_db
GUAC_DB_USER=guacamole_user
GUAC_DB_PASSWORD=REPLACE_ME
SESSIONGUARD_ENROLLMENT_TOKEN=REPLACE_ME
SESSIONGUARD_BROKER_API_KEY=REPLACE_ME
SESSIONGUARD_OIDC_CLIENT_SECRET=REPLACE_ME_ADMIN_OIDC_SECRET
SESSIONGUARD_ACCESS_OIDC_CLIENT_SECRET=REPLACE_ME_GUAC_ACCESS_OIDC_SECRET
SESSIONGUARD_ALERT_WEBHOOK_URL=

View File

@@ -0,0 +1,123 @@
services:
netbird:
image: netbirdio/netbird:${NETBIRD_VERSION:-latest}
container_name: netbird-sessionguard01
hostname: ${NETBIRD_PEER_NAME:-sessionguard01}
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
sessionguard-postgres:
image: postgres:${POSTGRES_VERSION:-17}
container_name: sessionguard-postgres
restart: unless-stopped
environment:
POSTGRES_DB: ${SESSIONGUARD_DB_NAME:-sessionguard}
POSTGRES_USER: ${SESSIONGUARD_DB_USER:-sessionguard}
POSTGRES_PASSWORD: ${SESSIONGUARD_DB_PASSWORD:?SESSIONGUARD_DB_PASSWORD is required}
volumes:
- sessionguard-postgres:/var/lib/postgresql/data
networks:
sessionguard_internal:
ipv4_address: ${SESSIONGUARD_DB_IP}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 20
sessionguard-master:
image: ${SESSIONGUARD_IMAGE:?SESSIONGUARD_IMAGE is required}
container_name: sessionguard
restart: unless-stopped
depends_on:
sessionguard-postgres:
condition: service_healthy
read_only: true
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
environment:
SESSIONGUARD_DATABASE_URL: postgres://${SESSIONGUARD_DB_USER:-sessionguard}:${SESSIONGUARD_DB_PASSWORD}@${SESSIONGUARD_DB_IP}:5432/${SESSIONGUARD_DB_NAME:-sessionguard}?sslmode=disable
SESSIONGUARD_ENROLLMENT_TOKEN: ${SESSIONGUARD_ENROLLMENT_TOKEN:?SESSIONGUARD_ENROLLMENT_TOKEN is required}
SESSIONGUARD_BROKER_API_KEY: ${SESSIONGUARD_BROKER_API_KEY:?SESSIONGUARD_BROKER_API_KEY is required}
SESSIONGUARD_OIDC_CLIENT_SECRET: ${SESSIONGUARD_OIDC_CLIENT_SECRET:?SESSIONGUARD_OIDC_CLIENT_SECRET is required}
SESSIONGUARD_ACCESS_OIDC_CLIENT_SECRET: ${SESSIONGUARD_ACCESS_OIDC_CLIENT_SECRET:?SESSIONGUARD_ACCESS_OIDC_CLIENT_SECRET is required}
SESSIONGUARD_ALERT_WEBHOOK_URL: ${SESSIONGUARD_ALERT_WEBHOOK_URL:-}
volumes:
- ./master.json:/etc/sessionguard/master.json:ro
tmpfs:
- /tmp:size=16m,mode=1777
networks:
sessionguard_internal:
ipv4_address: ${SESSIONGUARD_APP_IP}
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8080/healthz"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
# Generates the official Guacamole PostgreSQL schema once into a named volume.
guac-init:
image: guacamole/guacamole:${GUACAMOLE_VERSION:-1.6.0}
container_name: guac-init
restart: "no"
command:
- /bin/sh
- -ec
- |
/opt/guacamole/bin/initdb.sh --postgresql > /initdb/001-guacamole.sql
test -s /initdb/001-guacamole.sql
volumes:
- guac-init:/initdb
# One shared Guacamole DB for ALL guac01/02/03 workers.
guacamole-postgres:
image: postgres:${POSTGRES_VERSION:-17}
container_name: guacamole-postgres
restart: unless-stopped
depends_on:
guac-init:
condition: service_completed_successfully
environment:
POSTGRES_DB: ${GUAC_DB_NAME:-guacamole_db}
POSTGRES_USER: ${GUAC_DB_USER:-guacamole_user}
POSTGRES_PASSWORD: ${GUAC_DB_PASSWORD:?GUAC_DB_PASSWORD is required}
volumes:
- guacamole-postgres:/var/lib/postgresql/data
- guac-init:/docker-entrypoint-initdb.d:ro
networks:
sessionguard_internal:
ipv4_address: ${GUAC_DB_IP}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 20
start_period: 10s
networks:
sessionguard_internal:
driver: bridge
ipam:
config:
- subnet: ${SESSIONGUARD_DOCKER_SUBNET}
volumes:
netbird-client:
sessionguard-postgres:
guacamole-postgres:
guac-init:

View File

@@ -0,0 +1,72 @@
{
"listen": ":8080",
"public_url": "https://sessionguard.hilden.info",
"data_file": "/var/lib/sessionguard/master.json",
"database_url": "",
"enrollment_token": "SET-BY-SESSIONGUARD_ENROLLMENT_TOKEN",
"offline_after_seconds": 30,
"history_limit": 50000,
"oidc": {
"issuer": "https://auth.hilden.info",
"client_id": "POCKETID-SESSIONGUARD-ADMIN-CLIENT-ID",
"client_secret": "SET-BY-SESSIONGUARD_OIDC_CLIENT_SECRET",
"redirect_url": "https://sessionguard.hilden.info/oidc/callback",
"admin_groups": [],
"secure_cookie": true
},
"access_auth": {
"enabled": true,
"issuer": "https://auth.hilden.info",
"client_id": "POCKETID-GUACAMOLE-ACCESS-CLIENT-ID",
"client_secret": "SET-BY-SESSIONGUARD_ACCESS_OIDC_CLIENT_SECRET",
"redirect_url": "https://ts.hilden.info/_sessionguard/auth/oidc/callback",
"logout_redirect_url": "https://ts.hilden.info/",
"cookie_name": "sg_access_session",
"cookie_domain": "",
"secure_cookie": true,
"session_hours": 8,
"username_claim": "preferred_username",
"allowed_groups": [],
"allowed_hosts": [
"ts.hilden.info"
]
},
"rbac": {
"default_role": "viewer",
"groups": {
"sessionguard-admins": ["admin"],
"sessionguard-helpdesk": ["helpdesk"],
"sessionguard-operators": ["operator"],
"sessionguard-policy": ["policy_admin"],
"sessionguard-profile": ["profile_admin"],
"sessionguard-auditors": ["auditor"]
}
},
"broker": {
"enabled": true,
"api_key": "SET-BY-SESSIONGUARD_BROKER_API_KEY",
"lease_seconds": 900,
"min_health_score": 60,
"reconnect_existing": true,
"single_session_per_user": true,
"default_farm_id": ""
},
"alerts": {
"enabled": true,
"webhook_url": "",
"cpu_percent": 90,
"memory_percent": 90,
"disk_free_gb": 10,
"health_score": 50,
"offline_seconds": 120,
"profile_failures": 3,
"disconnected_sessions": 20,
"logon_duration_seconds": 30,
"notification_min_interval_seconds": 900
}
}