Compare commits

..

11 Commits

Author SHA1 Message Date
pdmarf
f362bd3721 Revert package/ restructure, restore scripts to repo root
Moves all automated scripts back to the repo root where setup.sh
expects them. standalone/ remains for manual-run tools.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 14:28:00 +01:00
pdmarf
7585a12b6d Restructure repo into package/ and standalone/ directories
Moves automated scan scripts and setup.sh into package/.
bind-ssh-tailscale.sh remains in standalone/ as a manual-run tool.
Updates README.md setup instructions to reflect new paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 14:25:11 +01:00
pdmarf
50aa38712e Add bind-ssh-tailscale.sh as standalone manual-run script
Places the script in standalone/ so it is excluded from setup.sh automation.
Documents manual curl-and-run usage in README.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 14:22:56 +01:00
pdmarf
c5037c0ac0 Fix branch name in README-scanner.md curl command (main -> master)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 11:01:13 +01:00
pdmarf
f257fcfcb9 Set git pull.rebase false in setup.sh to prevent divergent branch errors
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 16:59:15 +01:00
pdmarf
d9b4592c50 Fix setup.sh to print only current run of npm sudo config audit
Use tee -a instead of redirect + cat, so only the current run's output
is shown rather than the entire accumulated daily log.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 16:37:32 +01:00
pdmarf
72a8f37290 Add check-npm-sudo-config docs and print audit log on setup
- README: add Scripts section explaining what check-npm-sudo-config.sh
  does, what it checks, and that it is audit-only
- setup.sh: print check-npm-sudo-config log to terminal after initial scan

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 16:32:34 +01:00
pdmarf
4eee88a004 Add file logging to check-npm-sudo-config.sh v1.0 2026-04-18 10:07:29 +01:00
pdmarf
d2a0a0f4cc Remove committed logs and ignore all *.log files 2026-04-18 09:58:39 +01:00
pdmarf
94437506fa npm config checker 2026-04-18 09:57:36 +01:00
pdm
080073a7d7 Add npm sudo config audit script
Checks npm prefix ownership, PATH wiring, cache ownership, shell history
for sudo npm usage, and n/nvm version manager config. Runs daily at 08:10
via cron and on initial setup.sh run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 08:47:32 +00:00
8 changed files with 368 additions and 67 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
config.sh
whitelist.conf
logs/
*.log

View File

@@ -5,7 +5,7 @@ Quick scanner for CVE-2025-66478 / CVE-2025-55182 (CVSS 10.0)
## Usage
```bash
curl -o check-nextjs-rce.sh http://100.120.125.113:3000/pdm/security-tools/raw/branch/main/check-nextjs-rce.sh
curl -o check-nextjs-rce.sh http://100.120.125.113:3000/pdm/security-tools/raw/branch/master/check-nextjs-rce.sh
chmod +x check-nextjs-rce.sh
sudo ./check-nextjs-rce.sh
```

View File

@@ -2,6 +2,71 @@
A collection of security scripts versioned in this repository.
## Scripts
### check-npm-sudo-config.sh
Audits npm configuration on a Linux VM to detect cases where npm is — or has
been — configured to install packages into system-owned directories, which
requires `sudo` and creates security risks.
Running `sudo npm install -g` can deposit files owned by root inside your npm
prefix or cache directory. This causes permission errors for non-root users,
encourages further `sudo npm` use to work around them, and means malicious
packages run with root privileges during installation.
**This script is audit-only — it makes no changes.** It reports issues and
prints recommended commands, but you must run those commands yourself.
The script checks:
1. **npm prefix** — flags if it points to `/usr` or `/usr/local` (system-wide, requires sudo)
2. **~/.npmrc** — checks whether the prefix is explicitly pinned to a user directory
3. **PATH** — confirms the npm prefix bin directory is in PATH
4. **Root-owned files in the prefix** — evidence of past `sudo npm` usage
5. **Shell history** — scans `.bash_history` / `.zsh_history` for `sudo npm` commands
6. **npm cache ownership** — root-owned cache files cause EACCES errors
7. **Node version manager** — detects nvm, fnm, or n; flags if n is present without N_PREFIX set
If issues are found, it sends a Telegram alert and logs results to `logs/`.
The correct fix is to configure npm to install global packages into a
user-owned directory (e.g. `~/.npm-global`) so that `sudo` is never needed:
```bash
npm config set prefix ~/.npm-global
export PATH="$HOME/.npm-global/bin:$PATH"
```
## Standalone Scripts
These scripts live in `standalone/` and are **not run by `setup.sh`**. They are
single-use tools intended to be copied to a target machine and run manually.
### standalone/bind-ssh-tailscale.sh
Binds SSH to the Tailscale interface only and disables password authentication.
- Requires root (`sudo bash bind-ssh-tailscale.sh`)
- Tailscale must be installed and connected before running
- Uses a drop-in config at `/etc/ssh/sshd_config.d/99-tailscale-only.conf` if
that directory exists; otherwise edits `/etc/ssh/sshd_config` directly with
an automatic backup
- Validates the config with `sshd -t` before restarting the SSH service
- Prints revert instructions on completion
**To use on a target machine:**
```bash
curl -O https://gitea.pdmarf.co.uk/pdm/security-tools/raw/branch/master/standalone/bind-ssh-tailscale.sh
# or via Tailscale:
curl -O http://100.120.125.113:3000/pdm/security-tools/raw/branch/master/standalone/bind-ssh-tailscale.sh
sudo bash bind-ssh-tailscale.sh
```
---
## Claude Code Context
This project is maintained with Claude Code. The working directory on macOS is:
@@ -55,33 +120,6 @@ After cloning, run `setup.sh` once. It will:
- Send a test Telegram message confirming the VM is active
- Create a `logs/` folder — logs are kept for 60 days then auto-deleted
## Whitelisting Known-Safe Findings
If a script flags something you know is safe, add it to `whitelist.conf` on that VM to suppress it in future scans. This file is VM-specific and never committed to git.
Add a package name:
```bash
echo "ua-parser-js" >> ~/security-tools/whitelist.conf
```
Add a file path:
```bash
echo "/tmp/my-known-script.sh" >> ~/security-tools/whitelist.conf
```
View or edit the whitelist:
```bash
nano ~/security-tools/whitelist.conf
```
## Checking Script Versions
To see which version of a script is running on a VM:
```bash
head -2 ~/security-tools/npm-security-check.sh
head -2 ~/security-tools/check-nextjs-rce.sh
```
## Updating an Existing VM
When changes are pushed to this repo, update any VM by running:

View File

@@ -1,5 +1,4 @@
#!/bin/bash
# check-nextjs-rce.sh v1.0
# Next.js CVE-2025-66478 / CVE-2025-55182 Vulnerability Checker
# Checks if Next.js installations are vulnerable to critical RCE

200
check-npm-sudo-config.sh Executable file
View File

@@ -0,0 +1,200 @@
#!/usr/bin/env bash
# check-npm-sudo-config.sh v1.0
# Audits npm configuration on this VM for sudo-related issues and recommends fixes.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=config.sh
source "$SCRIPT_DIR/config.sh"
send_telegram() {
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-d chat_id="${TELEGRAM_CHAT_ID}" \
-d text="$1" \
-d parse_mode="HTML" > /dev/null || true
}
HOSTNAME=$(hostname)
DATE=$(date)
LOGFILE="$SCRIPT_DIR/logs/npm-sudo-config-$(date +%Y%m%d).log"
mkdir -p "$SCRIPT_DIR/logs"
RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
BOLD='\033[1m'
RESET='\033[0m'
ISSUES=0
WARNINGS=0
log() { echo "$*" | tee -a "$LOGFILE"; }
ok() { log "$(printf "${GREEN}${RESET} %s" "$*")"; }
warn() { log "$(printf "${YELLOW}${RESET} %s" "$*")"; (( WARNINGS++ )) || true; }
fail() { log "$(printf "${RED}${RESET} %s" "$*")"; (( ISSUES++ )) || true; }
rec() { log "$(printf " ${YELLOW}${RESET} %s" "$*")"; }
header() { log ""; log "=========================================="; log "$*"; log "=========================================="; }
log "=========================================="
log " npm sudo config audit"
log "=========================================="
log "Hostname : $HOSTNAME"
log "Date : $DATE"
# ── 1. npm present? ───────────────────────────────────────────────────────────
header "1. npm availability"
if ! command -v npm &>/dev/null; then
warn "npm not found in PATH — skipping remaining checks"
echo ""
echo "RESULT: 0 issue(s), 1 warning(s)"
exit 0
fi
NPM_PATH=$(command -v npm)
ok "npm found: $NPM_PATH"
# ── 2. npm prefix ─────────────────────────────────────────────────────────────
header "2. npm prefix"
PREFIX=$(npm config get prefix 2>/dev/null || echo "unknown")
log "Current prefix: $PREFIX"
if [[ "$PREFIX" == "/usr" || "$PREFIX" == "/usr/local" ]]; then
fail "npm prefix is $PREFIX (system-wide) — global installs require sudo"
rec "npm config set prefix ~/.npm-global"
rec "Add to ~/.profile: export PATH=\"\$HOME/.npm-global/bin:\$PATH\""
elif [[ "$PREFIX" == "unknown" ]]; then
warn "Could not determine npm prefix"
else
PREFIX_OWNER=$(stat -c "%U" "$PREFIX" 2>/dev/null || echo "unknown")
if [[ "$PREFIX_OWNER" == "root" ]]; then
fail "npm prefix $PREFIX is owned by root — global installs require sudo"
rec "sudo chown -R \$(whoami) $PREFIX"
rec "Or set a user-owned prefix: npm config set prefix ~/.npm-global"
else
ok "npm prefix is $PREFIX (owned by $PREFIX_OWNER)"
fi
fi
# ── 3. .npmrc ────────────────────────────────────────────────────────────────
header "3. ~/.npmrc"
if [[ -f "$HOME/.npmrc" ]]; then
log "$(cat "$HOME/.npmrc")"
NPM_PREFIX_LINE=$(grep "^prefix=" "$HOME/.npmrc" 2>/dev/null || true)
if [[ -n "$NPM_PREFIX_LINE" ]]; then
ok ".npmrc explicitly sets: $NPM_PREFIX_LINE"
else
warn ".npmrc exists but does not pin the prefix"
rec "npm config set prefix ~/.npm-global"
fi
else
warn "No ~/.npmrc — prefix is not pinned to a user directory"
rec "npm config set prefix ~/.npm-global"
fi
# ── 4. prefix/bin in PATH ─────────────────────────────────────────────────────
header "4. npm prefix bin in PATH"
if [[ "$PREFIX" != "unknown" ]]; then
PREFIX_BIN="${PREFIX}/bin"
if echo "$PATH" | tr ':' '\n' | grep -qxF "$PREFIX_BIN"; then
ok "$PREFIX_BIN is in PATH"
else
warn "$PREFIX_BIN is NOT in PATH — globally installed binaries won't run"
PROFILE_FILE="$HOME/.profile"
[[ -f "$HOME/.zshrc" ]] && PROFILE_FILE="$HOME/.zshrc"
rec "Add to $PROFILE_FILE: export PATH=\"$PREFIX_BIN:\$PATH\""
rec "Then reload: source $PROFILE_FILE"
fi
fi
# ── 5. Root-owned files in npm prefix ────────────────────────────────────────
header "5. Root-owned files in npm prefix"
if [[ -d "$PREFIX" ]]; then
ROOT_FILES=$(find "$PREFIX" -maxdepth 3 -user root 2>/dev/null | head -5 || true)
if [[ -n "$ROOT_FILES" ]]; then
fail "Root-owned files found in npm prefix (past sudo npm usage):"
echo "$ROOT_FILES"
rec "sudo chown -R \$(whoami) $PREFIX"
else
ok "No root-owned files in $PREFIX"
fi
else
ok "npm prefix directory does not exist yet (no global installs made)"
fi
# ── 6. sudo npm in shell history ─────────────────────────────────────────────
header "6. Shell history — sudo npm usage"
SUDO_NPM_FOUND=false
for hfile in "$HOME/.bash_history" "$HOME/.zsh_history"; do
if [[ -f "$hfile" ]]; then
HITS=$(grep -c "sudo npm" "$hfile" 2>/dev/null || true)
if [[ "$HITS" -gt 0 ]]; then
warn "Found $HITS occurrence(s) of \"sudo npm\" in $hfile"
SUDO_NPM_FOUND=true
fi
fi
done
$SUDO_NPM_FOUND || ok "No \"sudo npm\" in shell history"
# ── 7. npm cache ownership ───────────────────────────────────────────────────
header "7. npm cache ownership"
CACHE_DIR=$(npm config get cache 2>/dev/null || echo "$HOME/.npm")
if [[ -d "$CACHE_DIR" ]]; then
ROOT_CACHE=$(find "$CACHE_DIR" -maxdepth 2 -user root 2>/dev/null | head -3 || true)
if [[ -n "$ROOT_CACHE" ]]; then
fail "Root-owned files in npm cache ($CACHE_DIR) — will cause EACCES errors"
rec "sudo chown -R \$(whoami) $CACHE_DIR"
else
ok "npm cache ($CACHE_DIR) is user-owned"
fi
else
ok "npm cache directory does not exist yet"
fi
# ── 8. Node version manager ───────────────────────────────────────────────────
header "8. Node version manager"
if command -v n &>/dev/null; then
N_PREFIX_VAL="${N_PREFIX:-}"
if [[ -z "$N_PREFIX_VAL" ]]; then
warn "n is installed but N_PREFIX is not set — n defaults to /usr/local (requires sudo)"
rec "Add to ~/.profile: export N_PREFIX=\$HOME/.n"
rec "Add to ~/.profile: export PATH=\$PATH:\$N_PREFIX/bin"
else
ok "n is installed, N_PREFIX=$N_PREFIX_VAL"
fi
elif [[ -s "$HOME/.nvm/nvm.sh" ]] || command -v nvm &>/dev/null 2>&1; then
ok "nvm is managing Node (sudo-free by design)"
elif command -v fnm &>/dev/null; then
ok "fnm is managing Node (sudo-free by design)"
else
ok "No Node version manager detected"
fi
# ── Summary ───────────────────────────────────────────────────────────────────
header "SUMMARY"
log "Scan completed at: $(date)"
log "Log saved to : $LOGFILE"
log ""
if [[ $ISSUES -gt 0 ]]; then
printf "${RED}✗ %d issue(s) and %d warning(s) — see recommendations above${RESET}\n" "$ISSUES" "$WARNINGS"
send_telegram "⚠️ <b>npm sudo config issues</b>
Host: <code>${HOSTNAME}</code>
Issues: ${ISSUES} | Warnings: ${WARNINGS}
Run manually: bash check-npm-sudo-config.sh"
exit 1
elif [[ $WARNINGS -gt 0 ]]; then
printf "${YELLOW}⚠ Clean but %d warning(s) — see recommendations above${RESET}\n" "$WARNINGS"
exit 0
else
printf "${GREEN}✓ npm is correctly configured on %s${RESET}\n" "$HOSTNAME"
exit 0
fi

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# npm-security-check.sh v1.0
# npm-security-check.sh
# Scans for NPM/Node.js malware indicators on this VM.
set -euo pipefail
@@ -18,11 +18,6 @@ send_telegram() {
HOSTNAME=$(hostname)
DATE=$(date)
LOGFILE="${1:-npm_security_check_${HOSTNAME}_$(date +%Y%m%d_%H%M%S).log}"
WHITELIST="$SCRIPT_DIR/whitelist.conf"
is_whitelisted() {
[[ -f "$WHITELIST" ]] && grep -qF "$1" "$WHITELIST" 2>/dev/null
}
RED='\033[0;31m'
YELLOW='\033[1;33m'
@@ -90,10 +85,6 @@ else
COUNT=$(echo "$LOCKFILES" | wc -l)
log "Scanning $COUNT lock file(s)..."
for pkg in "${BAD_PKGS[@]}"; do
if is_whitelisted "$pkg"; then
ok "$pkg is whitelisted — skipping"
continue
fi
MATCHES=$(echo "$LOCKFILES" | xargs grep -l "\"$pkg\"" 2>/dev/null || true)
if [[ -n "$MATCHES" ]]; then
fail "Found '$pkg' in: $MATCHES"
@@ -215,14 +206,12 @@ for dir in /tmp /dev/shm /var/tmp; do
EXEC_FILES=$(find "$dir" -type f -executable 2>/dev/null | head -20 || true)
JS_FILES=$(find "$dir" -name "*.js" -o -name "*.mjs" 2>/dev/null | head -10 || true)
if [[ -n "$EXEC_FILES" ]]; then
while IFS= read -r f; do
is_whitelisted "$f" && ok "$f is whitelisted — skipping" || { warn "Executable file in $dir: $f"; }
done <<< "$EXEC_FILES"
warn "Executable files in $dir:"
log "$EXEC_FILES"
fi
if [[ -n "$JS_FILES" ]]; then
while IFS= read -r f; do
is_whitelisted "$f" && ok "$f is whitelisted — skipping" || { warn "JS file in $dir: $f"; }
done <<< "$JS_FILES"
warn "JS files in $dir:"
log "$JS_FILES"
fi
done
ok "Temp directory scan complete"

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# setup.sh v1.1
# setup.sh
# Run once after cloning on any VM where you want security scanning active.
set -euo pipefail
@@ -9,6 +9,10 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "=== Security Tools Setup ==="
echo ""
# ── Git config ────────────────────────────────────────────────────────────────
git -C "$SCRIPT_DIR" config pull.rebase false
echo "Git pull strategy set to merge."
# ── Telegram credentials ───────────────────────────────────────────────────────
if [[ -f "$SCRIPT_DIR/config.sh" ]]; then
echo "config.sh already exists — skipping credential setup."
@@ -31,33 +35,16 @@ fi
# ── Make scripts executable ────────────────────────────────────────────────────
chmod +x "$SCRIPT_DIR/npm-security-check.sh"
chmod +x "$SCRIPT_DIR/check-nextjs-rce.sh"
chmod +x "$SCRIPT_DIR/check-npm-sudo-config.sh"
# ── Create logs directory ──────────────────────────────────────────────────────
mkdir -p "$SCRIPT_DIR/logs"
# ── Create whitelist if absent ─────────────────────────────────────────────────
if [[ ! -f "$SCRIPT_DIR/whitelist.conf" ]]; then
cat > "$SCRIPT_DIR/whitelist.conf" <<'EOF'
# whitelist.conf — one entry per line, exact match against package names or file paths
# Example:
# ua-parser-js
# /tmp/my-known-safe-script.sh
EOF
echo "whitelist.conf created — add known-safe items to suppress false positives."
fi
# ── Ensure cron is available ──────────────────────────────────────────────────
if ! command -v crontab &>/dev/null; then
echo "cron not found — installing..."
apt install cron -y
systemctl enable cron
systemctl start cron
fi
# ── Cron jobs ──────────────────────────────────────────────────────────────────
CRON_1="0 8 * * * $SCRIPT_DIR/npm-security-check.sh >> $SCRIPT_DIR/logs/npm-security-check-\$(date +\%Y\%m\%d).log 2>&1"
CRON_2="5 8 * * * $SCRIPT_DIR/check-nextjs-rce.sh >> $SCRIPT_DIR/logs/check-nextjs-rce-\$(date +\%Y\%m\%d).log 2>&1"
CRON_3="0 9 * * * find $SCRIPT_DIR/logs -name '*.log' -mtime +60 -delete"
CRON_3="10 8 * * * $SCRIPT_DIR/check-npm-sudo-config.sh >> $SCRIPT_DIR/logs/check-npm-sudo-config-\$(date +\%Y\%m\%d).log 2>&1"
CRON_4="0 9 * * * find $SCRIPT_DIR/logs -name '*.log' -mtime +60 -delete"
EXISTING=$(crontab -l 2>/dev/null || true)
@@ -75,10 +62,17 @@ else
echo "Cron job registered: check-nextjs-rce.sh daily at 08:05."
fi
if echo "$EXISTING" | grep -qF "check-npm-sudo-config.sh"; then
echo "Cron job for check-npm-sudo-config.sh already registered — skipping."
else
(crontab -l 2>/dev/null; echo "$CRON_3") | crontab -
echo "Cron job registered: check-npm-sudo-config.sh daily at 08:10."
fi
if echo "$EXISTING" | grep -qF "logs -name '*.log'"; then
echo "Log cleanup cron already registered — skipping."
else
(crontab -l 2>/dev/null; echo "$CRON_3") | crontab -
(crontab -l 2>/dev/null; echo "$CRON_4") | crontab -
echo "Cron job registered: log cleanup daily at 09:00 (60 day retention)."
fi
@@ -109,5 +103,10 @@ echo ""
echo "Running initial security scan..."
bash "$SCRIPT_DIR/npm-security-check.sh" >> "$SCRIPT_DIR/logs/npm-security-check-$(date +%Y%m%d).log" 2>&1 && echo "npm-security-check: done." || echo "npm-security-check: issues found — check Telegram."
bash "$SCRIPT_DIR/check-nextjs-rce.sh" >> "$SCRIPT_DIR/logs/check-nextjs-rce-$(date +%Y%m%d).log" 2>&1 && echo "check-nextjs-rce: done." || echo "check-nextjs-rce: issues found — check Telegram."
NPM_SUDO_LOG="$SCRIPT_DIR/logs/check-npm-sudo-config-$(date +%Y%m%d).log"
echo ""
echo "--- npm sudo config audit results ---"
bash "$SCRIPT_DIR/check-npm-sudo-config.sh" 2>&1 | tee -a "$NPM_SUDO_LOG"
echo "-------------------------------------"
echo ""
echo "Initial scan complete. Check Telegram for any alerts."

View File

@@ -0,0 +1,75 @@
#!/usr/bin/env bash
set -euo pipefail
log() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1"; }
if [ "$EUID" -ne 0 ]; then
log "ERROR: Please run as root"
exit 1
fi
# Check Tailscale is installed and connected
if ! command -v tailscale &>/dev/null; then
log "ERROR: Tailscale is not installed"
exit 1
fi
TAILSCALE_IP=$(tailscale ip -4 2>/dev/null)
if [ -z "$TAILSCALE_IP" ]; then
log "ERROR: Could not get Tailscale IP — is Tailscale connected?"
exit 1
fi
log "Tailscale IP: ${TAILSCALE_IP}"
# Detect SSH service name
if systemctl is-active --quiet ssh 2>/dev/null; then
SSH_SERVICE="ssh"
elif systemctl is-active --quiet sshd 2>/dev/null; then
SSH_SERVICE="sshd"
else
log "ERROR: No running SSH service found (tried ssh, sshd)"
exit 1
fi
# Use a drop-in file if sshd_config.d exists, otherwise edit sshd_config directly
if [ -d /etc/ssh/sshd_config.d ]; then
DROPIN="/etc/ssh/sshd_config.d/99-tailscale-only.conf"
[ -f "$DROPIN" ] && log "WARNING: ${DROPIN} already exists — overwriting"
cat > "$DROPIN" << EOF
# Bind SSH to Tailscale interface only
# To revert: rm ${DROPIN} && systemctl restart ${SSH_SERVICE}
ListenAddress ${TAILSCALE_IP}
PasswordAuthentication no
PermitEmptyPasswords no
EOF
log "Written: ${DROPIN}"
else
BACKUP="/etc/ssh/sshd_config.bak.$(date +%Y%m%d_%H%M%S)"
cp /etc/ssh/sshd_config "$BACKUP"
log "Backed up sshd_config to ${BACKUP}"
# Remove any existing ListenAddress lines and add ours
sed -i '/^[#]*ListenAddress/d' /etc/ssh/sshd_config
echo "ListenAddress ${TAILSCALE_IP}" >> /etc/ssh/sshd_config
log "Updated /etc/ssh/sshd_config"
fi
# Validate config before restarting
if ! sshd -t; then
log "ERROR: SSH config validation failed — aborting without restart"
[ -n "${DROPIN:-}" ] && rm -f "$DROPIN"
exit 1
fi
log "SSH config validated OK"
systemctl restart "$SSH_SERVICE"
log "SSH service restarted"
log ""
log "Done. SSH is now bound to Tailscale only (${TAILSCALE_IP})"
log "Connect with: ssh root@${TAILSCALE_IP}"
if [ -d /etc/ssh/sshd_config.d ]; then
log "To revert: rm ${DROPIN} && systemctl restart ${SSH_SERVICE}"
else
log "To revert: cp ${BACKUP} /etc/ssh/sshd_config && systemctl restart ${SSH_SERVICE}"
fi