自动化脚本:上架首小时的幂等初始化
新服务器上架第一小时的标准动作写成幂等脚本:跑十遍与跑一遍结果相同。本文给出完整可复制的 first-hour-init 脚本骨架(SSH 加固、防火墙、更新策略、基线采集),以及幂等设计的三条原则。
手工初始化服务器的问题不在慢,在不可复现:十台机器十个样子,出事时没人说得清哪台改过什么。解法是把上架第一小时的标准动作写成幂等脚本——跑十遍与跑一遍结果相同,新机器一条命令到位。本篇给出可直接抄走的脚本骨架与设计原则。本文属于安全基线中心系列的工具分支;每个动作的原理与清单在SSH 加固清单与防火墙基线。
幂等的三条设计原则
- 检查后动作,不是盲目动作:改配置前先 grep 目标状态(已是目标值就跳过)——
grep -q '^PasswordAuthentication no' sshd_config || sed -i ...的模式贯穿全脚本; - 追加用标记块,不整文件覆盖:自定义配置写进带标记的块(# BEGIN ax-init / # END ax-init),重跑时先删旧块再写新块——不动发行版原生行;
- 失败要响,成功要静:
set -euo pipefail起手,每步失败立即退出并留日志;成功路径只输出摘要——脚本的输出是审计记录,不是噪音。
脚本骨架:first-hour-init.sh(可直接抄)
#!/usr/bin/env bash
# first-hour-init.sh —— 上架首小时幂等初始化(Debian/Ubuntu 口径,其他发行版改包管理器)
set -euo pipefail
LOG=/var/log/first-hour-init.log; exec >>("$LOG") 2>&1; echo "=== $(date -u +%FT%TZ) 开始 ==="
# ── 0. 前置:必须以 root 或 sudo 运行;先确认 SSH 密钥已可用再禁密码 ──
[ "$(id -u)" = 0 ] || { echo "需要 root"; exit 1; }
[ -s /root/.ssh/authorized_keys ] || { echo "❌ 先配置密钥再运行(禁用密码前必须验证密钥可登录)"; exit 1; }
# ── 1. SSH 加固(幂等:sshd_config.d 独立文件,存在即覆盖)──
cat > /etc/ssh/sshd_config.d/99-hardening.conf <<'EOF'
PermitRootLogin prohibit-password
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
LoginGraceTime 20
EOF
systemd-delta 2>/dev/null | head -1 || true
systemctl reload ssh 2>/dev/null || systemctl reload sshd
# ── 2. 防火墙基线:默认拒绝入站 + 最小白名单(幂等:先重置再声明)──
if command -v ufw >/dev/null; then
ufw --force reset >/dev/null
ufw default deny incoming; ufw default allow outgoing
ufw allow 22/tcp comment 'ssh' # 改了 SSH 端口的同步改这里
[ -n "${WEB:-}" ] && { ufw allow 80/tcp; ufw allow 443/tcp; }
ufw --force enable; ufw status numbered
fi
# ── 3. fail2ban(存在即启用,配置用标记块幂等写入)──
if command -v fail2ban-client >/dev/null; then
printf '[sshd]
enabled = true
maxretry = 5
findtime = 600
bantime = 3600
' > /etc/fail2ban/jail.d/sshd.local
systemctl enable --now fail2ban; fail2ban-client status sshd | head -5
fi
# ── 4. 自动安全更新(只装补丁,不跨大版本——纪律见正文)──
if command -v apt >/dev/null; then
apt-get install -y unattended-upgrades >/dev/null
dpkg-reconfigure -f noninteractive unattended-upgrades >/dev/null || true
fi
# ── 5. 基线采集(装机快照:日后「变没变」的对照物)──
mkdir -p /var/log/baseline
{ echo "# $(date -u +%FT%TZ)"; uname -a; lscpu | grep -E "Model name|^CPU\(s\)"; free -h; df -h /;
ss -tlnp; ip -brief addr; } > /var/log/baseline/boot-$(date +%F).txt
# ── 6. 时间同步与健康行 ──
timedatectl set-ntp true 2>/dev/null || true
echo "=== 完成:SSH/防火墙/fail2ban/更新/基线 五段全部就位 ==="
用法与验证
# 上传并执行(新机器第一条命令):
scp first-hour-init.sh root@新服务器IP:/root/ && ssh root@新服务器IP "bash /root/first-hour-init.sh"
# 幂等验证:再跑一遍,结果应无差异、无报错:
ssh root@新服务器IP "bash /root/first-hour-init.sh && tail -3 /var/log/first-hour-init.log"
# 效果验证(从外部):
ssh -o PreferredAuthentications=password 新服务器IP exit # 应被拒绝(密码登录已禁)
nc -zv -w3 新服务器IP 22 && echo "22 开(预期)"
nc -zv -w3 新服务器IP 3306 2>/dev/null || echo "3306 关(预期:默认拒绝生效)"
脚本之外的三个配套动作
- 版本化:脚本进你的 Git 仓库(私有库即可),每次修改留 commit——「谁在什么时候改了初始化逻辑」有账可查;配套的作品版(带注释的教学版)在创作社区的 first-hour-init.sh 作品页;
- 分层扩展:首小时脚本只管安全基线;业务栈(Nginx/数据库/应用)另写第二层脚本,两层分开跑——基线层稳定不动,业务层随需求迭代;多机器用 Ansible 把两层编排起来(单脚本是起点不是终点);
- 恢复演练联动:备份脚本与恢复演练的自动化口径见面板与备份——初始化的最后一块拼图是「这台机器没了,多久能重建」:有 first-hour-init + 备份 + 配置仓库,答案是一台新机器的开通时间——这个「重建时间」本身就是你架构成熟度的最好度量。
第二层脚本:业务栈的最小样例
基线层之上,业务栈脚本同样守幂等纪律。以最常见的 Nginx + 静态站为例,骨架三段:①安装段——command -v nginx || apt-get install -y nginx(已装即跳过);②配置段——站点配置写成独立文件放入 sites-available 并软链到 sites-enabled,配置内容用「校验后重载」模式:nginx -t && systemctl reload nginx(校验失败不重载,坏配置进不了生产);③验证段——脚本末尾自测:curl -sf -o /dev/null http://127.0.0.1/ && echo "nginx ok",失败即非零退出。三层结构(装/配/验)适用于一切业务栈脚本;机器多起来后把两层脚本翻译成 Ansible role(tasks 即命令、handlers 即重载、inventory 即机器清单)——脚本是 Ansible 的草稿,先写对脚本再谈编排。
常见翻车点清单
- 禁密码前没验证密钥 → 把自己锁在门外(脚本第 0 段的前置检查就是防这个;真锁了用机房 IPMI/VNC 通道救回);
- ufw allow 顺序错 → 改过 SSH 端口的机器先 allow 新端口再 deny 22,顺序反了同样锁门外;
- sed 盲改配置文件 → 重跑一次改一次,配置越跑越花——用独立 drop-in 文件(sshd_config.d 模式)替代 sed 直改;
- 把密钥/密码写进脚本 → 脚本进 Git 就是泄露:凭据永远走环境变量或部署时的 secret 注入,脚本里只留占位符。
下一步
把脚本抄走、按你的发行版与端口改三行、进你的仓库——第一台机器开始就用幂等初始化,运维债从源头少一半。安全动作的原理与完整清单在安全基线中心(SSH 十二动作/防火墙基线/备份演练三篇是一组);新机器开通入口在在售配置(入门 VPS $5.34/月起,练手成本最低的沙盒)。