SSH transport for git: serve clone/fetch and push over ssh:// #1

Closed
opened 2026-09-02 23:26:59 +00:00 by crueber · 0 comments
Owner

Motivation

HTTP is walhub's only git transport today. Every mainstream git host also serves git over SSH, and many CI robots and desktop tools expect it (git clone git@host:owner/repo.git). We need both standard directions:

  • out (git-upload-pack): clone and fetch
  • in (git-receive-pack): push

Prior art (Gitea / Forgejo)

Both Gitea and Forgejo do this well in Go and their shape is a strong hint:

  • modules/ssh/ssh.go: a golang.org/x/crypto/ssh server; the session handler receives the command string (git-upload-pack '/owner/repo.git'), parses it with a shell-word splitter, and dispatches to an internal serv path.
  • Verbs accepted: git-upload-pack, git-receive-pack, git-upload-archive; interactive shells/PTY requests are refused.
  • The client's GIT_PROTOCOL env (SSH SendEnv) is honored and forwarded so protocol v2 works over SSH.
  • Public-key auth against stored keys; each key maps to a principal with write/admin flags.

Design sketch for walhub

  1. New package internal/sshd built on golang.org/x/crypto/ssh (needs a Law 1 amendment in AGENTS.md + the doc decisions ledger; hand-rolling SSH is a non-starter). It defines a small consumer-side Transport interface that internal/server implements, so the SSH path reuses the exact same git pipeline as HTTP (sync → upload-pack; parse → ingest → connectivity → publish → report).
  2. Reuse, not duplication: extract the transport-agnostic cores of uploadPack and receivePackLocal (they already only write bytes) and have both HTTP handlers and the SSH dispatcher call them.
  3. Config (mirrors the static-token pattern):
    [server.ssh]
    listen = ""            # e.g. ":2222"; empty = disabled (default)
    host_key = ""          # path to an ed25519 host key; auto-generated under <data-dir>/ssh/ when empty
    host_key_env = ""      # or an env var holding the key
    [[server.ssh.keys]]
    principal = "ada"
    key = "ssh-ed25519 AAAA... ada@laptop"   # or key_env
    write = true
    admin = false
    
  4. Auth: public keys from config, parsed with ssh.ParseAuthorizedKey; the matched key's principal gets write/admin from its entry, and receive-pack requires write (same rule as the HTTP route). Keys are a credential class of their own, like static tokens.
  5. Security: no PTY/shell/subsystem (sftp) ever; the command is strictly verb '<owner/repo[.git]>' with option-prefixed argv rejected (the classic SSH option-injection guard); repo paths go through ValidateRepoPath; placement/drain gates apply before any git work.
  6. Protocol: GIT_PROTOCOL=version=2 passthrough for v2 negotiation.

Testing

  • command parsing table (verbs, quoting, .git suffix, injection attempts)
  • auth matrix (known key → principal + flags; unknown key refused; write flag enforced on push)
  • end-to-end: real git clone and git push over ssh://127.0.0.1:<random> against the in-process server with a generated client key and GIT_SSH_COMMAND (both directions, protocol v2)

Out of scope (for this first cut)

  • git-upload-archive and LFS-over-SSH (git-lfs-authenticate)
  • sftp subsystem
  • SSH certificates and CA-based auth
## Motivation HTTP is walhub's only git transport today. Every mainstream git host also serves git over SSH, and many CI robots and desktop tools expect it (`git clone git@host:owner/repo.git`). We need both standard directions: - **out** (`git-upload-pack`): clone and fetch - **in** (`git-receive-pack`): push ## Prior art (Gitea / Forgejo) Both Gitea and Forgejo do this well in Go and their shape is a strong hint: - `modules/ssh/ssh.go`: a `golang.org/x/crypto/ssh` server; the session handler receives the command string (`git-upload-pack '/owner/repo.git'`), parses it with a shell-word splitter, and dispatches to an internal `serv` path. - Verbs accepted: `git-upload-pack`, `git-receive-pack`, `git-upload-archive`; interactive shells/PTY requests are refused. - The client's `GIT_PROTOCOL` env (SSH `SendEnv`) is honored and forwarded so protocol v2 works over SSH. - Public-key auth against stored keys; each key maps to a principal with write/admin flags. ## Design sketch for walhub 1. **New package `internal/sshd`** built on `golang.org/x/crypto/ssh` (needs a Law 1 amendment in `AGENTS.md` + the doc decisions ledger; hand-rolling SSH is a non-starter). It defines a small consumer-side `Transport` interface that `internal/server` implements, so the SSH path reuses the exact same git pipeline as HTTP (sync → upload-pack; parse → ingest → connectivity → publish → report). 2. **Reuse, not duplication**: extract the transport-agnostic cores of `uploadPack` and `receivePackLocal` (they already only write bytes) and have both HTTP handlers and the SSH dispatcher call them. 3. **Config** (mirrors the static-token pattern): ```toml [server.ssh] listen = "" # e.g. ":2222"; empty = disabled (default) host_key = "" # path to an ed25519 host key; auto-generated under <data-dir>/ssh/ when empty host_key_env = "" # or an env var holding the key [[server.ssh.keys]] principal = "ada" key = "ssh-ed25519 AAAA... ada@laptop" # or key_env write = true admin = false ``` 4. **Auth**: public keys from config, parsed with `ssh.ParseAuthorizedKey`; the matched key's principal gets write/admin from its entry, and receive-pack requires `write` (same rule as the HTTP route). Keys are a credential class of their own, like static tokens. 5. **Security**: no PTY/shell/subsystem (sftp) ever; the command is strictly `verb '<owner/repo[.git]>'` with option-prefixed argv rejected (the classic SSH option-injection guard); repo paths go through `ValidateRepoPath`; placement/drain gates apply before any git work. 6. **Protocol**: `GIT_PROTOCOL=version=2` passthrough for v2 negotiation. ## Testing - command parsing table (verbs, quoting, `.git` suffix, injection attempts) - auth matrix (known key → principal + flags; unknown key refused; write flag enforced on push) - end-to-end: real `git clone` and `git push` over `ssh://127.0.0.1:<random>` against the in-process server with a generated client key and `GIT_SSH_COMMAND` (both directions, protocol v2) ## Out of scope (for this first cut) - `git-upload-archive` and LFS-over-SSH (`git-lfs-authenticate`) - sftp subsystem - SSH certificates and CA-based auth
crueber added this to the v1 milestone 2026-09-10 22:27:24 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
crueber/walhub#1
No description provided.