ssh sidecar: default to multiplexing, as a default and not an override
Lint / actionlint (push) Successful in 15s
Lint / hadolint (push) Successful in 16s

A target whose ~/.ssh/config entry never mentioned ControlMaster got no
multiplexing from the sidecar (only ControlPath was supplied), so every ssh call
opened a fresh TCP connection. On 2026-08-25 that produced ~12 connections to
one host in 15 min and a fail2ban block that looked like an outage — the tell
being that HTTPS to the same estate stayed healthy.

The correctness of this depends entirely on WHERE the block goes. ssh_config is
first-value-wins:

  ControlPath   before the Include -> override (the user's value points at
                read-only ~/.ssh and cannot work in the container)
  ControlMaster after  the Include -> default  (an explicit per-host
                'ControlMaster no' must keep winning)

Force what is broken, default what is merely absent. The first draft put both in
the leading block and would have silently overridden an explicit 'no'.

Verified with ssh -G rather than from the man page, including the counterfactual:
under the shipped layout an explicit 'no' resolves to controlmaster false while a
silent host resolves to auto; under the rejected layout the 'no' host flips to
auto. So the test discriminates position, not presence. Plus a sandbox render of
the real script, bash -n, and shellcheck -S error (the v1.8.7 gate) clean.

Effect measured on 41 real host aliases: 22 silent entries gain auto+10m, 0
overridden. Note the fleet's one deliberate opt-out is written as absence plus a
comment ('# No ControlMaster — VPN means direct route'), which ssh cannot
distinguish from no opinion; that host now multiplexes, which its own comment
says is unnecessary rather than harmful.

Skill documents the sidecar-vs-~/.ssh trap (the failure misleads: read-only
ControlPath makes multiplexing look impossible rather than misconfigured) and
the stale-master recovery, ssh -O check / -O exit.
This commit is contained in:
pi
2026-08-25 23:09:46 +02:00
parent ebd0de0be2
commit 657b1ad856
3 changed files with 130 additions and 0 deletions
+61
View File
@@ -11,6 +11,67 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
---
## Unreleased
### Added
- **The SSH sidecar now defaults to connection multiplexing, without overriding
anyone's explicit choice.** `~/.ssh-local/config` already forced `ControlPath`
into the writable sidecar dir, but nothing supplied `ControlMaster` for targets
coming from the user's own bind-mounted `~/.ssh/config`. An entry that never
mentioned it therefore opened a **fresh TCP connection per `ssh` call** — and
an agent doing a dozen calls in a few minutes is exactly the traffic shape that
trips fail2ban or a CGNAT flow-table cap. Observed 2026-08-25 on this fleet:
~12 connections to one host in 15 minutes, after which port 22 stopped
answering while HTTPS to the same estate stayed healthy in 0.44 s (that
asymmetry is the tell for rate-limiting rather than an outage).
**The fix is where the block sits, not what it says.** `ssh_config` is
first-value-wins, so position encodes intent, and the two settings need
opposite treatment:
| Setting | Position | Meaning | Why |
|---|---|---|---|
| `ControlPath` | **before** `Include ~/.ssh/config` | override | the user's value points at read-only `~/.ssh`; it cannot work here, so it must lose |
| `ControlMaster auto` + `ControlPersist 10m` | **after** the `Include` | default | an explicit per-host `ControlMaster no` must keep winning; we only supply an opinion where the user expressed none |
*Force what is broken, default what is merely absent.* The first draft of this
put both in the leading block, which would have silently overridden an explicit
`ControlMaster no` — the counterfactual is in the test below.
Verified with `ssh -G` (the resolved-config oracle) rather than by reading the
man page, against a fixture with one host set to `no`, one silent, one set to
`auto`: the explicit `no` resolves to `controlmaster false` **and** still gets
the writable `ControlPath`, the silent host resolves to `auto`, and the same
fixture under the rejected layout flips the `no` host to `auto` — so the test
discriminates the *position*, not merely the presence of the block. Then
end-to-end: the real script rendered in a sandbox `HOME`, block last, `bash -n`
clean, `shellcheck -S error` clean (the gate added in v1.8.7).
Measured effect on the author's own config (41 host aliases): **22 were silent
about `ControlMaster` and gain `auto` + 10 m persist; 0 are overridden**, since
the fleet contains no explicit `no`. Worth noting *how* the one deliberate
exception is written — `proxmox002-vpn` carries `# No ControlMaster — VPN means
direct route, no CGNAT flow cap`, i.e. the intent is expressed as **absence
plus a comment**, which `ssh` cannot distinguish from "no opinion". That host
does now get multiplexing; its comment says multiplexing is *unnecessary*
there, not harmful. Anything that must stay unmultiplexed needs a literal
`ControlMaster no`.
Why the ordering matters beyond this one config: `~/.ssh/config` is
**per-machine**, differs across the fleet, and future machines' versions do not
exist yet to be audited. A default-not-override design is correct without
needing to inspect any of them.
`ControlPersist` is deliberately short (10 m idle, and each new session resets
the idle timer — long enough to collapse an agent's burst, short enough that an
abandoned socket ages out). A per-host entry that sets its own value keeps it:
hosts already specifying `ControlPersist 4h` still resolve to 4 h. The known
cost of multiplexing is the **stale master** — socket present, daemon gone,
after a suspend or network change — which makes every later `ssh` to that host
hang; recovery is `ssh -F ~/.ssh-local/config -O exit <host>`, now documented
in the `pi-devbox-environment` skill along with `-O check`.
## v1.8.7 — 2026-08-25
Patch release, and the fastest turnaround in the series (~9 h after v1.8.6) for