开放端口的清单,就是通往你服务器的路径清单。其余的一切——防火墙、WAF、入侵检测——都建在它之上。正因如此,「这里有什么在监听」在任何一次审计里都排在最前面,也正因如此,这项检查最常给出令人不快的答案:查出来的东西里有一半不是你打开的,而是某个软件包的安装脚本打开的。

读懂输出

sudo ss -tulpn

各个标志:t 是 TCP,u 是 UDP,l 只看监听中的,p 是进程,n 让数字保持为数字。没有 sudo 时进程那一列是空的,整个练习也就失去了意义。

要紧的是 Local Address:Port 这一列,而那里的区别是根本性的:

  • 127.0.0.1:3306——服务只能从本机够到。这是好事;
  • 0.0.0.0:3306——从任何 IPv4 地址,也就是从互联网。这正是需要检查的;
  • [::]:3306——IPv6 的同样情况。单独的一行,而且经常被漏看;
  • 203.0.113.25:443——在某个具体地址上,通常是有意为之。

然后对每一行带 0.0.0.0[::] 的记录问一个问题:陌生人是否应当能够到这个东西。对 80 和 443 答案是肯定的。对几乎其余的一切答案都是否定的。

常见的发现

Redis,端口 6379。那里最危险的一行。Redis 默认不要求密码,而它的命令允许往磁盘写文件——也就是往 authorized_keys 里塞一把别人的密钥。从 Redis 出现在公网地址到被利用,中间是几个小时,有时更短。请检查配置里的 bind 127.0.0.1protected-mode yes

Memcached,11211/UDP。即便里面没有什么值钱的东西,你的服务器也会变成别人攻击的放大器——而投诉会从你的服务商那里来。

MySQL 和 PostgreSQL,3306 与 5432。密码是有的,但对它的猜测持续不断,而数据库版本的更新频率又低于人们希望的水平。它们几乎从来不需要朝外:应用就在同一台机器上,而你自己的工作用一条 SSH 隧道就够了。

Elasticsearch 9200、MongoDB 27017。历史上默认是不带认证的。它们的公网实例是数据泄露新闻的常备来源。

Docker API,2375。开放的 Docker 控制端口就等于宿主机上完全不需要密码的 root。它通常在尝试远程访问 Docker 之后出现。

控制面板与 phpMyAdmin,各自的端口:8080、8083、10000。倒不是绝对不能开,但它们恰恰是把大部分尝试都招过来的东西。

要在服务上修,而不是在防火墙上

想用一条 UFW 规则把每个发现都堵上,这可以理解,但那是第二道防线而不是第一道。规则可能被误删,防火墙在排障时可能被临时关掉,而 Docker 更是完全绕过 UFW 去发布端口。服务自身配置里的绑定设置能挺过这一切:

  • MySQL/MariaDB——bind-address = 127.0.0.1
  • PostgreSQL——listen_addresses = 'localhost'
  • Redis——bind 127.0.0.1 ::1
  • Docker Compose——按 "127.0.0.1:5432:5432" 的形式发布。

防火墙叠在上面作为保险,而不是取代这一步。

不清楚时怎么找到那个进程

ss 给出名字和 pid。然后:

sudo systemctl status <pid>
sudo lsof -i :8080

第一条命令会说明这个进程属于哪个 systemd 单元——通常足以判断它是什么、是否需要。一个在高端口上监听、且从系统目录之外启动的陌生进程——比方说从 /tmp/dev/shm——就已经不再是配置问题,而是另起一次调查的理由。

从外部检查是强制的

ss 回答的是「什么在监听」,而不是「什么够得着」。这两者之间隔着防火墙、NAT 和服务商自己的规则。诚实的答案只能靠从另一台机器扫描得到:

nmap -Pn -p- 203.0.113.25
nmap -Pn -p- -6 2001:db8::1

别跳过第二条命令:几乎每台 VPS 都有 IPv6 地址,它的规则是单独写的,而服务是同时在协议的两个版本上监听的。

这不是一次性的检查

开放端口的清单会自己变化。你装了一个包,它带来了一个服务并打开了一个端口。你更新了面板,它把默认值恢复了。你启动了一个容器,它绕过防火墙发布了端口。一次性的检查只对今天有效,仅此而已。

价值不在清单本身而在它的变化:一个昨天还没有的新端口,是一个简短却信息量很大的信号。正该这样去盯着它——作为带历史的快照,而不是凭记忆想起来的 ss 输出。下面的演示页面展示的正是这个。