以前做跳板机的程序,都是使用的密码认证,因为这样好区分不同的权限。如果使用 key 的话,因为文件在本地,担心不安全容易复制出去,也没法控制用户只使用指定的跳板程序来做登陆的过程。
所以我们有个强列的需求,远程登陆只有指定的程序才能登陆,这样好做审核,所以系统的 ssh 命令不能登陆才对,key 不能让用户能见到才能包证其它程序不使用,当然也不能让用户支持密码登陆。
所以做一个正规一定的跳板机需要以上的条件。所以我们只有给 key 写到程序本身才能实现。但基本的 Net::SSH::Perl 这个模块只支持指定文件路径的 publickey 才行。我们必须修复这个程序本身。
我试着重载的方式不破坏程序本身来修改,发现根本不能实现,因为我在主程序中修改完后。后面的 Net::SSH::Perl 内部有多层的 require 会直接冲掉我的重载。还有回调也会使用当前的命名空间。所以这个方案失败了。只能使用修改模块,然后放自己的目标的方式来实现。
给 key 写到程序本身这个事情非常容易。我们使用 Perl 中强大的现成的 Data::Section::Simple 模块,就不在自己造轮子了。这个模块可以直接从 __DATA__ 中读取指定的 @@ 开关命名的字段.
use Data::Section::Simple qw(get_data_section);
my $rootkey = get_data_section('rootkey');
__DATA__
@@ rootkey
-----BEGIN RSA PRIVATE KEY-----
MIIEoQIBAAKCAQEA1QjVtiYPs5ONAZc6XGYc37zrsqnU8OR5Gs7TNwTo1fzomMKk
7j/HS0f64eC72/LjIxGWQZABwhnKf1W2VD017C3fNDniCDHaksGGXEL+7oZ5Ae6f
接下来我自己造了一个 Net::SSH::Perl 中的配置关键字 identity_mem_keys 来给指定的 key 的内容传进去.
use lib 'lib';
my $rootkey = get_data_section('rootkey');
my $ssh = Net::SSH::Perl->new('xxx.xxx.xxx.xxx',
port => 22,
privileged => 0,
identity_mem_keys => [ $rootkey],
debug => 1
);
$ssh->login;
my ($stdout, $stderr, $exit) = $ssh->cmd("ls -l /tmp");
print "$stdoutn";
关于程序本身,我就 hook 了一个文件 Net::SSH::Perl::Auth::PublicKey 中的二个部分,替换了原生的的 authenticate 加入了可以从内存中取 key 的功能。
sub authenticate {
my $auth = shift;
my $ssh = $auth->{ssh};
my $sent = 0;
if (my $agent = $auth->mgr->agent) {
do {
$sent = $auth->_auth_agent;
} until $sent || $agent->num_left <= 0;
}
return $sent if $sent;
my $ik = $ssh->config->get('identity_mem_keys') || [];
for my $key (@$ik) {
return 1 if $auth->_auth_key($key);
}
my $if = $ssh->config->get('identity_files') || [];
my $idx = $auth->{_identity_idx} || 0;
for my $f (@$if[$idx..$#$if]) {
$auth->{_identity_idx}++;
return 1 if $auth->_auth_identity($f);
}
};
这个新加的配置文件的关键字,就是在这个地方生效,自己定了一个 _auth_key 的函数。然后在这个函数中做了很复杂的读 key 之类的操作.
sub _auth_key {
my $auth = shift;
my($auth_data) = @_;
my $ssh = $auth->{ssh};
my($key);
$ssh->debug("Trying pubkey authentication with memory key file ");
no warnings qw(redefine);
*Net::SSH::Perl::Key::read_mem_private_pem = sub {
my $class = shift;
my $desc = $_[0];
return unless $desc;
chomp($desc);
my($object) = $desc =~ /^-----?s?BEGIN ([^n-]+)s?-?----/;
$object =~ s/s*$//;
my %OBJ_MAP = (
'DSA PRIVATE KEY' => [ 'DSA' ],
'SSH2 ENCRYPTED PRIVATE KEY' => [ 'DSA', [ 'SSH2' ] ],
'RSA PRIVATE KEY' => [ 'RSA' ],
);
my $rec = $OBJ_MAP{$object} or return;
$class = "Net::SSH::Perl::Key::" . $rec->[0];
eval "use $class;";
die "Key class '$class' is unsupported: $@" if $@;
my @args = $rec->[1] ? @{ $rec->[1] } : ();
# read_private
my($key_file, $passphrase, $datafellows) = (@_, @args);
my $key = $class->new(undef, $datafellows);
my $pem = $key->_pem;
my $pkey = $pem->decode(
Content => $desc,
Password => $passphrase,
);
return unless $pkey;
for my $m (qw( n e )) {
$key->{rsa_pub}->$m( $pkey->{RSAPrivateKey}->{$m} );
}
## Don't use iqmp from the keyfile; let Crypt::RSA compute
## it on its own, because the parameters in Crypt::RSA CRT
## differ from those in OpenSSL, and we need to use ipmq,
## not iqmp.
for my $m (qw( n d p q dp dq )) {
$key->{rsa_priv}->$m( $pkey->{RSAPrivateKey}->{$m} );
}
return $key;
};
$key = Net::SSH::Perl::Key->read_mem_private_pem($auth_data, '', $ssh->{datafellows});
if (!$key) {
my $passphrase = "";
if ($ssh->config->get('interactive')) {
$passphrase = _read_passphrase("Enter passphrase for keyfile : ");
}
else {
$ssh->debug("Will not query passphrase for in batch mode.");
}
$key = Net::SSH::Perl::Key->read_mem_private_pem($auth_data, $passphrase, $ssh->{datafellows});
if (!$key) {
$ssh->debug("Loading private key failed.");
return 0;
}
}
$auth->_sign_send_pubkey($key, &key_sign);
};
在这个中我还重载了一下 Net::SSH::Perl::Key::read_mem_private_pem 这个函数,来支持从内存参数中读取 key 的功能。这样整个文件完成了。
以后只要连接的时候判断一下环境变量的用户是什么,然后根据用户所在组的不同,来读不同的 key ,然后连接就好了。这样整个过程都是保密的。。当然记得给你的程序过一下 filter 模块来加密一下。
以上前一个函数的修改参考于 Use in-memory file as argument in SFTP