Release v4.0.0: Platinum Edition - Health Reports, Metadata Backups & POSIX Compliance Major release v4.0.0 brings enterprise-grade reliability and strict POSIX compliance. New Features: - Feat: Added comprehensive `preflight_checks` to verify core tools, disk space, and memory before execution. - Feat: Implemented triple-redundancy IPv6 detection (checking `/proc`, `ip`, and `ifconfig`) to ensure accuracy on minimal/container systems. - Feat: Added persistent, versioned backups in `/var/backups/ssh-config/` with accompanying `.meta` files for audit trails. - Feat: Automatically generates a detailed `Health Report` upon successful completion. Fixes & Improvements: - Refactor: Replaced all Bash-isms (e.g., arrays) with strict POSIX string handling for full Alpine/Dash compatibility. - Fix: Clarified random port generation logic to strictly avoid Kubernetes NodePort range (30000-32767). - Security: Enhanced temporary directory handling using `mktemp` and traps for secure cleanup.
1255 lines
41 KiB
Bash
1255 lines
41 KiB
Bash
#!/bin/sh
|
|
# =========================================================
|
|
# linux-ssh-init-sh
|
|
# Server Init & SSH Hardening Script
|
|
#
|
|
# Release: v4.0.0 (Platinum Edition)
|
|
#
|
|
# POSIX sh compatible (works on Debian/CentOS/Alpine/Ubuntu)
|
|
#
|
|
# Change Log v4.0.0:
|
|
# - FEAT: Added 'preflight_checks' for core commands/disk/mem
|
|
# - FEAT: Robust Triple-Check IPv6 detection (Proc/IP/Ifconfig)
|
|
# - FEAT: Backup metadata generation (.meta files)
|
|
# - FEAT: Final Health Report generation
|
|
# - FIX: Random port math comment clarification
|
|
# - FIX: Replaced bash-isms (arrays) with POSIX string handling
|
|
# =========================================================
|
|
|
|
set -u
|
|
SCRIPT_START_TIME=$(date +%s)
|
|
|
|
# ---------------- Configuration ----------------
|
|
LANG_CUR="zh" # Default Language
|
|
LOG_FILE="/var/log/server-init.log"
|
|
AUDIT_FILE="/var/log/server-init-audit.log"
|
|
BACKUP_REPO="/var/backups/ssh-config"
|
|
SSH_CONF="/etc/ssh/sshd_config"
|
|
SSH_CONF_D="/etc/ssh/sshd_config.d"
|
|
DEFAULT_USER="deploy"
|
|
BLOCK_BEGIN="# BEGIN SERVER-INIT MANAGED BLOCK"
|
|
BLOCK_END="# END SERVER-INIT MANAGED BLOCK"
|
|
|
|
# Create Secure Temp Directory
|
|
TMP_DIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'ssh-init-XXXXXX')
|
|
chmod 700 "$TMP_DIR"
|
|
|
|
# Colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
CYAN='\033[0;36m'
|
|
NC='\033[0m' # No Color
|
|
|
|
# ---------------- Automation Variables ----------------
|
|
ARG_USER=""
|
|
ARG_PORT=""
|
|
ARG_KEY_TYPE=""
|
|
ARG_KEY_VAL=""
|
|
ARG_UPDATE=""
|
|
ARG_BBR=""
|
|
AUTO_CONFIRM="n"
|
|
STRICT_MODE="n"
|
|
ARG_DELAY_RESTART="n"
|
|
|
|
# Parse Arguments
|
|
for a in "$@"; do
|
|
case "$a" in
|
|
--lang=zh) LANG_CUR="zh" ;;
|
|
--lang=en) LANG_CUR="en" ;;
|
|
--strict) STRICT_MODE="y" ;;
|
|
--yes) AUTO_CONFIRM="y" ;;
|
|
--user=*) ARG_USER="${a#*=}" ;;
|
|
--port=random) ARG_PORT="random" ;;
|
|
--port=*) ARG_PORT="${a#*=}" ;;
|
|
--key-gh=*) ARG_KEY_TYPE="gh"; ARG_KEY_VAL="${a#*=}" ;;
|
|
--key-url=*) ARG_KEY_TYPE="url"; ARG_KEY_VAL="${a#*=}" ;;
|
|
--key-raw=*) ARG_KEY_TYPE="raw"; ARG_KEY_VAL="${a#*=}" ;;
|
|
--update) ARG_UPDATE="y" ;;
|
|
--no-update) ARG_UPDATE="n" ;;
|
|
--bbr) ARG_BBR="y" ;;
|
|
--no-bbr) ARG_BBR="n" ;;
|
|
--delay-restart) ARG_DELAY_RESTART="y" ;;
|
|
esac
|
|
done
|
|
|
|
# ---------------- Logging & Audit ----------------
|
|
touch "$LOG_FILE" "$AUDIT_FILE" 2>/dev/null || true
|
|
chmod 600 "$LOG_FILE" "$AUDIT_FILE" 2>/dev/null || true
|
|
|
|
log() { echo "$(date '+%F %T') $*" >>"$LOG_FILE"; }
|
|
|
|
audit_log() {
|
|
action="$1"
|
|
details="$2"
|
|
{
|
|
echo "=== $(date '+%F %T') ==="
|
|
echo "ACTION: $action"
|
|
echo "USER: $(whoami 2>/dev/null || echo root)"
|
|
echo "DETAILS: $details"
|
|
echo "---"
|
|
} >> "$AUDIT_FILE" 2>/dev/null || true
|
|
log "[AUDIT] $action - $details"
|
|
}
|
|
|
|
info() { printf "${BLUE}[INFO]${NC} %s\n" "$*"; log "[INFO] $*"; }
|
|
warn() { printf "${YELLOW}[WARN]${NC} %s\n" "$*"; log "[WARN] $*"; }
|
|
err() { printf "${RED}[ERR ]${NC} %s\n" "$*"; log "[ERR ] $*"; }
|
|
ok() { printf "${GREEN}[ OK ]${NC} %s\n" "$*"; log "[OK] $*"; }
|
|
|
|
die() {
|
|
err "$*"
|
|
exit 1
|
|
}
|
|
|
|
# ---------------- Internationalization ----------------
|
|
msg() {
|
|
key="$1"
|
|
if [ "$LANG_CUR" = "zh" ]; then
|
|
case "$key" in
|
|
MUST_ROOT) echo "必须以 root 权限运行此脚本" ;;
|
|
BANNER) echo "服务器初始化 & SSH 安全加固 (v4.0.0 Platinum)" ;;
|
|
STRICT_ON) echo "STRICT 模式已开启:任何关键错误将直接退出" ;;
|
|
ASK_USER) echo "SSH 登录用户 (root 或普通用户,默认 " ;;
|
|
ERR_USER_INV) echo "❌ 用户名无效 (仅限小写字母/数字/下划线,且避开系统保留名)" ;;
|
|
ASK_PORT_T) echo "SSH 端口配置:" ;;
|
|
OPT_PORT_1) echo "1) 使用 22 (默认)" ;;
|
|
OPT_PORT_2) echo "2) 随机高端口 (49152+, 自动避开 K8s)" ;;
|
|
OPT_PORT_3) echo "3) 手动指定" ;;
|
|
SELECT) echo "请选择 [1-3]: " ;;
|
|
INPUT_PORT) echo "请输入端口号 (1024-65535): " ;;
|
|
PORT_ERR) echo "❌ 端口输入无效 (非数字或超范围)" ;;
|
|
PORT_RES) echo "❌ 端口被系统保留或不建议使用 (如 80, 443, 3306 等)" ;;
|
|
PORT_K8S) echo "⚠️ 警告: 此端口位于 Kubernetes NodePort 常用范围 (30000-32767),可能冲突" ;;
|
|
ASK_KEY_T) echo "SSH 公钥来源:" ;;
|
|
OPT_KEY_1) echo "1) GitHub 用户导入" ;;
|
|
OPT_KEY_2) echo "2) URL 下载" ;;
|
|
OPT_KEY_3) echo "3) 手动粘贴" ;;
|
|
INPUT_GH) echo "请输入 GitHub 用户名: " ;;
|
|
INPUT_URL) echo "请输入公钥 URL: " ;;
|
|
INPUT_RAW) echo "请粘贴公钥内容 (空行结束输入): " ;;
|
|
ASK_UPD) echo "是否更新系统软件包? [y/n] (默认 n): " ;;
|
|
ASK_BBR) echo "是否开启 BBR 加速? [y/n] (默认 n): " ;;
|
|
CONFIRM_T) echo "---------------- 执行确认 ----------------" ;;
|
|
C_USER) echo "登录用户: " ;;
|
|
C_PORT) echo "端口模式: " ;;
|
|
C_KEY) echo "密钥来源: " ;;
|
|
C_UPD) echo "系统更新: " ;;
|
|
C_BBR) echo "开启 BBR: " ;;
|
|
WARN_FW) echo "⚠ 注意:修改端口前,请确认云厂商防火墙/安全组已放行对应 TCP 端口" ;;
|
|
ASK_SURE) echo "确认执行? [y/n]: " ;;
|
|
CANCEL) echo "已取消操作" ;;
|
|
I_INSTALL) echo "正在安装基础依赖..." ;;
|
|
I_UPD) echo "正在更新系统..." ;;
|
|
I_BBR) echo "正在配置 BBR..." ;;
|
|
I_USER) echo "正在配置用户..." ;;
|
|
I_SSH_INSTALL) echo "未检测到 OpenSSH,正在安装..." ;;
|
|
I_KEY_OK) echo "公钥部署成功" ;;
|
|
W_KEY_FAIL) echo "公钥部署失败,将保留密码登录以防失联" ;;
|
|
I_BACKUP) echo "已全量备份配置 (SSH/User/Firewall): " ;;
|
|
E_SSHD_CHK) echo "sshd 配置校验失败,正在回滚..." ;;
|
|
E_GREP_FAIL) echo "配置验证失败:关键参数未生效,正在回滚..." ;;
|
|
W_RESTART) echo "无法自动重启 SSH 服务,请手动重启" ;;
|
|
W_LISTEN_FAIL) echo "SSHD 已重启但端口未监听,可能启动失败,正在回滚..." ;;
|
|
DONE_T) echo "================ 完成 ================" ;;
|
|
DONE_MSG1) echo "请【不要关闭】当前窗口。" ;;
|
|
DONE_MSG2) echo "请新开一个终端窗口测试登录:" ;;
|
|
DONE_FW) echo "⚠ 若无法连接,请再次检查防火墙设置" ;;
|
|
AUTO_SKIP) echo "检测到参数输入,跳过询问: " ;;
|
|
RB_START) echo "脚本执行出现关键错误,开始自动回滚..." ;;
|
|
RB_DONE) echo "回滚完成。系统状态已恢复。" ;;
|
|
RB_FAIL) echo "致命错误:回滚失败!请立即手动检查 /etc/ssh/sshd_config" ;;
|
|
SELINUX_DET) echo "检测到 SELinux Enforcing 模式,正在配置端口规则..." ;;
|
|
SELINUX_OK) echo "SELinux 端口规则添加成功" ;;
|
|
SELINUX_FAIL) echo "SELinux 规则添加失败,请手动执行: semanage port -a -t ssh_port_t -p tcp PORT" ;;
|
|
SELINUX_INS) echo "正在安装 SELinux 管理工具..." ;;
|
|
CLEAN_D) echo "检测到冲突的配置片段,已备份并移除: " ;;
|
|
TEST_CONN) echo "正在进行 SSH 连接测试 (IPv4/IPv6/Local)..." ;;
|
|
TEST_OK) echo "SSH 连接测试通过" ;;
|
|
TEST_FAIL) echo "SSH 连接测试全部失败!新配置可能无法连接,正在回滚..." ;;
|
|
IPV6_CFG) echo "检测到全局 IPv6 环境,已添加 :: 监听支持" ;;
|
|
SYS_PROT) echo "正在添加 systemd 服务防误杀保护..." ;;
|
|
MOTD_UPD) echo "正在更新登录提示信息 (MotD)..." ;;
|
|
COMPAT_WARN) echo "检测到旧版 OpenSSH,自动调整配置兼容性..." ;;
|
|
AUDIT_START) echo "开始执行审计记录..." ;;
|
|
BOX_TITLE) echo "初始化完成 - 安全配置已生效" ;;
|
|
BOX_SSH) echo "SSH 连接信息:" ;;
|
|
BOX_KEY_ON) echo "🔐 密钥认证: 已启用 (密码登录已禁用)" ;;
|
|
BOX_KEY_OFF) echo "⚠️ 密钥认证: 未启用 (密码登录保持可用)" ;;
|
|
BOX_PORT) echo "📍 端口变更: 22 → " ;;
|
|
BOX_FW) echo "⚠️ 请确认防火墙已开放 TCP 端口" ;;
|
|
BOX_WARN) echo "重要: 请在新窗口中测试连接,确认成功后再关闭此窗口!" ;;
|
|
BOX_K8S_WARN) echo "⚠️ 注意: 使用了 Kubernetes NodePort 范围端口" ;;
|
|
ERR_MISSING) echo "❌ 缺少必要命令,无法继续: " ;;
|
|
WARN_DISK) echo "⚠️ 磁盘空间不足: " ;;
|
|
WARN_MEM) echo "⚠️ 可用内存不足: " ;;
|
|
*) echo "$key" ;;
|
|
esac
|
|
else
|
|
# English Full Support
|
|
case "$key" in
|
|
MUST_ROOT) echo "Must be run as root" ;;
|
|
BANNER) echo "Server Init & SSH Hardening (v4.0.0 Platinum)" ;;
|
|
STRICT_ON) echo "STRICT mode ON: Critical errors will abort" ;;
|
|
ASK_USER) echo "SSH Login User (root or normal 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)" ;;
|
|
OPT_PORT_2) echo "2) Random High Port (49152+, avoids K8s)" ;;
|
|
OPT_PORT_3) echo "3) Manual Input" ;;
|
|
SELECT) echo "Select [1-3]: " ;;
|
|
INPUT_PORT) echo "Enter Port (1024-65535): " ;;
|
|
PORT_ERR) echo "❌ Invalid port (not numeric or out of range)" ;;
|
|
PORT_RES) echo "❌ Port is reserved (e.g. 80, 443, 3306)" ;;
|
|
PORT_K8S) echo "⚠️ Warning: Port falls in Kubernetes NodePort range (30000-32767)" ;;
|
|
ASK_KEY_T) echo "SSH Public Key Source:" ;;
|
|
OPT_KEY_1) echo "1) GitHub User" ;;
|
|
OPT_KEY_2) echo "2) URL Download" ;;
|
|
OPT_KEY_3) echo "3) Manual Paste" ;;
|
|
INPUT_GH) echo "Enter GitHub Username: " ;;
|
|
INPUT_URL) echo "Enter Key URL: " ;;
|
|
INPUT_RAW) echo "Paste Key (Empty line to finish): " ;;
|
|
ASK_UPD) echo "Update system packages? [y/n] (default n): " ;;
|
|
ASK_BBR) echo "Enable TCP BBR? [y/n] (default n): " ;;
|
|
CONFIRM_T) echo "---------------- Confirmation ----------------" ;;
|
|
C_USER) echo "User: " ;;
|
|
C_PORT) echo "Port: " ;;
|
|
C_KEY) echo "Key Source: " ;;
|
|
C_UPD) echo "Update: " ;;
|
|
C_BBR) echo "Enable BBR: " ;;
|
|
WARN_FW) echo "⚠ WARNING: Ensure Cloud Firewall/Security Group allows the new TCP port" ;;
|
|
ASK_SURE) echo "Proceed? [y/n]: " ;;
|
|
CANCEL) echo "Cancelled." ;;
|
|
I_INSTALL) echo "Installing dependencies..." ;;
|
|
I_UPD) echo "Updating system..." ;;
|
|
I_BBR) echo "Configuring BBR..." ;;
|
|
I_USER) echo "Configuring user..." ;;
|
|
I_SSH_INSTALL) echo "OpenSSH not found, installing..." ;;
|
|
I_KEY_OK) echo "SSH Key deployed successfully" ;;
|
|
W_KEY_FAIL) echo "Key deployment failed. Password login kept enabled to avoid lockout." ;;
|
|
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..." ;;
|
|
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 ================" ;;
|
|
DONE_MSG1) echo "Please DO NOT close this window yet." ;;
|
|
DONE_MSG2) echo "Open a NEW terminal to test login:" ;;
|
|
DONE_FW) echo "⚠ If connection fails, check your Firewall settings." ;;
|
|
AUTO_SKIP) echo "Argument detected, skipping prompt: " ;;
|
|
RB_START) echo "Critical error. Starting automatic rollback..." ;;
|
|
RB_DONE) echo "Rollback complete. System state restored." ;;
|
|
RB_FAIL) echo "FATAL: Rollback failed! Manually check /etc/ssh/sshd_config" ;;
|
|
SELINUX_DET) echo "SELinux Enforcing detected. Configuring port rules..." ;;
|
|
SELINUX_OK) echo "SELinux port rule added successfully." ;;
|
|
SELINUX_FAIL) echo "SELinux rule failed. Manually run: semanage port -a -t ssh_port_t -p tcp PORT" ;;
|
|
SELINUX_INS) echo "Installing SELinux management tools..." ;;
|
|
CLEAN_D) echo "Detected conflicting config fragment, backed up and removed: " ;;
|
|
TEST_CONN) echo "Testing SSH connection (IPv4/IPv6/Local)..." ;;
|
|
TEST_OK) echo "SSH connection test passed." ;;
|
|
TEST_FAIL) echo "SSH connection test FAILED! Rolling back..." ;;
|
|
IPV6_CFG) echo "Global IPv6 detected. Added listen address :: support." ;;
|
|
SYS_PROT) echo "Adding systemd service protection (anti-kill)..." ;;
|
|
MOTD_UPD) echo "Updating Message of the Day (MotD)..." ;;
|
|
COMPAT_WARN) echo "Older OpenSSH detected. Adjusting compatibility settings..." ;;
|
|
AUDIT_START) echo "Starting audit logging..." ;;
|
|
BOX_TITLE) echo "Init Complete - Security Applied" ;;
|
|
BOX_SSH) echo "SSH Connection Info:" ;;
|
|
BOX_KEY_ON) echo "🔐 Key Auth: ENABLED (Password Disabled)" ;;
|
|
BOX_KEY_OFF) echo "⚠️ Key Auth: DISABLED (Password Enabled)" ;;
|
|
BOX_PORT) echo "📍 Port Change: 22 → " ;;
|
|
BOX_FW) echo "⚠️ Verify Firewall Open for TCP Port" ;;
|
|
BOX_WARN) echo "IMPORTANT: Test connection in NEW window before closing this one!" ;;
|
|
BOX_K8S_WARN) echo "⚠️ NOTE: Using K8s NodePort range" ;;
|
|
ERR_MISSING) echo "❌ Missing essential commands: " ;;
|
|
WARN_DISK) echo "⚠️ Low disk space: " ;;
|
|
WARN_MEM) echo "⚠️ Low memory: " ;;
|
|
*) echo "$key" ;;
|
|
esac
|
|
fi
|
|
}
|
|
|
|
# =========================================================
|
|
# Core Logic
|
|
# =========================================================
|
|
|
|
# v4.0.0: Preflight Checks (POSIX compatible)
|
|
preflight_checks() {
|
|
# Check essential commands
|
|
essential_cmds="cat grep awk sed cp mv chmod chown mkdir rm"
|
|
missing_cmds=""
|
|
|
|
for cmd in $essential_cmds; do
|
|
if ! command -v "$cmd" >/dev/null 2>&1; then
|
|
missing_cmds="$missing_cmds $cmd"
|
|
fi
|
|
done
|
|
|
|
if [ -n "$missing_cmds" ]; then
|
|
die "$(msg ERR_MISSING)$missing_cmds"
|
|
fi
|
|
|
|
# Check Disk Space (Need ~5MB)
|
|
available_kb=$(df -k / | awk 'NR==2 {print $4}' 2>/dev/null || echo 99999)
|
|
if [ "$available_kb" -lt 5120 ]; then
|
|
warn "$(msg WARN_DISK)${available_kb}KB"
|
|
fi
|
|
|
|
# Check Memory (Need ~50MB)
|
|
if [ -f /proc/meminfo ]; then
|
|
mem_avail=$(grep MemAvailable /proc/meminfo | awk '{print $2}' 2>/dev/null || echo 999999)
|
|
if [ "$mem_avail" -lt 51200 ]; then
|
|
warn "$(msg WARN_MEM)${mem_avail}KB"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# ---------------- Robust Rollback ----------------
|
|
# Use secure temp dir for backups too
|
|
ROLLBACK_DIR="$TMP_DIR/rollback"
|
|
setup_rollback() {
|
|
mkdir -p "$ROLLBACK_DIR"
|
|
|
|
# 1. Config Backup
|
|
[ -f "$SSH_CONF" ] && cp -p "$SSH_CONF" "$ROLLBACK_DIR/sshd_config"
|
|
if [ -d "$SSH_CONF_D" ]; then
|
|
mkdir -p "$ROLLBACK_DIR/sshd_config.d"
|
|
cp -p "$SSH_CONF_D"/* "$ROLLBACK_DIR/sshd_config.d/" 2>/dev/null || true
|
|
fi
|
|
|
|
# 2. User/Shadow Backup
|
|
cp -p /etc/passwd /etc/shadow /etc/group "$ROLLBACK_DIR/" 2>/dev/null || true
|
|
[ -d /etc/sudoers.d ] && cp -rp /etc/sudoers.d "$ROLLBACK_DIR/" 2>/dev/null || true
|
|
|
|
# 3. Firewall State Backup
|
|
if command -v iptables-save >/dev/null 2>&1; then
|
|
iptables-save > "$ROLLBACK_DIR/iptables.backup" 2>/dev/null || true
|
|
fi
|
|
|
|
# Catch signals (Wait for exit code in handler)
|
|
trap 'rollback_handler' INT TERM EXIT HUP
|
|
}
|
|
|
|
# v4.0.0: Persistent Versioned Backup with Metadata
|
|
backup_config_persistent() {
|
|
timestamp=$(date +%Y%m%d_%H%M%S)
|
|
mkdir -p "$BACKUP_REPO"
|
|
chmod 700 "$BACKUP_REPO" 2>/dev/null || true
|
|
|
|
if [ -f "$SSH_CONF" ]; then
|
|
cp -p "$SSH_CONF" "$BACKUP_REPO/sshd_config.$timestamp"
|
|
chmod 600 "$BACKUP_REPO/sshd_config.$timestamp" 2>/dev/null || true
|
|
|
|
# Generate Metadata
|
|
cat > "$BACKUP_REPO/sshd_config.$timestamp.meta" <<EOF
|
|
Backup-Time: $(date)
|
|
SSH-Port: $SSH_PORT
|
|
User: $TARGET_USER
|
|
Key-Auth: $KEY_OK
|
|
Script-Version: 4.0.0
|
|
EOF
|
|
chmod 600 "$BACKUP_REPO/sshd_config.$timestamp.meta" 2>/dev/null || true
|
|
fi
|
|
|
|
# Keep last 10 backups (exclude meta files in count, remove both)
|
|
ls -t "$BACKUP_REPO"/sshd_config.* 2>/dev/null | grep -v '\.meta$' | tail -n +11 | \
|
|
while read -r backup; do
|
|
rm -f "$backup" "${backup}.meta" 2>/dev/null || true
|
|
done
|
|
}
|
|
|
|
rollback_handler() {
|
|
RET=$? # Capture exit code immediately
|
|
trap - INT TERM EXIT HUP # Disable trap
|
|
|
|
# Only rollback on error (RET != 0)
|
|
if [ "$RET" -ne 0 ]; then
|
|
warn ""
|
|
warn "$(msg RB_START)"
|
|
|
|
# Restore SSH Config
|
|
if [ -f "$ROLLBACK_DIR/sshd_config" ]; then
|
|
cp -p "$ROLLBACK_DIR/sshd_config" "$SSH_CONF"
|
|
chmod 600 "$SSH_CONF"
|
|
fi
|
|
|
|
# Restore .d configs
|
|
if [ -d "$ROLLBACK_DIR/sshd_config.d" ]; then
|
|
cp -p "$ROLLBACK_DIR/sshd_config.d"/* "$SSH_CONF_D/" 2>/dev/null || true
|
|
fi
|
|
|
|
# Attempt restart to restore service
|
|
restart_sshd >/dev/null 2>&1
|
|
|
|
warn "$(msg RB_DONE)"
|
|
audit_log "ROLLBACK" "System rolled back due to error code $RET"
|
|
else
|
|
# Success cleanup - Remove the whole temp dir
|
|
rm -rf "$TMP_DIR"
|
|
fi
|
|
|
|
exit "$RET"
|
|
}
|
|
|
|
[ "$(id -u)" -eq 0 ] || { echo "$(msg MUST_ROOT)"; exit 1; }
|
|
audit_log "START" "Script started with args: $*"
|
|
|
|
# ---------------- Package Manager ----------------
|
|
detect_pm() {
|
|
[ -f /etc/alpine-release ] && { echo apk; return; }
|
|
[ -f /etc/debian_version ] && { echo apt; return; }
|
|
[ -f /etc/redhat-release ] && { echo yum; return; }
|
|
echo unknown
|
|
}
|
|
PM="$(detect_pm)"
|
|
APT_UPDATED="n"
|
|
APK_UPDATED="n"
|
|
YUM_PREPARED="n"
|
|
|
|
pm_prepare_once() {
|
|
case "$PM" in
|
|
apt) [ "$APT_UPDATED" != "y" ] && { apt-get update -y >>"$LOG_FILE" 2>&1; APT_UPDATED="y"; } ;;
|
|
apk) [ "$APK_UPDATED" != "y" ] && { apk update >>"$LOG_FILE" 2>&1 || true; APK_UPDATED="y"; } ;;
|
|
yum) [ "$YUM_PREPARED" != "y" ] && {
|
|
if command -v dnf >/dev/null 2>&1; then dnf makecache -y >>"$LOG_FILE" 2>&1;
|
|
else yum makecache -y >>"$LOG_FILE" 2>&1; fi
|
|
YUM_PREPARED="y"; } ;;
|
|
esac
|
|
}
|
|
|
|
install_pkg() {
|
|
case "$PM" in
|
|
apt) pm_prepare_once; DEBIAN_FRONTEND=noninteractive apt-get install -y "$@" >>"$LOG_FILE" 2>&1 ;;
|
|
yum) pm_prepare_once;
|
|
if command -v dnf >/dev/null 2>&1; then dnf install -y "$@" >>"$LOG_FILE" 2>&1;
|
|
else yum install -y "$@" >>"$LOG_FILE" 2>&1; fi ;;
|
|
apk) pm_prepare_once; apk add --no-cache "$@" >>"$LOG_FILE" 2>&1 ;;
|
|
esac
|
|
}
|
|
|
|
install_pkg_try() {
|
|
for p in "$@"; do
|
|
if install_pkg "$p" >/dev/null 2>&1; then return 0; fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# ---------------- System Update ----------------
|
|
update_system() {
|
|
case "$PM" in
|
|
apt) pm_prepare_once; DEBIAN_FRONTEND=noninteractive apt-get upgrade -y >>"$LOG_FILE" 2>&1 ;;
|
|
yum) pm_prepare_once;
|
|
if command -v dnf >/dev/null 2>&1; then dnf upgrade -y >>"$LOG_FILE" 2>&1;
|
|
else yum update -y >>"$LOG_FILE" 2>&1; fi ;;
|
|
apk) pm_prepare_once; apk upgrade >>"$LOG_FILE" 2>&1 ;;
|
|
esac
|
|
}
|
|
|
|
# ---------------- BBR ----------------
|
|
enable_bbr() {
|
|
command -v sysctl >/dev/null 2>&1 || return
|
|
if ! sysctl -n net.ipv4.tcp_available_congestion_control 2>/dev/null | grep -q bbr; then
|
|
warn "Kernel does not support BBR, skipping."
|
|
return
|
|
fi
|
|
sysctl_conf="/etc/sysctl.conf"
|
|
grep -q '^net.core.default_qdisc=fq$' "$sysctl_conf" 2>/dev/null || echo 'net.core.default_qdisc=fq' >>"$sysctl_conf"
|
|
grep -q '^net.ipv4.tcp_congestion_control=bbr$' "$sysctl_conf" 2>/dev/null || echo 'net.ipv4.tcp_congestion_control=bbr' >>"$sysctl_conf"
|
|
sysctl -p >>"$LOG_FILE" 2>&1 || true
|
|
}
|
|
|
|
# ---------------- SSHD Helpers ----------------
|
|
ensure_ssh_server() {
|
|
[ -f "$SSH_CONF" ] && return 0
|
|
info "$(msg I_SSH_INSTALL)"
|
|
case "$PM" in
|
|
apk) install_pkg openssh ;;
|
|
*) install_pkg openssh-server ;;
|
|
esac
|
|
[ -f "$SSH_CONF" ] || die "OpenSSH Install Failed"
|
|
}
|
|
|
|
protect_sshd_service() {
|
|
if command -v systemctl >/dev/null 2>&1; then
|
|
info "$(msg SYS_PROT)"
|
|
systemctl enable ssh sshd 2>/dev/null || true
|
|
systemctl unmask ssh sshd 2>/dev/null || true
|
|
|
|
mkdir -p /etc/systemd/system/sshd.service.d/ 2>/dev/null || true
|
|
cat > /etc/systemd/system/sshd.service.d/override.conf <<EOF
|
|
[Service]
|
|
Restart=on-failure
|
|
RestartSec=5s
|
|
OOMScoreAdjust=-500
|
|
EOF
|
|
systemctl daemon-reload >>"$LOG_FILE" 2>&1 || true
|
|
fi
|
|
}
|
|
|
|
restart_sshd() {
|
|
if [ "$ARG_DELAY_RESTART" = "y" ]; then
|
|
warn "DELAY RESTART: Please manually restart sshd later."
|
|
return 0
|
|
fi
|
|
|
|
local res=1
|
|
if command -v systemctl >/dev/null 2>&1; then
|
|
systemctl restart sshd >>"$LOG_FILE" 2>&1 || systemctl restart ssh >>"$LOG_FILE" 2>&1
|
|
res=$?
|
|
elif command -v rc-service >/dev/null 2>&1; then
|
|
rc-service sshd restart >>"$LOG_FILE" 2>&1
|
|
res=$?
|
|
elif command -v service >/dev/null 2>&1; then
|
|
service sshd restart >>"$LOG_FILE" 2>&1 || service ssh restart >>"$LOG_FILE" 2>&1
|
|
res=$?
|
|
else
|
|
[ -x /etc/init.d/sshd ] && /etc/init.d/sshd restart >>"$LOG_FILE" 2>&1 && res=0
|
|
[ -x /etc/init.d/ssh ] && /etc/init.d/ssh restart >>"$LOG_FILE" 2>&1 && res=0
|
|
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
|
|
}
|
|
|
|
verify_sshd_listening() {
|
|
local port="$1"
|
|
local timeout=10
|
|
local elapsed=0
|
|
|
|
ensure_port_tools
|
|
|
|
while [ $elapsed -lt $timeout ]; do
|
|
if ! is_port_free "$port"; then
|
|
# Port is occupied, which means SSHD (or something) is up
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
elapsed=$((elapsed + 1))
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# v4.0.0: Robust Connection Testing with Fallback
|
|
test_ssh_connection() {
|
|
port="$1"
|
|
user="$2"
|
|
info "$(msg TEST_CONN)"
|
|
|
|
sleep 2
|
|
|
|
# Try to install clients if missing
|
|
if ! command -v ssh >/dev/null 2>&1; then
|
|
install_pkg_try openssh-clients openssh-client >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
# Determine IPv6 capability for testing
|
|
local targets="127.0.0.1 localhost"
|
|
if has_global_ipv6; then targets="$targets ::1"; fi
|
|
|
|
# METHOD 1: SSH Client
|
|
if command -v ssh >/dev/null 2>&1; then
|
|
for target in $targets; do
|
|
if ssh -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -p "$port" "$user"@"$target" "echo ok" >/dev/null 2>&1; then
|
|
ok "$(msg TEST_OK) ($target via SSH)"
|
|
return 0
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# METHOD 2: Netcat (Fallback if keyauth fails or client missing)
|
|
if command -v nc >/dev/null 2>&1; then
|
|
# Use first target for port check
|
|
if nc -z -w 5 127.0.0.1 "$port" 2>/dev/null; then
|
|
ok "SSH port $port is open (verified via Netcat)"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
err "$(msg TEST_FAIL)"
|
|
return 1
|
|
}
|
|
|
|
# ---------------- Firewall & SELinux ----------------
|
|
allow_firewall_port() {
|
|
p="$1"
|
|
# IPv4
|
|
if command -v ufw >/dev/null 2>&1; then
|
|
ufw allow "${p}/tcp" >>"$LOG_FILE" 2>&1 || true
|
|
elif command -v firewall-cmd >/dev/null 2>&1; then
|
|
firewall-cmd --permanent --add-port="${p}/tcp" >>"$LOG_FILE" 2>&1 || true
|
|
firewall-cmd --reload >>"$LOG_FILE" 2>&1 || true
|
|
elif command -v iptables >/dev/null 2>&1; then
|
|
iptables -I INPUT -p tcp --dport "$p" -j ACCEPT 2>>"$LOG_FILE" || true
|
|
fi
|
|
|
|
# IPv6
|
|
if command -v ip6tables >/dev/null 2>&1; then
|
|
ip6tables -I INPUT -p tcp --dport "$p" -j ACCEPT 2>>"$LOG_FILE" || true
|
|
fi
|
|
}
|
|
|
|
handle_selinux() {
|
|
port="$1"
|
|
if command -v getenforce >/dev/null 2>&1; then
|
|
if getenforce | grep -qi "Enforcing"; then
|
|
info "$(msg SELINUX_DET)"
|
|
|
|
if ! command -v semanage >/dev/null 2>&1; then
|
|
info "$(msg SELINUX_INS)"
|
|
case "$PM" in
|
|
yum) install_pkg_try policycoreutils-python-utils policycoreutils-python ;;
|
|
apt) install_pkg_try policycoreutils python3-policycoreutils ;;
|
|
esac
|
|
fi
|
|
|
|
if command -v semanage >/dev/null 2>&1; then
|
|
if semanage port -a -t ssh_port_t -p tcp "$port" >>"$LOG_FILE" 2>&1 || \
|
|
semanage port -m -t ssh_port_t -p tcp "$port" >>"$LOG_FILE" 2>&1; then
|
|
ok "$(msg SELINUX_OK)"
|
|
else
|
|
warn "$(msg SELINUX_FAIL)"
|
|
fi
|
|
else
|
|
warn "$(msg SELINUX_FAIL)"
|
|
fi
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# ---------------- Port Logic ----------------
|
|
is_hard_reserved() {
|
|
case "$1" in
|
|
80|443|3306|5432|6379|8080|8443|21|23|25|110|143) return 0 ;;
|
|
esac
|
|
return 1
|
|
}
|
|
|
|
is_k8s_nodeport() {
|
|
[ "$1" -ge 30000 ] && [ "$1" -le 32767 ]
|
|
}
|
|
|
|
rand_u16() {
|
|
if [ -r /dev/urandom ] && command -v od >/dev/null 2>&1; then
|
|
od -An -N2 -tu2 /dev/urandom 2>/dev/null | tr -d ' '
|
|
elif command -v shuf >/dev/null 2>&1; then
|
|
shuf -i 1024-65535 -n 1
|
|
else
|
|
echo $(( ( $(date +%s 2>/dev/null || echo 12345) + $$ ) % 65536 ))
|
|
fi
|
|
}
|
|
|
|
ensure_port_tools() {
|
|
command -v ss >/dev/null 2>&1 && return 0
|
|
command -v netstat >/dev/null 2>&1 && return 0
|
|
case "$PM" in
|
|
apt) install_pkg_try iproute2 >/dev/null 2>&1 || true ;;
|
|
yum) install_pkg_try iproute >/dev/null 2>&1 || true ;;
|
|
apk) install_pkg_try iproute2 iproute2-ss >/dev/null 2>&1 || true ;;
|
|
esac
|
|
install_pkg_try net-tools >/dev/null 2>&1 || true
|
|
}
|
|
|
|
is_port_free() {
|
|
p="$1"
|
|
if command -v ss >/dev/null 2>&1; then
|
|
# Reliable parsing for IPv4 (0.0.0.0:22) and IPv6 ([::]:22)
|
|
if ss -lnt 2>/dev/null | awk -v port="$p" '
|
|
{
|
|
n = split($4, parts, ":")
|
|
last = parts[n]
|
|
if (last == port) { found=1; exit }
|
|
}
|
|
END { exit !found }
|
|
'; then
|
|
return 1 # Found (Occupied)
|
|
else
|
|
return 0 # Free
|
|
fi
|
|
fi
|
|
# Fallback
|
|
if command -v netstat >/dev/null 2>&1; then
|
|
netstat -lnt 2>/dev/null | awk '{print $4}' | grep -q ":$p$" && return 1 || return 0
|
|
fi
|
|
return 1 # Conservative fail
|
|
}
|
|
|
|
pick_random_port() {
|
|
ensure_port_tools
|
|
i=0
|
|
while [ $i -lt 100 ]; do
|
|
r="$(rand_u16)"
|
|
# Logic: 49152 + (0 to 16383) = 49152 to 65535.
|
|
# This range is strictly above K8s NodePort (30000-32767).
|
|
p=$(( 49152 + (r % (65535 - 49152)) ))
|
|
|
|
if is_port_free "$p"; then echo "$p"; return 0; fi
|
|
i=$((i+1))
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# ---------------- User & Key ----------------
|
|
validate_username() {
|
|
u="$1"
|
|
# Length 2-32
|
|
len=${#u}
|
|
if [ "$len" -lt 2 ] || [ "$len" -gt 32 ]; then return 1; fi
|
|
# Regex: Lowercase, digits, underscore, dash. Must start with letter/underscore.
|
|
echo "$u" | grep -Eq '^[a-z_][a-z0-9_-]*$' || return 1
|
|
# Reserved words
|
|
case "$u" in
|
|
root|bin|daemon|adm|lp|sync|shutdown|halt|mail|operator|games|ftp|nobody) return 1 ;;
|
|
esac
|
|
return 0
|
|
}
|
|
|
|
ensure_user() {
|
|
u="$1"
|
|
[ "$u" = "root" ] && return 0
|
|
id "$u" >/dev/null 2>&1 && return 0
|
|
|
|
info "$(msg I_USER) $u"
|
|
install_pkg_try bash sudo >/dev/null 2>&1 || true
|
|
shell="/bin/sh"
|
|
[ -x /bin/bash ] && shell="/bin/bash"
|
|
|
|
if command -v useradd >/dev/null 2>&1; then
|
|
useradd -m -s "$shell" "$u"
|
|
else
|
|
adduser -D -s "$shell" "$u"
|
|
fi
|
|
|
|
if [ -d /etc/sudoers.d ]; then
|
|
echo "$u ALL=(ALL) NOPASSWD:ALL" >"/etc/sudoers.d/$u" 2>/dev/null || true
|
|
chmod 440 "/etc/sudoers.d/$u" 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
fetch_keys() {
|
|
local url=""
|
|
case "$1" in
|
|
gh) url="https://github.com/$2.keys" ;;
|
|
url) url="$2" ;;
|
|
raw) printf "%s\n" "$2"; return ;;
|
|
esac
|
|
|
|
local max_retries=3
|
|
local retry=0
|
|
|
|
while [ $retry -lt $max_retries ]; do
|
|
if command -v curl >/dev/null 2>&1; then
|
|
if curl -fsSL --connect-timeout 10 --max-time 30 "$url" 2>>"$LOG_FILE"; then
|
|
return 0
|
|
fi
|
|
elif command -v wget >/dev/null 2>&1; then
|
|
if wget -qO- --timeout=30 "$url" 2>>"$LOG_FILE"; then
|
|
return 0
|
|
fi
|
|
else
|
|
warn "Need curl or wget to fetch keys"
|
|
return 1
|
|
fi
|
|
retry=$((retry + 1))
|
|
[ $retry -lt $max_retries ] && sleep 2
|
|
done
|
|
|
|
warn "Failed to fetch keys after $max_retries attempts"
|
|
return 1
|
|
}
|
|
|
|
deploy_keys() {
|
|
user="$1"
|
|
keys="$2"
|
|
home="$(eval echo "~$user")"
|
|
dir="$home/.ssh"
|
|
auth="$dir/authorized_keys"
|
|
|
|
mkdir -p "$dir"
|
|
chmod 700 "$dir"
|
|
touch "$auth"
|
|
chmod 600 "$auth"
|
|
chown -R "$user:" "$dir" 2>/dev/null || true
|
|
|
|
printf "%s\n" "$keys" | while IFS= read -r line; do
|
|
[ -z "$line" ] && continue
|
|
echo "$line" | grep -Eq '^(ssh-(rsa|ed25519|dss)|ecdsa-|sk-)' || continue
|
|
grep -qxF "$line" "$auth" || echo "$line" >>"$auth"
|
|
done
|
|
grep -Eq '^(ssh-|ecdsa-|sk-)' "$auth"
|
|
}
|
|
|
|
# ---------------- Config Management ----------------
|
|
cleanup_sshd_config_d() {
|
|
if [ -d "$SSH_CONF_D" ]; then
|
|
for conf in "$SSH_CONF_D"/*.conf; do
|
|
[ -f "$conf" ] || continue
|
|
if grep -Eq '^[[:space:]]*(Port|PermitRootLogin|PasswordAuthentication)' "$conf"; then
|
|
mv "$conf" "${conf}.bak_server_init"
|
|
warn "$(msg CLEAN_D) $conf"
|
|
fi
|
|
done
|
|
fi
|
|
}
|
|
|
|
remove_managed_block() {
|
|
tmp="$TMP_DIR/sshd_config.tmp"
|
|
cp -p "$SSH_CONF" "$tmp"
|
|
|
|
awk -v b="$BLOCK_BEGIN" -v e="$BLOCK_END" '
|
|
$0==b {skip=1; next}
|
|
$0==e {skip=0; next}
|
|
skip!=1 {print}
|
|
' "$SSH_CONF" >"$tmp"
|
|
|
|
cat "$tmp" > "$SSH_CONF"
|
|
rm -f "$tmp"
|
|
}
|
|
|
|
# v4.0.0: Robust Triple-Check IPv6
|
|
has_global_ipv6() {
|
|
# Method 1: Proc file (Common Linux)
|
|
if [ -f /proc/net/if_inet6 ]; then
|
|
if grep -v '^fe80::' /proc/net/if_inet6 2>/dev/null | grep -q '^[0-9a-f]'; then
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
# Method 2: ip command
|
|
if command -v ip >/dev/null 2>&1; then
|
|
if ip -6 addr show scope global 2>/dev/null | grep -q inet6; then
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
# Method 3: ifconfig (Legacy/BSD-like)
|
|
if command -v ifconfig >/dev/null 2>&1; then
|
|
if ifconfig 2>/dev/null | grep -i 'inet6.*global' >/dev/null; then
|
|
return 0
|
|
fi
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
build_block() {
|
|
file="$1"
|
|
{
|
|
echo "$BLOCK_BEGIN"
|
|
echo "# Managed by server-init v4.0.0"
|
|
echo "# Generated: $(date)"
|
|
echo "# Do NOT edit inside this block. Changes will be overwritten."
|
|
echo ""
|
|
echo "Port $SSH_PORT"
|
|
|
|
echo "KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256"
|
|
echo "Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com"
|
|
echo "MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com"
|
|
|
|
# Smart IPv6 Check
|
|
if has_global_ipv6; then
|
|
echo "AddressFamily any"
|
|
echo "ListenAddress ::"
|
|
echo "ListenAddress 0.0.0.0"
|
|
info "$(msg IPV6_CFG)"
|
|
else
|
|
echo "AddressFamily inet"
|
|
echo "ListenAddress 0.0.0.0"
|
|
fi
|
|
|
|
if [ "$KEY_OK" = "y" ]; then
|
|
echo "PasswordAuthentication no"
|
|
echo "ChallengeResponseAuthentication no"
|
|
echo "PubkeyAuthentication yes"
|
|
fi
|
|
|
|
if [ "$TARGET_USER" = "root" ]; then
|
|
if [ "$KEY_OK" = "y" ]; then
|
|
if sshd -V 2>&1 | grep -q "OpenSSH_[1-6]"; then
|
|
echo "PermitRootLogin without-password"
|
|
warn "$(msg COMPAT_WARN)"
|
|
else
|
|
echo "PermitRootLogin prohibit-password"
|
|
fi
|
|
else
|
|
echo "PermitRootLogin yes"
|
|
fi
|
|
else
|
|
echo "PermitRootLogin no"
|
|
fi
|
|
|
|
echo ""
|
|
echo "$BLOCK_END"
|
|
echo ""
|
|
} >"$file"
|
|
}
|
|
|
|
insert_block_at_top() {
|
|
block="$1"
|
|
tmp="$TMP_DIR/sshd_config.merge"
|
|
cat "$block" "$SSH_CONF" >"$tmp"
|
|
chmod 600 "$tmp"
|
|
mv "$tmp" "$SSH_CONF"
|
|
}
|
|
|
|
update_motd() {
|
|
info "$(msg MOTD_UPD)"
|
|
motd="/etc/motd"
|
|
tmp="$TMP_DIR/motd.new"
|
|
|
|
if [ -f "$motd" ]; then
|
|
grep -v "Server Init Complete" "$motd" > "$tmp" 2>/dev/null || true
|
|
fi
|
|
|
|
{
|
|
echo "==============================================================================="
|
|
echo " Server Init Complete - SSH Hardened"
|
|
echo "==============================================================================="
|
|
echo " Login User: $TARGET_USER"
|
|
echo " SSH Port: $SSH_PORT"
|
|
echo " Auth Type: $([ "$KEY_OK" = "y" ] && echo "Key Only" || echo "Password")"
|
|
echo " Firewall: Please ensure TCP/$SSH_PORT is allowed."
|
|
echo "==============================================================================="
|
|
echo ""
|
|
[ -s "$tmp" ] && cat "$tmp"
|
|
} > "${motd}.final"
|
|
|
|
mv "${motd}.final" "$motd"
|
|
}
|
|
|
|
# v4.0.0: Final Health Report
|
|
generate_health_report() {
|
|
report_file="/var/log/server-init-health.log"
|
|
# Calculate Duration
|
|
end_time=$(date +%s)
|
|
duration=$((end_time - SCRIPT_START_TIME))
|
|
|
|
{
|
|
echo "=== Server Init Health Report ==="
|
|
echo "Generated: $(date)"
|
|
echo "Version: v4.0.0 Platinum"
|
|
echo "Execution Time: ${duration}s"
|
|
echo ""
|
|
echo "--- SSH Config ---"
|
|
echo "Port: $SSH_PORT"
|
|
echo "User: $TARGET_USER"
|
|
echo "KeyAuth: $([ "$KEY_OK" = "y" ] && echo "YES" || echo "NO")"
|
|
echo ""
|
|
echo "--- Network ---"
|
|
echo "Public IP: ${public_ip:-unknown}"
|
|
echo "IPv6: $(has_global_ipv6 && echo "Enabled" || echo "Disabled")"
|
|
echo "Port Listening: $(is_port_free "$SSH_PORT" && echo "NO (Error)" || echo "YES")"
|
|
} > "$report_file"
|
|
|
|
chmod 600 "$report_file"
|
|
info "Health report saved to: $report_file"
|
|
}
|
|
|
|
print_final_summary() {
|
|
|
|
# Try to detect public IP
|
|
public_ip=""
|
|
if command -v curl >/dev/null 2>&1; then
|
|
public_ip=$(curl -4fsSL --max-time 2 https://api.ipify.org 2>/dev/null || echo "")
|
|
fi
|
|
local_ip=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "")
|
|
|
|
end_time=$(date +%s)
|
|
duration=$((end_time - SCRIPT_START_TIME))
|
|
|
|
echo ""
|
|
echo "${CYAN}╔════════════════════════════════════════════════════════════════════╗${NC}"
|
|
printf "${CYAN}║ %-66s ║${NC}\n" "$(msg BOX_TITLE)"
|
|
echo "${CYAN}╠════════════════════════════════════════════════════════════════════╣${NC}"
|
|
printf "${CYAN}║ %-66s ║${NC}\n" " $(msg BOX_SSH)"
|
|
|
|
if [ -n "$public_ip" ]; then
|
|
printf "${CYAN}║ Public: ssh -p %-5s %s@%s %-16s ║${NC}\n" "$SSH_PORT" "$TARGET_USER" "$public_ip" ""
|
|
fi
|
|
if [ -n "$local_ip" ]; then
|
|
printf "${CYAN}║ Local: ssh -p %-5s %s@%s %-16s ║${NC}\n" "$SSH_PORT" "$TARGET_USER" "$local_ip" ""
|
|
fi
|
|
|
|
echo "${CYAN}║ ║${NC}"
|
|
|
|
if [ "$KEY_OK" = "y" ]; then
|
|
printf "${CYAN}║ %-66s ║${NC}\n" " $(msg BOX_KEY_ON)"
|
|
else
|
|
printf "${CYAN}║ %-66s ║${NC}\n" " $(msg BOX_KEY_OFF)"
|
|
fi
|
|
|
|
if [ "$SSH_PORT" != "22" ]; then
|
|
printf "${CYAN}║ %-66s ║${NC}\n" " $(msg BOX_PORT)$SSH_PORT"
|
|
printf "${CYAN}║ %-66s ║${NC}\n" " $(msg BOX_FW)"
|
|
if is_k8s_nodeport "$SSH_PORT"; then
|
|
printf "${CYAN}║ %-66s ║${NC}\n" " $(msg BOX_K8S_WARN)"
|
|
fi
|
|
fi
|
|
|
|
echo "${CYAN}║ ║${NC}"
|
|
printf "${CYAN}║ %-66s ║${NC}\n" " $(msg BOX_WARN)"
|
|
echo "${CYAN}╚════════════════════════════════════════════════════════════════════╝${NC}"
|
|
echo ""
|
|
echo "Log: $LOG_FILE"
|
|
echo "Time: ${duration}s"
|
|
}
|
|
|
|
# =========================================================
|
|
# Phase 1: Input
|
|
# =========================================================
|
|
clear
|
|
echo "================================================="
|
|
msg BANNER
|
|
echo "================================================="
|
|
[ "$STRICT_MODE" = "y" ] && msg STRICT_ON
|
|
|
|
# Preflight Checks
|
|
preflight_checks
|
|
|
|
# 1. User
|
|
if [ -n "$ARG_USER" ]; then
|
|
TARGET_USER="$ARG_USER"
|
|
# Validating argument user
|
|
validate_username "$TARGET_USER" || die "$(msg ERR_USER_INV): $TARGET_USER"
|
|
printf "%s%s\n" "$(msg AUTO_SKIP)" "$TARGET_USER"
|
|
else
|
|
while :; do
|
|
printf "%s%s): " "$(msg ASK_USER)" "$DEFAULT_USER"
|
|
read TARGET_USER
|
|
[ -z "$TARGET_USER" ] && TARGET_USER="$DEFAULT_USER"
|
|
if validate_username "$TARGET_USER"; then
|
|
break
|
|
else
|
|
msg ERR_USER_INV
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# 2. Port
|
|
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" ;;
|
|
esac
|
|
printf "%s%s\n" "$(msg AUTO_SKIP)" "$ARG_PORT (Mode $PORT_OPT)"
|
|
else
|
|
echo ""
|
|
msg ASK_PORT_T
|
|
msg OPT_PORT_1
|
|
msg OPT_PORT_2
|
|
msg OPT_PORT_3
|
|
printf "%s" "$(msg SELECT)"
|
|
read PORT_OPT
|
|
[ -z "$PORT_OPT" ] && PORT_OPT="1"
|
|
|
|
SSH_PORT="22"
|
|
if [ "$PORT_OPT" = "3" ]; then
|
|
while :; do
|
|
printf "%s" "$(msg INPUT_PORT)"
|
|
read MANUAL_PORT
|
|
echo "$MANUAL_PORT" | grep -Eq '^[0-9]+$' || { msg PORT_ERR; continue; }
|
|
[ "$MANUAL_PORT" -ge 1024 ] 2>/dev/null && [ "$MANUAL_PORT" -le 65535 ] 2>/dev/null || { msg PORT_ERR; continue; }
|
|
|
|
if is_hard_reserved "$MANUAL_PORT"; then
|
|
msg PORT_RES
|
|
continue
|
|
elif is_k8s_nodeport "$MANUAL_PORT"; then
|
|
msg PORT_K8S
|
|
printf "%s" "$(msg ASK_SURE)"
|
|
read force_port
|
|
[ "${force_port:-n}" = "y" ] || continue
|
|
fi
|
|
|
|
SSH_PORT="$MANUAL_PORT"
|
|
break
|
|
done
|
|
fi
|
|
fi
|
|
|
|
# 3. Key
|
|
if [ -n "$ARG_KEY_TYPE" ]; then
|
|
KEY_OPT="auto"
|
|
KEY_TYPE="$ARG_KEY_TYPE"
|
|
KEY_VAL="$ARG_KEY_VAL"
|
|
printf "%s%s\n" "$(msg AUTO_SKIP)" "$KEY_TYPE ($KEY_VAL)"
|
|
else
|
|
echo ""
|
|
msg ASK_KEY_T
|
|
msg OPT_KEY_1
|
|
msg OPT_KEY_2
|
|
msg OPT_KEY_3
|
|
printf "%s" "$(msg SELECT)"
|
|
read KEY_OPT
|
|
|
|
case "$KEY_OPT" in
|
|
1) KEY_TYPE="gh"; printf "%s" "$(msg INPUT_GH)"; read KEY_VAL ;;
|
|
2) KEY_TYPE="url"; printf "%s" "$(msg INPUT_URL)"; read KEY_VAL ;;
|
|
3)
|
|
KEY_TYPE="raw"
|
|
msg INPUT_RAW
|
|
raw=""
|
|
while IFS= read -r l; do
|
|
[ -z "$l" ] && break
|
|
raw="${raw}${l}\n"
|
|
done
|
|
KEY_VAL="$(printf "%b" "$raw")"
|
|
;;
|
|
*) die "Invalid Option" ;;
|
|
esac
|
|
fi
|
|
|
|
# 4. Update
|
|
if [ -n "$ARG_UPDATE" ]; then
|
|
DO_UPDATE="$ARG_UPDATE"
|
|
printf "%s%s\n" "$(msg AUTO_SKIP)" "Update=$DO_UPDATE"
|
|
else
|
|
printf "%s" "$(msg ASK_UPD)"
|
|
read DO_UPDATE
|
|
[ -z "$DO_UPDATE" ] && DO_UPDATE="n"
|
|
fi
|
|
|
|
# 5. BBR
|
|
if [ -n "$ARG_BBR" ]; then
|
|
DO_BBR="$ARG_BBR"
|
|
printf "%s%s\n" "$(msg AUTO_SKIP)" "BBR=$DO_BBR"
|
|
else
|
|
printf "%s" "$(msg ASK_BBR)"
|
|
read DO_BBR
|
|
[ -z "$DO_BBR" ] && DO_BBR="n"
|
|
fi
|
|
|
|
# =========================================================
|
|
# Phase 2: Confirm
|
|
# =========================================================
|
|
if [ "$AUTO_CONFIRM" = "y" ]; then
|
|
echo ""
|
|
info "Auto-Confirm: Skipping interactive confirmation."
|
|
else
|
|
echo ""
|
|
msg CONFIRM_T
|
|
echo "$(msg C_USER)$TARGET_USER"
|
|
echo "$(msg C_PORT)$SSH_PORT (Mode: $PORT_OPT)"
|
|
echo "$(msg C_KEY)$KEY_TYPE"
|
|
echo "$(msg C_UPD)$DO_UPDATE"
|
|
echo "$(msg C_BBR)$DO_BBR"
|
|
[ "$PORT_OPT" != "1" ] && msg WARN_FW
|
|
|
|
printf "%s" "$(msg ASK_SURE)"
|
|
read CONFIRM
|
|
[ "${CONFIRM:-n}" = "y" ] || die "$(msg CANCEL)"
|
|
fi
|
|
|
|
# =========================================================
|
|
# Phase 3: Execute (With Enhanced Rollback)
|
|
# =========================================================
|
|
msg AUDIT_START
|
|
setup_rollback
|
|
backup_config_persistent
|
|
|
|
info "$(msg I_INSTALL)"
|
|
ensure_ssh_server
|
|
install_pkg_try curl >/dev/null 2>&1 || true # Soft check, fetch_keys handles fail
|
|
install_pkg_try wget >/dev/null 2>&1 || true
|
|
|
|
# Updates & BBR
|
|
if [ "$DO_UPDATE" = "y" ]; then
|
|
info "$(msg I_UPD)"
|
|
update_system
|
|
fi
|
|
|
|
if [ "$DO_BBR" = "y" ]; then
|
|
info "$(msg I_BBR)"
|
|
enable_bbr
|
|
fi
|
|
|
|
# Random Port Calculation
|
|
if [ "$PORT_OPT" = "2" ]; then
|
|
p="$(pick_random_port || true)"
|
|
if [ -n "$p" ]; then
|
|
SSH_PORT="$p"
|
|
info "Random Port: $SSH_PORT"
|
|
else
|
|
[ "$STRICT_MODE" = "y" ] && die "STRICT: Random port failed"
|
|
warn "Random port failed, fallback to 22"
|
|
SSH_PORT="22"
|
|
fi
|
|
fi
|
|
|
|
# Firewall & SELinux
|
|
if [ "$SSH_PORT" != "22" ]; then
|
|
allow_firewall_port "$SSH_PORT"
|
|
handle_selinux "$SSH_PORT"
|
|
fi
|
|
|
|
# User ensure
|
|
ensure_user "$TARGET_USER"
|
|
|
|
# Key Deploy
|
|
KEY_OK="n"
|
|
KEY_DATA="$(fetch_keys "$KEY_TYPE" "$KEY_VAL")"
|
|
if [ -n "$KEY_DATA" ] && deploy_keys "$TARGET_USER" "$KEY_DATA"; then
|
|
KEY_OK="y"
|
|
info "$(msg I_KEY_OK)"
|
|
else
|
|
[ "$STRICT_MODE" = "y" ] && die "STRICT: Key deploy failed"
|
|
warn "$(msg W_KEY_FAIL)"
|
|
fi
|
|
|
|
# SSH Config Manipulation
|
|
info "$(msg I_BACKUP)$SSH_CONF"
|
|
cleanup_sshd_config_d
|
|
remove_managed_block
|
|
|
|
tmp="$TMP_DIR/sshd_block_final"
|
|
build_block "$tmp"
|
|
insert_block_at_top "$tmp"
|
|
|
|
# Optimized: Apply Systemd protection once
|
|
if [ "$ARG_DELAY_RESTART" != "y" ]; then
|
|
protect_sshd_service
|
|
fi
|
|
|
|
# Validation 1: Syntax
|
|
if ! sshd -t -f "$SSH_CONF" 2>>"$LOG_FILE"; then
|
|
die "$(msg E_SSHD_CHK)"
|
|
fi
|
|
|
|
# Restart
|
|
if ! restart_sshd; then
|
|
warn "$(msg W_RESTART)"
|
|
fi
|
|
|
|
# Validation 2: Verification (Grep)
|
|
if ! grep -q "^Port $SSH_PORT" "$SSH_CONF"; then
|
|
die "$(msg E_GREP_FAIL)"
|
|
fi
|
|
|
|
# Validation 3: Active Listening (Network)
|
|
if ! verify_sshd_listening "$SSH_PORT"; then
|
|
die "$(msg W_LISTEN_FAIL)"
|
|
fi
|
|
|
|
# Self-Test Connection (Check BEFORE removing trap)
|
|
if ! test_ssh_connection "$SSH_PORT" "$TARGET_USER"; then
|
|
die "$(msg TEST_FAIL)"
|
|
fi
|
|
|
|
# MotD Update
|
|
update_motd
|
|
generate_health_report
|
|
|
|
# Only remove trap if EVERYTHING passed
|
|
trap - INT TERM EXIT HUP
|
|
rm -rf "$TMP_DIR"
|
|
|
|
# =========================================================
|
|
# Done
|
|
# =========================================================
|
|
print_final_summary
|