参考连接:https://blog.csdn.net/weixin_66855479/article/details/150932310
JumpServer中会有配置资产授权:进入 “权限” 模块 → 选择 “资产授权” → 点击 “创建授权规则” ;
配置授权:
资产:选择刚添加的服务器;
用户:选择当前用户(或用户组);
系统用户:选择为该资产配置的系统用户(如 opsuser)。这会告诉 JumpServer 使用哪个用户身份登录目标服务器;
保存授权,完成权限绑定。
说明:由于在创建“系统用户”时已经关联了 SSH 密钥,所以在此处选择了系统用户,就相当于间接选择了认证方式和密钥。
一、本地客户端机器上的SSH配置文件
1.1 配置文件位置
SSH配置文件始终位于本地客户端机器上,具体位置为:
- Linux/macOS:
~/.ssh/config - Windows:
%USERPROFILE%\.ssh\config
部署架构说明:
本地PC/移动端(有config文件)
↓
跳板机/堡垒机(无需配置文件)
↓
内网服务器(只需服务端配置sshd_config)
1.2 完整配置文件示例
# ======================
# SSH客户端配置文件示例
# ======================
# 全局默认配置(适用于所有连接)
Host *
# 连接设置
ServerAliveInterval 30
ServerAliveCountMax 3
TCPKeepAlive yes
ConnectTimeout 10
# 性能优化
Compression yes
ControlMaster auto
ControlPath ~/.ssh/control-%r@%h:%p
ControlPersist 10m
# 安全设置
StrictHostKeyChecking accept-new
HashKnownHosts yes
# ======================
# 跳板机配置
# ======================
# 主跳板机
Host jump-primary
HostName bastion.company.com
User jumpuser
Port 22
IdentityFile ~/.ssh/id_rsa_jump
ForwardAgent yes # 启用代理转发
# 备用跳板机
Host jump-backup
HostName backup-bastion.company.com
User jumpuser
Port 10122
IdentityFile ~/.ssh/id_rsa_jump_backup
# ======================
# 直接访问的服务器
# ======================
Host dev-server
HostName dev.company.com
User developer
Port 22
IdentityFile ~/.ssh/id_rsa_dev
# ======================
# 通过跳板机访问的内网服务器
# ======================
# 方式1:使用ProxyJump(OpenSSH 7.3+推荐)
Host internal-server-1
HostName 10.0.0.100
User admin
ProxyJump jump-primary
IdentityFile ~/.ssh/id_rsa_internal
# 方式2:使用ProxyCommand(兼容旧版本)
Host internal-server-2
HostName 10.0.0.101
User admin
ProxyCommand ssh -W %h:%p jump-primary
IdentityFile ~/.ssh/id_rsa_internal
# ======================
# 多级跳板配置
# ======================
Host deep-internal-server
HostName 192.168.1.100
User root
ProxyJump jump-primary,jump-backup # 通过两个跳板机
IdentityFile ~/.ssh/id_rsa_deep
# ======================
# 端口转发配置
# ======================
# 本地端口转发
Host tunnel-mysql
HostName 10.0.0.200
User dbadmin
LocalForward 3306 localhost:3306
ProxyJump jump-primary
# 远程端口转发
Host reverse-tunnel
HostName 10.0.0.201
User admin
RemoteForward 8080 localhost:80
ProxyJump jump-primary
使用示例:
# 连接跳板机
ssh jump-primary
# 通过跳板机连接内网服务器
ssh internal-server-1
# 建立端口转发
ssh tunnel-mysql
二、通过跳板机访问内网服务器需要几步认证?
2.1 标准认证流程(2步认证)
本地PC → [第1步:跳板机认证] → 跳板机 → [第2步:目标服务器认证] → 内网服务器
2.2 详细认证步骤
步骤1:本地PC到跳板机认证
- 认证方式:密码、SSH密钥、双因素认证
-
配置位置:跳板机的
/etc/ssh/sshd_config -
密钥管理:本地PC的
~/.ssh/id_rsa_jump
步骤2:跳板机到目标服务器认证
- 认证方式:密码、SSH密钥、证书认证
- 自动化方案:跳板机配置免密登录到目标服务器
2.3 认证矩阵
| 认证场景 | 本地→跳板机 | 跳板机→目标 | 总认证次数 | 用户体验 |
|---|---|---|---|---|
| 双密码认证 | 密码 | 密码 | 2 | 需输入两次密码 |
| 密钥+密码 | 密钥自动 | 密码 | 1.5 | 输入一次密码 |
| 双密钥认证 | 密钥自动 | 密钥自动 | 2(自动) | 无需交互 |
| SSH代理转发 | 密钥自动 | 通过代理自动 | 2(自动) | 最佳体验 |
2.4 最优配置方案
# 1. 在本地PC上启动SSH代理
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_rsa_jump
ssh-add ~/.ssh/id_rsa_target
# 2. 配置SSH代理转发
cat >> ~/.ssh/config << EOF
Host jump-primary
HostName bastion.company.com
User jumpuser
ForwardAgent yes
Host internal-*
User admin
ProxyJump jump-primary
EOF
# 3. 一键连接
ssh internal-server-1
三、libssh2在iOS端能支持端口转发吗?
3.1 支持能力分析
| 功能 | 支持状态 | 说明 |
|---|---|---|
| SSH连接 | ✅ 支持 | 基本的SSH会话建立 |
| 用户认证 | ✅ 支持 | 密码、密钥、证书认证 |
| 通道操作 | ✅ 支持 | 创建、读写、关闭通道 |
| 直接TCP通道 | ✅ 支持 | libssh2_channel_direct_tcpip_ex() |
| OpenSSH配置语法 | ❌ 不支持 | 无法直接使用ProxyCommand |
| 自动端口转发 | 🔄 需编程实现 | 需要手动实现跳板逻辑 |
3.2 iOS端实现方案
方案A:编程实现多跳连接
import Foundation
class SSHJumpConnection {
private var jumpSession: OpaquePointer?
private var targetSession: OpaquePointer?
// 连接到目标服务器(通过跳板机)
func connectThroughJumpHost(jumpHost: String, jumpPort: Int32,
jumpUser: String, jumpPassword: String,
targetHost: String, targetPort: Int32,
targetUser: String, targetKeyPath: String) -> Bool {
// 1. 初始化libssh2
libssh2_init(0)
// 2. 连接到跳板机
let jumpSocket = createSocket(to: jumpHost, port: jumpPort)
jumpSession = libssh2_session_init()
guard libssh2_session_handshake(jumpSession, jumpSocket) == 0 else {
print("跳板机SSH握手失败")
return false
}
// 3. 跳板机认证
guard libssh2_userauth_password(jumpSession, jumpUser, jumpPassword) == 0 else {
print("跳板机认证失败")
return false
}
// 4. 创建到目标服务器的TCP通道
guard let tcpChannel = libssh2_channel_direct_tcpip_ex(
jumpSession, targetHost, targetPort, "127.0.0.1", 0
) else {
print("TCP通道创建失败")
return false
}
// 5. 建立端口转发或新的SSH会话
return setupPortForwarding(tcpChannel: tcpChannel,
targetUser: targetUser,
targetKeyPath: targetKeyPath)
}
// 设置端口转发
private func setupPortForwarding(tcpChannel: OpaquePointer,
targetUser: String,
targetKeyPath: String) -> Bool {
// 由于libssh2的限制,需要通过其他方式实现
// 常见方案:在跳板机上建立本地端口转发
return createLocalPortForwarding(targetUser: targetUser,
targetKeyPath: targetKeyPath)
}
// 执行命令
func executeCommand(_ command: String) -> String? {
guard let session = targetSession else { return nil }
let channel = libssh2_channel_open_session(session)
guard channel != nil else { return nil }
libssh2_channel_exec(channel, command)
var output = ""
var buffer = [CChar](repeating: 0, count: 4096)
while true {
let n = libssh2_channel_read(channel, &buffer, buffer.count)
if n <= 0 { break }
if let str = String(validatingUTF8: buffer) {
output += str
}
}
libssh2_channel_close(channel)
libssh2_channel_free(channel)
return output
}
}
方案B:使用第三方库(推荐)
// 使用NMSSH(iOS原生SSH库)
import NMSSH
class SSHManager {
let session: NMSSHSession
init(host: String, username: String) {
session = NMSSHSession(host: host, username: username)
}
func connectThroughJump(jumpHost: String, jumpUser: String,
targetHost: String, targetUser: String) -> Bool {
// 1. 连接到跳板机
session.host = jumpHost
session.username = jumpUser
session.connect()
guard session.isConnected else { return false }
// 2. 在跳板机上执行SSH命令连接到目标服务器
// 需要跳板机配置免密登录到目标服务器
let tunnelCommand = "ssh -o BatchMode=yes \(targetUser)@\(targetHost)"
let (output, error) = session.channel.execute(tunnelCommand)
return error == nil
}
}
3.3 iOS开发注意事项
- 网络权限:需要在Info.plist中添加网络权限
- 后台执行:SSH操作应在后台线程执行
- 密钥管理:使用Keychain安全存储密钥
- 超时处理:设置合理的连接和操作超时
四、移动端、跳板机、目标机器的验证流程
4.1 完整验证流程图
sequenceDiagram
participant M as 移动端(iOS/Android)
participant J as 跳板机
participant T as 目标服务器
Note over M,T: 步骤1:连接到跳板机
M->>J: TCP连接请求(端口22)
J->>M: SSH协议版本协商
M->>J: 密钥交换请求
J->>M: 服务器公钥
M->>J: 加密的会话密钥
J->>M: 用户认证请求
M->>J: 认证信息(密码/密钥/证书)
J->>M: 认证结果
Note over M,T: 步骤2:通过跳板机连接目标服务器
M->>J: 通道创建请求(direct-tcpip)
J->>T: TCP连接建立(端口22)
T->>J: SSH协议版本协商
J->>T: 密钥交换请求
T->>J: 服务器公钥
J->>T: 加密的会话密钥
T->>J: 用户认证请求
J->>T: 认证信息(基于跳板机配置)
T->>J: 认证结果
J->>M: 通道创建成功
Note over M,T: 步骤3:执行操作
M->>J: SSH命令/数据通过通道传输
J->>T: 转发SSH命令/数据
T->>J: 返回执行结果
J->>M: 转发执行结果
4.2 详细验证步骤
阶段一:移动端到跳板机验证
// iOS移动端验证跳板机
func authenticateJumpHost() -> Bool {
// 1. 主机密钥验证
let hostKey = session.hostKey
if !verifyHostKey(hostKey) {
showAlert("警告:跳板机密钥未验证")
return false
}
// 2. 用户认证
if usePasswordAuth {
return session.authenticateByPassword(password)
} else if useKeyAuth {
let keyData = loadPrivateKeyFromKeychain()
return session.authenticateByPrivateKey(keyData, passphrase: nil)
} else if useCertificate {
let certData = loadCertificate()
return session.authenticateByCertificate(certData)
}
return false
}
阶段二:跳板机到目标服务器验证
# 跳板机上的验证配置
# /etc/ssh/sshd_config 配置示例:
# 1. 允许端口转发
AllowTcpForwarding yes
PermitTunnel yes
# 2. 强制使用特定用户连接到目标服务器
Match User jumpuser
ForceCommand /usr/local/bin/ssh-proxy %h %p
# 3. ssh-proxy脚本示例
#!/bin/bash
TARGET_HOST=$1
TARGET_PORT=$2
# 使用配置好的密钥自动连接
ssh -o BatchMode=yes \
-o StrictHostKeyChecking=yes \
-i /etc/ssh/jump-to-target.key \
admin@$TARGET_HOST -p $TARGET_PORT
阶段三:移动端到目标服务器的透明验证
// 移动端无需感知二次验证
class TransparentSSHConnection {
func connectToInternalServer() {
// 用户只需操作一次
let success = ssh.connect(host: "internal-server-1")
// 底层自动完成:
// 1. 验证跳板机
// 2. 建立隧道
// 3. 验证目标服务器(自动使用跳板机配置的密钥)
// 4. 建立SSH会话
}
}
4.3 安全增强措施
- 双因素认证:跳板机启用2FA
- 证书认证:使用短期证书替代长期密钥
- 会话审计:记录所有SSH会话操作
- 时间限制:限制访问时间段
- 命令限制:限制可在目标服务器执行的命令
五、端口转发SSH连接,目标主机是否可以不验证?
5.1 技术分析:必须验证
核心结论:目标主机必须进行验证,无法跳过。
原因分析:
-
SSH协议设计:
SSH连接 = TCP连接 + SSH握手 + 用户认证 + 通道管理 libssh2_channel_direct_tcpip_ex() 只完成:TCP连接 缺失:SSH握手 + 用户认证 + 通道管理 -
安全模型限制:
- 每个SSH连接都是独立的
- 认证状态不能在不同连接间传递
- 通道复用不等于认证复用
代码验证:
// 尝试跳过认证(不会成功)
func trySkipAuthentication() -> Bool {
// 1. 建立TCP通道
let tcpChannel = libssh2_channel_direct_tcpip_ex(...)
// 2. 尝试直接执行命令(会失败)
let rc = libssh2_channel_exec(tcpChannel, "ls -la")
// rc ≠ 0,因为tcpChannel不是SSH会话通道
// 3. 尝试发送SSH协议数据(复杂且不安全)
let sshBanner = "SSH-2.0-libssh2_1.11.1\r\n"
libssh2_channel_write_ex(tcpChannel, 0, sshBanner, strlen(sshBanner))
// 需要完整实现SSH协议栈,不现实
return false
}
5.2 看似"无验证"的实现方案
方案A:跳板机代理执行(非真正无验证)
// 在跳板机上执行SSH命令,而不是在目标服务器上建立SSH会话
func executeViaJumpHost(command: String) -> String? {
// 命令格式:让跳板机SSH到目标服务器执行命令
let proxyCommand = "ssh -o BatchMode=yes admin@target-server '\(command)'"
// 在跳板机上执行这个命令
let channel = libssh2_channel_open_session(jumpSession)
libssh2_channel_exec(channel, proxyCommand)
// 读取输出(来自目标服务器,但通过跳板机中转)
return readChannelOutput(channel)
}
// 特点:目标服务器仍然验证了跳板机的SSH连接
方案B:配置极简认证(不推荐)
# 目标服务器上的危险配置
# /etc/ssh/sshd_config
# 1. 允许空密码(极度危险!)
PermitEmptyPasswords yes
# 2. 创建空密码用户
sudo useradd -m -s /bin/bash nopass
sudo passwd -d nopass # 删除密码
# 3. 限制仅跳板机IP可访问
AllowUsers nopass@跳板机IP
方案C:基于主机的认证(仍有限验证)
# 目标服务器配置
# 1. 启用基于主机的认证
HostbasedAuthentication yes
IgnoreRhosts no
# 2. 配置信任的跳板机
echo "jumpmachine" >> /etc/ssh/shosts.equiv
# 3. 跳板机需要生成主机密钥
ssh-keyscan -t rsa jumpmachine >> /etc/ssh/ssh_known_hosts
5.3 为什么必须验证:安全架构视角
企业安全要求:
| 要求 | 无验证架构 | 标准验证架构 |
|---|---|---|
| 身份验证 | ❌ 无法验证具体用户 | ✅ 明确用户身份 |
| 访问审计 | ❌ 无法追踪谁做了什么 | ✅ 完整操作日志 |
| 权限控制 | ❌ 全部或无 | ✅ 细粒度控制 |
| 合规性 | ❌ 违反所有安全标准 | ✅ 符合合规要求 |
攻击场景分析:
无验证架构攻击链:
攻击者 → 入侵跳板机 → 无限制访问所有内网服务器
↑
单点防线突破
标准架构攻击链:
攻击者 → 入侵跳板机 → 仍需破解每个服务器的认证
↑
多层防线
5.4 正确的"简化"验证方案
推荐方案:SSH证书认证
# 架构:中央CA → 签发证书 → 跳板机 → 自动验证 → 目标服务器
# 1. 创建CA
ssh-keygen -t rsa -b 4096 -f ca_key -C "Company SSH CA"
# 2. 为目标服务器签发证书
ssh-keygen -s ca_key -I "jump-access" -h \
-V +1d -n server1,server2,server3 \
/etc/ssh/ssh_host_rsa_key.pub
# 3. 目标服务器信任CA
echo "@cert-authority * $(cat ca_key.pub)" >> /etc/ssh/ssh_known_hosts
# 4. 跳板机使用证书连接(自动验证)
ssh -o CertificateFile=/etc/ssh/certificate \
admin@target-server
iOS端证书认证实现:
class CertificateSSHConnection {
func connectWithCertificate() -> Bool {
// 1. 加载证书
guard let certData = loadCertificateFromKeychain() else {
return false
}
// 2. 使用证书认证
let rc = libssh2_userauth_publickey(
session,
username,
certData.publicKey,
certData.privateKey,
nil
)
// 3. 证书自动验证,无需用户交互
return rc == 0
}
}
5.5 总结:验证的必要性
- 技术层面:SSH协议设计要求每个连接都必须验证
- 安全层面:无验证架构存在巨大安全风险
- 合规层面:违反所有行业安全标准
- 运维层面:无法审计和追踪操作
最佳实践:
- 使用SSH证书认证实现"免交互"验证
- 配置合理的证书有效期(如24小时)
- 启用详细的审计日志
- 实施基于角色的访问控制
记住:在安全领域,便利性永远不应牺牲安全性。正确的做法是使用技术手段(如证书认证)在保持安全的同时提升用户体验,而不是通过降低安全标准来获取便利。