集成 PHP、SSH 和 ssh-agent
Integrating PHP, SSH and ssh-agent
我打算编写一个 PHP 脚本来建立 SSH 连接。我研究了如何做到这一点,这看起来是最有希望的解决方案:https://github.com/phpseclib/phpseclib 我唯一的问题是如何处理我的 SSH 密钥有密码的事实,我不想每次都输入它时间我 运行 脚本。对于每天的 SSH 使用,我在后台都有 ssh-agent 运行ning,它被配置为使用 pinentry。这使得我不必每次都输入我的密码。关于如何让 PHP 和 ssh-agent 相互交谈的任何想法?我唯一的线索是 ssh-agent 设置了一个环境变量,SSH_AUTH_SOCK
,指向一个套接字文件。
虽然 phpseclib 的文档解决了这个问题,但它的回答很愚蠢(只需将密码短语放入代码中):http://phpseclib.sourceforge.net/ssh/2.0/auth.html#encrsakey
更新:我深入研究了 phpseclib 并编写了我自己的简单包装器 class。但是,我无法通过 ssh-agent 或提供我的 RSA 密钥让它登录。只有基于密码的身份验证有效,这与我使用 ssh 命令直接登录的经验相反。这是我的代码:
<?php
// src/Connection.php
declare(strict_types=1);
namespace MyNamespace\PhpSsh;
use phpseclib\System\SSH\Agent;
use phpseclib\Net\SSH2;
use phpseclib\Crypt\RSA;
use Exception;
class Connection
{
private SSH2 $client;
private string $host;
private int $port;
private string $username;
/**
* @param string $host
* @param int $port
* @param string $username
*
* @return void
*/
public function __construct(string $host, int $port,
string $username)
{
$this->host = $host;
$this->port = $port;
$this->username = $username;
$this->client = new SSH2($host, $port);
}
/**
* @return bool
*/
public function connectUsingAgent(): bool
{
$agent = new Agent();
$agent->startSSHForwarding($this->client);
return $this->client->login($this->username, $agent);
}
/**
* @param string $key_path
* @param string $passphrase
*
* @return bool
* @throws Exception
*/
public function connectUsingKey(string $key_path, string $passphrase = ''): bool
{
if (!file_exists($key_path)) {
throw new Exception(sprintf('Key file does not exist: %1$s', $key_path));
}
if (is_dir($key_path)) {
throw new Exception(sprintf('Key path is a directory: %1$s', $key_path));
}
if (!is_readable($key_path)) {
throw new Exception(sprintf('Key file is not readable: %1$s', $key_path));
}
$key = new RSA();
if ($passphrase) {
$key->setPassword($passphrase);
}
$key->loadKey(file_get_contents($key_path));
return $this->client->login($this->username, $key);
}
/**
* @param string $password
*
* @return bool
*/
public function connectUsingPassword(string $password): bool
{
return $this->client->login($this->username, $password);
}
/**
* @return void
*/
public function disconnect(): void
{
$this->client->disconnect();
}
/**
* @param string $command
* @param callable $callback
*
* @return string|false
*/
public function exec(string $command, callable $callback = null)
{
return $this->client->exec($command, $callback);
}
/**
* @return string[]
*/
public function getErrors(): array {
return $this->client->getErrors();
}
}
并且:
<?php
// test.php
use MyNamespace\PhpSsh\Connection;
require_once(__DIR__ . '/vendor/autoload.php');
(function() {
$host = '0.0.0.0'; // Fake, obviously
$username = 'user'; // Fake, obviously
$connection = new Connection($host, 22, $username);
$connection_method = 'AGENT'; // or 'KEY', or 'PASSWORD'
switch($connection_method) {
case 'AGENT':
$connected = $connection->connectUsingAgent();
break;
case 'KEY':
$key_path = getenv( 'HOME' ) . '/.ssh/id_rsa.pub';
$passphrase = trim(fgets(STDIN)); // Pass this in on command line via < key_passphrase.txt
$connected = $connection->connectUsingKey($key_path, $passphrase);
break;
case 'PASSWORD':
default:
$password = trim(fgets(STDIN)); // Pass this in on command line via < password.txt
$connected = $connection->connectUsingPassword($password);
break;
}
if (!$connected) {
fwrite(STDERR, "Failed to connect to server!" . PHP_EOL);
$errors = implode(PHP_EOL, $connection->getErrors());
fwrite(STDERR, $errors . PHP_EOL);
exit(1);
}
$command = 'whoami';
$result = $connection->exec($command);
echo sprintf('Output of command "%1$s:"', $command) . PHP_EOL;
echo $result . PHP_EOL;
$command = 'pwd';
$result = $connection->exec($command);
echo sprintf('Output of command "%1$s:"', $command) . PHP_EOL;
echo $result . PHP_EOL;
$connection->disconnect();
})();
SSH2
class 有一个 getErrors()
方法,不幸的是它没有记录任何我的情况。我不得不调试 class。我发现,无论是使用 ssh-agent 还是传入我的密钥,它总是到达这个位置 (https://github.com/phpseclib/phpseclib/blob/2.0.23/phpseclib/Net/SSH2.php#L2624):
<?php
// vendor/phpseclib/phpseclib/phpseclib/Net/SSH2.php: line 2624
extract(unpack('Ctype', $this->_string_shift($response, 1)));
switch ($type) {
case NET_SSH2_MSG_USERAUTH_FAILURE:
// either the login is bad or the server employs multi-factor authentication
return false;
case NET_SSH2_MSG_USERAUTH_SUCCESS:
$this->bitmap |= self::MASK_LOGIN;
return true;
}
显然返回的响应是 NET_SSH2_MSG_USERAUTH_FAILURE
类型。登录没有任何问题,我敢肯定,所以根据代码中的注释,这意味着主机 (Digital Ocean) 必须使用多因素身份验证。这就是我难住的地方。我还缺少哪些其他身份验证方式?这是我对 SSH 的理解失败的地方。
phpseclib 支持 SSH 代理。例如
use phpseclib\Net\SSH2;
use phpseclib\System\SSH\Agent;
$agent = new Agent;
$ssh = new SSH2('localhost');
if (!$ssh->login('username', $agent)) {
throw new \Exception('Login failed');
}
正在更新您最近的编辑
UPDATE: I've looked more into phpseclib and written my own simple wrapper class. However, I cannot get it to login either through ssh-agent or by supplying my RSA key. Only password-based authentication works, contrary to my experiences logging in directly with the ssh command.
使用代理身份验证和直接 public 密钥身份验证时,您的 SSH 日志是什么样的?您可以通过在顶部执行 define('NET_SSH2_LOGGING', 2)
并在身份验证失败后执行 echo $ssh->getLog()
来获取 SSH 日志。
Per neubert,我必须做的是将这一行添加到 Connection.php 并且我能够让基于代理的身份验证工作:
$this->client->setPreferredAlgorithms(['hostkey' => ['ssh-rsa']]);
我仍然无法使用基于密钥的身份验证,但我不太关心这个。
我打算编写一个 PHP 脚本来建立 SSH 连接。我研究了如何做到这一点,这看起来是最有希望的解决方案:https://github.com/phpseclib/phpseclib 我唯一的问题是如何处理我的 SSH 密钥有密码的事实,我不想每次都输入它时间我 运行 脚本。对于每天的 SSH 使用,我在后台都有 ssh-agent 运行ning,它被配置为使用 pinentry。这使得我不必每次都输入我的密码。关于如何让 PHP 和 ssh-agent 相互交谈的任何想法?我唯一的线索是 ssh-agent 设置了一个环境变量,SSH_AUTH_SOCK
,指向一个套接字文件。
虽然 phpseclib 的文档解决了这个问题,但它的回答很愚蠢(只需将密码短语放入代码中):http://phpseclib.sourceforge.net/ssh/2.0/auth.html#encrsakey
更新:我深入研究了 phpseclib 并编写了我自己的简单包装器 class。但是,我无法通过 ssh-agent 或提供我的 RSA 密钥让它登录。只有基于密码的身份验证有效,这与我使用 ssh 命令直接登录的经验相反。这是我的代码:
<?php
// src/Connection.php
declare(strict_types=1);
namespace MyNamespace\PhpSsh;
use phpseclib\System\SSH\Agent;
use phpseclib\Net\SSH2;
use phpseclib\Crypt\RSA;
use Exception;
class Connection
{
private SSH2 $client;
private string $host;
private int $port;
private string $username;
/**
* @param string $host
* @param int $port
* @param string $username
*
* @return void
*/
public function __construct(string $host, int $port,
string $username)
{
$this->host = $host;
$this->port = $port;
$this->username = $username;
$this->client = new SSH2($host, $port);
}
/**
* @return bool
*/
public function connectUsingAgent(): bool
{
$agent = new Agent();
$agent->startSSHForwarding($this->client);
return $this->client->login($this->username, $agent);
}
/**
* @param string $key_path
* @param string $passphrase
*
* @return bool
* @throws Exception
*/
public function connectUsingKey(string $key_path, string $passphrase = ''): bool
{
if (!file_exists($key_path)) {
throw new Exception(sprintf('Key file does not exist: %1$s', $key_path));
}
if (is_dir($key_path)) {
throw new Exception(sprintf('Key path is a directory: %1$s', $key_path));
}
if (!is_readable($key_path)) {
throw new Exception(sprintf('Key file is not readable: %1$s', $key_path));
}
$key = new RSA();
if ($passphrase) {
$key->setPassword($passphrase);
}
$key->loadKey(file_get_contents($key_path));
return $this->client->login($this->username, $key);
}
/**
* @param string $password
*
* @return bool
*/
public function connectUsingPassword(string $password): bool
{
return $this->client->login($this->username, $password);
}
/**
* @return void
*/
public function disconnect(): void
{
$this->client->disconnect();
}
/**
* @param string $command
* @param callable $callback
*
* @return string|false
*/
public function exec(string $command, callable $callback = null)
{
return $this->client->exec($command, $callback);
}
/**
* @return string[]
*/
public function getErrors(): array {
return $this->client->getErrors();
}
}
并且:
<?php
// test.php
use MyNamespace\PhpSsh\Connection;
require_once(__DIR__ . '/vendor/autoload.php');
(function() {
$host = '0.0.0.0'; // Fake, obviously
$username = 'user'; // Fake, obviously
$connection = new Connection($host, 22, $username);
$connection_method = 'AGENT'; // or 'KEY', or 'PASSWORD'
switch($connection_method) {
case 'AGENT':
$connected = $connection->connectUsingAgent();
break;
case 'KEY':
$key_path = getenv( 'HOME' ) . '/.ssh/id_rsa.pub';
$passphrase = trim(fgets(STDIN)); // Pass this in on command line via < key_passphrase.txt
$connected = $connection->connectUsingKey($key_path, $passphrase);
break;
case 'PASSWORD':
default:
$password = trim(fgets(STDIN)); // Pass this in on command line via < password.txt
$connected = $connection->connectUsingPassword($password);
break;
}
if (!$connected) {
fwrite(STDERR, "Failed to connect to server!" . PHP_EOL);
$errors = implode(PHP_EOL, $connection->getErrors());
fwrite(STDERR, $errors . PHP_EOL);
exit(1);
}
$command = 'whoami';
$result = $connection->exec($command);
echo sprintf('Output of command "%1$s:"', $command) . PHP_EOL;
echo $result . PHP_EOL;
$command = 'pwd';
$result = $connection->exec($command);
echo sprintf('Output of command "%1$s:"', $command) . PHP_EOL;
echo $result . PHP_EOL;
$connection->disconnect();
})();
SSH2
class 有一个 getErrors()
方法,不幸的是它没有记录任何我的情况。我不得不调试 class。我发现,无论是使用 ssh-agent 还是传入我的密钥,它总是到达这个位置 (https://github.com/phpseclib/phpseclib/blob/2.0.23/phpseclib/Net/SSH2.php#L2624):
<?php
// vendor/phpseclib/phpseclib/phpseclib/Net/SSH2.php: line 2624
extract(unpack('Ctype', $this->_string_shift($response, 1)));
switch ($type) {
case NET_SSH2_MSG_USERAUTH_FAILURE:
// either the login is bad or the server employs multi-factor authentication
return false;
case NET_SSH2_MSG_USERAUTH_SUCCESS:
$this->bitmap |= self::MASK_LOGIN;
return true;
}
显然返回的响应是 NET_SSH2_MSG_USERAUTH_FAILURE
类型。登录没有任何问题,我敢肯定,所以根据代码中的注释,这意味着主机 (Digital Ocean) 必须使用多因素身份验证。这就是我难住的地方。我还缺少哪些其他身份验证方式?这是我对 SSH 的理解失败的地方。
phpseclib 支持 SSH 代理。例如
use phpseclib\Net\SSH2;
use phpseclib\System\SSH\Agent;
$agent = new Agent;
$ssh = new SSH2('localhost');
if (!$ssh->login('username', $agent)) {
throw new \Exception('Login failed');
}
正在更新您最近的编辑
UPDATE: I've looked more into phpseclib and written my own simple wrapper class. However, I cannot get it to login either through ssh-agent or by supplying my RSA key. Only password-based authentication works, contrary to my experiences logging in directly with the ssh command.
使用代理身份验证和直接 public 密钥身份验证时,您的 SSH 日志是什么样的?您可以通过在顶部执行 define('NET_SSH2_LOGGING', 2)
并在身份验证失败后执行 echo $ssh->getLog()
来获取 SSH 日志。
Per neubert,我必须做的是将这一行添加到 Connection.php 并且我能够让基于代理的身份验证工作:
$this->client->setPreferredAlgorithms(['hostkey' => ['ssh-rsa']]);
我仍然无法使用基于密钥的身份验证,但我不太关心这个。