diff --git a/docs/research/steward-orders-prior-art.md b/docs/research/steward-orders-prior-art.md new file mode 100644 index 0000000..18d9557 --- /dev/null +++ b/docs/research/steward-orders-prior-art.md @@ -0,0 +1,285 @@ +# Steward orders prior art: privileged agents, break-glass, two-person rule + +Serves Eureka campaign `eureka-body`, follow-up `eureka-root-idunn-orders`, under +ruling `eureka-root-idunn-orders-break-glass` (operator 2026-10-31: Eureka issues +typed orders to Idunn, run as root and posted to #operator, plus an announced, +brakeable break-glass order). Imagination pass, session eureka-self-2026-10-31. + +Written from recall of the systems' documentation, not re-fetched on +2026-10-31; each claim names where to check it. Facts first, then what each one +says about this design. + +## 1. Privileged agents that take requests from less privileged callers + +### nix-daemon (closest shape) + +- Unprivileged users never run as root: the Nix client talks to `nix-daemon` + over a Unix socket (`/nix/var/nix/daemon-socket/socket`), and the daemon reads + the caller's uid from the socket's peer credentials. `nix.conf` + `allowed-users` says who may connect at all; `trusted-users` says who may + pass privileged settings. (nix.conf manual, "allowed-users", + "trusted-users".) +- The daemon accepts a closed set of typed operations (the worker protocol), + not shell strings. +- Lesson: a local Unix socket plus `SO_PEERCRED` is the established way for a + root agent to know which local principal is asking. The principal is derived + from the kernel, never declared by the caller. + +### Postfix maildrop and pickup + +- Unprivileged `sendmail` writes into the `maildrop` queue through the setgid + `postdrop` helper; the privileged `pickup` daemon reads each file, checks it, + and copies it into its own queue before anything acts on it. (Postfix + architecture overview, `pickup(8)`, `postdrop(1)`.) +- Lesson: the submitter's inbox is not the record. The privileged side copies + the request into a store only it can write, and only that copy is acted on. + +### SaltStack + +- The master publishes jobs to minions that run as root; each job gets a job + id (jid) from the master, not from the caller, and returns are kept in the + job cache (`salt-run jobs.lookup_jid`). (Salt docs, "Job management".) +- `publisher_acl` and external auth (`external_auth`) grant a user named + functions only, for example `pkg.install` and `service.restart`, optionally + with argument patterns. (Salt docs, "Publisher ACL system", "External + Authentication System".) +- CVE-2020-11651 and CVE-2020-11652: the master's request server exposed + methods that skipped authentication, which gave unauthenticated root on every + minion. (Salt advisory, April 2020.) +- Lesson: grant typed functions with argument grammar, not a shell; ids belong + to the authority; and a root agent's intake is its attack surface, so keep it + local and small. + +### Ansible and AWX (Tower) + +- Modules (`ansible.builtin.apt`, `ansible.builtin.systemd_service`) take typed + parameters and report changed, ok or failed; `become` is per task. +- AWX job templates with RBAC let a user launch only pre-approved templates, + with survey fields as typed, validated parameters; the activity stream and + job output are the audit. (AWX docs, "Job Templates", "Surveys", "Activity + Stream".) +- Lesson: "a named operation with validated parameters, launched by a principal + who may launch only that" is the mature shape of a typed order. + +### NixOS deploy tools (deploy-rs, colmena, nixos-rebuild) + +- Activation switches a whole system closure; deploy-rs "magic rollback" rolls + the activation back unless the deployer confirms it within a timeout over a + fresh connection. (deploy-rs README, "Magic Rollback".) +- Lesson: a change that can cut off its own operator wants a confirm-or-revert + window. For Idunn this already exists for releases (continuity, fencing); it + matters for any later host-level order (reboot, network) and for break-glass. + +### polkit and sudoers allowlists + +- polkit rules can grant a user `org.freedesktop.systemd1.manage-units` for + named units and verbs only (rules see `action.lookup("unit")` and `"verb"`). + (polkit(8), systemd's polkit actions.) The mind units already use a polkit + start grant. +- sudoers `Cmnd_Alias` allowlists and `log_output` I/O logs are what the interim + root (`gamecult-ops deploy/eureka-root`) uses today. +- Lesson: a unit allowlist is ordinary practice. polkit alone would give + restarts without a single order record, brake gate or #operator post, so it + is the smaller tool but not the ruled one. + +## 2. Break-glass access + +- The common pattern (healthcare "break the glass", cloud emergency accounts, + Teleport access requests, AWS's guidance on emergency access): the actor + states a justification; use raises a loud alert at once; access is + time-boxed; the session is recorded; every use is reviewed afterwards. + (Teleport docs, "Access Requests" and "Session Recording"; AWS Well-Architected + security pillar, emergency access.) +- What these systems assume and Yggdrasil lacks: a human on call who sees the + alert in minutes. With the operator away, the review is done by Minds and + members in #operator, and the alert reaches them only if chat is up. +- Lesson: break-glass is defended by visibility and review, not by its + intake. It must announce before it runs and keep a full record of what ran. + +## 3. Two-person rule and veto windows + +- Two-person integrity (military nuclear surety; banking dual control): no + single person can complete a critical action; a second must positively + approve. Software forms: required reviewers on protected branches; Google's + multi-party authorization for production changes; Teleport requests needing + N approvers. +- A veto window is weaker and different: lazy consensus (Apache "lazy + consensus": silence means assent after a stated period). It protects only if + someone is watching during the window, and an attacker who controls the + timing picks the quiet hour. +- The ruling in force (`brake-anyone-pulls-motion-releases`) is a veto shape: + anyone pulls at once, only a motion releases. Positive approval would need a + second principal who is awake; with the operator gone that is another Mind, + and Minds read the same chat an injection arrives through. +- Lesson: for typed orders the type is the defence (closed operations, + validated arguments, an allowlist); for break-glass the window plus the + record is the defence, and its strength is the attention of whoever reads + #operator. Say so where the window is configured. + +## 4. What this gives the first cut + +- Authenticate by kernel peer credentials on a root-owned local socket; derive + the principal and the issuing unit, never accept a declared requester. +- Idunn assigns the order id and keeps the only record in its own store; + the caller's request is not the record. +- Orders are a closed set of typed operations with argument grammar and an + allowlist in root-owned configuration. +- Every order and its result are posted; the store is the record and the post + is its projection. +- Break-glass, when it lands, waits on an announcement and a window, and is + recorded in full. + +## 5. Windows hosts: Starfire and Raven + +Serves follow-up `steward-orders-windows-hosts` under ruling +`remote-admin-on-desktops` (operator 2026-10-31: "We'll need a way to do this +kind of thing remotely"). The trigger: on 2026-10-31 she approved through UAC, +sitting at Starfire, an elevated script that added an inbound firewall rule for +Ollama on the WireGuard interface and registered a SYSTEM startup task. She is +away from today, possibly for months; a hard reboot already needs a call. + +### Body, as read on 2026-10-31 (read-only) + +- Both hosts answer TCP 22 and 3389 from Yggdrasil over WireGuard: Starfire + 10.77.0.2, Raven 10.77.0.4. WinRM is stopped (start type Manual) on both. + winget v1.29 is present; choco is not on Raven. +- Yggdrasil holds no SSH client config and no private key for either host: + `gamecultadmin` has only `authorized_keys` and `known_hosts`, root has no + client key, and the mind users have no home. Nothing on Yggdrasil can reach a + Windows shell today. +- Starfire: sshd runs, the stock `Match Group administrators` stanza is in + force, `C:\ProgramData\ssh\administrators_authorized_keys` does not exist and + the operator's user has no `authorized_keys`. Its admins are the operator's + user (`Meta`) and the built-in Administrator. No key reaches Starfire; RDP, + Sunshine/Moonlight and her own UAC click are the only privileged paths in. +- Raven: `administrators_authorized_keys` holds one key, commented + `192.168.1.84 access key`, whose private half is on Starfire (the `raven` + host entry in the operator's `~/.ssh/config`). A session with it is + `BUILTIN\Administrators` at High Mandatory Level: Starfire already holds raw, + unaudited admin on Raven. +- Raven already runs Idunn's host actuator (`idunn-host serve`, task + `GameCult-Idunn-Host`, at logon, interactive, Limited): it dials + `10.77.0.1:17890/udp` over CultNet RUDP, accepts only requests Idunn's + service identity signed, and signs its reports with its own host identity + (`docs/host-actuator.md`; runbook `gamecult-ops/runbooks/idunn-host-raven.md`). + It is unelevated by design: Muninn must live in the desktop session. Starfire + has no actuator. +- Memory, not re-proven here: Starfire has frozen under load (PSU or GPU + suspected), and nothing that must survive may depend on Starfire or Raven + staying up (`workstation-load-budget.md`, 2026-10-29). + +### PowerShell JEA (Just Enough Administration) + +- A session configuration (`.pssc`, registered with + `Register-PSSessionConfiguration`) maps caller groups to role capabilities + (`.psrc`); a role lists `VisibleCmdlets` with per-parameter `ValidateSet` or + `ValidatePattern`, the session runs in `NoLanguage` mode and executes as a + temporary virtual account (`RunAsVirtualAccount`) or a gMSA, so the caller + holds no admin right. `TranscriptDirectory` records every session. + (Microsoft Learn: "JEA overview", "JEA role capabilities", "JEA session + configurations", "Auditing and reporting on JEA".) +- JEA rides WinRM. SSH-based PowerShell remoting has not supported endpoint + configurations or JEA (Microsoft Learn, "PowerShell remoting over SSH", + limitations; the current page may have changed, check before relying on it). +- Microsoft's own warning: a role that exposes a command able to run arbitrary + code (`Start-Process`, `Invoke-Expression`, `Register-ScheduledTask` with a + free action, `New-Service` with a free path) is an escape to the run-as + account ("JEA security considerations"). +- On a workgroup desktop, WinRM needs a listener (HTTPS for anything but + Kerberos), a local Windows account for the caller, and that account's + password or certificate mapping held by the caller. +- Lesson: JEA is the platform's answer to "typed, parameter-validated admin + with a transcript", and its role file is the same shape as a typed order. It + brings WinRM (off today), a Windows credential that would live on Yggdrasil, + and no order id, brake or post: those would still be Idunn's, so the grammar + would live twice, once in PowerShell attributes on each host and once in the + order record. + +### Ansible `win_*` modules over WinRM or SSH + +- `ansible.windows.win_service` (state started, stopped, restarted; start + mode), `community.windows.win_firewall_rule` (name, direction, action, + protocol, localport, remoteip, profiles, interfacetypes), + `community.windows.win_scheduled_task` (actions, triggers, principal, run + level), `ansible.windows.win_package` and the community `win_chocolatey` and + winget modules. Each takes typed parameters, declares desired state, is + idempotent and reports changed, ok or failed. Transport is WinRM or OpenSSH + with `ansible_shell_type: powershell`; over OpenSSH an admin key already has + the elevated token, so no `become`. (Ansible docs: "Windows Remote + Management", "Windows SSH", module pages.) +- Lesson: these modules are the order vocabulary to copy, with desired-state + semantics ("rule X exists with these fields") rather than imperative verbs. + Agentless push means the controller holds admin credentials for every host, + which is the shape the brief rules out. + +### Intune Remediations and Endpoint Privilege Management + +- The Intune Management Extension runs on the device as SYSTEM, polls the + service outbound over HTTPS, runs a detection script and, when it reports + non-compliant, the remediation script, and returns both outputs to the + console. No inbound port; the device authenticates by its enrolment + certificate. (Microsoft Learn: "Remediations", "Intune Management + Extension".) +- Endpoint Privilege Management adds support-approved elevation: the user asks + to elevate one file, the request reaches the console, an admin approves + there, and the device elevates that file hash only, logged. (Microsoft + Learn: "Endpoint Privilege Management", "support approved elevation".) +- Lesson: the mature Windows pattern for an absent administrator is a SYSTEM + agent that dials out, executes only what its authority signed and reports + back, and EPM is the nearest thing to "remote approval in place of a UAC + click". It is the shape `idunn-host` already has on Raven, minus elevation. + +### Windows OpenSSH and administrators + +- For a member of Administrators, sshd reads only + `%ProgramData%\ssh\administrators_authorized_keys` (ACL Administrators and + SYSTEM), and a key-authenticated session gets the full token: UAC does not + apply. Observed on Raven at High Mandatory Level. (Win32-OpenSSH wiki; + issue #1942 on the file's location.) +- Forced commands: `ForceCommand` in a `Match` block is the documented route; + whether `command=` in an authorized_keys line is honoured on Windows has an + open report of it being ignored (Win32-OpenSSH issue #473). Unproven on + these hosts. +- Lesson: an admin key is a remote UAC bypass with nothing but sshd's event log + for a record. As an order transport it needs a forced command that parses + the order on the host, and that parser is the whole boundary. + +### How remote approval replaces a local UAC click + +- Entra PIM: a user eligible for a role activates it for a bounded time with a + justification, optional MFA, and optional approval by a named approver who + is notified and approves in the portal or app; every activation is logged. + (Microsoft Learn, "Privileged Identity Management".) +- CyberArk dual control: use of a privileged account needs an approver's + confirmation in the vault's web or mobile client; sessions run through a + recording proxy (PSM). (CyberArk docs: "Dual control", "Privileged Session + Manager".) +- Teleport access requests: a role is requested with a reason; reviewers + approve or deny, thresholds can require N approvals, and plugins put the + request in Slack, Mattermost or PagerDuty with approve buttons; the grant is + time-boxed and sessions are recorded. (Teleport docs: "Access Requests", + "Access Request plugins".) +- Shared shape: the approval arrives over an out-of-band channel authenticated + as the approver, is scoped to one request, expires, and is recorded beside + the request. A request nobody answers is denied, never run. Shared + assumption: an approver who answers in minutes. +- Lesson for an operator away for months: positive approval is only as live as + her reachability. Where her approval is kept, the channel is her Matrix + account replying in #operator to one order id (Synapse authenticates the + sender; a Mind cannot forge her mxid without root on Yggdrasil), with an + expiry after which the order is refused. Where it is not, the order's type + is the defence, as for Linux (section 3). + +### What this gives the Windows design + +- A SYSTEM agent on each host dials out to an authority that signs every + request; no inbound port, no Windows credential and no admin key on + Yggdrasil (Intune, Salt minion, `idunn-host`). +- The four Ansible modules become typed, desired-state orders, and JEA's + warning becomes the grammar rule: no order field names a free program, + script, or path to run outside a root the host binding declares. +- A Windows host is often absent (a frozen Starfire, a reboot waiting on a + call): an order to an unattached host is refused at once, never queued to + fire days later. +- Approval, where kept, is a Matrix reply from her account to one order id, + scoped and expiring; anything outside the grammar is break-glass.