第 1 章
FRP内网穿透
实验室GPU服务器远程访问系统:从手工配置到自动化运维
FRP内网穿透 + SSH双因素认证 + Ansible批量部署的完整实践
作者:Zeqi Li
完成日期:2026年1月
服务规模:5台GPU服务器,10-20名用户
一、背景:实验室的远程访问难题
我们实验室有5台深度学习服务器,分散在不同的网段和位置:
| 服务器 | 位置 | GPU配置 | 用途 |
|---|---|---|---|
| 2080Ti | 办公室 | 1×RTX 2080Ti | 轻量级训练 + 控制平面 |
| 5070Ti | 实验室 | 1×RTX 5070Ti | Squid代理 + 监控中心 |
| 3090 | 机房 | 4×RTX 3090 | 重型训练主力 |
| 4070Ti-001 | 实验室 | 1×RTX 4070Ti | 个人训练 |
| 4070Ti-002 | 实验室 | 1×RTX 4070Ti | 个人训练 |
核心矛盾
问题1:校园网物理隔离
- WiFi网段(10.176.0.0/16)和有线网段(10.177.0.0/16)无法互通
- 在宿舍连WiFi时无法访问实验室服务器
问题2:外网访问需求
- 用户需要在家里、出差时提交作业、查看训练进度
- 所有服务器都在内网,没有公网IP
问题3:用户体验要求
- 希望访问方式尽可能简单,最好"像在校内一样用SSH"
为什么不用VPN?
在确定方案前,我们对比了主流的远程访问方案:
| 方案 | 用户体验 | 实施难度 | 维护成本 | 适用场景 |
|---|---|---|---|---|
| VPN | 需要先连VPN再SSH | 高(证书管理、客户端配置) | 高(用户端故障支持) | 大型企业(>50用户) |
| FRP | 直接SSH,对用户透明 | 低(一次配置) | 低(无客户端) | 中小团队(<20用户) |
| SSH隧道 | 需要手动建立隧道 | 中 | 中(用户需理解原理) | 个人使用 |
最终选择FRP的核心原因:对用户透明。
用户不需要知道什么是内网穿透,也不需要安装任何客户端,就像在校内一样用SSH连接。这对实验室场景至关重要——我们不想花时间教大家配置VPN客户端。
二、架构设计:控制平面集中化
2.1 最初的想法(❌ 放弃)
一开始的直觉是:每台服务器独立部署frpc客户端。
阿里云 frps
↓
┌─┴─┬─────┬─────┬─────┐
↓ ↓ ↓ ↓ ↓
frpc frpc frpc frpc frpc
(5台独立配置)
问题:
- 5台服务器 = 5份frpc配置文件(重复劳动)
- IP地址变化需要逐台修改
- FRP Token分散存储在5个地方(泄露风险高)
- 配置不一致容易出错
2.2 集中控制平面(✅ 采用)
最终采用的架构:用一台服务器作为统一的FRP控制平面。
┌─────────────────────────────────────────────┐
│ 外网用户 │
└───────────────┬─────────────────────────────┘
↓
┌───────────────────────────────────────────┐
│ 阿里云 frps (8.162.3.156) │
│ 公网IP + FRP服务端 │
└───────────────┬───────────────────────────┘
↓ FRP隧道
┌───────────────────────────────────────────┐
│ 2080Ti (控制平面) 10.177.19.137 │
│ 统一管理5个FRP代理: │
│ ├─ :12080 → 127.0.0.1:22022 (本机) │
│ ├─ :15070 → 10.177.19.58:22022 (5070Ti) │
│ ├─ :13090 → 10.177.11.62:22022 (3090) │
│ ├─ :24070 → 10.177.18.132:22022 (4070-2)│
│ └─ :14070 → 10.177.18.221:52223 (4070-1)│
└───────────────┬───────────────────────────┘
↓ SSH连接
┌───────┴───────┬───────┬───────┐
↓ ↓ ↓ ↓
[5台目标服务器 - 业务平面]
核心设计理念:控制平面与业务平面分离
- 控制平面(2080Ti): 专职管理FRP隧道,转发流量
- 业务平面(5台服务器): 专注计算任务,无需关心FRP细节
2.3 为什么选2080Ti作为控制平面?
| 考虑因素 | 2080Ti | 之前的kirara VM |
|---|---|---|
| 稳定性 | ✅ 机房服务器,24小时运行 | ❌ 个人PC虚拟机,可能关机 |
| 网络 | ✅ 直连校园有线网10.177网段 | ⚠️ 在私人路由器内部,多一步路由转发配置 |
| 电源 | ✅ 常开 | ❌ 过年会断电 |
决策: 用稳定的机房服务器做控制平面,而不是个人PC虚拟机。
2.4 零权限设计
控制平面的关键安全设计:不存储任何目标服务器的认证凭据。
2080Ti上只有frpc配置文件:
[[proxies]]
name = "3090-ssh"
type = "tcp"
localIP = "10.177.11.62" # 只知道IP
localPort = 22022 # 只知道端口
remotePort = 13090
没有:
- ❌ SSH私钥
- ❌ 用户密码
- ❌ 任何认证信息
安全价值:
- 即使2080Ti被攻破,攻击者也无法直接访问其他服务器
- 只能建立FRP隧道,仍需通过SSH双因素认证
- 限制横向移动的可能性
三、安全设计:SSH双因素认证是核心
3.1 诚实的安全评估
技术文章经常会说"我们部署了六层防护",听起来很厉害。但实际上,不同防护层的贡献度是不均等的。
防护层级 真实贡献度
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
SSH双因素认证 ████████████████ 80% ← 主力防线
UFW最小权限 ███ 15% ← 访问控制
fail2ban █ 3% ← 辅助防护
FRP Token █ 2% ← 准入门槛
iptables限速 边缘作用
systemd资源限制 边缘作用
核心认知:
- 如果SSH双因素被破解,其他层也挡不了多久
- 安全设计的重点是SSH双因素认证,最大的保障其实来源于秘钥,其他防护是辅助
3.2 SSH Match LocalPort:核心技术亮点 ⭐⭐⭐
这是整个系统最有价值的设计:同一个SSH服务,根据访问端口不同,应用不同的认证策略。
# /etc/ssh/sshd_config
Include /etc/ssh/sshd_config.d/*.conf
Port 22 # 校内直连端口
Port 22022 # 外网FRP访问端口
PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication yes
# ... 其他配置 ...
# ========================================
# 差异化认证策略(配置文件末尾)
# ========================================
# 外网访问(22022端口):密钥 AND 密码
Match LocalPort 22022
AuthenticationMethods publickey,password
# 逗号 = AND关系,必须同时满足
# 校内访问(22端口):密钥 OR 密码
Match LocalPort 22
AuthenticationMethods publickey password
# 空格 = OR关系,满足其一即可
设计思想:
| 访问场景 | 端口 | 威胁模型 | 认证策略 | 理由 |
|---|---|---|---|---|
| 外网FRP | 22022 | 面向全球,24小时扫描 | 密钥 AND 密码 | 双因素强制,即使密钥泄露也需要密码 |
| 校内直连 | 22 | 仅校园网可达 | 密钥 OR 密码 | 灵活便捷,有密钥可无密码登录 |
为什么这样设计?
外网必须双因素:
- 公网端口暴露在互联网,面临全球自动化扫描攻击
- 即使SSH密钥泄露(笔记本丢失、U盘泄露),攻击者仍需账户密码
- 双因素认证的安全性是指数级提升
校内允许单因素:
- 校内只有校园网能访问(10.177.0.0/16, 10.176.0.0/16),相对可信
- 研究生日常使用频繁,如果每次都要双因素会影响效率
- 配置了密钥的用户可以无密码登录(便利性)
- 没配置密钥的用户用密码也能登录(灵活性)
用户无感知:
- 用户不需要知道这个设计存在
- SSH客户端会自动适应(有密钥就用密钥,需要密码就提示)
- 从校内访问:
ssh user@10.177.19.58(有密钥就免密码) - 从外网访问:
ssh -p 15070 user@8.162.3.156(必须输入密码)
这是整个安全设计的核心。
3.3 UFW:最小权限原则
防火墙规则遵循"最小权限原则":只开放必要的访问,拒绝其他一切。
# 校内直连(22端口):允许校园网
ufw allow from 10.177.0.0/16 to any port 22 comment 'Campus Wired'
ufw allow from 10.176.0.0/16 to any port 22 comment 'Campus WiFi'
# 外网FRP(22022端口):只允许控制平面
ufw allow from 10.177.19.137 to any port 22022 comment 'FRP from 2080Ti'
纵深防御体现:
即使外网攻击者绕过了FRP,直接尝试访问22022端口:
- UFW会检查源IP
- 发现不是2080Ti(10.177.19.137)
- 拒绝连接
这是第二层防线。但如果攻击者能绕过FRP直接访问,说明已经攻破了某些环节,这时候UFW的作用有限。
UFW的实际价值:
- ✅ 减少攻击面(关闭不必要的端口)
- ✅ 防止配置错误(只允许特定IP访问22022)
- ⚠️ 但不是主要防护手段
3.4 fail2ban:锦上添花,非雪中送炭
# /etc/fail2ban/jail.local
[sshd]
enabled = true
maxretry = 3
bantime = 3600
findtime = 600
fail2ban的作用:
| 场景 | fail2ban效果 |
|---|---|
| 低级暴力破解(字典攻击) | ✅ 有效阻止 |
| 降低日志噪音 | ✅ 减少无意义的攻击尝试 |
| 有组织的攻击 | ❌ 无效(攻击者可以很慢地尝试) |
| 只有密码认证的系统 | ❌ 只能延缓,无法根治 |
讨论:fail2ban是否忽略控制平面IP?
忽略的理由:
- 所有外网流量都来自2080Ti,封禁它会影响所有用户
- 有SSH Match规则的强制双因素认证作为主防护
结论: fail2ban其实这里感觉有点鸡肋,也不能说完全没有作用,但真的用处不大,等效于访问频率限制,哈哈。
3.5 FRP Token:准入门槛
# frps.toml (阿里云)
[auth]
method = "token"
token = "256位随机字符串"
FRP Token的作用:
- 防止未授权的frpc接入frps(准入控制)
- 类似于"大门钥匙",没有token无法建立隧道
FRP Token的局限:
- 不是加密传输(FRP本身不加密,依赖SSH的加密)
- 泄露后,攻击者只能建立FRP隧道,仍需通过SSH双因素认证
- 真正的安全防护还是在SSH层
Token管理:
- 集中存储在2080Ti的frpc.toml中
- 服务器端存储在阿里云frps.toml中
- 只有这两处存储,降低泄露风险
四、部署实践:踩过的坑
4.1 systemd socket陷阱(Ubuntu 24.04)
现象:
在/etc/ssh/sshd_config中明确配置了Port 22022,重启SSH后,端口却没有监听。
$ sudo ss -tlnp | grep sshd
LISTEN 0 4096 [::]:22 # 只有22端口,22022不见了!
排查过程:
# 1. 检查配置语法(无错误)
$ sudo sshd -t
# 2. 检查SSH是如何启动的
$ sudo systemctl status ssh.service | grep "TriggeredBy"
TriggeredBy: ● ssh.socket # ← 发现是socket activation启动
原因:
Ubuntu 24.04的SSH默认由systemd的ssh.socket管理,而不是传统的sshd直接启动。这种情况下,sshd_config中的Port配置会被socket的ListenStream覆盖。
解决方案:
创建systemd socket override配置:
sudo mkdir -p /etc/systemd/system/ssh.socket.d
sudo nano /etc/systemd/system/ssh.socket.d/override.conf
# /etc/systemd/system/ssh.socket.d/override.conf
[Socket]
ListenStream=
ListenStream=0.0.0.0:22
ListenStream=[::]:22
ListenStream=0.0.0.0:22022
ListenStream=[::]:22022
关键点:
- 第一行的空
ListenStream=必须存在(清除默认值) - 必须明确指定IPv4(0.0.0.0)和IPv6(::)
- 不指定IPv4会导致只监听IPv6,IPv4连接被拒绝
应用配置:
sudo systemctl daemon-reload
sudo systemctl restart ssh.socket
sudo systemctl restart ssh.service
# 验证(应该看到4行)
sudo ss -tlnp | grep sshd
# 0.0.0.0:22
# 0.0.0.0:22022
# [::]:22
# [::]:22022
重要:并非所有服务器都需要这个配置!
# 检测方法
sudo systemctl status ssh.service | grep "TriggeredBy"
# 如果输出 "TriggeredBy: ● ssh.socket" → 需要override
# 如果无输出 → 使用原生SSH,只需修改sshd_config
在我们的5台服务器中:
- 2080Ti, 5070Ti: 需要override(Ubuntu 24.04)
- 3090, 4070Ti-002: 不需要(原生SSH)
教训:
- 这不是"设计创新",纯粹是踩坑记录,但对遇到同样问题的人有参考价值
- 系统版本升级可能改变默认行为,需要关注
4.2 Ansible密码配置的单引号陷阱
问题:
在Ansible inventory中配置sudo密码时,认证失败。
# ❌ 错误示例
server1 ansible_become_pass=Pass@2024!
fatal: [server1]: FAILED! => {"msg": "Incorrect sudo password"}
原因:
Ansible解析配置文件时,特殊字符会被解释:
!- 某些shell中触发历史替换$- 变量开始符号`- 命令替换@- 在某些上下文有特殊含义
解决方案:用单引号
# ✅ 正确示例
server1 ansible_become_pass='Pass@2024!'
单引号 vs 双引号:
| 引号类型 | 字符解释 | 适用场景 |
|---|---|---|
单引号 '...' | 所有字符都是字面量 | 密码、特殊字符 ✅ |
双引号 "..." | 解释 $, !, `, \ | 需要变量替换 |
| 无引号 | 依赖解析器,不安全 | 简单字母数字 |
如果密码包含单引号怎么办?
# 密码是 it's-a-password
ansible_become_pass='it'\''s-a-password'
# ↑转义单引号的技巧
最佳实践:
- 密码值使用单引号
4.3 UFW规则的grep匹配问题
问题:
Ansible playbook检查UFW规则是否存在时,grep匹配失败。
UFW输出格式:
$ sudo ufw status numbered
[3] 22022 ALLOW IN 10.177.19.137
↑端口 ↑IP
错误的grep:
shell: ufw status numbered | grep "10.177.19.137.*22022"
# 这是在找:IP...端口
# 但实际是:端口...IP
# 所以匹配不到!
正确的做法:
shell: ufw status numbered | grep "22022" | grep "10.177.19.137"
# 先匹配端口,再匹配IP
经验:
- 先观察实际输出格式,再写匹配规则
- 复杂匹配拆成多个简单grep(更清晰、更可靠)
4.4 幂等性设计:可重复运行的Playbook
Ansible最重要的特性是幂等性:重复运行应该安全,不产生副作用。
UFW规则的幂等性处理:
- name: 检查UFW规则是否存在
shell: ufw status numbered | grep "22022" | grep "{{ control_plane_ip }}"
register: rule_check
ignore_errors: yes
changed_when: false
- name: 只在规则不存在时添加
ufw:
rule: allow
from_ip: "{{ control_plane_ip }}"
to_port: 22022
comment: 'SSH FRP from 2080Ti'
when: rule_check.rc != 0 # ← 关键:只在不存在时执行
关键设计:
register: rule_check- 保存检查结果when: rule_check.rc != 0- 只在规则不存在时添加changed_when: false- 检查操作不算"改变"
验证幂等性:
# 第一次运行
$ ansible-playbook update-frp-ufw.yml
TASK [添加UFW规则] ***
changed: [server1] # ← 添加了规则
# 第二次运行
$ ansible-playbook update-frp-ufw.yml
TASK [检查UFW规则] ***
ok: [server1]
TASK [添加UFW规则] ***
skipping: [server1] # ← 规则已存在,跳过
经验:
- 自动化脚本必须考虑幂等性,"先检查,再操作"
五、自动化演进:从手工到批量
5.1 三个阶段的演进
Phase 1: 纯手工时代(2025.11)
# 每台服务器都要登录,逐一操作
ssh server1
sudo nano /etc/ssh/sshd_config # 手工复制粘贴
sudo nano /etc/systemd/system/ssh.socket.d/override.conf
sudo ufw allow from ... to any port 22022
sudo systemctl daemon-reload
sudo systemctl restart ssh
exit
# 重复5次...
痛点:
- ⏱️ 每台服务器30分钟,5台 = 2.5小时
- 😵 重复劳动,容易出错
- 🤔 配置不一致(有的服务器忘了某步)
- 📝 需要记录哪台配过哪台没配
Phase 2: Ansible批量配置(2025.12)
# update-frp-ufw.yml
- hosts: gpu_servers
become: yes
vars:
control_plane_ip: "10.177.19.137"
frp_port: 22022
tasks:
- name: 添加UFW规则
ufw:
rule: allow
from_ip: "{{ control_plane_ip }}"
to_port: "{{ frp_port }}"
comment: 'SSH FRP from 2080Ti'
# 一条命令更新所有服务器
ansible-playbook -i inventory.ini update-frp-ufw.yml
收益:
- ⏱️ 5台服务器5分钟搞定
- ✅ 配置一致性保证
- 🔄 幂等性,可重复运行
- 📊 清晰的执行报告
Phase 3: 用户体验优化(2026.01)
不仅自动化了管理员操作,还优化了用户端体验。
用户侧:Python脚本自动生成密钥
#!/usr/bin/env python3
# generate_ssh_key.py
import os
import subprocess
def main():
print("=== SSH密钥生成工具 ===\n")
username = input("用户名: ")
hostname = input("服务器名(如 3090, 5070ti): ")
key_name = f"{username}@{hostname}"
key_path = os.path.expanduser(f"~/.ssh/{key_name}")
# 生成密钥
cmd = [
'ssh-keygen',
'-t', 'ed25519',
'-f', key_path,
'-C', key_name
]
subprocess.run(cmd)
# 显示公钥
print("\n" + "="*60)
print("✅ 密钥生成成功!")
print("="*60)
print("\n📋 公钥内容(发送给管理员):\n")
with open(f"{key_path}.pub") as f:
print(f.read())
print("📁 私钥位置:", key_path)
print("🔐 请妥善保管私钥文件!\n")
if __name__ == "__main__":
main()
管理员侧:Ansible批量分发密钥
# add-user-key.yml
- name: 为用户添加SSH公钥
hosts: "{{ target_host }}"
become: yes
tasks:
- name: 确保.ssh目录存在
file:
path: "/home/{{ target_user }}/.ssh"
state: directory
owner: "{{ target_user }}"
mode: '0700'
- name: 添加公钥
authorized_key:
user: "{{ target_user }}"
key: "{{ user_public_key }}"
state: present
# 一键添加用户密钥到所有GPU服务器
ansible-playbook add-user-key.yml \
-e "target_user=zhangsan" \
-e "user_public_key='ssh-ed25519 AAAA...'" \
-e "target_host=gpu_servers"
用户体验对比:
| 阶段 | 用户操作 | 管理员操作 | 总耗时 | 体验 |
|---|---|---|---|---|
| Phase 1 | 手工生成密钥 复制粘贴给管理员 | 逐台SSH登录 手工添加 | ~30min | 😫 |
| Phase 2 | 手工生成密钥 复制粘贴给管理员 | Ansible批量添加 | ~10min | 🙂 |
| Phase 3 | 运行脚本自动生成 | Ansible批量添加 | ~5min | 😄 |
5.2 什么值得自动化,什么不值得
值得自动化的:
| 任务 | 重复频率 | 出错风险 | 自动化收益 |
|---|---|---|---|
| ✅ UFW规则批量配置 | 高(IP变化、新增服务器) | 高(容易遗漏) | ⭐⭐⭐⭐⭐ |
| ✅ 用户密钥批量分发 | 高(新用户入组) | 中(权限错误) | ⭐⭐⭐⭐⭐ |
| ✅ 配置一致性检查 | 中(定期巡检) | 高(配置漂移) | ⭐⭐⭐⭐ |
| ✅ IP变化后批量更新 | 低但关键 | 高(容易遗漏服务器) | ⭐⭐⭐⭐ |
不值得自动化的:
| 任务 | 理由 |
|---|---|
| ❌ SSH基础配置 | 每台只做一次,手工操作更灵活(踩坑时方便调试) |
| ❌ 从零搭建整套系统 | 步骤复杂、变数多,自动化脚本维护成本爆炸 |
| ❌ 过度抽象的配置模板 | 为了"通用性"引入过多变量和条件,反而难以理解 |
务实的自动化思路:
"在一定基础上运维" - 接受手工配置一遍基础环境,然后在此基础上自动化重复性工作。
这不是偷懒,而是工程判断:
- 手工配置一遍,可以理解每个步骤的作用
- 找到重复性高、易出错的部分
- 然后针对性自动化
反面教材:
有些"全自动部署脚本"为了处理各种边界情况,变得极其复杂:
# 过度设计的"全自动部署"(伪代码)
if is_ubuntu_24_04 && systemd_socket_enabled; then
create_socket_override
elif is_ubuntu_22_04 && systemd_socket_enabled; then
use_different_syntax
elif is_ubuntu_20_04; then
use_sysvinit_style
elif is_debian; then
check_debian_version
if version > 11; then
...
fi
elif is_centos; then
...
fi
# 500行后...
这种脚本的维护成本远超手工操作。好的自动化是简化工作,不是制造复杂性。
5.3 实例:IP变化的自动化处理
场景: 2080Ti的IP从10.177.19.137变成10.177.19.200
手工时代(Phase 1):
# 需要逐台操作,容易遗漏
ssh server1
sudo ufw delete [规则编号]
sudo ufw allow from 10.177.19.200 to any port 22022
exit
ssh server2
# ... 重复
# 5台服务器 × 5分钟 = 25分钟
自动化时代(Phase 3):
# 1. 修改inventory.ini的变量(10秒)
vim ansible-infra/group_vars/all.yml
# control_plane_ip: "10.177.19.200"
# 2. 运行playbook(1分钟)
ansible-playbook update-frp-ufw.yml
# 完成!所有5台服务器的UFW规则已更新
节省时间: 25分钟 → 1分钟
更重要的是: 不会遗漏任何一台服务器,配置一致性有保证。
5.4 未来改进方向
目前还有一处手工操作:修改frpc.toml中的IP地址。
当前做法:
# 手工编辑frpc.toml
vim /path/to/frpc.toml
[[proxies]]
localIP = "10.177.19.58" # ← 手工改这里
未来改进: 用Jinja2模板化
# frpc.toml.j2 (模板)
{% for server in gpu_servers %}
[[proxies]]
name = "{{ server.name }}-ssh"
type = "tcp"
localIP = "{{ server.ansible_host }}"
localPort = 22022
remotePort = {{ server.external_port }}
{% endfor %}
# group_vars/gpu_servers.yml
gpu_servers:
- name: 2080ti
ansible_host: "{{ hostvars['2080ti']['ansible_host'] }}"
external_port: 12080
- name: 5070ti
ansible_host: "{{ hostvars['5070ti']['ansible_host'] }}"
external_port: 15070
# ...
这样修改inventory.ini后,一条命令就能重新生成frpc.toml并重启服务。
目标: 真正的"改一处,全局生效"。
六、总结与反思
完成了什么
历时三个月(2025.11 - 2026.01),完成了5台GPU服务器的远程访问系统:
✅ 核心成果:
- 5台服务器全部实现外网穿透,双因素认证
- 用户体验透明(直接SSH,无需VPN客户端)
- 运维成本低(Ansible批量管理)
- 安全性可控(SSH Match LocalPort + UFW + fail2ban)
- 易于扩展(新增服务器只需改配置+运行playbook)
📊 效率提升:
- 添加用户:30分钟 → 5分钟
- IP变化更新:25分钟 → 1分钟
- 新增服务器:2小时 → 30分钟
- 故障排查:有清晰的检查清单和脚本
核心发现与思考
1. 方案选择:场景适配比技术复杂度重要
选FRP而不是VPN,核心原因是"对用户透明"。对于10-20人的实验室,用户体验优先于技术炫技。
2. 安全设计:诚实评估每层的真实贡献
SSH双因素认证贡献80%,其他都是辅助。承认哪里是核心、哪里是辅助,比堆砌防护层数更有意义。
3. 自动化边界:在一定基础上运维
不追求"从零全自动",而是手工打好基础,然后自动化重复性工作。好的自动化简化工作,不制造复杂性。
4. 架构思想:控制/业务分离的价值
用2080Ti统一管理FRP,而不是每台服务器独立配置,体现了清晰的职责分离。这不仅简化配置,也降低了安全风险。
建议与启发
如果你也在做类似的FRP内网穿透系统:
- 优先考虑用户体验 - 技术方案要让用户"无感知",而不是展示技术复杂度
- 诚实评估技术含量 - 承认哪些是核心设计,哪些是常规操作,不夸大
- 准确评估安全机制 - SSH双因素认证是主力,其他是辅助
- 自动化要有边界 - 不是所有事情都值得自动化,找到重复性高、易出错的部分
- 渐进式改进 - 先手工做一遍理解流程,再自动化,不要一开始就追求完美
- 文档和代码同样重要 - 踩过的坑记录下来,未来的自己会感谢现在的你
附录:配置参考
A. frpc配置模板(控制平面)
# /path/to/frp/frpc.toml
# FRP客户端配置 - 2080Ti控制平面
serverAddr = "你的公网IP"
serverPort = 7000
[auth]
method = "token"
token = "你的256位随机Token" # openssl rand -hex 32
# 本机SSH穿透
[[proxies]]
name = "2080ti-ssh"
type = "tcp"
localIP = "127.0.0.1"
localPort = 22022
remotePort = 12080
# 其他服务器示例
[[proxies]]
name = "3090-ssh"
type = "tcp"
localIP = "10.177.11.62"
localPort = 22022
remotePort = 13090
# ... 其他服务器配置
B. sshd_config模板(目标服务器)
# /etc/ssh/sshd_config
# SSH服务器配置 - 双端口 + 差异化认证
Include /etc/ssh/sshd_config.d/*.conf
Port 22 # 校内直连
Port 22022 # 外网FRP
PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
# 性能优化
Compression yes
ClientAliveInterval 60
UseDNS no
# ========================================
# 差异化认证策略(文件末尾)
# ========================================
# 外网:密钥 AND 密码
Match LocalPort 22022
AuthenticationMethods publickey,password
# 校内:密钥 OR 密码
Match LocalPort 22
AuthenticationMethods publickey password
C. systemd socket override(如需要)
# /etc/systemd/system/ssh.socket.d/override.conf
# 仅在使用socket activation时需要
[Socket]
ListenStream=
ListenStream=0.0.0.0:22
ListenStream=[::]:22
ListenStream=0.0.0.0:22022
ListenStream=[::]:22022
应用配置:
sudo systemctl daemon-reload
sudo systemctl restart ssh.socket
sudo systemctl restart ssh.service
D. UFW配置脚本
#!/bin/bash
# ufw-setup.sh - UFW规则配置
CONTROL_PLANE_IP="10.177.19.137"
# 校内访问(22端口)
ufw allow from 10.177.0.0/16 to any port 22 comment 'Campus Wired'
ufw allow from 10.176.0.0/16 to any port 22 comment 'Campus WiFi'
# 外网FRP(22022端口)
ufw allow from $CONTROL_PLANE_IP to any port 22022 comment 'FRP from 2080Ti'
# 启用UFW
ufw --force enable
# 显示规则
ufw status numbered
E. Ansible inventory示例
# inventory.ini
# ⚠️ 包含密码,务必使用 chmod 600 保护
[gpu_servers]
2080ti ansible_host=10.177.19.137 ansible_user=user1 ansible_become_pass='pass1'
5070ti ansible_host=10.177.19.58 ansible_user=user2 ansible_become_pass='pass2'
3090 ansible_host=10.177.11.62 ansible_user=user3 ansible_become_pass='pass3'
4070ti-002 ansible_host=10.177.18.132 ansible_user=user4 ansible_become_pass='pass4'
4070ti-001 ansible_host=192.168.31.98 ansible_user=user5 ansible_become_pass='pass5'
[cloud]
astra ansible_host=公网IP ansible_user=root ansible_become_pass='pass'
[all:vars]
ansible_port=22
ansible_ssh_private_key_file=~/.ssh/id_ed25519
control_plane_ip=10.177.19.137
F. 常用运维命令
# ========================================
# FRP相关
# ========================================
# 查看frpc状态
systemctl status frpc
# 查看frpc日志
journalctl -u frpc -n 50
# 实时日志
journalctl -u frpc -f
# 验证隧道建立
journalctl -u frpc | grep "start proxy success"
# 重启frpc
sudo systemctl restart frpc
# ========================================
# SSH相关
# ========================================
# 检查端口监听
ss -tlnp | grep sshd
# 测试配置语法
sshd -t
# 重启SSH
systemctl restart ssh.service
# 查看当前连接
who | grep pts
# ========================================
# UFW相关
# ========================================
# 查看规则
ufw status numbered
# 删除规则
ufw delete [编号]
# 重新加载
ufw reload
# ========================================
# Ansible相关
# ========================================
# 测试连通性
ansible all -i inventory.ini -m ping
# 批量执行命令
ansible gpu_servers -i inventory.ini -m shell -a "ufw status"
# 批量检查SSH端口
ansible gpu_servers -i inventory.ini -m shell -a "ss -tlnp | grep sshd"
# 运行playbook
ansible-playbook -i inventory.ini playbook.yml
# ========================================
# 故障排查
# ========================================
# 测试外网穿透
ssh -v -p 外网端口 用户@公网IP
# 检查防火墙
telnet 公网IP 端口
# 查看认证日志
tail -f /var/log/auth.log
项目完成时间: 2026年1月
文档版本: v3.0
作者: Zeqi Li
希望这篇实践记录对你有帮助!如有问题欢迎交流。