Skip to content

Jumpbox: optional staff-only SSH filter - #73

Merged
WittsEnd2 merged 4 commits into
mainfrom
jumpbox-ssh-filter
Oct 27, 2026
Merged

WittsEnd2 merged 4 commits into
mainfrom
jumpbox-ssh-filter

Conversation

@WittsEnd2

Copy link
Copy Markdown
Member

Adds --ssh-staff-only to scripts/jumpbox_configure.sh. Students route through the jumpbox and never log in to it, but its lab gateway addresses sit inside the advertised 10.0.0.0/8 route, so a VPN ACL alone can't close SSH. This generates a separate nftables table that allows tcp/22 only from loopback, the management interface, and listed staff tailnet addresses (JUMPBOX_SSH_STAFF_IPS), and drops the rest. Forwarding and ICMP are untouched. It is loaded by a oneshot systemd unit. Dry-run/diff by default; --apply validates with nft -c first; a lockout guard refuses with no staff list and no management interface.

Tests (red first): rule order, mixed v4/v6/CIDR sets, determinism, empty-staff table, dry-run writes nothing, apply writes and enables, idempotence, nft rejection, lockout guard, unit ordering. Validated with real nft -c -f / nft -f on Ubuntu 24.04. Docs: docs/JUMPBOX.md (why an ACL is not enough, adding a staff device, rollback).

Students route through the jumpbox and never log in, but its 10.x.x.1
gateway addresses fall inside the advertised 10.0.0.0/8 route, so a Headscale
ACL cannot close SSH. Generate a separate nftables table (input hook, priority
-10, policy accept) that accepts tcp/22 on lo, the management interface and
tailscale0 from JUMPBOX_SSH_STAFF_IPS, and counts and drops the rest, plus a
oneshot unit that loads it before the network and sshd. Forwarding is
untouched. Validated with nft -c -f; refuses when the staff list is empty and
the management interface is unknown.

Co-Authored-By: Claude <noreply@anthropic.com>
@WittsEnd2
WittsEnd2 requested a review from a team as a code owner October 27, 2026 16:54
WittsEnd2 and others added 3 commits October 27, 2026 13:49
…lockout guards

- Collect the ports sshd (-T) and ssh.socket actually listen on and render
  them as a port set; validate --ports (1-65535). Non-22 listeners no longer
  bypass the filter.
- Unit gains ExecReload and a "-" ExecStop; the script uses
  reload-or-restart on an active unit (start otherwise), never restart, so
  the table is never deleted while SSH is up.
- Applying over SSH refuses unless the session provably survives (loopback,
  management NIC, or listed staff source on tailscale0) or --i-have-console.
- --apply requires nft and always validates with nft -c -f; dry run without
  nft prints the ruleset with a note.

Co-Authored-By: Claude <noreply@anthropic.com>
…t guard

- Parse sshd -T listenaddress ports (host:port, [v6]:port; bare address
  means the port values apply). If sshd or ssh.socket cannot be read, or sshd
  reports no port, --apply refuses instead of assuming 22; 22 is the default
  only when neither exists.
- Decide whether an SSH session survives from the interface `ip route get`
  picks for its source (lo, management NIC, or tailscale0 with a staff
  source), not from the destination address. Validate SSH_CONNECTION strictly,
  fold IPv4-mapped IPv6, pass values after `--`, and refuse when the route or
  the value cannot be determined unless --i-have-console.

Co-Authored-By: Claude <noreply@anthropic.com>
@WittsEnd2
WittsEnd2 merged commit c617a9e into main Oct 27, 2026
3 checks passed
@WittsEnd2
WittsEnd2 deleted the jumpbox-ssh-filter branch October 27, 2026 18:12
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