Skip to content

feat: dedicated jumpbox user for SSH key delegation#89

Merged
smalex-z merged 1 commit intomainfrom
fix/jumpbox-ssh
May 7, 2026
Merged

feat: dedicated jumpbox user for SSH key delegation#89
smalex-z merged 1 commit intomainfrom
fix/jumpbox-ssh

Conversation

@smalex-z
Copy link
Copy Markdown
Owner

@smalex-z smalex-z commented May 7, 2026

All Gopher-managed SSH public keys now live under a dedicated, restricted system user (gopher-jump) instead of the dashboard's service account. Closes a privilege escalation path where a leaked SSH key gave the holder root-equivalent access to the VPS and read access to the dashboard database

Pre-fix, ReconcileAuthorizedKeys appended every Gopher-managed public key
into ~/.ssh/authorized_keys of the OS user the dashboard runs as
(typically gopher). That user has NOPASSWD sudo for iptables / fail2ban,
read access to gopher.db (every per-machine SSH private key, every token,
the password hash, TOTP secrets), and write access to /opt/gopher/gopher.
A leaked Gopher SSH key gave the holder a full path to root + DB
exfiltration + every-client compromise.

Fix: dedicated `gopher-jump` system user, no shell, no sudo, no homedir
contents the dashboard cares about. Each authorized_keys line is wrapped
with `restrict,permitopen="127.0.0.1:*",permitopen="localhost:*"` so even
a stolen key can only forward to localhost ports on the VPS — which is
exactly what the jumpbox flow legitimately needs. `restrict` also
disables shell, X11, agent forwarding, env, and ~/.ssh/rc.

Auto-migration: the next ReconcileAuthorizedKeys call after `gopher-jump`
exists writes Gopher keys into ~gopher-jump/.ssh/authorized_keys with the
new options and scrubs them from the dashboard user's authorized_keys.
Matching is by type+keydata, so any operator-added non-Gopher keys
survive untouched. Replace-in-place semantics on existing matching lines
mean re-runs upgrade no-options entries to restrict/permitopen without
duplicating.

User creation lives in two places:
  - cmd/server/install.go: ensureSystemUser(gopher-jump, /var/lib/gopher-jump)
    runs as part of `gopher install`. New deployments get it for free.
  - scripts/reinstall.sh: same idempotent useradd. Existing deployments
    using reinstall.sh as their upgrade path also get the safer config
    without an extra step.

Frontend: /api/local/status now exposes `jumpbox_user`. SSH commands on
Tunnels and Machines pages use it instead of os_user. Empty string
(legacy install hasn't created the user yet) falls back to vps.username
or os_user with the same UX the operator had before — so the upgrade
isn't visible until they re-run install/reinstall.

Threat model after: a leaked Gopher SSH key only gives the attacker the
ability to forward TCP to localhost ports they were already authorized
to reach via the jumpbox. No shell. No DB read. No iptables. No binary
replacement. The blast radius collapses from "full system + every client
compromise" to "what they could already do with the same key."

Tests cover the keydata-token parser (the heart of the migration logic):
plain keys, options-prefixed keys, options containing quoted commas,
malformed input, blank/comment lines.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 7, 2026

Unit tests run: 281
Unit tests passed: 281
Test coverage: 21.4%

@smalex-z smalex-z merged commit d05d9c1 into main May 7, 2026
5 checks passed
@smalex-z smalex-z deleted the fix/jumpbox-ssh branch May 7, 2026 05:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant