基于跳板机进行SSH连接的认证流程

参考连接: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开发注意事项

  1. 网络权限:需要在Info.plist中添加网络权限
  2. 后台执行:SSH操作应在后台线程执行
  3. 密钥管理:使用Keychain安全存储密钥
  4. 超时处理:设置合理的连接和操作超时

四、移动端、跳板机、目标机器的验证流程

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 安全增强措施

  1. 双因素认证:跳板机启用2FA
  2. 证书认证:使用短期证书替代长期密钥
  3. 会话审计:记录所有SSH会话操作
  4. 时间限制:限制访问时间段
  5. 命令限制:限制可在目标服务器执行的命令

五、端口转发SSH连接,目标主机是否可以不验证?

5.1 技术分析:必须验证

核心结论:目标主机必须进行验证,无法跳过。

原因分析:

  1. SSH协议设计

    SSH连接 = TCP连接 + SSH握手 + 用户认证 + 通道管理
    
    libssh2_channel_direct_tcpip_ex() 只完成:TCP连接
    缺失:SSH握手 + 用户认证 + 通道管理
    
  2. 安全模型限制

    • 每个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 总结:验证的必要性

  1. 技术层面:SSH协议设计要求每个连接都必须验证
  2. 安全层面:无验证架构存在巨大安全风险
  3. 合规层面:违反所有行业安全标准
  4. 运维层面:无法审计和追踪操作

最佳实践

  • 使用SSH证书认证实现"免交互"验证
  • 配置合理的证书有效期(如24小时)
  • 启用详细的审计日志
  • 实施基于角色的访问控制

记住:在安全领域,便利性永远不应牺牲安全性。正确的做法是使用技术手段(如证书认证)在保持安全的同时提升用户体验,而不是通过降低安全标准来获取便利。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容