Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Bound what an unauthenticated SSH client can spend

78cd3437a597b0155682d2e8d5129bf7cdeac85d · Keenan Nemetz · 12d ago · parent b1499c72

Bound what an unauthenticated SSH client can spend

The SSH listener accepted every connection, gave each one a goroutine, and
started a process per session channel. A client that opened a socket and
never finished the handshake held all three for as long as it liked, and a
client that opened channels held a goroutine each. The host key was
hardened by `vole serve` on the way in, but the SSH package on its own
would happily load a key the whole machine could read.

Four bounds, all enforced before the expensive thing happens:

  - A handshake deadline, cleared once the handshake completes so a clone
    or a push is not cut. `ssh_handshake_timeout_seconds`, default 20.
  - `ssh_max_connections` (default 64) tokens, one per open connection. Past
    the cap the connection is closed before the handshake, the goroutine or
    the process, and the accept loop keeps running so the listener cannot be
    wedged. The "listener is full" warning is throttled to once per 10s so a
    flood cannot become a disk problem instead.
  - Eight session channels per connection, rejected at the channel layer
    rather than by spawning a goroutine per request.
  - `ssh_max_git_processes` (default 16) across every connection, covering
    upload-pack, receive-pack and vole-change-merge. A command waits up to
    30s for a slot and is then told to retry, rather than parked forever.

Payloads are bounded too: an exec line over 4 KiB is refused before it is
parsed, and a vole-runner request line over 1 MiB ends the session with an
error. The runner bound is a per-line reader, so json.Decoder's buffering
cannot hide an oversized request and a long-lived runner is not limited in
total.

Loading an existing host key now tightens it to 0600 and warns, and a mode
that cannot be tightened fails startup, so the package is safe on its own
rather than only behind `vole serve`.

Tests hold a connection without handshaking (it is dropped and its slot
returns), flood one connection with channels (the ninth is rejected and
closing one releases a slot), send an oversized exec line and an oversized
runner request (both refused), saturate the process pool (a bounded wait,
then a refusal), and load a 0644 host key (tightened).

docs/operations.md +14−4

@@ -188,10 +188,20 @@ the linked sections explain each in full.
188 188 The same goes for `open_registration`, which is off by default.
189 189 - **Conservative limits.** The defaults are chosen for a small instance:
190 190 `git_http_max_concurrent = 16`, `git_http_max_per_client = 8`,
191 `max_blob_bytes = 100 MiB`, `receive_max_bytes = 1 GiB`, and a 600 s budget for
192 one clone. Raise them with measurements, not with optimism; the read path is
193 anonymous, so a limit that is too low is a slow clone and a limit that is
194 missing is an outage.
191 `ssh_max_connections = 64`, `ssh_max_git_processes = 16`,
192 `ssh_handshake_timeout_seconds = 20`, `max_blob_bytes = 100 MiB`,
193 `receive_max_bytes = 1 GiB`, and a 600 s budget for one clone. Raise them with
194 measurements, not with optimism; the read path is anonymous, so a limit that is
195 too low is a slow clone and a limit that is missing is an outage.
196 - **Bounded before authentication.** The SSH listener refuses connections past
197 `ssh_max_connections` before the handshake and drops a connection that has not
198 finished its handshake in `ssh_handshake_timeout_seconds`, so an anonymous
199 client cannot hold a file descriptor by opening a socket and saying nothing.
200 After the handshake one connection may hold eight session channels, and the
201 git commands they run share `ssh_max_git_processes`; an exec line longer than
202 4 KiB and a runner request line longer than 1 MiB are refused. Address-based
203 backoff for repeated bad keys is a quota concern, not a transport bound, so it
204 belongs with the abuse controls rather than here.
195 205 - **Audited administration.** Remote administration uses the same SSH listener
196 206 and is restricted to instance admins. Ship the audit log off the host: local
197 207 append-only is not tamper-evident. See [Audit log](#audit-log) and