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.
This commit is contained in:
247LIKE authored and GitHub committed 2025-12-12 19:41:06 +08:00
1 parent c888e95680
commit 7da9688943
1 file changed
+629
+629
View File
@@ -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@<IP>"
[ "$SSH_PORT" != "22" ] && msg DONE_FW
echo "Log: $LOG_FILE"