为什么自动安全更新这件事,配置错了比不配更危险
大多数人的服务器安全更新流程是这样的:某天看到一条 CVE 通告,SSH 上去跑一遍 apt update && apt upgrade -y,回车按到底,看到 Setting up ... 滚完就关掉窗口。这套流程能撑住一个博客站,但它有一个致命问题——你不可能每天都这么做。真实情况是,一次升级之后三周没人再登录服务器,期间内核和 OpenSSL 都出了高危补丁,而你的机器还停在旧版本上。
unattended-upgrades 就是为这件事存在的。它是 Debian/Ubuntu 官方的自动更新组件,通过 systemd timer 每天触发,只安装安全源里的补丁。听起来是「装上就完事」的那种工具,但实际上它是本文见过最容易配错的系统组件之一:配置错了,要么静默不干活(你以为在自动打补丁,其实三周没更新过一次),要么干得过头(半夜自动重启了你的生产机,第二天早上数据库对不上账)。
下面按「它到底怎么工作 → 只装安全补丁的正确配置 → 内核与重启策略 → 怎么证明它真的在跑 → 出事怎么查」的顺序讲。全部命令在 Debian 12 上实测。
第一层:它由什么驱动,为什么你配了却感觉没生效
先看清楚它的执行链。很多事情想不通,是因为把它当成了一个守护进程。
# 查看 timer 状态
systemctl list-timers 'apt-daily*' --all
# 典型输出(注意 NEXT 和 LAST 两列)
# NEXT LEFT LAST PASSED UNIT
# Tue 2026-10-20 06:12:03 CST 5h left Mon 2026-10-19 06:21:44 CST 18h ago apt-daily-upgrade.timer
# Tue 2026-10-20 01:37:11 CST 1h left Mon 2026-10-19 00:59:02 CST 23h ago apt-daily.timer两个 timer 分工不同:apt-daily.timer 只负责 update(刷新包索引),apt-daily-upgrade.timer 负责 unattended-upgrade。很多人只看到第一个在跑,就以为配置生效了,其实真正干活的是第二个。
三个高频「配了没生效」的原因:
- timer 被 mask 了。
systemctl status apt-daily-upgrade.timer显示masked或inactive (dead)。云厂商的镜像(尤其是某些为了缩短首次开机时间的模板)会主动 mask 这两个 timer。systemctl unmask之后还要enable --now。 - RandomizedDelaySec 让你的执行时间飘忽不定。 默认配置里有
RandomizedDelaySec=12h(新版是 1h 或 12h,视发行版而定),意思是「在触发点之后随机延迟这么久再跑」。所以你半夜盯日志看不到任何活动是正常的,它可能拖到下午才执行。这一点在做故障复现时特别坑。 - 索引过期导致整个升级被跳过。
unattended-upgrade默认会检查/var/lib/apt/periodic/update-success-stamp,如果上次apt update超过一定时间(默认相关配置APT::Periodic::MaxAge),它会先自己跑一次 update;但如果 update 因为网络或 GPG 问题失败,整个升级就静默退出,退出码是 0。这就是「看起来跑了,其实没装」的来源。
验证 timer 是否真的在跑,最直接的方式不是看 timer 状态,而是看历史执行结果:
# 看最近几次执行的时间点
systemctl show apt-daily-upgrade.timer -p LastTriggerUSec -p NextElapseUSecRealtime
# 看服务本身的历史运行记录
systemctl status apt-daily-upgrade.service --no-pager -l | head -30第二层:只装安全补丁的正确写法
Debian 系的默认配置文件是 /etc/apt/apt.conf.d/50unattended-upgrades,但不要直接改它。这个文件由包管理器维护,下次升级时可能被覆盖或产生 .dpkg-dist 冲突。正确做法是新建一个编号更大的文件来覆盖,数字越大优先级越高:
sudo tee /etc/apt/apt.conf.d/52unattended-upgrades-local <<'EOF'
// 只允许官方安全源,不碰 backports 和第三方源
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}-security";
"${distro_id}ESMApps:${distro_codename}-apps-security";
"${distro_id}ESM:${distro_codename}-infra-security";
};
// 显式清空黑名单再按需添加,避免默认值里莫名其妙的排除
Unattended-Upgrade::Package-Blacklist {
// "nginx";
};
// 自动处理配置文件冲突:保留本机版本
Unattended-Upgrade::DpkgOptions "-o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold";
// 自动清理不再需要的依赖
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
// 邮件通知(需要本机有 MTA,见下文)
Unattended-Upgrade::Mail "root";
Unattended-Upgrade::MailReport "on-change";
EOF几个关键点值得展开:
${distro_id}ESM* 那两行是什么。 Ubuntu 的扩展安全维护源。如果机器上没订阅 ESM,这两行不会报错,只是匹配不到东西。加着无害,删了也无所谓,但保留它们能让脚本在订阅了 ESM 的机器上自动生效。
--force-confold 是保命的。 没有这个参数,当安全补丁同时改动了某个配置文件(比如 openssh-server 更新 sshd_config、nginx 更新默认配置),dpkg 会在无 tty 的环境下直接放弃操作,然后整个 unattended-upgrade 就卡在那里直到超时被杀。更糟的情况是它选了默认的 --force-confnew,把你的自定义配置覆盖掉。用 confold 保留本机版本,代价是你得自己关注 .dpkg-dist 文件——这是可接受的,因为静默覆盖配置的代价大得多。
按 origin 而不是按包名过滤。 很多人写:
Unattended-Upgrade::Package-Blacklist {
"mysql-server";
"php8.2-fpm";
};这个写法的问题是它把整个包永久排除在安全更新之外。MySQL 出高危漏洞时,你的机器不会动。更精细的做法是用 Package-Whitelist(注意这是较新版本才有的键,Debian 12 的版本支持),或者反过来——只在极少数确实不能自动更新的包上用黑名单,并且落实一条「手工检查该包的 CVE」的日历提醒。
改完配置必须语法检查,否则一个拼错的引号会让整个文件失效,而 unattended-upgrade 通常只打一行 warning 就继续跑:
# 这条命令会同时检查 50unattended-upgrades 和你的覆盖文件
sudo unattended-upgrade --dry-run --debug 2>&1 | head -60看 --debug 输出的开头部分,它会打印最终生效的 Allowed-Origins 列表和 Blacklist。这是唯一能确认你写的覆盖真的被读进去的方法。
第三层:内核更新与重启——最容易翻车的地方
安全补丁里有一类特殊的东西:内核。内核补丁装进磁盘了,但只有重启才生效。这就引出一个经典问题:自动更新到底该不该自动重启?
默认情况下 unattended-upgrades 不会重启。配置项是 Unattended-Upgrade::Automatic-Reboot,默认 false。这意味着你的机器可能跑了 400 天没重启,期间打了 20 个内核安全补丁,实际生效的仍是 400 天前的那个——你以为修好了,其实漏洞还在。这是比「自动重启」更普遍的安全隐患。
但直接开 Automatic-Reboot "true" 也很危险:它会在装完内核后立刻重启,不管现在几点、有没有人在跑批处理。正确做法是配合时间窗口:
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:30";
Unattended-Upgrade::Automatic-Reboot-WithUsers "false";含义:装完内核后,等到 04:30 再重启;如果那个时刻有登录用户(WithUsers "false"),就不重启。对单机博客站来说这个配置完全够用。
但「等到某个时间点」的逻辑有个陷阱:如果内核是在 05:00 装完的,Automatic-Reboot-Time 04:30 会等到下一个 04:30,也就是 23.5 小时后——那台机器实际上一天之后才重启。这没错,但如果你在 06:00 发现网站挂了、以为是重启引起的,去查 06:00 前的日志会一无所获。看重启记录要看 uptime 和 last reboot,不要靠猜。
另一个必须检查的点:重启之后新内核有没有真的被引导。 装了新内核并重启,不保证新内核在跑。Debian 的 grub 通常会自动选最新的,但如果机器上有 LVM、加密盘、或者手工改过 grub.cfg,可能会继续引导旧内核。
# 当前运行的内核版本
uname -r
# 已安装的最新内核
dpkg -l | grep -E '^ii linux-image' | awk '{print $2}' | sort -V | tail -3
# 两者不一致 = 重启了但没进新内核,需要检查 grub
sudo grep -E '^menuentry' /boot/grub/grub.cfg | head -5
sudo update-grub还有一件经常被忽略的事:needrestart。内核之外,libssl、glibc 这类库的安全补丁也需要重启相关进程才生效。装了补丁但 nginx 还在跑旧的 libssl,等于没修。needrestart 会告诉你哪些服务需要重启:
sudo apt install needrestart -y
sudo needrestart -b -r l # -b 批处理模式,-r l 只列服务
# 输出形如:
# NEEDRESTART-VER: 3.6
# NEEDRESTART-KCUR: 6.1.0-31-cloud-amd64
# NEEDRESTART-KEXP: 6.1.0-31-cloud-amd64
# NEEDRESTART-MESSAGES-MAX: 5
# NEEDRESTART-SVC: nginx.service
# NEEDRESTART-SVC: php8.2-fpm.service把这个命令挂到每次更新之后跑一遍,输出里有 SVC 行就去重启对应服务。如果开了 Unattended-Upgrade::Automatic-Reboot,needrestart 本身已经能自动处理,但默认它在非交互环境下只会提示。要让它自动重启服务,得改 /etc/needrestart/conf.d/ 下的策略。
第四层:怎么证明它真的在干活
这是本文的核心。绝大多数人配完之后从没验证过,直到某天手动 apt upgrade 发现积压了 80 个包,才意识到自动更新根本没跑。
方法一:看日志。 主日志在 /var/log/unattended-upgrades/,两个文件:unattended-upgrades.log(它自己的决策过程)和 unattended-upgrades-dpkg.log(dpkg 的实际操作)。注意 Debian 12 之后,systemd 环境的输出也会进 journald。
# 最近一次执行的完整决策记录
sudo tail -50 /var/log/unattended-upgrades/unattended-upgrades.log
# 关键行长这样:
# 2026-10-19 06:21:51,203 INFO Initial whitelist
# 2026-10-19 06:21:52,110 INFO Adjusting candidate version: libssl3=3.0.16-1~deb12u1
# 2026-10-19 06:21:53,004 INFO Packages that will be upgraded: libssl3 openssl
# 2026-10-19 06:21:58,772 INFO All upgrades installed判断标准:如果日志里每天都有一条「No packages found that can be upgraded unattended」,那是健康的(确实没补丁)。如果连着几天完全没有新行,那就是它没跑。
方法二:模拟运行。 --dry-run 会真实计算一遍「如果现在跑,会装什么」,但不落地:
sudo unattended-upgrade --dry-run --debug 2>&1 | grep -E 'PKGS|will be upgraded|candidate'如果 dry-run 显示一堆待装的安全包,而日志显示昨天「All upgrades installed」,那说明有东西在阻塞——最常见的是 dpkg 锁没释放,或者有 held 包。
# 检查被 hold 住的包,它们会阻塞整批升级
apt-mark showhold
# 检查有没有卡住的 dpkg 进程
ps aux | grep -E 'dpkg|apt' | grep -v grep方法三:外部哨兵。 这是最可靠的一层。日志可以被截断、可以被静默、可以从没写过。真正能证明「这台机器的安全补丁是最新的」的,是从外部看它的版本状态。
# 在监控机上跑:SSH 过去取补丁积压数量
#!/bin/bash
HOST="$1"
N=$(ssh -o BatchMode=yes -o ConnectTimeout=8 "$HOST" \
"apt-get -s -o Debug::NoLocking=1 upgrade 2>/dev/null | grep -c '^Inst .*security'")
STAMP=$(ssh -o BatchMode=yes -o ConnectTimeout=8 "$HOST" \
"stat -c %Y /var/lib/apt/periodic/update-success-stamp 2>/dev/null")
AGE_H=$(( ( $(date +%s) - ${STAMP:-0} ) / 3600 ))
echo "host=$HOST security_pending=$N index_age_h=${AGE_H}"
[ "${AGE_H}" -gt 48 ] && echo "ALERT: 包索引超过 48 小时未刷新,自动更新可能已停摆" >&2
[ "${N}" -gt 5 ] && echo "ALERT: 安全补丁积压 ${N} 个" >&2这个脚本的价值在于它不信任机器上的任何日志,直接看「现在有多少安全补丁待装」和「包索引多久没刷新了」。索引超过 48 小时没刷新,基本可以断定 apt-daily.timer 出问题了,哪怕 unattended-upgrades.log 看起来一切正常。
第五层:半夜自动重启了,怎么查清是谁干的
真实事故场景:早上发现站点 502,登录机器发现 uptime 只有 3 小时,重启发生在 04:31。unattended-upgrades 开启了 Automatic-Reboot-Time "04:30"。但你要确认的不是「可能」,而是「确实是它」。
# 1. 确认重启时刻
last reboot | head -3
# 2. 该时刻前后 unattended-upgrades 的日志
sudo awk '$0 >="2026-10-19 04:25" && $0 <= "2026-10-19 04:35"' \
/var/log/unattended-upgrades/unattended-upgrades.log
# 3. 看是谁调用了 reboot —— 关键线索
sudo grep -E 'reboot|shutdown' /var/log/syslog | tail -20
# 4. 如果日志已被轮转覆盖,看 journald
sudo journalctl --since "2026-10-19 04:25" --until "2026-10-19 04:35" | grep -iE 'shutdown|reboot|unattended'判断要点:unattended-upgrades 触发的重启,在 syslog 里的特征是先有 unattended-upgrade 的 Installing... 记录,紧接着一条 systemd-logind: System is rebooting 或者 shutdown[1]: Rebooting,两者间隔通常在几秒内。如果是云平台强制重启(宿主机维护),会出现 Booting Linux 但前面对应时段没有任何 shutdown 记录——日志直接断在一半。这个区别很重要:前者是你自己配置的问题,后者要找服务商。
如果确认是自动重启导致的业务中断,处理方向不是关掉自动重启(那等于放弃内核补丁),而是:
- 把
Automatic-Reboot-Time挪到业务低谷,并用访问日志确认真低谷在哪,而不是凭感觉; - 确认所有服务都是
systemctl enable过的——自动重启暴露的最大问题通常是某个服务没设开机自启,重启后就没起来; - 检查
Restart=on-failure和RestartSec,给启动依赖(数据库先于应用)留出足够的重试空间。
第六层:mail 通知为什么从来不响
Unattended-Upgrade::Mail "root" 这个配置在 90% 的机器上不工作,原因很简单:本机没有 MTA,mail 命令根本不存在。日志里会出现一行 Unable to send mail 然后继续跑,没人注意到。
# 检查
which mail sendmail
dpkg -l | grep -E 'postfix|exim4|msmtp|ssmtp'轻量替代方案是 msmtp,不占资源,走外部 SMTP:
sudo apt install msmtp msmtp-mta bsd-mailx -y
# /etc/msmtprc
# defaults
# auth on
# tls on
# tls_trust_file /etc/ssl/certs/ca-certificates.crt
# logfile ~/.msmtp.log
#
# account default
# host smtp.example.com
# port 587
# from server@example.com
# user server@example.com
# password xxxxxx
#
# account default : default
chmod 600 /etc/msmtprc
echo "test body" | mail -s "msmtp test from $(hostname)" you@example.com但更推荐的做法是不要依赖邮件。邮件通知在凌晨发出去,你要到早上才看到,中间可能已经过了 6 小时。上一节那个外部哨兵脚本,接一个 webhook 或者 ntfy 推送,响应时间是分钟级而不是小时级。自动更新这件事的本质诉求是「万一它停了我要尽快知道」,用邮件满足不了这个诉求。
一份可以直接抄的完整配置
把上面所有内容合成一份可用的最小配置。适用于单机、非集群、无编排系统的个人站长服务器(Debian 12 / Ubuntu 22.04+):
# /etc/apt/apt.conf.d/20auto-upgrades —— 控制频率
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";
APT::Periodic::Download-Upgradeable-Packages "1";
# /etc/apt/apt.conf.d/52unattended-upgrades-local —— 控制行为
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}-security";
"${distro_id}ESMApps:${distro_codename}-apps-security";
"${distro_id}ESM:${distro_codename}-infra-security";
};
Unattended-Upgrade::Package-Blacklist {};
Unattended-Upgrade::DpkgOptions "-o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:30";
Unattended-Upgrade::Automatic-Reboot-WithUsers "false";
Unattended-Upgrade::MinimalSteps "true";MinimalSteps "true" 值得单独说一句:它会先把所有包下载完,再进 dpkg 阶段,减少「下载到一半网络断了,dpkg 状态混乱」的可能。代价是磁盘要预留出所有待装包的空间——对/var 分区紧张的小 VPS,这一项要自己权衡。
最后:自动更新不是「设了就不用管」
写这篇文章的起因是一个很典型的误判。有台机器配了自动更新,一年没管,某次登录发现积压了 137 个包,其中 42 个是安全更新。查日志才发现 unattended-upgrades 三个月前因为一次 DpkgOptions 的引号写错,每次启动就报一行 Unparsed option 然后退出。systemctl status 显示绿色的 inactive (dead)——在 systemd 里,一次性服务的成功退出就是这个状态,看起来和「已停止」一模一样。
所以真正的结论只有一条:自动更新本身也需要监控。 而监控它的方法,不是看它的日志,是看你机器上「还有多少安全补丁没装」。前者的信息源是被监控对象自己提供的,后者是独立的事实。任何只有内部自述、没有外部验证的自动机制,都会在某个你没注意的日子里静默失效,而失效那天恰好是你最需要它工作的时候。