# SSH access — production server (Hetzner) Two SSH paths into the production server, with different audiences and different break-glass behavior. Read this before disabling either one. ## The two paths | Port | Interface | Audience | Allowed from | |------|-----------|----------|--------------| | `22` | public (eth0) | mnw-cli git operations | anywhere (firewall + sshd config) | | `2200` | Tailscale (tailscale0) | admin access | tailnet only (firewall blocks public) | - **Public :22** is intentionally open so creators can `git clone`/`push` over SSH against `ssh.makenot.work`. The sshd config on this port is locked to git-shell only — see `setup-git-ssh.sh` and `sshd-git.conf`. No interactive shell, no port forwarding, no admin access. - **Tailnet :2200** is the admin path. Full interactive shell, used for every `deploy.sh` invocation and any manual maintenance. Reachable only from devices on the tailnet (firewall rule `ufw allow in on tailscale0` in `setup-firewall.sh`). ## Why this split exists The audit-flagged risk was disabling Tailscale SSH (the admin path) without first verifying the public sshd was still functional. Tailscale runs its own SSH server when configured; if that goes down — Tailscale service crashes, ACL misconfiguration, accidental `tailscale down` — you can lose admin access entirely if you've also locked down public sshd. The split solves this: public :22 is always alive (firewalled to allow SSH from anywhere) but restricted to git-shell, so an attacker who probes :22 finds nothing but git commands. Admin :2200 lives on the tailnet, where the firewall blocks public access and the surface area is small. ## Break-glass procedure If the tailnet path stops working (Tailscale down, key revoked, ACL broken): 1. **Verify public sshd is up** from any machine: `ssh -p 22 root@5.78.144.244 -o BatchMode=yes -o ConnectTimeout=5 true` Expect a key-based prompt or a refused git-shell — both prove sshd is listening. A timeout or "Connection refused" means public sshd is ALSO down and you need Hetzner Cloud Console. 2. **Edit `/etc/ssh/sshd_config.d/git-shell.conf` from Hetzner Console** to temporarily restore an interactive shell for the `root` user on port `22`. Match `User root` block, set `ForceCommand` to nothing. 3. **Restart sshd**: `systemctl restart ssh`. Test from your laptop. 4. **Fix the tailnet path** (re-auth `tailscale up`, restore key, etc). 5. **Revert the sshd edit** and restart `ssh` again. Don't leave the interactive root shell on public :22 — it defeats the whole split. ## What NOT to do - **Do not** disable Tailscale SSH (`tailscale set --ssh=false`) without first proving public :22 is reachable and you have a working root key for it. Memory rule: `feedback_tailscale_ssh` — getting locked out requires Hetzner Console access, which costs time we don't always have. - **Do not** restrict public :22 to specific IPs without coordinating — the mnw-cli git endpoint serves users worldwide. - **Do not** open port 2200 to the public — it's the admin shell. ## Hetzner Console (last resort) If both paths are dead, the Hetzner Cloud Console provides KVM-style access to the server's serial console. Login at , select the project, select the server, "Console" tab. Slow but always available. Same root credentials. ## Key paths - Firewall: `deploy/setup-firewall.sh` - Public sshd (git-only): `deploy/sshd-git.conf`, `deploy/setup-git-ssh.sh` - Admin sshd (tailnet): `/etc/ssh/sshd_config` on the server - Deploy entry point: `deploy/deploy.sh` (uses `-p 2200`) - CI runner setup: `deploy/setup-ci.sh` (uses `-p 2200`)