docs(design): add DESIGN-WINDOWS.md for the Windows agent
Covers full Windows parity for the theta-agent: remote operations executors, Windows service layout, WireGuard mesh client (WS-pushed config), LDAP directory logins via a vendored OpenCredential credential provider, enriched tray, fully-offline Inno installer, and GitHub Actions + Azure Trusted Signing build with the SSO holding all release resources. Optimized for air-gapped deployment.
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
# theta-agent on Windows — design
|
||||
|
||||
Status: **planning / design draft**. Companion to `DESIGN.md`, which describes the
|
||||
Linux-first agent. This document covers bringing the agent to Windows as a first-class
|
||||
platform, optimized for **air-gapped** deployment: everything the host needs ships in
|
||||
one installer, and nothing on the target machine requires internet access.
|
||||
|
||||
## 1. Goals
|
||||
|
||||
1. **Parity, not a port.** Every capability Linux has, Windows has too: telemetry,
|
||||
remote operations (reboot/shutdown, service control, desktop control, arbitrary
|
||||
commands, logs, IAM, self-update), LDAP-backed directory logins, secrets, and a
|
||||
WireGuard mesh client. The capability matrix and Ed25519 signature model from
|
||||
`DESIGN.md` apply unchanged; only the executors change.
|
||||
2. **One installer, fully offline.** A single Inno Setup `.exe` installs everything the
|
||||
Windows host needs — agent service, tray, credential provider, WireGuard client,
|
||||
runtime dependencies. No downloads at install time and no internet calls at runtime.
|
||||
3. **Single outbound connection preserved.** The agent still dials one persistent WSS
|
||||
connection to the SSO. No inbound ports, no firewall rules.
|
||||
4. **The SSO holds all resources.** The Windows installer, loose binaries, and the
|
||||
self-update feed are served from the SSO, mirroring the existing
|
||||
`/resources/theta-agent/...` tree.
|
||||
|
||||
## 2. Current Windows compatibility status
|
||||
|
||||
The agent already compiles, runs, and enrolls on Windows. Validated against a live SSO
|
||||
(`sso.suite.vm42.us`):
|
||||
|
||||
- `go build` for `windows/amd64` succeeds; the agent enrolls via join key, persists its
|
||||
issued token + the SSO's Ed25519 public key to `agent.yml`, and streams discovery /
|
||||
telemetry over WSS.
|
||||
- Unix-domain sockets work on Windows (Go ≥ 1.18, needs Win10 1803+); the LDAP byte-pump
|
||||
and tray IPC both bind and accept connections under the per-user temp dir.
|
||||
- Two unit tests assert POSIX `0600` modes that Windows does not implement; they are the
|
||||
only failures and are not runtime defects (`go test` gate must be made Windows-aware).
|
||||
|
||||
### Changes already merged
|
||||
|
||||
- **Tray IPC socket paths are platform-aware.** Linux keeps `/run/theta/tray.sock` +
|
||||
`/tmp/theta-tray.sock`; Windows uses a Unix socket under the per-user temp dir
|
||||
(`tray_ipc.go`). The tray companion dials the same path.
|
||||
- **The tray no longer exits on Windows.** The `DISPLAY`/`WAYLAND_DISPLAY` graphical
|
||||
session guard is now non-Windows only (`cmd/theta-agent-tray/main.go`).
|
||||
|
||||
## 3. Process & services layout
|
||||
|
||||
| Component | Runs as | Purpose |
|
||||
| :--- | :--- | :--- |
|
||||
| `theta-agent` | Windows service (SYSTEM, auto-start) | WSS to SSO, command dispatch, telemetry, LDAP byte-pump, loopback `/bind`, IAM, WG control |
|
||||
| `theta-agent-tray` | Per-user, logon autostart | Status icon, enrollment dialog, VPN control, auto-VPN |
|
||||
| `theta-agent-helper` (new) | Per-interactive-session, spawned by the service | Desktop controls that cannot run from session 0 |
|
||||
| OpenCredential CP DLL | Loaded by `LogonUI.exe` (Winlogon) | LDAP-backed Windows logon |
|
||||
| `WireGuardTunnel$<name>` | Service installed by WireGuard client | Mesh tunnel |
|
||||
|
||||
The service is the lynchpin: it is up before any user session, which is what lets the
|
||||
credential provider validate logins at Ctrl+Alt+Del.
|
||||
|
||||
## 4. Remote operations (full parity)
|
||||
|
||||
Command dispatch, capability gating, and Ed25519 verification in `websocket.go` are
|
||||
platform-neutral. A `executor_windows.go` (plus a small `executor_linux.go` containing
|
||||
today's Linux mapping) selects the underlying implementation.
|
||||
|
||||
| Command | Linux (today) | Windows |
|
||||
| :--- | :--- | :--- |
|
||||
| `reboot` | `reboot` | `shutdown.exe /r /t 0` |
|
||||
| `shutdown` | `shutdown -h now` / `poweroff` | `shutdown.exe /s /t 0` |
|
||||
| `service_restart` | `systemctl restart <svc>` | `sc.exe stop/start <svc>` (allowed-list unchanged) |
|
||||
| `systemd_action` | `systemctl <action> <svc>` | `sc.exe <action> <svc>`; `status` → `sc.exe query` |
|
||||
| `arbitrary_bash` | `bash -c <script>` | `powershell -NoProfile -Command <script>` |
|
||||
| `fetch_logs` | `journalctl -u <svc>` | `Get-WinEvent -LogName <svc logs>` (application/system log for the service) |
|
||||
| `configure_ldap` | write `/etc/sssd/sssd.conf`, restart sssd | N/A on Windows — see §6 |
|
||||
| `render_secrets` | render templates atomically | identical (Go is cross-platform) |
|
||||
| `iam_apply` | sudoers.d, `authorized_keys`, access.conf | local groups + Windows OpenSSH `authorized_keys` (see below) |
|
||||
| `update_binary` | download → verify SHA256 → rename | download → verify → **staged rename** (running exe is locked; write `.new`, stop service, replace, start) |
|
||||
| `reboot`-gated desktop ops | `loginctl`/`xset` | see desktop control below |
|
||||
|
||||
### Desktop control (session 0 problem)
|
||||
|
||||
A SYSTEM service runs in session 0, which has no interactive desktop. Ops that need an
|
||||
interactive session must run in the user's session:
|
||||
|
||||
- `lock_session` → `LockWorkStation` — must run in the interactive session → **helper**.
|
||||
- `display_off` → `SC_MONITORPOWER` broadcast — needs a window station → **helper**.
|
||||
- `logout_user` → `logoff.exe <session-id>` via `WTSEnumerateSessions` — service can do it.
|
||||
- `sleep_host` → `SetSuspendState` (needs `SeShutdownPrivilege`) — service can do it.
|
||||
|
||||
The service launches the helper in the interactive session (`CreateProcessAsUser` /
|
||||
WTS), passing the action as an argument; the helper performs the op and exits. The helper
|
||||
is installed by the installer and is a tiny self-contained exe.
|
||||
|
||||
### IAM on Windows
|
||||
|
||||
`iam_apply` semantics map to local-account security:
|
||||
|
||||
- `allowed_login_groups` → local groups (create/ensure membership on the mapped local
|
||||
accounts), consumed by OpenCredential authorization rules.
|
||||
- `ssh_keys` → per-user `%ProgramData%\ssh\administrators_authorized_keys` or per-profile
|
||||
`.ssh\authorized_keys` depending on OpenSSH server configuration.
|
||||
- `revoke_users` → terminate sessions (`logoff <id>` / `rwinsta`) and disable the mapped
|
||||
local account.
|
||||
- `sudo_rules` → no direct equivalent; mapped to local group membership / UAC elevation
|
||||
policy, or ignored with a logged warning.
|
||||
|
||||
## 5. WireGuard mesh client
|
||||
|
||||
### Server side (jump-host) — already complete
|
||||
|
||||
jump-host mints peers and exit sites, renders standard `wg0.conf`
|
||||
(`nodejs/utils/wg_conf.js`), and serves `/api/wireguard/peers/:id/conf` + QR. The
|
||||
generated config is compatible with the WireGuard Windows client.
|
||||
|
||||
### Agent side — to build
|
||||
|
||||
1. **Delivery:** new signed command `wireguard_apply` pushed over the existing WSS
|
||||
channel (same model as `iam_apply`): server includes the peer conf; agent verifies the
|
||||
Ed25519 signature, persists it, and applies it. `wireguard_remove` tears it down. No
|
||||
new outbound ports or HTTP endpoints.
|
||||
2. **Apply/teardown (Windows):** official WireGuard client, bundled:
|
||||
- install: `wireguard.exe /installtunnelservice "<name>" <conf>`
|
||||
- remove: `wireguard.exe /uninstalltunnelservice "<name>"`
|
||||
- (Linux would use `wg-quick up|down`.)
|
||||
3. **State detection:** poll `sc.exe query "WireGuardTunnel$<name>"` (or adapter
|
||||
existence) → set `vpn_active` → tray turns blue; drives the existing auto-VPN logic in
|
||||
`home_detect.go`.
|
||||
4. **Auto-VPN:** when away-from-home and `auto_vpn` is set, bring the tunnel up; the tray
|
||||
checkbox now persists the preference to `agent.yml` (today it is memory-only).
|
||||
|
||||
## 6. LDAP: directory logins and the byte-pump
|
||||
|
||||
### The byte-pump tunnel (already Windows-portable)
|
||||
|
||||
The LDAP tunnel is a pure byte pump (`ldap_tunnel.go`) and is transport-agnostic. On
|
||||
Windows it binds `127.0.0.1:389` (fallback `3890`) — the same TCP loopback path Linux
|
||||
falls back to. Gated by the existing `ldap_tunnel` / `configure_ldap` capability flags.
|
||||
|
||||
### Windows logon via OpenCredential (vendored pGina fork)
|
||||
|
||||
Windows has no native OpenLDAP logon (AD only). The plan is the **credential-provider
|
||||
pattern**, using **OpenCredential**, a maintained BSD-3 fork of pGina
|
||||
(`github.com/pedropablobm/OpenCredential`), vendored as a submodule and built from source
|
||||
in CI:
|
||||
|
||||
```
|
||||
LogonUI (Secure Desktop)
|
||||
│ OpenCredential LDAP auth plugin
|
||||
▼
|
||||
127.0.0.1:389 ──► theta-agent byte-pump ──► WSS (ldap_tunnel) ──► SSO ──► OpenLDAP bind
|
||||
```
|
||||
|
||||
- **Zero custom CP code.** OpenCredential's LDAP auth plugin points at `127.0.0.1:389`
|
||||
(simple bind, TLS off). The installer pre-seeds its plugin config (server, username and
|
||||
group attributes matching the OpenLDAP schema) so logon works unattended.
|
||||
- **Local-account bridge.** OpenLDAP cannot mint a Windows token; OpenCredential performs
|
||||
the standard bridge — validate against LDAP, then log into a mapped local account
|
||||
(auto-provisioned on first logon), applying LDAP group membership to local groups.
|
||||
- **Offline cache.** OpenCredential ships a SQLite offline auth cache, which addresses
|
||||
first-boot / SSO-unreachable logon.
|
||||
- **Security note.** The password crosses loopback as a plaintext LDAP simple bind, then
|
||||
rides the already-TLS WSS tunnel; the agent never parses or stores it. Loopback-only
|
||||
listener, no inbound exposure.
|
||||
|
||||
### Alternative (documented, not chosen)
|
||||
|
||||
A thin managed .NET OpenCredential plugin that `POST /bind`s to an agent loopback HTTP
|
||||
endpoint (mirroring DESIGN.md §3's `POST /api/v1/ldap/bind`), giving the agent control of
|
||||
the transport and caching. Kept as a fallback if the LDAP plugin's TLS expectations prove
|
||||
inflexible.
|
||||
|
||||
## 7. Tray companion (enriched)
|
||||
|
||||
`cmd/theta-agent-tray` gains:
|
||||
|
||||
- **Enrollment dialog** — server URL + join key entry; writes `agent.yml` (or a
|
||||
user-scoped config) and signals the service to reload.
|
||||
- **Status panel** — connection state, home/away, VPN, public IP.
|
||||
- **VPN control** — connect/disconnect, auto-VPN checkbox now persisted.
|
||||
- Rendered with the existing systray stack (`fyne.io/systray` supports Windows natively).
|
||||
|
||||
## 8. Installer (Inno Setup, fully offline)
|
||||
|
||||
One `.exe`, built on a connected machine, runnable on an air-gapped one. Bundles:
|
||||
|
||||
- `theta-agent-windows-amd64.exe` (+ arm64) and `theta-agent-tray-windows-amd64.exe`
|
||||
- `theta-agent-helper.exe` (desktop-control helper)
|
||||
- OpenCredential CP binaries + **VC++ v14 redistributable** (its native runtime; .NET
|
||||
Framework 4.8 is built into Windows 10/11 so needs no bundle)
|
||||
- The official, vendor-signed WireGuard for Windows client (signed drivers install
|
||||
offline without signature phone-home)
|
||||
- OpenCredential's pre-seeded LDAP plugin config
|
||||
- Agent `agent.yml` template + self-signed CP cert installed into the machine trusted root
|
||||
|
||||
Install-time behavior:
|
||||
|
||||
- `/SILENT` supported; `SERVER_URL=` and `JOIN_KEY=` as install parameters (the SSO's
|
||||
"Install Agent" modal emits the Windows command instead of the bash one).
|
||||
- Creates the `theta-agent` service (SYSTEM, auto-start), registers the tray for logon
|
||||
autostart, registers the CP with Winlogon, installs the WireGuard service components.
|
||||
- Writes `agent.yml`; a blank join key leaves enrollment to the first service start.
|
||||
|
||||
## 9. Build & release (GitHub Actions + Azure Trusted Signing)
|
||||
|
||||
- **Local dev builds** are self-signed / unsigned.
|
||||
- **Production builds** run in GitHub Actions:
|
||||
- Build matrix: agent `windows-amd64`/`arm64`, tray, helper, OpenCredential CP
|
||||
(MSBuild/.NET 4.8), then the Inno installer.
|
||||
- **Azure Trusted Signing** signs the agent, tray, helper, CP DLL, and installer
|
||||
(workflow federated identity → AzureSignTool). Authenticode chains verify offline,
|
||||
which suits air-gap; SmartScreen reputation simply won't accumulate, which is expected.
|
||||
- Release tags (e.g. `v2.0.1`) name the artifacts; `SHA256SUMS` manifest is generated.
|
||||
- **The SSO holds all resources** (client installer is the deliverable; the server stack
|
||||
is assumed to already run inside the air-gap):
|
||||
- New resource tree: `/resources/theta-agent/windows/...` for the installer, loose
|
||||
binaries, and `SHA256SUMS`.
|
||||
- Release workflow uploads artifacts (admin-gated publish endpoint or mounted resource
|
||||
dir); SSO pins a "latest" pointer.
|
||||
- Self-update feed uses the existing signed `update_binary` flow; the agent's fetch is
|
||||
made platform-aware (currently `cli.go` hardcodes the Linux artifact name).
|
||||
|
||||
## 10. Air-gap considerations
|
||||
|
||||
- **No internet calls at runtime.** The only external-internet dependency in the agent
|
||||
today is public-IP detection (`telemetry.go`, `home_detect.go` hit ipify/icanhazip
|
||||
etc.); on an air-gapped host these fail and degrade silently (accepted behavior).
|
||||
"Home/away" tray logic is meaningless without a public IP and must not flap.
|
||||
- **Self-update is LAN-only** (SSO serves the binary), so it works inside the air-gap.
|
||||
- **All runtime deps bundled:** VC++ redist, WireGuard client + driver, CP + cert,
|
||||
.NET 4.8 is OS-built-in.
|
||||
- **No SmartScreen/driver phone-home** for bundled vendor-signed components.
|
||||
|
||||
## 11. Configuration additions (`agent.yml`)
|
||||
|
||||
```yaml
|
||||
# Windows-specific
|
||||
service_name: theta-agent # windows service name
|
||||
desktop_helper: "C:\\Program Files\\Theta42\\theta-agent-helper.exe"
|
||||
public_ip_detect: true # false disables external lookups (air-gap)
|
||||
wireguard:
|
||||
tunnel_name: theta-mesh
|
||||
conf: "C:\\Program Files\\Theta42\\wg\\theta-mesh.conf"
|
||||
# OpenCredential plugin config is managed by the installer, not agent.yml
|
||||
```
|
||||
|
||||
## 12. Open questions
|
||||
|
||||
1. Desktop-control helper: one exe for all ops vs. per-op; interaction with multiple
|
||||
interactive sessions.
|
||||
2. IAM `sudo_rules` on Windows: local-group mapping vs. explicit no-op.
|
||||
3. Self-update of the *service* binary on Windows: staged rename requires the service
|
||||
to stop; whether the service restarts itself or defers to the tray.
|
||||
4. OpenCredential: whether the LDAP plugin's schema expectations (username/group
|
||||
attributes) match the suite's OpenLDAP without a small config-only shim.
|
||||
5. Air-gap first-boot logon: rely on OpenCredential's offline cache vs. pre-seeding
|
||||
local accounts at install time.
|
||||
|
||||
## 13. Build order
|
||||
|
||||
1. `executor_windows.go` (remote ops) + service wrapper — unlocks everything.
|
||||
2. Tray enrichment + enrollment dialog + auto-VPN persistence.
|
||||
3. WireGuard client (`wireguard_apply`/`remove` + state).
|
||||
4. LDAP byte-pump on Windows (already portable; gate + verify).
|
||||
5. Inno installer bundling the above; CI + Azure signing; SSO resource tree.
|
||||
6. OpenCredential submodule + plugin config + logon validation.
|
||||
Reference in New Issue
Block a user