以前做跳板机的程序,都是使用的密码认证,因为这样好区分不同的权限。如果使用 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