Ubuntu服务器跑后端程序时,端口防护不是装个防火墙就完事。真正的问题是:后端程序监听在哪个端口、哪些端口需要对公网开放、哪些端口只允许本机或内网访问、以及程序本身崩溃或配置错误时会不会把不该暴露的端口带出来。很多人把22端口改了、装了ufw,结果后端程序一启动,0.0.0.0:3306直接对外,或者Redis没绑127.0.0.1,等于防火墙形同虚设。所以端口防护要围绕“后端程序实际运行方式”来做,而不是只做系统层规则。
先明确一个基本判断:Ubuntu服务器上,系统防火墙和后端程序绑定地址是两层防线。系统防火墙负责“哪些端口允许外部访问”,程序绑定地址负责“服务本身监听在哪个网络接口”。两层必须配合。只做一层,另一层出错就会暴露。比如ufw允许了8080端口,但后端程序绑定0.0.0.0,外部就能访问;如果程序绑定127.0.0.1,即使ufw放行8080,外部也访问不到。反过来,程序绑定0.0.0.0,但ufw没有放行对应端口,外部同样进不来。理解这个关系,后面的配置才有意义。
先检查当前端口监听状态部署后端程序前,先跑一遍ss命令,看清楚系统里哪些进程在监听哪些地址和端口。这个习惯能避免很多低级错误。
sudo ss -tulnp | sort -k4
输出里重点看Local Address这一列。如果显示0.0.0.0:3306或者:::6379,说明MySQL或Redis对所有网卡开放,外部网络可能直接访问。如果显示127.0.0.1:3306或者::1:6379,说明只监听本机回环地址,外部无法直接连接。后端程序常用的数据库、缓存、消息队列,默认配置经常是监听所有地址,装完不检查就直接跑,等于把内网服务暴露到公网。
后端程序绑定地址要显式配置不要依赖默认配置。每个后端程序启动前,明确指定监听地址。对于需要被外部访问的HTTP/HTTPS服务,绑定0.0.0.0是正常的,但必须配合防火墙只放行80和443。对于数据库、Redis、Memcached、Elasticsearch这类组件,只绑定127.0.0.1或内网IP。
以Node.js后端为例,监听时显式写地址:
const server = app.listen(3000, '127.0.0.1', () => {
console.log('Server running on 127.0.0.1:3000');
});
如果这个Node服务只需要被本机Nginx反向代理访问,绑定127.0.0.1就够了。外部请求先到Nginx的80或443端口,再由Nginx转发到本机3000端口。这样即使后端程序有漏洞,外部也无法直接绕过Nginx访问3000端口。
Python的Flask或FastAPI同理:
if __name__ == '__main__':
app.run(host='127.0.0.1', port=8000)
生产环境一般用Gunicorn或Uvicorn启动,也要指定绑定地址:
gunicorn -w 4 -b 127.0.0.1:8000 app:app
Java的Spring Boot在application.properties里配置:
server.address=127.0.0.1 server.port=8080
Go的net/http监听:
http.ListenAndServe("127.0.0.1:8080", nil)
这些显式绑定是端口防护的第一道关。程序层面把端口限制在本机,外部攻击者连端口都摸不到,比任何防火墙规则都直接。
用ufw做系统层端口白名单Ubuntu默认的ufw足够应对大多数场景。原则是默认拒绝所有入站,只放行必须对外的端口。安装后先不要急着启用,先配置规则。
sudo apt update sudo apt install ufw -y sudo ufw default deny incoming sudo ufw default allow outgoing
然后只放行SSH、HTTP、HTTPS。如果SSH改了端口,把22换成实际端口:
sudo ufw allow 22/tcp sudo ufw allow 80/tcp sudo ufw allow 443/tcp
如果后端程序有WebSocket或自定义TCP端口需要对外,单独放行:
sudo ufw allow 8080/tcp
确认规则无误后启用:
sudo ufw enable sudo ufw status verbose
这里有个关键点:ufw只控制入站流量。后端程序如果主动向外发起连接,默认是允许的,这符合大多数场景。但有些程序会监听UDP端口,比如DNS服务或某些游戏后端,需要单独放行UDP。另外,如果服务器上有Docker,Docker会直接操作iptables,绕过ufw的规则。这是很多人踩坑的地方:ufw显示22/80/443放行,但Docker容器映射的端口直接暴露在公网,ufw管不到。
Docker场景下的端口防护要单独处理如果后端程序跑在Docker容器里,端口映射方式直接决定暴露面。docker run -p 3306:3306这种写法,会在宿主机所有网卡上监听3306端口,而且Docker会修改iptables,ufw默认拦不住。正确的做法是:
数据库、缓存这类容器,只绑定宿主机回环地址:
docker run -d --name mysql \ -p 127.0.0.1:3306:3306 \ -e MYSQL_ROOT_PASSWORD=yourpassword \ mysql:8
这样容器端口只映射到宿主机的127.0.0.1,外部无法访问。对于需要对外提供服务的容器,比如Nginx或后端API,可以映射到0.0.0.0,但宿主机ufw必须放行对应端口。不过由于Docker绕过ufw,更稳妥的办法是让Docker容器不直接暴露端口,而是通过宿主机Nginx反向代理。
具体做法:Docker容器只映射到127.0.0.1,宿主机安装Nginx监听80和443,由Nginx转发到127.0.0.1的容器端口。这样外部流量必须经过Nginx,Nginx可以做访问控制、限流、TLS终止,端口防护的层次更清晰。
针对后端程序常见端口的加固清单SSH端口:改掉默认22,禁止密码登录,只允许密钥。修改/etc/ssh/sshd_config:
Port 2222 PasswordAuthentication no PermitRootLogin no
改完重启sshd,并在ufw放行新端口,删除22端口规则。
MySQL/MariaDB:检查/etc/mysql/mysql.conf.d/mysqld.cnf,确认bind-address为127.0.0.1。如果必须远程访问,只绑定内网IP,不要用0.0.0.0。同时创建专用账号并限制来源IP。
Redis:编辑/etc/redis/redis.conf,设置bind 127.0.0.1,设置requirepass,禁用危险命令。默认配置下Redis没有任何认证,一旦暴露6379端口,攻击者可以直接写入公钥或计划任务。
MongoDB:默认绑定127.0.0.1,但老版本有绑定0.0.0.0的情况。检查/etc/mongod.conf的bindIp字段,启用认证。
Elasticsearch:默认绑定0.0.0.0,且无认证。必须设置network.host为127.0.0.1或内网IP,并启用X-Pack安全认证。
后端API端口:如果使用Node、Go、Java等自建HTTP服务,不要直接把端口暴露到公网。前面加Nginx或Caddy做反向代理,后端只绑定127.0.0.1。
用Nginx反向代理收敛端口暴露面后端程序直接监听公网端口有几个问题:无法统一做TLS、无法做路径级访问控制、无法做限流和日志审计。用Nginx做前置代理,后端只绑定127.0.0.1,外部只开放80和443。Nginx配置示例:
server {
listen 80;
server_name api.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
这样后端程序的8000端口完全不对外,外部只能通过Nginx的443端口访问。Nginx还可以加IP白名单、Basic Auth、请求频率限制,进一步收紧端口和访问控制。
定期审计端口和监听地址端口防护不是一次性配置,后端程序更新、新增服务、调整架构都可能引入新的监听端口。建议每周跑一次端口审计,把当前监听状态和预期清单做对比。
sudo ss -tulnp | awk '{print $1, $5, $7}' | sort -u
把输出保存下来,和文档里记录的“允许对外端口清单”对比。任何不在清单里的0.0.0.0监听都要排查。另外可以用nmap从外部扫描自己的服务器,验证实际暴露面:
nmap -sS -p- your-server-ip
从外部视角看,只有22、80、443(或改过的SSH端口)开放,其他端口全部过滤,说明端口防护到位。如果扫描出意外的开放端口,立刻检查对应进程和防火墙规则。
结合fail2ban做端口暴力破解防护端口开放给公网后,SSH和HTTP服务会持续受到暴力破解和扫描。fail2ban可以监控日志,自动封禁可疑IP。安装配置:
sudo apt install fail2ban -y sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
在jail.local里启用SSH和Nginx防护:
[sshd] enabled = true port = 2222 maxretry = 3 bantime = 3600 [nginx-limit-req] enabled = true port = 80,443 maxretry = 5 bantime = 600
重启fail2ban后,它会自动读取日志,对连续失败的IP进行封禁。这属于端口防护的动态层,和静态防火墙规则互补。
总结端口防护的层次Ubuntu服务器跑后端程序,端口防护应该形成四层:第一层是程序绑定地址,数据库和内部服务只绑127.0.0.1;第二层是系统防火墙ufw,默认拒绝入站,只放行必要端口;第三层是反向代理,后端HTTP服务不直接对外,由Nginx统一入口;第四层是动态封禁和定期审计,用fail2ban和ss、nmap持续监控。四层配合起来,即使某一层配置失误,其他层还能兜底,端口暴露风险会降到很低。
实际部署时,最容易忽略的是Docker绕过ufw和数据库默认绑定0.0.0.0这两个问题。只要把这两点处理好,加上程序显式绑定地址和Nginx反向代理,Ubuntu服务器的端口防护就能适配绝大多数后端程序的运行需求。
