Files
bot-bottle/docs/prds/0009-remove-ssh-gate.md

206 lines
8.4 KiB
Markdown

# 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.
- **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.