diff --git a/README.md b/README.md index 96abfdc..fa9125c 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,12 @@ A governance layer that violated its own policies would be theater. **paved-org prevents what [CloudCanary](https://github.com/ChrisInvictus/CloudCanary) detects.** Run both: prevention for the known, detection for the drift. The key-creation constraint above prevents the exact event CloudCanary's identity-drift canary alerts on. + +## AI-workload security +Extending the prevention/detection thesis to AI workloads: +- [**MCP trust-boundary threat model**](docs/threat-models/mcp-trust-boundaries.md) — five boundaries (B1–B5), STRIDE × OWASP LLM Top 10 (2025), with an IAM lens. +- [**MCP security runbook**](docs/runbooks/securing-mcp-setup.md) — a two-part applied audit: a managed-connector host and a self-hosted agent gateway, with host hardening, a fail-closed exec-approval gate, and a staging-validated container. +- [**`modules/ai-guardrails`**](modules/ai-guardrails/) — org-policy guardrails for AI workloads (verified canned constraints), the prevention half of what [CloudCanary](https://github.com/ChrisInvictus/CloudCanary) detects. ## Cost & reproducibility Built end-to-end on GCP free-trial credits. The org layer (policies, folders, WIF, state) is control-plane configuration and costs $0 to keep alive. The workload layer (Act 3) carries the one real cost — a global external HTTPS load balancer for IAP, ~$18/month — so it lives for roughly 48 hours: built, evidenced, then `terraform destroy`ed. diff --git a/docs/evidence/README.md b/docs/evidence/README.md index f0c7fdf..8733ea2 100644 --- a/docs/evidence/README.md +++ b/docs/evidence/README.md @@ -19,3 +19,20 @@ Each core claim maps to a captured artifact. Every one of these states is reprod Rows 8–10 are the zero-trust proof set: anonymous → challenged, authorized → named, authenticated-but-unauthorized → denied. A control you've only seen permit is a control you haven't tested. Trap 11 to the runbook's field notes: deletion_protection is enforced from state, not config — flipping it and destroying in one change deadlocks; apply the flag first, or evict via terraform state rm + delete out-of-band. Set it false from birth on resources with planned lifespans. + +## Part 2 — MCP / AI-agent connector audit (`securing-mcp-setup.md`) + +Host hardening and containment evidence for the self-hosted gateway audit. Rows p2-08–10 are captured on a non-production staging host; rows p2-01–07 are from the production gateway. + +| # | Claim | Artifact | Status | +|---|---|---|---| +| p2-01/02 | SSH password authentication disabled: effective `sshd` config `yes` → `no` | `p2-01-ssh-passwordauth-before.png`, `p2-02-ssh-passwordauth-after.png` | ✅ | +| p2-03/04 | Control-UI insecure auth disabled: `allowInsecureAuth` `true` → `false`, gateway active after change | `p2-03-insecureauth-before.png`, `p2-04-insecureauth-after.png` | ✅ | +| p2-05 | Exec authorization baseline: `security=full, ask=off`, empty allowlist — ungated shell/file-write | `p2-05-exec-approvals-before.png` | ✅ | +| p2-06 | Exec-approval gate configured: `ask=always` | `p2-06-exec-approvals-after.png` | ✅ | +| p2-07 | Gate enforced (fail-closed): a shell command over the channel returns an approval prompt, does not execute | `p2-07-exec-gate-approval-required-telegram.png` | ✅ | +| p2-08 | Container isolation: read-only rootfs, `cap_drop=ALL`, non-root uid 1000, `no-new-privileges`, not privileged | `p2-08-container-isolation-hardening.png` | ✅ (staging) | +| p2-09 | Exec-approval gate operates in-container: `security=full, ask=always, askFallback=deny` | `p2-09-exec-gate-in-container.png` | ✅ (staging) | +| p2-10 | Image credential-free: zero credential-string matches in image layers | `p2-10-image-credential-free.png` | ✅ (staging) | + +The exec-gate set (p2-05 → p2-07) is the B5 remediation: ungated → configured → enforced, verified behaviorally rather than by config inspection. The container set (p2-08 → p2-10) is the containment layer, validated on staging ahead of a documented production promotion. diff --git a/docs/evidence/p2-01-ssh-passwordauth-before.png b/docs/evidence/p2-01-ssh-passwordauth-before.png new file mode 100644 index 0000000..7be31a7 Binary files /dev/null and b/docs/evidence/p2-01-ssh-passwordauth-before.png differ diff --git a/docs/evidence/p2-02-ssh-passwordauth-after.png b/docs/evidence/p2-02-ssh-passwordauth-after.png new file mode 100644 index 0000000..afcba72 Binary files /dev/null and b/docs/evidence/p2-02-ssh-passwordauth-after.png differ diff --git a/docs/evidence/p2-03-insecureauth-before.png b/docs/evidence/p2-03-insecureauth-before.png new file mode 100644 index 0000000..4b66e24 Binary files /dev/null and b/docs/evidence/p2-03-insecureauth-before.png differ diff --git a/docs/evidence/p2-04-insecureauth-after.png b/docs/evidence/p2-04-insecureauth-after.png new file mode 100644 index 0000000..8fe7168 Binary files /dev/null and b/docs/evidence/p2-04-insecureauth-after.png differ diff --git a/docs/evidence/p2-05-exec-approvals-before.png b/docs/evidence/p2-05-exec-approvals-before.png new file mode 100644 index 0000000..df07c88 Binary files /dev/null and b/docs/evidence/p2-05-exec-approvals-before.png differ diff --git a/docs/evidence/p2-06-exec-approvals-after.png b/docs/evidence/p2-06-exec-approvals-after.png new file mode 100644 index 0000000..f2d6f6f Binary files /dev/null and b/docs/evidence/p2-06-exec-approvals-after.png differ diff --git a/docs/evidence/p2-07-exec-gate-approval-required-telegram.png b/docs/evidence/p2-07-exec-gate-approval-required-telegram.png new file mode 100644 index 0000000..3a00476 Binary files /dev/null and b/docs/evidence/p2-07-exec-gate-approval-required-telegram.png differ diff --git a/docs/evidence/p2-08-container-isolation-hardening.png b/docs/evidence/p2-08-container-isolation-hardening.png new file mode 100644 index 0000000..428ec67 Binary files /dev/null and b/docs/evidence/p2-08-container-isolation-hardening.png differ diff --git a/docs/evidence/p2-09-exec-gate-in-container.png b/docs/evidence/p2-09-exec-gate-in-container.png new file mode 100644 index 0000000..1869c20 Binary files /dev/null and b/docs/evidence/p2-09-exec-gate-in-container.png differ diff --git a/docs/evidence/p2-10-image-credential-free.png b/docs/evidence/p2-10-image-credential-free.png new file mode 100644 index 0000000..865d7d9 Binary files /dev/null and b/docs/evidence/p2-10-image-credential-free.png differ diff --git a/docs/runbooks/compose.yml b/docs/runbooks/compose.yml new file mode 100644 index 0000000..527bfff --- /dev/null +++ b/docs/runbooks/compose.yml @@ -0,0 +1,64 @@ +# OpenClaw gateway — hardened containerized deployment (staging). +# +# Every hardening decision is declarative and lives in this file, so the file is +# the documentation. The image is the official GHCR build, pinned by immutable +# digest. The gateway runs through OpenClaw's own documented startup; all +# hardening here is deployment-level isolation, applied around that startup. +# +# Threat model: self-hosted, always-on, loopback-bound gateway reached over a +# private tailnet. See docs/runbooks/securing-mcp-setup.md (Part 2). +# +# Secret-handling note: OpenClaw's documented flow persists its gateway token and +# channel tokens into its state directory (the named volume below). This +# deployment does not claim to remove those from disk; it bounds access to them +# with container isolation (read-only rootfs, dropped capabilities, +# no-new-privileges, non-root user). Native Docker-secrets injection is a +# documented enhancement, deferred (see runbook roadmap). + +services: + openclaw-gateway: + # Official image, pinned by immutable digest (not a mutable tag) for + # reproducibility and provenance. + image: ghcr.io/openclaw/openclaw@sha256:6a31d44b2944e7adcd2b582bf6fb463111264ebca97a0201795b799135bd102c + + container_name: openclaw-gateway + + # Runs as the image's built-in non-root 'node' user (uid 1000), declared + # explicitly rather than trusted implicitly. + user: "1000:1000" + + # --- Isolation (the containment layer) --------------------------------- + read_only: true # immutable root filesystem + cap_drop: [ALL] # drop every Linux capability + security_opt: + - no-new-privileges:true # block setuid/privilege escalation + # No docker socket mount. No privileged. No added capabilities. + + # --- Writable surfaces: explicit and minimal --------------------------- + # With a read-only rootfs, the process needs declared scratch. tmpfs is + # RAM-backed and never persisted. + tmpfs: + - /tmp:mode=1777,size=128m + + # Named volume for gateway state: OpenClaw persists its config store here, + # AND the exec-approvals gate (exec-approvals.json + socket). This MUST + # persist or the B5 approval gate reverts to the ask=off default — the + # High-severity finding, re-opened. Validation gate 2 checks this explicitly. + volumes: + - openclaw-state:/home/node/.openclaw + + # --- Exposure ---------------------------------------------------------- + # Publish to host loopback ONLY. Tailscale reaches the gateway via the + # host's tailnet interface. No routable bind; no public ingress. + ports: + - "127.0.0.1:18789:18789" + + # --- Resource limits --------------------------------------------------- + mem_limit: 1g + cpus: 1.5 + + # --- Supervision ------------------------------------------------------- + restart: unless-stopped + +volumes: + openclaw-state: diff --git a/docs/runbooks/securing-mcp-setup.md b/docs/runbooks/securing-mcp-setup.md new file mode 100644 index 0000000..8fbe852 --- /dev/null +++ b/docs/runbooks/securing-mcp-setup.md @@ -0,0 +1,321 @@ +# Securing MCP and AI-Agent Connectors — A Two-Part Audit + +> **Changelog** +> - *Part 2 added:* self-hosted OpenClaw gateway audited; host hardening (SSH, control-UI auth) and a fail-closed exec-approval gate implemented and evidenced. +> - *Containment layer validated on staging:* hardened container build (read-only rootfs, dropped capabilities, non-root, digest-pinned image) validated on a non-production host; production promotion documented as steps, not yet executed. + +Applied security review of my own AI-assistant tooling against the +[MCP trust-boundary threat model](../threat-models/mcp-trust-boundaries.md). +The threat model defines the boundaries (B1–B5); this document is the checklist +run against two live setups, with findings, dispositions, and evidence. + +Two setups sit at opposite ends of the operational-responsibility spectrum: + +- **Part 1 — Managed-connector host.** Vendor-operated, OAuth-brokered + (Claude Desktop connectors). The platform holds credentials. +- **Part 2 — Self-hosted gateway.** Self-operated (OpenClaw on a dedicated + always-on host). The operator holds credentials, patch cadence, exposure, and + process privilege. + +The same five boundaries apply to both. The analytical value is in **where +responsibility moves** between the models — custody, patching, and isolation that +the vendor performs implicitly in Part 1 become explicit operator obligations in +Part 2. + +All identifiers are redacted per the repository +[disclosure policy](../../SECURITY.md): no hostnames, IP addresses beyond +loopback, tailnet names, account handles, or credential material. Evidence +captures in `docs/evidence/` are redacted to the same standard. + +--- + +# Part 1 — Managed-connector host + +## 1.1 Environment + +Host: Claude Desktop with platform-managed connectors. State at audit start: + +| Connector | State | Granted surface | +|---|---|---| +| Google Drive | Connected | Account-wide document read | +| Claude in Chrome | Connected | Agentic browsing (live browser actions) | +| Gmail | Connected → disconnected during audit | Mail read | +| Google Calendar | Connected → disconnected during audit | Schedule read | +| GitHub | Available, not connected | — | + +These connectors are OAuth-brokered and platform-managed; no local configuration +holds credentials. Credential custody (B3) is performed platform-side. The audit +therefore focuses on scope, surface, and the model boundary rather than on +secret storage. + +**Scope.** This part covers the managed-connector host only. The self-hosted +agent gateway on separate hardware is a distinct trust model, audited in Part 2. + +## 1.2 Boundary review + +**B1 — User ↔ Host.** Drive is a read surface; Chrome is an action surface. The +two carry different trust and are governed differently (§1.4). + +**B2 — Host ↔ Server.** Both connected surfaces are first-party; no +community-authored servers are installed, so the supply-chain exposure (LLM03) +central to B2 is not present. The inventory surfaced unused grants — Gmail and +Calendar were connected with no consuming workflow (finding 5). + +**B3 — Server ↔ Downstream.** No credentials on disk. The Drive grant is +account-wide read, broader than the folders in actual use; the integration +offers no folder-level scoping (finding 3). + +**B4 — Server ↔ Server.** The two surfaces can share a session context. +Mitigation folds into the B5 controls below. + +**B5 — Model ↔ Tool output.** Agentic browsing ingests third-party page content +into a context that can also act. No configuration control exists for this +boundary; it is governed behaviorally (§1.4). + +## 1.3 Findings + +| # | Finding | Severity | Disposition | +|---|---|---|---| +| 1 | No credentials stored on disk (OAuth pattern) | Info | Custody is platform-side | +| 2 | No third-party servers installed | Info | Standing rule governs future additions (§1.4) | +| 3 | Drive grant account-wide; no folder scoping available | Low | Accepted; quarterly grant review | +| 4 | Agentic browser ingests untrusted content into an action-capable context | Medium | Behavioral controls (§1.4); no config control exists | +| 5 | Gmail and Calendar connected without a consuming workflow | Low–Medium | **Remediated** — disconnected during audit; two standing grants against high-sensitivity data removed | + +## 1.4 Standing controls + +- Connect on demand; disconnect on disuse. Reviewed quarterly. +- New connector adoption follows a vendor-onboarding check: provenance, + publisher, requested scopes, credential location. +- Action-capable surfaces require human confirmation for irreversible operations + and are isolated from credential-adjacent sessions. +- Quarterly third-party access review of the underlying account includes AI + connectors. + +--- + +# Part 2 — Self-hosted gateway + +The dependency Part 1 accepts — vendor-side credential custody — is the operator's +responsibility here. This part audits OpenClaw, a self-hosted AI-agent gateway on +a dedicated always-on host, reached over a private tailnet. + +| Responsibility | Managed host (Part 1) | Self-hosted gateway (Part 2) | +|---|---|---| +| Credential custody (B3) | Vendor, platform-side | Operator — on-disk config | +| Patch cadence (B2) | Vendor | Operator | +| Network exposure | Vendor | Operator | +| Process privilege & supervision | Vendor | Operator | + +All findings were confirmed against the live host. Evidence references +(`p2-NN`) are captures in `docs/evidence/`. + +## 2.1 Environment + +| Attribute | State | +|---|---| +| Gateway | OpenClaw v2026.5.28 (production host) | +| Bind | `127.0.0.1:18789` and `[::1]` only; reached over private tailnet; no port-forward, no public ingress | +| Process | Non-root systemd *user* service, lingering enabled | +| Channel | Telegram, sender-allowlisted (operator account only) | +| Config | `~/.openclaw/openclaw.json`, mode `0600`, not version-controlled, no backup/sync copy off-host | +| Host firewall | `ufw` default-deny inbound | +| Runtime | Node v22.23.1 (current) | + +## 2.2 Boundary review and remediation + +**Exposure (verified).** The gateway binds loopback only and is reached over the +tailnet; no ports are forwarded. Recorded as a verified property of the current +configuration. An unrelated loopback service (CUPS, `:631`) is present and +benign; disabling it is an optional surface reduction. + +**B1 — User ↔ Host.** The Telegram channel is sender-allowlisted; only the +operator account can issue instructions, which closes unauthenticated-sender +access. An allowlist authenticates the sender, not message content — relayed +hijacked content remains a B5 concern (below). + +**B2 — Host ↔ Server.** OpenClaw is package-manager installed; Node is current. +Patch cadence is manual, approximately monthly. Accepted with note: patch latency +is now an operator SLA. The container deployment (§2.4) reduces update effort to a +single operation. + +**B3 — Credential custody.** Five credential values are stored in plaintext +across the OpenClaw configuration (gateway auth token, Telegram bot token, Brave +API key, Notion API key, and an exec-approvals socket token). File mode is `0600` +and no backup or sync client copies the configuration off-host, which bounds +local exposure. Runtime secret handling is addressed by the container work in §2.4. + +**B3-UI — Control-UI authentication.** `gateway.controlUi.allowInsecureAuth` was +`true`, permitting authentication over a transport the UI otherwise rejects. +Mitigated in practice by the loopback bind, but posture dependent solely on +network position is not defense-in-depth. **Remediated:** set to `false`; gateway +confirmed active after restart. Evidence: `p2-03` → `p2-04`. + +**Host — SSH authentication.** Effective `sshd` configuration was +`PasswordAuthentication yes`; the intended posture was key-only. Five authorized +keys were present and key login was confirmed. **Remediated:** set +`PasswordAuthentication no` via drop-in, reloaded `sshd`, and verified a new key +session before terminating the existing one. `PermitRootLogin prohibit-password` +retained. Evidence: `p2-01` → `p2-02`. + +**B5 — Model ↔ tool output (exec authorization).** The agent has shell and +file-write capability, runs continuously, and processes messages without a human +in the loop. Baseline exec policy (`p2-05`, `openclaw approvals get`) was +`security=full, ask=off` with an empty allowlist: any command received over the +channel executed immediately at the service user's privilege. Under the threat +model, the sender allowlist stops an unauthenticated stranger, but hijacked +content relayed through a trusted sender reaches the shell. + +Remediation and verification: + +1. Set exec policy to require approval per invocation (`ask=always`); confirmed in + configuration (`p2-06`). +2. Behavioral test — a shell command issued over the channel still executed. +3. Root cause: `askFallback=full` permits execution when no approval UI is + reachable. The channel is not an approval-capable UI in that path, so every + invocation reached the fallback and ran. The gate was fail-open. +4. Set `askFallback=deny` — deny when approval cannot be obtained. Fail-closed. +5. Re-test (`p2-07`): the same command now returns an approval prompt + (Allow Once / Deny, 30-minute expiry, per-request identifier) and does not + execute without explicit approval. + +Configuration state alone did not reflect enforcement; only the behavioral test +identified the fail-open fallback. + +## 2.3 Findings + +| # | Finding | Severity | Disposition | +|---|---|---|---| +| 1 | Ungated exec (`security=full, ask=off`): shell + file-write, unattended, at service-user privilege | **High** | **Remediated** — `ask=always, askFallback=deny`; approval enforced (`p2-05` → `p2-07`) | +| 2 | `PasswordAuthentication yes` on SSH | Medium | **Remediated** — set `no`; key login verified (`p2-01` → `p2-02`) | +| 3 | Five plaintext credentials in on-disk config | Medium | Mode `0600`, no off-host copy; container isolation applied on staging (§2.4); production runtime-secret handling and rotation are promotion steps | +| 4 | `allowInsecureAuth: true` on control UI | Low–Medium | **Remediated** — set `false` (`p2-03` → `p2-04`) | +| 5 | Manual, ~monthly patch cadence | Low | Accepted; container deployment reduces update effort | +| — | Non-root user service, `ufw` default-deny, current runtime, sender-allowlisted channel, no config sync-leak | Pass | Baseline sound | + +## 2.4 Containment layer — hardened container (validated on staging) + +The §2.2 remediations are preventive: they stop the unauthorized command and +reduce host surface. The containment layer bounds blast radius if execution does +occur. On bare metal, agent-initiated shell runs as the service user against its +home directory, SSH keys, and the credential-bearing configuration. Under +containerization, the same execution is confined to an isolated filesystem +without access to those assets. + +This section was **built and validated on a non-production staging host.** The +production gateway remains the bare-metal service pending the promotion window +(steps below). Evidence `p2-08` through `p2-10` is from the staging host. + +### Deployment + +The official OpenClaw image is used, pinned by immutable digest rather than a +mutable tag. All hardening is declarative in the Compose file, so the file is the +control documentation: + +```yaml +services: + openclaw-gateway: + image: ghcr.io/openclaw/openclaw@sha256: # official image, digest-pinned + user: "1000:1000" # non-root + read_only: true # immutable root filesystem + cap_drop: [ALL] # drop all Linux capabilities + security_opt: [no-new-privileges:true] # no privilege escalation + tmpfs: [/tmp:mode=1777,size=128m] # RAM-backed scratch + volumes: [openclaw-state:/home/node/.openclaw] # persisted state + approvals gate + ports: ["127.0.0.1:18789:18789"] # host loopback only + mem_limit: 1g + cpus: 1.5 + restart: unless-stopped +# No Docker-socket mount. No privileged. No added capabilities. +``` + +### Validation (all gates passed on staging) + +| Gate | Method | Result | Evidence | +|---|---|---|---| +| Isolation | `docker inspect` of runtime host-config | `ReadonlyRootfs=true`, `CapDrop=[ALL]`, `User=1000:1000`, `no-new-privileges:true`, `Privileged=false` | `p2-08` | +| Exec-approval gate persists | `openclaw approvals get` inside the container | `security=full, ask=always, askFallback=deny` — the B5 gate operates identically in-container, mounted on the state volume so it does not revert | `p2-09` | +| Image credential-free | Scan image layers for credential strings | Zero matches — no credential material baked into the image | `p2-10` | +| Loopback isolation | Cross-tailnet reachability test from a separate device | Connection timeout — the port is unreachable directly over the tailnet, confirming the loopback bind by negative result | (recorded in text) | + +**Secret handling — verified, and stated precisely.** The image contains no +credential material (`p2-10`). OpenClaw's documented startup persists its tokens +into the state directory, which is the named volume; this deployment does not +claim to eliminate secrets from disk. It bounds access to them through the +container's isolation — read-only root filesystem, dropped capabilities, non-root +user. Native Docker-secrets injection (composing credentials from +`/run/secrets/*` into a RAM-only environment at runtime) was scoped and deferred: +OpenClaw's config flow does not natively consume Docker secrets, so the honest +options are an entrypoint-templating layer (a follow-up) or the accurate weaker +claim stated here. Deferred rather than approximated. + +**Tailnet reachability.** The container binds host loopback only, confirmed +unreachable directly over the tailnet (validation table, row 4). Production +tailnet access is provided by an authenticated proxy layer (Tailscale Serve or +equivalent) bridging the tailnet to the loopback port **without widening the +container bind** — a host-layer concern documented as a promotion step, not +exercised on the staging host. Tailscale Funnel (public-internet ingress) was +explicitly declined: it contradicts the no-public-ingress posture that defines +this deployment. + +### Production promotion (documented; not yet executed) + +1. Snapshot the production `~/.openclaw` state (config, approvals file). +2. Schedule a maintenance window; stop the systemd user service. +3. Stand up the container with the validated Compose file, mounting the + production state into the named volume. +4. **Rotate all five credentials** during migration — relocating a credential + without rotating it leaves the previously-exposed value valid. Regenerate the + gateway token, Telegram bot token, and API keys; update the mounted config. +5. Verify the three isolation gates (`docker inspect`) and the exec-approval gate + (`approvals get`) in-container before cutover. +6. Confirm tailnet reachability via the authenticated proxy; confirm no public + ingress. +7. Decommission the bare-metal service once the container is confirmed healthy. + +### Interim host hardening (production, independent of containerization) + +systemd unit directives deliver substantial containment without a container and +apply to the existing service now: `ProtectHome`, `ReadOnlyPaths`, +`NoNewPrivileges`, `SystemCallFilter`. A candidate for the production host in +advance of, or instead of, the container migration. + +### Residual risk + +A container shares the host kernel: this is containment, not sandboxing. It +reduces blast radius but does not make arbitrary execution safe. The B5 approval +gate remains the control for the model-in-the-loop boundary; packaging does not +address a model choosing to execute hijacked instructions. + +--- + +# Responsibility by boundary + +| Boundary | Managed host | Self-hosted gateway | +|---|---|---| +| B1 · User ↔ Host | Vendor UI mediates intent | Operator owns the channel and its sender allowlist | +| B2 · Host ↔ Server | Vendor ships and patches connectors | Operator vets, installs, patches; patch latency is an operator SLA | +| B3 · Server ↔ Downstream | Vendor holds tokens (OAuth, platform-side) | Operator holds tokens on-disk; container isolation bounds access, runtime injection pending | +| B4 · Server ↔ Server | Vendor isolates | Operator owns isolation; validated on staging, promotion pending | +| B5 · Model ↔ Tool output | Behavioral controls only | Behavioral controls plus an operator-configured, fail-closed exec-approval gate | + +A managed host delegates custody, patching, and isolation to the vendor; a +self-hosted gateway returns all three to the operator. Neither is inherently more +secure — the managed host is bounded by vendor controls, the self-hosted gateway +by operator discipline. B5 is invariant across both: no hosting model resolves the +model-in-the-loop boundary, consistent with its identification in the threat model +as the boundary without a classic analogue. + +## Scope and limitations + +- Two setups, single operator, single point in time: a workstation-and-homelab + pattern, not an enterprise fleet. Fleet-scale controls — policy-based server + allowlisting, centralized tool-call audit, per-agent identity — are addressed in + the threat model's mitigations, not here. +- Part 1's OAuth model delegates B3 to the platform: a benefit and a dependency. +- Part 2's container work is validated on staging; production promotion (including + credential rotation) is documented as steps, not executed. The preventive layer + (host hardening, exec-approval gate) is implemented and evidenced on the + production host. +- B5 has no configuration fix in either model. The behavioral and approval + controls are mitigations, not a claim that prompt injection is solved.