From 03c43cd2a8d184c42bc4a28bfd75d0bde46c4563 Mon Sep 17 00:00:00 2001 From: 247LIKE <54342887+247like@users.noreply.github.com> Date: Mon, 15 Dec 2025 18:39:12 +0800 Subject: [PATCH] Update init.sh --- init.sh | 318 +++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 235 insertions(+), 83 deletions(-) diff --git a/init.sh b/init.sh index ba7d05d..b042560 100644 --- a/init.sh +++ b/init.sh @@ -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)"