# 自托管服务器的五层纵深防御：从 SSH 加固到统一认证门户

> 作者: Lazybones | 日期: 2026-09-07

# 自托管服务器的五层纵深防御：从 SSH 加固到统一认证门户

> 自托管服务器直接暴露在公网，安全不是「加个防火墙」就够了。本文讲如何用五层纵深防御把攻击面压到最小——SSH 密钥认证、fail2ban 自动封禁、Authelia 统一认证门户、端口收敛，以及它们之间如何联动。

## 纵深防御的思路

单层防御总有被绕过的可能。纵深防御的核心是**每一层都能独立挡住一类攻击**，即使某层被突破，下一层依然有效：

```text
┌─────────────────────────────────────────────┐
│ 第1层: SSH 密钥认证 + 禁密码登录             │  ← 挡暴力破解
├─────────────────────────────────────────────┤
│ 第2层: fail2ban 自动封禁                     │  ← 挡高频扫描
├─────────────────────────────────────────────┤
│ 第3层: 端口收敛（UFW 只放行必要端口）        │  ← 缩小攻击面
├─────────────────────────────────────────────┤
│ 第4层: Authelia 统一认证门户                 │  ← 挡 Web 暴力破解
├─────────────────────────────────────────────┤
│ 第5层: Traefik ForwardAuth + 路由隔离        │  ← 挡未授权访问
└─────────────────────────────────────────────┘
```

## 第1层：SSH 密钥认证

SSH 是服务器的「后门」，一旦被突破，后面所有层都形同虚设。

### 配置

`/etc/ssh/sshd_config` 关键配置：

```text
PubkeyAuthentication yes          # 启用密钥认证
PasswordAuthentication no         # 禁用密码登录
PermitRootLogin no                # 禁止 root 直接登录
MaxAuthTries 3                    # 最多尝试 3 次
LoginGraceTime 30                 # 30 秒内必须完成认证
```

禁用密码登录后，暴力破解字典攻击直接失效——没有密码可以猜。

### SSH 探针监控

为了确保 SSH 服务持续可用（被 fail2ban 误封、sshd 崩溃等），可以设置一个受限的探针用户：

```bash
# 创建受限用户 probe，只允许执行 /bin/true
# authorized_keys 限制行：
command="/bin/true",no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-user-rc ssh-ed25519 AAAA...
```

探针脚本每 2 分钟用这个受限用户连一次 SSH，成功后向 Uptime Kuma 的 Push 端点发送心跳。如果连续几次失败，Uptime Kuma 会告警。这个设计的好处是：

- 探针用户**只能执行 `/bin/true`**，即使私钥泄露也无法做任何操作。
- `no-pty` + `no-port-forwarding` 等限制进一步收窄能力。
- 探针验证的是完整的 SSH 认证链路（网络连通 + sshd 运行 + 密钥认证正常）。

## 第2层：fail2ban 自动封禁

fail2ban 监控认证日志，同一 IP 短时间内多次失败就自动封禁。

### SSH jail 配置

```ini
# /etc/fail2ban/jail.local
[DEFAULT]
# 白名单：本机、宿主机内网 IP、固定公网 IP
ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8 172.16.0.0/12

[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 5
findtime = 600
bantime = 3600
banaction = nftables
```

**关键点**：SSH jail 用 `banaction = nftables`，只封锁 SSH 端口（22）。这意味着被封禁的 IP **仍然可以访问 Web 服务**——这是有意为之的设计，后面会讲为什么。

### Web 服务 jail（Authelia）

```ini
[authelia]
enabled = true
port = http,https
filter = authelia
logpath = /var/log/authelia.log
maxretry = 3
findtime = 120
bantime = 300
banaction = nftables[type=allports]
```

Web jail 用 `nftables[type=allports]`——**封锁所有端口**，包括 SSH。这里有一个安全不对称：

| Jail | 封禁范围 | 效果 |
|------|----------|------|
| sshd | 仅 SSH 端口 | 攻击者仍可访问 Web |
| authelia | 所有端口 | 攻击者完全被挡在外面 |

为什么 Web jail 更严格？因为**Web 暴力破解往往是 SSH 暴力破解的前奏**——攻击者先试 Web 面板弱密码，拿到 shell 权限后再横向。Web jail 把所有端口一封，直接切断后续攻击路径。

### Authelia 内置防暴力破解

Authelia 自带 regulation 模块，独立于 fail2ban：

```yaml
# Authelia 配置
regulation:
  max_retries: 3        # 2 分钟内最多失败 3 次
  find_time: '2 minutes'
  ban_time: '5 minutes' # 触发后封禁 5 分钟
```

这意味着攻击者面对的是**双层封禁**：Authelia 先在应用层封 5 分钟，如果频繁触发，fail2ban 在系统层封更长时间。两层的计数器和封禁时长独立，形成递进式防御。

### 排坑：banned vs 密码错

调试 Authelia 登录失败时，必须先区分两种原因：

- `banned`：被 regulation 封禁，**即使密码正确也返回 401**。重启容器即可清除。
- `do not match`：密码真的不对。

短时间反复跑自检脚本很容易把自己锁死，然后误判成「配置坏了」。**排查登录失败的第一步永远是看容器日志。**

## 第3层：端口收敛

容器化最大的安全收益之一是**端口收敛**。以前裸机部署时，每个服务都想监听自己的端口，防火墙规则越加越乱。容器化后，只有反向代理的 80/443 对外，其余服务全走 Docker 内部网络。

对外暴露的端口清单：

| 端口 | 用途 | 必要性 |
|------|------|--------|
| 22 | SSH | 必须 |
| 80 | HTTP 入口（ACME 验证 + HTTP→HTTPS 跳转） | 必须（所有 Web 服务入口） |
| 443 | HTTPS 入口 | 必须（所有 Web 服务入口） |
| 自定义高位端口 | 确有需要的非 HTTP 服务 | 按需，越少越好 |

对比重构前暴露十几个端口，攻击面可以缩小 70% 以上。

## 第4层：Authelia 统一认证门户

所有管理面板和监控工具统一走 Authelia 认证，不再各自维护 BasicAuth 或弱密码。

### 架构

```text
浏览器 → Traefik → (ForwardAuth) → Authelia
                      │ 认证通过
                      └→ 后端服务
                      注入 Remote-User / Remote-Groups / Remote-Email
```

未登录时 302 跳转到登录门户，登录成功后回跳原地址。密码用 argon2id 哈希存储，支持 TOTP 二步验证。

### 哈希生成踩坑

`users.yml` 里的密码字段是 argon2id 哈希，满是 `$` 特殊字符。用 `sed` 替换会静默失败：

```bash
# ❌ sed 把 $ 当成正则锚点，替换失败
sed -i 's/$argon2id$.../$argon2id$.../' users.yml

# ✅ 用 Python 正则 + re.escape 处理特殊字符
python3 -c "
import re
path = '/srv/stacks/authelia/config/users.yml'
old, new = OLD_DIGEST, NEW_DIGEST
text = open(path).read()
open(path, 'w').write(re.sub(re.escape(old), new, text))
"
```

## 第5层：Traefik 路由隔离

每个受保护域名的路由只在 File Provider 里定义一次，带 `authelia-forward` 中间件。关键教训是**混用 Docker Provider 和 File Provider 会导致认证被绕过**——这是反向代理配置里最常见的一类坑。

验证认证是否生效的方法：

```bash
# 无凭据访问必须 302/401，绝不能 200
curl -s -o /dev/null -w '%{http_code}\n' https://logs.example.com/

# 检查是否有 @docker 幽灵路由
docker exec traefik wget -qO- http://127.0.0.1:8080/api/http/routers | grep '@docker'
```

这个检查可以固化为回归护栏脚本 `authelia-check.sh`，定期跑一次确保认证没被意外绕过。

## SSH 探针 + Uptime Kuma 联动

前面提到的 SSH 探针不只是「能连就行」，它和 Uptime Kuma 形成了完整的监控闭环：

```text
┌──────────┐    每2分钟SSH连接    ┌──────────┐
│ crontab  │ ──────────────────→ │ sshd     │
│ 探针脚本 │                     └──────────┘
└────┬─────┘
     │ 成功 → push heartbeat
     ▼
┌──────────┐    告警             ┌──────────┐
│ Uptime   │ ─────────────────→ │ 通知渠道 │
│ Kuma     │    连续失败         │ (邮件等) │
└──────────┘                    └──────────┘
```

探针脚本验证的是完整的 SSH 认证链路：网络连通 → sshd 运行 → 密钥认证正常 → 命令执行成功。任何一环断裂都会被 Uptime Kuma 捕获。

## 小结

五层防御各司其职：

| 层 | 防什么 | 技术 |
|----|--------|------|
| SSH 密钥 | 暴力破解密码 | 禁密码登录 |
| fail2ban | 高频扫描 | 自动封禁 IP |
| 端口收敛 | 不必要的暴露 | UFW + Docker 内部网络 |
| Authelia | Web 面板弱密码 | argon2id + TOTP + regulation |
| 路由隔离 | 未授权访问 | ForwardAuth + 路由规则 |

**核心经验**：安全不是一层就够，而是层层叠加。每一层假设前面的层可能被突破，自己仍然独立有效。Web jail 封全端口、SSH jail 只封 SSH，这种不对称设计是有意为之——Web 攻击者比 SSH 攻击者更危险，因为 Web 面板通常有更高的系统权限。

下一篇讲自动备份与恢复演练。
