Update init.sh

This commit is contained in:
247LIKE authored and GitHub committed 2025-12-15 18:39:12 +08:00
1 parent 2696a28765
commit 03c43cd2a8
1 file changed
+235 -83
+235 -83
View File
@@ -3,7 +3,7 @@
# linux-ssh-init-sh
# Server Init & SSH Hardening Script
#
# Release: v4.5.1 (Fortress Pro - POSIX Final)
# Release: v4.6.0 (Fortress Pro - Security Audit Fix)
#
# POSIX sh compatible (Debian dash / CentOS / Alpine / Ubuntu)
#
@@ -11,15 +11,17 @@
# - If `base64` exists: strict base64 decode + key length checks
# - If `base64` missing: downgrade to format-only validation (skip length checks)
#
# Changelog v4.5.1:
# - Fix: awk match() 3-arg replaced with sed (mawk/BusyBox compat)
# - Fix: install_managed_block uses head/tail instead of awk catfile
# - Fix: IFS restoration handled correctly (unset case)
# - Fix: sshd -T -C only used on OpenSSH >= 6.8
# - Fix: Removed 22 from hard-reserved ports list
# - Fix: Reduced duplicate warnings in key validation
# - Fix: Numeric comparisons with empty value guards
# - Fix: TMP_DIR fallback includes random component
# Changelog v4.6.0:
# - [SEC] Fix: Removed dangerous `. source` of state file (RCE risk)
# - [SEC] Fix: RUNTIME_DIR never falls back to bare /tmp
# - [SEC] Fix: KEY_OK now based on actually deployed keys, not file existence
# - [SEC] Fix: restart_sshd failure now triggers rollback (not just warn)
# - [SEC] Fix: enhanced_ssh_test requires SSH-2.0 banner
# - Fix: --port parameter now validates range and reserved ports
# - Fix: validate_username now allows root (matching documentation)
# - Fix: csv_contains uses case instead of grep (regex safety)
# - Fix: IPv6 detection excludes loopback interface
# - Fix: install_managed_block uses awk instead of tail -n +N
# =========================================================
set -u
@@ -44,7 +46,6 @@ if command -v mktemp >/dev/null 2>&1; then
TMP_DIR=$(mktemp -d 2>/dev/null || mktemp -d -t ssh-init-XXXXXX 2>/dev/null || echo "")
fi
if [ -z "$TMP_DIR" ]; then
# [FIX] Fallback with random component for security
rand_suffix=""
if [ -r /dev/urandom ] && command -v od >/dev/null 2>&1; then
rand_suffix=$(od -An -N4 -tx1 /dev/urandom 2>/dev/null | tr -d ' \n')
@@ -56,14 +57,31 @@ fi
chmod 700 "$TMP_DIR" 2>/dev/null || true
umask "$old_umask"
# ---------------- State & Lock Management ----------------
RUNTIME_DIR="/run/server-init"
if ! mkdir -p "$RUNTIME_DIR" 2>/dev/null; then
RUNTIME_DIR="/var/lib/server-init"
if ! mkdir -p "$RUNTIME_DIR" 2>/dev/null; then
RUNTIME_DIR="/tmp/server-init"
mkdir -p "$RUNTIME_DIR" 2>/dev/null || RUNTIME_DIR="/tmp"
# ---------------- [SEC-FIX] State & Lock Management ----------------
# Never fall back to bare /tmp - always use a private directory
RUNTIME_DIR=""
for try_dir in "/run/server-init" "/var/lib/server-init"; do
if mkdir -p "$try_dir" 2>/dev/null; then
RUNTIME_DIR="$try_dir"
break
fi
done
if [ -z "$RUNTIME_DIR" ]; then
# Must create a private directory under /tmp, never use /tmp directly
rand_rt=""
if [ -r /dev/urandom ] && command -v od >/dev/null 2>&1; then
rand_rt=$(od -An -N4 -tx1 /dev/urandom 2>/dev/null | tr -d ' \n')
fi
[ -z "$rand_rt" ] && rand_rt="$$"
RUNTIME_DIR="/tmp/server-init.${rand_rt}.$(date +%s 2>/dev/null || echo 0)"
if ! mkdir -p "$RUNTIME_DIR" 2>/dev/null; then
# Last resort: use mktemp
if command -v mktemp >/dev/null 2>&1; then
RUNTIME_DIR=$(mktemp -d /tmp/server-init.XXXXXX 2>/dev/null || echo "")
fi
fi
[ -n "$RUNTIME_DIR" ] && [ -d "$RUNTIME_DIR" ] || { echo "FATAL: Cannot create runtime directory" >&2; exit 1; }
fi
chmod 700 "$RUNTIME_DIR" 2>/dev/null || true
@@ -140,9 +158,9 @@ msg() {
if [ "$LANG_CUR" = "zh" ]; then
case "$key" in
MUST_ROOT) echo "必须以 root 权限运行此脚本" ;;
BANNER) echo "服务器初始化 & SSH 安全加固 (v4.5.1 Fortress Pro)" ;;
BANNER) echo "服务器初始化 & SSH 安全加固 (v4.6.0 Fortress Pro)" ;;
STRICT_ON) echo "STRICT 模式已开启:任何关键错误将直接退出" ;;
ASK_USER) echo "SSH 登录用户 (root 或普通用户,默认 " ;;
ASK_USER) echo "SSH 登录用户 (默认 " ;;
ERR_USER_INV) echo "❌ 用户名无效 (仅限小写字母/数字/下划线,且避开系统保留名)" ;;
ASK_PORT_T) echo "SSH 端口配置:" ;;
OPT_PORT_1) echo "1) 使用 22 (默认)" ;;
@@ -181,6 +199,7 @@ msg() {
I_BACKUP) echo "已全量备份配置 (SSH/User/Firewall): " ;;
E_SSHD_CHK) echo "sshd 配置校验失败,正在回滚..." ;;
E_GREP_FAIL) echo "配置验证失败:关键参数未生效,正在回滚..." ;;
E_RESTART) echo "SSH 服务重启失败,正在回滚..." ;;
W_RESTART) echo "无法自动重启 SSH 服务,请手动重启" ;;
W_LISTEN_FAIL) echo "SSHD 已重启但端口未监听,可能启动失败,正在回滚..." ;;
DONE_T) echo "================ 完成 ================" ;;
@@ -262,14 +281,17 @@ msg() {
INFO_OLD_SSH_SKIP_ALGO) echo "ℹ️ OpenSSH较旧或无法检测支持列表:跳过现代加密算法强制配置" ;;
INFO_SANITIZE_DUP) echo "ℹ️ 清理原配置文件中的重复指令..." ;;
INFO_MATCH_INSERT) echo "ℹ️ 检测到 Match 块:托管配置将插入到首个 Match 之前,以避免语法/作用域问题" ;;
ERR_NO_BANNER) echo "❌ 未能获取 SSH-2.0 协议 banner,服务可能未正常启动" ;;
INFO_KEYS_DEPLOYED) echo "✅ 成功部署密钥数量:" ;;
WARN_NO_VALID_KEYS) echo "⚠️ 没有有效的SSH密钥被部署" ;;
*) echo "$key" ;;
esac
else
case "$key" in
MUST_ROOT) echo "Must be run as root" ;;
BANNER) echo "Server Init & SSH Hardening (v4.5.1 Fortress Pro)" ;;
BANNER) echo "Server Init & SSH Hardening (v4.6.0 Fortress Pro)" ;;
STRICT_ON) echo "STRICT mode ON: Critical errors will abort" ;;
ASK_USER) echo "SSH Login User (root or normal user, default " ;;
ASK_USER) echo "SSH Login User (default " ;;
ERR_USER_INV) echo "❌ Invalid username (lowercase/digits/underscore only, no reserved words)" ;;
ASK_PORT_T) echo "SSH Port Configuration:" ;;
OPT_PORT_1) echo "1) Use 22 (Default)" ;;
@@ -308,6 +330,7 @@ msg() {
I_BACKUP) echo "Full backup created (SSH/User/Firewall): " ;;
E_SSHD_CHK) echo "sshd config validation failed, rolling back..." ;;
E_GREP_FAIL) echo "Config validation failed: Critical settings not active. Rolling back..." ;;
E_RESTART) echo "SSH service restart failed, rolling back..." ;;
W_RESTART) echo "Could not restart sshd automatically. Please restart manually." ;;
W_LISTEN_FAIL) echo "SSHD restarted but port is not listening. Rolling back..." ;;
DONE_T) echo "================ DONE ================" ;;
@@ -389,6 +412,9 @@ msg() {
INFO_OLD_SSH_SKIP_ALGO) echo "ℹ️ Old OpenSSH or unable to detect supported lists: skipping forced crypto algorithms" ;;
INFO_SANITIZE_DUP) echo "ℹ️ Sanitizing duplicate directives in original config..." ;;
INFO_MATCH_INSERT) echo "ℹ️ Match blocks detected: inserting managed block before first Match to avoid scope issues" ;;
ERR_NO_BANNER) echo "❌ Failed to get SSH-2.0 protocol banner, service may not be running properly" ;;
INFO_KEYS_DEPLOYED) echo "✅ Number of keys deployed:" ;;
WARN_NO_VALID_KEYS) echo "⚠️ No valid SSH keys were deployed" ;;
*) echo "$key" ;;
esac
fi
@@ -463,7 +489,6 @@ preflight_checks() {
fi
done
# [FIX] Guard against empty value in numeric comparison
available_kb=$(df -k / 2>/dev/null | awk 'NR==2 {print $4}' 2>/dev/null || echo "")
if [ -n "$available_kb" ] && [ "$available_kb" -lt 5120 ] 2>/dev/null; then
warn "$(msg WARN_DISK)${available_kb}KB"
@@ -555,14 +580,7 @@ restart_sshd() {
if [ -x /etc/init.d/ssh ]; then /etc/init.d/ssh restart >>"$LOG_FILE" 2>&1 && res=0; fi
fi
if [ "$res" -ne 0 ]; then
if [ "$STRICT_MODE" = "y" ]; then
die "SSHD Restart Failed (Exit Code: $res)"
else
return 1
fi
fi
return 0
return "$res"
}
# ---------------- Robust Rollback ----------------
@@ -582,21 +600,51 @@ update_state() {
chmod 600 "$STATE_FILE" 2>/dev/null || true
}
# [SEC-FIX] Safe state file parsing - never source untrusted files
parse_state_value() {
key="$1"
file="$2"
if [ -f "$file" ] && [ -r "$file" ]; then
sed -n "s/^${key}=//p" "$file" 2>/dev/null | head -n 1 | tr -d '\r'
fi
}
check_previous_state() {
if [ -f "$STATE_FILE" ]; then
# Security check: ensure we own this file
state_owner=""
if stat -c "%u" "$STATE_FILE" >/dev/null 2>&1; then
state_owner=$(stat -c "%u" "$STATE_FILE" 2>/dev/null)
else
state_owner=$(ls -ln "$STATE_FILE" 2>/dev/null | awk '{print $3}')
fi
current_uid=$(id -u)
if [ "$state_owner" != "$current_uid" ] 2>/dev/null; then
warn "State file owned by different user, ignoring"
rm -f "$STATE_FILE" 2>/dev/null || true
return 0
fi
warn "$(msg WARN_RESUME)"
if [ -r "$STATE_FILE" ]; then
# shellcheck disable=SC1090
. "$STATE_FILE" 2>/dev/null || true
if [ "$AUTO_CONFIRM" != "y" ]; then
printf "%s" "$(msg ASK_RESUME)"
read -r continue_resume
if [ "${continue_resume:-n}" != "y" ]; then
rm -f "$STATE_FILE" 2>/dev/null || true
exit 1
fi
# [SEC-FIX] Safe parsing instead of source
prev_phase=$(parse_state_value "PHASE" "$STATE_FILE")
prev_user=$(parse_state_value "USER" "$STATE_FILE")
prev_port=$(parse_state_value "PORT" "$STATE_FILE")
if [ "$AUTO_CONFIRM" != "y" ]; then
printf "%s" "$(msg ASK_RESUME)"
read -r continue_resume
if [ "${continue_resume:-n}" != "y" ]; then
rm -f "$STATE_FILE" 2>/dev/null || true
exit 1
fi
fi
# Optionally restore previous values
[ -n "$prev_user" ] && [ -z "$ARG_USER" ] && TARGET_USER="$prev_user"
[ -n "$prev_port" ] && [ -z "$ARG_PORT" ] && SSH_PORT="$prev_port"
fi
}
@@ -670,7 +718,7 @@ cleanup_old_backups() {
if [ -n "$count" ] && [ "$count" -gt "$keep_count" ] 2>/dev/null; then
to_rm=$((count - keep_count))
info "$(msg INFO_CLEANING_BACKUPS) $to_rm $(msg INFO_OLD_BACKUPS)"
echo "$backup_list" | tail -n "$to_rm" | while IFS= read -r d; do
echo "$backup_list" | awk -v n="$to_rm" 'NR > (NR - n)' | while IFS= read -r d; do
[ -n "$d" ] && [ -d "$d" ] && rm -rf "$d" 2>/dev/null || true
done
fi
@@ -710,7 +758,7 @@ backup_config_persistent() {
{
echo "=== Server Init Backup ==="
echo "Time: $(date)"
echo "Version: 4.5.1"
echo "Version: 4.6.0"
echo "User: ${TARGET_USER:-unknown}"
echo "Port: ${SSH_PORT:-unknown}"
echo "OpenSSH: ${OPENSSH_VER_MAJOR}.${OPENSSH_VER_MINOR}"
@@ -786,7 +834,6 @@ EOF
fi
}
# [FIX] Detect OpenSSH version using sed (POSIX compatible, no gawk match())
detect_openssh_version() {
OPENSSH_VER_MAJOR=0
OPENSSH_VER_MINOR=0
@@ -805,7 +852,6 @@ detect_openssh_version() {
OPENSSH_VER_MINOR=$(echo "$ver_str" | cut -d. -f2 2>/dev/null || echo 0)
fi
# [FIX] Proper numeric check
case "$OPENSSH_VER_MAJOR" in
''|*[!0-9]*) OPENSSH_VER_MAJOR=7 ;;
esac
@@ -868,7 +914,6 @@ handle_selinux() {
}
# ---------------- Port Logic ----------------
# [FIX] Removed 22 from hard-reserved list (it's the default SSH port)
is_hard_reserved() {
case "$1" in
53|80|443|3306)
@@ -962,13 +1007,40 @@ pick_random_port() {
return 1
}
# [SEC-FIX] Validate port number for command line arguments
validate_port() {
port="$1"
# Must be numeric
case "$port" in
''|*[!0-9]*) return 1 ;;
esac
# Must be in valid range
[ "$port" -ge 1 ] 2>/dev/null && [ "$port" -le 65535 ] 2>/dev/null || return 1
# Check reserved ports (allow 22)
if [ "$port" -lt 1024 ] && [ "$port" != "22" ]; then
return 1
fi
return 0
}
# ---------------- User & Sudo ----------------
# [SEC-FIX] Allow root username (matching documentation)
validate_username() {
u="$1"
# Allow root explicitly
[ "$u" = "root" ] && return 0
len=${#u}
[ "$len" -ge 2 ] && [ "$len" -le 32 ] || return 1
echo "$u" | grep -Eq '^[a-z_][a-z0-9_-]*$' || return 1
case "$u" in root|bin|daemon|adm|lp|sync|shutdown|halt|mail|operator|games|ftp|nobody) return 1 ;; esac
# Block system reserved names (except root which is handled above)
case "$u" in bin|daemon|adm|lp|sync|shutdown|halt|mail|operator|games|ftp|nobody) return 1 ;; esac
return 0
}
@@ -1143,7 +1215,6 @@ fetch_keys() {
return 1
}
# [FIX] Simplified: only return clean line or empty (caller handles warning)
validate_ssh_key_line() {
line="$1"
@@ -1177,6 +1248,7 @@ validate_ssh_key_line() {
return 0
}
# [SEC-FIX] Improved deploy_keys - returns success only if THIS deployment added valid keys
deploy_keys() {
user="$1"
keys="$2"
@@ -1193,17 +1265,51 @@ deploy_keys() {
chown -R "$user" "$dir" 2>/dev/null || true
fi
# Track keys we successfully deploy in this run
valid_keys_file="$TMP_DIR/valid_keys.$$"
deployed_count=0
skipped_count=0
: > "$valid_keys_file"
# First pass: validate and collect valid keys
printf "%s\n" "$keys" | while IFS= read -r line; do
[ -z "$line" ] && continue
clean_line=$(validate_ssh_key_line "$line")
if [ -n "$clean_line" ]; then
grep -qxF "$clean_line" "$auth" 2>/dev/null || printf "%s\n" "$clean_line" >>"$auth"
printf "%s\n" "$clean_line" >> "$valid_keys_file"
fi
done
[ -s "$auth" ]
# Check if we have any valid keys from this input
if [ ! -s "$valid_keys_file" ]; then
warn "$(msg WARN_NO_VALID_KEYS)"
rm -f "$valid_keys_file" 2>/dev/null || true
return 1
fi
# Second pass: deploy valid keys and verify they exist in authorized_keys
while IFS= read -r key; do
[ -z "$key" ] && continue
# Add if not already present
if ! grep -qxF "$key" "$auth" 2>/dev/null; then
printf "%s\n" "$key" >> "$auth"
fi
# Verify it's now in the file
if grep -qxF "$key" "$auth" 2>/dev/null; then
deployed_count=$((deployed_count + 1))
fi
done < "$valid_keys_file"
rm -f "$valid_keys_file" 2>/dev/null || true
# Success only if at least one key from THIS deployment is in the file
if [ "$deployed_count" -gt 0 ]; then
info "$(msg INFO_KEYS_DEPLOYED) $deployed_count"
return 0
else
warn "$(msg WARN_NO_VALID_KEYS)"
return 1
fi
}
# ---------------- sshd_config management ----------------
@@ -1256,27 +1362,39 @@ sanitize_sshd_config() {
fi
}
# [SEC-FIX] Improved IPv6 detection - exclude loopback
has_global_ipv6() {
if [ -f /proc/net/if_inet6 ]; then
grep -v '^fe80' /proc/net/if_inet6 2>/dev/null | grep -q '^[0-9a-f]' && return 0
fi
# Prefer ip command for accurate scope detection
if command -v ip >/dev/null 2>&1; then
ip -6 addr show scope global 2>/dev/null | grep -q inet6 && return 0
fi
# Fallback to /proc but exclude loopback interface
if [ -f /proc/net/if_inet6 ]; then
# Format: address ifindex prefix_len scope flags ifname
# Field 4 is scope: 00=global, 20=link-local, 10=host(loopback)
# Field 6 is interface name
awk '$4 == "00" && $6 != "lo" { found=1; exit } END { exit !found }' /proc/net/if_inet6 2>/dev/null && return 0
fi
if command -v ifconfig >/dev/null 2>&1; then
ifconfig 2>/dev/null | grep -i 'inet6.*global' >/dev/null 2>&1 && return 0
fi
return 1
}
# ----- Crypto selection -----
# [SEC-FIX] Use case for csv_contains instead of grep (regex safety)
csv_contains() {
csv="$1"
item="$2"
echo ",$csv," | grep -q ",$item," 2>/dev/null
case ",$csv," in
*,"$item",*) return 0 ;;
*) return 1 ;;
esac
}
# [FIX] Proper IFS restoration handling unset case
csv_intersect_ordered() {
pref="$1"
supp="$2"
@@ -1296,7 +1414,6 @@ csv_intersect_ordered() {
echo "$result" | tr '\n' ',' | sed 's/,$//'
}
# [FIX] Only use -C flag on OpenSSH >= 6.8
get_sshd_T_value() {
key="$1"
v=""
@@ -1364,7 +1481,7 @@ build_block() {
file="$1"
{
echo "$BLOCK_BEGIN"
echo "# Managed by server-init v4.5.1"
echo "# Managed by server-init v4.6.0"
echo "# Generated: $(date)"
echo "# OpenSSH: ${OPENSSH_VER_MAJOR}.${OPENSSH_VER_MINOR}"
echo "# Do NOT edit inside this block. Changes will be overwritten."
@@ -1425,7 +1542,7 @@ build_block() {
} >"$file"
}
# [FIX] Use head/tail instead of awk catfile for better compatibility
# [SEC-FIX] Use awk instead of tail -n +N for better POSIX compatibility
install_managed_block() {
block="$1"
tmp="$TMP_DIR/sshd_config.merge"
@@ -1438,10 +1555,16 @@ install_managed_block() {
cat "$SSH_CONF" "$block" > "$tmp"
else
info "$(msg INFO_MATCH_INSERT)"
# Insert before Match block using head/tail
head -n $((match_line - 1)) "$SSH_CONF" > "$tmp"
cat "$block" >> "$tmp"
tail -n +$match_line "$SSH_CONF" >> "$tmp"
# Insert before Match block using awk
awk -v ml="$match_line" -v bf="$block" '
NR < ml { print }
NR == ml {
while ((getline line < bf) > 0) print line
close(bf)
print
}
NR > ml { print }
' "$SSH_CONF" > "$tmp"
fi
chmod 600 "$tmp" 2>/dev/null || true
@@ -1468,6 +1591,7 @@ verify_sshd_listening() {
return 1
}
# [SEC-FIX] Enhanced SSH test - require SSH-2.0 banner
enhanced_ssh_test() {
port="$1"
user="$2"
@@ -1478,16 +1602,34 @@ enhanced_ssh_test() {
return 1
fi
# [SEC-FIX] Banner check is now required, not optional
banner_ok=0
if command -v nc >/dev/null 2>&1; then
proto=""
if command -v timeout >/dev/null 2>&1; then
proto=$(printf "SSH-2.0-TEST\r\n" | timeout 2 nc localhost "$port" 2>/dev/null || true)
proto=$(printf "SSH-2.0-TEST\r\n" | timeout 3 nc localhost "$port" 2>/dev/null || true)
else
proto=$(printf "SSH-2.0-TEST\r\n" | nc -w 2 localhost "$port" 2>/dev/null || true)
proto=$(printf "SSH-2.0-TEST\r\n" | nc -w 3 localhost "$port" 2>/dev/null || true)
fi
echo "$proto" | grep -q "SSH-2.0" && ok "$(msg INFO_SSH_PROTOCOL_OK)" || warn "$(msg WARN_SSH_PROTOCOL)"
if echo "$proto" | grep -q "SSH-2.0"; then
ok "$(msg INFO_SSH_PROTOCOL_OK)"
banner_ok=1
else
err "$(msg ERR_NO_BANNER)"
fi
else
# If nc is not available, we can't verify banner - consider it passed with warning
warn "nc not available, skipping banner check"
banner_ok=1
fi
# [SEC-FIX] Must have valid SSH banner to pass
if [ "$banner_ok" -ne 1 ]; then
return 1
fi
# SSH client test - this may fail without the right key, but banner was OK
attempts=1
max_attempts=3
success=0
@@ -1506,18 +1648,12 @@ enhanced_ssh_test() {
if [ "$success" -eq 1 ]; then
ok "$(msg TEST_OK)"
return 0
else
# Banner was OK but auth failed - this is expected without the right key
warn "$(msg WARN_PORT_OPEN_BUT_FAIL)"
fi
if command -v nc >/dev/null 2>&1; then
if nc -z -w 2 localhost "$port" 2>/dev/null; then
warn "$(msg WARN_PORT_OPEN_BUT_FAIL)"
return 0
fi
fi
err "$(msg TEST_FAIL)"
return 1
return 0
}
update_motd() {
@@ -1549,7 +1685,7 @@ generate_health_report() {
{
echo "=== Server Init Health Report ==="
echo "Generated: $(date)"
echo "Version: v4.5.1 Fortress Pro"
echo "Version: v4.6.0 Fortress Pro"
echo "Execution Time: ${duration}s"
echo ""
echo "--- System ---"
@@ -1635,9 +1771,9 @@ validate_ssh_config_comprehensive() {
return 1
fi
password_auth=$(grep -E '^[[:space:]]*PasswordAuthentication[[:space:]]' "$config_file" 2>/dev/null | tail -1 | awk '{print $2}')
pubkey_auth=$(grep -E '^[[:space:]]*PubkeyAuthentication[[:space:]]' "$config_file" 2>/dev/null | tail -1 | awk '{print $2}')
port_setting=$(grep -E '^[[:space:]]*Port[[:space:]]' "$config_file" 2>/dev/null | tail -1 | awk '{print $2}')
password_auth=$(grep -E '^[[:space:]]*PasswordAuthentication[[:space:]]' "$config_file" 2>/dev/null | awk '{print $NF}' | tr -d '\r')
pubkey_auth=$(grep -E '^[[:space:]]*PubkeyAuthentication[[:space:]]' "$config_file" 2>/dev/null | awk '{print $NF}' | tr -d '\r')
port_setting=$(grep -E '^[[:space:]]*Port[[:space:]]' "$config_file" 2>/dev/null | awk '{print $NF}' | tr -d '\r')
[ -n "$password_auth" ] || password_auth="yes"
[ -n "$pubkey_auth" ] || pubkey_auth="yes"
@@ -1699,11 +1835,24 @@ else
done
fi
# [SEC-FIX] Validate --port parameter
if [ -n "$ARG_PORT" ]; then
case "$ARG_PORT" in
22) PORT_OPT="1"; SSH_PORT="22" ;;
random) PORT_OPT="2"; SSH_PORT="22" ;;
*) PORT_OPT="3"; SSH_PORT="$ARG_PORT" ;;
*)
# Validate the port
if ! validate_port "$ARG_PORT"; then
die "$(msg PORT_ERR): $ARG_PORT"
fi
if is_hard_reserved "$ARG_PORT"; then
die "$(msg PORT_RES): $ARG_PORT"
fi
if is_k8s_nodeport "$ARG_PORT"; then
warn "$(msg PORT_K8S)"
fi
PORT_OPT="3"; SSH_PORT="$ARG_PORT"
;;
esac
printf "%s%s\n" "$(msg AUTO_SKIP)" "$ARG_PORT (Mode $PORT_OPT)"
else
@@ -1846,7 +1995,10 @@ if [ "$ARG_DELAY_RESTART" != "y" ]; then protect_sshd_service; fi
if ! sshd -t -f "$SSH_CONF" 2>>"$LOG_FILE"; then die "$(msg E_SSHD_CHK)"; fi
validate_ssh_config_comprehensive "$SSH_CONF" "$TARGET_USER" "$KEY_OK"
restart_sshd || warn "$(msg W_RESTART)"
# [SEC-FIX] restart_sshd failure now triggers rollback
if ! restart_sshd; then
die "$(msg E_RESTART)"
fi
grep -Eq "^[[:space:]]*Port[[:space:]]+$SSH_PORT([[:space:]]|\$)" "$SSH_CONF" 2>/dev/null || die "$(msg E_GREP_FAIL)"
verify_sshd_listening "$SSH_PORT" || die "$(msg W_LISTEN_FAIL)"