背景

给一台 Linux 做初始化确实不复杂,几条命令而已。但是,当需要面对多台、多种发行版、反复重建的环境时,这几条命令里藏着的细节差异,比如说 NetworkManager 覆写 DNS、sshd_config.d 覆盖 root 登录、ufwfirewalld 命令不同,就会变成没有必要的重复劳动。这个脚本的作用是,能灵活地根据不同的发行版进行针对性的配置,使得无论在哪台机器上都能得到正确的结果。

这个脚本中没有包含软件源配置的原因是,由于镜像站可能发生变动,有时候会使用内网源、官方源或第三方源,把这些写进脚本,既要维护一堆映射表,又未必适配所有环境,显得不是很有必要。


设计

在编写这个脚本时,首先把 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 <脚本名>.shchmod +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.5Ubuntu-24.04