markdown · 5119 bytes Raw Blame History

SSH git protocol

Wires git@shithub.sh:owner/repo.git clone/push to the same authorize-and-dispatch path as the HTTPS git surface. Three pieces must all be in place; missing any gives users a clone URL that errors at connect:

  1. Code (internal/auth/policy/..., cmd/shithubd/ssh.go): shithubd ssh-authkeys <fingerprint> returns the matching user's authorized_keys line with a forced command= that runs shithubd ssh-shell <user_id>. The shell subcommand parses SSH_ORIGINAL_COMMAND, authorizes, and syscall.Execs into git-{upload,receive}-pack. Already in tree (S13).
  2. sshd (deploy/sshd_config.j2): a Match User git block wires AuthorizedKeysCommand /usr/local/bin/shithubd ssh-authkeys %f and AuthorizedKeysCommandUser shithub-ssh.
  3. Config (web.env): SHITHUB_AUTH__SSH__ENABLED=true and SHITHUB_AUTH__SSH__HOST=git@<domain>. Without this, the sshd path is reachable but the UI hides it from clone widgets.

First-time enable on an existing droplet

The clean path is ansible-playbook deploy/ansible/site.yml -l shithub-app, which provisions everything below. SSH-git turned out to need six pieces — the obvious ones from S13 plus five subtleties discovered live. They are all encoded in ansible (after this PR); this list is for operators who want to apply piece-by-piece or debug one failure mode at a time.

# Piece Lives in Failure if missing
1 git system user with git-shell, in shithub group, password-cleared (passwd -d) roles/base/tasks/main.yml "User git not allowed because account is locked"
2 Match User git sshd block pointing at the AKC wrapper deploy/sshd_config.j2 sshd offers no AKC for git@; "Permission denied (publickey)"
3 /usr/local/bin/shithub-ssh-authkeys wrapper sourcing web.env then exec'ing shithubd ssh-authkeys roles/shithubd/files/shithub-ssh-authkeys AKC returns empty (no SHITHUB_DATABASE_URL in env); "Permission denied (publickey)"
4 /var/lib/git/git-shell-commands/shithubd wrapper sourcing web.env then exec'ing the bare binary roles/shithubd/files/git-shell-commands-shithubd git-shell rejects forced command; "fatal: unrecognized command"
5 /etc/shithub/web.env mode 0640 (group=shithub) so the git user can read it through the wrappers above roles/shithubd/tasks/main.yml ssh-shell: cfg: ... permission denied
6 SHITHUB_AUTH__SSH__{ENABLED,HOST} env vars on web.env roles/shithubd/templates/web.env.j2 repo pages don't show the SSH clone URL (sshd path still works)

Verify:

# (a) repo page now shows the SSH clone URL
curl -fsS https://shithub.sh/tenseleyflow/shithub | grep -i 'ssh' | head

# (b) the git user exists with git-shell
ssh root@shithub.sh 'getent passwd git'
# git:x:117:65534::/var/lib/git:/usr/bin/git-shell

# (c) AKC handshake works for an unknown fingerprint (returns nothing,
#     exits 0 — that's the fail-closed contract):
ssh root@shithub.sh '/usr/local/bin/shithubd ssh-authkeys SHA256:not-a-real-key 2>&1; echo rc=$?'
# (empty output, rc=0)

# (d) end-to-end clone with a key you've added at /settings/keys:
ssh-keygen -t ed25519 -f ~/.ssh/shithub-test -N '' -C 'shithub-ssh-test'
cat ~/.ssh/shithub-test.pub
# paste into https://shithub.sh/settings/keys
GIT_SSH_COMMAND='ssh -i ~/.ssh/shithub-test -o IdentitiesOnly=yes' \
  git clone git@shithub.sh:tenseleyflow/shithub.git /tmp/shithub-ssh-clone

If (d) hangs or rejects:

  • journalctl -u ssh -n 50 --no-pager | grep -E "git|AKC|authkeys"
  • journalctl -u shithubd-web -n 100 | grep ssh-shell
  • Manually invoke the AKC with the real fingerprint: ssh root@shithub.sh "/usr/local/bin/shithubd ssh-authkeys SHA256:<your-fp>" Should print one line starting with command="...shithubd ssh-shell...".

When something breaks

Symptom Likely cause
git clone git@shithub.sh:... hangs at "Connecting" sshd not listening on 22, or firewall blocking. systemctl status ssh; ufw status
Hangs at "Permission denied (publickey)" Key not in user's /settings/keys, OR fingerprint mismatch. The DB stores SHA256:... from ssh-keygen -lf <key>.
"Permission denied" right after key offer AKC returned an empty string (key DOES match no row). Run shithubd ssh-authkeys <your-fp> manually as root to debug.
Connects then "fatal: protocol error" ssh-shell rejected the requested op. Check journalctl -u shithubd-web for the ssh-shell: denied entry.
git push works but post-receive hooks don't fire SHITHUB_* env not threaded through. Check protocol.PrepareDispatch — that's the env-build site.

Disabling without removing

Set SHITHUB_AUTH__SSH__ENABLED=false in web.env and restart shithubd-web. The clone widget hides the SSH URL but the sshd path still works for anyone who knows it. Use this when the SSH service is having issues but you don't want to alarm people who try the UI.

To fully disable: comment the Match User git block in /etc/ssh/sshd_config and reload sshd. The git user can stay.

View source
1 # SSH git protocol
2
3 Wires `git@shithub.sh:owner/repo.git` clone/push to the same
4 authorize-and-dispatch path as the HTTPS git surface. Three pieces
5 must all be in place; missing any gives users a clone URL that
6 errors at connect:
7
8 1. **Code** (`internal/auth/policy/...`, `cmd/shithubd/ssh.go`):
9 `shithubd ssh-authkeys <fingerprint>` returns the matching
10 user's authorized_keys line with a forced `command=` that runs
11 `shithubd ssh-shell <user_id>`. The shell subcommand parses
12 `SSH_ORIGINAL_COMMAND`, authorizes, and `syscall.Exec`s into
13 `git-{upload,receive}-pack`. Already in tree (S13).
14 2. **sshd** (`deploy/sshd_config.j2`): a `Match User git` block
15 wires `AuthorizedKeysCommand /usr/local/bin/shithubd
16 ssh-authkeys %f` and `AuthorizedKeysCommandUser shithub-ssh`.
17 3. **Config** (`web.env`): `SHITHUB_AUTH__SSH__ENABLED=true` and
18 `SHITHUB_AUTH__SSH__HOST=git@<domain>`. Without this, the
19 sshd path is reachable but the UI hides it from clone widgets.
20
21 ## First-time enable on an existing droplet
22
23 The clean path is `ansible-playbook deploy/ansible/site.yml -l shithub-app`,
24 which provisions everything below. SSH-git turned out to need
25 **six** pieces — the obvious ones from S13 plus five subtleties
26 discovered live. They are all encoded in ansible (after this PR);
27 this list is for operators who want to apply piece-by-piece or
28 debug one failure mode at a time.
29
30 | # | Piece | Lives in | Failure if missing |
31 |---|---|---|---|
32 | 1 | `git` system user with `git-shell`, in `shithub` group, password-cleared (`passwd -d`) | `roles/base/tasks/main.yml` | "User git not allowed because account is locked" |
33 | 2 | `Match User git` sshd block pointing at the AKC wrapper | `deploy/sshd_config.j2` | sshd offers no AKC for `git@`; "Permission denied (publickey)" |
34 | 3 | `/usr/local/bin/shithub-ssh-authkeys` wrapper sourcing web.env then exec'ing `shithubd ssh-authkeys` | `roles/shithubd/files/shithub-ssh-authkeys` | AKC returns empty (no `SHITHUB_DATABASE_URL` in env); "Permission denied (publickey)" |
35 | 4 | `/var/lib/git/git-shell-commands/shithubd` wrapper sourcing web.env then exec'ing the bare binary | `roles/shithubd/files/git-shell-commands-shithubd` | git-shell rejects forced command; "fatal: unrecognized command" |
36 | 5 | `/etc/shithub/web.env` mode 0640 (group=shithub) so the git user can read it through the wrappers above | `roles/shithubd/tasks/main.yml` | `ssh-shell: cfg: ... permission denied` |
37 | 6 | `SHITHUB_AUTH__SSH__{ENABLED,HOST}` env vars on web.env | `roles/shithubd/templates/web.env.j2` | repo pages don't show the SSH clone URL (sshd path still works) |
38
39 Verify:
40
41 ```sh
42 # (a) repo page now shows the SSH clone URL
43 curl -fsS https://shithub.sh/tenseleyflow/shithub | grep -i 'ssh' | head
44
45 # (b) the git user exists with git-shell
46 ssh root@shithub.sh 'getent passwd git'
47 # git:x:117:65534::/var/lib/git:/usr/bin/git-shell
48
49 # (c) AKC handshake works for an unknown fingerprint (returns nothing,
50 # exits 0 — that's the fail-closed contract):
51 ssh root@shithub.sh '/usr/local/bin/shithubd ssh-authkeys SHA256:not-a-real-key 2>&1; echo rc=$?'
52 # (empty output, rc=0)
53
54 # (d) end-to-end clone with a key you've added at /settings/keys:
55 ssh-keygen -t ed25519 -f ~/.ssh/shithub-test -N '' -C 'shithub-ssh-test'
56 cat ~/.ssh/shithub-test.pub
57 # paste into https://shithub.sh/settings/keys
58 GIT_SSH_COMMAND='ssh -i ~/.ssh/shithub-test -o IdentitiesOnly=yes' \
59 git clone git@shithub.sh:tenseleyflow/shithub.git /tmp/shithub-ssh-clone
60 ```
61
62 If (d) hangs or rejects:
63 - `journalctl -u ssh -n 50 --no-pager | grep -E "git|AKC|authkeys"`
64 - `journalctl -u shithubd-web -n 100 | grep ssh-shell`
65 - Manually invoke the AKC with the real fingerprint:
66 `ssh root@shithub.sh "/usr/local/bin/shithubd ssh-authkeys SHA256:<your-fp>"`
67 Should print one line starting with `command="...shithubd ssh-shell..."`.
68
69 ## When something breaks
70
71 | Symptom | Likely cause |
72 |---|---|
73 | `git clone git@shithub.sh:...` hangs at "Connecting" | sshd not listening on 22, or firewall blocking. `systemctl status ssh; ufw status` |
74 | Hangs at "Permission denied (publickey)" | Key not in user's `/settings/keys`, OR fingerprint mismatch. The DB stores `SHA256:...` from `ssh-keygen -lf <key>`. |
75 | "Permission denied" right after key offer | AKC returned an empty string (key DOES match no row). Run `shithubd ssh-authkeys <your-fp>` manually as root to debug. |
76 | Connects then "fatal: protocol error" | `ssh-shell` rejected the requested op. Check `journalctl -u shithubd-web` for the `ssh-shell: denied` entry. |
77 | `git push` works but post-receive hooks don't fire | `SHITHUB_*` env not threaded through. Check `protocol.PrepareDispatch` — that's the env-build site. |
78
79 ## Disabling without removing
80
81 Set `SHITHUB_AUTH__SSH__ENABLED=false` in `web.env` and restart
82 shithubd-web. The clone widget hides the SSH URL but the sshd path
83 still works for anyone who knows it. Use this when the SSH service
84 is having issues but you don't want to alarm people who try the UI.
85
86 To fully disable: comment the `Match User git` block in
87 `/etc/ssh/sshd_config` and reload sshd. The `git` user can stay.