全新的 Ubuntu VPS 并不具备生产就绪的状态。公网 IP 上线后数小时内,自动化扫描器就会在 SSH 上尝试 root/admin/test 等常见密码。通用清单——更新、非 root 用户、仅 SSH 密钥认证、防火墙、fail2ban、swap、自动补丁——能将一台裸机变成可以放心指向域名的服务器。二十分钟的投入,在第一次真正需要的时候就会得到回报。
全新 Linux VPS 的默认状态是"一切放行,请进"。SSH 开启了 root 登录,密码认证也处于启用状态(通常是服务商通过邮件发送的默认密码),没有运行任何防火墙,也没有配置自动更新。公网 IP 分配后数分钟内,日志里就会出现来自自动化僵尸网络的大量 SSH 尝试记录,每小时数以千计,它们不断试探常见用户名和弱密码。大多数会失败,但也有漏网之鱼。
以下的加固措施并不特别。DigitalOcean、AWS、Hetzner、Linode 以及其他所有 VPS 服务商十年来一直在推荐同样的清单。之所以要认真完成,而不是跳过,是因为这些步骤有条件地很快:第一次需要二十分钟;之后每次复制同一份镜像或复用同一个 Ansible role 就行了。做过一次之后,这永远不会成为瓶颈。
本教程将逐步介绍 Ubuntu 22.04 / 24.04 服务器的通用首次启动清单(Debian 操作完全相同;Alpine 有所不同,相关差异会单独说明)。完成这些步骤后,你的服务器将具备:一个非 root 的部署用户、仅 SSH 密钥认证、活跃的防火墙、SSH 暴力破解限速、适合实例大小的 swap 文件、正常运行的日志轮转,以及配置好的无人值守安全更新。之后你就可以部署你的应用程序了。
root 仅用于紧急情况;日常工作通过具有 sudo 权限的用户进行~/.ssh/id_ed25519.pub 或 ~/.ssh/id_rsa.pub)。如果你的笔记本上还没有 SSH 密钥,请先生成一个:ssh-keygen -t ed25519 -C "[email protected]"。不要跳过这一步、继续使用密码认证——那样会让本教程的其他步骤失去意义。
你启动的镜像是某个时间点的快照。从快照到你启动之间,安全补丁已经发布。在做其他任何事之前先把它们拉下来,因为后续步骤假设你使用的是最新的软件包:
ssh root@<PUBLIC_IP>
apt update
apt full-upgrade -y
# 内核更新可能会提示重启——选择接受。
reboot
等待一分钟后重新连接。现在你运行的是最新的补丁。选用 full-upgrade 而非 upgrade,是因为后者会搁置那些需要依赖变更的包——而那些通常正是安全相关的包。
每个常见的用户名——root、admin、ubuntu、deploy、www——都在被扫描器猜测。缓解措施是将非默认用户名与仅密钥认证结合起来,但用户名的重要性其实低于人们的想象——防火墙和密钥认证才是真正起作用的。随便取个好记的名字;用 deploy 就挺好。
adduser deploy
# 设置一个密码。你基本上永远不会手动输入它;
# 随机生成一个长密码存进密码管理器即可。
# 只有在 sudo 要求时才会用到(你也可以禁用它)。
usermod -aG sudo deploy
将你的 SSH 密钥复制到新用户的 authorized_keys,这样你就可以无密码登录为 deploy:
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy
# rsync 会将 root 的 .ssh/authorized_keys 复制到 /home/deploy/.ssh/
# 并修正所有权。验证:
ls -la /home/deploy/.ssh/
打开第二个终端(先不要断开 root 会话)并测试:
ssh deploy@<PUBLIC_IP>
# 应该可以无密码登录。从现在起,以 deploy 身份工作;
# 需要时用 sudo 提权。
以 deploy 用户身份操作(这样你也能顺便验证 sudo 可用):
sudo nano /etc/ssh/sshd_config
找到并设置以下内容(如有注释则取消注释):
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
# 可选但建议:
ChallengeResponseAuthentication no
UsePAM yes
KbdInteractiveAuthentication no
保存。在 Ubuntu 22.04+ 上,SSH 配置经常分散在 /etc/ssh/sshd_config.d/*.conf 文件中;服务商镜像有时会放一个 50-cloud-init.conf,重新启用密码认证。检查一下:
sudo grep -rE "^(Password|PermitRoot)" /etc/ssh/sshd_config /etc/ssh/sshd_config.d/
# .d/ 目录中任何将 PasswordAuthentication 设为 yes 或
# PermitRootLogin 设为 yes 的行都会覆盖你的修改。将它们注释掉。
应用配置:
sudo systemctl restart ssh
# (在旧版 Ubuntu 上,unit 名称是 sshd.service。systemctl restart sshd
# 效果相同;systemd 会解析别名。)
在你的笔记本上,打开一个新终端,尝试:
ssh deploy@<PUBLIC_IP> # 应该仍然能正常登录
ssh root@<PUBLIC_IP> # 应该被拒绝:"Permission denied (publickey)"
如果两者的表现都符合预期,原来的 root 会话就可以关闭了。如果出了任何问题,原窗口中开着的 root 终端就是你重新编辑 sshd_config 的方式。
ufw("简单防火墙")是带有友好界面的 iptables。在 Ubuntu 上已预装但默认未激活:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH # 端口 22
sudo ufw allow http # 端口 80
sudo ufw allow https # 端口 443
sudo ufw enable
# 它会提示断开 SSH 的警告——你在上一行已经放行了,所以是安全的。
sudo ufw status verbose
如果你的服务商有自己的外部防火墙(Lightsail 的防火墙标签、AWS 安全组、DigitalOcean Cloud Firewalls),请同时保留 ufw 和服务商防火墙。双重防护;任何一层都能在另一层失效时守住阵线。
127.0.0.1,通过 nginx 在 80/443 端口对外提供服务。防火墙规则列表应该永远只有三行;其他一切都走内部通道。
fail2ban 监视日志文件中的失败登录尝试,并添加防火墙规则封禁相应 IP。在仅密钥 SSH 的情况下,这是双重保险,但保险本身的成本很低:
sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban
默认的 jail 会监控 SSH。在本地配置文件中覆盖默认设置(不要编辑原始文件——软件包更新会将其覆盖):
sudo tee /etc/fail2ban/jail.local >/dev/null <<'EOF'
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
[sshd]
enabled = true
EOF
sudo systemctl restart fail2ban
验证:sudo fail2ban-client status sshd。已封禁的 IP 会随时间积累;如果你不小心把自己封禁了,使用 sudo fail2ban-client unban <IP> 解封。
大多数 VPS 服务商默认不分配 swap。在 512 MB 或 1 GB 的实例上,一次突发内存峰值(失控的日志解析、配置有误的 Node 应用)会触发 Linux OOM killer,它通常会干掉内存占用最高的进程——往往就是你的应用。Swap 不能让系统变快;它给 OOM killer 提供了喘息空间,防止最糟糕的情况——"服务随机消失"。
# 对于 1 GB 实例,2 GB swap 是合理的。按比例调整。
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
# 使其在重启后持久化:
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
# 调优:swap 使用的激进程度。数值越低 = 越晚才动用 swap。
echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-swap.conf
sudo sysctl -p /etc/sysctl.d/99-swap.conf
# 验证:
free -h
free -h 的输出现在会显示 Swap 行,容量为 2.0 GB。服务器再也不会因为一次内存峰值就 OOM 掉你的应用了。
没打的补丁,就是漏洞利用的入口。Ubuntu 自带 unattended-upgrades 用于自动安全更新;在大多数服务商镜像上它已安装但未启用:
sudo apt install -y unattended-upgrades apt-listchanges
sudo dpkg-reconfigure --priority=low unattended-upgrades
# 对"自动下载并安装稳定版更新"选择"是"
# 验证它按计划运行:
systemctl status unattended-upgrades.timer
默认只有安全更新会自动应用。应用更新(Node、Postgres)保持手动——这些更新可能破坏兼容性,不应自动应用。如果想启用更广泛的自动更新(不推荐用于生产环境),编辑 /etc/apt/apt.conf.d/50unattended-upgrades 并取消注释其他源的行。
内核更新后自动重启默认关闭。如果你想开启(推荐用于无状态服务器),在同一文件中设置 Unattended-Upgrade::Automatic-Reboot "true"; 和 Unattended-Upgrade::Automatic-Reboot-Time "03:00";。有持久化状态的服务器(数据库)应保持手动重启。
Ubuntu 自带配置合理的 logrotate。你不需要额外设置,但要验证它确实在运行——配置错误的 logrotate 会在几个月内用旧日志填满磁盘:
sudo logrotate -d /etc/logrotate.conf 2>&1 | head -20
# -d 是"调试"模式——显示它将要做什么,但不实际执行。
# 你应该看到日志文件被识别并等待轮转。
# 手动运行(强制执行):
sudo logrotate -f /etc/logrotate.conf
当你安装自己的服务(nginx、postgres、自定义应用)时,它们通常会在 /etc/logrotate.d/ 中放置配置文件。默认设置——每周轮转、保留 4 周、压缩旧日志——对大多数服务来说都足够了。
sudo reboot
# 等待 30 秒,然后重新连接:
ssh deploy@<PUBLIC_IP>
重新连接后,快速运行五项检查:
# 1. 你是 deploy 用户,而不是 root。
whoami # → deploy
# 2. Swap 处于活跃状态。
free -h | grep Swap # → 2.0Gi total
# 3. 防火墙处于活跃状态并已正确配置。
sudo ufw status # → Status: active,以及你的三条规则
# 4. fail2ban 正在运行。
sudo systemctl is-active fail2ban # → active
# 5. Unattended-upgrades 正在运行。
sudo systemctl is-active unattended-upgrades.timer # → active
五项全绿:服务器已就绪。是时候安装你的应用程序栈并部署了。
数据库 / 缓存——Postgres、Redis 等是独立的话题,各有其加固方式。参见安装 Postgres 和 Redis。
将 SSH 移到非标准端口——将 SSH 从 22 迁移到随机高端口存在争议:它能减少日志中大部分僵尸网络的噪音,但这属于通过隐晦实现的安全(security-through-obscurity),而非真正的防护。fail2ban + 仅密钥认证 + 禁止 root 才是真正有效的防御。如果你确实要移动端口,记得 ufw allow <new-port>/tcp 并更新本地的 ~/.ssh/config。
监控与日志传输——本地 syslog 作为审计记录还不错,但在服务器无法访问时毫无调试价值。CloudWatch agent(AWS)、Grafana Cloud(免费层)或 Sentry(用于应用错误)可以将日志传输到服务器之外。
备份——服务商快照是最简单的基础。对于数据库状态,通过 cron 定期执行 pg_dump | gzip | aws s3 cp -,并定期验证恢复流程。
幂等性重复执行——当你需要在第二台服务器上重复这些操作时,将它写成 Ansible playbook 或 bash 脚本。服务商专属镜像是下一步的选择;枯燥的文本脚本才是正确的起点。