diff --git a/.gitignore b/.gitignore index 7097ee0..35bb318 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ /bin/ -*.exe *.log state.json master.json diff --git a/production/NETBIRD-NETWORKS.md b/production/NETBIRD-NETWORKS.md new file mode 100644 index 0000000..3c3d5e6 --- /dev/null +++ b/production/NETBIRD-NETWORKS.md @@ -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`. diff --git a/production/README.md b/production/README.md new file mode 100644 index 0000000..c79080c --- /dev/null +++ b/production/README.md @@ -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. diff --git a/production/guac-worker/.env.guac01.example b/production/guac-worker/.env.guac01.example new file mode 100644 index 0000000..9052c12 --- /dev/null +++ b/production/guac-worker/.env.guac01.example @@ -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 diff --git a/production/guac-worker/.env.guac02.example b/production/guac-worker/.env.guac02.example new file mode 100644 index 0000000..d0581d2 --- /dev/null +++ b/production/guac-worker/.env.guac02.example @@ -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 diff --git a/production/guac-worker/.env.guac03.example b/production/guac-worker/.env.guac03.example new file mode 100644 index 0000000..9a9622a --- /dev/null +++ b/production/guac-worker/.env.guac03.example @@ -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 diff --git a/production/guac-worker/docker-compose.yml b/production/guac-worker/docker-compose.yml new file mode 100644 index 0000000..f8996aa --- /dev/null +++ b/production/guac-worker/docker-compose.yml @@ -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: diff --git a/production/install-agent.ps1 b/production/install-agent.ps1 new file mode 100644 index 0000000..216b3ba --- /dev/null +++ b/production/install-agent.ps1 @@ -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" } diff --git a/production/public-vps/.env.example b/production/public-vps/.env.example new file mode 100644 index 0000000..c712aee --- /dev/null +++ b/production/public-vps/.env.example @@ -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 diff --git a/production/public-vps/Caddyfile b/production/public-vps/Caddyfile new file mode 100644 index 0000000..1a2ae5b --- /dev/null +++ b/production/public-vps/Caddyfile @@ -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 + } +} diff --git a/production/public-vps/docker-compose.yml b/production/public-vps/docker-compose.yml new file mode 100644 index 0000000..be0c81c --- /dev/null +++ b/production/public-vps/docker-compose.yml @@ -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: diff --git a/production/sessionguard-agent.exe b/production/sessionguard-agent.exe new file mode 100644 index 0000000..52d807e Binary files /dev/null and b/production/sessionguard-agent.exe differ diff --git a/production/sessionguard/.env.example b/production/sessionguard/.env.example new file mode 100644 index 0000000..e8ed6f3 --- /dev/null +++ b/production/sessionguard/.env.example @@ -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= diff --git a/production/sessionguard/docker-compose.yml b/production/sessionguard/docker-compose.yml new file mode 100644 index 0000000..501a5c3 --- /dev/null +++ b/production/sessionguard/docker-compose.yml @@ -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: diff --git a/production/sessionguard/master.json.example b/production/sessionguard/master.json.example new file mode 100644 index 0000000..c7a8624 --- /dev/null +++ b/production/sessionguard/master.json.example @@ -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 + } +}