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.

#53

Bound what an unauthenticated SSH client can spend

merged · by keenan 13d ago · merged into main at 78cd3437 by keenan 13d ago

keenan opened this 13d ago

Closes #5.

What was wrong

Serve accepted every connection and started a goroutine and a process for each session channel, with no handshake deadline. A client that opened a socket and said nothing held a connection slot, a goroutine and a file descriptor indefinitely; a client that opened channels held a goroutine each. The host key was only hardened by vole serve on the way in, so the SSH package on its own would load a 0644 key.

What this does

  • Handshake deadline (ssh_handshake_timeout_seconds, default 20) set on the raw connection before NewServerConn and cleared afterwards, so a clone or a push is not cut. Test proves a session that outlives the deadline still works.
  • Connection cap (ssh_max_connections, default 64): one token per open connection; past it the connection is closed before any handshake, goroutine or process is spent on it, and the accept loop keeps running. The full-listener warning is throttled to once per 10 s so a flood cannot fill the disk.
  • Channel cap: eight session channels per connection, taken from a per-connection token channel, so a flood is rejected at the channel layer.
  • Process cap (ssh_max_git_processes, default 16) across all connections, covering git-upload-pack, git-receive-pack and vole-change-merge. A command waits up to 30 s for a slot, then is refused with a retry message.
  • Payload bounds: exec lines over 4 KiB are refused before parsing; 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 buffering cannot hide a large request and a long-lived runner is not limited in total.
  • Host key mode: loading an existing key tightens it to 0600 and warns; a mode that cannot be tightened fails startup. vole serve still tightens first via TightenSecrets, so the warning only appears when something changed the mode after startup.

Per-address backoff for repeated unknown keys is intentionally not here: it is a quota rather than a transport bound, and belongs with the abuse controls, as the issue itself says (“Consider…”). Nothing added requires tuning to be safe by default.

Verification

  • gofmt, go vet ./..., go test -race ./... clean.
  • ./vole workflow check and ./vole workflow run verify pass.

Tests (server/ssh/limits_test.go)

  • TestHandshakeSlowlorisIsCutOff: with max_connections = 1, an idle connection holds the only slot, a second connection is refused without an answer, the idle one is dropped at its deadline, and its slot comes back for a real client.
  • TestChannelFloodIsRejected: eight channels accepted, the ninth rejected, and closing one releases a slot.
  • TestOversizedCommandIsRefused, TestRunnerRequestLineIsBounded, TestLineLimitReaderIsPerLine.
  • TestGitProcessCapRefusesAfterAWait: a full pool refuses after the bounded wait, and a release frees a slot.
  • TestLooseHostKeyModeIsTightened and TestHandshakeDeadlineIsClearedForTheSession.

Docs: the three settings are in docs/configuration.md, summarised in docs/operations.md under “Bounded before authentication”, and shown on the status page’s read-only configuration view.

Discussion (0)

No discussion yet.