openEuler 24.03 LTS 服务器初始化实践:从系统配置到安全基线自动化
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 "完成" "安全基线配置已完成, 请验证并重新启动系统以确保设置生效"
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)