SSH 改个端口就算加固?一台 Linux 服务器上线前我会检查这些东西

新买一台云服务器,装好 Docker,项目一跑,域名一绑,看起来已经可以用了。

然后你打开 SSH 日志,通常很快就能看到各种陌生 IP 在试 rootadminubuntutest。很多人的第一反应是把 22 改成 22222,再装个 Fail2ban,觉得差不多了。

改端口当然有用,至少日志能安静不少。但它解决的主要是扫描噪声。只要服务还在公网监听,端口最终都能被扫出来。服务器真正需要处理的是认证、权限、暴露面、更新和恢复能力。

这篇按我自己给一台新 Linux 服务器做上线检查的顺序来写。示例以 Ubuntu Server 为主,Debian 基本一样,其他发行版只需要替换包管理器和防火墙工具。

先说一个最重要的原则:所有 SSH 和防火墙改动,都不要在只剩一条远程连接的情况下直接提交。

保留当前 SSH 会话,另开一个终端验证新配置能登录,再关闭旧会话。云服务器最好同时确认厂商控制台、VNC 或救援模式可用。安全加固把自己锁在门外,这种事一点也不少见。


先看清服务器到底暴露了什么

我不会一上来先改配置,第一步通常是看现状。

sudo ss -lntup

重点看监听地址:

127.0.0.1:5432
0.0.0.0:22
0.0.0.0:80
0.0.0.0:443
[::]:22

127.0.0.1 只监听本机,0.0.0.0 表示监听所有 IPv4 地址,[::] 通常意味着 IPv6 也在监听。

很多“我明明没开放这个端口”的问题,到这里就已经能看出一半。

如果装了 Docker,再补一条:

docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}'

服务器的网络边界大概可以画成这样:

Internet
   |
   v
云厂商安全组 / 防火墙
   |
   v
Linux 主机防火墙(UFW / nftables)
   |
   +------ sshd
   +------ Caddy / Nginx
   +------ Docker 发布到宿主机的端口
               |
               v
          Docker 内部网络

这几层任何一层配错,最后看到的结果都可能和你的预期不同。


先准备一个正常的管理账号

日常管理没有必要一直用 root。

sudo adduser ops
sudo usermod -aG sudo ops

然后切到新账号确认 sudo 正常:

su - ops
sudo whoami

正常应输出:

root

这里的意思是“普通账号需要时可以提权”,并不是让这个账号一直以 root 身份跑。

Ubuntu 官方的安全建议也一直强调最小权限:平时使用普通用户,只在管理任务中使用 sudo。


SSH 密钥先配好,再关密码登录

在自己的电脑上生成一把 Ed25519 密钥:

ssh-keygen -t ed25519 -a 64 -C "server-admin"

Linux、macOS 或 WSL 可以直接:

ssh-copy-id ops@SERVER_IP

然后试一次:

ssh ops@SERVER_IP

确认不用输入服务器密码也能正常登录以后,再开始改 sshd。

我比较喜欢把自己的配置放到 drop-in 文件里,而不是直接把 /etc/ssh/sshd_config 改得面目全非:

sudo nano /etc/ssh/sshd_config.d/99-hardening.conf

基础配置可以从下面这份开始:

PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication no
MaxAuthTries 3
LoginGraceTime 30
X11Forwarding no
AllowUsers ops

如果确定没有使用 PAM 一次性验证码、SSH MFA 之类的认证方式,还可以加:

KbdInteractiveAuthentication no

但这条不要无脑复制。你以后如果要做多因素认证,它很可能需要重新打开。

改完以后先检查语法:

sudo sshd -t

没有任何输出才继续。

然后重新加载:

sudo systemctl reload ssh

此时不要退出当前终端,另开窗口重新登录一次。

ssh ops@SERVER_IP

确认没问题,再继续后面的操作。

那 SSH 要不要改端口?

可以改,但我不会把它当成核心防线。

例如:

Port 22222

它确实能过滤掉一批只盯着 22 端口乱撞的脚本,日志会清爽很多。但端口号不是秘密,公网扫描器迟早会找到它。

真正重要的是:

  • root 不直接远程登录;
  • 密码认证关闭;
  • 私钥保管好;
  • SSH 入口受到防火墙限制;
  • 登录行为有日志和封禁策略。

端口只是锦上添花。


防火墙别等到最后才开

Ubuntu 自带 UFW,但默认通常没有启用。

先设置默认策略:

sudo ufw default deny incoming
sudo ufw default allow outgoing

在启用之前先放行 SSH。

如果还是 22:

sudo ufw allow 22/tcp

如果已经改到 22222:

sudo ufw allow 22222/tcp

Web 服务再放 80 和 443:

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

确认规则:

sudo ufw status numbered

最后才启用:

sudo ufw enable

如果你有固定公网 IP,SSH 还可以继续收紧:

sudo ufw allow proto tcp from 203.0.113.10 to any port 22

这样公网其他地址连 SSH 都到不了认证阶段。

当然,移动网络、校园网、家庭宽带经常换 IP 的人不要照抄,否则下次换个网络就会发现自己进不去了。

别忘了云厂商安全组

云服务器一般还有一层平台侧安全组。

我习惯让它和主机规则都保持最小开放:

22/22222    只允许管理来源(能固定的话)
80          0.0.0.0/0
443         0.0.0.0/0
其他端口     默认关闭

安全组和 UFW 是两套东西,改一边不会自动同步另一边。

IPv6 也别忘。很多人只检查 IPv4,结果服务在 [::]:端口 上照样对外监听。


Fail2ban 可以装,但别把参数配成报复社会

Fail2ban 的作用很简单:持续读取日志,发现某个来源反复认证失败后,临时封掉它。

安装:

sudo apt update
sudo apt install -y fail2ban

新建:

sudo nano /etc/fail2ban/jail.d/sshd.local

一份比较正常的起点:

[sshd]
enabled = true
port = ssh
backend = systemd
maxretry = 5
findtime = 10m
bantime = 1h

如果 SSH 已经换端口,把 port 改成对应数字。

启动并检查:

sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd

我不建议把配置写成:

maxretry = 1
bantime = 几十年

一次失败就永久封禁,带来的安全收益很有限,误伤自己、同事、跳板机、NAT 出口的概率却会明显上升。

攻击者真正需要面对的是“根本没有密码可以猜”,这比把封禁时间写得特别夸张有效得多。


Docker 是最容易漏掉的一层

这里非常值得单独说。

假设你的 Compose 里写了:

services:
  postgres:
    image: postgres:17
    ports:
      - "5432:5432"

这通常意味着 PostgreSQL 被发布到了宿主机网络接口。

如果数据库只给同一台机器里的应用访问,这个 ports 很可能根本没必要。

更合理的写法通常是让两个容器进入同一个 Docker 网络:

services:
  app:
    image: example/app:latest
    networks:
      - backend

  postgres:
    image: postgres:17
    networks:
      - backend

networks:
  backend:

应用直接访问:

postgres:5432

数据库完全不需要发布到公网。

如果确实需要宿主机访问,但不需要外网访问,可以绑定回环地址:

ports:
  - "127.0.0.1:5432:5432"

Docker 官方文档明确提醒:Linux 上 Docker 会创建自己的防火墙规则来实现端口发布和网络隔离。已发布到宿主机外部地址的端口,默认可能对远程主机可访问。因此不要只看 ufw status 就断言“这个端口肯定被挡住了”。

至少同时检查:

sudo ss -lntup
docker ps --format 'table {{.Names}}\t{{.Ports}}'

如果你前面已经用了 Caddy 做统一入口,通常最舒服的结构就是:

公网
 |
80 / 443
 |
Caddy
 |
Docker 内部网络
 |--- Web
 |--- API
 |--- 其他服务

PostgreSQL / Redis / NATS 等内部组件不发布公网端口

之前写过一篇完整部署过程,可以直接接着看:

用 Caddy + Docker Compose 给自托管服务自动上 HTTPS


系统更新不能靠“有空再说”

很多服务器刚装好时会认真更新两次,半年以后就再也没人碰。

至少定期执行:

sudo apt update
apt list --upgradable

然后安排升级:

sudo apt upgrade

Ubuntu Server 支持 unattended-upgrades 自动安装安全更新,很多镜像默认已经开启,但不同云厂商镜像的实际配置可能不一样,最好自己确认:

systemctl status unattended-upgrades

没有安装的话:

sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades

自动更新也不是设置完就再也不管。内核、OpenSSL、OpenSSH、容器运行时这些东西升级以后,有时需要重启服务甚至重启机器才能真正使用修复后的版本。


密钥、Token 和 .env 也属于服务器安全

SSH 锁得再严,结果管理员把数据库密码写进 Git 仓库,还是白忙。

至少做到:

chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
chmod 600 .env

chmod 600 .env 只是最基础的一层。真正重要的是:

  • .env 不提交 Git;
  • CI/CD Token 不写进仓库;
  • 数据库、对象存储、云 API 使用独立凭据;
  • 能给只读权限就别给管理员权限;
  • 凭据疑似泄露时直接轮换,不要只删聊天记录或 Git 提交;
  • 生产环境不要和开发环境共用同一套密钥。

如果项目已经开始认真做发布流程,可以再看之前的供应链安全文章:

你扫描的是代码,攻击者盯的是发布链路:软件供应链安全到底在防什么


日志要看得见,出事以后才有东西查

SSH 登录先看:

sudo journalctl -u ssh --since today

查看近期登录:

last -a

Fail2ban:

sudo fail2ban-client status sshd

UFW:

sudo ufw status verbose

Docker:

docker ps

服务运行状态:

systemctl --type=service --state=running

对一两台个人服务器,不一定非要先上完整 SIEM。但最起码你得知道日志在哪、保留多久、服务器重启后还有没有,以及磁盘满了会发生什么。

如果网站经过反向代理,还要确认访问日志里没有把 Authorization、Cookie、密码、Token 等敏感信息完整记录下来。

这部分和上一篇 HTTPS 文章是连着的:

上了 HTTPS,渗透测试为什么还会报“密码明文传输”


备份不是运维附加项,它决定你被打以后还能不能回来

很多安全文章只讲“怎么不被打”,很少讲“打穿以后怎么办”。

现实里没有哪台公网服务器能承诺永远不出问题。

所以至少要有:

  • 数据库定期备份;
  • 关键配置备份;
  • 备份放到另一台机器或对象存储;
  • 备份文件加密;
  • 保留多个历史版本;
  • 真正做过一次恢复测试。

最后一条尤其重要。

backup completed successfully 只能证明备份程序没报错,不能证明那份东西真的能恢复。

另外,SSH 管理密钥最好至少有第二种恢复路径:备用管理员密钥、云厂商控制台或者救援系统。主电脑硬盘损坏的时候,你不应该顺便失去服务器管理权。


我现在给服务器上线前会跑的一组检查

下面这些命令不修改系统,适合做最后一次人工检查:

printf '\n=== Listening ports ===\n'
sudo ss -lntup

printf '\n=== SSH effective config ===\n'
sudo sshd -T | grep -E \
'^(port|permitrootlogin|passwordauthentication|pubkeyauthentication|maxauthtries|kbdinteractiveauthentication) '

printf '\n=== Firewall ===\n'
sudo ufw status verbose

printf '\n=== Fail2ban ===\n'
sudo fail2ban-client status sshd 2>/dev/null || true

printf '\n=== Docker published ports ===\n'
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}' 2>/dev/null || true

printf '\n=== Running services ===\n'
systemctl --type=service --state=running --no-pager

printf '\n=== Pending package updates ===\n'
apt list --upgradable 2>/dev/null

printf '\n=== Recent SSH log ===\n'
sudo journalctl -u ssh -n 30 --no-pager

跑完以后问自己几个很简单的问题:

  1. 现在公网到底有哪些端口?
  2. SSH 还能不能用密码和 root 直接登录?
  3. Docker 有没有把数据库、中间件、管理后台顺手暴露出去?
  4. 系统最近一次安全更新是什么时候?
  5. 如果服务器今天被删掉,我能不能在另一台机器上恢复?

这几个问题比“SSH 到底是 22 还是 35271”重要得多。


最后

Linux 服务器安全没有一个神奇配置,复制进去就能结束。

真正麻烦的是那些很普通的小地方:一个多余的 ports:,一把几年没轮换的 Token,一个从来没测试恢复的备份,一台半年没升级的服务器,或者一个为了方便一直开着的 root 密码登录。

这些东西单独看都不刺激,也不会让终端里出现绿色的 SECURE 字样。但服务器长期跑在公网,最后出问题的往往就是这些地方。

所以我的习惯一直很简单:先把暴露面缩小,再把身份和权限管住,然后保证补丁、日志和恢复链路真的能工作。

改 SSH 端口可以做。

但别做到这里就收工。


参考资料