diff --git a/scripts/setup-admin-tunnel.sh b/scripts/setup-admin-tunnel.sh new file mode 100755 index 00000000..27b1dec3 --- /dev/null +++ b/scripts/setup-admin-tunnel.sh @@ -0,0 +1,183 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LicenseRef-CMSD-1.0 +# Copyright (c) 2026 CyberMind — Gérald Kerma +# +# SecuBox-Deb :: setup-admin-tunnel.sh (#529) +# +# Provision a dedicated WireGuard admin tunnel (wg-admin) for secure +# out-of-band management access, distinct from the R3 cabine tunnel +# (wg-toolbox). Idempotent : keys are generated once and reused. +# +# wg-admin : UDP 51821, server 10.98.0.1/24, peer(s) 10.98.0.2+/32 +# endpoint : admin.gk2.secubox.in:51821 +# +# ADDITIVE ONLY : this script opens udp/51821 and brings up the tunnel. +# It does NOT touch sshd or the existing nft rules — sshd hardening is a +# deliberate separate step (after the tunnel is confirmed) to avoid +# locking anyone out. +# +# Usage (run as root on the board): +# setup-admin-tunnel.sh # provision + show server status +# setup-admin-tunnel.sh add # add a peer, print its client .conf +set -euo pipefail +readonly MODULE="setup-admin-tunnel" +readonly VERSION="529" + +WG_IFACE="wg-admin" +WG_PORT="${ADMIN_WG_PORT:-51821}" +WG_NET4="10.98.0" +WG_SERVER_IP="${WG_NET4}.1" +WG_CIDR="24" +ENDPOINT_HOST="${ADMIN_WG_ENDPOINT:-admin.gk2.secubox.in}" +WG_DIR="/etc/wireguard" +SRV_CONF="${WG_DIR}/${WG_IFACE}.conf" +KEY_DIR="${WG_DIR}/${WG_IFACE}.keys" +PEERS_STATE="${KEY_DIR}/peers.tsv" # nameippubkey +NFT_DROPIN="/etc/nftables.d/secubox-admin-wg.nft" + +log() { echo "[$MODULE] $*" >&2; } +die() { log "ERROR: $*"; exit 1; } + +[ "$(id -u)" = "0" ] || die "must run as root" +command -v wg >/dev/null 2>&1 || die "wireguard-tools not installed (apt install wireguard)" + +umask 077 +mkdir -p "$WG_DIR" "$KEY_DIR" + +# ── 1. Server keypair (generate once) ── +if [ ! -s "${KEY_DIR}/server.key" ]; then + wg genkey | tee "${KEY_DIR}/server.key" | wg pubkey > "${KEY_DIR}/server.pub" + chmod 600 "${KEY_DIR}/server.key" + log "generated server keypair" +fi +SRV_PRIV=$(cat "${KEY_DIR}/server.key") +SRV_PUB=$(cat "${KEY_DIR}/server.pub") + +# ── 2. Server config (rewritten from the peers state each run) ── +write_server_conf() { + { + echo "# SecuBox admin tunnel (wg-admin) — managed by setup-admin-tunnel.sh (#529)" + echo "[Interface]" + echo "Address = ${WG_SERVER_IP}/${WG_CIDR}" + echo "ListenPort = ${WG_PORT}" + echo "PrivateKey = ${SRV_PRIV}" + echo + if [ -s "$PEERS_STATE" ]; then + while IFS=$'\t' read -r pname pip ppub; do + [ -n "$ppub" ] || continue + echo "# peer: ${pname}" + echo "[Peer]" + echo "PublicKey = ${ppub}" + echo "AllowedIPs = ${pip}/32" + echo + done < "$PEERS_STATE" + fi + } > "$SRV_CONF" + chmod 600 "$SRV_CONF" +} + +# ── 3. nft drop-in : allow the WG handshake on udp/51821 (persistent) ── +write_nft_dropin() { + install -d -m 0755 /etc/nftables.d + cat > "$NFT_DROPIN" </dev/null; then + nft -f "$NFT_DROPIN" 2>/dev/null || true + fi +} + +# ── peer add ── +next_peer_ip() { + local last=1 + if [ -s "$PEERS_STATE" ]; then + last=$(awk -F'\t' '{n=split($2,a,"."); if(a[4]>last) last=a[4]} END{print last+0}' last=1 "$PEERS_STATE") + fi + echo "${WG_NET4}.$((last + 1))" +} + +add_peer() { + local name="$1" + [ -n "$name" ] || die "peer name required" + touch "$PEERS_STATE" + if grep -qP "^${name}\t" "$PEERS_STATE" 2>/dev/null; then + die "peer '${name}' already exists" + fi + local pip ppriv ppub + pip=$(next_peer_ip) + ppriv=$(wg genkey) + ppub=$(echo "$ppriv" | wg pubkey) + printf '%s\t%s\t%s\n' "$name" "$pip" "$ppub" >> "$PEERS_STATE" + echo "$ppriv" > "${KEY_DIR}/peer-${name}.key" + chmod 600 "${KEY_DIR}/peer-${name}.key" + + write_server_conf + # Hot-add the peer to the running interface if up (no full restart). + if wg show "$WG_IFACE" >/dev/null 2>&1; then + wg set "$WG_IFACE" peer "$ppub" allowed-ips "${pip}/32" 2>/dev/null || \ + systemctl restart "wg-quick@${WG_IFACE}" 2>/dev/null || true + fi + + # Emit the client config to stdout. + cat </dev/null 2>&1; then + echo "# QR (scan from the WireGuard mobile app):" >&2 + qrencode -t ansiutf8 <&2 +[Interface] +PrivateKey = ${ppriv} +Address = ${pip}/32 +DNS = ${WG_SERVER_IP} +[Peer] +PublicKey = ${SRV_PUB} +Endpoint = ${ENDPOINT_HOST}:${WG_PORT} +AllowedIPs = ${WG_NET4}.0/${WG_CIDR} +PersistentKeepalive = 25 +EOF2 + fi + log "peer '${name}' added at ${pip}" +} + +# ── main ── +case "${1:-provision}" in + provision) + write_server_conf + write_nft_dropin + systemctl enable "wg-quick@${WG_IFACE}" >/dev/null 2>&1 || true + systemctl restart "wg-quick@${WG_IFACE}" 2>/dev/null || \ + wg-quick up "$WG_IFACE" 2>/dev/null || true + log "provisioned. server pubkey: ${SRV_PUB}" + log "endpoint: ${ENDPOINT_HOST}:${WG_PORT} server ip: ${WG_SERVER_IP}" + wg show "$WG_IFACE" 2>/dev/null || true + log "next: $0 add # to mint an admin client config" + ;; + add) + add_peer "${2:-}" + ;; + *) + die "usage: $0 [provision | add ]" + ;; +esac