端口扫描告警:个人站的暴露面巡检与告警收敛

前阵子帮一个做工具站的朋友看后台,发现了一件挺有意思的事:他每天定时巡检的端口只有 22、80、443 三个,监控面板一路绿灯,但站上的 Redis 却被人从公网连上去写了几个 key,还留下了可疑的计划任务。事后复盘才发现,那台机器上 6379 是通的——因为他当初装 Redis 时没改 bind,测试完就忘了关。巡检脚本没发现,是因为它只盯着它认为「应该有端口」的那几个,从不去看「不该出现的端口」。

端口扫描告警这件事,在很多个人站长眼里属于「大厂才需要的东西」,觉得自己的小机器没人惦记。实际情况正好相反:自动化扫描器是不挑目标的,全网每几分钟就有一轮 SYN 扫描从前 65535 个端口上碾过去,你的机器只要 22 开着、密码是弱口令、或者某个服务是默认配置,就是它名单里的一个数字。这篇讲的是怎么在自己的一台或几台 VPS 上,把「谁在扫我、扫到了什么、我该不该紧张」这三件事变成能看见、能告警、又不至于天天吵人的日常。

一、先分清:扫描、探测、撞库是三件不同的事

告警做得不好,最常见的病因是一开始就没分清自己在看什么。日志里出现的可疑流量,大致分三层:

第一层是端口扫描。特征是「同一来源 IP 在很短时间内访问大量不同端口」。这种流量本身不构成入侵,它只是别人在做侦察,甚至连手都没伸进来。全网的 Shodan、Censys 以及各种拿脚本站写扫描器的人每天都在做这件事,你拦不住也不需要拦,只需要知道它发生了。

第二层是服务指纹探测。扫到了 22 端口之后,对方会顺手抓一个 SSH banner,看到 SSH-2.0-OpenSSH_7.4 就知道你是 CentOS 7 的老系统,接下来就会针对性地试已知漏洞或者直接开始爆破。这一层比扫描更值得关心,因为它是「有针对性」的开始。

第三层是实际尝试。也就是 SSH 登录失败、Web 路径爆破、Redis 未授权写文件这类真正会留下后果的动作。这一层才是需要立刻响应的。

为什么要分层?因为告警噪音的来源全在第一层。如果你把所有「陌生 IP 访问了不常见端口」都当告警,那你的手机一天能响两百次,最后的结果一定是你把告警静音了——那么真正的第三层也就没人看了。正确的设计是:第一层只记录、按天汇总;第二层降级记录、异常频次才吭声;第三层立刻告警、并且带上足够上下文让你一分钟内判断真假。

二、先把事实摸清楚:这台机器到底开了哪些端口

做告警之前,必须先有一份「基线」。没有基线,你知道某个端口被扫了也不知道该不该慌。摸端口这件事有三层,三层结果经常不一致,这是排查的第一课。

第一层是进程视角,看的是本机有哪些程序在监听:

ss -tlnp
ss -ulnp

输出里 Local Address:Port 那一列最关键。如果是 0.0.0.0:6379 或 [::]:6379,说明监听在所有网卡上,公网可达;如果是 127.0.0.1:6379,那就只有本机能连。很多「我以为只开了 80/443」的误判,就是没看这一列。

第二层是内核视角,看的是有没有防火墙规则在拦:

nft list ruleset
iptables -S
iptables -t nat -S

注意要连 nat 表一起看。Docker 启动容器时如果用了 -p 6379:6379,它会自己在 nat 表里插一条 DNAT 规则,绕开你手写的 INPUT 链限制。这是经典踩坑:你以为 iptables 只放了 22/80/443,实际上容器端口是全通的。

第三层是外部视角,看的是外面的人能不能真的连上。前两层都是自说自话,只有这一层是事实。从另一台机器(或者手机热点上的笔记本)上做一次定向探测:

nc -zv -w2 你的服务器IP 6379
nc -zv -w2 你的服务器IP 3306

更省事的办法是用 nmap 扫自己常用端口段,但要提醒一句:不要拿 nmap 去扫别人的机器,未经授权扫描在多数服务商条款里都是违规行为,自己的机器扫自己没问题。

三层摸完之后,把结果整理成一张表:端口、进程名、监听地址、防火墙状态、外网是否可达。这张表就是你的基线,后面所有告警都基于它判断「变化」。

三、让机器自己发现扫描:从日志到规则的落地

个人站长手头最顺手的两个工具是 iptables 加 iptables -j LOG,或者更现代的 nftables 加日志前缀。原理很简单:在所有 accept 规则之前插一条 LOG 规则,把没被放过的新连接记下来,这些记录就构成了「被扫」的证据。

nftables 里的写法是这样,先建一个带前缀的日志规则:

nft add rule inet filter input tcp flags syn \
  ct state new log prefix "NEWCONN " level info

这条规则放在 input 链里、允许规则之前,就能把每一个新到的 TCP 连接记进内核日志,日志里会带源 IP、目的端口、TCP 标志。然后用 journald 收走:

journalctl -k --since "1 hour ago" | grep NEWCONN | tail -20

iptables 的等价写法是 -j LOG --log-prefix "NEWCONN " --log-level 4,效果一样,但要知道 -j LOG 不能终止匹配,后面还要跟真实的 ACCEPT 或 DROP 规则。

这一步的产出是「有数据了」,但还不是告警。接下来要做的聚合,用一个几十行的 Python 脚本就能完成——它读内核日志,按源 IP 归组,统计每个 IP 在过去十分钟里访问了多少个不同的目的端口。一个判断阈值很实用:十分钟内来自同一 IP、目的端口数超过 15 个,基本可以确定是扫描器,因为正常的用户访问只会集中在一两个端口上。

具体到代码,把 journalctl -k --output=json 的输出喂给脚本,取出 SRC= 和 DPT= 两个字段(就是 iptables LOG 里的固定格式),用字典做 {ip: set(ports)} 的聚合,最后输出超过阈值的 IP 列表。这段代码不用写得漂亮,能跑在 cron 里每五分钟一次就够:

journalctl -k --since "-10 min" --output=cat \
  | grep "NEWCONN" \
  | sed -n 's/.*SRC=\([0-9.]*\).*DPT=\([0-9]*\).*/\1 \2/p'

这一行就能把「IP 端口」对打出来,剩下的计数用 awk 或 Python 都行。关键是把它落到文件里,留下按天的历史,这样你能看出「最近一周扫描量是不是变多了」,而不是只看到某一时刻的瞬时数字。

四、真正该告警的是「端口变化」,不是「被扫」

前面说了扫描属于噪音,那什么才值得一条告警?答案是基线变了。具体有三种情况:

第一种,多了一个监听端口。这是最值得关注的一类。有人在机器上开了一个新服务、装了一个面板、或者某个程序被改了配置,把本地端口改成了 0.0.0.0。这种变化往往意味着一次配置事故,而配置事故后面经常跟着安全事故。做法是每天跑一次端口快照,和基线做差集,多出的端口立刻报警。

ss -tlnH | awk '{print $4}' | sed 's/.*://' | sort -n | uniq > /var/lib/portcheck/today.txt
comm -13 /var/lib/portcheck/baseline.txt /var/lib/portcheck/today.txt

comm -13 输出的就是「今天有、基线没有」的端口,一行一个。把这段塞进每天早上的巡检脚本,有输出就发消息,这是性价比最高的一条告警。

第二种,某个端口从只有本机监听变成了全公网监听。比如 MySQL 原本是 127.0.0.1:3306,某次改 bind-address 或者换了个安装脚本,变成了 0.0.0.0:3306。这个变化用 ss -tlnp 的第一列就能抓到,比查防火墙可靠得多,因为防火墙规则可能压根没生效。

第三种,已知端口的连接数异常。比如 22 端口在一小时内出现上千次失败登录,或者 443 端口突然来了几千个不同 IP 的请求。前者是爆破,后者可能是 CC 攻击的开始。这一层的告警应该带上下文:来源 IP 的前三个、失败次数、时间窗口,让你一眼判断是「广撒网爆破」(IP 极多、每个试几次)还是「针对我」(同一 IP 高频重试)。

这三种告警加起来,一天大概响 0 到 2 次。这个频率是健康的——低于它说明你漏了,高于它说明阈值该调了。

五、告警来了之后:三步判定,别一上来就封 IP

收到一条「陌生 IP 扫了你 20 个端口」的告警,最糟的反应是立刻 iptables -A INPUT -s 那个IP -j DROP。原因是:扫描器绝大多数来自被感染的肉鸡或者云厂商的 IP 段,你封一个它换一个,封 IP 只是让自己心里舒服,不解决任何问题。而且如果是 GEOIP 判断失误封了正常用户的 IP,还会带来真实损失。

正确的顺序是三步。第一步,确认扫描是否「成功」了:查一下在那个时间窗口里,对方有没有真的连上过服务。ss -tan state established 看当前连接,journalctl -u ssh --since 看 SSH 登录记录,nginx 的 access log 看有没有对应的请求。如果全是 SYN 没有 ESTABLISHED,那就只是探测,记录一下即可。

第二步,确认那个被扫的端口上跑的是什么,以及它的版本。用 ss -tlnp 查 PID,ps -p PID -o cmd 看启动命令,再确认版本号是否在已知漏洞列表里。这一步经常查出意外发现:比如被扫的 888 上跑着一个两年前装的 phpMyAdmin,那才是真正需要处理的问题。

第三步才是处置。分两种情况:如果这个端口本来就不该暴露,那要做的是收口,不是封 IP——把服务改回 127.0.0.1 监听,或者用防火墙把整个端口段挡掉。如果这个端口必须暴露(比如 22),那要评估的是加固强度:是否禁用了密码登录、是否配了 fail2ban 之类的速率限制、是否有登录告警。封单个 IP 只在「同一 IP 高频重试」时才有意义,而且最好用自动化工具做,不要手动。

六、把扫描记录变成有用的东西:三条实用分析

积累了几天日志之后,这些数据本身就有价值了。三个最实用的分析方向:

一是看「被扫最多的端口」。把一周的日志按 DPT 聚合排序,排在前面的通常是一个固定的集合:22、23(telnet)、445、1433、3306、6379、9200、2375(Docker API)、8080。如果你的机器上恰好开着其中某一个,它就是下一件要处理的事。这个列表还能反过来告诉你:攻击者最关心哪些服务,你就不该让这些服务裸奔在公网。

二是看「扫描来源的分布」。按来源 IP 的 /24 段聚合,经常能看到成片出现的某个云厂商网段。这些不一定是攻击者,很多是安全公司在做互联网测绘。判断方法看访问模式:测绘型扫描通常规规矩矩按顺序扫端口、不重试、不带 payload;攻击型扫描会跳过明显关闭的端口、集中在少数几个高风险端口上反复试。

三是看「同一 IP 的行为变化」。一个 IP 上周只是扫端口,这周开始尝试 SSH 登录,这种「侦察转尝试」的变化比单次扫描重要得多。实现上不难:把每天的告警 IP 列表存下来,做一个「连续两天出现」的交集,命中的 IP 单独标出来。

这三条分析每周花十分钟看一次就够了,不需要做成看板。个人站的运维原则就是「够用即可」,花在监控系统本身的时间如果超过了花在内容上的时间,那这个监控系统就该简化了。

七、一个容易忽略的盲区:出方向的异常连接

绝大多数端口告警只看入方向,这没错,但只看出方向会漏掉一类最严重的场景:机器已经被拿下了,正在往外连。挖矿木马要连矿池、反弹 shell 要连 C2、数据窃取要往外传文件,这些动作在入方向日志里一点痕迹都没有。

所以基线里应该再加一条:出方向的连接目标。正常的 Web 服务器,出方向连接大概就是几个已知的地方——软件源、CDN 回源、第三方 API。如果出现了一个陌生的 IP 和陌生端口,尤其是一个高位端口,那就值得看一眼。

ss -tnp state established '( dport != :80 and dport != :443 and dport != :22 )'

这条命令列出所有当前建立的非 80/443/22 连接,正常情况下应该只有极少数几条(数据库连接、本地服务)。如果冒出一堆连向陌生 IP 高位端口的连接,配上 ss -tnp 里的进程名,基本当场就能判断出问题。把这条也放进每日巡检,输出非空就发告警,是一个成本极低、覆盖极广的保险。

八、告警规则的收敛:从吵闹到可信

监控系统做起来之后,最常见的结局不是它发现了问题,而是没人再看它了。收敛告警有几条实操原则。

第一,只对「需要你做决定」的事情发通知。被扫描不需要你决定任何事情,它只需要被记录。端口新增需要你判断这是自己装的还是别人装的,所以值得一条消息。半夜三点的爆破告警如果每天都有,那就改成早上一封汇总邮件,别在夜里叫醒自己。

第二,告警要自带足够的现场。一条只说「检测到异常」的告警等于没告警,你得再花十分钟去查。合格的告警应该包含:什么时候、什么端口、什么来源、之前有没有出现过、需要做什么。这几项其实在脚本里都能自动带上,多的不过是几行字符串拼接。

第三,分级,并且允许自己无视低级别。设计成三级:记录级(扫描、普通探测)只进日志,日汇总里出现;提示级(新端口、出方向异常)当天发一条消息;紧急级(成功登录失败多次、疑似 webshell、异常出站)立刻发。大部分时间你只会看到提示级,这很正常。

第四,每季度回顾一次阈值。站点的流量在变、机器上装的软件在变,去年合适的阈值今年可能每天都在误报。回顾的方法很简单:翻一下过去一个月的告警记录,如果某类告警 90% 都是误报,就把它降一级或者调阈值;如果某类告警一次都没响过,要确认它不是被写错了。沉默的监控和吵闹的监控一样不可信。

九、小结:把「不可见」变成「可解释」

端口扫描告警这件事,说到底不是为了抓到攻击者——个人站长基本抓不到,也不需要抓。它的价值在于把服务器的暴露面变成一件看得见、说得清的事:你知道自己开了哪些端口、哪些对公网开放、哪些是意料之外的、有没有人在试探你的边界。

落地顺序建议就四步:先摸清三层事实(进程、防火墙、外部可达)形成基线;然后上 -j LOG 记录新连接,得到「被扫」的原始数据;再加一条每日端口差集检查,抓「端口变化」;最后给出方向连接加一条告警兜底。这套东西全部加起来不到两百行脚本,跑在一台 1 核 1G 的小机器上毫无压力。

真正要养成的习惯只有一个:任何一次「多出来的东西」都要问一句为什么。多出来的端口、多出来的进程、多出来的计划任务、多出来的出站连接——这四样里出现任意一样,都值得当天花十分钟查清楚。多数时候答案是无害的,但正是这个习惯,让那次 Redis 未授权访问在造成更大损失之前被发现了。

Last modification:November 3rd, 2026 at 09:32 pm

Leave a Comment