一、巡检为什么要分层

巡检不是为了“跑一堆命令”,而是为了回答三个问题:站点能不能访问、服务是不是正常、机器有没有被拖垮或被人盯上。

我把它拆成两个视角:

  • 外部视角:从公网用 curl 访问真实域名,看到的是读者实际体验。它不关心服务器内部怎么配置,只关心“请求能不能拿到正确的响应”。
  • 内部视角:SSH 登录服务器,看证书、服务、防火墙、负载、日志。它能解释“为什么外部看到的是这个结果”。

两个视角必须都看。只看外部,页面 200 也可能是缓存或旧内容;只看内部,服务全绿也可能因为证书过期、DNS 或防火墙问题,读者根本打不开。

下面的命令分两类:

  • 在本地 Windows PowerShell 执行的,标 PowerShell;
  • 在服务器执行的,标 bash(通过 SSH 进入服务器后执行)。

本文命令与数值来自 2026-11-08 的实际采集。凡是未在本机验证过的写法,都会单独标注“未在本机验证”。


二、第一层:可达性

2.1 命令

curl.exe -s -o NUL -w "%{http_code} %{time_total}`n" https://notes.hokachi.top/

一次检查多个地址:

$urls = @(
  'https://notes.hokachi.top/',
  'https://notes.hokachi.top/internal/ncat-reverse-shell/',
  'https://notes.hokachi.top/linux/ssh-hardening-practice/',
  'https://notes.hokachi.top/contact/',
  'https://notes.hokachi.top/favicon.svg'
)
foreach ($u in $urls) {
  $r = curl.exe -s -o NUL -w "%{http_code} %{time_total}" $u
  Write-Output "$r  $u"
}

2.2 参数拆解

参数含义
curl.exeWindows 自带的真正 curl。必须写 .exe,原因见“三个实操陷阱”
-ssilent,静默模式:不显示进度条和错误说明,只输出 -w 指定的内容
-o NUL把响应正文写到 NUL(Windows 的空设备),等于丢弃正文,只看状态码
-w "%{http_code} %{time_total}"请求结束后按模板输出:%{http_code} 是 HTTP 状态码,%{time_total} 是从发起到结束的总耗时(秒)
末尾的 URL要访问的地址,注意文章地址结尾有 /

2.3 正常 / 异常 / 先查什么

项目正常异常异常时先查什么
状态码全部 200404、502、503、000404 查页面是否构建/上传;502、503 查 nginx 与后端;000 查 DNS、网络、TLS
耗时多数 0.2~0.4 秒连续多次 > 2 秒看服务器负载、磁盘、nginx error log、网络线路
证书HTTPS 正常完成curl 报证书错误看第二层的到期时间与 SAN

本层判断的是“读者能不能打开”,不是“服务器内部健不健康”。


三、第二层:证书

3.1 命令

certbot certificates | grep -E 'Certificate Name|Expiry Date'

echo | openssl s_client -servername notes.hokachi.top -connect notes.hokachi.top:443 2>/dev/null \
  | openssl x509 -noout -dates -subject -ext subjectAltName

systemctl list-timers --all --no-pager | grep -i certbot

3.2 参数拆解

certbot certificates:列出 certbot 管理的所有证书,包括名称、覆盖域名、到期时间、剩余有效天数和文件路径。

grep -E 'Certificate Name|Expiry Date':

  • -E:使用扩展正则表达式,才能让 | 表示“或”;
  • 这里只保留含 Certificate Name 或 Expiry Date 的行,减少无关输出。

openssl s_client:

  • -servername notes.hokachi.top:发送 SNI,让服务器返回对应域名的证书;不写可能拿到默认证书;
  • -connect notes.hokachi.top:443:连接目标主机和端口;
  • echo |:向 openssl 输入一个空行,让握手流程完整结束,否则命令可能一直等待输入;
  • 2>/dev/null:丢弃 stderr 里的握手过程噪音;
  • | openssl x509:把拿到的证书交给 x509 子命令解析。

openssl x509:

  • -noout:不打印完整 PEM 证书体;
  • -dates:输出 notBefore 和 notAfter;
  • -subject:输出证书主体(通常是 CN);
  • -ext subjectAltName:输出 SAN,即证书实际覆盖的域名列表。

systemctl list-timers --all --no-pager:见第三层。

3.3 正常 / 异常 / 先查什么

项目正常异常异常时先查什么
剩余天数大于 30 天小于 30 天预警,小于 14 天必须处理certbot renew --dry-run 是否成功(该演练本轮未执行)
覆盖域名notes.hokachi.top 与 www.hokachi.top缺少其中一个签发时的 -d 参数、证书 SAN
到期日与上次续期后明显推进多次检查日期不变certbot.timer 是否 active、certbot.service 日志
链验证浏览器与 curl 均正常报证书过期/不受信任系统时间、证书链、SNI

四、第三层:服务状态

4.1 命令

systemctl is-active nginx
systemctl is-enabled nginx
systemctl status nginx --no-pager

systemctl is-active certbot.timer
systemctl is-enabled certbot.timer
systemctl status certbot.timer --no-pager
systemctl list-timers --all --no-pager | grep -i certbot

systemctl is-active fail2ban
systemctl is-enabled fail2ban
systemctl is-active ssh

nginx -t

4.2 is-active、is-enabled、status、list-timers 的区别

命令回答的问题典型输出
systemctl is-active <unit>现在是否在运行active、inactive、failed
systemctl is-enabled <unit>开机时是否自动启动enabled、disabled、masked
systemctl status <unit>综合状态:描述、加载路径、运行时间、最近日志多行详情
systemctl list-timers --all所有定时器及上次/下次触发时间表格

几个细节:

  • is-active 只回答当前运行状态,不代表开机自启;is-enabled 只回答自启配置,不代表现在正在运行。两者要一起看。
  • status 默认会调用分页器,脚本或窄终端里容易卡住,所以加 --no-pager,让它一次性输出。
  • list-timers 默认只显示还会再次触发的定时器;加 --all 才能看到已经停止或不活跃的定时器。
  • nginx -t 只做配置语法检查,不会启动、停止或重载 nginx。

4.3 正常 / 异常 / 先查什么

项目正常异常异常时先查什么
nginxactive、enabled、nginx -t 成功inactive、failed、配置测试失败systemctl status nginx、nginx -t 的具体报错行
certbot.timeractive (waiting)、enabled,有下次触发时间inactive、masked、无下次触发systemctl status certbot.timer、journalctl -u certbot.timer
fail2banactive、enabledinactive、failedsystemctl status fail2ban、fail2ban-client status
SSHssh 与 sshd 均为 active任一异常控制台/救援模式,避免直接断掉远程连接

五、第四层:访问控制

5.1 命令

ufw status numbered
fail2ban-client status sshd
ss -lntp

5.2 参数拆解

ufw status numbered:

  • status:显示防火墙状态和规则;
  • numbered:给每条规则加序号,便于后续按序号删除或调整。

fail2ban-client status sshd:

  • fail2ban-client:fail2ban 的管理客户端;
  • status:查看运行概况;
  • sshd:指定 jail 名称。只写 status 会列出所有 jail。

ss -lntp 四个字母:

参数含义
-l只显示处于 LISTEN(监听)状态的套接字
-n以数字显示地址和端口,不解析成服务名
-t只显示 TCP
-p显示占用端口的进程(需要 root 才能看到别人的进程)

5.3 正常 / 异常 / 先查什么

项目正常异常异常时先查什么
ufwStatus: active,只放行 22/80/443(IPv4 与 IPv6)出现未知端口放行、防火墙 inactive规则是否被误加、是否有其他防火墙层
fail2banactive,Currently banned 为 0 或少量banned 突然增多、jail 消失fail2ban-client status、/var/log/fail2ban.log
监听端口22、80、443 对外;53 与 6010 仅本机出现未知的 0.0.0.0 监听ss -lntp 的进程名、systemctl status 对应服务

ss 只是“看谁在监听”,它不改变任何策略;真正决定“谁能进来”的是 ufw 与云厂商安全组。


六、第五层:机器健康

6.1 命令

uptime
nproc

df -h /
df -i /

free -m

ps aux --sort=-%mem | head -6

6.2 参数拆解

uptime:输出当前时间、已运行时长、登录用户数,以及 1、5、15 分钟平均负载。

nproc:输出可用 CPU 核心数。负载要和它对比,单看负载数字没有意义。

df -h / 与 df -i /:

命令看什么单位
df -h /根分区的磁盘块使用率-h 人类可读(G/M)
df -i /根分区的 inode 使用率个数

磁盘满和 inode 满都会导致写不进去文件,但原因不同:小文件极多时,磁盘还有空间,inode 可能先用完。

free -m 的各列:

列含义
total总内存
used已使用
free完全空闲
shared共享内存
buff/cache缓存与缓冲区,可被回收
available估算的可用内存,新进程真正能拿到的量,优先看这一列

ps aux --sort=-%mem | head -6:

  • a:所有用户的进程;
  • u:以用户友好格式显示;
  • x:包含没有终端的进程;
  • --sort=-%mem:按内存占用百分比降序,- 表示倒序;
  • head -6:表头 + 前 5 个进程。

6.3 正常 / 异常 / 先查什么

项目正常异常异常时先查什么
负载长期低于核数15 分钟负载持续高于核数top、慢查询、备份任务、异常进程
磁盘小于 80%大于 80% 预警,大于 90% 危险大日志、缓存、未清理的备份
inode小于 80%大于 80%小文件数量、会话文件、临时文件
内存available 充足小于 200MB 预警,小于 100MB 危险前几名进程、是否被 OOM kill
Swap有 swap 或确实不需要无 swap 且内存吃紧dmesg 中的 OOM 记录

七、第六层:日志与异常流量

7.1 SSH 失败登录

journalctl -u ssh --since "24 hours ago" --no-pager | grep -ciE 'invalid user|failed password|authentication failure|maximum authentication attempts'
grep -ciE 'invalid user|failed password|authentication failure' /var/log/auth.log

参数:

  • journalctl -u ssh:只看 ssh.service 这个单元的日志;
  • --since "24 hours ago":时间窗口为最近 24 小时;也支持 "3 days ago" 这类写法;
  • --no-pager:不分页,适合管道和脚本;
  • grep -c:统计匹配的行数,不是匹配次数;
  • -i:忽略大小写;
  • -E:扩展正则,使 | 表示多个关键词之一。

7.2 nginx 4xx / 5xx

先确认状态码在第 9 列:

awk '{print $1, $9}' /var/log/nginx/access.log | head -5

输出示例(第一列是客户端 IP,第二列是状态码):

39.105.14.55 "-"
45.198.224.125 404
39.105.14.55 404

再用 $9 统计当前日志文件:

awk '$9>=400 && $9<500 {c4++} $9>=500 && $9<600 {c5++} END {print c4+0, c5+0}' /var/log/nginx/access.log

awk 里的 $9 就是 access log 按空格切分后的第 9 列,即 HTTP 状态码。注意这个写法只统计单个文件;如果要跨轮转文件统计近 24 小时,需要按日志里的时间字段过滤,不能简单相加。

7.3 日志目录大小

du -sh /var/log
  • -s:只汇总总大小,不逐个列出子目录;
  • -h:人类可读单位。

7.4 正常 / 异常 / 先查什么

项目正常异常异常时先查什么
SSH 失败几百次/天属常见扫描超过基线 2 倍或短时突增来源 IP、fail2ban 是否封禁
5xx接近 0持续出现nginx error log、后端服务、磁盘是否满
4xx有一定比例,多为扫描超过基线 2 倍或占比过高主要 404 路径,是否出现批量探测
/var/log百 MB 级数百 MB 且快速增长日志轮转配置、是否有服务疯狂报错

4xx 高不等于站点坏了。公网上的扫描器会不断请求不存在的路径,这部分请求会稳定存在,重点看趋势和突增。


八、第七层:内容完整性

8.1 命令

echo IP_COUNT=$(grep -o "206.187.208.43" /var/www/notes/index.html | wc -l)
echo DOMAIN_COUNT=$(grep -o "notes.hokachi.top" /var/www/notes/index.html | wc -l)

参数:

  • grep -o:只输出匹配到的文本,每处一行,而不是输出整行;
  • wc -l:统计行数,也就是出现次数;
  • IP_COUNT 必须为 0;
  • DOMAIN_COUNT 必须大于 0。

8.2 为什么看这两个数字

站点构建时会把 baseURL 写进所有绝对链接。如果 baseURL 误写成服务器 IP,线上首页就会生成大量 http://206.187.208.43/... 链接:

  • 读者点击会 404;
  • HTTPS 证书与 IP 不匹配;
  • 搜索引擎可能收录错误地址。

因此 IP_COUNT=0 是硬性要求,DOMAIN_COUNT>0 代表链接使用域名生成。

8.3 正常 / 异常 / 先查什么

项目正常异常异常时先查什么
IP 计数0大于 0config.toml 的 baseURL、是否重新构建并发布
域名计数大于 00 或明显下降构建配置、上传是否完整、首页模板
页面数量与内容目录大致一致明显减少public/ 是否残留旧产物、是否漏上传

九、2026-11-08 实测基线

以下数值是 2026-11-08 06:24 UTC 左右的一次真实采集,用于建立对照。这些数字会随访问量、日志轮转、系统负载变化,不能当成永久标准;只有量级和阈值有参考意义。

9.1 可达性

URL状态码总耗时
/2000.390634s
/internal/ncat-reverse-shell/2000.249803s
/linux/ssh-hardening-practice/2001.432453s
/contact/2000.229885s
/favicon.svg2000.252819s

9.2 证书

Certificate Name: notes.hokachi.top
Expiry Date: 2027-02-04 16:13:14+00:00 (VALID: 88 days)
notBefore=Nov  6 16:13:15 2026 GMT
notAfter=Feb  4 16:13:14 2027 GMT
SAN: DNS:notes.hokachi.top, DNS:www.hokachi.top
下次触发: 2026-11-08 08:00:45 UTC
上次触发: 2026-11-07 14:34:13 UTC

9.3 服务与访问控制

nginx: active;nginx -t 成功
certbot.timer: active (waiting)、enabled
fail2ban: active、enabled
ssh / sshd: active / active

ufw: active;放行 22、80、443(IPv4 与 IPv6)
fail2ban sshd: Currently failed 9;Total failed 258;Currently banned 0;Total banned 15
监听端口: 22、80、443 对外;53、127.0.0.1:6010 仅本机

9.4 机器健康

uptime: up 2 days, 20:09,load average 0.00 / 0.00 / 0.00
nproc: 2
df -h /: 29G 总量,2.3G 已用,27G 可用,8%
df -i /: 3870720 inodes,105512 已用,3%
free -m: total 1977;used 280;free 410;buff/cache 1286;available 1502
Swap: 0
内存前 5: systemd-journald 2.8%、snapd 1.8%、multipathd 1.3%、fail2ban-server 1.1%、unattended-upgrade 0.9%

9.5 日志与异常流量

SSH 失败类日志(24 小时,journal): 356
SSH 失败类日志(当前 auth.log): 138
nginx 近 24 小时: 总请求 2776;4xx 987;5xx 0
当前 access.log(非 24 小时): 4xx 385;5xx 0
4xx 主要路径: 404 "/" 62 次;400 无法解析 37 次;404 "/favicon.ico" 24 次;400 "/" 22 次;405 "/" 12 次
/var/log 总大小: 102M

9.6 内容完整性

IP_COUNT=0
DOMAIN_COUNT=29

9.7 时间同步

Time zone: Etc/UTC (UTC, +0000)
System clock synchronized: no
NTP service: active
timesync-status: Offset +15.545ms;IPv6 NTP 服务器可用;IPv4 NTP 请求多次超时

这些数字的意义是“当前这个站点的正常量级”,不是所有服务器的通用标准。


十、三个实操陷阱

10.1 PowerShell 里 curl 和 curl.exe 不是一回事

在本机 Windows PowerShell 5 中实测:

Get-Command curl
# Name: curl
# CommandType: Alias
# Definition: Invoke-WebRequest

也就是说,直接输入 curl 调用的是 PowerShell 的 Invoke-WebRequest 别名,不是真正的 curl。它的参数体系完全不同:

  • -s、-o NUL、-w 这些 curl 参数在 Invoke-WebRequest 上不存在;
  • 同样一条命令,写成 curl ... 可能报“找不到参数”,让人误以为网络有问题。

而:

Get-Command curl.exe
# CommandType: Application
# Source: C:\Windows\system32\curl.exe
curl.exe --version
# curl 8.21.0

所以本手册统一写 curl.exe,确保执行的是真正的 curl,输出格式也可控。

10.2 nginx 和 nginx -t 后果不同

  • nginx -t:只检查配置语法和文件引用,不加载新配置,不影响运行中的服务。
  • 裸 nginx:尝试启动 nginx 主进程并加载配置。线上已有 nginx 在运行时,它会因为 80/443 端口被占用而报 bind() ... failed;如果服务本来是停的,它可能启动一个不受 systemd 管理的进程,后续状态就难以判断。

本次采集过程中曾因引号转义问题误执行过一次裸 nginx,输出多行:

nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Unknown error)

当时现有服务没有停止,systemctl is-active nginx 仍是 active,master PID 与启动时间未变。但这只是侥幸没有影响,正确做法是:

  • 只检查配置:nginx -t
  • 要重载配置:systemctl reload nginx
  • 要重启服务:systemctl restart nginx

说明:systemctl reload nginx 与 systemctl restart nginx 本轮未实际执行,仅作为正确操作方向列出;执行前应确认变更窗口。

不要把裸 nginx 当成测试命令。

10.3 systemctl is-active 对不存在的单元也打印 inactive

实测:

systemctl is-active no-such-unit-xyz.timer
# inactive
# 退出码 3

这就带来一个坑:如果在一台根本没有安装 certbot 的机器上执行 systemctl is-active certbot.timer,它同样会输出 inactive。看起来像“服务停了”,实际是“没有这个单元”。

判断方法:

hostname
systemctl status certbot.timer --no-pager
  • hostname 确认你连的是不是目标服务器;
  • systemctl status 对不存在的单元会明确显示 could not be found,对真实存在但停止的单元会显示 inactive (dead),两者可以区分。

另外,is-active 的退出码也能辅助判断:0 表示 active,非 0 表示不是 active;脚本里不要只解析文本。


十一、ufw、fail2ban、curl 的职责边界

工具管什么不管什么典型命令
ufw管“谁能连进来”:按端口和来源放行/拒绝,是常驻的包过滤策略不分析登录行为,不会因为某人失败多次就自动封禁ufw status numbered
fail2ban管“谁敲门太多次”:读日志,发现重复失败后临时封 IP不替代 ufw,不负责常规端口放行;规则通常由它自己写入防火墙fail2ban-client status sshd
curl只是一个访问工具:发请求、看响应不做任何拦截、封禁或策略控制curl.exe -s -o NUL -w "%{http_code}" URL

可以这样记:

  • ufw 是门禁:决定哪些门常开。
  • fail2ban 是保安:看到有人反复撬锁,就临时把人拉黑。
  • curl 是访客:只是去敲门,然后告诉你门开没开、等了多久。

本次基线中,ufw 放行 22/80/443;fail2ban 只启用了 sshd jail,当前封禁 0 个 IP;curl 只负责外部可达性验证。


十二、系统时间与 TLS 证书

TLS 证书有两个时间字段:

  • notBefore:证书生效时间;
  • notAfter:证书到期时间。

浏览器和 curl 在握手时,会用本机或服务器的系统时间去判断“当前是否落在有效期内”。如果系统时间偏差过大:

  • 证书可能被判定为“尚未生效”或“已过期”;
  • HTTPS 握手失败,外部巡检会看到证书错误;
  • ACME 签发/续期的签名与校验也可能失败;
  • 日志时间错乱,排查事件时对不上先后顺序。

查看时间状态:

timedatectl
timedatectl timesync-status
journalctl -u systemd-timesyncd --since "3 days ago" --no-pager | tail -12

timedatectl 的关键字段:

字段含义
Local time本机当前时间
Time zone时区
System clock synchronized系统时钟是否被确认同步
NTP service时间同步服务是否启用
RTC in local TZ硬件时钟是否按本地时区解释

System clock synchronized: no 的含义是:systemd-timesyncd 最近没有确认一次成功同步,不等于“时间一定错了”,也不等于“NTP 服务没开”。本次实测就是:

System clock synchronized: no
NTP service: active
timesync-status: Offset +15.545ms
IPv6 NTP 服务器可用;IPv4 NTP 请求多次超时

偏差只有 15.5 毫秒,证书和日志都不受影响。需要关注的是:

  • 偏差持续增大(例如超过 1 秒);
  • synchronized: no 长期不恢复;
  • timesync-status 里没有可用服务器或 offset 明显异常。

风险判断:

现象风险
偏差毫秒级基本无影响,记录即可
偏差秒级日志对时困难,ACME 可能受影响
偏差分钟级以上证书可能被判无效,续期与握手可能失败
时间跳变定时器触发时间可能偏移,需检查 list-timers

本次证书剩余 88 天,certbot.timer 状态正常,时间偏差毫秒级,因此 synchronized: no 目前只是一个待观察项,不构成证书失守风险。


十三、检查清单

每天 1 分钟

  • 五个关键 URL 全部返回 200,耗时在正常量级
  • certbot.timer 为 active,且有下次触发时间
  • nginx、fail2ban、ssh 均为 active
  • 首页 IP_COUNT=0、DOMAIN_COUNT>0
  • 证书剩余天数没有突然下降

每周 5 分钟

  • df -h / 小于 80%,df -i / 小于 80%
  • free -m 的 available 没有逼近危险线
  • uptime 负载没有持续高于 nproc
  • fail2ban-client status sshd 的 banned 数量没有异常增长
  • nginx 5xx 仍接近 0,4xx 没有明显突增
  • SSH 失败次数没有达到基线 2 倍
  • /var/log 没有异常膨胀
  • timedatectl 时间偏差正常
  • 需要时做一次 certbot renew --dry-run(本轮未执行,属建议演练)

十四、命令速查块

本地 Windows PowerShell

cd d:\ProgramFilesFolder\Project\PersonalNotesBlog\blog-hugo

$urls = @(
  'https://notes.hokachi.top/',
  'https://notes.hokachi.top/internal/ncat-reverse-shell/',
  'https://notes.hokachi.top/linux/ssh-hardening-practice/',
  'https://notes.hokachi.top/contact/',
  'https://notes.hokachi.top/favicon.svg'
)
foreach ($u in $urls) {
  $r = curl.exe -s -o NUL -w "%{http_code} %{time_total}" $u
  Write-Output "$r  $u"
}

服务器

# 证书
certbot certificates | grep -E 'Certificate Name|Expiry Date'
echo | openssl s_client -servername notes.hokachi.top -connect notes.hokachi.top:443 2>/dev/null \
  | openssl x509 -noout -dates -subject -ext subjectAltName

# 服务
systemctl is-active nginx fail2ban ssh
systemctl is-enabled nginx fail2ban
systemctl list-timers --all --no-pager | grep -i certbot
nginx -t

# 访问控制
ufw status numbered
fail2ban-client status sshd
ss -lntp

# 机器健康
uptime
nproc
df -h /
df -i /
free -m
ps aux --sort=-%mem | head -6

# 日志与攻击面
journalctl -u ssh --since "24 hours ago" --no-pager | grep -ciE 'invalid user|failed password|authentication failure'
awk '$9>=400 && $9<500 {c4++} $9>=500 && $9<600 {c5++} END {print c4+0, c5+0}' /var/log/nginx/access.log
du -sh /var/log

# 内容完整性
echo IP_COUNT=$(grep -o "206.187.208.43" /var/www/notes/index.html | wc -l)
echo DOMAIN_COUNT=$(grep -o "notes.hokachi.top" /var/www/notes/index.html | wc -l)

# 时间同步
timedatectl
timedatectl timesync-status

说明:以上命令均来自 2026-11-08 的真实执行记录。awk 那条只统计当前 access.log,不等于“近 24 小时”;要统计 24 小时需按日志时间字段过滤 access.log 与 access.log.1。本文没有给出未验证的跨文件统计脚本。