Update mit Guacamole-Extension
All checks were successful
release-tag / release-image (push) Successful in 2m5s
release-main / release-images (push) Successful in 5m5s

This commit is contained in:
2026-08-22 15:19:17 +02:00
parent 0f4a234f52
commit 7972ed7e38
45 changed files with 6448 additions and 402 deletions

133
docs/API.md Normal file
View File

@@ -0,0 +1,133 @@
# HTTP API (v0.3)
All JSON APIs return an error object with an `error` field on failure unless otherwise stated.
## Authentication classes
### Public/internal service endpoints
- `GET /healthz`
- `GET /metrics`
- `POST /api/v1/agents/enroll` enrollment-token authenticated in request body
- `POST /api/v1/agents/heartbeat` per-agent bearer authentication
- `POST /api/v1/broker/resolve` broker bearer API key
- `POST /api/v1/broker/tokens` broker bearer API key, form-encoded for Guacamole extension
### OIDC-protected admin endpoints
All remaining `/api/v1/...` endpoints require an authenticated Master OIDC session; write operations additionally require the documented RBAC permission.
## Broker
### `POST /api/v1/broker/resolve`
Header:
```text
Authorization: Bearer <SESSIONGUARD_BROKER_API_KEY>
```
JSON request:
```json
{
"username": "DOMAIN\\user",
"connection_id": "42",
"connection_name": "Office Desktop",
"resource_id": "optional",
"farm_id": "optional"
}
```
Response includes selected agent/hostname, farm/resource, reconnect reason, health score, lease expiry and token map.
### `POST /api/v1/broker/tokens`
Same broker credential. Accepts form fields `username`, `connection_id`, `connection_name`, `resource_id`, `farm_id`. Returns URL-encoded token pairs for the Guacamole extension.
## Agents and dashboard
- `GET /api/v1/dashboard`
- `GET /api/v1/agents/{id}`
- `PATCH /api/v1/agents/{id}/control` `maintenance`
Control body:
```json
{
"mode": "online|drain|maintenance",
"restart_when_drained": false,
"tags": {"role":"office"},
"farm_ids": ["office"]
}
```
## Sessions
- `POST /api/v1/agents/{id}/sessions/{session}/action` `session`
- `POST /api/v1/agents/{id}/sessions/bulk` `session`
Actions: `message`, `disconnect`, `logoff`. Forced logoff additionally requires the server-side role permission checks applicable to the operation.
Bulk request example:
```json
{
"action": "logoff",
"scope": "disconnected"
}
```
Scopes include `all`, `active`, and `disconnected`.
## Processes
- `POST /api/v1/agents/{id}/processes/{pid}/kill` `process`
The Master queues an expiring command; it is not a synchronous remote Win32 call.
## Policies
- `PUT /api/v1/agents/{id}/policy` `policy`
- `PUT /api/v1/policy/all` `policy`
- `PUT /api/v1/farms/{id}/policy` `policy`
- `GET /api/v1/policy/history` `policy`
- `POST /api/v1/agents/{id}/policy/rollback/{revision}` `policy`
- `POST /api/v1/farms/{id}/policy/rollback/{revision}` `policy`
- `POST /api/v1/policy/global/rollback/{revision}` `policy`
Policy writes are normalized, validated, assigned a fresh revision and appended to policy history.
## Farms
- `GET /api/v1/farms`
- `POST /api/v1/farms` `manage`
- `PUT /api/v1/farms/{id}` `manage`
- `DELETE /api/v1/farms/{id}` `manage`
Farm fields include `name`, `description`, `agent_ids`, `required_tags`, optional `policy`, and `enabled`.
## Published resources
- `GET /api/v1/resources`
- `POST /api/v1/resources` `manage`
- `PUT /api/v1/resources/{id}` `manage`
- `DELETE /api/v1/resources/{id}` `manage`
Resource fields include desktop/RemoteApp kind, farm ID, Guacamole connection ID/name and RemoteApp parameters.
## Director/history
- `GET /api/v1/history?user=&agent=&limit=`
- `GET /api/v1/alerts`
- `GET /api/v1/leases`
- `GET /api/v1/audit` `audit`
- `GET /api/v1/me` current OIDC user, roles and permissions
`history_limit` bounds retained history. Query `limit` is additionally constrained server-side.
## Agent protocol
`model.ProtocolVersion` is `3` for v0.3.
Heartbeat snapshots contain server/health/session/process/telemetry/profile/event state. The response contains effective desired policy and pending commands. Agents acknowledge completed commands in later heartbeats.

View File

@@ -1,51 +1,176 @@
# SessionGuard architecture
# SessionGuard 0.3 Architecture
## Components
## Purpose
- **Agent (Windows service):** watches RDS/WTS sessions, applies user-profile templates, schedules profile cleanup, exposes a local management UI, and sends outbound heartbeats to the master.
- **Master (Linux/Docker):** receives enrollments and heartbeats, stores the latest server snapshots, provides a consolidated dashboard, and distributes per-agent or bulk policies.
- **Pocket ID:** authenticates administrators through OIDC. The master and each independently usable agent UI have their own callback URL.
SessionGuard separates the functions commonly bundled into a Citrix deployment into components that can be independently replaced:
## Connection model
- **PocketID/OIDC**: administrator identity and group claims.
- **Traefik / forward-auth**: trusted ingress for Guacamole.
- **Apache Guacamole**: browser gateway and RDP transport.
- **Windows RDS**: Windows session runtime.
- **SessionGuard Master**: broker, Director, policy control plane, history, alerting and command queue.
- **SessionGuard Agent**: Windows/RDS integration, profile lifecycle, templates, telemetry and local fallback administration.
Agents initiate HTTPS calls to the master. There is no requirement for the master to open an inbound management connection to a terminal server. Enrollment uses a bootstrap secret once; the master then returns an agent-specific bearer token and stores only its SHA-256 hash.
SessionGuard intentionally does not implement a new remote-display protocol.
## Profile cleanup state machine
## Network model
1. The agent polls WTS sessions.
2. A session that was present in the previous persisted snapshot and disappears is treated as logged off.
3. If no other session with the same SID exists, the profile is scheduled for cleanup after `grace_seconds`.
4. If the SID appears again before the deadline, cleanup is cancelled.
5. Immediately before deletion, the allowed-root rule and active-session rule are checked again.
6. Deletion uses the Windows `DeleteProfileW` API. Failures are retried.
Agents initiate all master communication:
The agent persists the previous session set and pending cleanup jobs so a service restart does not normally lose a logout transition.
```text
Agent --HTTPS heartbeat--> Master
<-- policy + commands--
-- results + telemetry-->
```
## Template engine
The master does not open SMB/RPC/WMI/WinRM management sessions to RDS hosts. This keeps the Windows hosts behind normal firewall/NAT boundaries and makes master outages less disruptive.
Targets are always relative to the resolved user profile path. Supported types:
Guacamole reaches only the broker API using a dedicated API key:
- `directory`: ensure a directory exists.
- `file`: write inline content/base64 content or copy a source file (including a UNC path).
- `url`: create an Internet Shortcut (`.url`).
- `shortcut`: create or update a Windows Shell Link (`.lnk`) and compare its key properties before changing it.
```text
Guacamole extension --HTTP(S) + bearer key--> /api/v1/broker/tokens
```
Templates are evaluated on a newly observed user session and again when a policy revision changes.
That API key is separate from agent enrollment credentials and PocketID secrets.
## Policy precedence
## Master persistence
- The agent starts with its local configured/persisted policy.
- A master policy for an agent becomes authoritative once received.
- If the master is unavailable, the last policy remains active and can be edited locally.
- When the master reconnects and still has a different desired policy, the master's policy wins.
Production mode uses PostgreSQL.
## Deliberate non-goal in v0.1: full roaming-profile replacement
### `sessionguard_state`
A complete restore of a Windows user profile from a share is not implemented. Restoring `NTUSER.DAT`, registry state, and profile files after the Windows profile has already been loaded is race-prone and can corrupt state. Citrix Profile Management operates much deeper in the logon/logoff lifecycle than a normal post-logon service loop.
Single JSONB control-plane document containing relatively small mutable state:
A future profile provider should therefore either:
- agents and last snapshots
- farms
- published resources
- leases
- policy versions/control state
- active alerts
1. synchronize only explicitly selected user-data directories, or
2. integrate with a supported pre-profile-load mechanism / profile-container technology.
### `sessionguard_audit`
The current design keeps this concern separate from cleanup and template enforcement rather than pretending that copying a profile directory after logon is equivalent.
Append-only administrative and command audit events. Indexed by time and actor.
### `sessionguard_session_history`
Append-only Director session history. Indexed by time, lower-cased username and agent ID.
History retention is bounded by `history_limit`. This avoids rewriting a permanently growing history document on every agent heartbeat.
JSON-file storage remains supported when `database_url` is empty. It is intended for development, migration and small single-node installations, not a HA master.
### Single-active-master guard
v0.3 is intentionally single-active-master. A dedicated PostgreSQL connection holds a session-level advisory lock for the lifetime of the Master, and control-plane writes verify that lock is still held. A second Master against the same database refuses to start. This prevents accidental split-brain; it is not a claim of seamless HA/failover.
## Agent state
Persistent agent state under `data_dir` contains:
- enrolled agent ID/token
- last accepted policy
- pending cleanup/profile jobs
- disconnected-session timers
- command deduplication/results
- profile status
- bounded event history
- bounded logon telemetry
The service can therefore continue cleanup/profile/session policy during a master outage using the last accepted policy.
## Session event model
The Windows service subscribes to service session-change notifications and also polls WTS. Immediate notifications wake the worker; polling is the reconciliation mechanism.
The agent records:
- session ID
- user/domain/SID
- state
- client name/address where available
- logon/connect/last-input timestamps
- disconnected-since timestamp
- idle seconds
The master compares consecutive snapshots and emits history events such as `logon`, `reconnect`, `disconnect`, `state_change`, `logoff`, and `logon_ready`.
## Broker model
Broker selection is entirely master-side and never modifies the Guacamole database.
A request contains the authenticated username plus Guacamole connection ID/name or an explicit SessionGuard resource/farm. The master resolves the resource and farm, then follows the placement algorithm documented in `BROKER.md`.
The response contains tokens rather than Guacamole-specific mutable state. The extension injects them into the current user context immediately before the connection parameters are resolved.
## Policy hierarchy
Policy precedence:
1. explicit agent desired policy
2. policy of a farm explicitly listed in the agent's `farm_ids`, in configured order
3. policy of a centrally matching farm (`agent_ids` or `required_tags`), deterministic farm-ID order
4. global policy
5. agent's local policy if no master desired policy applies
Master policy changes are versioned. Rollback creates a new revision derived from the selected historical version, so history remains append-only rather than being rewritten.
## Command model
Administrative actions are queued on the master and delivered through the next heartbeat:
```text
UI -> Master command queue -> heartbeat response -> Agent -> WTS/Win32 -> result -> heartbeat -> audit
```
Commands have IDs, creation time, expiry time and actor. The agent remembers recently executed IDs to avoid duplicate execution if a heartbeat is retried.
Supported command families include:
- message session
- disconnect session
- logoff session
- terminate process
- restart server after drain
## Maintenance states
- `online`: new and existing connections allowed.
- `drain`: no new broker placements; existing sessions may reconnect.
- `maintenance`: neither new placement nor reconnect is allowed.
With `restart_when_drained`, the master queues a restart only once the agent reports zero user sessions.
## Health model
The agent calculates a 0100 health score from local checks. Current checks include:
- CPU pressure
- memory pressure
- system-disk free space
- local RDP listener reachability
- profile-store reachability when profile sync is enabled
The broker excludes hosts below `broker.min_health_score` for new sessions. Alerts have independent thresholds.
## Failure behavior
### Master unavailable
- Existing Windows sessions continue.
- Agent keeps its last accepted policy.
- Profile jobs and disconnect timers continue.
- Local agent UI remains usable if its OIDC provider is reachable.
- New Guacamole broker requests fail because authoritative farm placement is unavailable. Do not silently fall back to an arbitrary RDS host if duplicate-profile protection matters.
### PostgreSQL unavailable
Master writes fail rather than pretending state was persisted. Existing master in-memory state may still answer reads, but operators should treat the master as degraded and restore DB connectivity.
### Profile share unavailable
Backup/restore jobs retry within their configured semantics. A failed backup blocks cleanup. Restore stops retrying after the configured restore window rather than injecting files into an already-running desktop later.
### Agent unavailable
It is removed from new broker placement after `offline_after_seconds`. Offline alerts use their own threshold.

160
docs/BROKER.md Normal file
View File

@@ -0,0 +1,160 @@
# Broker, Farms and Published Resources
## Goal
The SessionGuard broker provides the Citrix-like placement functions that Guacamole's generic RDP connections do not know about: reconnect affinity, farm boundaries, drain/maintenance state, health-aware placement and user leases.
## Farms
A farm is a logical group of RDS hosts.
Membership can be expressed in three ways:
1. the agent has the farm ID in `farm_ids`;
2. the farm has the agent ID in `agent_ids`;
3. the agent satisfies all `required_tags` of the farm.
Example:
```json
{
"id": "office",
"name": "Office",
"required_tags": {
"role": "office",
"environment": "production"
},
"enabled": true
}
```
Tags are exact key/value matches. Explicit membership and tag membership can coexist.
## Published resources
A Resource maps a Guacamole connection to a farm and optionally to a RemoteApp definition.
Desktop example:
```json
{
"name": "Office Desktop",
"kind": "desktop",
"farm_id": "office",
"guacamole_connection_name": "Office Desktop",
"enabled": true
}
```
RemoteApp example:
```json
{
"name": "Sage",
"kind": "remoteapp",
"farm_id": "erp",
"guacamole_connection_name": "Sage",
"remote_app": "||Sage",
"remote_app_dir": "C:\\Program Files\\Sage",
"remote_app_args": "",
"enabled": true
}
```
Connection ID matching is preferred where stable IDs are known; connection-name matching is case-insensitive and convenient for initial deployment.
## Placement algorithm
Given `username`, optional `resource_id`, `farm_id`, and Guacamole connection identity:
1. Resolve an enabled Resource by explicit resource ID, Guacamole connection ID, or Guacamole connection name.
2. Resolve the farm: Resource farm → explicit request farm → configured default farm.
3. If a non-empty farm does not exist or is disabled, fail closed.
4. If `reconnect_existing` is enabled, search only matching farm members for an existing `Active`, `Connected` or `Disconnected` session for the user.
5. `maintenance` hosts are excluded from reconnect; `drain` hosts are allowed for reconnect.
6. Reuse a non-expired lease if its host is still available and belongs to the farm.
7. For a new session, consider only hosts that are:
- online,
- in `online` maintenance mode,
- members of the farm,
- at or above `min_health_score`.
8. Rank candidates by broker score and select the highest score.
9. Create or refresh the lease and return connection tokens.
## Broker score
The current score deliberately favors health and penalizes load:
```text
health score * 10
- active sessions * 20
- disconnected sessions * 5
- CPU percent * 2
- memory-used percent
```
This is an intentionally understandable heuristic rather than an opaque ML model. Thresholds and formula can be evolved without changing the Guacamole integration.
## User leases
A lease reduces races between two nearly simultaneous connection attempts.
With:
```json
"single_session_per_user": true
```
the normalized username is the global lease key. The intent is that a user is directed back to one RDS host whenever possible.
With it disabled, the lease key is scoped by:
```text
user | farm | resource
```
This permits independent sessions in separate published resources/farms.
Leases expire after `lease_seconds`; successful broker use refreshes the expiry.
## Username normalization
Broker comparisons are case-insensitive. `DOMAIN\user` and matching WTS domain/user fields are normalized before comparison.
In environments with ambiguous duplicate short usernames across domains, pass the domain-qualified username from the identity/gateway layer.
## Drain and maintenance
### Drain
Use before patching or planned retirement of a server:
- new sessions: blocked
- existing-session reconnect: allowed
- existing sessions: untouched
Optionally set `restart_when_drained=true`. When zero user sessions remain, a restart command is queued.
### Maintenance
Use for a host that must not receive any brokered access:
- new sessions: blocked
- reconnect: blocked
Existing Windows sessions are not forcefully killed merely by changing this state; use bulk session actions if required.
## Farm policy inheritance
Farm policies are independent from broker selection but use the same membership concepts. Explicit `agent.farm_ids` determine precedence when an agent intentionally belongs to multiple farms. Centrally inferred memberships are evaluated deterministically by farm ID.
## Broker API security
The broker endpoints are not user-OIDC endpoints. They are server-to-server APIs protected by `SESSIONGUARD_BROKER_API_KEY`.
Recommendations:
- generate a high-entropy secret;
- keep Guacamole and Master on a private Docker/network path;
- do not reuse the enrollment token, OIDC client secret or PostgreSQL password;
- rotate the key by updating Master and Guacamole together during a controlled maintenance window.

113
docs/DIRECTOR.md Normal file
View File

@@ -0,0 +1,113 @@
# Director Functions
SessionGuard 0.3 turns the master UI into an RDS-oriented operational console rather than merely a configuration page.
## Server telemetry
Each heartbeat can include:
- hostname and Windows version/build
- uptime
- CPU utilization
- physical memory total/available
- system disk total/free
- composite health score and health-check results
- agent version and heartbeat time
## Session telemetry
For each RDS session, the agent reports the available WTS data:
- session ID
- state
- domain/user/SID
- client name/address where available
- logon timestamp
- connect timestamp
- last-input timestamp
- persistent disconnected-since timestamp
- calculated idle seconds
Not every WTS field is guaranteed to be populated by every Windows version/client combination. The UI treats missing values as unavailable rather than inventing them.
## Session history
The master diffs consecutive snapshots and records history events. Typical timeline:
```text
08:01:12 logon
08:01:20 logon_ready logon=8124ms restore=1380ms
10:44:03 disconnect
10:55:11 reconnect
16:31:52 disconnect
17:31:55 logoff (possibly automatic timeout)
```
History can be filtered by user and agent through the API. Production PostgreSQL stores history in its own indexed append-only table.
## Logon telemetry
SessionGuard can observe part of the logon path that it directly participates in:
- first observed new session
- native WTS logon time if available
- profile restore start/completion
- time until restore/template stage is ready
`ObservedLogonMS` is therefore an **RDS/SessionGuard observed duration**, not a claim to reproduce all Citrix Director logon phases. It is useful for trend detection and alerting, especially when restore/profile storage is responsible for slow logons.
## Process inventory
The Windows agent enumerates processes and associates them with RDS session IDs. The heartbeat includes only user-session processes and caps the list to protect payload size.
From the Director UI an authorized operator can request termination of a process. The action is queued, expires, is executed locally by the agent and is audited. The command carries the session ID observed with the PID; immediately before termination the agent re-enumerates processes and refuses the action if that PID no longer belongs to the expected RDS session. This reduces PID-reuse risk for delayed commands.
Process termination is intentionally an elevated operation and requires the `process` permission.
## Health score
Current local checks start at 100 and subtract penalties when a check fails. The UI exposes both score and check details. The same score feeds new-session brokering.
Current checks:
- CPU below the critical local threshold
- memory below the critical local threshold
- minimum system-disk free space
- TCP reachability of the local RDP listener
- configured profile store reachable
This health score is a placement signal, not a substitute for Windows monitoring/EDR.
## Alerts
When enabled, the master maintains active alerts for conditions including:
- agent offline
- CPU above configured threshold
- memory above configured threshold
- system disk below configured free GiB
- health score below configured threshold
- profile failures above configured count
- disconnected sessions above configured count
- observed logon duration above configured threshold
Alerts have first-seen, last-seen, active/resolved state and notification throttling. A generic JSON webhook can receive alert changes.
## Prometheus
`GET /metrics` exposes aggregate master metrics without usernames/SIDs:
- `sessionguard_master_up`
- `sessionguard_agents_total`
- `sessionguard_agents_online`
- `sessionguard_sessions_active`
- `sessionguard_sessions_disconnected`
- `sessionguard_profile_jobs`
- `sessionguard_cleanup_jobs`
- `sessionguard_health_score_sum`
The endpoint is intentionally small in v0.3. Place it behind network ACLs or your metrics network even though it contains no per-user data.
## Retention
`history_limit` bounds session history and audit retention. PostgreSQL retention is applied to the append-only tables; it is not necessary to rewrite an ever-growing control-plane blob on every heartbeat.

92
docs/GUACAMOLE.md Normal file
View File

@@ -0,0 +1,92 @@
# Guacamole 1.6 Integration
## Design
The SessionGuard Guacamole extension is deliberately **not** an authentication provider in the identity sense. PocketID/header authentication stays responsible for user identity.
The extension subclasses Guacamole's authentication-provider extension point only to decorate the authenticated `UserContext` with a `TokenInjectingUserContext`. At connection time it sends the current Guacamole username plus connection identity to the SessionGuard broker and injects returned tokens.
This avoids:
- writing dynamic hostnames directly into the Guacamole PostgreSQL database;
- reimplementing PocketID authentication in SessionGuard;
- opening a management connection from Master to RDS hosts.
## Build
```bash
cd guacamole-extension
mvn -DskipTests package
```
Or use `deploy/guacamole/Dockerfile.guacamole`, which builds the JAR and copies it to the stock Guacamole extension directory.
## Required Guacamole environment
```yaml
SESSIONGUARD_MASTER_URL: http://sessionguard-master:8080
SESSIONGUARD_BROKER_API_KEY: ${SESSIONGUARD_BROKER_API_KEY}
SESSIONGUARD_BROKER_TIMEOUT_MS: "2500"
```
Use the same broker API key configured on the SessionGuard Master. The URL should preferably be an internal network URL, not the public Internet endpoint.
## Desktop connection
Configure the Guacamole RDP hostname as:
```text
${SESSIONGUARD_HOST}
```
The extension resolves the target host at connection time.
## RemoteApp
Use:
```text
hostname: ${SESSIONGUARD_HOST}
remote-app: ${SESSIONGUARD_REMOTE_APP}
remote-app-dir: ${SESSIONGUARD_REMOTE_APP_DIR}
remote-app-args: ${SESSIONGUARD_REMOTE_APP_ARGS}
```
Create a matching Published Resource in SessionGuard by Guacamole connection ID or name.
## Header-auth security boundary
Your Guacamole configuration uses a trusted header such as `X-Guacamole-User`. That is safe only if untrusted requests cannot reach Guacamole directly with an attacker-controlled copy of that header.
Recommended topology:
```text
Internet
|
Traefik + forward-auth/header scrubber
|
dedicated trusted Guacamole proxy network
|
Guacamole
+--- private backend ---> guacd
+--- private backend ---> PostgreSQL
+--- private control ---> SessionGuard Master
```
Do not rely solely on the fact that the public Traefik router strips inbound identity headers if arbitrary unrelated containers can directly connect to Guacamole's port on a shared Docker network.
## Integration with the supplied compose
Your existing PocketID → traefik-forward-auth → Guacamole chain can remain unchanged. The required additions are:
1. build Guacamole using `deploy/guacamole/Dockerfile.guacamole`;
2. add the three `SESSIONGUARD_*` variables above;
3. give Guacamole private reachability to the SessionGuard Master;
4. add SessionGuard Published Resources matching your Guacamole connections;
5. replace fixed RDP hostnames in brokered connections with `${SESSIONGUARD_HOST}`.
`deploy/guacamole/docker-compose.sessionguard.example.yml` provides a focused example rather than replacing your complete existing stack.
## Failure behavior
If the SessionGuard broker is unavailable, the extension fails the brokered connection instead of choosing an arbitrary backend. This is intentional where reconnect affinity/profile concurrency matters. Existing already-established Guacamole/RDP sessions are unaffected.

106
docs/OPERATIONS.md Normal file
View File

@@ -0,0 +1,106 @@
# Production Operations
## Recommended rollout order
1. Deploy PostgreSQL and Master behind TLS/reverse proxy.
2. Configure PocketID group RBAC and verify a read-only user and an administrator.
3. Upgrade one RDS host/agent and keep profile cleanup in Dry-Run.
4. Verify heartbeat telemetry, processes, health and session history.
5. Create a canary Farm containing only that host.
6. Load the Guacamole extension and create one brokered canary connection.
7. Verify existing-session reconnect and lease behavior.
8. Add a second canary RDS host; test load balancing, drain and maintenance.
9. Enable selected-folder profile restore/backup if not already validated.
10. Enable automatic disconnected-session logoff.
11. Enable destructive cleanup only after profile backup/recovery has been exercised.
12. Expand farm membership gradually.
## PostgreSQL
Production should set `SESSIONGUARD_DATABASE_URL`.
Example:
```text
postgres://sessionguard:SECRET@sessionguard-postgres:5432/sessionguard?sslmode=disable
```
Use TLS (`sslmode=require`/verify modes) when PostgreSQL crosses an untrusted network. The provided compose uses an internal Docker network and therefore demonstrates `sslmode=disable` only for that local link.
Back up the SessionGuard database like any other control-plane database. It contains operational metadata, session usernames/SIDs, audit history and policies, but not profile files themselves.
## Master backup and restore
Back up:
- PostgreSQL database
- `master.json` configuration (without assuming env-injected secrets are present)
- external secret-management values
- custom Guacamole extension image/tag
Restore database and configuration together. After Master starts, Agents will reconnect with their existing per-agent credentials.
## Profile store backup
The profile store is independent from PostgreSQL and must be protected separately. Snapshot/backup the underlying SMB storage according to your recovery objectives.
SessionGuard maintains per-user `current` plus optional historical snapshots, but these are operational generations, not a replacement for storage backup.
## Drain workflow
For planned patching:
1. Set host to `drain`.
2. Optionally send a message to users.
3. Existing users can reconnect to the same host; new users go elsewhere.
4. Wait for sessions to finish or use policy/bulk logoff as appropriate.
5. Set `restart_when_drained` if automatic restart is desired.
6. After reboot/health verification, set host to `online`.
Use `maintenance` when reconnects must also be blocked.
## Upgrades
### Master
- back up PostgreSQL;
- deploy the new binary/image;
- start one master against the DB and verify `/healthz` reports the expected version/store;
- verify OIDC and dashboard;
- then upgrade the Guacamole extension if required.
The database migration code is designed to be forward-applied by the Master. v0.3 also holds a PostgreSQL advisory lock and intentionally refuses a second active Master against the same database. Do not attempt active/active deployment; for manual failover stop/lose the old Master first, then start the replacement against the same restored/reachable database.
### Agents
Upgrade in farm batches using drain mode. Agent state in `C:\ProgramData\SessionGuard` should be preserved across binary replacement.
## Observability
Monitor at minimum:
- `/healthz`
- `/metrics`
- active alerts in Director
- PostgreSQL health
- profile-store availability
- master heartbeat freshness
Alert webhook delivery is best-effort and should complement, not replace, your infrastructure monitoring.
## Master outage
Existing sessions and agent-side policy continue. Brokered **new connections** cannot be authoritatively placed while the Master is unavailable. This fail-closed behavior protects farm affinity and profile concurrency.
If emergency bypass access is required, maintain a separately controlled break-glass RDP path rather than silently making the normal broker choose an arbitrary host.
## JSON fallback migration
If upgrading from JSON storage:
1. stop the old Master;
2. preserve `master.json`;
3. start v0.3 once with the existing file and verify state;
4. configure PostgreSQL and perform an explicit controlled migration/import workflow before retiring the file.
The code can read legacy control-plane JSON state and PostgreSQL migrations can normalize embedded history, but operationally treat backend migration as a change window and verify counts/history.

163
docs/PROFILE-LIFECYCLE.md Normal file
View File

@@ -0,0 +1,163 @@
# Profile lifecycle
## Scope
SessionGuard synchronizes configured **folders inside an existing Windows profile**. It does not replace Windows profile loading and does not roam registry hives.
Do not configure the entire `C:\Users\username` profile root. In particular, do not use SessionGuard to copy:
- `NTUSER.DAT` / `UsrClass.dat`;
- registry hive transaction logs;
- Windows profile service metadata;
- profile container/VHD(X) files;
- EFS material that depends on user-specific key handling.
Use application/data directories whose consistency model is compatible with file copying after logoff and merging at logon.
## Configuration
```json
"profiles": {
"enabled": true,
"store_root": "\\\\fileserver\\SessionGuardProfiles",
"backup_on_logoff": true,
"restore_on_logon": true,
"backup_delay_seconds": 5,
"retry_seconds": 60,
"restore_window_seconds": 120,
"keep_versions": 2,
"exclude_users": ["Administrator"],
"exclude_sids": ["S-1-5-18", "S-1-5-19", "S-1-5-20"],
"folders": [
{
"path": "AppData\\Roaming\\Example",
"exclude_globs": ["Cache/**", "*.tmp"]
},
{ "path": "Desktop" },
{ "path": "Documents" }
]
}
```
### `store_root`
May be a local path or UNC path. In a multi-server RDS farm it should normally be a resilient SMB location.
A user's data is keyed by SID, not username, so renamed accounts do not silently receive a second profile snapshot namespace.
### `backup_delay_seconds`
Delay after the session has disappeared before a backup job starts. Default: 5 seconds. This gives Windows and applications a short period to release profile file handles; locked files are then handled by the normal retry pipeline.
### `retry_seconds`
Backup retry interval and restore retry interval while the restore window is still open.
### `restore_window_seconds`
Maximum period, measured from the first restore job creation for a session, during which restore retries are allowed. Default: 120 seconds.
If the store is still unavailable after this window, SessionGuard marks the restore as abandoned, records the error, marks the session as past its restore phase and applies templates. It will not suddenly perform a late restore into a long-running session.
### `keep_versions`
Number of previous completed `current` snapshots retained under `history`. `0` keeps only `current`.
### `folders[].path`
Must be relative to the Windows user profile. Absolute paths, drive-qualified paths and `..` traversal are rejected.
### `exclude_globs`
Patterns are evaluated relative to that configured folder. Supported behavior:
- standard `path.Match`-style patterns such as `*.tmp`;
- `prefix/**` to exclude a complete subtree such as `Cache/**`.
## Backup semantics
1. Backup is queued after the previous session disappears and waits `backup_delay_seconds`.
2. Before copying, no other session with the same SID may be active.
3. Every configured folder is copied into a new staging tree.
4. Missing configured folders are accepted; different users may not have every application installed/initialized.
5. Symbolic links are not followed.
6. Regular files are copied through a temporary destination file before rename; replacement uses a rollback file when required by the platform.
7. A manifest is written after all configured folders complete.
8. Immediately before activation, the agent performs another WTS session check. If the SID became active while copying, activation is rejected and the previous `current` remains authoritative.
9. The old `current` is moved aside.
10. Staging is renamed to `current`.
11. Historical retention is pruned.
12. Only then is the backup considered successful.
If any required read/write operation fails, the staging tree is removed and the backup job is retried. Cleanup does not proceed.
## Restore semantics
Restore is a **merge** operation. Files present in the snapshot overwrite the corresponding local files. Files that exist locally but are absent from the snapshot are not deleted.
This is intentionally safer than mirroring/deleting at logon. An application may create new machine-specific files between deployments, and deleting them merely because they were absent in an older snapshot would be hazardous.
After restore succeeds (or no previous snapshot exists), templates are evaluated.
## Service startup safety
On service startup, sessions already present are treated as already past the restore phase. This prevents installing/upgrading SessionGuard during working hours from overwriting profile files in sessions that were opened before the service started.
## ACLs and NTFS metadata
The built-in Go copy engine copies regular file data and timestamps and creates destination files/directories under the service identity. It does not claim to reproduce the full NTFS security descriptor, owner, SACL, alternate data streams or every application-specific extended attribute.
For the intended selected-folder use case, destination files normally inherit ACLs from the existing user profile directories. Validate permissions with the applications you actually roam before production rollout.
If your requirement is byte-for-byte/full-metadata profile portability, use a profile-container or specialized profile-management product rather than expanding the selected-folder engine to the entire Windows profile.
## SMB identity
### LocalSystem
For remote SMB resources, a domain-joined server running as LocalSystem commonly authenticates as its machine account:
```text
DOMAIN\RDSHOST01$
```
Grant that account share and NTFS rights if you keep LocalSystem.
### gMSA
For farms, prefer a dedicated group Managed Service Account. Grant it:
- modify/create/delete rights under the SessionGuard profile share;
- local Full Control on `C:\ProgramData\SessionGuard`;
- "Log on as a service" as required by your domain policy.
The installation script supports a gMSA service identity.
## Failure examples
### Profile share unavailable on logoff
- backup job changes to error/retry state;
- local profile remains on disk;
- cleanup is blocked;
- master/local UI shows the job and error;
- when the share returns, backup retries and cleanup can continue.
### Profile share unavailable on logon
- restore retries during `restore_window_seconds`;
- after the window expires, SessionGuard logs the failure and stops retrying for that session;
- templates still run;
- the next real logon gets a fresh restore opportunity.
### User reconnects while backup is pending
If the same SID becomes active before the backup starts, the pending backup and cleanup are cancelled. SessionGuard avoids copying/deleting a profile that has become live again.
## Interaction with the v0.3 broker
Profile correctness is one reason the broker prefers an existing session and maintains a user lease. With `broker.single_session_per_user=true`, two nearly simultaneous Guacamole starts for the same normalized username are steered toward the same leased host whenever possible.
This is an additional coordination layer, not a distributed filesystem lock. The profile pipeline still performs its own WTS rechecks before snapshot activation and cleanup because users may reach RDS through emergency/direct paths outside the broker.
For selected-folder profile synchronization in a multi-host farm, keep the normal Guacamole path brokered and avoid exposing arbitrary direct RDP paths to regular users. Maintain any direct RDP route as an explicitly controlled break-glass path.

55
docs/RBAC.md Normal file
View File

@@ -0,0 +1,55 @@
# RBAC and PocketID Group Mapping
## Identity
The Master uses standard OIDC and expects the `groups` claim when group-based RBAC is configured. PocketID can provide this claim to the SessionGuard OIDC client.
## Configuration
Example:
```json
"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"]
}
}
```
Group matching is case-insensitive. A user may receive multiple roles and therefore the union of their permissions.
If `rbac.groups` is empty, all authenticated users receive `admin` for backwards compatibility. This is convenient for upgrades but should not be used as the final production configuration.
## Built-in roles
| Role | Permissions | Intended use |
|---|---|---|
| `viewer` | `view` | read-only Director |
| `helpdesk` | `view`, `session` | messages/disconnect operations that do not require forced logoff/process control |
| `operator` | `view`, `session`, `session_logoff`, `process`, `maintenance` | RDS operations staff |
| `profile_admin` | `view`, `session`, `profile` | profile operations/pipeline administration |
| `policy_admin` | `view`, `policy` | policy versions and rollout |
| `auditor` | `view`, `audit` | audit review |
| `admin` | all current permissions | full control |
`manage` covers farm/resource administration. `maintenance` covers server mode/restart-when-drained. `process` covers process termination.
## Separation of identities
Do not reuse credentials across these trust relationships:
- PocketID OIDC client secret: browser/admin authentication
- broker API key: Guacamole extension → Master
- enrollment token: new Agent → Master bootstrap
- per-agent bearer token: established Agent → Master heartbeat
- PostgreSQL password: Master → database
## Audit actor
For authenticated administrative changes, SessionGuard records the OIDC email when present, otherwise name, otherwise subject identifier. System-generated operations use an explicit system actor such as `system:drain`.

92
docs/SECURITY.md Normal file
View File

@@ -0,0 +1,92 @@
# Security Model
## Trust boundaries
SessionGuard has five distinct credential classes:
1. OIDC/PocketID browser administrator authentication.
2. Agent enrollment token used only to bootstrap a new agent.
3. Per-agent bearer tokens for heartbeats.
4. Guacamole broker API key.
5. PostgreSQL credentials.
Use independent high-entropy values and rotate them independently.
## Guacamole header authentication
If Guacamole trusts `X-Guacamole-User` or another header, the reverse proxy/header-auth component is part of the authentication boundary. An attacker who can directly reach Guacamole and set the trusted header may bypass the upstream authentication flow.
Therefore:
- do not publish Guacamole's backend port directly;
- strip user-supplied identity headers at the trusted edge;
- prefer a dedicated network between Traefik/forward-auth and Guacamole;
- do not attach unrelated/untrusted containers to a network that can directly reach Guacamole's trusted backend listener.
## Master split-brain protection
v0.3 is a single-active-master design. PostgreSQL mode acquires a dedicated advisory lock and verifies it on control-plane writes. This prevents an accidentally duplicated Master from independently assigning leases/policies against the same database. It does not provide automatic HA; operational failover is still explicit.
## Broker fail-closed behavior
The broker does not fall back from a missing/disabled farm to an arbitrary server. `maintenance` blocks reconnect and new placement; `drain` blocks new placement. This prevents availability logic from silently violating profile/session isolation.
## Profile deletion
SessionGuard does not recursively remove arbitrary configured paths. Cleanup resolves the Windows profile path for the SID, verifies it is beneath an allowed root, verifies exclusions, performs a fresh session recheck and calls Windows `DeleteProfileW`.
When profile backup is enabled, a failed backup blocks cleanup.
## Profile synchronization
Only explicitly configured relative folders are copied. Configuration rejects paths that are absolute or escape the profile root. The copy engine does not intentionally follow symlinks/reparse-like entries into unrelated filesystem locations.
Do not include:
- `NTUSER.DAT`
- `UsrClass.dat`
- registry hives
- the entire profile root
- EFS/private-key material without a separately reviewed design
## Reconnect races
Before activating a completed backup as `current`, the agent rechecks whether the SID became active. If so, activation is abandoned. Before cleanup the agent also performs a fresh WTS recheck.
These checks reduce profile corruption/data-loss risk when a user reconnects during backup or cleanup grace periods.
## Command execution
Master commands are short-lived and authenticated through the per-agent heartbeat. Agent command IDs are deduplicated. Results are returned and audited.
High-impact actions such as process termination, forced logoff and maintenance changes are protected by RBAC on the Master. Process termination is additionally bound to the observed RDS session and revalidated on the agent immediately before termination to reduce PID-reuse risk.
## Local agent UI
The local UI is an intentional break-glass/control-plane fallback. Protect it with OIDC and network ACLs. Do not expose it broadly to the Internet merely because it supports PocketID.
## Container hardening
The supplied Master compose demonstrates:
- read-only root filesystem
- `no-new-privileges`
- dropped Linux capabilities
- private backend network
- a small tmpfs
Review the Guacamole stack separately. Avoid forcing `user: 0:0` on long-running containers unless the specific image/runtime actually requires it.
## Database data sensitivity
PostgreSQL includes usernames, SIDs, client metadata, session history, audit actors, policies and operational state. Treat DB backups accordingly. SessionGuard does not store user passwords.
## Webhook security
Alert webhooks may contain hostnames and operational details. Send them only to trusted HTTPS destinations. v0.3 uses a generic webhook and does not currently implement request signing; if the receiver requires authenticated provenance, place a trusted relay in front of it or add a signing layer before exposing it externally.
## Limitations
- The RDS agent runs with high Windows privileges to enumerate/control sessions and delete profiles; compromise of the agent binary/service account is high impact.
- The current WTS extended-session parsing is targeted at supported x64 Windows Server deployment. Validate on your exact Windows Server versions.
- SessionGuard is not an EDR, PAM product, or complete Windows profile provider.

184
docs/TESTING.md Normal file
View File

@@ -0,0 +1,184 @@
# Release and Failure-Mode Test Matrix
A production release should pass this matrix on the actual supported Windows Server, Guacamole and storage versions.
## Automated Go tests
Run:
```bash
go mod tidy
go test ./...
go vet ./...
```
Current unit coverage includes:
- policy normalization/validation
- template create/update logic
- profile backup/restore merge behavior
- version retention
- snapshot activation guard when a user becomes active
- broker reconnect to an existing session on a drain host
- drain exclusion for new sessions
- farm isolation
- unknown-farm fail-closed behavior
- farm policy selection through tags
## Guacamole extension build
```bash
cd guacamole-extension
mvn -DskipTests package
```
Then launch Guacamole 1.6 and verify the extension appears in startup logs without version/API errors.
## Broker integration
### New connection
- two online agents in the same farm
- both above minimum health
- verify one is selected and a lease appears
- make selected host significantly more loaded; after lease expiry use a new test user and verify score affects placement
### Reconnect
- disconnect a user's RDS session
- initiate the same Guacamole resource again
- verify broker returns the host containing that session even if that host is `drain`
### Farm isolation
- same username has a stale/disconnected session in Farm B
- connect to a resource in Farm A
- verify Farm B is never selected
### Maintenance
- `drain`: existing session reconnect allowed, new user rejected/placed elsewhere
- `maintenance`: existing-session reconnect is not brokered there
- below-minimum-health host: no new placement
- disabled/missing farm: request fails closed
### Lease race
Issue two near-simultaneous broker requests for the same user and verify they converge on the same valid lease/host in the expected deployment topology.
## Drain / restart-when-empty
1. Set server to drain and `restart_when_drained`.
2. Verify no new users are placed there.
3. Verify existing disconnected user can reconnect.
4. End all user sessions.
5. Verify exactly one restart command is queued/executed.
6. Verify the server returns with agent heartbeat and can be set online.
## Session control
- message
- disconnect
- forced logoff
- bulk disconnected logoff
- process termination
- command expiry if agent remains offline past TTL
- duplicate heartbeat delivery must not re-execute the same command ID
- result appears in audit log
## Profile pipeline
### Normal logoff
- modify selected profile data
- log off
- verify post-logoff delay
- verify `staging` is built
- verify snapshot becomes `current`
- verify cleanup starts only after successful backup
### SMB failure
- make profile store unavailable before logoff
- verify backup enters error/retry
- verify local profile is **not** deleted
- restore SMB and verify retry succeeds before cleanup proceeds
### Reconnect during backup
- use enough data to keep backup running
- reconnect same SID before snapshot activation
- verify new snapshot is not promoted over `current`
- verify no destructive cleanup occurs for the active user
### Restore window
- make SMB unavailable at logon
- keep unavailable beyond `restore_window_seconds`
- verify SessionGuard stops trying to inject the restore into the now-running desktop
### Cleanup race
- let cleanup grace nearly expire
- reconnect user
- verify final WTS recheck cancels `DeleteProfileW`
### Retention
Generate more backups than `keep_versions` and verify `current` plus the configured history count remain valid.
## Director/history
- logon, disconnect, reconnect and logoff should create ordered history rows
- `logon_ready` should contain observed and restore timing when available
- process list should contain user-session processes and remain payload-bounded
- history user/agent filters should work after PostgreSQL restart
## Alerts
Individually cross each configured threshold and verify:
- active alert created
- `last_seen_at` updates
- webhook fires at most at notification minimum interval
- alert resolves after condition clears
Also test profile-failure and offline-agent alerts.
## PostgreSQL
- clean schema bootstrap
- restart Master and verify state/history
- DB unavailable during write: API should surface failure rather than claim success
- restore DB and verify agents resume heartbeats
- backup/restore PostgreSQL into a test instance
- retention with history above configured limit
## OIDC/RBAC
With real PocketID groups:
- viewer cannot mutate
- helpdesk can perform allowed session operations
- operator can use maintenance/process functions
- policy admin can write/rollback policy
- auditor can read audit
- admin can manage farms/resources
- user with no matching group receives `default_role`
## Header-auth / network test
From an unrelated container/network, attempt direct HTTP access to Guacamole with a forged `X-Guacamole-User`. The architecture is acceptable only if that bypass path is unreachable/blocked.
## Canary release gate
Before broad rollout record:
- exact Windows Server builds tested
- exact Guacamole image/extension build
- PocketID version/client configuration
- PostgreSQL version
- profile-store SMB server/version
- successful restore from PostgreSQL backup
- successful restore of at least one user profile snapshot
- successful drain/reconnect/restart workflow