背景
给一台 Linux 做初始化确实不复杂,几条命令而已。但是,当需要面对多台、多种发行版、反复重建的环境时,这几条命令里藏着的细节差异,比如说 NetworkManager 覆写 DNS、sshd_config.d 覆盖 root 登录、ufw 和 firewalld 命令不同,就会变成没有必要的重复劳动。这个脚本的作用是,能灵活地根据不同的发行版进行针对性的配置,使得无论在哪台机器上都能得到正确的结果。
这个脚本中没有包含软件源配置的原因是,由于镜像站可能发生变动,有时候会使用内网源、官方源或第三方源,把这些写进脚本,既要维护一堆映射表,又未必适配所有环境,显得不是很有必要。
设计
在编写这个脚本时,首先把 DNS 地址、软件包列表这些容易变化的内容提取为全局变量,后续改起来不用修改核心逻辑。然后我意识到不同 Linux 发行版的命令差异很大,于是加了系统检测,比如说,让脚本在 Rocky 上走的是 firewalld 分支,在 Ubuntu 上是 ufw 分支。此外,有时候只想改个 DNS 而不动其他配置,于是把每个功能拆成独立函数,再用一个循环菜单串联起来。
脚本开头的 set -euo pipefail 是健壮性的保障。它要求变量必须声明、任何命令报错立即退出、管道符中任何一环报错即视为整体报错。将基础配置定义为环境变量,为后续的交互式修改提供基础。
现代 Linux 发行版都会遵循统一的规范,通过解析 /etc/os-release 文件,可以拿到系统的族系(如 debian, rocky)和主版本号。
在开启了 set -e 的脚本中,使用 read 命令进行用户交互时存在一个隐患:当用户输入 EOF(如 Ctrl+D)时,read 会返回非 0 状态码,在 set -e 下会导致脚本退出,因此需要 || true 来忽略该错误,同时,利用 [ -n "$input" ] 来判断用户是否输入了新值。
脚本
建议直接使用 root 账户或具备免密 sudo 权限的用户执行脚本并确保网络的连通性,执行脚本时,用 bash <脚本名>.sh 或 chmod +x <脚本名>.sh 之后,再执行 ./<脚本名>.sh,经测试,在某些场景下,使用 sh <脚本名>.sh 会报错。
#!/bin/bash
set -euo pipefail
# 全局默认配置
export CUSTOM_DNS_1="114.114.114.114"
export CUSTOM_DNS_2="8.8.8.8"
export DEFAULT_PKGS="curl wget vim tar net-tools"
# 安全策略默认状态
export PERMIT_ROOT_LOGIN="no"
export SELINUX_STATE="disabled" # 可选值: enforcing, disabled
export FIREWALL_STATE="stopped" # 可选值: started, stopped
# 颜色定义与基础日志函数
CYAN='\033[1;36m'
YELLOW='\033[1;33m'
GREEN='\033[1;32m'
RED='\033[1;31m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO] $1${NC}"; }
log_warn() { echo -e "${YELLOW}[WARN] $1${NC}"; }
log_err() { echo -e "${RED}[ERROR] $1${NC}"; }
# 检测是否为root用户
check_root() {
if [ $EUID -ne 0 ]; then
log_err "请使用 root 用户执行此脚本"
exit 1
fi
}
# 系统检测
detect_os() {
if [ -f /etc/os-release ]; then
. /etc/os-release
OS_FAMILY="${ID}"
OS_VERSION="${VERSION_ID%%.*}"
else
log_err "无法识别系统类型:缺失 /etc/os-release"
exit 1
fi
}
# 系统当前状态检测
fetch_system_live_status() {
# 1. 获取当前主 DNS
LIVE_DNS=$(grep -m 1 "^nameserver" /etc/resolv.conf | awk '{print $2}' || true)
[ -z "$LIVE_DNS" ] && LIVE_DNS="未配置"
# 2. 获取当前 SSH Root 登录状态
local sshd_conf="/etc/ssh/sshd_config"
if [ -f "$sshd_conf" ]; then
LIVE_ROOT=$(sshd -T 2>/dev/null | grep -i "^permitrootlogin" | awk '{print $2}' || echo "未知")
[ -z "$LIVE_ROOT" ] && LIVE_ROOT="prohibit-password(默认)"
else
LIVE_ROOT="未知"
fi
# 3. 结合 OS_FAMILY 检测 SELinux 和防火墙状态
case "${OS_FAMILY}" in
ubuntu|debian)
LIVE_SELINUX="n/a (Debian系默认不启用)"
# Debian/Ubuntu 直接侦测 ufw 状态
if ufw status 2>/dev/null | grep -q "active"; then
LIVE_FW="running (ufw)"
else
LIVE_FW="stopped (ufw)"
fi
;;
rocky|almalinux|centos|rhel)
# RHEL系直接侦测 getenforce
if command -v getenforce >/dev/null 2>&1; then
LIVE_SELINUX=$(getenforce | tr 'A-Z' 'a-z')
else
LIVE_SELINUX="未安装"
fi
# RHEL系直接侦测 firewalld 状态
if systemctl is-active --quiet firewalld 2>/dev/null; then
LIVE_FW="running (firewalld)"
else
LIVE_FW="stopped (firewalld)"
fi
;;
*)
LIVE_SELINUX="未知"
LIVE_FW="未知"
;;
esac
}
# 默认值调整函数
set_parameters_interactively() {
echo -e "\n${CYAN}====== 动态配置参数修改 (直接回车则保持默认值) ======${NC}"
# 使用 || true 避免 read 触发 set -e 导致退出
read -p "请输入主 DNS 服务器 [当前: $CUSTOM_DNS_1]: " input_dns1 || true
[ -n "$input_dns1" ] && CUSTOM_DNS_1="$input_dns1"
read -p "请输入备 DNS 服务器 [当前: $CUSTOM_DNS_2]: " input_dns2 || true
[ -n "$input_dns2" ] && CUSTOM_DNS_2="$input_dns2"
read -p "请输入需安装的软件,多个软件用空格隔开 [当前: $DEFAULT_PKGS]: " input_pkgs || true
[ -n "$input_pkgs" ] && DEFAULT_PKGS="$input_pkgs"
while true; do
read -p "是否允许 Root 用户 SSH 登录?(yes/no) [当前: $PERMIT_ROOT_LOGIN]: " input_root || true
case "${input_root,,}" in
yes|no ) PERMIT_ROOT_LOGIN="${input_root,,}"; break ;;
"" ) break ;;
* ) echo "请输入 yes 或 no。" ;;
esac
done
while true; do
read -p "SELINUX 目标状态 (enforcing/disabled) [当前: $SELINUX_STATE]: " input_selinux || true
case "${input_selinux,,}" in
enforcing|disabled ) SELINUX_STATE="${input_selinux,,}"; break ;;
"" ) break ;;
* ) echo "请输入 enforcing 或 disabled。" ;;
esac
done
while true; do
read -p "防火墙目标状态 (started/stopped) [当前: $FIREWALL_STATE]: " input_fw || true
case "${input_fw,,}" in
started|stopped ) FIREWALL_STATE="${input_fw,,}"; break ;;
"" ) break ;;
* ) echo "请输入 started 或 stopped。" ;;
esac
done
log_info "默认配置更新成功!"
}
# DNS配置函数
config_dns() {
log_info "正在配置全局 DNS: ${CUSTOM_DNS_1}, ${CUSTOM_DNS_2} ..."
# 1. 备份原文件
if [ -f /etc/resolv.conf ]; then
cp /etc/resolv.conf /etc/resolv.conf.bak.$(date +%F_%T)
fi
# 2. 检测 NetworkManager 是否接管了网络
if systemctl is-active --quiet NetworkManager 2>/dev/null; then
echo -e "${YELLOW}=================================================="
echo -e "[检测到 NetworkManager 正在运行]"
echo -e " 提示:如果直接修改 /etc/resolv.conf,重启后会被 NetworkManager 覆写!"
echo -e " 将自动创建全局配置规则,阻止 NM 覆写 DNS。"
echo -e "==================================================${NC}"
sleep 2
local nm_conf="/etc/NetworkManager/conf.d/90-dns-none.conf"
mkdir -p /etc/NetworkManager/conf.d/
cat > "$nm_conf" <<EOF
[main]
dns=none
EOF
systemctl reload NetworkManager 2>/dev/null || true
log_info "NetworkManager DNS 锁定配置已应用。 (${nm_conf})"
fi
# 3. 写入新的 DNS 配置
# 解除可能存在的锁定属性
chattr -i /etc/resolv.conf 2>/dev/null || true
cat > /etc/resolv.conf <<EOF
# Generated by Linux Initialization Script
# NetworkManager DNS tracking has been disabled by script rule.
nameserver ${CUSTOM_DNS_1}
nameserver ${CUSTOM_DNS_2}
options timeout:2 attempts:3 single-request-reopen
EOF
log_info "DNS 配置覆盖成功。"
}
# 安全策略配置函数
config_sec_ssh() {
log_info "--- 开始执行安全策略配置 ---"
local sshd_conf="/etc/ssh/sshd_config"
local backup_suffix=$(date +%F_%T)
local backup_file="${sshd_conf}.bak.${backup_suffix}"
[ -f "${sshd_conf}" ] && cp -a ${sshd_conf} ${backup_file}
log_info "原配置文件备份完成。"
# 1. SSH 策略
if [ -f "${sshd_conf}" ]; then
sed -i "s/^#\?PermitRootLogin.*/PermitRootLogin ${PERMIT_ROOT_LOGIN}/g" ${sshd_conf}
sed -i 's/^#\?UseDNS.*/UseDNS no/g' ${sshd_conf}
sed -i 's/^#\?GSSAPIAuthentication.*/GSSAPIAuthentication no/g' ${sshd_conf}
fi
# 清理 sshd_config.d 目录中所有包含 PermitRootLogin 的配置,防止覆盖
if [ -d /etc/ssh/sshd_config.d ]; then
for conf in /etc/ssh/sshd_config.d/*.conf; do
if [ -f "$conf" ] && grep -q "^PermitRootLogin" "$conf"; then
sed -i '/^PermitRootLogin/d' "$conf"
log_info "已从 $conf 中移除冲突的 PermitRootLogin 配置"
fi
done
fi
# 2. 根据系统进行配置防火墙与 SELinux
case "${OS_FAMILY}" in
ubuntu|debian)
if [ "$FIREWALL_STATE" == "stopped" ]; then
ufw disable 2>/dev/null || true
log_info "ufw 防火墙服务已停止并禁用"
else
ufw --force enable 2>/dev/null || true
log_info "ufw 防火墙服务已启动并启用"
fi
;;
rocky|almalinux|centos|rhel)
# SELinux 控制
if [ "$SELINUX_STATE" == "disabled" ]; then
setenforce 0 2>/dev/null || true
[ -f /etc/selinux/config ] && sed -i 's/^SELINUX=.*/SELINUX=disabled/g' /etc/selinux/config
log_info "SELinux 目标状态已设置为: disabled"
else
setenforce 1 2>/dev/null || true
[ -f /etc/selinux/config ] && sed -i 's/^SELINUX=.*/SELINUX=enforcing/g' /etc/selinux/config
log_info "SELinux 目标状态已设置为: enforcing"
fi
# Firewalld 控制
if [ "$FIREWALL_STATE" == "stopped" ]; then
systemctl stop firewalld 2>/dev/null || true
systemctl disable firewalld 2>/dev/null || true
log_info "firewalld 防火墙服务已停止并禁用"
else
systemctl enable --now firewalld 2>/dev/null || true
log_info "firewalld 防火墙服务已启动并启用"
fi
;;
esac
if ! sshd -t; then
log_err "SSH 配置语法错误,恢复备份"
[ -f "${backup_file}" ] && mv ${backup_file} ${sshd_conf}
exit 1
fi
# 3. 重启 SSH 服务生效
if systemctl is-active --quiet sshd 2>/dev/null; then systemctl restart sshd; fi
if systemctl is-active --quiet ssh 2>/dev/null; then systemctl restart ssh; fi
log_info "安全策略与 SSH 优化配置完毕。"
}
install_env() {
log_info "正在安装 ${OS_FAMILY} 基础软件: ${DEFAULT_PKGS}..."
if [ -z "${DEFAULT_PKGS}" ]; then
log_warn "未指定任何安装包,跳过安装环节。"
return 0
fi
case "${OS_FAMILY}" in
ubuntu|debian)
export DEBIAN_FRONTEND=noninteractive
apt-get install -y ${DEFAULT_PKGS}
;;
rocky|almalinux|centos|rhel)
yum install -y epel-release 2>/dev/null || true
yum install -y ${DEFAULT_PKGS}
;;
esac
log_info "基础软件安装完成。"
}
# 主控菜单
main_menu() {
check_root
detect_os
while true; do
fetch_system_live_status
echo -e "\n${CYAN}======= Linux 多系统通用初始化脚本 =======${NC}"
echo "=================================================="
echo -e "[系统当前实际状态]"
echo "--------------------------------------------------"
echo -e " 当前操作系统: ${YELLOW}${OS_FAMILY} ${OS_VERSION}${NC}"
echo -e " 当前主 DNS: ${YELLOW}${LIVE_DNS}${NC}"
echo -e " 当前Root登录: ${YELLOW}${LIVE_ROOT}${NC}"
echo -e " 当前SELinux: ${YELLOW}${LIVE_SELINUX}${NC}"
echo -e " 当前防火墙: ${YELLOW}${LIVE_FW}${NC}"
echo "=================================================="
echo -e "[脚本默认配置]"
echo "--------------------------------------------------"
echo -e " 目标主/备DNS: ${CYAN}${CUSTOM_DNS_1} / ${CUSTOM_DNS_2}${NC}"
echo -e " 预装工具包: ${CYAN}${DEFAULT_PKGS:-[未指定,将跳过安装]}${NC}"
echo -e " 允许Root登录: ${CYAN}${PERMIT_ROOT_LOGIN}${NC}"
echo -e " SELinux目标: ${CYAN}${SELINUX_STATE}${NC}"
echo -e " 防火墙目标: ${CYAN}${FIREWALL_STATE}${NC}"
echo "=================================================="
echo " 1) [全量执行] 一键初始化"
echo " 2) [参数调整] 修改脚本的默认配置项"
echo " 3) [单项执行] 仅配置 DNS"
echo " 4) [单项执行] 仅配置安全与 SSH"
echo " 5) [单项执行] 仅安装常用软件"
echo " 0) 退出脚本"
echo "--------------------------------------------------"
read -p "请选择操作 [0-5]: " ch || true
case "$ch" in
1)
config_dns
config_sec_ssh
install_env
log_info "全量初始化完成!"
break
;;
2) set_parameters_interactively ;;
3) config_dns ;;
4) config_sec_ssh ;;
5) install_env ;;
0)
log_info "退出脚本。"
exit 0
;;
*)
log_warn "无效输入,请重新选择 [0-5]"
;;
esac
done
}
# 启动入口
main_menu
测试
经实际环境测试,脚本在以下系统上能正常运行:
Rocky-9.5、Ubuntu-24.04