# SSH 密钥与 Root 维护规范

> 适用目标：让新的 Windows、Linux 或 macOS 客户端通过 Tailscale，以独立 SSH 公钥直接登录 Ubuntu 服务器的 `root` 账户。

## 一、优先选择更安全的方式

日常管理推荐先登录普通账户，再提升权限：

```bash
ssh zrh@100.99.232.7
sudo -i
```

与 root 直连相比，这种方式保留登录用户和提权记录。只有无人值守运维、救援或明确要求直接获得 root shell 时，才启用本文的 root SSH 登录。

服务器已知信息：

| 项目 | 值 |
| --- | --- |
| 主机名 | `zrh-dracarys` |
| Tailscale IP | `100.99.232.7` |
| SSH 端口 | `22` |
| ED25519 主机指纹 | `SHA256:1KPaatBGKwjQjQu5+5zTEcsCLUYZp5o3J8xkeXDd9so` |

---

## 二、前提条件

添加新设备前，必须同时满足：

1. 新设备已加入与服务器相同的 Tailscale tailnet，并通过设备审批和 ACL。
2. 新设备能够 `tailscale ping 100.99.232.7`。
3. Ubuntu 的 `sshd` 正在监听 22 端口。
4. 当前仍保留一个可用的 `zrh` 管理会话，配置失败时可以回退。
5. 新设备拥有自己的密钥对；不得复制其他设备的私钥。
6. root 只允许公钥认证，不允许密码或键盘交互认证。

Linux 上的只读检查：

```bash
sudo systemctl is-active ssh
sudo ss -lntp | grep ':22'
sudo sshd -t
```

在新客户端检查网络：

```text
tailscale status
tailscale ping 100.99.232.7
```

---

## 三、每次添加设备必须提供的信息

只需要提供下列内容：

| 信息 | 示例 |
| --- | --- |
| 客户端设备名 | `2024-bg-217` |
| 客户端 Tailscale IP | `100.x.y.z` |
| 完整 SSH 公钥 | `ssh-ed25519 AAAA... device-to-zrh-root` |
| 公钥 SHA-256 指纹 | `SHA256:...` |
| 是否允许端口/X11/Agent 转发 | 默认全部禁止 |

绝对不能提供：

- 没有 `.pub` 后缀的私钥文件
- 私钥文本内容
- 私钥口令
- Linux 用户密码或 root 密码
- Tailscale 登录密钥或账户凭据

公钥末尾的注释建议使用：

```text
<客户端设备名>-to-zrh-root
```

---

## 四、在新客户端生成独立密钥

### Windows PowerShell

先设置设备名，并使用独立文件名：

```powershell
$deviceName = "替换为客户端设备名"
$keyPath = "$env:USERPROFILE\.ssh\id_ed25519_zrh_root"

Test-Path $keyPath
Test-Path "$keyPath.pub"
```

只有两项都返回 `False` 时才生成，禁止覆盖已有文件。

有口令版本，安全性更高：

```powershell
ssh-keygen -t ed25519 -a 100 -f $keyPath -C "$deviceName-to-zrh-root"
```

无人值守版本，不询问口令：

```powershell
ssh-keygen -t ed25519 -a 100 -N "" -f $keyPath -C "$deviceName-to-zrh-root"
```

读取允许传输的公钥和指纹：

```powershell
Get-Content "$keyPath.pub"
ssh-keygen -lf "$keyPath.pub"
```

### Linux 或 macOS 客户端

```bash
device_name="替换为客户端设备名"
key_path="$HOME/.ssh/id_ed25519_zrh_root"

test ! -e "$key_path" && test ! -e "$key_path.pub" || {
  echo "目标密钥文件已存在，停止，禁止覆盖"
  exit 1
}

ssh-keygen -t ed25519 -a 100 -f "$key_path" -C "${device_name}-to-zrh-root"
ssh-keygen -lf "$key_path.pub"
cat "$key_path.pub"
```

需要无人值守时，可在生成命令中添加 `-N ""`。无口令私钥一旦被复制，持有者无需任何密码即可登录 root，因此必须保护客户端设备和磁盘。

---

## 五、首次启用服务器的 root 公钥登录

这部分在服务器上只需要配置一次。保持现有 `zrh` SSH 会话打开。

创建独立 sshd 配置：

```bash
sudo nano /etc/ssh/sshd_config.d/60-root-key-only.conf
```

内容：

```text
PermitRootLogin prohibit-password
PubkeyAuthentication yes
```

`prohibit-password` 允许 root 使用公钥，同时禁止 root 使用密码和键盘交互认证。不要设置为 `PermitRootLogin yes`。

保存后先验证，不能跳过：

```bash
sudo sshd -t
sudo sshd -T -C user=root,host=客户端设备名,addr=客户端Tailscale-IP |
grep -E '^(permitrootlogin|pubkeyauthentication|passwordauthentication|kbdinteractiveauthentication)'
```

必须看到：

```text
permitrootlogin prohibit-password
pubkeyauthentication yes
```

全局 `passwordauthentication` 应继续为 `no`。语法和生效值正确后才重载：

```bash
sudo systemctl reload ssh
```

Ubuntu 24.04 的 `PermitRootLogin prohibit-password` 行为可查阅 [Ubuntu sshd_config 手册](https://manpages.ubuntu.com/manpages/noble/man5/sshd_config.5.html)。

---

## 六、把新设备公钥加入 root

以下变量必须替换为新设备的真实信息：

```bash
client_ip='100.x.y.z'
client_public_key='ssh-ed25519 AAAA... 客户端设备名-to-zrh-root'
```

添加来源限制和常用安全限制：

```bash
sudo install -d -m 700 -o root -g root /root/.ssh

restricted_key="from=\"${client_ip}\",no-agent-forwarding,no-port-forwarding,no-X11-forwarding,no-user-rc ${client_public_key}"

sudo grep -qxF "$restricted_key" /root/.ssh/authorized_keys 2>/dev/null ||
printf '%s\n' "$restricted_key" | sudo tee -a /root/.ssh/authorized_keys >/dev/null

sudo chown root:root /root/.ssh /root/.ssh/authorized_keys
sudo chmod 700 /root/.ssh
sudo chmod 600 /root/.ssh/authorized_keys
```

验证：

```bash
sudo ssh-keygen -lf /root/.ssh/authorized_keys
sudo wc -l /root/.ssh/authorized_keys
```

输出中必须出现新客户端报告的公钥指纹。

说明：

- `from="客户端Tailscale-IP"`：只允许该 Tailscale IP 使用这把密钥。
- `no-agent-forwarding`：禁止转发 SSH Agent。
- `no-port-forwarding`：禁止借 root 会话建立端口转发。
- `no-X11-forwarding`：禁止 X11 转发。
- `no-user-rc`：禁止执行用户自定义 SSH RC 文件。
- 以上限制不会禁止普通交互式 root shell。

如果设备退出 tailnet后重新注册，Tailscale IP 可能改变。此时应更新 `from=`，不能取消来源限制来绕过问题。

---

## 七、从新设备首次测试

先保持现有 `zrh` 管理会话打开。新客户端执行：

### Windows

```powershell
ssh -F NUL -o IdentitiesOnly=yes `
  -i "$env:USERPROFILE\.ssh\id_ed25519_zrh_root" `
  root@100.99.232.7 "id -un; hostname"
```

### Linux 或 macOS

```bash
ssh -F /dev/null -o IdentitiesOnly=yes \
  -i "$HOME/.ssh/id_ed25519_zrh_root" \
  root@100.99.232.7 'id -un; hostname'
```

首次连接时，只在主机指纹完全匹配下列值时接受：

```text
SHA256:1KPaatBGKwjQjQu5+5zTEcsCLUYZp5o3J8xkeXDd9so
```

成功输出：

```text
root
zrh-dracarys
```

未成功前不要关闭原有 `zrh` 会话，不要开启 root 密码登录。

---

## 八、配置客户端一键连接

### Windows OpenSSH

在客户端的 `C:\Users\<用户名>\.ssh\config` 中添加：

```sshconfig
Host zrh-root
    HostName 100.99.232.7
    User root
    Port 22
    IdentityFile ~/.ssh/id_ed25519_zrh_root
    IdentitiesOnly yes
    ServerAliveInterval 30
    ServerAliveCountMax 3
```

测试：

```powershell
ssh -G zrh-root | Select-String '^(user|hostname|port|identityfile|identitiesonly) '
ssh zrh-root "id -un; hostname"
```

### MobaXterm

创建 SSH Session：

| 设置 | 值 |
| --- | --- |
| Remote host | `100.99.232.7` |
| Specify username | `root` |
| Port | `22` |
| Advanced SSH settings → Use private key | 客户端自己的 `id_ed25519_zrh_root` |

保存会话名为 `zrh-root`。无人值守密钥不需要输入口令；有口令密钥应使用 MobaXterm 内部 SSH Agent 缓存解锁状态。

---

## 九、以后再添加其他设备

服务器的 sshd 配置不需要重复修改。每增加一台设备，只执行：

1. 新设备生成自己的独立密钥。
2. 记录设备名、Tailscale IP、公钥和指纹。
3. 在 `/root/.ssh/authorized_keys` 追加带该设备 `from=` 限制的公钥行。
4. 检查 root 目录和授权文件权限。
5. 保持旧管理连接，完成首次测试。

每台设备一行公钥，便于独立撤销。不要让多台设备共用同一私钥。

---

## 十、撤销某台设备的 root 权限

1. 在服务器上找到该设备对应的注释和指纹：

```bash
sudo ssh-keygen -lf /root/.ssh/authorized_keys
sudo nano /root/.ssh/authorized_keys
```

2. 只删除以该设备名结尾的公钥行。
3. 保存后权限立即失效，不需要重启 sshd。
4. 再次确认权限：

```bash
sudo chown root:root /root/.ssh /root/.ssh/authorized_keys
sudo chmod 700 /root/.ssh
sudo chmod 600 /root/.ssh/authorized_keys
```

若要关闭所有 root SSH 登录：

```bash
echo 'PermitRootLogin no' | sudo tee /etc/ssh/sshd_config.d/60-root-key-only.conf
sudo sshd -t && sudo systemctl reload ssh
```

关闭 root SSH 不影响通过 `zrh` 登录后使用 `sudo -i`。

---

## 十一、故障定位

### 连接超时

尚未进入 SSH 认证阶段。检查 Tailscale、ACL、防火墙和 22 端口：

```text
tailscale ping 100.99.232.7
```

Windows：

```powershell
Test-NetConnection 100.99.232.7 -Port 22
```

### `Permission denied (publickey)`

检查：

- 用户名是否为 `root`
- 客户端是否指定了自己的 root 专用私钥
- 公钥指纹是否出现在 `/root/.ssh/authorized_keys`
- `from=` 是否等于客户端当前 Tailscale IP
- `/root/.ssh` 是否为 `700 root:root`
- `authorized_keys` 是否为 `600 root:root`
- `sshd -T -C` 是否显示 `permitrootlogin prohibit-password`

查看服务端日志：

```bash
sudo journalctl -u ssh --since '5 minutes ago' --no-pager | tail -n 100
```

### `Too many authentication failures`

客户端提交了过多密钥。使用 `IdentitiesOnly=yes` 并显式指定正确私钥，不要在 `Host *` 下堆叠多条 `IdentityFile`。

---

## 十二、发给 Linux 设备 AI 的通用提示词

完整提示词已独立成文件，便于直接复制或由脚本注入：

[自动化提示词：新设备 Root SSH 登录](../自动化提示词/新设备Root-SSH登录提示词.txt)

使用时把占位符替换成实际值：

| 占位符 | 填入内容 |
| --- | --- |
| `<设备名>` | 客户端主机名，如 `2024-bg-217` |
| `<100.x.y.z>` | 客户端 Tailscale IP |
| `<ssh-ed25519 AAAA... 注释>` | 客户端完整公钥（`.pub` 全文） |
| `<SHA256:...>` | `ssh-keygen -lf` 输出的指纹 |
