# PRD 0009: Remove ssh-gate and bottle.ssh - **Status:** Active - **Author:** didericis - **Created:** 2026-05-13 ## Summary Delete the ssh-gate sidecar and the `bottle.ssh` manifest field. Git-gate (PRD 0008) covers every current SSH use case in bot-bottle: each declared upstream gets a per-bottle gate with gitleaks scanning, an `insteadOf` rewrite that captures push / fetch / clone / pull / ls-remote, and credential isolation from the agent. ssh-gate is now redundant L4 forwarding with no gating value, and the only remaining users of `bottle.ssh` were git remotes that git-gate handles better. ## Problem PRD 0007 introduced ssh-gate as an L4 SSH forwarder so an agent could reach declared SSH upstreams without a default route off-box. At the time that meant git. With PRD 0008 every git upstream now flows through git-gate, which: - Holds the upstream credential in the gate, not in the agent - Runs gitleaks against every incoming ref - Refreshes from upstream on every fetch (fail-closed when the upstream is unreachable) - Rewrites the agent's URLs via `insteadOf`, so push, fetch, clone, pull, and ls-remote all route through the gate ssh-gate does none of those — it's transport-only — and offers no benefit over git-gate for git, which is the only upstream class currently used. Carrying it means a dead manifest field, a redundant sidecar lifecycle, a shadow-route validator between `bottle.ssh` and `bottle.git`, and a third place to keep an SSH identity in sync. The project's stated goal is minimum credentials and minimum egress; the simpler answer is to drop the unused path. ## Goals / Success Criteria - `bottle.ssh` is no longer a valid manifest field. Manifests carrying it parse-fail with an error message pointing at `bottle.git`. - `SSHGate` / `DockerSSHGate`, `provision_ssh`, and the socat sidecar lifecycle are gone from the codebase. - Unit + integration tests pass without the ssh-gate suites; no test references `SshEntry` or `bottle.ssh`. - The y/N preflight no longer mentions the ssh sidecar; the README's architecture diagram drops the socat box; the example manifest drops `ssh:`. - PRD 0007 carries a "Superseded by PRD 0009" header so the history of intent stays in the tree. ## Non-goals - Removing pipelock or git-gate. Both keep their current roles. - Removing the SSH egress *capability* in general — git-gate uses SSH inside the gate to reach the real upstream. - Building a generic L4 egress proxy as a replacement. If non-git SSH ever returns, design a fresh gate for the use case; don't resurrect ssh-gate's L4-only design. - Preserving a "ssh: []" compatibility shim. If the field is dead it should error, not silently parse. - Auto-migrating user manifests. The replacement (`bottle.git`) needs a `Name` and full upstream URL that `bottle.ssh` does not carry; a hard error with a migration hint is cleaner than a partial rewrite. ## Scope ### In scope - **Manifest.** Delete `SshEntry`, `Bottle.ssh`, and `_validate_no_shadow_route`. Add an explicit branch in `Bottle.from_dict` that dies on a `ssh` key with a one-line "move this to `bottle.git` (see PRD 0008)" hint. - **Sidecar.** Delete `bot_bottle/ssh_gate.py` and `bot_bottle/backend/docker/ssh_gate.py`. Drop the socat image build path. - **Provisioner.** Delete `bot_bottle/backend/docker/provision/ssh.py` and its `~/.ssh/config` render. - **Docker backend wiring.** Drop `DockerSSHGate` from `backend.py`; drop its start / stop from `launch.py`'s `ExitStack`; drop the plan field from `bottle_plan.py`. - **Pipelock interaction.** Drop the SSH-derived branch from pipelock's `ssrf.ip_allowlist` build. With no `bottle.ssh` there is no per-upstream IP carve-out to render; git-gate has its own egress network and pulls in upstream resolution via `ExtraHosts` plus DNS. - **Tests.** Delete the ssh-gate unit + integration suites, the ssh fixtures in `tests/fixtures.py`, and the shadow-route assertions in `test_manifest_git.py`. Adjust any tests that asserted on a "git + ssh" combined manifest to be git-only. - **README.** Drop the socat / ssh image box from the architecture diagram and its bullet; drop `ssh:` from the manifest example. - **Example manifest.** Drop `ssh:` from `bot-bottle.example.json`. - **PRD 0007.** Add a `Status: Superseded by PRD 0009` header at the top of the document. Do not delete the file; the history of intent matters for the audit trail. - **Migration note.** A short paragraph in the README explaining the swap (drop the `ssh` entry, add a `git` entry pointing at the same upstream with a `Name` and full URL). ### Out of scope - A generic non-git SSH gate. If/when the use case returns, design a fresh sidecar with its own threat model. - Migrating user manifests automatically. Parse-fail with a clear error is the path. - Reworking git-gate to absorb non-git protocols. git-gate remains git-only. ## Proposed Design Most of the work is deletion. The substantive decisions are at the seams between ssh-gate and the rest of the system: 1. **Manifest parse-error shape.** `Bottle.from_dict` adds an explicit `if "ssh" in d:` branch that dies with a hint naming `bottle.git` and PRD 0008. Better than silent ignore — the migration is visible and one-shot — and better than a warning, which agents wouldn't see anyway. 2. **Pipelock allowlist build.** Today pipelock pulls SSH upstream IPs into its `ssrf.ip_allowlist` so ssh-gate can dial out. With ssh-gate gone, that branch goes away; verify nothing else in the pipelock render touches `bottle.ssh`. 3. **Plan rendering.** `bottle_plan.py` and the y/N preflight currently list the socat sidecar. Remove the field and any rendering branch; the dry-run output simplifies. 4. **PRD 0007 marker.** A header line at the top, not a delete. The PRD's rationale (why ssh-gate was built) is still useful context when reading PRD 0008 and PRD 0009. ### Existing code touched - `bot_bottle/manifest.py` — delete `SshEntry`, `Bottle.ssh`, `_validate_no_shadow_route`; add the parse-fail branch. - `bot_bottle/ssh_gate.py` — delete. - `bot_bottle/backend/docker/ssh_gate.py` — delete. - `bot_bottle/backend/docker/provision/ssh.py` — delete. - `bot_bottle/backend/docker/backend.py` — drop `DockerSSHGate` instantiation. - `bot_bottle/backend/docker/launch.py` — drop the ssh-gate start / stop from the `ExitStack`. - `bot_bottle/backend/docker/bottle_plan.py` — drop the ssh-gate plan field. - `bot_bottle/pipelock.py` — drop the `bottle.ssh`-derived branch in the allowlist render. - `tests/unit/test_ssh_gate.py` — delete. - `tests/integration/` — delete any ssh-gate-specific tests. - `tests/unit/test_manifest_git.py` — drop the shadow-route assertions. - `tests/fixtures.py` — drop `fixture_with_ssh` and its dict helper. - `README.md` — drop the socat image box from the diagram and the matching bullet; drop `ssh:` from the manifest example. - `bot-bottle.example.json` — drop the `ssh` field. - `docs/prds/0007-ssh-egress-gate.md` — add a `Status: Superseded by PRD 0009` header at the top. ### Data model changes - `SshEntry` removed. - `Bottle.ssh: tuple[SshEntry, ...] = ()` removed. - `_validate_no_shadow_route` removed. ### External dependencies Nothing added. The `alpine/socat` image is no longer pulled by bot-bottle; the cleanup of any existing local image is the user's choice (a single `docker image rm` if they care). ## Future work If a non-git SSH use case returns (deployment, rsync, remote management), build it as its own gate with its own threat model. Don't resurrect ssh-gate's L4-only design. The git-gate pattern (gate holds credentials, agent gets a rewritten URL, gate makes the upstream connection) is the template. ## Open questions - **Error vs migrate on legacy `bottle.ssh`.** Default: hard error with a one-line "move this to `bottle.git`" hint. Migrating in-place is brittle — the entry has no `Name` field and no upstream URL, so we'd be inventing both. - **One PR or two.** The removal touches enough files that a single big PR may be hard to review; splitting into "deprecate manifest field" and "remove sidecar code" reads cleaner, but the field can't be removed cleanly while the sidecar still references it. Default: single PR, deletion commit per layer (manifest, sidecar, provisioner, tests, docs). ## References - PRD 0007: SSH egress gate — the design being superseded. - PRD 0008: Git gate — the design that subsumes ssh-gate's only current use case.