2018 lines
71 KiB
Bash
2018 lines
71 KiB
Bash
#!/bin/sh
|
||
# =========================================================
|
||
# linux-ssh-init-sh
|
||
# Server Init & SSH Hardening Script
|
||
#
|
||
# Release: v4.6.0 (Fortress Pro - Security Audit Fix)
|
||
#
|
||
# POSIX sh compatible (Debian dash / CentOS / Alpine / Ubuntu)
|
||
#
|
||
# Key policy:
|
||
# - If `base64` exists: strict base64 decode + key length checks
|
||
# - If `base64` missing: downgrade to format-only validation (skip length checks)
|
||
#
|
||
# 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
|
||
SCRIPT_START_TIME=$(date +%s)
|
||
|
||
# ---------------- Configuration ----------------
|
||
LANG_CUR="zh"
|
||
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"
|
||
|
||
# ---------------- [SEC] Atomic Secure Temp Directory ----------------
|
||
old_umask=$(umask)
|
||
umask 077
|
||
TMP_DIR=""
|
||
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
|
||
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')
|
||
fi
|
||
[ -z "$rand_suffix" ] && rand_suffix="$$"
|
||
TMP_DIR="/tmp/ssh-init.${$}.${rand_suffix}.$(date +%s 2>/dev/null || echo 0)"
|
||
mkdir -p "$TMP_DIR" 2>/dev/null || { echo "FATAL: Cannot create temp directory: $TMP_DIR" >&2; exit 1; }
|
||
fi
|
||
chmod 700 "$TMP_DIR" 2>/dev/null || true
|
||
umask "$old_umask"
|
||
|
||
# ---------------- [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
|
||
|
||
STATE_FILE="$RUNTIME_DIR/state-$(id -u)"
|
||
LOCK_DIR="$RUNTIME_DIR/locks-$(id -u)"
|
||
|
||
[ -L "$STATE_FILE" ] && rm -f "$STATE_FILE" 2>/dev/null || true
|
||
if [ -e "$LOCK_DIR" ] && [ ! -d "$LOCK_DIR" ]; then
|
||
rm -f "$LOCK_DIR" 2>/dev/null || true
|
||
fi
|
||
|
||
# Colors
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
CYAN='\033[0;36m'
|
||
NC='\033[0m'
|
||
|
||
# ---------------- Initialize Variables ----------------
|
||
TARGET_USER=""
|
||
SSH_PORT="22"
|
||
KEY_OK="n"
|
||
PORT_OPT="1"
|
||
KEY_TYPE=""
|
||
KEY_VAL=""
|
||
DO_UPDATE="n"
|
||
DO_BBR="n"
|
||
OPENSSH_VER_MAJOR=0
|
||
OPENSSH_VER_MINOR=0
|
||
|
||
KEX_LINE=""
|
||
CIPHERS_LINE=""
|
||
MACS_LINE=""
|
||
CRYPTO_MODE="skip"
|
||
IPV6_ENABLED="n"
|
||
ROOT_KEY_PRESENT="n"
|
||
|
||
# ---------------- 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
|
||
|
||
# ---------------- Internationalization ----------------
|
||
msg() {
|
||
key="$1"
|
||
if [ "$LANG_CUR" = "zh" ]; then
|
||
case "$key" in
|
||
MUST_ROOT) echo "必须以 root 权限运行此脚本" ;;
|
||
BANNER) echo "服务器初始化 & SSH 安全加固 (v4.6.0 Fortress Pro)" ;;
|
||
STRICT_ON) echo "STRICT 模式已开启:任何关键错误将直接退出" ;;
|
||
ASK_USER) echo "SSH 登录用户 (默认 " ;;
|
||
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 "配置验证失败:关键参数未生效,正在回滚..." ;;
|
||
E_RESTART) echo "SSH 服务重启失败,正在回滚..." ;;
|
||
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 "检测到兼容性限制,已自动调整配置..." ;;
|
||
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 "❌ 缺少必要命令,无法继续: " ;;
|
||
ERR_MISSING_SSHD) echo "❌ 未找到 sshd 命令,请先安装 OpenSSH Server" ;;
|
||
WARN_DISK) echo "⚠️ 磁盘空间不足: " ;;
|
||
WARN_MEM) echo "⚠️ 可用内存不足: " ;;
|
||
WARN_RESUME) echo "检测到未完成的初始化,可能上次执行异常终止" ;;
|
||
ASK_RESUME) echo "检测到未完成的操作,是否继续? [y/N]: " ;;
|
||
ERR_BACKUP_DIR) echo "❌ 无法创建备份目录:" ;;
|
||
ERR_BACKUP_DIR_ALT) echo "❌ 无法创建备用备份目录" ;;
|
||
ERR_BACKUP_SUBDIR) echo "❌ 无法创建备份子目录:" ;;
|
||
INFO_BACKUP_CREATED) echo "✅ 备份已创建:" ;;
|
||
INFO_CLEANING_BACKUPS) echo "🧹 正在清理" ;;
|
||
INFO_OLD_BACKUPS) echo "个旧备份..." ;;
|
||
ERR_LOCK_DIR) echo "❌ 无法创建锁目录:" ;;
|
||
WARN_LOCK_DIR_PERM) echo "⚠️ 无法设置锁目录权限,继续尝试..." ;;
|
||
WARN_CLEAN_LOCKS) echo "⚠️ 清理旧的锁文件..." ;;
|
||
WARN_INVALID_KEY) echo "⚠️ 跳过无效的SSH密钥行" ;;
|
||
WARN_SHORT_RSA_KEY) echo "⚠️ RSA密钥过短:" ;;
|
||
WARN_SHORT_ED25519_KEY) echo "⚠️ Ed25519密钥过短:" ;;
|
||
WARN_SHORT_DSA_KEY) echo "⚠️ DSA密钥过短:" ;;
|
||
ERR_INVALID_KEY_FORMAT) echo "❌ SSH密钥格式无效" ;;
|
||
ERR_MISSING_BASE64) echo "❌ SSH密钥缺少base64部分" ;;
|
||
ERR_INVALID_BASE64) echo "❌ SSH密钥base64编码无效" ;;
|
||
WARN_NO_BASE64_SKIPLEN) echo "⚠️ 未检测到 base64 命令:将跳过密钥长度校验,仅做格式校验" ;;
|
||
WARN_USER_SHELL) echo "⚠️ 用户shell不允许登录:" ;;
|
||
ASK_CHANGE_SHELL) echo "是否更改用户的shell为/bin/bash? [y/N]: " ;;
|
||
WARN_CHANGE_SHELL_FAIL) echo "⚠️ 更改shell失败" ;;
|
||
WARN_UNUSUAL_SHELL) echo "⚠️ 用户使用非常规shell:" ;;
|
||
WARN_HOME_OWNER) echo "⚠️ 用户家目录所有者异常:" ;;
|
||
WARN_HOME_NOT_WRITABLE) echo "⚠️ 用户家目录不可写" ;;
|
||
ERR_USER_CREATE_FAIL) echo "❌ 创建用户失败" ;;
|
||
ERR_USER_VERIFY_FAIL) echo "❌ 用户创建后验证失败" ;;
|
||
WARN_NO_SUDOERS_DIR) echo "⚠️ 没有/etc/sudoers.d目录,跳过sudo配置" ;;
|
||
INFO_SUDO_EXISTS) echo "ℹ️ 用户已配置sudo权限" ;;
|
||
ERR_SUDOERS_SYNTAX) echo "❌ sudoers文件语法错误,已删除" ;;
|
||
ERR_SUDOERS_PERM) echo "❌ 无法设置sudoers文件权限" ;;
|
||
INFO_SUDO_CONFIGURED) echo "✅ 为用户配置了sudo权限" ;;
|
||
WARN_SSH_PROTOCOL) echo "⚠️ SSH协议握手失败或超时" ;;
|
||
INFO_SSH_PROTOCOL_OK) echo "✅ SSH协议握手成功" ;;
|
||
WARN_PORT_OPEN_BUT_FAIL) echo "⚠️ 端口已打开,但SSH客户端连接失败" ;;
|
||
WARN_X11_FORWARDING) echo "⚠️ X11转发已启用,可能存在安全风险" ;;
|
||
WARN_EMPTY_PASSWORDS) echo "⚠️ 允许空密码,存在安全风险" ;;
|
||
WARN_INSECURE_OPTIONS) echo "⚠️ 发现潜在的不安全选项" ;;
|
||
ERR_DEADLOCK) echo "❌ 致命错误:密码和密钥认证同时被禁用,将导致锁定!" ;;
|
||
ERR_PASSWORD_NO_KEY) echo "❌ 致命错误:密码认证已禁用但未成功部署SSH密钥" ;;
|
||
ERR_ROOT_NO_KEY) echo "❌ 致命错误:root密码登录已禁用但未部署SSH密钥" ;;
|
||
WARN_PORT_MISMATCH) echo "⚠️ 配置中的端口与目标端口不匹配" ;;
|
||
ERR_CANNOT_RESERVE_PORT) echo "❌ 无法预留端口,端口可能已被占用" ;;
|
||
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.6.0 Fortress Pro)" ;;
|
||
STRICT_ON) echo "STRICT mode ON: Critical errors will abort" ;;
|
||
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)" ;;
|
||
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; enabling fallback policy 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..." ;;
|
||
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 ================" ;;
|
||
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 "Compatibility limits detected; adjusted configuration automatically..." ;;
|
||
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/Fallback 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: " ;;
|
||
ERR_MISSING_SSHD) echo "❌ sshd command not found, please install OpenSSH Server first" ;;
|
||
WARN_DISK) echo "⚠️ Low disk space: " ;;
|
||
WARN_MEM) echo "⚠️ Low memory: " ;;
|
||
WARN_RESUME) echo "Detected incomplete initialization, last execution may have crashed" ;;
|
||
ASK_RESUME) echo "Detected incomplete operation, continue? [y/N]: " ;;
|
||
ERR_BACKUP_DIR) echo "❌ Cannot create backup directory:" ;;
|
||
ERR_BACKUP_DIR_ALT) echo "❌ Cannot create alternative backup directory" ;;
|
||
ERR_BACKUP_SUBDIR) echo "❌ Cannot create backup subdirectory:" ;;
|
||
INFO_BACKUP_CREATED) echo "✅ Backup created:" ;;
|
||
INFO_CLEANING_BACKUPS) echo "🧹 Cleaning" ;;
|
||
INFO_OLD_BACKUPS) echo "old backups..." ;;
|
||
ERR_LOCK_DIR) echo "❌ Cannot create lock directory:" ;;
|
||
WARN_LOCK_DIR_PERM) echo "⚠️ Cannot set lock directory permissions, continuing..." ;;
|
||
WARN_CLEAN_LOCKS) echo "⚠️ Cleaning old lock files..." ;;
|
||
WARN_INVALID_KEY) echo "⚠️ Skipping invalid SSH key line" ;;
|
||
WARN_SHORT_RSA_KEY) echo "⚠️ RSA key too short:" ;;
|
||
WARN_SHORT_ED25519_KEY) echo "⚠️ Ed25519 key too short:" ;;
|
||
WARN_SHORT_DSA_KEY) echo "⚠️ DSA key too short:" ;;
|
||
ERR_INVALID_KEY_FORMAT) echo "❌ SSH key format invalid" ;;
|
||
ERR_MISSING_BASE64) echo "❌ SSH key missing base64 part" ;;
|
||
ERR_INVALID_BASE64) echo "❌ SSH key base64 encoding invalid" ;;
|
||
WARN_NO_BASE64_SKIPLEN) echo "⚠️ base64 not found: skipping key length checks (format-only validation)" ;;
|
||
WARN_USER_SHELL) echo "⚠️ User shell does not allow login:" ;;
|
||
ASK_CHANGE_SHELL) echo "Change user's shell to /bin/bash? [y/N]: " ;;
|
||
WARN_CHANGE_SHELL_FAIL) echo "⚠️ Failed to change shell" ;;
|
||
WARN_UNUSUAL_SHELL) echo "⚠️ User uses unusual shell:" ;;
|
||
WARN_HOME_OWNER) echo "⚠️ User home directory owner mismatch:" ;;
|
||
WARN_HOME_NOT_WRITABLE) echo "⚠️ User home directory not writable" ;;
|
||
ERR_USER_CREATE_FAIL) echo "❌ Failed to create user" ;;
|
||
ERR_USER_VERIFY_FAIL) echo "❌ User verification failed after creation" ;;
|
||
WARN_NO_SUDOERS_DIR) echo "⚠️ No /etc/sudoers.d directory, skipping sudo config" ;;
|
||
INFO_SUDO_EXISTS) echo "ℹ️ User already has sudo permissions" ;;
|
||
ERR_SUDOERS_SYNTAX) echo "❌ sudoers file syntax error, deleted" ;;
|
||
ERR_SUDOERS_PERM) echo "❌ Cannot set sudoers file permissions" ;;
|
||
INFO_SUDO_CONFIGURED) echo "✅ Configured sudo permissions for user" ;;
|
||
WARN_SSH_PROTOCOL) echo "⚠️ SSH protocol handshake failed or timed out" ;;
|
||
INFO_SSH_PROTOCOL_OK) echo "✅ SSH protocol handshake successful" ;;
|
||
WARN_PORT_OPEN_BUT_FAIL) echo "⚠️ Port is open but SSH client connection failed" ;;
|
||
WARN_X11_FORWARDING) echo "⚠️ X11 forwarding enabled, potential security risk" ;;
|
||
WARN_EMPTY_PASSWORDS) echo "⚠️ Empty passwords allowed, security risk" ;;
|
||
WARN_INSECURE_OPTIONS) echo "⚠️ Found potential insecure options" ;;
|
||
ERR_DEADLOCK) echo "❌ FATAL: Both password and key authentication disabled, will cause lockout!" ;;
|
||
ERR_PASSWORD_NO_KEY) echo "❌ FATAL: Password auth disabled but no SSH key deployed" ;;
|
||
ERR_ROOT_NO_KEY) echo "❌ FATAL: Root password login disabled but no SSH key deployed" ;;
|
||
WARN_PORT_MISMATCH) echo "⚠️ Port in config does not match target port" ;;
|
||
ERR_CANNOT_RESERVE_PORT) echo "❌ Cannot reserve port, port may be occupied" ;;
|
||
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
|
||
}
|
||
|
||
# ---------------- Logging & Audit ----------------
|
||
init_log_files() {
|
||
for logfile in "$LOG_FILE" "$AUDIT_FILE"; do
|
||
if [ -f "$logfile" ]; then
|
||
if [ ! -w "$logfile" ]; then
|
||
logfile_new="${logfile}.$(date +%s)"
|
||
if touch "$logfile_new" 2>/dev/null; then
|
||
chmod 600 "$logfile_new" 2>/dev/null || true
|
||
if [ "$logfile" = "$LOG_FILE" ]; then
|
||
LOG_FILE="$logfile_new"
|
||
else
|
||
AUDIT_FILE="$logfile_new"
|
||
fi
|
||
fi
|
||
fi
|
||
else
|
||
touch "$logfile" 2>/dev/null || true
|
||
chmod 600 "$logfile" 2>/dev/null || true
|
||
fi
|
||
done
|
||
}
|
||
|
||
init_log_files
|
||
|
||
log() { echo "$(date '+%F %T') $*" >>"$LOG_FILE" 2>/dev/null || true; }
|
||
|
||
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; }
|
||
|
||
# =========================================================
|
||
# Core Logic Functions
|
||
# =========================================================
|
||
|
||
preflight_checks() {
|
||
essential_cmds="cat grep awk sed cp mv chmod chown mkdir rm id"
|
||
extended_cmds="wc tr head cut touch find sleep date df uname tail"
|
||
|
||
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
|
||
|
||
for cmd in $extended_cmds; do
|
||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||
warn "Optional command not found: $cmd (some features may be limited)"
|
||
fi
|
||
done
|
||
|
||
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"
|
||
fi
|
||
|
||
if [ -f /proc/meminfo ]; then
|
||
mem_avail=$(grep MemAvailable /proc/meminfo 2>/dev/null | awk '{print $2}' 2>/dev/null || echo "")
|
||
if [ -n "$mem_avail" ] && [ "$mem_avail" -lt 51200 ] 2>/dev/null; then
|
||
warn "$(msg WARN_MEM)${mem_avail}KB"
|
||
fi
|
||
fi
|
||
|
||
if ! command -v base64 >/dev/null 2>&1; then
|
||
warn "$(msg WARN_NO_BASE64_SKIPLEN)"
|
||
fi
|
||
}
|
||
|
||
# ---------------- 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 || true; 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 || true;
|
||
else yum makecache -y >>"$LOG_FILE" 2>&1 || true; 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 ;;
|
||
*) return 1 ;;
|
||
esac
|
||
}
|
||
|
||
install_pkg_try() {
|
||
for p in "$@"; do
|
||
if install_pkg "$p" >/dev/null 2>&1; then return 0; fi
|
||
done
|
||
return 1
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
# ---------------- SSHD Restart ----------------
|
||
restart_sshd() {
|
||
if [ "$ARG_DELAY_RESTART" = "y" ]; then
|
||
warn "DELAY RESTART: Please manually restart sshd later."
|
||
return 0
|
||
fi
|
||
|
||
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
|
||
if [ -x /etc/init.d/sshd ]; then /etc/init.d/sshd restart >>"$LOG_FILE" 2>&1 && res=0; fi
|
||
if [ -x /etc/init.d/ssh ]; then /etc/init.d/ssh restart >>"$LOG_FILE" 2>&1 && res=0; fi
|
||
fi
|
||
|
||
return "$res"
|
||
}
|
||
|
||
# ---------------- Robust Rollback ----------------
|
||
ROLLBACK_DIR="$TMP_DIR/rollback"
|
||
|
||
update_state() {
|
||
phase="$1"
|
||
details="${2:-}"
|
||
{
|
||
echo "PHASE=$phase"
|
||
echo "TIMESTAMP=$(date +%s)"
|
||
echo "USER=${TARGET_USER:-unknown}"
|
||
echo "PORT=${SSH_PORT:-22}"
|
||
echo "KEY_OK=${KEY_OK:-n}"
|
||
echo "DETAILS=$details"
|
||
} > "$STATE_FILE" 2>/dev/null || true
|
||
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)"
|
||
|
||
# [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
|
||
}
|
||
|
||
cleanup_state() { rm -f "$STATE_FILE" 2>/dev/null || true; }
|
||
|
||
cleanup_locks() {
|
||
if [ -n "${LOCK_DIR:-}" ] && [ -d "$LOCK_DIR" ]; then
|
||
rm -rf "$LOCK_DIR" 2>/dev/null || true
|
||
fi
|
||
}
|
||
|
||
rollback_handler() {
|
||
RET=$?
|
||
trap - INT TERM EXIT HUP
|
||
|
||
if [ "$RET" -ne 0 ]; then
|
||
warn ""
|
||
warn "$(msg RB_START)"
|
||
|
||
if [ -f "$ROLLBACK_DIR/sshd_config" ]; then
|
||
cp -p "$ROLLBACK_DIR/sshd_config" "$SSH_CONF" 2>/dev/null || true
|
||
chmod 600 "$SSH_CONF" 2>/dev/null || true
|
||
fi
|
||
|
||
if [ -d "$ROLLBACK_DIR/sshd_config.d" ] && [ -d "$SSH_CONF_D" ]; then
|
||
for f in "$ROLLBACK_DIR/sshd_config.d"/*; do
|
||
[ -f "$f" ] || continue
|
||
cp -p "$f" "$SSH_CONF_D"/ 2>/dev/null || true
|
||
done
|
||
fi
|
||
|
||
restart_sshd >/dev/null 2>&1 || true
|
||
warn "$(msg RB_DONE)"
|
||
audit_log "ROLLBACK" "System rolled back due to error code $RET"
|
||
else
|
||
rm -rf "$TMP_DIR" 2>/dev/null || true
|
||
fi
|
||
|
||
cleanup_locks
|
||
exit "$RET"
|
||
}
|
||
|
||
setup_rollback() {
|
||
mkdir -p "$ROLLBACK_DIR" 2>/dev/null || true
|
||
|
||
check_previous_state
|
||
update_state "setup" "Init"
|
||
|
||
[ -f "$SSH_CONF" ] && cp -p "$SSH_CONF" "$ROLLBACK_DIR/sshd_config" 2>/dev/null || true
|
||
if [ -d "$SSH_CONF_D" ]; then
|
||
mkdir -p "$ROLLBACK_DIR/sshd_config.d" 2>/dev/null || true
|
||
for f in "$SSH_CONF_D"/*; do
|
||
[ -f "$f" ] || continue
|
||
cp -p "$f" "$ROLLBACK_DIR/sshd_config.d/" 2>/dev/null || true
|
||
done
|
||
fi
|
||
|
||
if command -v iptables-save >/dev/null 2>&1; then
|
||
iptables-save > "$ROLLBACK_DIR/iptables.backup" 2>/dev/null || true
|
||
fi
|
||
|
||
trap 'rollback_handler' INT TERM EXIT HUP
|
||
}
|
||
|
||
# ---------------- Persistent Backup ----------------
|
||
cleanup_old_backups() {
|
||
keep_count=10
|
||
if [ -d "$BACKUP_REPO" ]; then
|
||
backup_list=$(ls -dt "$BACKUP_REPO"/*/ 2>/dev/null) || return 0
|
||
count=$(echo "$backup_list" | grep -c . 2>/dev/null || echo 0)
|
||
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" | 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
|
||
fi
|
||
}
|
||
|
||
backup_config_persistent() {
|
||
timestamp=$(date +%Y%m%d_%H%M%S)
|
||
if command -v date >/dev/null 2>&1 && date --version 2>&1 | grep -q GNU; then
|
||
timestamp=$(date +%Y%m%d_%H%M%S%N 2>/dev/null || echo "$timestamp")
|
||
fi
|
||
|
||
backup_dir="$BACKUP_REPO/$timestamp"
|
||
|
||
if ! mkdir -p "$BACKUP_REPO" 2>/dev/null; then
|
||
warn "$(msg ERR_BACKUP_DIR) $BACKUP_REPO"
|
||
BACKUP_REPO="/tmp/server-init-backups"
|
||
backup_dir="$BACKUP_REPO/$timestamp"
|
||
mkdir -p "$BACKUP_REPO" 2>/dev/null || {
|
||
warn "$(msg ERR_BACKUP_DIR_ALT)"
|
||
return 1
|
||
}
|
||
fi
|
||
|
||
if ! mkdir -p "$backup_dir" 2>/dev/null; then
|
||
warn "$(msg ERR_BACKUP_SUBDIR) $backup_dir"
|
||
return 1
|
||
fi
|
||
|
||
chmod 700 "$backup_dir" 2>/dev/null || true
|
||
|
||
if [ -f "$SSH_CONF" ]; then
|
||
cp -p "$SSH_CONF" "$backup_dir/sshd_config" 2>/dev/null || true
|
||
chmod 600 "$backup_dir/sshd_config" 2>/dev/null || true
|
||
fi
|
||
|
||
{
|
||
echo "=== Server Init Backup ==="
|
||
echo "Time: $(date)"
|
||
echo "Version: 4.6.0"
|
||
echo "User: ${TARGET_USER:-unknown}"
|
||
echo "Port: ${SSH_PORT:-unknown}"
|
||
echo "OpenSSH: ${OPENSSH_VER_MAJOR}.${OPENSSH_VER_MINOR}"
|
||
echo "--- System ---"
|
||
uname -a 2>/dev/null || true
|
||
} > "$backup_dir/backup.info" 2>/dev/null || true
|
||
|
||
cat > "$backup_dir/restore.sh" <<'EOF'
|
||
#!/bin/sh
|
||
set -e
|
||
BACKUP_DIR=$(dirname "$0")
|
||
SSH_CONFIG="/etc/ssh/sshd_config"
|
||
echo "Restoring SSH Config..."
|
||
[ -f "$BACKUP_DIR/sshd_config" ] || exit 1
|
||
cp -p "$SSH_CONFIG" "$SSH_CONFIG.bak-$(date +%s)" 2>/dev/null || true
|
||
cp -p "$BACKUP_DIR/sshd_config" "$SSH_CONFIG"
|
||
chmod 600 "$SSH_CONFIG" 2>/dev/null || true
|
||
systemctl restart sshd 2>/dev/null || service sshd restart 2>/dev/null || true
|
||
echo "Done."
|
||
EOF
|
||
chmod +x "$backup_dir/restore.sh" 2>/dev/null || true
|
||
|
||
(cd "$backup_dir" && sha256sum * 2>/dev/null > checksums.sha256) || true
|
||
|
||
cleanup_old_backups
|
||
info "$(msg INFO_BACKUP_CREATED) $backup_dir"
|
||
return 0
|
||
}
|
||
|
||
# ---------------- BBR ----------------
|
||
enable_bbr() {
|
||
command -v sysctl >/dev/null 2>&1 || return 0
|
||
if ! sysctl -n net.ipv4.tcp_available_congestion_control 2>/dev/null | grep -q bbr; then
|
||
warn "Kernel does not support BBR, skipping."
|
||
return 0
|
||
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() {
|
||
if [ -f "$SSH_CONF" ] && command -v sshd >/dev/null 2>&1; then
|
||
return 0
|
||
fi
|
||
info "$(msg I_SSH_INSTALL)"
|
||
case "$PM" in
|
||
apk) install_pkg openssh openssh-server ;;
|
||
*) install_pkg openssh-server ;;
|
||
esac
|
||
|
||
if ! command -v sshd >/dev/null 2>&1; then
|
||
die "$(msg ERR_MISSING_SSHD)"
|
||
fi
|
||
[ -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
|
||
}
|
||
|
||
detect_openssh_version() {
|
||
OPENSSH_VER_MAJOR=0
|
||
OPENSSH_VER_MINOR=0
|
||
ver_str=""
|
||
|
||
if command -v sshd >/dev/null 2>&1; then
|
||
ver_str=$(sshd -V 2>&1 | sed -n 's/.*OpenSSH_\([0-9]*\)\.\([0-9]*\).*/\1.\2/p' | head -1)
|
||
fi
|
||
|
||
if [ -z "$ver_str" ] && command -v ssh >/dev/null 2>&1; then
|
||
ver_str=$(ssh -V 2>&1 | sed -n 's/.*OpenSSH_\([0-9]*\)\.\([0-9]*\).*/\1.\2/p' | head -1)
|
||
fi
|
||
|
||
if [ -n "$ver_str" ]; then
|
||
OPENSSH_VER_MAJOR=$(echo "$ver_str" | cut -d. -f1 2>/dev/null || echo 0)
|
||
OPENSSH_VER_MINOR=$(echo "$ver_str" | cut -d. -f2 2>/dev/null || echo 0)
|
||
fi
|
||
|
||
case "$OPENSSH_VER_MAJOR" in
|
||
''|*[!0-9]*) OPENSSH_VER_MAJOR=7 ;;
|
||
esac
|
||
case "$OPENSSH_VER_MINOR" in
|
||
''|*[!0-9]*) OPENSSH_VER_MINOR=0 ;;
|
||
esac
|
||
[ "$OPENSSH_VER_MAJOR" -eq 0 ] 2>/dev/null && OPENSSH_VER_MAJOR=7
|
||
}
|
||
|
||
openssh_version_ge() {
|
||
req_major="$1"
|
||
req_minor="${2:-0}"
|
||
|
||
[ "$OPENSSH_VER_MAJOR" -gt "$req_major" ] && return 0
|
||
[ "$OPENSSH_VER_MAJOR" -eq "$req_major" ] && [ "$OPENSSH_VER_MINOR" -ge "$req_minor" ] && return 0
|
||
return 1
|
||
}
|
||
|
||
# ---------------- Firewall / SELinux ----------------
|
||
allow_firewall_port() {
|
||
p="$1"
|
||
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 -C INPUT -p tcp --dport "$p" -j ACCEPT 2>/dev/null || iptables -I INPUT -p tcp --dport "$p" -j ACCEPT 2>>"$LOG_FILE" || true
|
||
fi
|
||
|
||
if command -v ip6tables >/dev/null 2>&1; then
|
||
ip6tables -C INPUT -p tcp --dport "$p" -j ACCEPT 2>/dev/null || 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 2>/dev/null | 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
|
||
53|80|443|3306)
|
||
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
|
||
if ss -lnt 2>/dev/null | awk -v port="$p" '
|
||
NR > 1 {
|
||
n = split($4, parts, ":")
|
||
if (parts[n] == port) { exit 0 }
|
||
}
|
||
END { exit 1 }
|
||
'; then
|
||
return 1
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
if command -v netstat >/dev/null 2>&1; then
|
||
if netstat -lnt 2>/dev/null | awk -v port="$p" '
|
||
NR > 2 {
|
||
n = split($4, parts, ":")
|
||
if (parts[n] == port) { exit 0 }
|
||
}
|
||
END { exit 1 }
|
||
'; then
|
||
return 1
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
return 1
|
||
}
|
||
|
||
pick_random_port() {
|
||
ensure_port_tools
|
||
i=0
|
||
|
||
if ! mkdir -p "$LOCK_DIR" 2>/dev/null; then
|
||
warn "$(msg ERR_LOCK_DIR) $LOCK_DIR"
|
||
return 1
|
||
fi
|
||
chmod 700 "$LOCK_DIR" 2>/dev/null || warn "$(msg WARN_LOCK_DIR_PERM)"
|
||
find "$LOCK_DIR" -name "port-*.lock" -mmin +5 -delete 2>/dev/null || true
|
||
|
||
while [ "$i" -lt 100 ]; do
|
||
r="$(rand_u16)"
|
||
p=$(( 49152 + (r % (65535 - 49152)) ))
|
||
lockfile="$LOCK_DIR/port-$p.lock"
|
||
if mkdir "$lockfile" 2>/dev/null; then
|
||
if is_port_free "$p"; then
|
||
echo "$p"
|
||
return 0
|
||
else
|
||
rmdir "$lockfile" 2>/dev/null || true
|
||
fi
|
||
fi
|
||
i=$((i+1))
|
||
done
|
||
|
||
warn "$(msg ERR_CANNOT_RESERVE_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
|
||
|
||
# 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
|
||
}
|
||
|
||
safe_configure_sudo() {
|
||
user="$1"
|
||
if [ ! -d /etc/sudoers.d ]; then warn "$(msg WARN_NO_SUDOERS_DIR)"; return 0; fi
|
||
if grep -q "^[[:space:]]*$user" /etc/sudoers /etc/sudoers.d/* 2>/dev/null; then info "$(msg INFO_SUDO_EXISTS)"; return 0; fi
|
||
|
||
timestamp=$(date +%Y%m%d%H%M%S)
|
||
sudoers_file="/etc/sudoers.d/server-init-$user-$timestamp"
|
||
cat > "$sudoers_file" <<EOF
|
||
# Generated by server-init
|
||
$user ALL=(ALL) NOPASSWD:ALL
|
||
Defaults:$user !requiretty
|
||
Defaults:$user env_keep += "SSH_AUTH_SOCK"
|
||
EOF
|
||
|
||
if command -v visudo >/dev/null 2>&1; then
|
||
if ! visudo -c -f "$sudoers_file" >/dev/null 2>&1; then
|
||
rm -f "$sudoers_file"
|
||
err "$(msg ERR_SUDOERS_SYNTAX)"
|
||
return 1
|
||
fi
|
||
fi
|
||
chmod 440 "$sudoers_file" 2>/dev/null || { rm -f "$sudoers_file"; err "$(msg ERR_SUDOERS_PERM)"; return 1; }
|
||
info "$(msg INFO_SUDO_CONFIGURED)"
|
||
return 0
|
||
}
|
||
|
||
get_user_home() {
|
||
user="$1"
|
||
home=""
|
||
|
||
if command -v getent >/dev/null 2>&1; then
|
||
home=$(getent passwd "$user" 2>/dev/null | cut -d: -f6)
|
||
fi
|
||
|
||
if [ -z "$home" ] && [ -r /etc/passwd ]; then
|
||
home=$(awk -F: -v u="$user" '$1==u {print $6}' /etc/passwd 2>/dev/null)
|
||
fi
|
||
|
||
if [ -z "$home" ]; then
|
||
if [ "$user" = "root" ]; then
|
||
home="/root"
|
||
else
|
||
home="/home/$user"
|
||
fi
|
||
fi
|
||
|
||
echo "$home"
|
||
}
|
||
|
||
get_user_shell() {
|
||
user="$1"
|
||
shell=""
|
||
|
||
if command -v getent >/dev/null 2>&1; then
|
||
shell=$(getent passwd "$user" 2>/dev/null | cut -d: -f7)
|
||
fi
|
||
|
||
if [ -z "$shell" ] && [ -r /etc/passwd ]; then
|
||
shell=$(awk -F: -v u="$user" '$1==u {print $7}' /etc/passwd 2>/dev/null)
|
||
fi
|
||
|
||
echo "$shell"
|
||
}
|
||
|
||
safe_ensure_user() {
|
||
user="$1"
|
||
[ "$user" = "root" ] && return 0
|
||
|
||
if id "$user" >/dev/null 2>&1; then
|
||
shell=$(get_user_shell "$user")
|
||
home_dir=$(get_user_home "$user")
|
||
|
||
case "$shell" in
|
||
/bin/bash|/bin/sh|/usr/bin/bash|/usr/bin/sh|/bin/dash|/bin/ash) ;;
|
||
/sbin/nologin|/bin/false|/usr/sbin/nologin)
|
||
warn "$(msg WARN_USER_SHELL) $shell"
|
||
if [ "$AUTO_CONFIRM" != "y" ]; then
|
||
printf "%s" "$(msg ASK_CHANGE_SHELL)"
|
||
read -r change_shell
|
||
if [ "${change_shell:-n}" = "y" ]; then
|
||
new_shell="/bin/sh"
|
||
for try_shell in /bin/bash /bin/ash /bin/sh; do
|
||
[ -x "$try_shell" ] && { new_shell="$try_shell"; break; }
|
||
done
|
||
if command -v chsh >/dev/null 2>&1; then
|
||
chsh -s "$new_shell" "$user" 2>>"$LOG_FILE" || warn "$(msg WARN_CHANGE_SHELL_FAIL)"
|
||
elif command -v usermod >/dev/null 2>&1; then
|
||
usermod -s "$new_shell" "$user" 2>>"$LOG_FILE" || warn "$(msg WARN_CHANGE_SHELL_FAIL)"
|
||
else
|
||
warn "$(msg WARN_CHANGE_SHELL_FAIL)"
|
||
fi
|
||
fi
|
||
fi
|
||
;;
|
||
"") ;;
|
||
*) warn "$(msg WARN_UNUSUAL_SHELL) $shell" ;;
|
||
esac
|
||
|
||
if [ -n "$home_dir" ] && [ -d "$home_dir" ]; then
|
||
dir_owner=""
|
||
if stat -c "%U" "$home_dir" >/dev/null 2>&1; then
|
||
dir_owner=$(stat -c "%U" "$home_dir" 2>/dev/null)
|
||
else
|
||
dir_owner=$(ls -ld "$home_dir" 2>/dev/null | awk '{print $3}')
|
||
fi
|
||
[ -n "$dir_owner" ] && [ "$dir_owner" != "$user" ] && warn "$(msg WARN_HOME_OWNER) $dir_owner"
|
||
[ ! -w "$home_dir" ] && warn "$(msg WARN_HOME_NOT_WRITABLE)"
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
info "$(msg I_USER) $user"
|
||
shell="/bin/sh"
|
||
for test_shell in /bin/bash /usr/bin/bash /bin/ash /bin/sh /usr/bin/sh /bin/dash; do
|
||
if [ -x "$test_shell" ]; then shell="$test_shell"; break; fi
|
||
done
|
||
|
||
user_created=0
|
||
if command -v useradd >/dev/null 2>&1; then
|
||
useradd -m -s "$shell" "$user" >>"$LOG_FILE" 2>&1 && user_created=1
|
||
elif command -v adduser >/dev/null 2>&1; then
|
||
adduser -D -s "$shell" "$user" >>"$LOG_FILE" 2>&1 && user_created=1
|
||
fi
|
||
|
||
[ "$user_created" -eq 1 ] || { err "$(msg ERR_USER_CREATE_FAIL)"; return 1; }
|
||
id "$user" >/dev/null 2>&1 || { err "$(msg ERR_USER_VERIFY_FAIL)"; return 1; }
|
||
|
||
safe_configure_sudo "$user" || true
|
||
return 0
|
||
}
|
||
|
||
# ---------------- Keys ----------------
|
||
fetch_keys() {
|
||
mode="$1"
|
||
val="$2"
|
||
url=""
|
||
|
||
case "$mode" in
|
||
gh) url="https://github.com/$val.keys" ;;
|
||
url) url="$val" ;;
|
||
raw) printf "%s\n" "$val"; return 0 ;;
|
||
*) return 1 ;;
|
||
esac
|
||
|
||
if [ "$mode" = "url" ]; then
|
||
echo "$url" | grep -Eq '^https://|^http://' || { warn "Invalid URL scheme (must be http/https)"; return 1; }
|
||
fi
|
||
|
||
retries=0
|
||
max_retries=3
|
||
while [ "$retries" -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
|
||
retries=$((retries+1))
|
||
[ "$retries" -lt "$max_retries" ] && sleep 2
|
||
done
|
||
|
||
warn "Failed to fetch keys after $max_retries attempts"
|
||
return 1
|
||
}
|
||
|
||
validate_ssh_key_line() {
|
||
line="$1"
|
||
|
||
line=$(printf '%s' "$line" | tr -d '\000-\037\177' | sed 's/[[:space:]]*#.*$//')
|
||
[ -z "$line" ] && return 1
|
||
|
||
if ! printf '%s' "$line" | grep -Eq '^(ssh-(rsa|dss|ed25519)|ecdsa-sha2-nistp[0-9]+|sk-(ssh-ed25519|ecdsa-sha2-nistp[0-9]+)@openssh\.com|ssh-(rsa|dss|ed25519)-cert-v01@openssh\.com|ecdsa-sha2-nistp[0-9]+-cert-v01@openssh\.com|rsa-sha2-(256|512))[[:space:]]+[A-Za-z0-9+/]+=*([[:space:]]+.*)?$'; then
|
||
return 1
|
||
fi
|
||
|
||
key_part=$(printf '%s' "$line" | awk '{print $2}')
|
||
[ -n "$key_part" ] || return 1
|
||
|
||
if command -v base64 >/dev/null 2>&1; then
|
||
if ! printf '%s' "$key_part" | base64 -d >/dev/null 2>&1; then
|
||
return 1
|
||
fi
|
||
key_type=$(printf '%s' "$line" | awk '{print $1}')
|
||
key_bytes=$(printf '%s' "$key_part" | base64 -d 2>/dev/null | wc -c | tr -d ' ')
|
||
case "$key_type" in
|
||
ssh-rsa|rsa-sha2-256|rsa-sha2-512)
|
||
[ -n "$key_bytes" ] && [ "$key_bytes" -ge 256 ] 2>/dev/null || return 1 ;;
|
||
ssh-ed25519|sk-ssh-ed25519@openssh.com)
|
||
[ -n "$key_bytes" ] && [ "$key_bytes" -ge 32 ] 2>/dev/null || return 1 ;;
|
||
ssh-dss)
|
||
[ -n "$key_bytes" ] && [ "$key_bytes" -ge 40 ] 2>/dev/null || return 1 ;;
|
||
esac
|
||
fi
|
||
|
||
printf "%s\n" "$line"
|
||
return 0
|
||
}
|
||
|
||
# [SEC-FIX] Improved deploy_keys - returns success only if THIS deployment added valid keys
|
||
deploy_keys() {
|
||
user="$1"
|
||
keys="$2"
|
||
home=$(get_user_home "$user")
|
||
dir="$home/.ssh"
|
||
auth="$dir/authorized_keys"
|
||
|
||
mkdir -p "$dir" 2>/dev/null || return 1
|
||
chmod 700 "$dir" 2>/dev/null || true
|
||
touch "$auth" 2>/dev/null || return 1
|
||
chmod 600 "$auth" 2>/dev/null || true
|
||
|
||
if ! chown -R "${user}:${user}" "$dir" 2>/dev/null; then
|
||
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
|
||
|
||
: > "$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
|
||
printf "%s\n" "$clean_line" >> "$valid_keys_file"
|
||
fi
|
||
done
|
||
|
||
# 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 ----------------
|
||
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|PubkeyAuthentication|ChallengeResponseAuthentication|KexAlgorithms|Ciphers|MACs|AddressFamily|ListenAddress)[[:space:]]' "$conf"; then
|
||
mv "$conf" "${conf}.bak_server_init" 2>/dev/null || true
|
||
warn "$(msg CLEAN_D) $conf"
|
||
fi
|
||
done
|
||
fi
|
||
}
|
||
|
||
remove_managed_block() {
|
||
tmp_in="$TMP_DIR/sshd_config.in"
|
||
tmp_out="$TMP_DIR/sshd_config.out"
|
||
cp -p "$SSH_CONF" "$tmp_in" 2>/dev/null || true
|
||
awk -v b="$BLOCK_BEGIN" -v e="$BLOCK_END" '
|
||
$0==b {skip=1; next}
|
||
$0==e {skip=0; next}
|
||
skip!=1 {print}
|
||
' "$tmp_in" >"$tmp_out"
|
||
if [ -s "$tmp_out" ]; then
|
||
cat "$tmp_out" > "$SSH_CONF"
|
||
fi
|
||
}
|
||
|
||
sanitize_sshd_config() {
|
||
info "$(msg INFO_SANITIZE_DUP)"
|
||
tmp_san="$TMP_DIR/sshd_config.sanitized"
|
||
|
||
awk '
|
||
/^[[:space:]]*Port[[:space:]]/ { print "# [server-init disabled] " $0; next }
|
||
/^[[:space:]]*PermitRootLogin[[:space:]]/ { print "# [server-init disabled] " $0; next }
|
||
/^[[:space:]]*PasswordAuthentication[[:space:]]/ { print "# [server-init disabled] " $0; next }
|
||
/^[[:space:]]*PubkeyAuthentication[[:space:]]/ { print "# [server-init disabled] " $0; next }
|
||
/^[[:space:]]*ChallengeResponseAuthentication[[:space:]]/ { print "# [server-init disabled] " $0; next }
|
||
/^[[:space:]]*KexAlgorithms[[:space:]]/ { print "# [server-init disabled] " $0; next }
|
||
/^[[:space:]]*Ciphers[[:space:]]/ { print "# [server-init disabled] " $0; next }
|
||
/^[[:space:]]*MACs[[:space:]]/ { print "# [server-init disabled] " $0; next }
|
||
/^[[:space:]]*AddressFamily[[:space:]]/ { print "# [server-init disabled] " $0; next }
|
||
/^[[:space:]]*ListenAddress[[:space:]]/ { print "# [server-init disabled] " $0; next }
|
||
{ print }
|
||
' "$SSH_CONF" > "$tmp_san"
|
||
|
||
if [ -s "$tmp_san" ]; then
|
||
cat "$tmp_san" > "$SSH_CONF"
|
||
fi
|
||
}
|
||
|
||
# [SEC-FIX] Improved IPv6 detection - exclude loopback
|
||
has_global_ipv6() {
|
||
# 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"
|
||
case ",$csv," in
|
||
*,"$item",*) return 0 ;;
|
||
*) return 1 ;;
|
||
esac
|
||
}
|
||
|
||
csv_intersect_ordered() {
|
||
pref="$1"
|
||
supp="$2"
|
||
result=""
|
||
|
||
# Use subshell to isolate IFS change
|
||
result=$(
|
||
IFS=,
|
||
for a in $pref; do
|
||
if [ -n "$a" ] && csv_contains "$supp" "$a"; then
|
||
printf "%s\n" "$a"
|
||
fi
|
||
done
|
||
)
|
||
|
||
# Convert newlines to commas
|
||
echo "$result" | tr '\n' ',' | sed 's/,$//'
|
||
}
|
||
|
||
get_sshd_T_value() {
|
||
key="$1"
|
||
v=""
|
||
|
||
# Only try -C on OpenSSH 6.8+
|
||
if openssh_version_ge 6 8; then
|
||
v=$(sshd -T -C user=root,host=localhost,addr=127.0.0.1 -f "$SSH_CONF" 2>/dev/null | awk -v k="$key" 'tolower($1)==k {print $2; exit}')
|
||
fi
|
||
|
||
# Fallback without -C
|
||
if [ -z "$v" ]; then
|
||
v=$(sshd -T -f "$SSH_CONF" 2>/dev/null | awk -v k="$key" 'tolower($1)==k {print $2; exit}')
|
||
fi
|
||
|
||
echo "$v"
|
||
}
|
||
|
||
compute_crypto_lines() {
|
||
KEX_LINE=""
|
||
CIPHERS_LINE=""
|
||
MACS_LINE=""
|
||
CRYPTO_MODE="skip"
|
||
|
||
if ! command -v sshd >/dev/null 2>&1; then
|
||
CRYPTO_MODE="skip"
|
||
return 0
|
||
fi
|
||
|
||
supp_kex=$(get_sshd_T_value "kexalgorithms")
|
||
supp_ciphers=$(get_sshd_T_value "ciphers")
|
||
supp_macs=$(get_sshd_T_value "macs")
|
||
|
||
pref_kex="curve25519-sha256@libssh.org,curve25519-sha256,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512"
|
||
pref_ciphers="chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr"
|
||
pref_macs="hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-512,hmac-sha2-256,umac-128@openssh.com"
|
||
|
||
if [ -n "$supp_kex" ] || [ -n "$supp_ciphers" ] || [ -n "$supp_macs" ]; then
|
||
sel_kex=$(csv_intersect_ordered "$pref_kex" "$supp_kex")
|
||
sel_ciphers=$(csv_intersect_ordered "$pref_ciphers" "$supp_ciphers")
|
||
sel_macs=$(csv_intersect_ordered "$pref_macs" "$supp_macs")
|
||
|
||
[ -n "$sel_kex" ] && KEX_LINE="KexAlgorithms $sel_kex"
|
||
[ -n "$sel_ciphers" ] && CIPHERS_LINE="Ciphers $sel_ciphers"
|
||
[ -n "$sel_macs" ] && MACS_LINE="MACs $sel_macs"
|
||
|
||
if [ -n "$KEX_LINE" ] || [ -n "$CIPHERS_LINE" ] || [ -n "$MACS_LINE" ]; then
|
||
CRYPTO_MODE="filtered"
|
||
else
|
||
CRYPTO_MODE="skip"
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
if openssh_version_ge 6 5; then
|
||
KEX_LINE="KexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256"
|
||
CIPHERS_LINE="Ciphers chacha20-poly1305@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr"
|
||
MACS_LINE="MACs hmac-sha2-512,hmac-sha2-256"
|
||
CRYPTO_MODE="fallback"
|
||
else
|
||
CRYPTO_MODE="skip"
|
||
fi
|
||
}
|
||
|
||
build_block() {
|
||
file="$1"
|
||
{
|
||
echo "$BLOCK_BEGIN"
|
||
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."
|
||
echo ""
|
||
|
||
echo "Port $SSH_PORT"
|
||
|
||
[ -n "$KEX_LINE" ] && echo "$KEX_LINE"
|
||
[ -n "$CIPHERS_LINE" ] && echo "$CIPHERS_LINE"
|
||
[ -n "$MACS_LINE" ] && echo "$MACS_LINE"
|
||
|
||
if [ "$IPV6_ENABLED" = "y" ]; then
|
||
echo "AddressFamily any"
|
||
echo "ListenAddress ::"
|
||
echo "ListenAddress 0.0.0.0"
|
||
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"
|
||
else
|
||
echo "PasswordAuthentication yes"
|
||
echo "PubkeyAuthentication yes"
|
||
fi
|
||
|
||
if [ "$TARGET_USER" = "root" ]; then
|
||
if [ "$KEY_OK" = "y" ]; then
|
||
if openssh_version_ge 7 0; then
|
||
echo "PermitRootLogin prohibit-password"
|
||
else
|
||
echo "PermitRootLogin without-password"
|
||
fi
|
||
else
|
||
echo "PermitRootLogin yes"
|
||
fi
|
||
else
|
||
if [ "$KEY_OK" = "y" ]; then
|
||
echo "PermitRootLogin no"
|
||
else
|
||
if [ "$ROOT_KEY_PRESENT" = "y" ]; then
|
||
if openssh_version_ge 7 0; then
|
||
echo "PermitRootLogin prohibit-password"
|
||
else
|
||
echo "PermitRootLogin without-password"
|
||
fi
|
||
else
|
||
echo "PermitRootLogin yes"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
echo ""
|
||
echo "$BLOCK_END"
|
||
} >"$file"
|
||
}
|
||
|
||
# [SEC-FIX] Use awk instead of tail -n +N for better POSIX compatibility
|
||
install_managed_block() {
|
||
block="$1"
|
||
tmp="$TMP_DIR/sshd_config.merge"
|
||
|
||
# Find first non-comment Match line
|
||
match_line=$(awk '/^[[:space:]]*#/ {next} /^[[:space:]]*Match[[:space:]]/ {print NR; exit}' "$SSH_CONF" 2>/dev/null)
|
||
|
||
if [ -z "$match_line" ]; then
|
||
# No Match block found -> append at end
|
||
cat "$SSH_CONF" "$block" > "$tmp"
|
||
else
|
||
info "$(msg INFO_MATCH_INSERT)"
|
||
# 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
|
||
mv "$tmp" "$SSH_CONF"
|
||
}
|
||
|
||
verify_sshd_listening() {
|
||
port="$1"
|
||
timeout_s=10
|
||
elapsed=0
|
||
|
||
ensure_port_tools
|
||
|
||
while [ "$elapsed" -lt "$timeout_s" ]; do
|
||
if ! is_port_free "$port"; then
|
||
return 0
|
||
fi
|
||
if command -v nc >/dev/null 2>&1; then
|
||
nc -z -w 1 localhost "$port" 2>/dev/null && return 0
|
||
fi
|
||
sleep 1
|
||
elapsed=$((elapsed + 1))
|
||
done
|
||
return 1
|
||
}
|
||
|
||
# [SEC-FIX] Enhanced SSH test - require SSH-2.0 banner
|
||
enhanced_ssh_test() {
|
||
port="$1"
|
||
user="$2"
|
||
info "$(msg TEST_CONN)"
|
||
|
||
if ! verify_sshd_listening "$port"; then
|
||
err "SSHD not listening on port $port"
|
||
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 3 nc localhost "$port" 2>/dev/null || true)
|
||
else
|
||
proto=$(printf "SSH-2.0-TEST\r\n" | nc -w 3 localhost "$port" 2>/dev/null || true)
|
||
fi
|
||
|
||
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
|
||
while [ "$attempts" -le "$max_attempts" ]; do
|
||
if command -v ssh >/dev/null 2>&1; then
|
||
if ssh -o BatchMode=yes -o ConnectTimeout=3 -o StrictHostKeyChecking=no \
|
||
-o UserKnownHostsFile=/dev/null -o GlobalKnownHostsFile=/dev/null \
|
||
-p "$port" "$user@localhost" "exit 0" >/dev/null 2>&1; then
|
||
success=1
|
||
break
|
||
fi
|
||
fi
|
||
attempts=$((attempts + 1))
|
||
[ "$attempts" -le "$max_attempts" ] && sleep 1
|
||
done
|
||
|
||
if [ "$success" -eq 1 ]; then
|
||
ok "$(msg TEST_OK)"
|
||
else
|
||
# Banner was OK but auth failed - this is expected without the right key
|
||
warn "$(msg WARN_PORT_OPEN_BUT_FAIL)"
|
||
fi
|
||
|
||
return 0
|
||
}
|
||
|
||
update_motd() {
|
||
info "$(msg MOTD_UPD)"
|
||
motd="/etc/motd"
|
||
tmp="$TMP_DIR/motd.new"
|
||
[ -f "$motd" ] && grep -v "Server Init Complete" "$motd" > "$tmp" 2>/dev/null || true
|
||
{
|
||
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/Fallback")"
|
||
echo " Firewall: Please ensure TCP/$SSH_PORT is allowed."
|
||
echo "==============================================================================="
|
||
echo ""
|
||
[ -s "$tmp" ] && cat "$tmp"
|
||
} > "${motd}.final"
|
||
mv "${motd}.final" "$motd" 2>/dev/null || true
|
||
}
|
||
|
||
generate_health_report() {
|
||
report_file="/var/log/server-init-health.log"
|
||
end_time=$(date +%s)
|
||
duration=$((end_time - SCRIPT_START_TIME))
|
||
sys_uptime=$(uptime -p 2>/dev/null || uptime 2>/dev/null | awk -F, '{print $1}')
|
||
|
||
{
|
||
echo "=== Server Init Health Report ==="
|
||
echo "Generated: $(date)"
|
||
echo "Version: v4.6.0 Fortress Pro"
|
||
echo "Execution Time: ${duration}s"
|
||
echo ""
|
||
echo "--- System ---"
|
||
echo "Uptime: $sys_uptime"
|
||
echo "OpenSSH: ${OPENSSH_VER_MAJOR}.${OPENSSH_VER_MINOR}"
|
||
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 "IPv6: $([ "$IPV6_ENABLED" = "y" ] && echo "Enabled" || echo "Disabled")"
|
||
echo "Port Status: $(is_port_free "$SSH_PORT" && echo "NOT LISTENING (Error)" || echo "LISTENING (OK)")"
|
||
echo "Crypto Mode: $CRYPTO_MODE"
|
||
} > "$report_file" 2>/dev/null || true
|
||
chmod 600 "$report_file" 2>/dev/null || true
|
||
info "Health report saved to: $report_file"
|
||
}
|
||
|
||
print_final_summary() {
|
||
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=""
|
||
if command -v hostname >/dev/null 2>&1; then
|
||
local_ip=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||
fi
|
||
if [ -z "$local_ip" ] && command -v ip >/dev/null 2>&1; then
|
||
local_ip=$(ip -4 addr show 2>/dev/null | awk '
|
||
/inet / {
|
||
ip=$2
|
||
sub(/\/.*/, "", ip)
|
||
if (ip !~ /^127\./) { print ip; exit }
|
||
}
|
||
')
|
||
fi
|
||
|
||
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)"
|
||
[ -n "$public_ip" ] && printf "${CYAN}║ Public: ssh -p %-5s %s@%s %-16s ║${NC}\n" "$SSH_PORT" "$TARGET_USER" "$public_ip" ""
|
||
[ -n "$local_ip" ] && printf "${CYAN}║ Local: ssh -p %-5s %s@%s %-16s ║${NC}\n" "$SSH_PORT" "$TARGET_USER" "$local_ip" ""
|
||
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 "Audit: $AUDIT_FILE"
|
||
echo "Time: ${duration}s"
|
||
}
|
||
|
||
validate_ssh_config_comprehensive() {
|
||
config_file="$1"
|
||
user="$2"
|
||
key_ok="$3"
|
||
|
||
if ! sshd -t -f "$config_file" 2>>"$LOG_FILE"; then
|
||
err "SSH Config Syntax Error"
|
||
return 1
|
||
fi
|
||
|
||
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"
|
||
[ -n "$port_setting" ] || port_setting="22"
|
||
|
||
if [ "$password_auth" = "no" ] && [ "$pubkey_auth" = "no" ]; then
|
||
die "$(msg ERR_DEADLOCK)"
|
||
fi
|
||
if [ "$password_auth" = "no" ] && [ "$key_ok" != "y" ]; then
|
||
die "$(msg ERR_PASSWORD_NO_KEY)"
|
||
fi
|
||
if [ "$user" = "root" ] && [ "$password_auth" = "no" ] && [ "$key_ok" != "y" ]; then
|
||
die "$(msg ERR_ROOT_NO_KEY)"
|
||
fi
|
||
if [ "$port_setting" != "$SSH_PORT" ]; then
|
||
warn "$(msg WARN_PORT_MISMATCH) ($port_setting vs $SSH_PORT)"
|
||
fi
|
||
|
||
insecure_options=0
|
||
if grep -q "^[[:space:]]*X11Forwarding[[:space:]]*yes" "$config_file" 2>/dev/null; then
|
||
warn "$(msg WARN_X11_FORWARDING)"
|
||
insecure_options=$((insecure_options + 1))
|
||
fi
|
||
if grep -q "^[[:space:]]*PermitEmptyPasswords[[:space:]]*yes" "$config_file" 2>/dev/null; then
|
||
warn "$(msg WARN_EMPTY_PASSWORDS)"
|
||
insecure_options=$((insecure_options + 1))
|
||
fi
|
||
[ "$insecure_options" -gt 0 ] && warn "$(msg WARN_INSECURE_OPTIONS) $insecure_options"
|
||
|
||
return 0
|
||
}
|
||
|
||
# =========================================================
|
||
# Entry
|
||
# =========================================================
|
||
[ "$(id -u)" -eq 0 ] || { echo "$(msg MUST_ROOT)"; exit 1; }
|
||
audit_log "START" "Script started with args: $*"
|
||
|
||
if command -v clear >/dev/null 2>&1; then clear; fi
|
||
echo "================================================="
|
||
msg BANNER
|
||
echo "================================================="
|
||
[ "$STRICT_MODE" = "y" ] && msg STRICT_ON
|
||
|
||
preflight_checks
|
||
|
||
# Phase 1: Input
|
||
if [ -n "$ARG_USER" ]; then
|
||
TARGET_USER="$ARG_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 -r TARGET_USER
|
||
[ -z "$TARGET_USER" ] && TARGET_USER="$DEFAULT_USER"
|
||
validate_username "$TARGET_USER" && break
|
||
msg ERR_USER_INV
|
||
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" ;;
|
||
*)
|
||
# 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
|
||
echo ""
|
||
msg ASK_PORT_T; msg OPT_PORT_1; msg OPT_PORT_2; msg OPT_PORT_3
|
||
printf "%s" "$(msg SELECT)"; read -r PORT_OPT
|
||
[ -z "$PORT_OPT" ] && PORT_OPT="1"
|
||
SSH_PORT="22"
|
||
if [ "$PORT_OPT" = "3" ]; then
|
||
while :; do
|
||
printf "%s" "$(msg INPUT_PORT)"
|
||
read -r 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 -r force_port
|
||
[ "${force_port:-n}" = "y" ] || continue
|
||
fi
|
||
SSH_PORT="$MANUAL_PORT"
|
||
break
|
||
done
|
||
fi
|
||
fi
|
||
|
||
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 -r KEY_OPT
|
||
case "$KEY_OPT" in
|
||
1) KEY_TYPE="gh"; printf "%s" "$(msg INPUT_GH)"; read -r KEY_VAL ;;
|
||
2) KEY_TYPE="url"; printf "%s" "$(msg INPUT_URL)"; read -r 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
|
||
|
||
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 -r DO_UPDATE; [ -z "$DO_UPDATE" ] && DO_UPDATE="n"; fi
|
||
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 -r 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 -r CONFIRM
|
||
[ "${CONFIRM:-n}" = "y" ] || die "$(msg CANCEL)"
|
||
fi
|
||
|
||
# Phase 3: Execute
|
||
msg AUDIT_START
|
||
setup_rollback
|
||
backup_config_persistent || true
|
||
|
||
info "$(msg I_INSTALL)"
|
||
ensure_ssh_server
|
||
|
||
detect_openssh_version
|
||
info "OpenSSH Version: ${OPENSSH_VER_MAJOR}.${OPENSSH_VER_MINOR}"
|
||
|
||
install_pkg_try curl >/dev/null 2>&1 || true
|
||
install_pkg_try wget >/dev/null 2>&1 || true
|
||
|
||
if [ "$DO_UPDATE" = "y" ]; then info "$(msg I_UPD)"; update_system; fi
|
||
if [ "$DO_BBR" = "y" ]; then info "$(msg I_BBR)"; enable_bbr; fi
|
||
|
||
if [ "$PORT_OPT" = "2" ]; then
|
||
p="$(pick_random_port 2>/dev/null || 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
|
||
|
||
if [ "$SSH_PORT" != "22" ]; then
|
||
allow_firewall_port "$SSH_PORT"
|
||
handle_selinux "$SSH_PORT"
|
||
fi
|
||
|
||
safe_ensure_user "$TARGET_USER" || die "User setup failed"
|
||
|
||
root_home=$(get_user_home root)
|
||
[ -s "$root_home/.ssh/authorized_keys" ] && ROOT_KEY_PRESENT="y" || ROOT_KEY_PRESENT="n"
|
||
|
||
KEY_OK="n"
|
||
KEY_DATA="$(fetch_keys "$KEY_TYPE" "$KEY_VAL" 2>/dev/null || true)"
|
||
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
|
||
|
||
if has_global_ipv6; then
|
||
IPV6_ENABLED="y"
|
||
info "$(msg IPV6_CFG)"
|
||
else
|
||
IPV6_ENABLED="n"
|
||
fi
|
||
|
||
compute_crypto_lines
|
||
if [ "$CRYPTO_MODE" = "skip" ]; then
|
||
info "$(msg INFO_OLD_SSH_SKIP_ALGO)"
|
||
elif [ "$CRYPTO_MODE" = "fallback" ]; then
|
||
warn "$(msg COMPAT_WARN)"
|
||
fi
|
||
|
||
info "$(msg I_BACKUP)$SSH_CONF"
|
||
cleanup_sshd_config_d
|
||
remove_managed_block
|
||
sanitize_sshd_config
|
||
|
||
tmp="$TMP_DIR/sshd_block_final"
|
||
build_block "$tmp"
|
||
|
||
install_managed_block "$tmp"
|
||
|
||
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"
|
||
|
||
# [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)"
|
||
enhanced_ssh_test "$SSH_PORT" "$TARGET_USER" || die "$(msg TEST_FAIL)"
|
||
|
||
update_motd
|
||
generate_health_report
|
||
|
||
trap - INT TERM EXIT HUP
|
||
cleanup_state
|
||
cleanup_locks
|
||
rm -rf "$TMP_DIR" 2>/dev/null || true
|
||
|
||
print_final_summary
|
||
audit_log "DONE" "Completed successfully. user=$TARGET_USER port=$SSH_PORT key_ok=$KEY_OK"
|
||
exit 0
|