前言

新买的 VPS 能通过 SSH 登录,并不代表基础安全已经配置完毕。本文以 Debian 13 为例,从本机生成 SSH 密钥开始,完成普通管理账号、SSH 登录限制、防火墙和自动安全更新的配置。

本机可以使用 macOS 或 Windows,SSH 客户端不限品牌。文中的命令行示例使用标准 OpenSSH;使用图形客户端时,按对应字段填写即可。

本文适合刚安装系统、尚未部署业务的新服务器。已经运行网站、容器、VPN 或其他服务的机器,需要先核对现有账号、端口和防火墙规则,再决定如何合并配置。

一、先看懂操作位置和自定义参数

1. 哪些命令在本机执行,哪些在服务器执行

本文使用三种标记:

标记执行位置使用的环境
本机 · macOS自己的 Mac终端中的 Bash 或 Zsh
本机 · Windows自己的 Windows 电脑PowerShell,已安装 OpenSSH 客户端
服务器已连接 VPS 的 SSH 窗口Debian 的 Bash

Mac 和 Windows 的本机步骤是两条分支,选择自己正在使用的一条。服务器步骤相同。

建议保留一个窗口用于本机命令,另一个窗口用于连接服务器。执行 ssh 后,这个窗口就进入了服务器;不要在里面误执行标记为“本机”的命令。新开本机终端时,也要重新设置该终端需要使用的连接和密钥变量。

不要把整篇文章一次性粘贴执行。 尤其是安装软件、创建用户、输入密码等交互步骤,应逐步执行,核对输出后再继续。遇到报错,先解决当前问题。

2. 可以自定义的内容

参数或项目本文示例自定义说明
服务器地址203.0.113.10替换为自己的公网 IP 或域名。这是文档示例地址,不能直接用于连接
初始登录用户root如果商家提供的是可使用 sudo 的普通账号,填写那个账号
新管理用户名myadmin可以自定义,例如 imx、opsuser。建议以小写英文字母开头,使用字母、数字、下划线或连字符;不要用 root 或已有系统账号
当前 SSH 端口22必须填写服务器实际正在监听的端口;设置变量不会修改服务器的 SSH 监听端口
本机私钥文件名vps_ed25519可以改名,也可以使用 .pem 后缀;后缀不决定文件内容格式
密钥备注my-laptop可以自定义,用来区分设备或用途;建议使用简短英文,不要填写密码
密钥类型ed25519由 -t 指定,适用于本文环境;其他类型需要同时核对客户端和服务器兼容性,不靠修改文件后缀转换
私钥密码派生轮数64由 -a 指定,可调整;使用 Passphrase 时影响私钥保护的派生开销,数值越大,解锁通常越慢
上传到服务器的公钥文件名client-login.pub可以改名,但本机上传和服务器读取时必须一致
密钥保护密码创建密钥时输入自行设置,用于保护本机私钥
新用户密码创建账号时输入自行设置,后续 sudo 提权时使用
自动回滚等待时间10m可以改成 15m 等,给自己留出重新连接和验证的时间
回滚任务名称ufw-safety-rollback、ssh-safety-rollback可以改名,但创建、检查和停止任务时必须使用同一个名称
业务端口与协议按实际服务填写部署服务后再放行,注意区分 TCP 和 UDP

/etc/ssh/sshd_config、authorized_keys 等是本教程使用的标准配置路径或文件名,不是随意替换的占位符。如果镜像修改了默认路径,应先核对实际配置。

本机和服务器的变量互不共享。新开终端、切换用户或执行 sudo -i 后,需要重新设置后续步骤依赖的变量。

3. 先保留恢复途径

修改前确认商家是否提供网页控制台、VNC、救援模式或密码重置等恢复方式。没有控制台时,更要保留当前有效的 SSH 窗口,并按本文设置临时回滚任务。

验证登录是否正常,要建立一条新的 SSH 连接,不能只看旧窗口是否还能输入命令。旧会话可以在新登录被禁止后继续保持连接。

本文的定时回滚任务只存在于当前启动周期,配置和验证期间不要重启服务器。它只能撤销本文指定的服务器内改动,不能撤销商家安全组的修改,也不能修复服务器本身无法启动等故障。

二、理解公钥、私钥和两种密码

SSH 密钥通常成对生成:

内容放在哪里用途
私钥自己的电脑或客户端密钥库登录时证明身份,需要妥善保护
公钥服务器目标用户的 authorized_keys授权对应私钥登录,可以公开
私钥保护密码,即 Passphrase自己记住或存入密码管理器解锁本机私钥,不是服务器登录密码
服务器用户密码服务器账号对应的密码本文后续主要用于 sudo 提权

本文采用在本机生成密钥,只把公钥上传服务器的方式。私钥不需要上传到商家后台或服务器。

首次连接服务器时,客户端还会提示确认服务器主机指纹。它用于识别服务器,与个人登录公钥的指纹是两回事。应通过商家提供的信息或其他可信途径核对;遇到主机指纹变化提示,先确认是否重装过系统或连接到了错误地址。

三、在本机生成并保存 SSH 密钥

1. 本机 · macOS

设置自己的参数:

SERVER_HOST='203.0.113.10'
INITIAL_USER='root'
SSH_PORT=22
KEY_FILE="$HOME/.ssh/vps_ed25519"
KEY_COMMENT='my-laptop'

创建存放密钥的目录:

mkdir -p "$HOME/.ssh"
chmod 700 "$HOME/.ssh"

先检查文件名是否已被使用:

ls -l "$KEY_FILE" "${KEY_FILE}.pub"

如果提示两个文件不存在,可以继续。如果已经存在,先确认是否是已有密钥;需要新建时应换一个文件名,不要覆盖旧私钥。

生成 Ed25519 密钥:

ssh-keygen -t ed25519 -a 64 -f "$KEY_FILE" -C "$KEY_COMMENT"

按提示输入并确认 Passphrase,建议设置。输入时不会显示字符,这是正常现象。如果出现是否覆盖文件的提示,输入 n,先检查文件名。

生成后会得到:

~/.ssh/vps_ed25519       私钥
~/.ssh/vps_ed25519.pub   公钥

设置私钥权限:

chmod 600 "$KEY_FILE"

验证私钥能否解析,不显示其内容:

ssh-keygen -y -f "$KEY_FILE" >/dev/null

如果私钥设置了保护密码,这里会要求输入。没有报错并返回命令提示符,表示文件可以解析,尚不代表服务器登录已经验证。

2. 本机 · Windows

在 PowerShell 中先检查 OpenSSH 客户端是否可用:

ssh -V
Get-Command ssh-keygen, scp

如果命令不存在,通过 Windows 的“可选功能”安装 OpenSSH 客户端。这里只需要客户端,不需要在本机启用 SSH 服务器。

设置自己的参数:

$ServerHost = '203.0.113.10'
$InitialUser = 'root'
$SshPort = 22
$KeyFile = Join-Path $env:USERPROFILE '.ssh\vps_ed25519'
$KeyComment = 'my-laptop'

创建目录并检查文件名:

New-Item -ItemType Directory -Force -Path (Split-Path -Parent $KeyFile) | Out-Null
Test-Path -LiteralPath $KeyFile
Test-Path -LiteralPath "${KeyFile}.pub"

两个结果应为 False。如果任一结果为 True,先确认已有文件,或修改 $KeyFile 使用新文件名,不要直接覆盖。

生成密钥:

ssh-keygen -t ed25519 -a 64 -f $KeyFile -C $KeyComment

按提示设置并确认 Passphrase。生成的私钥和 .pub 公钥位于当前用户的 .ssh 文件夹。

验证私钥能否解析:

ssh-keygen -y -f $KeyFile > $null
if ($LASTEXITCODE -ne 0) { throw '私钥解析失败,请先解决格式、权限或保护密码问题。' }

Windows 使用文件 ACL 管理访问权限,不能照搬 macOS 的 chmod 600。OpenSSH 新生成的私钥通常已具有合适权限;如果复制文件后出现权限过宽的告警,按文末的 Windows 权限修复步骤处理。

3. 私钥如何保存和备份

私钥直接由 ssh-keygen 写入文件,不需要先复制成字符串,再通过文本编辑器保存。

文件名是否带 .pem 不影响密钥本身。把富文本、Word 文档或其他格式改成 .pem 后缀,并不会将它转换为私钥文件。

私钥应保存在自己控制的设备上,并保留加密备份。可以使用支持加密附件的密码管理器,或其他可信的加密备份方式。不要把私钥提交到博客、公开仓库或截图中。

四、上传公钥,验证密钥登录

这一阶段保留服务器原有的有效登录方式,先验证密钥,再关闭密码认证。

1. 将公钥上传到初始账号的家目录

本机 · macOS:

scp -P "$SSH_PORT" "${KEY_FILE}.pub" "${INITIAL_USER}@${SERVER_HOST}:~/client-login.pub"

本机 · Windows:

scp -P $SshPort "${KeyFile}.pub" "${InitialUser}@${ServerHost}:~/client-login.pub"

需要原账号密码时,输入服务器初始账号的密码。scp 指定端口使用大写 -P,ssh 指定端口使用小写 -p。

如果商家提供的初始账号已经只允许使用另一把密钥登录,应为本节的 scp 和初始 ssh 命令补上 -i,指定那把已有私钥,或使用已经配置好的客户端连接。本机新生成的公钥尚未安装前,不能用它取代原有登录凭据。

使用图形客户端也可以完成这一步:通过 SFTP 上传公钥文件到初始账号家目录,文件名使用 client-login.pub,或自己选定的名称。

2. 用初始账号登录服务器

本机 · macOS:

ssh -p "$SSH_PORT" "${INITIAL_USER}@${SERVER_HOST}"

本机 · Windows:

ssh -p $SshPort "${InitialUser}@${ServerHost}"

按原有有效方式完成登录,保留这个会话。此时还没有关闭密码认证,也没有禁止 root 登录。

3. 在服务器初始账号中安装公钥

服务器 · 当前初始登录账号。此处尚未切换到其他用户。

PUB_FILE="$HOME/client-login.pub"
ssh-keygen -l -f "$PUB_FILE"

应能输出公钥的指纹信息。如果无法解析,先检查是否上传了正确的 .pub 文件。

按顺序执行:

install -d -m 700 "$HOME/.ssh"
touch "$HOME/.ssh/authorized_keys"
chmod 600 "$HOME/.ssh/authorized_keys"
printf '\n' >> "$HOME/.ssh/authorized_keys"
cat "$PUB_FILE" >> "$HOME/.ssh/authorized_keys"

这里采用追加方式,保留原有授权。空行用于避免已有文件末尾缺少换行时,两把公钥粘在同一行。这一组不必反复运行,以免重复添加同一把公钥。

标准配置使用用户家目录下的 .ssh/authorized_keys。如果商家镜像修改了 AuthorizedKeysFile,或使用其他公钥授权机制,需要按实际路径配置,不能仅凭文件名判断。

4. 建立新的密钥连接

本机 · macOS:

ssh -o ControlPath=none -o IdentitiesOnly=yes -o PreferredAuthentications=publickey -i "$KEY_FILE" -p "$SSH_PORT" "${INITIAL_USER}@${SERVER_HOST}"

本机 · Windows:

ssh -o ControlPath=none -o IdentitiesOnly=yes -o PreferredAuthentications=publickey -i $KeyFile -p $SshPort "${InitialUser}@${ServerHost}"

这些选项用于测试指定密钥,并避免复用已有的 SSH 控制连接或回退到服务器密码认证。私钥设置了 Passphrase 时,输入的是私钥保护密码。

使用图形客户端时,新建或复制一个连接配置,填写:

客户端字段填写内容
地址自己的服务器公网 IP 或域名
端口当前实际 SSH 端口
用户名当前初始登录账号
认证方式密钥或公钥认证
私钥本机生成的私钥文件,或导入后的密钥条目
Passphrase创建密钥时设置的保护密码;未设置则留空

具体字段名称因客户端而异。需要导入的是私钥,不是 .pub 公钥。验证时只启用密钥认证,避免密码回退掩盖密钥配置错误。

登录成功后执行 whoami,确认是预期账号。保留原有 SSH 窗口,继续后面的服务器步骤。

五、更新系统并开启自动安全更新

1. 进入服务器管理环境

接下来的服务器配置步骤在 root 的 Bash 环境中执行。

如果初始账号就是 root,可以直接继续。如果是具有 sudo 权限的普通账号,先单独执行:

sudo -i

确认提示符已经进入 root 环境,再执行后续命令。不要把 sudo -i 和后面的配置命令当成一个代码块一次粘贴。

重新设置服务器端参数,和自己的实际情况保持一致:

INITIAL_USER='root'
ADMIN_USER='myadmin'
SSH_PORT=22
PUB_UPLOAD_NAME='client-login.pub'
ROLLBACK_DELAY='10m'

如果最初登录的是其他账号,INITIAL_USER 填那个账号;ADMIN_USER 填自己准备创建的新管理用户名;SSH_PORT 填当前实际监听端口。

2. 更新系统

apt update && apt upgrade

按提示确认更新。如果出现 /etc/ssh/sshd_config 的配置文件更新询问,先选择保留当前本地版本,再单独审查差异,避免覆盖镜像中已有的登录配置。

出现软件包锁占用时,应等待正在进行的 APT 任务结束,不要通过删除锁文件或强行终止进程解决。

3. 安装 sudo 和自动更新组件

apt install sudo unattended-upgrades

安装完成后,再执行:

dpkg-reconfigure -plow unattended-upgrades

询问是否自动下载、安装更新时选择 Yes/是。沿用 Debian 默认允许的更新来源;默认规则通常包含当前发行版和安全更新来源,并非只安装名称中带有“安全”的更新。默认来源配置

如果不希望无人值守更新自动重启服务器,可以明确设置:

cat > /etc/apt/apt.conf.d/99-local-no-auto-reboot <<'EOF'
Unattended-Upgrade::Automatic-Reboot "false";
EOF

这是本文新增的本地配置文件;如果已有同名文件,应先备份并合并内容。关闭自动重启后,内核等更新可能需要自己安排维护时间重启,才能完整生效。

启用定时器:

systemctl enable --now apt-daily.timer apt-daily-upgrade.timer

4. 核对配置和执行情况

apt-config dump | awk '/APT::Periodic::(Update-Package-Lists|Unattended-Upgrade)|Unattended-Upgrade::Automatic-Reboot /'

应包含:

APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
Unattended-Upgrade::Automatic-Reboot "false";

查看定时任务,不使用分页器:

systemctl list-timers --all --no-pager apt-daily.timer apt-daily-upgrade.timer

需要时可以进行一次模拟检查,不实际安装更新:

unattended-upgrade --dry-run --debug

检查允许来源中是否包含本发行版的安全更新来源,有没有解析、软件源或权限错误。真实执行记录可以通过下面的命令查看:

journalctl -u apt-daily-upgrade.service -n 50 --no-pager
tail -n 50 /var/log/unattended-upgrades/unattended-upgrades.log

刚安装且尚未执行过时,日志可能还不存在。配置已开启、定时器已排期,不等于已经成功安装过更新。 通过脚本或独立二进制部署的软件,也需要按照各自的更新方式维护。Debian 安全说明、unattended-upgrade 手册

六、创建自定义管理账号

服务器 · 继续在前面的 root Bash 会话操作。 如果新开了会话,先重新设置 INITIAL_USER、ADMIN_USER、PUB_UPLOAD_NAME 等变量。

1. 确认用户名未被占用

getent passwd "$ADMIN_USER"

新用户名没有被占用时,这条命令没有输出。如果有输出,先确认已有账号用途,或换一个名字。下面的公钥安装命令针对本节新创建的用户,不要直接用于覆盖已有用户的授权文件。

2. 创建账号并加入 sudo 组

adduser "$ADMIN_USER"

按提示设置并确认用户密码,姓名、房间等信息可回车跳过,最后确认保存。这个密码后续用于 sudo,不是私钥的 Passphrase。

创建成功后执行:

usermod -aG sudo "$ADMIN_USER"

3. 为新账号安装本机生成的公钥

根据系统账号记录获取家目录和主组,不把 /home/某个用户名 写死:

INITIAL_HOME=$(getent passwd "$INITIAL_USER" | cut -d: -f6)
ADMIN_HOME=$(getent passwd "$ADMIN_USER" | cut -d: -f6)
ADMIN_GROUP=$(id -gn "$ADMIN_USER")
PUB_FILE="${INITIAL_HOME}/${PUB_UPLOAD_NAME}"

先确认公钥文件存在并可以解析:

ssh-keygen -l -f "$PUB_FILE"

如果报错,先检查初始用户和上传路径,不要继续安装。确认无误后执行:

install -d -m 700 -o "$ADMIN_USER" -g "$ADMIN_GROUP" "$ADMIN_HOME/.ssh"
install -m 600 -o "$ADMIN_USER" -g "$ADMIN_GROUP" "$PUB_FILE" "$ADMIN_HOME/.ssh/authorized_keys"

这里安装的是本次上传的公钥,不是把本机私钥复制到服务器。

4. 新开管理账号连接并验证 sudo

本机 · macOS:

ADMIN_USER='myadmin'
ssh -o ControlPath=none -o IdentitiesOnly=yes -o PreferredAuthentications=publickey -i "$KEY_FILE" -p "$SSH_PORT" "${ADMIN_USER}@${SERVER_HOST}"

本机 · Windows:

$AdminUser = 'myadmin'
ssh -o ControlPath=none -o IdentitiesOnly=yes -o PreferredAuthentications=publickey -i $KeyFile -p $SshPort "${AdminUser}@${ServerHost}"

使用图形客户端时,复制原连接配置,将登录用户名改成新管理用户名,地址、端口和私钥保持一致,并重新建立连接。

在新管理账号会话中分别执行:

whoami

应输出自己设置的新用户名。再执行:

sudo whoami

如果要求密码,输入刚创建的新用户密码,应输出 root。

只有新账号的密钥登录和 sudo 均已验证,才能继续关闭 root 直接登录。保留这个有效的新用户会话,也保留前面的服务器管理会话。

七、配置双栈防火墙,并验证新连接

服务器 · 切回前面的 root Bash 会话。 后面的 SSH_PORT 必须是当前实际 SSH 端口,ROLLBACK_DELAY 是自己设置的回滚等待时间。

1. 核对端口和现有规则

ss -lntup
/usr/sbin/sshd -T | awk '/^(port|listenaddress|authorizedkeysfile|pubkeyauthentication) /'

确认自己的 SSH 端口;同时记录已经需要对外提供服务的其他端口。监听在 127.0.0.1 上的本地服务与监听在所有地址上的服务含义不同,不能只看到端口数字就全部放行。

安装 UFW 和用于检查完整 nftables 规则的命令行工具:

apt install ufw nftables

Debian 13 官方 nftables 软件包首次安装时不默认启用或启动 nftables.service。这里安装它用于检查规则,后续由 UFW 管理防火墙,不另外启动一套 nftables 配置服务。打包配置

安装后检查:

ufw status verbose
nft list ruleset
iptables-save
ip6tables-save
systemctl is-active nftables firewalld

本文接下来的启用步骤以 UFW 为 inactive、没有需要保留的已有规则、没有另一套防火墙管理服务正在运行为前提。nft 能检查原生 nftables 规则;iptables-save 和 ip6tables-save 分别检查兼容层的 IPv4、IPv6 规则。

如果看到现有规则或正在使用其他防火墙管理器,先确认其用途,不要执行清空规则、重置 UFW 或覆盖现有策略的命令。容器、VPN 和商家镜像可能维护自己的规则。

2. 配置规则,先放行现有 SSH 端口

满足上述前提后执行:

sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
ufw default deny incoming
ufw default allow outgoing
ufw allow "${SSH_PORT}/tcp"

确认 IPv6 配置:

awk '/^IPV6=/' /etc/default/ufw

应显示 IPV6=yes。如果不是,先检查配置,不要跳过 IPv6 的过滤。

新服务器尚无业务时,业务入站端口先显式放行 SSH 即可。如果已经运行其他必要服务,应在启用前补上它们的端口和协议。

UFW 会保留回环、已建立连接和部分网络必需报文等基础规则。因此“默认拒绝入站”并不等于禁止所有 ICMP 或所有非 SSH 数据包。UFW 手册

3. 创建临时回滚任务

systemd-run --unit=ufw-safety-rollback --on-active="$ROLLBACK_DELAY" /usr/sbin/ufw disable

确认任务已经启动:

systemctl is-active ufw-safety-rollback.timer
systemctl list-timers --all --no-pager ufw-safety-rollback.timer

第一条应输出 active,第二条应显示下一次执行时间。创建任务如果报错,先停止,不要启用防火墙。不要把同名旧任务仍在运行误当成这次创建成功。

这个回滚任务会在等待时间到达后关闭 UFW,适用于本节原先 UFW 未启用的场景。它不会恢复商家安全组,也不是已有复杂防火墙策略的通用备份工具。

4. 启用防火墙

ufw enable

由于已经放行实际 SSH 端口,并确认回滚任务有效,出现远程连接警告时可以输入 y。查看结果:

ufw status verbose

如果 SSH 使用 22 端口,应能看到类似结果:

Status: active
Default: deny (incoming), allow (outgoing), ...

22/tcp        ALLOW IN    Anywhere
22/tcp (v6)   ALLOW IN    Anywhere (v6)

使用其他 SSH 端口时,数字应对应自己的实际端口。

5. 新开连接,成功后取消回滚

从本机重新建立一条新管理账号的 SSH 连接,确认能登录并执行 sudo whoami,输出 root。

在这个新管理账号会话中执行:

sudo systemctl stop ufw-safety-rollback.timer
sudo ufw status verbose

最后仍应显示 Status: active。如果已经显示 inactive,可能是回滚任务已执行,需要重新安排回滚、启用并重新验证,不能把连接恢复当作防火墙启用成功。

如果新连接失败,不取消回滚;从任务创建时计算,等待设定时间到达后重试。不要关闭仍有效的旧窗口,也不要在等待期间重启服务器。

八、关闭 root 直接登录和 SSH 密码认证

前提是新管理账号已经能通过密钥建立新连接,并通过 sudo 提权。本节修改只在验证这些前提之后执行。

服务器使用标准 /etc/ssh/sshd_config 和 ssh.service 时,可按下面的方式配置。如果服务通过 -f 使用其他配置文件、通过 -o 覆盖配置,或包含复杂的 Match 规则,应先核对实际生效范围。

1. 回到 root 管理会话,备份配置

可以切回保留的 root 管理窗口;也可以在新管理账号窗口中先单独执行 sudo -i。如果切换了环境,重新设置 ROLLBACK_DELAY='10m',或自己选择的等待时间。

检查现有配置的语法:

/usr/sbin/sshd -t

没有报错后,生成独立备份:

SSH_BACKUP=$(mktemp /root/sshd_config.before-hardening.XXXXXX)
install -m 600 /etc/ssh/sshd_config "$SSH_BACKUP"
printf '本次配置备份:%s\n' "$SSH_BACKUP"

记录这次输出的备份路径。后续步骤继续在同一个 root Bash 会话执行,避免丢失 $SSH_BACKUP 变量。重复操作时应使用新的备份,不覆盖以前的备份。

2. 先安排 SSH 配置回滚

systemd-run \
  --unit=ssh-safety-rollback \
  --on-active="$ROLLBACK_DELAY" \
  --expand-environment=no \
  /bin/sh -c 'cp -p "$1" /etc/ssh/sshd_config && /usr/sbin/sshd -t && /usr/bin/systemctl reload ssh' \
  sh "$SSH_BACKUP"

该任务会恢复本次备份,检查语法后重新加载 SSH。--expand-environment=no 避免 systemd 提前展开交给 Shell 使用的参数。

检查任务:

systemctl is-active ssh-safety-rollback.timer
systemctl list-timers --all --no-pager ssh-safety-rollback.timer

必须确认本次创建成功、状态为 active,并有下一次执行时间,才继续。

3. 写入登录策略

标准 OpenSSH 的全局配置通常采用最先读到的值。单纯在文件末尾添加配置,或创建一个排序较后的片段文件,可能不能覆盖已有配置。因此这里在主配置开头写入策略,并保留备份中的其余内容。

{
  printf '%s\n' \
    'PermitRootLogin no' \
    'PubkeyAuthentication yes' \
    'PasswordAuthentication no' \
    'KbdInteractiveAuthentication no'
  cat "$SSH_BACKUP"
} > /etc/ssh/sshd_config

这一步关闭的是 SSH 的 root 直接登录和密码、键盘交互认证,不是删除 root 账号。管理账号仍然可以通过 sudo 获取 root 权限。sshd_config 手册

4. 检查语法和生效值

/usr/sbin/sshd -t

应没有报错。然后执行:

/usr/sbin/sshd -T | awk '/^(permitrootlogin|pubkeyauthentication|passwordauthentication|kbdinteractiveauthentication) /'

预期结果:

permitrootlogin no
pubkeyauthentication yes
passwordauthentication no
kbdinteractiveauthentication no

如果语法检查失败,或实际值不是预期结果,先停止,不要重新加载 SSH。 可以在仍有效的管理会话中使用下面的命令恢复刚才的配置,再排查原因:

cp -p "$SSH_BACKUP" /etc/ssh/sshd_config
/usr/sbin/sshd -t

sshd -T 默认展示全局配置。存在 Match User、Match Address 等规则时,还应检查对应连接上下文。例如在带有 $SSH_CONNECTION 的原 SSH 登录 Shell 中,可模拟当前来源对 root 的连接配置:

read -r CHECK_CLIENT_IP CHECK_CLIENT_PORT CHECK_SERVER_IP CHECK_SERVER_PORT <<< "$SSH_CONNECTION"
sudo /usr/sbin/sshd -T -C "user=root,addr=${CHECK_CLIENT_IP},host=${CHECK_CLIENT_IP},laddr=${CHECK_SERVER_IP},lport=${CHECK_SERVER_PORT}"

这里以客户端 IP 作为测试的 host 值。若配置使用反向解析主机名匹配,需要按实际解析结果填写;也应针对新管理账号和实际访问来源分别核对。不要把一次全局输出当作所有来源、所有用户都已验证。

5. 重新加载并建立新连接

检查符合预期后执行:

/usr/sbin/sshd -t && systemctl reload ssh

从本机再次建立一条新的管理账号连接,并分别执行:

whoami
sudo whoami

确认分别输出自己的管理用户名和 root。然后尝试建立一条全新的 root SSH 连接,应被拒绝。应使用先前已验证可用的同一把密钥、同一个地址和端口,避免把错误地址、错误密钥或复用旧会话误判为策略验证成功。

图形客户端有时会把账号被拒绝显示为“用户名/公钥组合错误”或其他泛化错误,不能仅凭一句错误文本判断原因。需要结合服务器的有效配置和新管理账号的正常登录一起判断。

6. 验证成功后取消回滚

在新管理账号会话中执行:

sudo systemctl stop ssh-safety-rollback.timer
sudo /usr/sbin/sshd -T | awk '/^(permitrootlogin|pubkeyauthentication|passwordauthentication|kbdinteractiveauthentication) /'

确认最终仍是预期设置。如果回滚已经执行,必须重新配置并验证,不要把被恢复后的配置当作加固完成。

如果新的管理账号无法登录,保留回滚任务,等待指定时间后重试;仍可用的旧会话也可以用来检查并恢复配置。已有 root 会话暂时还能使用属于正常现象,新策略主要限制后续新连接。

九、最终检查与日常使用

使用新管理账号执行:

whoami
sudo whoami
sudo ufw status verbose
sudo /usr/sbin/sshd -T | awk '/^(port|permitrootlogin|pubkeyauthentication|passwordauthentication|kbdinteractiveauthentication) /'
apt-config dump | awk '/APT::Periodic::(Update-Package-Lists|Unattended-Upgrade)|Unattended-Upgrade::Automatic-Reboot /'
systemctl list-timers --all --no-pager apt-daily.timer apt-daily-upgrade.timer

检查结果应与自己的自定义参数一致:

检查项预期结果
新的密钥连接使用自己的管理用户名登录成功
sudo 提权sudo whoami 输出 root
root 新连接被拒绝,服务器有效配置为 permitrootlogin no
SSH 认证公钥开启,密码及键盘交互认证关闭
UFWactive,实际 SSH 端口在 IPv4、IPv6 均有正确规则
APT 自动更新两个周期配置为 1,对应定时器已启动并排期
自动重启本文设置为 false,需要自己安排必要的维护重启
两个临时回滚任务验证成功后已停止,配置未被自动恢复

日常通过普通管理账号登录,需要临时进入 root 环境时执行:

sudo -i

完成管理操作后执行 exit 返回普通用户。安装业务软件后,仍要维护业务软件自身的版本、账号、管理入口和授权信息。

基础配置检查通过,表示本教程覆盖的项目符合预期,不等于完成了公网扫描、漏洞审计或所有软件的安全检查。

十、按需自定义端口、客户端和多设备使用

1. 放行业务端口

下面以一个自行选择的端口为例,先把数字改成服务实际监听的端口:

SERVICE_PORT=8443
sudo ufw allow "${SERVICE_PORT}/tcp"

只有服务实际需要 UDP 时,才单独添加:

sudo ufw allow "${SERVICE_PORT}/udp"

检查结果:

sudo ufw status numbered

不再使用某项放行规则时,可以按完整规则删除,例如:

sudo ufw delete allow "${SERVICE_PORT}/tcp"

操作前确认目标是业务端口,不是当前依赖的 SSH 端口。不要为了省事开放整个端口范围。

商家安全组和系统防火墙是两层独立控制。商家层如果有阻断规则,仅在 UFW 中放行仍可能无法访问。本文的回滚任务不会修改商家安全组。

2. 自定义 SSH 端口

SSH 端口可以自定义,但本文主线使用已有的实际 SSH 端口,并不要求修改它。

修改 SSH_PORT 变量只会影响后续命令连接或放行的端口,不会修改 SSH 服务。若需要迁移监听端口,应另做一次有备份和回滚的变更:

  1. 确认新端口未被占用,并在商家安全组及 UFW 中先放行新端口。
  2. 在 SSH 服务中暂时同时监听旧端口和新端口,检查语法,再重新加载。
  3. 通过新端口重新建立管理账号连接,并验证 sudo。
  4. 验证成功后,再移除旧端口监听和对应放行规则,并取消该次回滚任务。

标准 OpenSSH 支持重复的 Port 项,但 ListenAddress、服务启动参数和 socket 激活配置也可能影响实际监听地址或端口。应核对 ss -lntp、服务单元及实际连接结果,不要只改一个数字就关闭旧入口。

3. 使用自己习惯的 SSH 客户端

本文不依赖具体客户端。只要客户端支持所选密钥类型,就可以使用其密钥导入功能,或直接选择本地私钥文件。

切换客户端时需要对应好:地址、实际 SSH 端口、管理用户名、私钥,以及私钥保护密码。服务器用户密码和 Passphrase 不是同一项。

4. Mac 与 Windows 共用一把密钥

同一把私钥可以复制到自己控制的 Mac 和 Windows 上使用。macOS 的 chmod 600 不会把文件绑定到 Mac,也不会阻止文件所有者复制它;传到 Windows 后,访问权限由 Windows 的 ACL 管理。

跨设备传输时使用可信的加密方式。无需为了复制到 Windows,把 Mac 私钥权限放宽为 644 或 777。

长期使用时,也可以为每台设备单独生成密钥,把各设备公钥分别加入管理账号的 authorized_keys。这样某台设备丢失时,只需撤销那台设备的公钥,不必同时替换其他设备的密钥。

十一、常见问题

1. invalid format:私钥格式无效

常见原因包括:选错文件、复制不完整、保存成 RTF 富文本、把带有字面量 \n 的字符串直接保存,或客户端不支持当前格式。

优先检查自己是否选择了本机生成的私钥文件,而不是 .pub、网页文本、截图或其他文档。扩展名不是判断依据。

macOS 可以只检查文件类型,不输出私钥内容:

file -b "$KEY_FILE"
ssh-keygen -y -f "$KEY_FILE" >/dev/null

Windows 可以重新运行:

ssh-keygen -y -f $KeyFile > $null
if ($LASTEXITCODE -ne 0) { throw '私钥尚未通过解析检查。' }

如果私钥曾经手工保存,必须完整保留头尾标记、正文和换行。使用纯文本,不要只改富文本文件的后缀。没有备份且不能恢复时,应在仍有效的登录会话中部署新的公钥,验证成功后再撤销旧授权。

2. 私钥权限过宽

macOS:

chmod 700 "$HOME/.ssh"
chmod 600 "$KEY_FILE"

Windows 的修复方式不同。下面的 PowerShell 示例仅修改 $KeyFile 指向的单个私钥文件,将所有者设为当前 Windows 用户,并只给该用户显式文件访问权限。先核对路径,不要改成整个用户目录或磁盘。

$KeyFile = Join-Path $env:USERPROFILE '.ssh\vps_ed25519'
$CurrentSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
$PrivateAcl = [System.Security.AccessControl.FileSecurity]::new()
$PrivateAcl.SetOwner($CurrentSid)
$PrivateAcl.SetAccessRuleProtection($true, $false)
$PrivateRule = [System.Security.AccessControl.FileSystemAccessRule]::new(
    $CurrentSid,
    [System.Security.AccessControl.FileSystemRights]::FullControl,
    [System.Security.AccessControl.AccessControlType]::Allow
)
$PrivateAcl.AddAccessRule($PrivateRule)
Set-Acl -LiteralPath $KeyFile -AclObject $PrivateAcl
Get-Acl -LiteralPath $KeyFile | Format-List Owner, AccessToString

如果文件所有者不是自己、文件位于共享目录、文件系统不支持相应 ACL,或账号受组织策略管理,应先按实际情况处理,不要直接放宽全目录权限。

权限保护不等于加密,也不能防止以同一用户身份运行的恶意程序读取文件。私钥 Passphrase 和设备本身的安全仍然重要。Windows OpenSSH 权限说明

3. 提示用户名或公钥不匹配

检查地址、端口、用户名、选择的私钥是否正确,以及目标用户的 authorized_keys 是否包含对应公钥。一个账号授权了公钥,不代表另一个账号自动也被授权。

服务器上可以查看相关日志:

sudo journalctl -u ssh -n 50 --no-pager

如果只是在关闭 root 直登后,root 新连接失败,而新管理账号可以正常通过密钥登录,这是预期结果;结合有效配置确认,不要直接重新生成所有密钥。

4. 防火墙启用后无法建立新连接

优先使用仍有效的旧会话检查端口和规则,或等待已经确认启动的临时回滚任务到时执行。如果旧会话仍可用,也可以手动关闭本文刚启用的 UFW:

sudo ufw disable

它会关闭 UFW 并取消其开机启用配置,只适合明确需要撤销本次 UFW 启用的场景。重新确认实际监听端口、IPv4/IPv6、商家安全组和客户端参数后,再安排回滚并重新配置。

5. 定时器出现同名任务已存在

先查看任务状态:

systemctl list-timers --all --no-pager ufw-safety-rollback.timer ssh-safety-rollback.timer

不要无条件停止一个仍在保护当前改动的回滚任务。先明确它对应哪次变更、是否已经完成连接验证,再决定取消、等待回滚或使用新的任务名称。

6. 软件更新以后是否还需要检查

需要。自动更新负责允许来源中的系统软件包;通过脚本安装的应用、节点核心和管理面板通常有自己的更新方式。内核更新后也可能需要维护重启。

每次变更 SSH、防火墙或商家安全组,都应保留有效连接、准备适用于本次变更的回滚,并通过新连接验证。备份私钥、用户密码和必要的业务配置,确认自己知道商家的恢复渠道。

7. 公钥丢失、私钥丢失和更换密钥

有私钥时,可以使用 ssh-keygen -y -f 私钥路径 重新导出对应公钥;反过来,仅凭公钥不能恢复私钥。导出时把输出保存为新的公钥文件,不能覆盖私钥本身。

更换密钥时,先把新公钥追加到目标账号的 authorized_keys,通过新私钥建立新连接并验证 sudo,再删除旧公钥对应的那一行。不要直接清空整个授权文件。

如果丢失了唯一私钥,又没有其他已授权设备、有效会话或商家恢复入口,仅靠记得用户密码不能绕过本文已关闭的 SSH 密码认证。这也是需要提前做好加密备份的原因。

参考资料