From 154872da9f07eb154809ab060971ebc832e6befc Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Fri, 12 Jun 2026 15:09:00 +0200 Subject: [PATCH] =?UTF-8?q?feat(scripts):=20add=20idempotent=20'harden'=20?= =?UTF-8?q?subcommand=20=E2=80=94=20key-only=20sshd=20+=20nft=20SSH-guard?= =?UTF-8?q?=20(ref=20#529)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the wg-admin tunnel is confirmed working, 'harden' closes the public SSH brute-force surface: - sshd drop-in: PasswordAuthentication no + PermitRootLogin prohibit-password (refuses if no root authorized_keys — no lockout). - nft SSH-guard: drop tcp/22 from sources outside the LAN (192.168.1.0/24) + 10.x tunnels, inserted BEFORE the blanket 'iif eth1 accept' so the router's port-forwarded public SSH (real public src IP, DNAT no-SNAT) is dropped while LAN + wg-admin stay reachable. Persisted in /etc/nftables.conf + applied live without flushing other tables. Applied + verified on gk2: tunnel SSH (10.98.0.1) key-only works; public admin.gk2.secubox.in:22 now times out. Tables (blacklist/wg) intact. --- scripts/setup-admin-tunnel.sh | 64 ++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/scripts/setup-admin-tunnel.sh b/scripts/setup-admin-tunnel.sh index 8bd64c5a..fd65045a 100755 --- a/scripts/setup-admin-tunnel.sh +++ b/scripts/setup-admin-tunnel.sh @@ -164,6 +164,65 @@ EOF2 log "peer '${name}' added at ${pip}" } +# ── ssh hardening (idempotent) ── +# Run ONLY after the wg-admin tunnel is confirmed working (key auth over +# the tunnel). Closes the public SSH brute-force surface : +# - sshd : key-only (PasswordAuthentication no, root prohibit-password), +# - nft : drop tcp/22 from any source not in the LAN or the 10.x +# tunnels, inserted BEFORE the blanket `iif eth1 accept` so the +# router's port-forwarded public SSH (real public source IP) is +# dropped while LAN + wg-admin stay reachable. +# Trusted SSH source sets are tunable via env. +SSH_LAN_CIDR="${ADMIN_SSH_LAN:-192.168.1.0/24}" +SSH_TUN_CIDR="${ADMIN_SSH_TUN:-10.0.0.0/8}" +NFTCONF="/etc/nftables.conf" +SSHD_DROPIN="/etc/ssh/sshd_config.d/99-secubox-hardening.conf" + +harden_ssh() { + # Pre-flight : a working key in root's authorized_keys, or we refuse + # (don't lock the operator out by disabling passwords with no key). + [ -s /root/.ssh/authorized_keys ] || die "no /root/.ssh/authorized_keys — refusing to disable password auth" + + # 1) sshd key-only (idempotent : just (re)write the drop-in). + cat > "$SSHD_DROPIN" </dev/null || systemctl reload sshd 2>/dev/null || true + log "sshd hardened : key-only (password auth disabled)" + + # 2) nft SSH-guard — persist in $NFTCONF (after the established-accept). + local guard="tcp dport 22 ip saddr != { ${SSH_LAN_CIDR}, ${SSH_TUN_CIDR} } drop" + if ! grep -qF "SSH-guard (#529)" "$NFTCONF" 2>/dev/null; then + cp "$NFTCONF" "${NFTCONF}.bak.$(date +%s)" 2>/dev/null || true + sed -i "/ct state established,related accept/a\\ # SSH-guard (#529): drop public-sourced SSH — LAN + 10.x tunnels only.\\n ${guard}" "$NFTCONF" + nft -c -f "$NFTCONF" || die "nftables.conf failed syntax check after edit — restore from .bak" + log "SSH-guard persisted in ${NFTCONF}" + else + log "SSH-guard already in ${NFTCONF}" + fi + # 3) Apply live without flushing other tables : insert before the + # blanket `iif accept` rule (looked up by handle). + if ! nft list chain inet filter input 2>/dev/null | grep -q "dport 22 ip saddr !="; then + local h + h=$(nft -a list chain inet filter input 2>/dev/null \ + | awk '/iif "eth1" accept/ {for(i=1;i<=NF;i++) if($i=="handle"){print $(i+1); exit}}') + if [ -n "$h" ]; then + nft insert rule inet filter input position "$h" $guard 2>/dev/null \ + && log "SSH-guard applied live (before handle $h)" \ + || log "WARN: live insert failed — applies on next nft reload" + else + log "WARN: could not find LAN-accept handle — guard applies on next reload" + fi + else + log "SSH-guard already live" + fi + log "hardening done. VERIFY now: ssh root@${WG_SERVER_IP} (tunnel, must work) ; public :22 must time out." +} + # ── main ── case "${1:-provision}" in provision) @@ -180,7 +239,10 @@ case "${1:-provision}" in add) add_peer "${2:-}" ;; + harden) + harden_ssh + ;; *) - die "usage: $0 [provision | add ]" + die "usage: $0 [provision | add | harden]" ;; esac