Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 55 additions & 31 deletions docs/decisions/052-breakglass-ssh-and-key-vault.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
# 052. Breakglass terminal, two SSH paths, and a zero-knowledge key vault

> **Type:** decision
> **Status:** Proposed (2026-07-05) — resolves the key-posture fork in
> **Status:** Accepted (2026-07-05) — resolves the key-posture fork in
> [`discussions/desktop-breakglass-ssh.md`](../discussions/desktop-breakglass-ssh.md)
> §7: the director chose **B2 (zero-knowledge encrypted vault)**. Records the
> terminal backend, the two terminal paths, the cross-device sync model, and — the
> load-bearing part — an **amendment to forbidden-pattern #15** so a hub that holds
> only blind ciphertext is permitted.
> load-bearing part — an **amendment to forbidden-pattern #15** (now in effect,
> [`spine/forbidden-patterns.md`](../spine/forbidden-patterns.md) updated) so a hub
> that holds only blind ciphertext is permitted. **D-6 (hub PTY-relay + A2A-relay
> auth) is DEFERRED** by director direction (2026-07-05); the first build is the
> **vault key-sync service** (D-4) — the hub as a blind cross-device blob store.
> **Audience:** principal · contributors · maintainers
> **Last verified vs code:** v1.0.820

**TL;DR.** The breakglass SSH terminal is **xterm.js + a Tauri Rust `russh`
transport** (not `libghostty` — wrong layer), mirroring mobile. There are **two
paths**: to a **host-runner-managed host** the terminal is a **hub-brokered
interactive PTY — no SSH, no keys, fully audited** (the safe primary path); to a
**personal bare-SSH host** it is direct `russh` with client-held keys. Host and
connection **metadata** (non-secret) **syncs** through the hub across the director's
devices (extending `ssh_hint`). Private keys sync via a **zero-knowledge vault
(B2)**: the hub stores key material **encrypted client-side under a key it never
sees**, wrapped per enrolled device — a blind ciphertext blob store. This requires
**personal bare-SSH host** it is direct `russh` with client-held keys. The
**whole personal setup — connections (host/user/jump/tmux) *and* keys/passphrases/
passwords — syncs across devices as one bundle** via a **zero-knowledge vault
(B2)**: the hub stores it **encrypted client-side under a key it never sees**,
wrapped per enrolled device, plus a **recovery envelope** (the vault key wrapped
under a director-escrowed recovery code) so losing every device is survivable — a
blind ciphertext blob store throughout. This requires
**amending forbidden-pattern #15**: the rule forbids the hub holding *usable* SSH
secrets; a vault the hub cannot decrypt or use preserves the rule's intent (the hub
can never authenticate as the user) and is permitted. Two security fixes travel with
Expand Down Expand Up @@ -66,18 +71,22 @@ reconciles that by amending the rule to match its intent.
- **Personal bare-SSH host (no host-runner) → direct `russh`.** Client-held keys,
mirroring mobile. The hub's role is limited to the **non-secret** `ssh_hint`.

- **D-3 — Non-secret metadata syncs; plaintext keys never do.** Host/connection
bookmarks (hostname, port, user, key *fingerprint*, jump host, tmux path) are
non-secret and sync through the hub as **personal-scoped** metadata across the
director's devices, extending the existing `ssh_hint` concept to personal
connections. No private key, passphrase, or password ever transits or persists on
the hub in a form the hub can read. (This also fixes the current smell of
serializing `proxyPassword` into unencrypted prefs, `connection_provider.dart:138`.)

- **D-4 — Private keys share via a zero-knowledge vault (B2).** The hub stores key
material (private keys, passphrases, connection passwords) **encrypted client-side
under a vault key the hub never sees**, and serves it as an opaque, versioned blob
— a **blind blob store**. The crypto shape:
- **D-3 — The vault carries the *whole* connection bundle, hub-blind (revised
2026-07-05).** So a new device is directly reusable, the vault seals the complete
personal setup — **connection bookmarks (host, port, username, auth method, jump
host, tmux path) *and* the keys/passphrases/passwords** — as one client-encrypted
bundle. Personal connection metadata is therefore carried **inside** the
zero-knowledge vault (the hub never sees it, not even hostnames), **not** as
hub-visible personal `ssh_hint`. The team-host `ssh_hint` primitive keeps its
original, separate purpose (binding/displaying a *team* host row, non-secret,
hub-visible). This supersedes the earlier "non-secret metadata syncs via
`ssh_hint`" framing and also retires the smell of serializing `proxyPassword` into
unencrypted prefs (`connection_provider.dart:138`).

- **D-4 — The connection+key bundle shares via a zero-knowledge vault (B2).** The
hub stores the sealed bundle (D-3: connections + private keys + passphrases +
passwords) **encrypted client-side under a vault key the hub never sees**, and
serves it as an opaque, versioned blob — a **blind blob store**. The crypto shape:
- Each secret is sealed with **authenticated encryption** (an AEAD such as
XChaCha20-Poly1305 / AES-256-GCM) under a symmetric **vault key**.
- The vault key is **wrapped per enrolled device** (envelope encryption to each
Expand All @@ -96,6 +105,17 @@ reconciles that by amending the rule to match its intent.
sealing/opening happens on clients (Rust RustCrypto / Dart `cryptography`).
- The vault **replaces** the cleartext `DataPortService` export as the
cross-device mechanism.
- **Recovery escrow (decided 2026-07-05).** The vault key is *also* wrapped under
a director-held **recovery key** (a high-entropy recovery code, or a passphrase
via Argon2id), stored on the hub as an opaque **recovery envelope** — so a
principal who has lost **every** enrolled device can still recover. The director
**escrows the recovery code offline** (self-escrow, à la a BIP-39 phrase); the
hub holds only the wrapped envelope, never the recovery key, so zero-knowledge
holds. On a re-key (device revocation) the client re-wraps the recovery envelope
too. Operator/org escrow and Shamir-split recovery are possible extensions on the
same hub slot (one opaque envelope today; a keyed table later). Without a
recovery envelope set, losing all devices means losing the vault — the client
prompts the director to create one at first setup.

- **D-5 — Amend forbidden-pattern #15 (the reconciliation).** The rule's intent is
that **the hub must never be able to authenticate as the user** — never hold a
Expand All @@ -106,13 +126,15 @@ reconciles that by amending the rule to match its intent.
passwords may be stored **only** as client-side-encrypted, zero-knowledge vault
ciphertext the hub cannot decrypt (the hub never holds the vault key or any
plaintext); everything else stays in device secure storage."* The vault is
key-material-scale, not bulk bytes, so forbidden-pattern #1 is not engaged. On
**acceptance** of this ADR, `docs/spine/forbidden-patterns.md` #15 (and the
blueprint §7 reference) is updated to this text; until then #15 stands and no
key-storing code lands.

- **D-6 — Hub workstream: authenticate the relay + a streaming PTY channel.** Path 1
(D-2) needs a hub PTY-relay that does not exist today. Build: (a) a host-runner
key-material-scale, not bulk bytes, so forbidden-pattern #1 is not engaged.
**Applied 2026-07-05 on acceptance:** `docs/spine/forbidden-patterns.md` #15 now
carries this text; the zero-knowledge vault store (D-4) is thereby permitted.

- **D-6 — Hub workstream: authenticate the relay + a streaming PTY channel.**
**DEFERRED (2026-07-05, director direction)** — tracked, not scheduled; the
managed-host PTY path (D-2 Path 1) waits on it, and personal direct-SSH (Path 2)
plus the vault sync (D-4) ship first. Path 1
needs a hub PTY-relay that does not exist today. Build: (a) a host-runner
"open interactive shell PTY" capability, and (b) an **authenticated, streaming**
channel — either upgrade the A2A tunnel's `Kind` to carry a bidirectional PTY
stream, or add a dedicated authed `…/hosts/{host}/shell` (WebSocket) serviced over
Expand All @@ -130,10 +152,12 @@ reconciles that by amending the rule to match its intent.
- The A2A relay gets authenticated, closing a pre-existing gap.

**Harder / cost:**
- Real client-side crypto and a **device-enrollment / re-key** flow (trust a new
device, revoke one) on **both** clients — the highest-risk new surface; get the
key-wrapping and recovery story right (lose all devices ⇒ lost vault, unless a
recovery secret is escrowed by the director).
- Real client-side crypto and a **device-enrollment / re-key / recovery** flow
(trust a new device, revoke one, re-wrap the recovery envelope on re-key) on
**both** clients — the highest-risk new surface. Recovery is now designed (D-4
escrow), so "lose all devices ⇒ lost vault" is survivable *when the director set a
recovery code*; the client must make first-setup recovery prominent so it isn't
skipped.
- A new **hub PTY-relay + streaming-auth** workstream (D-6), plus mobile joining the
vault — this spans hub, desktop, and mobile.
- Amending a foundational axiom (D-5); done deliberately and narrowly.
Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ next sequential number — don't reserve, don't skip.
| [049](049-multi-agent-collaboration-via-github.md) | Multi-agent collaboration via GitHub — delegate any dev work to heterogeneous agent CLIs on different hosts; maintainer/builder roles, ticket lifecycle + tiers, two-axis identity (git-config handle vs shared account), `holds:<resource>` baton, verify-before-merge; vendor-agnostic, general (i18n was the pilot) | Accepted 2026-06-11 | — |
| [050](050-desktop-workbench-delivery-model.md) | Desktop research workbench delivery model — a local-first, hub-served **web-tech** app (second client on the client-agnostic API), not a wide Flutter layout; two-halves split (portable control plane vs component-heavy research workbench); operative rule build·embed·integrate·interop; BUILD the fleet-native surfaces (headline = multi-run comparison wall), EMBED/INTEGRATE the rest | Accepted 2026-07-04 · Amended 2026-07-05 | — |
| [051](051-desktop-client-stack.md) | Desktop client stack — **Tauri v2** shell (Win/Mac/Linux + plain browser) hosting **React + TypeScript** (chosen by the embed axis: the workbench's tldraw/BlockNote/Monaco/Rerun/Viser components are React-first); TanStack Query over REST+cache; the Rust core proxies SSE (auth header) + keychains the token; a shared **DTCG `tokens.json` → Style Dictionary → {Dart, CSS}** pipeline makes ADR-047 tokens load-bearing across both clients | Proposed 2026-07-05 | (implements [050](050-desktop-workbench-delivery-model.md)) |
| [052](052-breakglass-ssh-and-key-vault.md) | Breakglass SSH terminal — **xterm.js + Tauri `russh`** (not libghostty); **two paths** (managed host = hub-brokered keyless audited PTY; personal = direct SSH + client keys); non-secret host/connection metadata syncs; private keys share via a **zero-knowledge vault** (hub stores blind ciphertext it can't decrypt) → **amends forbidden-pattern #15**; authenticate the A2A relay + retire the cleartext backup | Proposed 2026-07-05 | (amends [forbidden-patterns.md](../spine/forbidden-patterns.md) #15; builds on [051](051-desktop-client-stack.md)) |
| [052](052-breakglass-ssh-and-key-vault.md) | Breakglass SSH terminal — **xterm.js + Tauri `russh`** (not libghostty); **two paths** (managed host = hub-brokered keyless audited PTY; personal = direct SSH + client keys); non-secret host/connection metadata syncs; private keys share via a **zero-knowledge vault** (hub stores blind ciphertext it can't decrypt) → **amends forbidden-pattern #15**; authenticate the A2A relay + retire the cleartext backup | Accepted 2026-07-05 | (amends [forbidden-patterns.md](../spine/forbidden-patterns.md) #15; builds on [051](051-desktop-client-stack.md)) |

---

Expand Down
15 changes: 12 additions & 3 deletions docs/spine/forbidden-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,18 @@ see [`information-architecture.md §8`](information-architecture.md).
binary presence and version only. Mixing the two reintroduces
provider-specific logic into the deputy layer.

15. **Hub storing SSH credentials to help with Enter-pane.** Only
non-secret `ssh_hint_json` (hostname, port, username) may live
in the hub. Secrets stay in the phone's secure storage.
15. **Hub storing SSH secrets it can read or use.** Non-secret
`ssh_hint_json` (hostname, port, username) may live in the hub.
Private keys, passphrases, and passwords may be stored **only**
as client-side-encrypted, **zero-knowledge vault** ciphertext the
hub cannot decrypt — the hub never holds the vault key or any
plaintext (it is a blind blob store); everything else stays in
device secure storage. *(Amended 2026-07-05 by
[ADR-052](../decisions/052-breakglass-ssh-and-key-vault.md): the
rule's intent is that the hub can never authenticate as the user,
which opaque ciphertext preserves. Was: "Only non-secret
`ssh_hint_json` … may live in the hub. Secrets stay in the phone's
secure storage.")*

---

Expand Down
Loading
Loading