From 7da9688943137f0235336872a333ecb87352c880 Mon Sep 17 00:00:00 2001 From: 247LIKE <54342887+247like@users.noreply.github.com> Date: Fri, 12 Dec 2025 19:41:06 +0800 Subject: [PATCH] Add initial server init and SSH hardening script Initial production release of the linux-ssh-init-sh script with integrated i18n support for English and Chinese. --- init.sh | 629 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 629 insertions(+) create mode 100644 init.sh diff --git a/init.sh b/init.sh new file mode 100644 index 0000000..48218a2 --- /dev/null +++ b/init.sh @@ -0,0 +1,629 @@ +#!/bin/sh +# ========================================================= +# linux-ssh-init-sh +# Server Init & SSH Hardening Script +# +# Release: v1.0.0 (Initial Production Release) +# +# POSIX sh compatible (dash / ash / busybox) +# +# Logic Baseline: +# v2.2 – Production-hardened internal version +# (Debian 12 Include-safe, SSH lockout-safe) +# +# Changes in v1.0.0: +# - Public open-source release +# - Integrated i18n (English / 中文) +# - User-visible messages only +# - NO changes to execution flow or security logic +# +# Repository: +# https://github.com/247like/linux-ssh-init-sh +# +# ========================================================= + +set -eu + +# ---------------- Language Config ---------------- +LANG_CUR="en" # Default + +# Check args for language override +for a in "$@"; do + case "$a" in + --lang=zh) LANG_CUR="zh" ;; + --lang=en) LANG_CUR="en" ;; + esac +done + +# Simple language selector if not provided +if [ -z "${LANG_CUR}" ]; then + # Auto-detect is hard in POSIX sh reliably, so we default to EN or prompt + # We just set default to EN above. + # If you want interactive prompt at start, uncomment below: + # echo "Select Language: 1) English (Default) 2) 简体中文" + # read l_opt + # [ "$l_opt" = "2" ] && LANG_CUR="zh" + true +fi + +# ---------------- Messages ---------------- +msg() { + key="$1" + if [ "$LANG_CUR" = "zh" ]; then + case "$key" in + MUST_ROOT) echo "必须以 root 权限运行此脚本" ;; + BANNER) echo "服务器初始化 & SSH 安全加固 (v2.3 i18n)" ;; + STRICT_ON) echo "STRICT 模式已开启:任何关键错误将直接退出" ;; + ASK_USER) echo "SSH 登录用户 (root 或普通用户,默认 " ;; + ASK_PORT_T) echo "SSH 端口配置:" ;; + OPT_PORT_1) echo "1) 使用 22 (默认)" ;; + OPT_PORT_2) echo "2) 随机高端口 (推荐)" ;; + OPT_PORT_3) echo "3) 手动指定" ;; + SELECT) echo "请选择 [1-3]: " ;; + INPUT_PORT) echo "请输入端口号 (1024-65535): " ;; + PORT_ERR) echo "❌ 端口输入无效 (非数字或超范围)" ;; + 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 "已备份配置: " ;; + E_SSHD_CHK) echo "sshd 配置校验失败,已回滚" ;; + W_RESTART) echo "无法自动重启 SSH 服务,请手动重启" ;; + DONE_T) echo "================ 完成 ================" ;; + DONE_MSG1) echo "请【不要关闭】当前窗口。" ;; + DONE_MSG2) echo "请新开一个终端窗口测试登录:" ;; + DONE_FW) echo "⚠ 若无法连接,请再次检查防火墙设置" ;; + *) echo "$key" ;; + esac + else + case "$key" in + MUST_ROOT) echo "Must be run as root" ;; + BANNER) echo "Server Init & SSH Hardening (v2.3 i18n)" ;; + STRICT_ON) echo "STRICT mode ON: Critical errors will abort" ;; + ASK_USER) echo "SSH Login User (root or normal user, default " ;; + ASK_PORT_T) echo "SSH Port Configuration:" ;; + OPT_PORT_1) echo "1) Use 22 (Default)" ;; + OPT_PORT_2) echo "2) Random High Port (Recommended)" ;; + 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)" ;; + 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 "Backup created: " ;; + E_SSHD_CHK) echo "sshd config validation failed, rolled back." ;; + W_RESTART) echo "Could not restart sshd automatically. Please restart manually." ;; + 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." ;; + *) echo "$key" ;; + esac + fi +} + +# ========================================================= +# Core Logic (Identical to v2.2) +# ========================================================= + +LOG_FILE="/var/log/server-init.log" +SSH_CONF="/etc/ssh/sshd_config" +DEFAULT_USER="deploy" +STRICT_MODE="n" + +BLOCK_BEGIN="# BEGIN SERVER-INIT MANAGED BLOCK" +BLOCK_END="# END SERVER-INIT MANAGED BLOCK" + +# Flags +APT_UPDATED="n" +APK_UPDATED="n" +YUM_PREPARED="n" + +log() { echo "$(date '+%F %T') $*" >>"$LOG_FILE"; } +info() { echo "\033[0;34m[INFO]\033[0m $*"; log "[INFO] $*"; } +warn() { echo "\033[0;33m[WARN]\033[0m $*"; log "[WARN] $*"; } +err() { echo "\033[0;31m[ERR ]\033[0m $*"; log "[ERR ] $*"; } +die() { err "$*"; exit 1; } + +[ "$(id -u)" -eq 0 ] || die "$(msg MUST_ROOT)" +touch "$LOG_FILE" 2>/dev/null || true + +for a in "$@"; do + [ "$a" = "--strict" ] && STRICT_MODE="y" +done + +# ---------------- 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)" + +pm_prepare_once() { + case "$PM" in + apt) + if [ "$APT_UPDATED" != "y" ]; then + apt-get update -y >>"$LOG_FILE" 2>&1 + APT_UPDATED="y" + fi + ;; + apk) + if [ "$APK_UPDATED" != "y" ]; then + apk update >>"$LOG_FILE" 2>&1 || true + APK_UPDATED="y" + fi + ;; + yum) + if [ "$YUM_PREPARED" != "y" ]; then + 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" + fi + ;; + 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 + ;; + *) + die "Unknown Package Manager" + ;; + 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 + + # Check if BBR available in kernel + 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" + # Idempotent write + 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 +} + +# ---------------- ssh server ensure ---------------- +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" +} + +# ---------------- restart sshd ---------------- +restart_sshd() { + if command -v systemctl >/dev/null 2>&1; then + systemctl restart sshd >>"$LOG_FILE" 2>&1 || systemctl restart ssh >>"$LOG_FILE" 2>&1 + return 0 + fi + if command -v rc-service >/dev/null 2>&1; then + rc-service sshd restart >>"$LOG_FILE" 2>&1 + return 0 + fi + if command -v service >/dev/null 2>&1; then + service sshd restart >>"$LOG_FILE" 2>&1 || service ssh restart >>"$LOG_FILE" 2>&1 + return 0 + fi + [ -x /etc/init.d/sshd ] && /etc/init.d/sshd restart >>"$LOG_FILE" 2>&1 && return 0 + [ -x /etc/init.d/ssh ] && /etc/init.d/ssh restart >>"$LOG_FILE" 2>&1 && return 0 + return 1 +} + +# ---------------- firewall ---------------- +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 + fi +} + +# ---------------- random port ---------------- +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 ' ' + 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 + command -v ss >/dev/null 2>&1 && return 0 + install_pkg_try net-tools >/dev/null 2>&1 || true +} + +is_port_free() { + p="$1" + if command -v ss >/dev/null 2>&1; then + ss -lnt 2>/dev/null | awk '{print $4}' | grep -q ":$p\$" && return 1 || return 0 + fi + 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 +} + +pick_random_port() { + ensure_port_tools + i=0 + while [ $i -lt 80 ]; do + r="$(rand_u16)" + p=$(( (r % 40000) + 20000 )) + if is_port_free "$p"; then echo "$p"; return 0; fi + i=$((i+1)) + done + return 1 +} + +# ---------------- user ensure ---------------- +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 >/dev/null 2>&1 || true + install_pkg_try 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 +} + +# ---------------- managed block ---------------- +remove_managed_block() { + tmp="${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" + mv "$tmp" "$SSH_CONF" +} + +insert_block_at_top() { + block="$1" + tmp="${SSH_CONF}.tmp.$$" + cat "$block" "$SSH_CONF" >"$tmp" + mv "$tmp" "$SSH_CONF" +} + +build_block() { + file="$1" + { + echo "$BLOCK_BEGIN" + echo "# Managed by server-init. DO NOT edit inside this block." + echo "" + echo "Port $SSH_PORT" + + 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 + echo "PermitRootLogin prohibit-password" + else + echo "PermitRootLogin yes" + fi + else + echo "PermitRootLogin no" + fi + + echo "" + echo "$BLOCK_END" + echo "" + } >"$file" +} + +# ---------------- key fetch ---------------- +fetch_keys() { + case "$1" in + gh) curl -fsSL "https://github.com/$2.keys" 2>>"$LOG_FILE" || true ;; + url) curl -fsSL "$2" 2>>"$LOG_FILE" || true ;; + raw) printf "%s\n" "$2" ;; + esac +} + +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-|ecdsa-)' || continue + grep -qxF "$line" "$auth" || echo "$line" >>"$auth" + done + + grep -Eq '^(ssh-|ecdsa-)' "$auth" +} + +# ========================================================= +# Phase 1: Input (I18N) +# ========================================================= +clear +echo "=================================================" +msg BANNER +echo "=================================================" +[ "$STRICT_MODE" = "y" ] && msg STRICT_ON + +printf "%s%s): " "$(msg ASK_USER)" "$DEFAULT_USER" +read TARGET_USER +[ -z "$TARGET_USER" ] && TARGET_USER="$DEFAULT_USER" + +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; } + SSH_PORT="$MANUAL_PORT" + break + done +fi + +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 + +printf "%s" "$(msg ASK_UPD)" +read DO_UPDATE +[ -z "$DO_UPDATE" ] && DO_UPDATE="n" + +printf "%s" "$(msg ASK_BBR)" +read DO_BBR +[ -z "$DO_BBR" ] && DO_BBR="n" + +# ========================================================= +# Phase 2: Confirm (I18N) +# ========================================================= +echo "" +msg CONFIRM_T +echo "$(msg C_USER)$TARGET_USER" +echo "$(msg C_PORT)$SSH_PORT ($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)" + +# ========================================================= +# Phase 3: Execute (Logic from v2.2) +# ========================================================= +info "$(msg I_INSTALL)" +ensure_ssh_server +install_pkg_try curl >/dev/null 2>&1 || die "curl install failed" + +# Optional updates +if [ "$DO_UPDATE" = "y" ]; then + info "$(msg I_UPD)" + update_system +fi + +# Optional BBR +if [ "$DO_BBR" = "y" ]; then + info "$(msg I_BBR)" + enable_bbr +fi + +# Random Port (handled here to ensure tools exist) +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 (Before restart) +[ "$SSH_PORT" != "22" ] && allow_firewall_port "$SSH_PORT" + +# 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 Write +bak="${SSH_CONF}.bak_$(date +%F_%H%M%S)" +cp "$SSH_CONF" "$bak" +info "$(msg I_BACKUP)$bak" + +remove_managed_block +tmp="/tmp/sshd_block.$$" +build_block "$tmp" +insert_block_at_top "$tmp" +rm -f "$tmp" + +if ! sshd -t -f "$SSH_CONF" 2>>"$LOG_FILE"; then + cp "$bak" "$SSH_CONF" + die "$(msg E_SSHD_CHK)" +fi + +if ! restart_sshd; then + warn "$(msg W_RESTART)" +fi + +# ========================================================= +# Done +# ========================================================= +echo "" +msg DONE_T +msg DONE_MSG1 +msg DONE_MSG2 +echo " ssh -p $SSH_PORT $TARGET_USER@" +[ "$SSH_PORT" != "22" ] && msg DONE_FW +echo "Log: $LOG_FILE"