openEuler 24.03 LTS 服务器初始化实践:从系统配置到安全基线自动化

Openeuler 24.03 服务器初始化,基线加固脚本运行效果

背景

在企业生产环境中,新服务器交付通常需要完成大量初始化工作,包括:

  • 创建标准运维账号
  • 配置 SSH 安全策略
  • 安装运行环境
  • 挂载数据磁盘
  • 调整系统参数
  • 满足安全基线要求
  • 配置日志审计

传统手工方式容易出现步骤遗漏、配置不一致以及难以审计的问题。

因此,将服务器初始化流程脚本化,可以实现标准化、自动化交付。

本文介绍一套面向 openEuler 24.03 LTS 的服务器初始化脚本设计思路。

设计目标

脚本主要目标:

  • 支持 openEuler 24.03 LTS
  • 支持生产服务器初始化
  • 支持 Docker/Kubernetes 节点
  • 支持安全基线自动加固
  • 支持重复执行
  • 保留执行日志
  • 高风险操作人工确认

整体流程:

服务器安装完成
|
v
系统检测
|
v
基础环境初始化
|
v
磁盘与存储配置
|
v
安全基线加固
|
v
完成服务器交付

系统环境检查

脚本执行前检查:

  • root权限
  • 操作系统版本
  • 网络连通性
  • 软件仓库可用性

当前脚本针对:

openEuler 24.03 LTS

进行适配。

日志机制

脚本自动生成执行日志:

YYYYMMDD_HHMMSS_init.log

日志包含:

  • 成功信息
  • 警告信息
  • 错误信息

方便初始化失败后的问题定位。

容器环境识别

脚本自动检测:

  • Docker
  • Kubernetes

根据服务器用途调整系统参数。

普通服务器:

net.ipv4.ip_forward=0

容器环境:

net.ipv4.ip_forward=1

Kubernetes环境额外配置:

net.bridge.bridge-nf-call-iptables=1
net.bridge.bridge-nf-call-ip6tables=1

JDK环境初始化

支持检测已有Java环境:

java -version

如果不存在,可以通过dnf安装:

  • OpenJDK 8
  • OpenJDK 11
  • OpenJDK 17
  • OpenJDK 21

运维账号初始化

生产环境建议禁止长期使用root登录。

脚本支持创建:

devops

账号,并完成:

  • wheel组授权
  • SSH目录权限配置
  • 密码生命周期管理

密码策略:

参数 说明

最小使用时间 6天
最大有效期 90天
提前提醒 30天

LVM磁盘自动化管理

脚本支持:

  • 自动发现裸盘
  • 创建PV
  • 创建VG
  • 创建LV
  • XFS格式化
  • 自动写入fstab

支持新建:

/dev/sdb
|
v
VG
|
v
LV
|
v
/data

同时支持:

  • 已有LV在线扩容
  • 多磁盘条带化

扩容流程:

pvcreate
|
vgextend
|
lvextend
|
xfs_growfs

Bind Mount目录迁移

针对日志等增长目录:

例如:

/var/log

迁移到数据盘:

/data/var_log

通过:

mount --bind

实现目录映射,并写入fstab保证重启生效。

SSH安全加固

配置:

AllowAgentForwarding no
GatewayPorts no
PermitTunnel no
PermitEmptyPasswords no
MaxAuthTries 5
X11Forwarding no

如果存在普通运维账号:

PermitRootLogin no

禁止root远程登录。

密码安全策略

密码复杂度:

minlen=12
minclass=4

ucredit=-1
lcredit=-1
dcredit=-1
ocredit=-1

密码历史:

remember=5

失败锁定:

deny=3
unlock_time=1800

系统网络安全参数

配置:

accept_redirects=0
send_redirects=0
accept_source_route=0
tcp_syncookies=1

提升Linux网络安全能力。

日志与审计

支持:

  • rsyslog配置
  • 远程日志转发
  • logrotate配置
  • audit审计规则

审计关键文件:

/etc/passwd
/etc/shadow
/etc/group
/etc/firewalld

临时目录安全

增强:

/tmp

安全属性:

nosuid
nodev
noexec

降低恶意程序执行风险。

文件权限加固

调整关键目录和文件权限:

例如:

/etc/passwd
/etc/group
/etc/services

以及系统启动配置文件。

脚本设计特点

幂等性

重复执行不会:

  • 重复创建用户
  • 重复追加配置
  • 覆盖已有数据

风险确认

涉及:

  • 删除数据
  • 清理目录
  • 关闭服务

均需要人工确认。

自动备份

修改关键配置前保留备份:

/etc/fstab
/etc/pam.d
/etc/ssh

总结

服务器初始化不仅是执行几条命令,而是一套标准化交付流程。

通过自动化初始化脚本,可以实现:

自动化交付
+
安全基线
+
资源配置
+
可审计

适用于:

  • 企业生产服务器
  • 云主机初始化
  • Kubernetes节点
  • 等保环境建设

后续可以继续扩展:

  • Ansible批量部署
  • Terraform自动创建
  • CMDB自动登记
  • 基线检查报告生成

形成完整的服务器自动交付体系。

#!/bin/bash

# set -x # 开启调试输出

#================================================================
# 用于 openEuler 24.03 LTS 操作系统的初始化脚本
# 包含系统devops用户创建,banner设置,jdk安装,环境变量设置,数据
# 磁盘挂载,基线配置等:
#================================================================

# 导出 LANG 变量,确保命令返回英文响应,避免结果解析错误导致执行失败
export LANG=en_US.UTF-8

# ************ 函数定义区*****************
# 设置脚本执行的临时目录

# 设置颜色变量
GREEN='\033[0;32m'
ORANGE='\033[38;5;208m'
RED='\033[0;31m'
NC='\033[0m'

# 根据时间设置日志文件, 默认存放在当前目录下
# 日志文件名格式: YYYYMMDD_HHMMSS_init.log
LOG_FILE="$(date +%Y%m%d_%H%M%S)_init.log"

# 输出成功信息
ok() {
    local message="$1"
    # 输出到控制台和日志文件
    echo -e "${GREEN}[OK]${NC} $message" | tee -a "$LOG_FILE"
}

# 输出失败信息并退出脚本
exit_error() {
    local message="$1"
    # 输出到控制台和日志文件
    echo -e "${RED}[Error]${NC} $message" | tee -a "$LOG_FILE"
    echo -e "退出执行.." | tee -a "$LOG_FILE"
    # 退出脚本并返回错误码 1
    exit 1
}

# 输出警告信息
warning() {
    local message="$1"
    # 输出到控制台和日志文件
    echo -e "${ORANGE}[Warning]${NC} $message" | tee -a "$LOG_FILE"
}

# 输出信息
msg() {
    local message="$1"
    # 输出到控制台和日志文件
    echo -e "$message" | tee -a "$LOG_FILE"
}

# 检查挂载点是否由 LVM 卷支撑,返回 "VG名 LV设备路径" 或空
check_existing_lvm() {
    local mp="$1"
    local source
    source=$(findmnt -n -o SOURCE "$mp" 2>/dev/null)
    if [ -n "$source" ] && lvs "$source" &>/dev/null; then
        local vgname
        vgname=$(lvs --noheadings -o vg_name "$source" 2>/dev/null | tr -d ' ')
        echo "$vgname $source"
    fi
}

# 将挂载点路径转换为合法的 VG/LV 名称后缀,如 /opt -> opt, /data/logs -> data_logs
sanitize_mount_path() {
    echo "$1" | sed 's|^/||;s|/|_|g' | sed 's|_$||'
}

# 根据磁盘名在发现阶段数组中查找大小
get_disk_size() {
    local target="$1"
    for i in "${!RAW_DISK_NAMES[@]}"; do
        if [ "${RAW_DISK_NAMES[$i]}" == "$target" ]; then
            echo "${RAW_DISK_SIZES[$i]}"
            return
        fi
    done
}

# 修改或追加 conf 文件中的参数(支持 key = value 和布尔标志)
modify_or_append_conf() {
    local file="$1" key="$2" value="$3"
    [ -f "$file" ] || touch "$file"
    if [ -z "$value" ]; then
        # 布尔标志(如 even_deny_root)
        grep -Eq "^[[:space:]]*${key}[[:space:]]*$" "$file" || echo "$key" >> "$file"
    elif grep -Eq "^[[:space:]]*${key}[[:space:]]*=" "$file"; then
        sed -i -E "s|^[[:space:]]*${key}[[:space:]]*=.*|${key} = ${value}|" "$file"
    elif grep -Eq "^[[:space:]]*#[[:space:]]*${key}[[:space:]]*=" "$file"; then
        sed -i -E "s|^[[:space:]]*#[[:space:]]*${key}[[:space:]]*=.*|${key} = ${value}|" "$file"
    else
        echo "${key} = ${value}" >> "$file"
    fi
}

append_line_if_missing() {
    local line="$1"
    local file="$2"
    grep -Fqx "$line" "$file" 2>/dev/null || echo "$line" >> "$file"
}

prepend_line_if_missing() {
    local line="$1"
    local file="$2"
    local temp_file

    [ -f "$file" ] || touch "$file"
    grep -Fqx "$line" "$file" 2>/dev/null && return 0

    temp_file=$(mktemp)
    printf '%s\n' "$line" > "$temp_file"
    cat "$file" >> "$temp_file"
    cat "$temp_file" > "$file"
    rm -f "$temp_file"
}

prepend_pam_rule_if_missing() {
    local file="$1"
    local rule="$2"
    local pattern="$3"

    [ -f "$file" ] || touch "$file"
    grep -Eq "$pattern" "$file" 2>/dev/null && return 0
    prepend_line_if_missing "$rule" "$file"
}

set_login_defs_value() {
    local file="$1"
    local key="$2"
    local value="$3"
    local temp_file

    [ -f "$file" ] || touch "$file"
    temp_file=$(mktemp)
    if awk -v key="$key" -v value="$value" '
        /^[[:space:]]*#/ {
            print
            next
        }
        $1 == key {
            if (!updated) {
                printf "%s   %s\n", key, value
                updated = 1
            }
            next
        }
        {
            print
        }
        END {
            if (!updated) {
                printf "%s   %s\n", key, value
            }
        }
    ' "$file" > "$temp_file"; then
        cat "$temp_file" > "$file"
    else
        rm -f "$temp_file"
        return 1
    fi
    rm -f "$temp_file"
}

set_shell_var() {
    local file="$1"
    local key="$2"
    local value="$3"

    [ -f "$file" ] || touch "$file"
    if grep -Eq "^[[:space:]]*#?[[:space:]]*${key}[[:space:]]*=" "$file"; then
        sed -i -E "s|^[[:space:]]*#?[[:space:]]*${key}[[:space:]]*=.*|${key}=${value}|" "$file"
    else
        echo "${key}=${value}" >> "$file"
    fi
}

set_sshd_option() {
    local file="$1"
    local key="$2"
    local value="$3"
    local temp_file

    [ -f "$file" ] || touch "$file"
    temp_file=$(mktemp)
    if awk -v key="$key" -v value="$value" '
        {
            probe = $0
            if (probe ~ /^[[:space:]]*#/) {
                sub(/^[[:space:]]*#[[:space:]]*/, "", probe)
            }
            sub(/^[[:space:]]*/, "", probe)
            split(probe, fields, /[[:space:]]+/)
            if (tolower(fields[1]) == tolower(key)) {
                if (!updated) {
                    printf "%s %s\n", key, value
                    updated = 1
                }
                next
            }
            print
        }
        END {
            if (!updated) {
                printf "%s %s\n", key, value
            }
        }
    ' "$file" > "$temp_file"; then
        cat "$temp_file" > "$file"
    else
        rm -f "$temp_file"
        return 1
    fi
    rm -f "$temp_file"
}

set_space_kv() {
    local file="$1"
    local key="$2"
    local value="$3"
    local temp_file

    [ -f "$file" ] || touch "$file"
    temp_file=$(mktemp)
    if awk -v key="$key" -v value="$value" '
        /^[[:space:]]*#/ {
            print
            next
        }
        $1 == key {
            if (!updated) {
                printf "%s %s\n", key, value
                updated = 1
            }
            next
        }
        {
            print
        }
        END {
            if (!updated) {
                printf "%s %s\n", key, value
            }
        }
    ' "$file" > "$temp_file"; then
        cat "$temp_file" > "$file"
    else
        rm -f "$temp_file"
        return 1
    fi
    rm -f "$temp_file"
}

ensure_logrotate_compress() {
    local file="$1"
    local temp_file

    [ -f "$file" ] || touch "$file"
    temp_file=$(mktemp)
    if awk '
        /^[[:space:]]*nocompress([[:space:]]|$)/ {
            next
        }
        /^[[:space:]]*#?[[:space:]]*compress([[:space:]]|$)/ {
            if (!updated) {
                print "compress"
                updated = 1
            }
            next
        }
        {
            print
        }
        END {
            if (!updated) {
                print "compress"
            }
        }
    ' "$file" > "$temp_file"; then
        cat "$temp_file" > "$file"
    else
        rm -f "$temp_file"
        return 1
    fi
    rm -f "$temp_file"
}

ensure_rsyslog_rule() {
    local selector="$1"
    local action="$2"
    local file="/etc/rsyslog.d/00-baseline-local.conf"
    local rsyslog_files=()
    local conf

    mkdir -p /etc/rsyslog.d
    [ -f "$file" ] || touch "$file"
    [ -f /etc/rsyslog.conf ] && rsyslog_files+=("/etc/rsyslog.conf")
    for conf in /etc/rsyslog.d/*.conf; do
        [ -e "$conf" ] && rsyslog_files+=("$conf")
    done
    if awk -v selector="$selector" -v action="$action" '
        /^[[:space:]]*#/ {
            next
        }
        $1 == selector {
            current_action = $2
            expected_action = action
            sub(/^-/, "", current_action)
            sub(/^-/, "", expected_action)
            if (current_action == expected_action) {
                found = 1
            }
        }
        END {
            exit !found
        }
    ' "${rsyslog_files[@]}" 2>/dev/null; then
        return 0
    fi
    printf "%-48s %s\n" "$selector" "$action" >> "$file"
}

systemd_unit_exists() {
    local unit="$1"

    systemctl list-unit-files "$unit" 2>/dev/null | awk -v unit="$unit" '$1 == unit { found = 1 } END { exit !found }' \
        || systemctl status "$unit" &>/dev/null
}

disable_service_if_present() {
    local unit="$1"
    local item="$2"

    if systemd_unit_exists "$unit"; then
        systemctl disable --now "$unit" &>/dev/null || true
        if systemctl is-active --quiet "$unit" || systemctl is-enabled "$unit" &>/dev/null; then
            baseline_warn "$item" "${unit} 存在但禁用失败, 请手动检查"
        else
            baseline_ok "$item" "${unit} 已停止并禁用"
        fi
    else
        baseline_ok "$item" "未检测到 ${unit}, 符合基线要求"
    fi
}

enable_audit_kernel_arg() {
    if command -v grubby &>/dev/null; then
        grubby --update-kernel=ALL --remove-args="audit=0" --args="audit=1"
        return $?
    fi
    return 1
}

ensure_fstab_mount_options() {
    local mount_point="$1"
    local required_options="$2"
    local default_source="$3"
    local default_fstype="$4"
    local default_options="$5"
    local default_dump="$6"
    local default_pass="$7"
    local temp_file

    [ -f /etc/fstab ] || touch /etc/fstab
    temp_file=$(mktemp)
    if awk -v mount_point="$mount_point" \
        -v required_options="$required_options" \
        -v default_source="$default_source" \
        -v default_fstype="$default_fstype" \
        -v default_options="$default_options" \
        -v default_dump="$default_dump" \
        -v default_pass="$default_pass" '
        function has_option(option_list, option, parts, count, i) {
            count = split(option_list, parts, ",")
            for (i = 1; i <= count; i++) {
                if (parts[i] == option) {
                    return 1
                }
            }
            return 0
        }
        function add_missing_options(option_list, required_list, req_parts, req_count, i) {
            if (option_list == "" || option_list == "-") {
                option_list = "defaults"
            }
            req_count = split(required_list, req_parts, ",")
            for (i = 1; i <= req_count; i++) {
                if (req_parts[i] != "" && !has_option(option_list, req_parts[i])) {
                    option_list = option_list "," req_parts[i]
                }
            }
            return option_list
        }
        /^[[:space:]]*#/ || NF < 2 {
            print
            next
        }
        $2 == mount_point {
            if (!updated) {
                source = $1
                fstype = $3
                options = add_missing_options($4, required_options)
                dump_value = (NF >= 5 ? $5 : default_dump)
                pass_value = (NF >= 6 ? $6 : default_pass)
                printf "%s    %s    %s    %s    %s %s\n", source, mount_point, fstype, options, dump_value, pass_value
                updated = 1
            }
            next
        }
        {
            print
        }
        END {
            if (!updated) {
                options = add_missing_options(default_options, required_options)
                printf "%s    %s    %s    %s    %s %s\n", default_source, mount_point, default_fstype, options, default_dump, default_pass
            }
        }
    ' /etc/fstab > "$temp_file"; then
        cat "$temp_file" > /etc/fstab
    else
        rm -f "$temp_file"
        return 1
    fi
    rm -f "$temp_file"
}

fstab_has_non_bind_tmp_entry() {
    awk '
        /^[[:space:]]*#/ || NF < 4 {
            next
        }
        $2 == "/tmp" {
            bind_found = 0
            option_count = split($4, options, ",")
            for (i = 1; i <= option_count; i++) {
                if (options[i] == "bind") {
                    bind_found = 1
                }
            }
            if ($1 != "/tmp" || $3 != "none" || !bind_found) {
                found = 1
            }
        }
        END {
            exit !found
        }
    ' /etc/fstab
}

fstab_has_tmp_bind_entry() {
    awk '
        /^[[:space:]]*#/ || NF < 4 {
            next
        }
        $2 == "/tmp" {
            option_count = split($4, options, ",")
            for (i = 1; i <= option_count; i++) {
                if (options[i] == "bind") {
                    found = 1
                }
            }
        }
        END {
            exit !found
        }
    ' /etc/fstab
}

mount_options_include() {
    local mount_point="$1"
    local required_options="$2"
    local current_options
    local option
    local option_list

    current_options=",${3:-$(findmnt -n -o OPTIONS --target "$mount_point" 2>/dev/null)},"
    IFS=',' read -ra option_list <<< "$required_options"
    for option in "${option_list[@]}"; do
        if [[ "$current_options" != *",${option},"* ]]; then
            return 1
        fi
    done
    return 0
}

harden_tmp_mount() {
    local tmp_target
    local tmp_source
    local tmp_fstype
    local tmp_fstab_options
    local tmp_pass
    local harden_tmp_bind
    local remount_options
    local tmpfs_size

    mkdir -p /tmp
    chmod 1777 /tmp

    tmp_target=$(findmnt -n -o TARGET --target /tmp 2>/dev/null)
    tmp_source=$(findmnt -n -o SOURCE --target /tmp 2>/dev/null)
    tmp_fstype=$(findmnt -n -o FSTYPE --target /tmp 2>/dev/null)

    if [ "$tmp_target" == "/tmp" ]; then
        tmp_fstab_options="defaults"
        tmp_pass="2"
        if [ "$tmp_fstype" == "tmpfs" ]; then
            tmp_source="tmpfs"
            read -p "/tmp 当前为 tmpfs, 请输入大小限制 (例如 1G, 2048M, 回车默认1G): " tmpfs_size
            tmpfs_size=${tmpfs_size:-1G}
            if [[ ! "$tmpfs_size" =~ ^[0-9]+[KkMmGgTt]?$ ]]; then
                baseline_warn "/tmp 权限控制" "tmpfs 大小 ${tmpfs_size} 格式不合法, 使用默认 1G"
                tmpfs_size="1G"
            fi
            tmp_fstab_options="defaults,mode=1777,size=${tmpfs_size}"
            tmp_pass="0"
        fi

        ensure_fstab_mount_options "/tmp" "nosuid,nodev,noexec" "$tmp_source" "$tmp_fstype" "$tmp_fstab_options" "0" "$tmp_pass" \
            || { baseline_warn "/tmp 权限控制" "写入 /etc/fstab 失败, 请手动检查"; return 0; }

        remount_options="remount,nosuid,nodev,noexec"
        if fstab_has_tmp_bind_entry; then
            remount_options="remount,bind,nosuid,nodev,noexec"
        fi
        if mount -o "$remount_options" /tmp; then
            chmod 1777 /tmp
            if mount_options_include "/tmp" "nosuid,nodev,noexec"; then
                baseline_ok "/tmp 权限控制" "/tmp 已补充 nosuid,nodev,noexec 并 remount 生效"
            else
                baseline_warn "/tmp 权限控制" "/tmp 已 remount, 但未验证到完整 nosuid,nodev,noexec 选项, 请手动检查"
            fi
        else
            baseline_warn "/tmp 权限控制" "/etc/fstab 已补充 /tmp 安全选项, 但 remount 失败, 请手动执行 mount -o remount /tmp"
        fi
        return 0
    fi

    read -p "/tmp 当前不是单独挂载点, 是否使用自绑定方式加固 /tmp? (输入 y/Y 或 n/N, 回车默认Y): " harden_tmp_bind
    harden_tmp_bind=${harden_tmp_bind:-Y}
    if [ "$harden_tmp_bind" != "Y" ] && [ "$harden_tmp_bind" != "y" ]; then
        baseline_skip "/tmp 权限控制" "用户选择不配置自绑定, 基线检查可能继续不通过"
        return 0
    fi

    if fstab_has_non_bind_tmp_entry; then
        baseline_warn "/tmp 权限控制" "/etc/fstab 已存在非自绑定 /tmp 记录, 未自动覆盖; 请手动检查后再配置"
        return 0
    fi

    ensure_fstab_mount_options "/tmp" "bind,nosuid,nodev,noexec" "/tmp" "none" "bind,nosuid,nodev,noexec" "0" "0" \
        || { baseline_warn "/tmp 权限控制" "写入 /etc/fstab 自绑定记录失败, 请手动检查"; return 0; }

    if ! findmnt -n -o TARGET --target /tmp 2>/dev/null | grep -Fxq "/tmp"; then
        mount --bind /tmp /tmp || { baseline_warn "/tmp 权限控制" "/tmp 自绑定 mount 失败, 请手动检查"; return 0; }
    fi

    if mount -o remount,bind,nosuid,nodev,noexec /tmp; then
        chmod 1777 /tmp
        if mount_options_include "/tmp" "nosuid,nodev,noexec"; then
            baseline_ok "/tmp 权限控制" "/tmp 已通过自绑定方式加固 nosuid,nodev,noexec"
        else
            baseline_warn "/tmp 权限控制" "/tmp 自绑定已执行, 但未验证到完整 nosuid,nodev,noexec 选项, 请手动检查"
        fi
    else
        baseline_warn "/tmp 权限控制" "/etc/fstab 已写入 /tmp 自绑定记录, 但 remount 安全选项失败, 请手动检查"
    fi
}

ensure_limits_rule() {
    local file="$1"
    local domain="$2"
    local limit_type="$3"
    local item="$4"
    local value="$5"
    local temp_file

    [ -f "$file" ] || touch "$file"
    temp_file=$(mktemp)
    if awk -v domain="$domain" -v limit_type="$limit_type" -v item="$item" -v value="$value" '
        /^[[:space:]]*#/ || NF < 4 {
            print
            next
        }
        $1 == domain && $2 == limit_type && $3 == item {
            if (!updated) {
                printf "%-14s %-7s %-16s %s\n", domain, limit_type, item, value
                updated = 1
            }
            next
        }
        {
            print
        }
        END {
            if (!updated) {
                printf "%-14s %-7s %-16s %s\n", domain, limit_type, item, value
            }
        }
    ' "$file" > "$temp_file"; then
        cat "$temp_file" > "$file"
    else
        rm -f "$temp_file"
        return 1
    fi
    rm -f "$temp_file"
}

ensure_user_in_group() {
    local user="$1"
    local group="$2"

    id "$user" &>/dev/null || exit_error "用户 ${user} 不存在, 无法加入 ${group} 组"
    getent group "$group" &>/dev/null || groupadd "$group" || exit_error "创建 ${group} 组失败"

    if id -nG "$user" | tr ' ' '\n' | grep -Fxq "$group"; then
        ok "用户 ${user} 已在 ${group} 组."
    else
        usermod -a -G "$group" "$user" || exit_error "添加用户 ${user}${group} 组失败"
        ok "用户 ${user} 已添加到 ${group} 组."
    fi
}

ensure_user_password_policy() {
    local user="$1"

    id "$user" &>/dev/null || exit_error "用户 ${user} 不存在, 无法设置口令有效期策略"
    chage -m 6 -M 90 -W 30 "$user" || exit_error "设置用户 ${user} 口令有效期策略失败"
    ok "用户 ${user} 口令策略已设置 (min=6, max=90, warn=30)."
}

ensure_user_home_permissions() {
    local user="$1"
    local harden_home="${2:-n}"
    local home_dir

    id "$user" &>/dev/null || exit_error "用户 ${user} 不存在, 无法收敛用户目录权限"
    home_dir=$(getent passwd "$user" | awk -F: '{print $6}')
    [ -n "$home_dir" ] || exit_error "无法获取用户 ${user} 的 home 目录"
    if [ "$home_dir" == "/" ]; then
        exit_error "用户 ${user} 的 home 目录为 /, 拒绝修改权限"
    fi
    if [ ! -d "$home_dir" ]; then
        if [ "$harden_home" == "Y" ] || [ "$harden_home" == "y" ]; then
            mkdir -p "$home_dir"
        else
            exit_error "用户 ${user} 的 home 目录 ${home_dir} 不存在"
        fi
    fi

    if [ "$harden_home" == "Y" ] || [ "$harden_home" == "y" ]; then
        chown "$user":"$user" "$home_dir" || exit_error "设置 ${home_dir} 属主失败"
        chmod 700 "$home_dir" || exit_error "设置 ${home_dir} 权限失败"
    fi

    mkdir -p "${home_dir}/.ssh"
    chown "$user":"$user" "${home_dir}/.ssh" || exit_error "设置 ${home_dir}/.ssh 属主失败"
    chmod 700 "${home_dir}/.ssh" || exit_error "设置 ${home_dir}/.ssh 权限失败"

    if [ -f "${home_dir}/.ssh/authorized_keys" ]; then
        chown "$user":"$user" "${home_dir}/.ssh/authorized_keys" || exit_error "设置 authorized_keys 属主失败"
        chmod 600 "${home_dir}/.ssh/authorized_keys" || exit_error "设置 authorized_keys 权限失败"
    fi

    touch "${home_dir}/.bashrc"
    chown "$user":"$user" "${home_dir}/.bashrc" || exit_error "设置 ${home_dir}/.bashrc 属主失败"
    chmod 644 "${home_dir}/.bashrc" || exit_error "设置 ${home_dir}/.bashrc 权限失败"
}

baseline_info() {
    local item="$1"
    local message="$2"
    msg "[INFO] [安全基线][${item}] ${message}"
}

baseline_ok() {
    local item="$1"
    local message="$2"
    ok "[安全基线][${item}] ${message}"
}

baseline_warn() {
    local item="$1"
    local message="$2"
    warning "[安全基线][${item}] ${message}"
}

baseline_skip() {
    local item="$1"
    local message="$2"
    warning "[安全基线][${item}] 跳过: ${message}"
}

set_umask_in_file() {
    local file="$1"
    local value="$2"

    [ -f "$file" ] || touch "$file"
    if grep -Eq "^[[:space:]]*umask[[:space:]]+[0-9]+" "$file"; then
        sed -i -E "s|^[[:space:]]*umask[[:space:]]+[0-9]+|umask ${value}|" "$file"
    else
        echo "umask ${value}" >> "$file"
    fi
}

is_valid_ipv4() {
    local ip="$1"
    local octet
    local IFS='.'
    local parts=()

    [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || return 1
    read -r -a parts <<< "$ip"
    for octet in "${parts[@]}"; do
        if (( octet < 0 || octet > 255 )); then
            return 1
        fi
    done
    return 0
}

is_valid_hostname() {
    local hostname="$1"
    local label
    local IFS='.'
    local labels=()

    [ "${#hostname}" -le 253 ] || return 1
    [[ "$hostname" == *.* ]] || return 1
    [[ "$hostname" =~ ^[A-Za-z0-9.-]+$ ]] || return 1
    [[ "$hostname" != .* && "$hostname" != *. ]] || return 1
    [[ "$hostname" != *..* ]] || return 1

    read -r -a labels <<< "$hostname"
    for label in "${labels[@]}"; do
        [ -n "$label" ] || return 1
        [ "${#label}" -le 63 ] || return 1
        [[ "$label" =~ ^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?$ ]] || return 1
    done
    return 0
}

is_valid_ntp_server() {
    local server="$1"
    is_valid_ipv4 "$server" || is_valid_hostname "$server"
}

parse_ntp_servers() {
    local input="$1"
    local server
    local normalized

    NTP_SERVERS=()
    normalized="${input//,/ }"

    for server in $normalized; do
        if ! is_valid_ntp_server "$server"; then
            return 1
        fi
        NTP_SERVERS+=("$server")
    done

    [ "${#NTP_SERVERS[@]}" -gt 0 ]
}

select_time_sync_service() {
    if systemctl is-enabled chronyd &>/dev/null || systemctl is-active --quiet chronyd; then
        echo "chronyd"
        return 0
    fi
    if systemctl is-enabled ntpd &>/dev/null || systemctl is-active --quiet ntpd; then
        echo "ntpd"
        return 0
    fi
    if command -v chronyd &>/dev/null || rpm -q chrony &>/dev/null || [ -f /etc/chrony.conf ]; then
        echo "chronyd"
        return 0
    fi
    if command -v ntpd &>/dev/null || rpm -q ntp &>/dev/null || [ -f /etc/ntp.conf ]; then
        echo "ntpd"
        return 0
    fi
    if dnf install -y chrony &>/dev/null; then
        echo "chronyd"
        return 0
    fi
    if dnf install -y ntp &>/dev/null; then
        echo "ntpd"
        return 0
    fi
    return 1
}

backup_file_no_overwrite() {
    local file="$1"
    local backup_path

    if [ ! -e "$file" ]; then
        return 0
    fi

    backup_path="${file}.bak"
    if [ -e "$backup_path" ]; then
        backup_path="${file}.bak.$(date +%Y%m%d_%H%M%S)_$RANDOM"
        while [ -e "$backup_path" ]; do
            backup_path="${file}.bak.$(date +%Y%m%d_%H%M%S)_$RANDOM"
        done
    fi

    mv "$file" "$backup_path"
}

is_bind_mount_active() {
    local source="$1"
    local target="$2"
    local mounted_source

    mounted_source=$(findmnt -n -o SOURCE --target "$target" 2>/dev/null)
    [ "$mounted_source" = "$source" ]
}

# 动态符号函数
show_spinner() {
    local pid=$1
    local message="$2"
    local delay=0.2
    local spinstr='|/-\'
    echo -n "$message"
    while [ -d "/proc/$pid" ]; do
        local temp=${spinstr#?}
        printf " [%c]  " "$spinstr"
        spinstr=$temp${spinstr%"$temp"}
        sleep $delay
        printf "\b\b\b\b\b\b"
    done
    printf "    \b\b\b\b" # 清除动态符号
    echo ""
}


# *************脚本开始执行*****************

# 检查是否使用root用户执行此脚本 -----------------
if [ "$EUID" -ne 0 ]; then
    exit_error "请使用 root 用户运行此脚本"
fi

# 检查标准输入是否连接到终端,脚本包含多处交互确认 -----------------
if [ ! -t 0 ]; then
    exit_error "请在交互式终端中运行此脚本"
fi

# 输出当前时间和用户信息  -----------------
msg "--------------------------------"
msg "- 初始化脚本启动..."
msg "- 当前时间: $(date +'%Y-%m-%d %H:%M:%S')"
msg "- 当前用户: $(whoami)"
msg "--------------------------------\n"

# 获取系统版本,并检查是否为 openEuler 24.03 LTS Start -----------------
VERSION=$(grep "^VERSION_ID=" /etc/os-release | cut -d '"' -f2)
DISTRO=$(grep "^ID=" /etc/os-release | cut -d '"' -f2)

if [[ -z "$VERSION" || -z "$DISTRO" ]]; then
    warning "未检测到操作系统版本信息"
    exit 1
fi

if [[ "$DISTRO" != "openEuler" || "$VERSION" != "24.03" ]]; then
    warning "当前操作系统不是 openEuler 24.03 LTS"
    exit 1
fi

ok "检测到符合要求的操作系统版本: $DISTRO $VERSION"

# 让用户确认是否开始初始化流程
# 输出说明:Do you confirm to begin the initialization process for this server? (type Y to continue).  中文:您确认要开始此服务器的初始化吗?(输入 Y 回车 继续)默认(N)
read -p "您确认要开始此服务器的初始化吗?输入 y/Y 回车继续: " confirm

if [ "$confirm" != "Y" ] && [ "$confirm" != "y" ]; then
        warning "放弃初始化进程执行."
        exit 0
fi

RUN_SCOPE="full"
read -p "请选择执行范围: [1] 全部初始化  [2] 仅执行安全基线 (输入 1/2, 回车默认1): " run_scope_choice
case "$run_scope_choice" in
    2)
        RUN_SCOPE="baseline"
        ok "执行范围: 仅执行安全基线"
        ;;
    *)
        RUN_SCOPE="full"
        ok "执行范围: 全部初始化"
        ;;
esac

if [ "$RUN_SCOPE" == "full" ]; then

# 检查网络连接是否可用 -----------------
if [[ -f /etc/resolv.conf ]]; then
        (
        curl -s --head https://www.neusoft.com > /dev/null
        ) &
        spinner_pid=$!
        show_spinner $spinner_pid  "测试网络连通性 curl -s --head https://www.neusoft.com"
        wait $spinner_pid
        curl_exit=$?
        if [[ $curl_exit -ne 0 ]]; then
            warning "当前外网网络不可用,如果需联网,请检查 DNS 配置和网络连接或申请网络策略,对于需要访问外网的服务器需要一并申请允许访问DNS服务器的策略"
        else
            ok "curl 测试网络连通性成功"
        fi
else
    warning "未找到 DNS 配置文件, /etc/resolv.conf 不存在, 请检查"
fi

# 检查当前包管理仓库源是否可用 -----------------
# 检查软件包仓库是否可用
dnf makecache -y
if [ $? -ne 0 ]; then
    warning "无法连接到软件包仓库"
    read -p "当前包管理仓库源不可用,无法自动安装JDK或lvm组件,是否继续执行脚本?(输入 y/Y 回车继续): " CONTINUE
    if [[ "$CONTINUE" != "y" && "$CONTINUE" != "Y" ]]; then
        exit 1
    fi
fi
# 获取系统版本,并检查是否为 openEuler 24.03 LTS End ------------------

else
    msg "仅执行安全基线模式: 跳过网络连通性和软件仓库预检查."
fi

# 检测服务器用途:是否用于容器环境 (Docker/K8S) -----------------
CONTAINER_ENV="none"

# 自动检测已安装的容器环境
if command -v kubectl &>/dev/null || systemctl is-active --quiet kubelet 2>/dev/null; then
    msg "检测到已安装 Kubernetes 组件."
    CONTAINER_ENV="k8s"
elif command -v docker &>/dev/null || systemctl is-active --quiet docker 2>/dev/null; then
    msg "检测到已安装 Docker."
    CONTAINER_ENV="docker"
fi

# 用户确认或手动选择
read -p "此服务器是否用于容器环境? [1] Docker [2] K8S [3] 非容器环境 (输入 1/2/3, 回车继续, 默认3): " container_choice
case "$container_choice" in
    1) CONTAINER_ENV="docker" ;;
    2) CONTAINER_ENV="k8s" ;;
    3) CONTAINER_ENV="none" ;;
    *)
        if [ "$CONTAINER_ENV" == "none" ]; then
            msg "未选择容器环境, 按非容器环境初始化."
        else
            msg "保持自动检测结果: $CONTAINER_ENV"
        fi
        ;;
esac

if [ "$CONTAINER_ENV" != "none" ]; then
    ok "服务器用途: 容器环境 ($CONTAINER_ENV)"
    msg "以下操作将根据容器环境自动调整:"
    msg "  - ip_forward 将设置为 1 (容器网络必需)"
    if [ "$CONTAINER_ENV" == "k8s" ]; then
        msg "  - K8S环境将配置 bridge-nf-call-iptables 参数"
    fi
else
    ok "服务器用途: 非容器环境"
fi

if [ "$RUN_SCOPE" == "baseline" ]; then
    msg "仅执行安全基线模式: 跳过 JRE、用户初始化、ulimit、Banner、SELinux、swap、lvm 与磁盘挂载流程."
else

# 检查是否已安装JRE
msg "检查是否安装JRE."
if command -v java >/dev/null 2>&1; then
# 检查到JDK/JRE已经安装
    # 输出说明:JRE or JDK exists already. 中文:JRE或JDK已存在。
    warning "JRE已经安装."

    # 获取当前已安装的 Java 版本号
    current_version=$(java -version 2>&1 | head -n 3)
    # 获取当前已安装的 Java 主版本号
    current_major_version=$(java -version 2>&1 | head -n 1 | awk -F'"' '{print $2}')

    # 处理 1.x 版本和 9 及以上版本的主版本号
    if [[ "$current_major_version" =~ ^1\.[0-9]+ ]]; then
        # 处理 1.x 版本,例如 1.8
        current_major_version=$(echo "$current_major_version" | cut -d'.' -f1-2)
    else
        # 处理 9 及以上版本,例如 9, 11
        current_major_version=$(echo "$current_major_version" | cut -d'.' -f1)
    fi

    # 输出当前的 Java 版本
    # 输出说明:Currently installed Java version 中文:当前已安装Java版本
    echo -e "${GREEN}当前安装JRE版本: $current_version ${NC}"
    warning "您可以手动安装其他版本, 通过 alternatives 命令切换默认生效版本."
else
   # CONTINUE =="Y" || CONTINUE == "y" 代表无法连接包仓库源,用户选择继续执行脚本,所以这里提示用户无法安装JRE
   if [ "$CONTINUE" == "Y" ] || [ "$CONTINUE" == "y" ]; then
        warning "当前无法连接包仓库源,无法安装JRE."
   else
       
        # 检查到JDK/JRE未安装
        warning "JRE或JDK未安装."
        # 已知的 JDK 版本列表
        valid_java_versions=("1.8" "9" "10" "11" "12" "13" "14" "15" "16" "17" "18" "19" "20" "21")

        # 输出说明:Do you want to install openjdk automatically(type y/Y or n/N, return to continue) 中文:您是否要自动安装 openjdk(输入 Y 或 n/N,回车继续)
        read -p "是否需要自动安装 openjdk(JRE only) 吗? (输入 y/Y 或 n/N, 回车继续): " install_java
        if [ "$install_java" != "Y" ] && [ "$install_java" != "y" ]
            then
                # 输出说明: JRE will not be installed in this process. 中文: 初始化流程不进行JRE自动安装
                warning "JRE将不会在此流程中自动安装."
            else
                # 输入版本号最大错误次数
                max_attempts=3
                # 已输入版本号次数
                attempts=0

                # 用户输入目标版本
                while (( attempts < max_attempts )); do
                    read -p "从列表中选择一个需要安装的Java版本 [${valid_java_versions[*]}]: " target_java_version

                    # 验证输入的版本是否有效
                    if [[ " ${valid_java_versions[@]} " =~ " ${target_java_version} " ]]; then
                        warning "选择的版本有效: $target_java_version"
                        break
                    else
                        echo "错误的Java版本: $target_java_version"
                        ((attempts++))
                        echo "剩余尝试次数: $((max_attempts - attempts))"
                    fi
                done

                # 如果超过最大尝试次数,退出脚本
                if (( attempts == max_attempts )); then
                    warning "超过最大尝试次数,初始化流程结束."
                    exit 1
                fi

                # 使用 dnf 安装对应版本的 JDK
                echo "Starting installation of JDK $target_java_version..."

                # 拼接版本号用于安装命令, 如果需要安装jdk以下package_name分别增加 java-1.8.0-openjdk-devel java-${target_java_version}-openjdk-devel
                if [[ "$target_java_version" == "1.8" ]]; then
                    package_name="java-1.8.0-openjdk"
                else
                    package_name="java-${target_java_version}-openjdk"
                fi

                # 安装命令
                dnf -y install $package_name

                # 检查安装是否成功
                if [[ $? -eq 0 ]]; then
                    echo -e "${GREEN}JRE $target_java_version 安装成功.${NC}"
                else
                    exit_error "安装 JDK $target_java_version.失败"
                fi
        fi
   fi
fi

# 确认是已否存在非 root 用户
read -p "是否已有非 root 用户? (输入 y/Y 或 n/N, 回车继续): " non_root_user_exists
if [ "$non_root_user_exists" != "Y" ] && [ "$non_root_user_exists" != "y" ]
    then
        warning "没有非 root 用户, 建议创建非 root 用户."
        
        # 确认是否需要创建devops用户
        read -p "是否创建 devops 用户? (输入 y/Y 或 n/N, 回车继续): " need_create_user
        if [ "$need_create_user" != "Y" ] && [ "$need_create_user" != "y" ]
            then
                warning "跳过 devops 用户创建"
            else
                username="devops"
                created_devops_user="n"
                if id "$username" &>/dev/null; then
                    warning "devops 用户已存在, 跳过重复创建和密码重置"
                else
                    msg "devops 用户创建中..."
                    max_pwd_attempts=3
                    pwd_attempts=0
                    pswd=""
                    while (( pwd_attempts < max_pwd_attempts )); do
                        read -s -p "设置devops密码: " pswd_input
                        echo
                        read -s -p "再次输入devops密码: " pswd_confirm
                        echo
                        if [ "$pswd_input" == "$pswd_confirm" ]; then
                            pswd="$pswd_input"
                            break
                        else
                            ((pwd_attempts++))
                            if (( pwd_attempts >= max_pwd_attempts )); then
                                exit_error "两次密码不匹配, 已超过最大尝试次数, 退出执行."
                            fi
                            warning "两次密码不匹配,请重新输入. 剩余尝试次数: $((max_pwd_attempts - pwd_attempts))"
                        fi
                    done
                    useradd "$username" || exit_error "创建用户 ${username} 失败"
                    created_devops_user="y"
                    echo "$pswd" | passwd --stdin "$username" || exit_error "设置用户 ${username} 密码失败"
                fi
                ensure_user_home_permissions devops "$created_devops_user"
                ensure_user_in_group devops wheel
                ensure_user_password_policy devops
                user_home_dir=$(getent passwd devops | awk -F: '{print $6}')
                append_line_if_missing 'PS1="\[\e[01;31m\][\u@\h \W]\$\[\e[00m\]"' "${user_home_dir}/.bashrc"
                append_line_if_missing 'export PS1' "${user_home_dir}/.bashrc"
                append_line_if_missing 'PS1="\[\e[01;31m\][\u@\h \W]\$\[\e[00m\]"' /root/.bashrc
                append_line_if_missing 'export PS1' /root/.bashrc
        fi
else
    read -p "已存在非 root 用户, 请输入用户名:" username
    # 判断输入的用户名在系统中否存在
    if id "$username" &>/dev/null; then
        ok "验证用户 $username 已存在."
    else
        warning "您输入的用户 $username 不存在, 请重新输入"
        read -p "请输入用户名:" username
        # 判断输入的用户名在系统中否存在
        if id "$username" &>/dev/null; then
            ok "验证用户 $username 已存在."
        else
            exit_error "您输入的用户 $username 不存在,退出执行."
        fi
    fi
    ensure_user_in_group "$username" wheel
    ensure_user_password_policy "$username"
    ensure_user_home_permissions "$username" "n"
    user_home_dir=$(getent passwd "$username" | awk -F: '{print $6}')
    # 设置彩色PS1提示符(仅在不存在时追加)
    append_line_if_missing 'PS1="\[\e[01;31m\][\u@\h \W]\$\[\e[00m\]"' "${user_home_dir}/.bashrc"
    append_line_if_missing 'export PS1' "${user_home_dir}/.bashrc"

fi

# 确认是否需要设置Max open files设置
read -p "是否修改默认 Max open files 限制为65536? (输入 y/Y 或 n/N, 回车继续): " need_set_ulimit

if [ "$need_set_ulimit" != "Y" ] && [ "$need_set_ulimit" != "y" ]; then
    warning "跳过 max open files 设置."
else

    limits_conf="/etc/security/limits.conf"
    [ -f "$limits_conf" ] || touch "$limits_conf"
    max_descriptor=$(cat /proc/sys/fs/file-max)
    echo "Max file descriptor of this machine is $max_descriptor"

    # 仅在最后一行是 '# End of file' 时才删除,避免误删配置
    if tail -1 "$limits_conf" | grep -qx '# End of file'; then
        sed -i '$ d' "$limits_conf"
    fi

    # 检查 username 变量是否已设置
    if [[ -z "$username" ]]; then
        warning "未设置用户名, 跳过用户级 ulimit 配置, 仅配置 root"
    fi

    ensure_limits_rule "$limits_conf" "root" "soft" "nofile" "65536"
    ensure_limits_rule "$limits_conf" "root" "hard" "nofile" "65536"

    if [[ -n "$username" ]]; then
        ensure_limits_rule "$limits_conf" "$username" "soft" "nofile" "65536"
        ensure_limits_rule "$limits_conf" "$username" "hard" "nofile" "65536"
        ensure_limits_rule "$limits_conf" "$username" "soft" "nproc" "65536"
        ensure_limits_rule "$limits_conf" "$username" "hard" "nproc" "65536"
    fi

    # 确保文件以 '# End of file' 结尾
    if ! tail -1 "$limits_conf" | grep -qx '# End of file'; then
        echo '# End of file' >> "$limits_conf"
    fi
    ok "Max open files 设置完成."
fi

# 设置系统登录 Banner -----------------
# 检查 /etc/motd 文件是否存在,如果不存在则创建
if [ ! -f /etc/motd ]; then
    touch /etc/motd
fi              
local_hostname=$(hostname)
local_host=$(hostname -I)
local_ip=${local_host%% *}
# 使用覆盖写入避免重复运行时 banner 重复追加
cat > /etc/motd <<EOF
################################################################
#  Login success. All activities will be monitored and reported
#                                  $local_ip
###############################################################
EOF
ok "登录 Banner 设置完成."

# 检查 SELinux 是否安装或启用函数
check_selinux_status() {
    # 运行时状态检查
    if command -v sestatus &> /dev/null; then
        runtime_status=$(sestatus | grep "SELinux status:" | awk '{print $3}')
    elif [ -f /sys/fs/selinux/enforce ]; then
        runtime_status=$(cat /sys/fs/selinux/enforce)
        runtime_status=$([[ "$runtime_status" -eq 1 ]] && echo "enabled" || echo "disabled")
    else
        runtime_status="not_installed"
    fi

    # 配置文件检查
    if [ -f /etc/selinux/config ]; then
        config_file="/etc/selinux/config"
    elif [ -f /etc/sysconfig/selinux ]; then
        config_file="/etc/sysconfig/selinux"
    else
        config_file="not_found"
    fi

    echo "$runtime_status" "$config_file"
}

# 禁用 SELinux 的操作函数
disable_selinux() {
    local config_file=$1

    if [ "$config_file" != "not_found" ]; then
        # 修改配置文件
        sed -i 's/^SELINUX=.*/SELINUX=disabled/' "$config_file"
        setenforce 0
        ok "SELinux 已禁用,配置文件: ($config_file)."
    else
        warning "SELinux 配置文件未找到,无法禁用 SELinux."
        echo 
        read -p "是否继续执行? (输入 y/Y 或 n/N, 回车继续): " disable_selinux_faild
        if [ "$disable_selinux_faild" != "Y" ] && [ "$disable_selinux_faild" != "y" ]
            then 
                exit_error "退出执行."
        fi
    fi
}

#Disable selinux
# 获取 SELinux 当前状态
read runtime_status config_file <<< "$(check_selinux_status)"
# 输出当前状态
if [ "$runtime_status" == "enabled" ]; then
    warning "SELinux 状态已启用."
    if [ "$CONTAINER_ENV" != "none" ]; then
        warning "容器环境 ($CONTAINER_ENV) 建议禁用 SELinux, 自动执行禁用..."
        disable_selinux "$config_file"
        ok "SELinux 已禁用 (容器环境)"
    else
        read -p "需要禁用SeLinux吗? (输入 y/Y 或 n/N, 回车继续): " need_disable_selinux
        if [ "$need_disable_selinux" != "Y" ] && [ "$need_disable_selinux" != "y" ]
            then
                warning "跳过SELinux禁用配置..."
            else
                disable_selinux "$config_file"
                ok "SELinux 已禁用"
        fi
    fi
elif [ "$runtime_status" == "disabled" ]; then
    ok "SELinux 当前未开启."
elif [ "$runtime_status" == "not_installed" ]; then
    ok "SELinux 当前未安装."
else
    warning "无法确认 SELinux 状态.如果需要禁用,请手动检查."
fi



#Disable swapoff
if [ "$CONTAINER_ENV" == "k8s" ]; then
    read -p "是否需要关闭swap? K8S 1.22+ 已支持开启 swap (建议配置 swapBehavior: NoSwap 提高边缘稳定性). 此操作会备份 /etc/fstab 到 /etc/fstab.bak 然后注释 swap 配置.(输入 y/Y 或 n/N, 回车继续):" need_swapoff
    if [ "$need_swapoff" != "Y" ] && [ "$need_swapoff" != "y" ]
        then
            warning "跳过关闭swap操作"
        else
            swapoff -a
            sed -i.bak '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab
            ok "swap 已关闭"
    fi
fi

if rpm -q lvm2 &>/dev/null; then
    ok "lvm2 已安装, 跳过安装."
else
    dnf install lvm2 -y || exit_error "安装 lvm2 失败"
fi

# ======================== 磁盘挂载 ========================
# 三阶段流程:发现 → 收集用户选项 → 统一执行

# === 阶段一:发现裸盘 ===
RAW_DISK_NAMES=()
RAW_DISK_SIZES=()

eval "$(python3 -c "
import json,subprocess
data = json.loads(subprocess.check_output(['lsblk','-J','-o','NAME,SIZE,TYPE,FSTYPE']).decode())
names=[]
sizes=[]
for d in data.get('blockdevices',[]):
    if d.get('type')=='disk' and not d.get('children') and not d.get('fstype'):
        names.append(d['name'])
        sizes.append(d.get('size','unknown'))
print('RAW_DISK_NAMES=(' + ' '.join(names) + ')')
print('RAW_DISK_SIZES=(' + ' '.join(sizes) + ')')
" 2>/dev/null)"

if [[ ${#RAW_DISK_NAMES[@]} -eq 0 ]]; then
    msg "未发现可用裸盘, 跳过磁盘挂载."
else

msg "\n发现以下未挂载的裸盘:"
msg "-----------------------------"
msg "  序号  磁盘           大小"
msg "-----------------------------"
for i in "${!RAW_DISK_NAMES[@]}"; do
    printf "  %-6s /dev/%-10s %s\n" "$((i+1))" "${RAW_DISK_NAMES[$i]}" "${RAW_DISK_SIZES[$i]}" | tee -a "$LOG_FILE"
done
msg "-----------------------------\n"

# === 阶段二:收集用户选项 ===
# 数据结构: 关联数组以挂载点为 key
declare -A MP_DISKS         # "/opt" -> "sdb,sdc"
declare -A MP_MODE          # "/opt" -> "new" | "extend"
declare -A MP_STRIPE        # "/opt" -> "yes" | "no"
declare -A MP_CONTENT       # "/opt" -> "clear" | "preserve" | "na"
declare -A MP_VG            # "/opt" -> "vg_opt"
declare -A MP_LV            # "/opt" -> "lv_opt"
declare -A MP_EXISTING_VG   # "/opt" -> 已有 VG 名 (仅扩容模式)
DISK_MOUNT=()               # 每块盘对应的挂载点,跳过的为空

for i in "${!RAW_DISK_NAMES[@]}"; do
    disk="${RAW_DISK_NAMES[$i]}"
    size="${RAW_DISK_SIZES[$i]}"

    # 步骤1: 是否挂载
    read -p "是否挂载磁盘 /dev/${disk} (${size})? (y/n): " mount_answer
    if [ "$mount_answer" != "Y" ] && [ "$mount_answer" != "y" ]; then
        warning "跳过磁盘 /dev/${disk}"
        DISK_MOUNT[$i]=""
        continue
    fi

    # 步骤2: 输入挂载点 (可重新输入)
    while true; do
        read -p "请输入 /dev/${disk} 的挂载点路径 (例如 /opt, /data): " mp
        # 校验路径格式
        if [[ ! "$mp" =~ ^/ ]]; then
            warning "挂载点必须是绝对路径 (以 / 开头), 请重新输入."
            continue
        fi

        # 步骤3a: 检查挂载点是否已由系统中的 LVM 卷支撑
        lvm_info=$(check_existing_lvm "$mp")
        if [ -n "$lvm_info" ]; then
            existing_vg=$(echo "$lvm_info" | awk '{print $1}')
            existing_lv_path=$(echo "$lvm_info" | awk '{print $2}')
            warning "挂载点 $mp 已存在 LVM 卷 $existing_lv_path (VG: $existing_vg)"
            read -p "选择操作: [1] 扩容现有LV  [2] 重新输入挂载点: " lvm_choice
            if [ "$lvm_choice" == "2" ]; then
                continue
            fi
            MP_MODE[$mp]="extend"
            MP_EXISTING_VG[$mp]="$existing_vg"
            MP_CONTENT[$mp]="na"
            # 追加磁盘到已有记录
            if [ -n "${MP_DISKS[$mp]}" ]; then
                MP_DISKS[$mp]="${MP_DISKS[$mp]},${disk}"
            else
                MP_DISKS[$mp]="$disk"
            fi
            DISK_MOUNT[$i]="$mp"
            break
        fi

        # 步骤3b: 检查是否有同一会话中的其他磁盘已选择此挂载点
        if [ -n "${MP_DISKS[$mp]}" ]; then
            msg "挂载点 $mp 已被磁盘 ${MP_DISKS[$mp]} 选用."
            read -p "选择操作: [1] 加入同一VG进行扩容  [2] 重新输入挂载点: " dup_choice
            if [ "$dup_choice" == "2" ]; then
                continue
            fi
            MP_DISKS[$mp]="${MP_DISKS[$mp]},${disk}"
            DISK_MOUNT[$i]="$mp"
            break
        fi

        # 步骤3c: 挂载点目录存在且非空
        if [ -d "$mp" ] && [ "$(ls -A "$mp" 2>/dev/null)" ]; then
            warning "挂载点 $mp 目录非空."
            read -p "选择操作: [1] 保留内容 (挂载后恢复)  [2] 清空内容 (备份到 ${mp}_bak): " content_choice
            if [ "$content_choice" == "2" ]; then
                MP_CONTENT[$mp]="clear"
            else
                MP_CONTENT[$mp]="preserve"
            fi
        else
            MP_CONTENT[$mp]="na"
        fi

        # 新建模式
        MP_MODE[$mp]="new"
        MP_DISKS[$mp]="$disk"
        local_suffix=$(sanitize_mount_path "$mp")
        MP_VG[$mp]="vg_${local_suffix}"
        MP_LV[$mp]="lv_${local_suffix}"
        DISK_MOUNT[$i]="$mp"
        break
    done
done

# 步骤4: Striping 检查 — 同一挂载点多块盘且大小相同时询问
for mp in "${!MP_DISKS[@]}"; do
    IFS=',' read -ra disks_for_mp <<< "${MP_DISKS[$mp]}"
    count=${#disks_for_mp[@]}
    MP_STRIPE[$mp]="no"
    if [ "$count" -ge 2 ] && [ "${MP_MODE[$mp]}" == "new" ]; then
        # 检查所有磁盘大小是否一致
        all_same="true"
        first_size=$(get_disk_size "${disks_for_mp[0]}")
        for d in "${disks_for_mp[@]:1}"; do
            if [ "$(get_disk_size "$d")" != "$first_size" ]; then
                all_same="false"
                break
            fi
        done
        if [ "$all_same" == "true" ]; then
            read -p "${count} 块磁盘 (${MP_DISKS[$mp]}) 将挂载到 $mp, 大小相同(${first_size}), 是否使用条带化(striping)模式? (y/n): " stripe_answer
            if [ "$stripe_answer" == "Y" ] || [ "$stripe_answer" == "y" ]; then
                MP_STRIPE[$mp]="yes"
            fi
        fi
    fi
done

# === 阶段二.五:确认执行计划 ===
has_plan="false"
for mp in "${!MP_DISKS[@]}"; do
    has_plan="true"
    break
done

if [ "$has_plan" == "true" ]; then
    msg "\n========= 磁盘挂载执行计划 ========="
    for mp in "${!MP_DISKS[@]}"; do
        if [ "${MP_MODE[$mp]}" == "new" ]; then
            mode_label="新建 VG/LV"
        else
            mode_label="扩容现有 LV"
        fi
        stripe_label=""
        if [ "${MP_STRIPE[$mp]}" == "yes" ]; then
            stripe_label=" (条带化)"
        fi
        msg "  挂载点: $mp"
        msg "    磁盘: ${MP_DISKS[$mp]}"
        msg "    操作: ${mode_label}${stripe_label}"
        if [ "${MP_MODE[$mp]}" == "new" ]; then
            msg "    VG/LV: ${MP_VG[$mp]}/${MP_LV[$mp]}"
        else
            msg "    目标VG: ${MP_EXISTING_VG[$mp]}"
        fi
        if [ "${MP_CONTENT[$mp]}" == "preserve" ]; then
            msg "    目录内容: 保留 (挂载后恢复)"
        elif [ "${MP_CONTENT[$mp]}" == "clear" ]; then
            msg "    目录内容: 清空 (备份到 ${mp}_bak)"
        fi
        msg ""
    done
    msg "====================================="

    read -p "确认执行以上操作? (y/n): " exec_confirm
    if [ "$exec_confirm" != "Y" ] && [ "$exec_confirm" != "y" ]; then
        warning "用户取消磁盘挂载操作."
    else

    # === 阶段三:统一执行 ===
    for mp in "${!MP_DISKS[@]}"; do
        IFS=',' read -ra disks_for_mp <<< "${MP_DISKS[$mp]}"
        dev_list=()
        for d in "${disks_for_mp[@]}"; do
            dev_list+=("/dev/$d")
        done

        if [ "${MP_MODE[$mp]}" == "new" ]; then
            vg="${MP_VG[$mp]}"
            lv="${MP_LV[$mp]}"

            # 检查 VG 名是否已存在,存在则追加数字后缀
            if vgs "$vg" &>/dev/null; then
                suffix=2
                while vgs "${vg}_${suffix}" &>/dev/null; do
                    ((suffix++))
                done
                vg="${vg}_${suffix}"
                MP_VG[$mp]="$vg"
                warning "VG 名称冲突, 使用 $vg"
            fi

            # pvcreate
            for dev in "${dev_list[@]}"; do
                pvcreate "$dev" || exit_error "pvcreate $dev 失败"
            done

            # vgcreate
            vgcreate "$vg" "${dev_list[@]}" || exit_error "vgcreate $vg 失败"

            # lvcreate
            if [ "${MP_STRIPE[$mp]}" == "yes" ]; then
                stripe_count=${#disks_for_mp[@]}
                lvcreate -l 100%FREE -i "$stripe_count" -I 64 -n "$lv" "$vg" \
                    || exit_error "lvcreate (striping) 失败"
                ok "已创建条带化 LV: /dev/${vg}/${lv} (stripe count=${stripe_count}, stripe size=64K)"
            else
                lvcreate -l 100%FREE -n "$lv" "$vg" \
                    || exit_error "lvcreate 失败"
            fi

            # mkfs
            mkfs.xfs "/dev/${vg}/${lv}" || exit_error "mkfs.xfs /dev/${vg}/${lv} 失败"

            # 处理挂载点内容并挂载
            mkdir -p "$mp"
            if [ "${MP_CONTENT[$mp]}" == "preserve" ]; then
                tmpbak="${mp}_bak_$(date +%s)"
                mkdir -p "$tmpbak"
                mv "${mp}"/* "$tmpbak/" 2>/dev/null
                mount "/dev/${vg}/${lv}" "$mp"
                mv "$tmpbak"/* "${mp}/" 2>/dev/null
                rm -rf "$tmpbak"
                ok "挂载 /dev/${vg}/${lv}$mp (原内容已恢复)"
            elif [ "${MP_CONTENT[$mp]}" == "clear" ]; then
                tmpbak="${mp}_bak_$(date +%s)"
                mkdir -p "$tmpbak"
                mv "${mp}"/* "$tmpbak/" 2>/dev/null
                mount "/dev/${vg}/${lv}" "$mp"
                ok "挂载 /dev/${vg}/${lv}$mp (原内容已备份到 $tmpbak)"
            else
                mount "/dev/${vg}/${lv}" "$mp"
                ok "挂载 /dev/${vg}/${lv}$mp"
            fi

            # 写入 fstab
            # LVM 设备路径基于 VG/LV 名称,稳定且可读,无需使用 UUID
            fstab_source="/dev/${vg}/${lv}"
            # 挂载选项: noatime 减少元数据写入, nofail 设备异常时不阻塞启动
            fstab_opts="defaults,noatime,nofail"
            # fsck pass: 非根分区设为 2 (开机自检)
            # 检查 fstab 中是否已存在该挂载点或设备
            if grep -q "${fstab_source}" /etc/fstab; then
                warning "fstab 中已存在 ${fstab_source} 的记录, 跳过写入"
            elif grep -qE "^[[:space:]]*[^#]+[[:space:]]+${mp}[[:space:]]+" /etc/fstab; then
                warning "fstab 中挂载点 ${mp} 已被其他设备占用, 跳过写入, 请手动检查"
            else
                echo "${fstab_source}    ${mp}    xfs    ${fstab_opts}    0 0" >> /etc/fstab
                ok "fstab 已写入: ${fstab_source} -> ${mp}"
            fi

        elif [ "${MP_MODE[$mp]}" == "extend" ]; then
            existing_vg="${MP_EXISTING_VG[$mp]}"
            existing_lv_path=$(findmnt -n -o SOURCE "$mp")

            # pvcreate
            for dev in "${dev_list[@]}"; do
                pvcreate "$dev" || exit_error "pvcreate $dev 失败"
            done

            # vgextend
            vgextend "$existing_vg" "${dev_list[@]}" || exit_error "vgextend $existing_vg 失败"

            # lvextend + 扩展文件系统
            lvextend -l +100%FREE "$existing_lv_path" || exit_error "lvextend $existing_lv_path 失败"
            xfs_growfs "$mp" || exit_error "xfs_growfs $mp 失败"

            ok "磁盘 ${MP_DISKS[$mp]} 已扩容到 VG $existing_vg, 挂载点 $mp"
        fi
    done

    # === Bind Mount 配置 ===
    # 收集本次新建的挂载点
    NEW_MPS=()
    for mp in "${!MP_MODE[@]}"; do
        if [ "${MP_MODE[$mp]}" == "new" ]; then
            NEW_MPS+=("$mp")
        fi
    done

    if [ ${#NEW_MPS[@]} -gt 0 ]; then
        msg "\n========= Bind Mount 配置 ========="
        msg "可将根分区上增长较快的目录 (如 /var/log, /tmp, /home) bind mount 到数据盘子目录"

        BIND_SOURCES=()
        BIND_TARGETS=()
        BIND_CONTENT_ACTION=()
        BIND_BASE_MP=()           # 每个 bind mount 依赖的底层挂载点

        for base_mp in "${NEW_MPS[@]}"; do
            read -p "是否为 ${base_mp} 配置 bind mount? (y/n): " need_bind
            if [ "$need_bind" != "Y" ] && [ "$need_bind" != "y" ]; then
                continue
            fi

            msg "输入需要 bind mount 到 ${base_mp} 的源目录, 输入 q 结束."
            while true; do
                read -p "源目录路径 (如 /var/log, 输入 q 结束): " bind_src
                # 退出
                if [ "$bind_src" == "q" ] || [ "$bind_src" == "Q" ]; then
                    break
                fi
                # 校验: 绝对路径
                if [[ ! "$bind_src" =~ ^/ ]]; then
                    warning "必须是绝对路径, 请重新输入."
                    continue
                fi
                # 校验: 不能是目标挂载点本身或其子目录
                if [[ "$bind_src" == "$base_mp" || "$bind_src" == "$base_mp"/* ]]; then
                    warning "源目录不能是 ${base_mp} 本身或其子目录 (避免循环挂载), 请重新输入."
                    continue
                fi
                # 校验: 不能与已收集的重复
                is_dup="false"
                for existing_src in "${BIND_SOURCES[@]}"; do
                    if [ "$existing_src" == "$bind_src" ]; then
                        is_dup="true"
                        break
                    fi
                done
                if [ "$is_dup" == "true" ]; then
                    warning "源目录 ${bind_src} 已添加过, 请输入其他目录."
                    continue
                fi

                # 生成目标子目录: /var/log → /data/var_log
                bind_subdir=$(sanitize_mount_path "$bind_src")
                bind_target="${base_mp}/${bind_subdir}"

                # 检查源目录内容
                if [ -d "$bind_src" ] && [ "$(ls -A "$bind_src" 2>/dev/null)" ]; then
                    msg "源目录 ${bind_src} 非空."
                    msg "  [1] 迁移内容到 ${bind_target} (mv 后 bind mount)"
                    msg "  [2] 直接挂载 (原数据保留在底层, unmount 后可见)"
                    msg "  [3] 清空内容 (备份后清空)"
                    msg "  [4] 跳过此目录"
                    read -p "选择操作 [1/2/3/4]: " bind_content_choice
                    case "$bind_content_choice" in
                        1) bind_action="migrate" ;;
                        2) bind_action="direct" ;;
                        3) bind_action="clear" ;;
                        4) continue ;;
                        *) warning "无效选择, 跳过此目录."; continue ;;
                    esac
                else
                    bind_action="direct"
                fi

                BIND_SOURCES+=("$bind_src")
                BIND_TARGETS+=("$bind_target")
                BIND_CONTENT_ACTION+=("$bind_action")
                BIND_BASE_MP+=("$base_mp")
                ok "已添加: ${bind_src}${bind_target} (${bind_action})"
            done
        done

        # Bind Mount 确认与执行
        if [ ${#BIND_SOURCES[@]} -gt 0 ]; then
            msg "\n--------- Bind Mount 执行计划 ---------"
            printf "  %-20s %-30s %s\n" "源目录" "目标目录" "内容处理" | tee -a "$LOG_FILE"
            for idx in "${!BIND_SOURCES[@]}"; do
                case "${BIND_CONTENT_ACTION[$idx]}" in
                    migrate)  action_label="迁移" ;;
                    direct)   action_label="直接挂载" ;;
                    clear)    action_label="清空(备份)" ;;
                esac
                printf "  %-20s %-30s %s\n" "${BIND_SOURCES[$idx]}" "${BIND_TARGETS[$idx]}" "$action_label" | tee -a "$LOG_FILE"
            done
            msg "---------------------------------------"

            read -p "确认执行 bind mount? (y/n): " bind_confirm
            if [ "$bind_confirm" == "Y" ] || [ "$bind_confirm" == "y" ]; then
                for idx in "${!BIND_SOURCES[@]}"; do
                    src="${BIND_SOURCES[$idx]}"
                    tgt="${BIND_TARGETS[$idx]}"
                    action="${BIND_CONTENT_ACTION[$idx]}"

                    mkdir -p "$tgt"
                    mkdir -p "$src"

                    if is_bind_mount_active "$tgt" "$src"; then
                        ok "bind mount 已存在: ${src}${tgt}, 跳过重复挂载"
                        continue
                    fi

                    case "$action" in
                        migrate)
                            if [ "$(ls -A "$src" 2>/dev/null)" ]; then
                                mv "$src"/* "$tgt/" 2>/dev/null
                                mv "$src"/.* "$tgt/" 2>/dev/null
                            fi
                            mount --bind "$tgt" "$src"
                            ok "bind mount: ${src}${tgt} (内容已迁移)"
                            ;;
                        direct)
                            mount --bind "$tgt" "$src"
                            ok "bind mount: ${src}${tgt} (直接挂载)"
                            ;;
                        clear)
                            if [ "$(ls -A "$src" 2>/dev/null)" ]; then
                                backup="${src}_bak_$(date +%s)"
                                mkdir -p "$backup"
                                mv "$src"/* "$backup/" 2>/dev/null
                                mv "$src"/.* "$backup/" 2>/dev/null
                                msg "源目录内容已备份到 ${backup}"
                            fi
                            mount --bind "$tgt" "$src"
                            ok "bind mount: ${src}${tgt} (已清空, 原内容已备份)"
                            ;;
                    esac

                    # 写入 fstab (避免重复)
                    # x-systemd.requires-mounts-for 确保底层挂载点先于 bind mount 挂载
                    bind_base="${BIND_BASE_MP[$idx]}"
                    bind_opts="bind,nofail,x-systemd.requires-mounts-for=${bind_base}"
                    if grep -qE "^[[:space:]]*[^#[:space:]]+[[:space:]]+${src}[[:space:]]+[^[:space:]]+[[:space:]]+([^[:space:],]+,)*bind(,[^[:space:],]+)*([[:space:]]|$)" /etc/fstab; then
                        warning "fstab 中已存在 ${src} 的 bind mount 记录, 跳过写入"
                    else
                        echo "${tgt}    ${src}    none    ${bind_opts}    0 0" >> /etc/fstab
                        ok "fstab 已写入 bind mount: ${tgt}${src} (依赖 ${bind_base})"
                    fi
                done
            else
                warning "用户取消 bind mount 操作."
            fi
        fi
    fi # NEW_MPS check

    fi # exec_confirm
else
    msg "没有需要挂载的磁盘."
fi # has_plan

fi # raw disks check

fi # RUN_SCOPE full init

if [ "$RUN_SCOPE" == "baseline" ]; then
    need_create_user="n"
    if [ -z "$non_root_user_exists" ]; then
        username=$(awk -F: '$3 >= 1000 && $3 < 65534 {print $1; exit}' /etc/passwd)
        if [ -n "$username" ]; then
            non_root_user_exists="Y"
            ok "仅安全基线模式自动检测到非 root 用户: ${username}"
            ensure_user_in_group "$username" wheel
            ensure_user_password_policy "$username"
        else
            non_root_user_exists="n"
            warning "仅安全基线模式未检测到 UID >= 1000 的非 root 用户, SSH root 登录限制将跳过."
        fi
    fi
fi


# 基线检查与加固 ########################################################
baseline_info "开始" "开始执行安全基线检查与加固"

# 是否配置 NTP 时间同步
read -p "是否配置 NTP 时间同步? (输入 y/Y 或 n/N, 回车继续): " need_config_ntp
if [ "$need_config_ntp" == "Y" ] || [ "$need_config_ntp" == "y" ]; then
    time_sync_service=""
    while true; do
        read -p "请输入 NTP 服务器 IP 或域名, 多个值可用空格或逗号分隔: " ntp_server_input
        if parse_ntp_servers "$ntp_server_input"; then
            break
        fi
        baseline_warn "NTP 时间同步" "NTP 服务器列表格式不正确, 请重新输入"
    done

    if ! time_sync_service=$(select_time_sync_service); then
        baseline_warn "NTP 时间同步" "chrony/ntp 软件包均不可用或安装失败, 请手动配置时间同步"
    elif [ "$time_sync_service" == "chronyd" ]; then
        [ -f /etc/chrony.conf ] || touch /etc/chrony.conf
        sed -i -E '/^[[:space:]]*server[[:space:]]+/d;/^[[:space:]]*pool[[:space:]]+/d' /etc/chrony.conf
        for ntp_server_ip in "${NTP_SERVERS[@]}"; do
            echo "server ${ntp_server_ip} iburst" >> /etc/chrony.conf
        done
        systemctl disable --now ntpd &>/dev/null || true
        if systemctl enable --now chronyd &>/dev/null; then
            baseline_ok "NTP 时间同步" "已使用 chronyd 配置并启动, 同步服务器: ${NTP_SERVERS[*]}"
        else
            baseline_warn "NTP 时间同步" "配置已写入 /etc/chrony.conf, 但 chronyd 启动失败, 请手动检查"
        fi
    else
        [ -f /etc/ntp.conf ] || touch /etc/ntp.conf
        sed -i -E '/^[[:space:]]*server[[:space:]]+/d;/^[[:space:]]*pool[[:space:]]+/d' /etc/ntp.conf
        for ntp_server_ip in "${NTP_SERVERS[@]}"; do
            echo "server ${ntp_server_ip} iburst" >> /etc/ntp.conf
        done
        systemctl disable --now chronyd &>/dev/null || true
        if systemctl enable --now ntpd &>/dev/null; then
            baseline_ok "NTP 时间同步" "已使用 ntpd 配置并启动, 同步服务器: ${NTP_SERVERS[*]}"
        elif service ntpd start &>/dev/null; then
            baseline_ok "NTP 时间同步" "已使用 ntpd 配置并启动, 同步服务器: ${NTP_SERVERS[*]}"
        else
            baseline_warn "NTP 时间同步" "配置已写入 /etc/ntp.conf, 但 ntpd 启动失败, 请手动检查"
        fi
    fi
else
    baseline_skip "NTP 时间同步" "用户选择不配置, 基线检查可能继续不通过"
fi

# 是否配置 rsyslog 远程日志
read -p "是否配置 rsyslog 远程日志转发? (输入 y/Y 或 n/N, 回车继续): " need_remote_rsyslog
if [ "$need_remote_rsyslog" == "Y" ] || [ "$need_remote_rsyslog" == "y" ]; then
    read -p "请输入远程日志服务器 IP 或域名: " remote_log_target
    [ -f /etc/rsyslog.conf ] || touch /etc/rsyslog.conf
    if grep -Eq '^[[:space:]]*\*\.\*[[:space:]]+@' /etc/rsyslog.conf; then
        sed -i -E "s|^[[:space:]]*\*\.\*[[:space:]]+@.*|*.*    @${remote_log_target}|" /etc/rsyslog.conf
    else
        echo "*.*    @${remote_log_target}" >> /etc/rsyslog.conf
    fi

    if systemctl enable --now rsyslog &>/dev/null; then
        baseline_ok "远程日志" "rsyslog 远程日志已配置到 ${remote_log_target}"
    elif systemctl restart rsyslog &>/dev/null; then
        baseline_ok "远程日志" "rsyslog 远程日志已配置到 ${remote_log_target}"
    else
        baseline_warn "远程日志" "rsyslog 配置已写入, 但服务重启失败, 请手动检查"
    fi
else
    baseline_skip "远程日志" "用户选择不配置, 基线检查可能继续不通过"
fi

if command -v rsyslogd &>/dev/null || systemd_unit_exists rsyslog.service; then
    ensure_rsyslog_rule "*.info;mail.none;authpriv.none;cron.none" "/var/log/messages"
    ensure_rsyslog_rule "authpriv.*" "/var/log/secure"
    ensure_rsyslog_rule "mail.*" "-/var/log/maillog"
    ensure_rsyslog_rule "cron.*" "/var/log/cron"
    if systemctl enable --now rsyslog &>/dev/null; then
        baseline_ok "本地日志" "rsyslog 本地关键规则已确保, 服务已启用并启动"
    elif systemctl restart rsyslog &>/dev/null; then
        baseline_ok "本地日志" "rsyslog 本地关键规则已确保, 服务已重启"
    else
        baseline_warn "本地日志" "rsyslog 本地关键规则已写入, 但服务启动失败, 请手动检查"
    fi
else
    baseline_warn "本地日志" "未检测到 rsyslog, 无法确保本地日志规则和服务状态"
fi

if [ -f /etc/logrotate.conf ]; then
    set_space_kv /etc/logrotate.conf "rotate" "12"
    ensure_logrotate_compress /etc/logrotate.conf
    baseline_ok "日志轮转" "/etc/logrotate.conf 已设置 rotate 12 并启用 compress"
else
    baseline_warn "日志轮转" "未找到 /etc/logrotate.conf, 请安装或配置 logrotate"
fi

# 检查是否安装 SNMP 服务
if rpm -q net-snmp &>/dev/null || [ -f /etc/snmp/snmpd.conf ] || systemctl list-unit-files 2>/dev/null | grep -q '^snmpd\.service'; then
    baseline_warn "SNMP 服务" "检测到 SNMP 服务或配置, 基线要求默认不安装"
    read -p "是否停止并禁用 SNMP 服务? (输入 y/Y 或 n/N, 回车继续): " disable_snmp
    if [ "$disable_snmp" == "Y" ] || [ "$disable_snmp" == "y" ]; then
        systemctl disable --now snmpd &>/dev/null || service snmpd stop &>/dev/null
        read -p "是否同时卸载 net-snmp 相关软件包? (输入 y/Y 或 n/N, 回车继续): " remove_snmp
        if [ "$remove_snmp" == "Y" ] || [ "$remove_snmp" == "y" ]; then
            dnf remove -y net-snmp net-snmp-utils || baseline_warn "SNMP 服务" "卸载 net-snmp 失败, 请手动检查"
            if ! rpm -q net-snmp &>/dev/null; then
                baseline_ok "SNMP 服务" "已停止并卸载"
            else
                baseline_warn "SNMP 服务" "软件包仍存在, 基线检查可能继续不通过"
            fi
        else
            baseline_warn "SNMP 服务" "已停止并禁用, 但未卸载软件包, 基线检查可能继续不通过"
        fi
    else
        baseline_skip "SNMP 服务" "用户选择保留, 基线检查可能继续不通过"
    fi
else
    baseline_ok "SNMP 服务" "未检测到 SNMP 服务, 符合基线要求"
fi

if systemd_unit_exists postfix.service; then
    read -p "是否禁用 Postfix 邮件服务? 不需要本机邮件投递时建议禁用 (输入 y/Y 或 n/N, 回车默认Y): " disable_postfix
    disable_postfix=${disable_postfix:-Y}
    if [ "$disable_postfix" == "Y" ] || [ "$disable_postfix" == "y" ]; then
        disable_service_if_present postfix.service "Postfix 服务"
    else
        baseline_skip "Postfix 服务" "用户选择保留, 如不需要邮件服务则基线检查可能继续不通过"
    fi
else
    baseline_ok "Postfix 服务" "未检测到 postfix.service, 符合基线要求"
fi

for unnecessary_unit in \
    cups.service cups.socket cups.path \
    avahi-daemon.service avahi-daemon.socket \
    telnet.socket rsh.socket rlogin.socket rexec.socket \
    tftp.socket tftp.service xinetd.service
do
    disable_service_if_present "$unnecessary_unit" "不必要服务"
done

# 设置密码修改最小天数 6天 (基线要求 >=6)
set_login_defs_value /etc/login.defs "PASS_MIN_DAYS" "6"
baseline_ok "口令最小修改天数" "PASS_MIN_DAYS 已设置为 6"

# 设置密码有效期最大 90 天
set_login_defs_value /etc/login.defs "PASS_MAX_DAYS" "90"
baseline_ok "口令最大有效期" "PASS_MAX_DAYS 已设置为 90"

# 设置密码过期前 30 天提示
set_login_defs_value /etc/login.defs "PASS_WARN_AGE" "30"
baseline_ok "口令过期预警天数" "PASS_WARN_AGE 已设置为 30"

# 设置密码最小长度 12 位
set_login_defs_value /etc/login.defs "PASS_MIN_LEN" "12"
baseline_ok "口令最小长度" "PASS_MIN_LEN 已设置为 12"

# 设置新建用户 home 默认权限
set_login_defs_value /etc/login.defs "UMASK" "077"
set_login_defs_value /etc/login.defs "HOME_MODE" "0700"
baseline_ok "新建用户 home 默认权限" "/etc/login.defs 已设置 UMASK=077, HOME_MODE=0700"

# 设置 umask 为 027 (基线要求 >=027)
set_umask_in_file /etc/bashrc 027
set_umask_in_file /etc/profile 027

if [ -f /etc/csh.cshrc ]; then
    set_umask_in_file /etc/csh.cshrc 027
fi

set_umask_in_file /root/.bashrc 027

if [ -f /root/.cshrc ]; then
    set_umask_in_file /root/.cshrc 027
fi

baseline_ok "默认 umask" "已设置为 027 (/etc/bashrc, /etc/profile, /etc/csh.cshrc, /root/.bashrc, /root/.cshrc)"

# ===== PAM 认证策略配置(authselect + pwquality + faillock)=====

# 前置检查:仅处理 pwquality,不处理 cracklib
if [ ! -f /lib64/security/pam_pwquality.so ]; then
    baseline_warn "PAM 密码策略" "当前系统未使用 pam_pwquality.so, 请手动配置密码策略"
    baseline_skip "PAM 密码策略" "跳过 PAM 密码策略、pwquality.conf、faillock.conf 配置"
else

# --- 配置 /etc/security/pwquality.conf ---
modify_or_append_conf /etc/security/pwquality.conf "minlen" "12"
modify_or_append_conf /etc/security/pwquality.conf "minclass" "4"
modify_or_append_conf /etc/security/pwquality.conf "ucredit" "-1"
modify_or_append_conf /etc/security/pwquality.conf "lcredit" "-1"
modify_or_append_conf /etc/security/pwquality.conf "dcredit" "-1"
modify_or_append_conf /etc/security/pwquality.conf "ocredit" "-1"
baseline_ok "PAM 密码强度" "pwquality.conf 已配置 (minlen=12, minclass=4, ucredit/lcredit/dcredit/ocredit=-1)"

# --- 配置 /etc/security/faillock.conf ---
modify_or_append_conf /etc/security/faillock.conf "deny" "3"
modify_or_append_conf /etc/security/faillock.conf "unlock_time" "1800"
modify_or_append_conf /etc/security/faillock.conf "even_deny_root" ""
modify_or_append_conf /etc/security/faillock.conf "audit" ""
baseline_ok "PAM 账户锁定" "faillock.conf 已配置 (deny=3, unlock_time=1800, even_deny_root, audit)"

# --- PAM 配置:优先 authselect,回退传统方式 ---
AUTHSELECT_APPLIED="false"
if command -v authselect &>/dev/null; then

    # 创建 neusoft-hardened 自定义 profile(基于 minimal)
    if [ ! -d /etc/authselect/custom/neusoft-hardened ]; then
        if authselect create-profile neusoft-hardened -b minimal --symlink-meta; then
            baseline_ok "PAM authselect" "自定义 profile neusoft-hardened 已创建"
        else
            baseline_warn "PAM authselect" "创建 custom/neusoft-hardened 失败, 回退传统方式编辑 PAM 文件"
        fi
    else
        baseline_ok "PAM authselect" "自定义 profile neusoft-hardened 已存在"
    fi

    if [ -d /etc/authselect/custom/neusoft-hardened ]; then
        # 定制 system-auth 模板(基于 minimal,增加 local_users_only 和 remember=5)
        cat > /etc/authselect/custom/neusoft-hardened/system-auth << 'AUTHEOF'
auth        required      pam_env.so
auth        required      pam_faildelay.so delay=2000000
auth        required      pam_faillock.so preauth silent              {include if "with-faillock"}
auth        sufficient    pam_unix.so {if not "without-nullok":nullok} try_first_pass
auth        required      pam_faillock.so authfail                    {include if "with-faillock"}
auth        requisite     pam_succeed_if.so uid >= 1000 quiet_success
auth        required      pam_deny.so

account     required      pam_access.so                               {include if "with-pamaccess"}
account     required      pam_faillock.so                             {include if "with-faillock"}
account     required      pam_unix.so

password    requisite     pam_pwquality.so try_first_pass local_users_only
password    sufficient    pam_unix.so sha512 shadow {if not "without-nullok":nullok} try_first_pass use_authtok remember=5
password    required      pam_deny.so

session     optional      pam_keyinit.so revoke
session     required      pam_limits.so
session     optional      pam_ecryptfs.so unwrap                      {include if "with-ecryptfs"}
-session    optional      pam_systemd.so
session     optional      pam_oddjob_mkhomedir.so umask=0077          {include if "with-mkhomedir"}
session     [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid
session     required      pam_unix.so
AUTHEOF

        # password-auth 与 system-auth 相同
        cp /etc/authselect/custom/neusoft-hardened/system-auth \
           /etc/authselect/custom/neusoft-hardened/password-auth

        # 激活 profile(with-faillock 启用账户锁定,without-nullok 禁止空密码)
        if authselect select custom/neusoft-hardened with-faillock without-nullok --force; then
            AUTHSELECT_APPLIED="true"
            baseline_ok "PAM authselect" "已激活 custom/neusoft-hardened (with-faillock, without-nullok)"
        else
            baseline_warn "PAM authselect" "激活 custom/neusoft-hardened 失败, 回退传统方式编辑 PAM 文件"
        fi
    fi

    # 验证
    if [ "$AUTHSELECT_APPLIED" == "true" ]; then
        if authselect check &>/dev/null; then
            baseline_ok "PAM authselect" "配置验证通过"
        else
            baseline_warn "PAM authselect" "authselect check 验证失败, 请手动检查 /etc/authselect/custom/neusoft-hardened/"
        fi
        if grep -q "remember=5" /etc/pam.d/system-auth; then
            baseline_ok "PAM 密码历史" "remember=5 已生效"
        else
            baseline_warn "PAM 密码历史" "remember=5 未验证通过, 请手动检查 /etc/pam.d/system-auth"
        fi
        if grep -q "pam_pwquality.so" /etc/pam.d/system-auth; then
            baseline_ok "PAM 密码强度" "pam_pwquality.so 已生效"
        else
            baseline_warn "PAM 密码强度" "pam_pwquality.so 未验证通过, 请手动检查 /etc/pam.d/system-auth"
        fi
        if grep -q "pam_faillock.so" /etc/pam.d/system-auth; then
            baseline_ok "PAM 账户锁定" "pam_faillock.so 已生效"
        else
            baseline_warn "PAM 账户锁定" "pam_faillock.so 未验证通过, 请手动检查 /etc/pam.d/system-auth"
        fi
    fi
else
    baseline_warn "PAM authselect" "authselect 未安装, 回退传统方式编辑 PAM 文件"
fi

if [ "$AUTHSELECT_APPLIED" != "true" ]; then
    # --- Fallback: authselect 不可用,传统方式编辑 PAM 文件 ---

    cp /etc/pam.d/system-auth /etc/pam.d/system-auth.bak.$(date +%F-%T)
    FILE="/etc/pam.d/system-auth"

    if grep -q "pam_pwquality.so" "$FILE"; then
        sed -i -E '/pam_pwquality\.so/ {
            /ucredit=-1/! s/$/ ucredit=-1/
            /lcredit=-1/! s/$/ lcredit=-1/
            /dcredit=-1/! s/$/ dcredit=-1/
            /ocredit=-1/! s/$/ ocredit=-1/
            /try_first_pass/! s/$/ try_first_pass/
            /local_users_only/! s/$/ local_users_only/
            /minlen=/ {
                s/minlen=[0-9]+/minlen=12/
            }
            /minlen=/! s/$/ minlen=12/
        }' "$FILE"
    else
        baseline_warn "PAM 密码强度" "$FILE 中未找到 pam_pwquality.so 行, 请手动配置密码策略"
    fi

    # 添加 remember=5
    if grep -q "pam_unix.so" "$FILE"; then
        if ! grep -q "pam_unix.so.*remember=" "$FILE"; then
            sed -i '/password.*pam_unix.so/ s/$/ remember=5/' "$FILE"
        fi
    fi

    baseline_ok "PAM 传统配置" "system-auth 密码策略配置完成"
fi

fi  # end pwquality check

# 禁止 root 用户通过 telnet/login 远程登录
if [ -f /etc/pam.d/login ]; then
    prepend_pam_rule_if_missing /etc/pam.d/login "auth required pam_securetty.so" '^[[:space:]]*auth[[:space:]]+.*pam_securetty\.so([[:space:]]|$)'
    baseline_ok "禁止 root 远程 telnet 登录" "/etc/pam.d/login 已配置 pam_securetty.so"
else
    baseline_skip "禁止 root 远程 telnet 登录" "未找到 /etc/pam.d/login"
fi

# 限制仅 wheel 组用户可 su 为 root
if [ -f /etc/pam.d/su ]; then
    prepend_pam_rule_if_missing /etc/pam.d/su "auth required pam_wheel.so group=wheel" '^[[:space:]]*auth[[:space:]]+required[[:space:]]+pam_wheel\.so([[:space:]]|$).*group=wheel'
    prepend_pam_rule_if_missing /etc/pam.d/su "auth sufficient pam_rootok.so" '^[[:space:]]*auth[[:space:]]+sufficient[[:space:]]+pam_rootok\.so([[:space:]]|$)'
    baseline_ok "限制 su 到 root" "/etc/pam.d/su 已限制仅 wheel 组用户可 su 为 root"
else
    baseline_skip "限制 su 到 root" "未找到 /etc/pam.d/su"
fi


# 设置 ICMP 重定向与 send_redirects 参数...

# 临时关闭(立即生效)
sysctl -w net.ipv4.conf.all.accept_redirects=0
sysctl -w net.ipv4.conf.default.accept_redirects=0
sysctl -w net.ipv4.conf.all.send_redirects=0
sysctl -w net.ipv4.conf.default.send_redirects=0

#设置ip_forward配置(立即生效)
if [ "$CONTAINER_ENV" != "none" ]; then
    sysctl -w net.ipv4.ip_forward=1
else
    sysctl -w net.ipv4.ip_forward=0
fi
sysctl -w net.ipv4.conf.all.accept_source_route=0
sysctl -w net.ipv4.icmp_echo_ignore_broadcasts=1
sysctl -w net.ipv4.tcp_syncookies=1
sysctl -w net.ipv4.tcp_max_syn_backlog=2048
sysctl -w net.ipv4.tcp_synack_retries=2
baseline_ok "网络安全参数" "运行时参数已设置: accept_redirects=0, send_redirects=0, accept_source_route=0, icmp_echo_ignore_broadcasts=1, SYN 防护已启用"

# 永久修改 /etc/sysctl.conf
modify_or_append() {
  local PARAM="$1"
  local VALUE="$2"
  local FILE="/etc/sysctl.conf"
  local temp_file

  [ -f "$FILE" ] || touch "$FILE"
  temp_file=$(mktemp)
  if awk -v param="$PARAM" -v value="$VALUE" '
    /^[[:space:]]*#/ {
      print
      next
    }
    {
      probe = $0
      sub(/^[[:space:]]*/, "", probe)
      if (index(probe, param) == 1) {
        rest = substr(probe, length(param) + 1)
        if (rest ~ /^[[:space:]]*=/ || rest ~ /^[[:space:]]+/) {
          if (!updated) {
            printf "%s = %s\n", param, value
            updated = 1
          }
          next
        }
      }
      print
    }
    END {
      if (!updated) {
        printf "%s = %s\n", param, value
      }
    }
  ' "$FILE" > "$temp_file"; then
    cat "$temp_file" > "$FILE"
  else
    rm -f "$temp_file"
    return 1
  fi
  rm -f "$temp_file"
}

modify_or_append "net.ipv4.conf.all.accept_redirects" 0
modify_or_append "net.ipv4.conf.default.accept_redirects" 0
modify_or_append "net.ipv4.conf.all.send_redirects" 0
modify_or_append "net.ipv4.conf.default.send_redirects" 0
modify_or_append "net.ipv4.conf.all.accept_source_route" 0
modify_or_append "net.ipv4.icmp_echo_ignore_broadcasts" 1
modify_or_append "net.ipv4.tcp_syncookies" 1
modify_or_append "net.ipv4.tcp_max_syn_backlog" 2048
modify_or_append "net.ipv4.tcp_synack_retries" 2

# 容器环境需要 ip_forward=1, 非容器环境设置为 0
if [ "$CONTAINER_ENV" != "none" ]; then
    modify_or_append "net.ipv4.ip_forward" 1
    baseline_ok "IP 转发" "ip_forward 已设置为 1 (容器环境必需)"
else
    modify_or_append "net.ipv4.ip_forward" 0
    baseline_ok "IP 转发" "ip_forward 已设置为 0 (非容器环境)"
fi

# K8S 环境需要 bridge-nf-call-iptables 参数
if [ "$CONTAINER_ENV" == "k8s" ]; then
    modprobe br_netfilter 2>/dev/null
    if [ $? -eq 0 ]; then
        modify_or_append "net.bridge.bridge-nf-call-iptables" 1
        modify_or_append "net.bridge.bridge-nf-call-ip6tables" 1
        baseline_ok "K8S bridge netfilter" "bridge-nf-call-iptables 与 bridge-nf-call-ip6tables 已配置"
    else
        baseline_warn "K8S bridge netfilter" "br_netfilter 模块加载失败, 请手动配置 bridge-nf-call-iptables"
    fi
    # 确保 br_netfilter 开机自动加载
    # if [ ! -f /etc/modules-load.d/k8s.conf ]; then
    #     echo "br_netfilter" > /etc/modules-load.d/k8s.conf
    #     ok "已配置 br_netfilter 模块开机自动加载"
    # fi
else
    baseline_skip "K8S bridge netfilter" "非 K8S 环境无需配置 bridge-nf-call-iptables"
fi

if sysctl -p; then
    baseline_ok "网络安全参数" "/etc/sysctl.conf 已写入并执行 sysctl -p"
else
    baseline_warn "网络安全参数" "/etc/sysctl.conf 已写入, 但 sysctl -p 执行失败, 请手动检查"
fi


# SSH 服务加固
set_sshd_option /etc/ssh/sshd_config "AllowAgentForwarding" "no"
set_sshd_option /etc/ssh/sshd_config "GatewayPorts" "no"
set_sshd_option /etc/ssh/sshd_config "PermitTunnel" "no"
set_sshd_option /etc/ssh/sshd_config "MaxStartups" "10:30:100"
set_sshd_option /etc/ssh/sshd_config "PermitEmptyPasswords" "no"
set_sshd_option /etc/ssh/sshd_config "MaxAuthTries" "5"
set_sshd_option /etc/ssh/sshd_config "X11Forwarding" "no"
set_sshd_option /etc/ssh/sshd_config "Banner" "none"
baseline_ok "SSH 补充加固" "AllowAgentForwarding/GatewayPorts/PermitTunnel/MaxStartups/PermitEmptyPasswords/MaxAuthTries/X11Forwarding/Banner 已配置"

# 如果存在非root用户或已创建devops用户,则禁止使用 root 账号通过 SSH 登录
if [ "$non_root_user_exists" == "Y" ] || [ "$non_root_user_exists" == "y" ] || [ "$need_create_user" == "Y" ] || [ "$need_create_user" == "y" ]; then
    set_sshd_option /etc/ssh/sshd_config "PermitRootLogin" "no"
    baseline_ok "SSH root 登录" "PermitRootLogin 已设置为 no"
else
    baseline_skip "SSH root 登录" "未创建非 root 用户, 保留 SSH 允许 root 用户登录配置"
fi

if command -v sshd &>/dev/null && ! sshd -t; then
    baseline_warn "SSH 服务" "sshd_config 配置验证失败, 未重启 sshd, 请手动检查"
elif systemctl restart sshd; then
    baseline_ok "SSH 服务" "sshd 配置验证通过并已重启"
else
    baseline_warn "SSH 服务" "sshd 配置已写入, 但服务重启失败, 请手动检查"
fi

# 设置登录超时 300 秒
set_shell_var /etc/profile "TMOUT" "300"
append_line_if_missing 'export TMOUT' /etc/profile
baseline_ok "登录超时" "TMOUT 已设置为 300 秒并导出"

# 设置全局历史记录保存条数为10条
set_shell_var /etc/profile "HISTSIZE" "10"
set_shell_var /etc/profile "HISTFILESIZE" "10"
baseline_ok "历史命令保留条数" "HISTSIZE 与 HISTFILESIZE 已设置为 10"

# 设置历史命令时间戳
set_shell_var /etc/profile "HISTTIMEFORMAT" "'%F %T '"
append_line_if_missing 'export HISTTIMEFORMAT' /etc/profile
baseline_ok "历史命令时间戳" "HISTTIMEFORMAT 已设置并导出"

# /tmp 权限控制: 已是独立挂载点/tmpfs 时直接补 fstab 并 remount; 否则提示自绑定
harden_tmp_mount

# 退出删除临时的历史记录
for user_home in /root /home/*; do
    logout_file="$user_home/.bash_logout"
    if [ ! -f "$logout_file" ]; then
        touch "$logout_file"
    fi
    if ! grep -q "^rm -f ~/.bash_history" "$logout_file"; then
        echo "rm -f ~/.bash_history" >> "$logout_file"
    fi
done
baseline_ok "退出清理历史记录" "已确保 /root 和 /home/* 的 .bash_logout 清理 ~/.bash_history"


# 加固:查找并备份 .netrc 文件
find / -maxdepth 3 -name .netrc 2>/dev/null | while read file; do
    backup_file_no_overwrite "$file"
done
baseline_ok "危险信任文件" "已扫描并备份 .netrc 文件"

# 加固:查找并备份 .rhosts 文件
find / -maxdepth 3 -name .rhosts 2>/dev/null | while read file; do
    backup_file_no_overwrite "$file"
done
baseline_ok "危险信任文件" "已扫描并备份 .rhosts 文件"

# 加固:查找并备份 hosts.equiv 文件
find / -maxdepth 3 -name hosts.equiv 2>/dev/null | while read file; do
    backup_file_no_overwrite "$file"
done
baseline_ok "危险信任文件" "已扫描并备份 hosts.equiv 文件"

baseline_info "文件权限" "开始执行系统目录和配置文件权限加固"

# 设置目录权限
chmod 750 /etc/rc0.d
chmod 750 /etc/rc1.d
chmod 750 /etc/rc2.d
chmod 750 /etc/rc3.d
chmod 750 /etc/rc4.d
chmod 750 /etc/rc5.d
chmod 750 /etc/rc6.d
chmod 750 /etc/rc.d/init.d
baseline_ok "启动脚本目录权限" "/etc/rc*.d 与 /etc/rc.d/init.d 已设置为 750"

# 设置配置文件权限(仅在文件存在时)
[[ -f /etc/grub.conf && ! -L /etc/grub.conf ]] && chmod 600 /etc/grub.conf
[[ -f /boot/grub/grub.conf ]] && chmod 600 /boot/grub/grub.conf
[[ -f /etc/lilo.conf ]] && chmod 600 /etc/lilo.conf
[[ -f /etc/grub2.cfg && ! -L /etc/grub2.cfg ]] && chmod 600 /etc/grub2.cfg
[[ -f /boot/grub2/grub.cfg ]] && chmod 600 /boot/grub2/grub.cfg
baseline_ok "引导配置权限" "grub/lilo 配置文件已按存在情况设置为 600"

# 设置其他关键配置文件权限
[[ -d /etc/security ]] && chmod 600 /etc/security
[[ -f /etc/xinetd.conf ]] && chmod 600 /etc/xinetd.conf
[[ -f /etc/inetd.conf ]] && chmod 600 /etc/inetd.conf
baseline_ok "关键配置权限" "/etc/security、xinetd.conf、inetd.conf 已按存在情况设置权限"

chmod 644 /etc/group
chmod 644 /etc/services
chmod 644 /etc/passwd
baseline_ok "账号基础文件权限" "/etc/group、/etc/services、/etc/passwd 已设置为 644"

[[ -f /etc/shadow ]] && chmod 600 /etc/shadow

# 设置常见文件审计
baseline_info "审计规则" "开始设置常见文件审计策略"
file_audit="/etc/audit/rules.d/audit.rules"
if [ ! -f "$file_audit" ]; then
    baseline_warn "审计规则" "文件不存在: $file_audit, 将创建文件"
    touch "$file_audit"
fi

append_rule() {
    local rule="$1"
    # 使用 -- 确保模式不会被误解析为选项
    if ! grep -Fxq -- "$rule" "$file_audit"; then
        echo "$rule" >> "$file_audit"
    fi
    baseline_ok "审计规则" "已确保规则: $rule"
}

append_rule "-w /etc/sysconfig -k SYSCONFIG"
append_rule "-w /etc/audit/audit.rules -p wa -k AUDIT_RULES"
append_rule "-w /etc/audit/auditd.conf -p wa -k AUDIT_CONF"
append_rule "-w /usr/bin/vpnc -k VPNC -p x"
append_rule "-w /etc/group -k PASSWD"
append_rule "-w /etc/passwd -k PASSWD"
append_rule "-w /etc/shadow -k PASSWD"
append_rule "-w /etc/firewalld/ -p wa -k FIREWALL_CONFIG"
append_rule "-w /etc/sysconfig/iptables -p wa -k IPTABLES_CONFIG"
append_rule "-w /etc/sysconfig/ip6tables -p wa -k IPTABLES_CONFIG"

audit_boot_fix_applied="n"
if grep -qw "audit=0" /proc/cmdline 2>/dev/null; then
    read -p "检测到当前内核启动参数包含 audit=0, 是否改为 audit=1? 修改后需重启生效 (输入 y/Y 或 n/N, 回车默认N): " fix_audit_boot
    if [ "$fix_audit_boot" == "Y" ] || [ "$fix_audit_boot" == "y" ]; then
        if enable_audit_kernel_arg; then
            audit_boot_fix_applied="y"
            baseline_ok "审计启用" "已将内核启动参数 audit=0 调整为 audit=1, 重启后生效"
        else
            baseline_warn "审计启用" "自动修改内核启动参数失败或 grubby 不可用, 请手动移除 audit=0 并设置 audit=1"
        fi
    else
        baseline_warn "审计启用" "保留 audit=0, auditd 当前无法启用, 基线检查可能继续不通过"
    fi
fi

# 重新加载审计规则
audit_load_output=$(augenrules --load 2>&1)
audit_load_status=$?
if [ "$audit_load_status" -eq 0 ]; then
    baseline_ok "审计规则" "日志审计策略配置完成并已加载"
elif grep -qw "audit=0" /proc/cmdline 2>/dev/null; then
    if [ "$audit_boot_fix_applied" == "y" ]; then
        baseline_warn "审计规则" "当前会话仍受 audit=0 影响, 规则已写入; 重启后 audit=1 生效再验证"
    else
        baseline_warn "审计规则" "当前内核启动参数包含 audit=0, auditd 无法启用; 规则已写入, 需移除 audit=0 或设置 audit=1 后重启"
    fi
elif echo "$audit_load_output" | grep -Fq "No change"; then
    baseline_ok "审计规则" "日志审计策略已是最新, 无需重新加载"
else
    baseline_warn "审计规则" "日志审计规则写入完成, 但 augenrules --load 执行失败: ${audit_load_output}"
fi

baseline_ok "完成" "安全基线配置已完成, 请验证并重新启动系统以确保设置生效"

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐