如何用phpseclib在PHP中模拟SSH跳板机(Jump Host)行为?
解决方案:模拟SSH跳板(ProxyJump)的正确实现
你遇到的问题根源在于:当前代码是在堡垒机的Shell环境中执行ssh命令,这要求堡垒机上的用户持有访问目标机器的私钥,且环境配置正确(比如SSH Agent、密钥权限),这和ssh -J的代理转发逻辑完全不同。ssh -J是通过堡垒机建立TCP隧道,本地直接与目标机器建立SSH连接,私钥始终在本地使用,无需堡垒机持有密钥。
以下是针对不同库的正确实现方案:
方案一:phpseclib3 实现跳板
利用phpseclib3的端口转发功能,通过已建立的堡垒机连接创建到目标机器的转发Socket,再基于该Socket连接目标机器:
<?php require __DIR__ . '/../vendor/autoload.php'; use phpseclib\Crypt\RSA; use phpseclib\Net\SSH2; // 堡垒机参数 $bastionHost = 'bastion.net'; $bastionPort = 22; $bastionUser = 'identifier'; $privateKeyPath = 'ssh_key'; // 目标机器参数 $targetHost = 'xx.xx.xx.xx'; $targetPort = 22; $targetUser = 'identifier'; // 加载私钥(本地持有,无需传到堡垒机) $rsa = new RSA(); $rsa->loadKey(file_get_contents($privateKeyPath)); // 1. 连接堡垒机 $bastionSSH = new SSH2($bastionHost, $bastionPort); if (!$bastionSSH->login($bastionUser, $rsa)) { throw new \Exception('堡垒机登录失败'); } // 2. 通过堡垒机创建到目标机器的转发Socket $forwardedSocket = $bastionSSH->getForwardedPort('127.0.0.1', $targetPort, $targetHost); // 3. 通过转发Socket连接目标机器 $targetSSH = new SSH2($forwardedSocket); if (!$targetSSH->login($targetUser, $rsa)) { throw new \Exception('目标机器登录失败'); } // 执行目标机器上的命令 echo $targetSSH->exec('whoami');
也可以使用phpseclib3的Proxy类简化流程:
<?php require __DIR__ . '/../vendor/autoload.php'; use phpseclib\Crypt\RSA; use phpseclib\Net\SSH2; use phpseclib\Net\Proxy\SSH; $bastionHost = 'bastion.net'; $bastionPort = 22; $bastionUser = 'identifier'; $privateKeyPath = 'ssh_key'; $targetHost = 'xx.xx.xx.xx'; $targetPort = 22; $targetUser = 'identifier'; $rsa = new RSA(); $rsa->loadKey(file_get_contents($privateKeyPath)); // 创建SSH代理(指向堡垒机) $proxy = new SSH($bastionHost, $bastionPort); $proxy->login($bastionUser, $rsa); // 通过代理直接连接目标机器 $targetSSH = new SSH2($targetHost, $targetPort, ['proxy' => $proxy]); if (!$targetSSH->login($targetUser, $rsa)) { throw new \Exception('目标机器登录失败'); } echo $targetSSH->exec('uptime');
方案二:原生libssh2 实现跳板
使用libssh2的ssh2_channel_direct_tcpip建立TCP转发通道,再基于该通道连接目标机器:
<?php // 堡垒机参数 $bastionHost = 'bastion.net'; $bastionPort = 22; $bastionUser = 'identifier'; $privateKeyPath = 'ssh_key'; $pubKeyPath = $privateKeyPath . '.pub'; // 目标机器参数 $targetHost = 'xx.xx.xx.xx'; $targetPort = 22; $targetUser = 'identifier'; // 1. 连接堡垒机 $bastionSession = ssh2_connect($bastionHost, $bastionPort); if (!ssh2_auth_pubkey_file($bastionSession, $bastionUser, $pubKeyPath, $privateKeyPath)) { throw new \Exception('堡垒机登录失败'); } // 2. 建立到目标机器的TCP转发通道 $forwardChannel = ssh2_channel_direct_tcpip($bastionSession, $targetHost, $targetPort); // 3. 通过转发通道连接目标机器 $targetSession = ssh2_connect('localhost', 0, ['session' => $forwardChannel]); if (!ssh2_auth_pubkey_file($targetSession, $targetUser, $pubKeyPath, $privateKeyPath)) { throw new \Exception('目标机器登录失败'); } // 执行命令并输出结果 $stream = ssh2_exec($targetSession, 'hostname'); stream_set_blocking($stream, true); echo stream_get_contents($stream);
关键原理说明
ssh -J的本质是本地→堡垒机→目标机器的TCP流量转发,私钥始终在本地用于身份验证,堡垒机仅作为流量中转节点。- 原方案的错误在于将SSH连接的控制权交给了堡垒机的Shell环境,依赖堡垒机的密钥配置,这不符合跳板的设计逻辑。
内容的提问来源于stack exchange,提问作者Raphaël
相关产品推荐
相关产品推荐

