mirror of
https://github.com/xcat2/xcat-core.git
synced 2026-08-27 09:06:39 +00:00
Merge pull request #7738 from VersatusHPC/fix/genesis-service-node-certificates
fix(credentials): delegate node certificates through service nodes
This commit is contained in:
@@ -146,6 +146,11 @@ group **compute2** are serviced by sn2:
|
||||
chdef -t group compute1 servicenode=sn1 xcatmaster=sn1-c
|
||||
chdef -t group compute2 servicenode=sn2 xcatmaster=sn2-c
|
||||
|
||||
For delegated certificate requests, the service node's source address when
|
||||
connecting to ``site.master`` must match one of the compute node's
|
||||
``servicenode`` entries. Hostname entries are resolved on the management node
|
||||
before comparison.
|
||||
|
||||
.. note:: In this example, sn1 and sn2 are the node names of the service nodes
|
||||
(and therefore the hostnames associated with the NICs that the MN talks to).
|
||||
The hostnames sn1-c and sn2-c are associated with the SN NICs that communicate
|
||||
|
||||
@@ -76,6 +76,11 @@ The following table illustrates the cluster being used in this example:
|
||||
* ``servicenode`` - defines which Service Node the **Management Node** should send commands to (e.g ``xdsh``) and should be set to the hostname or IP address of the service node that the management node can contact it by.
|
||||
* ``xcatmaster`` - defines which Service Node the **Compute Node** should boot from and should be set to the hostname or IP address of the service node that the compute node can contact it by.
|
||||
|
||||
For delegated certificate requests, the service node's source address when
|
||||
connecting to ``site.master`` must match one of the compute node's
|
||||
``servicenode`` entries. Hostname entries are resolved on the management
|
||||
node before comparison.
|
||||
|
||||
You must set both ``servicenode`` and ``xcatmaster`` regardless of whether or not you are using service node pools. For most scenarios, the value will be identical. ::
|
||||
|
||||
chdef -t group -o rack1 servicenode=r1n01 xcatmaster=r1n01
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
Supported command:
|
||||
getcredentials
|
||||
signx509cert (service-node delegation only)
|
||||
|
||||
=cut
|
||||
|
||||
@@ -29,15 +30,20 @@ use xCAT::Table;
|
||||
use Data::Dumper;
|
||||
use xCAT::NodeRange;
|
||||
use xCAT::Zone;
|
||||
use File::Temp qw(tempfile);
|
||||
use IO::Socket::INET;
|
||||
use Time::HiRes qw(sleep);
|
||||
|
||||
use xCAT::Utils;
|
||||
use xCAT::NetworkUtils;
|
||||
use xCAT::PasswordUtils;
|
||||
use xCAT::TableUtils;
|
||||
|
||||
use xCAT::MsgUtils;
|
||||
use Getopt::Long;
|
||||
|
||||
use constant DELEGATED_SIGNING_TIMEOUT => 30;
|
||||
|
||||
#-------------------------------------------------------
|
||||
|
||||
=head3 handled_commands
|
||||
@@ -50,7 +56,10 @@ Return list of commands handled by this plugin
|
||||
|
||||
sub handled_commands
|
||||
{
|
||||
return { getcredentials => "credentials" };
|
||||
return {
|
||||
getcredentials => "credentials",
|
||||
signx509cert => "credentials",
|
||||
};
|
||||
}
|
||||
|
||||
#-------------------------------------------------------
|
||||
@@ -74,6 +83,30 @@ sub process_request
|
||||
my $envs = $request->{env};
|
||||
my $client;
|
||||
|
||||
if ($command eq 'signx509cert') {
|
||||
my $node = $request->{arg} ? $request->{arg}->[0] : undef;
|
||||
my $csr = $request->{csr} ? $request->{csr}->[0] : undef;
|
||||
unless ($node and $csr and _delegated_signer_allowed($request, $node)) {
|
||||
xCAT::MsgUtils->trace(0, 'E', 'Rejected delegated certificate request');
|
||||
$callback->({ error => ['Delegated certificate request denied'], errorcode => [1] });
|
||||
return;
|
||||
}
|
||||
my $certificate = _sign_x509_certificate($node, $csr);
|
||||
unless ($certificate) {
|
||||
$callback->({ error => ["Unable to sign certificate for $node"], errorcode => [1] });
|
||||
return;
|
||||
}
|
||||
my $peer = $request->{'_xcat_clientip'}
|
||||
? $request->{'_xcat_clientip'}->[0]
|
||||
: $request->{'_xcat_clienthost'}
|
||||
? $request->{'_xcat_clienthost'}->[0]
|
||||
: $request->{'_xcat_clientfqdn'}->[0];
|
||||
xCAT::MsgUtils->trace(0, 'I',
|
||||
"credentials: signed certificate for $node requested by $peer");
|
||||
$callback->({ data => [{ content => [$certificate], desc => ['x509cert'] }] });
|
||||
return;
|
||||
}
|
||||
|
||||
#Because clients may be stuck with stunnel, we cannot presume they
|
||||
#can explicitly bind to a low port number as a client
|
||||
#unless ($request and $request->{'_xcat_clientport'} and $request->{'_xcat_clientport'}->[0] and $request->{'_xcat_clientport'}->[0] < 1000) {
|
||||
@@ -281,60 +314,18 @@ sub process_request
|
||||
} elsif ($parm =~ /x509cert/) {
|
||||
xCAT::MsgUtils->trace(0, 'I', "credentials: sending $parm to $client");
|
||||
my $csr = $request->{'csr'}->[0];
|
||||
my $csrfile;
|
||||
my $oldumask = umask 0077;
|
||||
if (-e "/tmp/xcat/client.csr.$$") { unlink "/tmp/xcat/client.csr.$$"; }
|
||||
open($csrfile, ">", "/tmp/xcat/client.csr.$$");
|
||||
unless ($csrfile) { next; }
|
||||
my @statdat = stat $csrfile;
|
||||
|
||||
while ($statdat[4] != 0 or $statdat[2] & 020 or $statdat[2] & 002) { #try to be paranoid, root better own the file, and it better not be writable by anyone but owner
|
||||
#this means to assure the filehandle is not write-accessible to others who may insert their malicious CSR
|
||||
close($csrfile);
|
||||
unlink("/tmp/xcat/client.csr.$$");
|
||||
open($csrfile, ">", "/tmp/xcat/client.csr.$$");
|
||||
@statdat = stat $csrfile;
|
||||
my ($certificate, $error);
|
||||
if (xCAT::Utils->isServiceNode()) {
|
||||
($certificate, $error) = _request_x509_from_master($client, $csr);
|
||||
} else {
|
||||
$certificate = _sign_x509_certificate($client, $csr);
|
||||
}
|
||||
print $csrfile $csr;
|
||||
close($csrfile);
|
||||
|
||||
#ok, at this point, we can verify that the subject is one we wouldn't mind signing...
|
||||
my $subject = `openssl req -in /tmp/xcat/client.csr.$$ -subject -noout`;
|
||||
chomp($subject);
|
||||
unless ($subject =~ /CN=$client\z/) { unlink("/tmp/xcat/client.csr.$$"); next; }
|
||||
unlink "/tmp/xcat/client.cert.$$";
|
||||
open($csrfile, ">", "/tmp/xcat/client.cert.$$");
|
||||
@statdat = stat $csrfile;
|
||||
while ($statdat[4] != 0 or $statdat[2] & 020 or $statdat[2] & 002) { #try to be paranoid, root better own the file, and it better not be writable by anyone but owner
|
||||
#this prevents an attacker from predicting pid and pre-setting up a file that they can corrupt for DoS
|
||||
close($csrfile);
|
||||
unlink("/tmp/xcat/client.csr.$$");
|
||||
open($csrfile, ">", "/tmp/xcat/client.csr.$$");
|
||||
@statdat = stat $csrfile;
|
||||
if ($certificate) {
|
||||
push @{ $rsp->{'data'} }, { content => [$certificate], desc => [$parm] };
|
||||
} elsif ($error) {
|
||||
push @{ $rsp->{'error'} }, $error;
|
||||
}
|
||||
close($csrfile);
|
||||
open($csrfile, "<", "/etc/xcat/ca/index");
|
||||
my @caindex = <$csrfile>;
|
||||
close($csrfile);
|
||||
foreach (@caindex) {
|
||||
chomp;
|
||||
my ($type, $expiry, $revoke, $serial, $fname, $subject) = split /\t/;
|
||||
if ($type eq 'V' and $subject =~ /CN=$client\z/) { #we already have a valid certificate, new request replaces it, revoke old
|
||||
#print "The time of replacing is at hand for $client\n";
|
||||
xCAT::MsgUtils->trace(0, 'I', "credentials: The time of replacing is at hand for $client");
|
||||
system("openssl ca -config /etc/xcat/ca/openssl.cnf -revoke /etc/xcat/ca/certs/$serial.pem");
|
||||
}
|
||||
}
|
||||
my $rc = system("openssl ca -config /etc/xcat/ca/openssl.cnf -in /tmp/xcat/client.csr.$$ -out /tmp/xcat/client.cert.$$ -batch");
|
||||
unlink("/tmp/xcat/client.csr.$$");
|
||||
umask($oldumask);
|
||||
if ($rc) { next; }
|
||||
open($csrfile, "<", "/tmp/xcat/client.cert.$$");
|
||||
my @certdata = <$csrfile>;
|
||||
close($csrfile);
|
||||
unlink "/tmp/xcat/client.cert.$$";
|
||||
my $certcontents = join('', @certdata);
|
||||
push @{ $rsp->{'data'} }, { content => [$certcontents], desc => [$parm] };
|
||||
next;
|
||||
} elsif ($parm =~ /xcat_secure_pw:/) {
|
||||
xCAT::MsgUtils->trace(0, 'I', "credentials: sending $parm to $client");
|
||||
my @users=split(/:/,$parm);
|
||||
@@ -376,6 +367,189 @@ sub process_request
|
||||
return;
|
||||
}
|
||||
|
||||
sub _delegated_signer_allowed {
|
||||
my ($request, $node) = @_;
|
||||
return 0 unless $request->{'_xcat_authname'}
|
||||
and $request->{'_xcat_authname'}->[0] eq 'root';
|
||||
return 0 unless $node;
|
||||
|
||||
my %requester;
|
||||
my @identity_attributes = $request->{'_xcat_clientip'}
|
||||
? qw(_xcat_clientip)
|
||||
: qw(_xcat_clienthost _xcat_clientfqdn);
|
||||
foreach my $attribute (@identity_attributes) {
|
||||
next unless $request->{$attribute};
|
||||
my $value = ref($request->{$attribute}) eq 'ARRAY'
|
||||
? $request->{$attribute}->[0] : $request->{$attribute};
|
||||
$requester{$_} = 1 for _endpoint_identities($value);
|
||||
}
|
||||
return 0 unless %requester;
|
||||
|
||||
my $noderes = xCAT::Table->new('noderes');
|
||||
return 0 unless $noderes;
|
||||
my $attributes = $noderes->getNodeAttribs($node, ['servicenode']);
|
||||
return 0 unless $attributes and $attributes->{servicenode};
|
||||
|
||||
foreach my $service_node (split /,/, $attributes->{servicenode}) {
|
||||
return 1 if grep { $requester{$_} } _endpoint_identities($service_node);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
sub _endpoint_identities {
|
||||
my $endpoint = shift;
|
||||
return unless defined($endpoint);
|
||||
$endpoint =~ s/^\s+|\s+$//g;
|
||||
return unless length($endpoint);
|
||||
|
||||
my %identities;
|
||||
my $name = _normalize_endpoint_identity($endpoint);
|
||||
$identities{$name} = 1;
|
||||
|
||||
my @addresses = xCAT::NetworkUtils->getipaddr($endpoint, GetAllAddresses => 1);
|
||||
foreach my $address (@addresses) {
|
||||
next unless defined($address) && length($address);
|
||||
$identities{_normalize_endpoint_identity($address)} = 1;
|
||||
}
|
||||
return keys %identities;
|
||||
}
|
||||
|
||||
sub _normalize_endpoint_identity {
|
||||
my $identity = lc(shift);
|
||||
$identity =~ s/\.$//;
|
||||
$identity =~ s/^::ffff:(?=\d+(?:\.\d+){3}\z)//;
|
||||
return $identity;
|
||||
}
|
||||
|
||||
sub _request_x509_from_master {
|
||||
my ($node, $csr) = @_;
|
||||
my @masters = xCAT::TableUtils->get_site_attribute('master');
|
||||
unless ($masters[0]) {
|
||||
my $error = 'The management node is not configured';
|
||||
xCAT::MsgUtils->trace(0, 'E', $error);
|
||||
return wantarray ? (undef, $error) : undef;
|
||||
}
|
||||
|
||||
require xCAT::Client;
|
||||
local $ENV{XCATHOST} = $masters[0] =~ /:/
|
||||
? "[$masters[0]]:3001"
|
||||
: "$masters[0]:3001";
|
||||
my $certificate;
|
||||
my @errors;
|
||||
my $request = {
|
||||
command => ['signx509cert'],
|
||||
arg => [$node],
|
||||
csr => [$csr],
|
||||
};
|
||||
my $failure;
|
||||
{
|
||||
local $SIG{ALRM} = sub {
|
||||
die 'management node request timed out after '
|
||||
. DELEGATED_SIGNING_TIMEOUT . " seconds\n";
|
||||
};
|
||||
my $request_ok = eval {
|
||||
alarm(DELEGATED_SIGNING_TIMEOUT);
|
||||
xCAT::Client::submit_request($request, sub {
|
||||
my $response = shift;
|
||||
my $response_errors = $response->{error};
|
||||
if (defined($response_errors)) {
|
||||
my @response_errors = ref($response_errors) eq 'ARRAY'
|
||||
? @{$response_errors} : ($response_errors);
|
||||
push @errors,
|
||||
grep { defined($_) && !ref($_) && length($_) } @response_errors;
|
||||
}
|
||||
foreach my $data (@{ $response->{data} || [] }) {
|
||||
next unless ref($data) eq 'HASH';
|
||||
my $description = ref($data->{desc}) eq 'ARRAY'
|
||||
? $data->{desc}->[0] : $data->{desc};
|
||||
next unless $description and $description eq 'x509cert';
|
||||
$certificate = ref($data->{content}) eq 'ARRAY'
|
||||
? $data->{content}->[0] : $data->{content};
|
||||
}
|
||||
});
|
||||
1;
|
||||
};
|
||||
$failure = $@ unless $request_ok;
|
||||
alarm(0);
|
||||
}
|
||||
unless ($certificate) {
|
||||
my $reason = $failure || join('; ', @errors) || 'management node returned no certificate';
|
||||
$reason =~ s/[\r\n]+/ /g;
|
||||
$reason =~ s/\s+$//;
|
||||
my $error = "Unable to obtain a delegated certificate for $node: $reason";
|
||||
xCAT::MsgUtils->trace(0, 'E', $error);
|
||||
return wantarray ? (undef, $error) : undef;
|
||||
}
|
||||
return wantarray ? ($certificate, undef) : $certificate;
|
||||
}
|
||||
|
||||
sub _csr_subject_matches_node {
|
||||
my ($csrpath, $client) = @_;
|
||||
open(my $subject_file, '-|', 'openssl', 'req', '-in', $csrpath,
|
||||
'-subject', '-noout', '-nameopt', 'RFC2253')
|
||||
or die "cannot inspect CSR";
|
||||
my $subject = <$subject_file>;
|
||||
close($subject_file) or die "invalid CSR";
|
||||
$subject =~ s/[\r\n]+$// if defined($subject);
|
||||
return 0 unless defined($subject);
|
||||
return $subject =~ /^subject=\s*CN=\Q$client\E\z/ ? 1 : 0;
|
||||
}
|
||||
|
||||
sub _sign_x509_certificate {
|
||||
my ($client, $csr) = @_;
|
||||
my $oldumask = umask 0077;
|
||||
my ($csrfile, $csrpath);
|
||||
my ($certfile, $certpath);
|
||||
my $certificate;
|
||||
my $failure;
|
||||
|
||||
my $signing_ok = eval {
|
||||
($csrfile, $csrpath) = tempfile('xcat-client-csr-XXXXXX', TMPDIR => 1, UNLINK => 0);
|
||||
print {$csrfile} $csr or die "cannot write CSR";
|
||||
close($csrfile) or die "cannot close CSR";
|
||||
|
||||
die "certificate subject does not match node"
|
||||
unless _csr_subject_matches_node($csrpath, $client);
|
||||
|
||||
($certfile, $certpath) = tempfile('xcat-client-cert-XXXXXX', TMPDIR => 1, UNLINK => 0);
|
||||
close($certfile) or die "cannot close certificate file";
|
||||
|
||||
open(my $index, '<', '/etc/xcat/ca/index') or die "cannot read CA index";
|
||||
my @caindex = <$index>;
|
||||
close($index) or die "cannot close CA index";
|
||||
foreach (@caindex) {
|
||||
chomp;
|
||||
my ($type, $expiry, $revoke, $serial, $fname, $certificate_subject) = split /\t/;
|
||||
if ($type eq 'V' and $certificate_subject =~ /^\/CN=\Q$client\E\z/) {
|
||||
xCAT::MsgUtils->trace(0, 'I', "credentials: replacing the certificate for $client");
|
||||
system('openssl', 'ca', '-config', '/etc/xcat/ca/openssl.cnf',
|
||||
'-revoke', "/etc/xcat/ca/certs/$serial.pem") == 0
|
||||
or die "cannot revoke previous certificate";
|
||||
}
|
||||
}
|
||||
system('openssl', 'ca', '-config', '/etc/xcat/ca/openssl.cnf',
|
||||
'-in', $csrpath, '-out', $certpath, '-batch') == 0
|
||||
or die "certificate signing failed";
|
||||
|
||||
open(my $signed, '<', $certpath) or die "cannot read signed certificate";
|
||||
local $/;
|
||||
$certificate = <$signed>;
|
||||
close($signed) or die "cannot close signed certificate";
|
||||
1;
|
||||
};
|
||||
$failure = $@ unless $signing_ok;
|
||||
|
||||
unlink($csrpath) if $csrpath and -e $csrpath;
|
||||
unlink($certpath) if $certpath and -e $certpath;
|
||||
umask($oldumask);
|
||||
if ($failure) {
|
||||
chomp($failure);
|
||||
xCAT::MsgUtils->trace(0, 'E', "Unable to sign certificate for $client: $failure");
|
||||
return;
|
||||
}
|
||||
return $certificate;
|
||||
}
|
||||
|
||||
sub ok_with_node {
|
||||
my $node = shift;
|
||||
|
||||
|
||||
@@ -1587,6 +1587,7 @@ until ($quit) {
|
||||
$clientselect->add($connection);
|
||||
my $peerhost = undef;
|
||||
my $peerfqdn = undef;
|
||||
my $peerip = $connection->peerhost();
|
||||
my $peer = $connection->peer_certificate("owner");
|
||||
if ($peer) {
|
||||
$peer =~ m/CN=([^\/]*)/;
|
||||
@@ -1629,11 +1630,12 @@ until ($quit) {
|
||||
|
||||
#printf('info'.": xcatd: connection from ".($peername ? $peername . "@" . $peerhost : $peerhost)."\n");
|
||||
|
||||
my $debugmsg = "xcatd: connection from " . ($peername ? $peername . "@" . $peerhost : $peerhost) . "\n";
|
||||
my $peerdisplay = $peerhost || $peerip;
|
||||
my $debugmsg = "xcatd: connection from " . ($peername ? $peername . "@" . $peerdisplay : $peerdisplay) . "\n";
|
||||
xCAT::MsgUtils->trace(0, "D", "$debugmsg");
|
||||
|
||||
$$progname = "xcatd SSL: Instance for " . ($peername ? $peername . "@" . $peerhost : $peerhost) if $peerhost;
|
||||
service_connection($connection, $peername, $peerhost, $peerfqdn, $peerhostorg);
|
||||
$$progname = "xcatd SSL: Instance for " . ($peername ? $peername . "@" . $peerdisplay : $peerdisplay) if $peerdisplay;
|
||||
service_connection($connection, $peername, $peerhost, $peerfqdn, $peerhostorg, $peerip);
|
||||
xexit(0);
|
||||
}
|
||||
if ($sslfudgefactor) { $sslfudgefactor -= 1; }
|
||||
@@ -2808,6 +2810,7 @@ sub service_connection {
|
||||
my $peerhost = shift;
|
||||
my $peerfqdn = shift;
|
||||
my $peerhostorg = shift;
|
||||
my $peerip = shift;
|
||||
my $peerport = $sock->peerport;
|
||||
|
||||
# some paranoid measures could reduce a third party abusing stage3 image to attempting to get USER/PASS for BMCs:
|
||||
@@ -2969,6 +2972,9 @@ sub service_connection {
|
||||
$req->{'_xcat_authname'} = [$peername];
|
||||
$req->{'_xcat_clienthost'} = [$peerhost];
|
||||
$req->{'_xcat_clientfqdn'} = [$peerfqdn];
|
||||
if ($req->{command}->[0] eq 'signx509cert') {
|
||||
$req->{'_xcat_clientip'} = [$peerip];
|
||||
}
|
||||
$req->{'_xcat_clientport'} = [$peerport];
|
||||
$$progname = "xcatd SSL: " . $req->{command}->[0];
|
||||
if ($req->{noderange} && defined($req->{noderange}->[0])) {
|
||||
|
||||
@@ -0,0 +1,493 @@
|
||||
#!/usr/bin/env perl
|
||||
## no critic (TestingAndDebugging::ProhibitNoStrict)
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use File::Spec;
|
||||
use File::Temp qw(tempdir);
|
||||
use FindBin;
|
||||
use Test::More;
|
||||
|
||||
BEGIN {
|
||||
package xCAT::Table;
|
||||
our $servicenode;
|
||||
sub import { }
|
||||
sub new { return bless {}, shift; }
|
||||
sub getNodeAttribs { return { servicenode => $servicenode }; }
|
||||
$INC{'xCAT/Table.pm'} = 1;
|
||||
|
||||
package xCAT::NodeRange;
|
||||
our %ranges;
|
||||
sub import {
|
||||
no strict 'refs';
|
||||
*{ caller() . '::noderange' } = \&noderange;
|
||||
}
|
||||
sub noderange {
|
||||
my ($name) = @_;
|
||||
return @{ $ranges{$name} || [] };
|
||||
}
|
||||
$INC{'xCAT/NodeRange.pm'} = 1;
|
||||
|
||||
package xCAT::Zone;
|
||||
sub import { }
|
||||
$INC{'xCAT/Zone.pm'} = 1;
|
||||
|
||||
package xCAT::Utils;
|
||||
our $service_node;
|
||||
sub import { }
|
||||
sub isAIX { return 0; }
|
||||
sub isServiceNode { return $service_node; }
|
||||
$INC{'xCAT/Utils.pm'} = 1;
|
||||
|
||||
package xCAT::NetworkUtils;
|
||||
our %addresses;
|
||||
sub import { }
|
||||
sub getipaddr {
|
||||
my ($class, $endpoint) = @_;
|
||||
return @{ $addresses{$endpoint} || [] };
|
||||
}
|
||||
$INC{'xCAT/NetworkUtils.pm'} = 1;
|
||||
|
||||
package xCAT::PasswordUtils;
|
||||
sub import { }
|
||||
$INC{'xCAT/PasswordUtils.pm'} = 1;
|
||||
|
||||
package xCAT::TableUtils;
|
||||
our $master = '192.0.2.10';
|
||||
sub import { }
|
||||
sub get_site_attribute { return ($master); }
|
||||
$INC{'xCAT/TableUtils.pm'} = 1;
|
||||
|
||||
package xCAT::MsgUtils;
|
||||
our @messages;
|
||||
sub import { }
|
||||
sub trace { push @messages, [@_]; }
|
||||
sub message {
|
||||
push @messages, [@_];
|
||||
my $callback = $_[3];
|
||||
$callback->($_[2]) if ref($callback) eq 'CODE';
|
||||
}
|
||||
$INC{'xCAT/MsgUtils.pm'} = 1;
|
||||
|
||||
package xCAT::Client;
|
||||
our @responses;
|
||||
our ($exception, $request, $host, $trigger_timeout);
|
||||
sub import { }
|
||||
sub submit_request {
|
||||
($request, my $callback) = @_;
|
||||
$host = $ENV{XCATHOST};
|
||||
die $exception if defined($exception);
|
||||
$SIG{ALRM}->() if $trigger_timeout;
|
||||
$callback->($_) for @responses;
|
||||
}
|
||||
$INC{'xCAT/Client.pm'} = 1;
|
||||
|
||||
package LWP;
|
||||
sub import { }
|
||||
$INC{'LWP.pm'} = 1;
|
||||
|
||||
package LWP::UserAgent;
|
||||
sub new { return bless {}, shift; }
|
||||
|
||||
package HTTP::Request::Common;
|
||||
sub import {
|
||||
no strict 'refs';
|
||||
*{ caller() . '::GET' } = sub { return $_[0]; };
|
||||
}
|
||||
$INC{'HTTP/Request/Common.pm'} = 1;
|
||||
}
|
||||
|
||||
my $repo_root = File::Spec->catdir( $FindBin::Bin, '..', '..' );
|
||||
my $plugin = File::Spec->catfile(
|
||||
$repo_root, qw(xCAT-server lib xcat plugins credentials.pm)
|
||||
);
|
||||
require $plugin;
|
||||
|
||||
sub reset_client {
|
||||
@xCAT::Client::responses = ();
|
||||
@xCAT::MsgUtils::messages = ();
|
||||
$xCAT::Client::exception = undef;
|
||||
$xCAT::Client::request = undef;
|
||||
$xCAT::Client::host = undef;
|
||||
$xCAT::Client::trigger_timeout = 0;
|
||||
$xCAT::TableUtils::master = '192.0.2.10';
|
||||
}
|
||||
|
||||
my $commands = xCAT_plugin::credentials::handled_commands();
|
||||
is( $commands->{getcredentials}, 'credentials',
|
||||
'credentials plugin still handles client requests' );
|
||||
is( $commands->{signx509cert}, 'credentials',
|
||||
'credentials plugin handles delegated signing' );
|
||||
|
||||
$xCAT::Table::servicenode = 'service-a, service-b.example.test';
|
||||
%xCAT::NetworkUtils::addresses = (
|
||||
'192.0.2.21' => ['192.0.2.21'],
|
||||
'192.0.2.22' => ['192.0.2.22'],
|
||||
'192.0.2.23' => ['192.0.2.23'],
|
||||
'::ffff:192.0.2.22' => ['::ffff:192.0.2.22'],
|
||||
'service-a' => ['192.0.2.21'],
|
||||
'service-b' => ['192.0.2.22'],
|
||||
'service-b-alias' => ['192.0.2.22'],
|
||||
'service-b.example.test' => ['192.0.2.22'],
|
||||
'service-c' => ['192.0.2.23'],
|
||||
'service-c.example.test' => ['192.0.2.23'],
|
||||
);
|
||||
|
||||
ok(
|
||||
xCAT_plugin::credentials::_delegated_signer_allowed(
|
||||
{
|
||||
_xcat_authname => ['root'],
|
||||
_xcat_clienthost => ['service-a'],
|
||||
_xcat_clientip => ['192.0.2.21'],
|
||||
},
|
||||
'compute-01'
|
||||
),
|
||||
'assigned authenticated service node may request a certificate'
|
||||
);
|
||||
ok(
|
||||
xCAT_plugin::credentials::_delegated_signer_allowed(
|
||||
{
|
||||
_xcat_authname => ['root'],
|
||||
_xcat_clientfqdn => ['service-b.example.test'],
|
||||
_xcat_clientip => ['192.0.2.22'],
|
||||
},
|
||||
'compute-01'
|
||||
),
|
||||
'assigned service node may match its FQDN'
|
||||
);
|
||||
ok(
|
||||
xCAT_plugin::credentials::_delegated_signer_allowed(
|
||||
{
|
||||
_xcat_authname => ['root'],
|
||||
_xcat_clientip => ['192.0.2.22'],
|
||||
},
|
||||
'compute-01'
|
||||
),
|
||||
'numeric peer address works without reverse DNS'
|
||||
);
|
||||
ok(
|
||||
xCAT_plugin::credentials::_delegated_signer_allowed(
|
||||
{
|
||||
_xcat_authname => ['root'],
|
||||
_xcat_clientip => ['::ffff:192.0.2.22'],
|
||||
},
|
||||
'compute-01'
|
||||
),
|
||||
'IPv4-mapped peer address matches the assigned IPv4 service node'
|
||||
);
|
||||
|
||||
$xCAT::Table::servicenode = '192.0.2.22';
|
||||
ok(
|
||||
xCAT_plugin::credentials::_delegated_signer_allowed(
|
||||
{
|
||||
_xcat_authname => ['root'],
|
||||
_xcat_clienthost => ['service-b'],
|
||||
_xcat_clientip => ['192.0.2.22'],
|
||||
},
|
||||
'compute-01'
|
||||
),
|
||||
'service node assignment may use an IP address'
|
||||
);
|
||||
|
||||
$xCAT::Table::servicenode = 'service-b-alias';
|
||||
ok(
|
||||
xCAT_plugin::credentials::_delegated_signer_allowed(
|
||||
{
|
||||
_xcat_authname => ['root'],
|
||||
_xcat_clientip => ['192.0.2.22'],
|
||||
},
|
||||
'compute-01'
|
||||
),
|
||||
'service node assignment may use a resolvable alias'
|
||||
);
|
||||
|
||||
$xCAT::Table::servicenode = 'service-b.example.test';
|
||||
ok(
|
||||
!xCAT_plugin::credentials::_delegated_signer_allowed(
|
||||
{
|
||||
_xcat_authname => ['root'],
|
||||
_xcat_clienthost => ['service-b'],
|
||||
_xcat_clientip => ['192.0.2.23'],
|
||||
},
|
||||
'compute-01'
|
||||
),
|
||||
'reverse DNS name cannot override a different peer address'
|
||||
);
|
||||
|
||||
ok(
|
||||
!xCAT_plugin::credentials::_delegated_signer_allowed(
|
||||
{
|
||||
_xcat_authname => ['root'],
|
||||
_xcat_clienthost => ['service-c'],
|
||||
_xcat_clientfqdn => ['service-c.example.test'],
|
||||
_xcat_clientip => ['192.0.2.23'],
|
||||
},
|
||||
'compute-01'
|
||||
),
|
||||
'resolved but unassigned service node is rejected'
|
||||
);
|
||||
ok(
|
||||
!xCAT_plugin::credentials::_delegated_signer_allowed(
|
||||
{
|
||||
_xcat_clienthost => ['service-b'],
|
||||
_xcat_clientip => ['192.0.2.22'],
|
||||
},
|
||||
'compute-01'
|
||||
),
|
||||
'unauthenticated request is rejected'
|
||||
);
|
||||
ok(
|
||||
!xCAT_plugin::credentials::_delegated_signer_allowed(
|
||||
{ _xcat_authname => ['root'] }, 'compute-01'
|
||||
),
|
||||
'authenticated request without a peer identity is rejected'
|
||||
);
|
||||
|
||||
reset_client();
|
||||
@xCAT::Client::responses = (
|
||||
{
|
||||
data => [
|
||||
{
|
||||
desc => ['x509cert'],
|
||||
content => 'scalar certificate',
|
||||
}
|
||||
]
|
||||
},
|
||||
{ serverdone => [undef] },
|
||||
);
|
||||
my ($certificate, $error) =
|
||||
xCAT_plugin::credentials::_request_x509_from_master(
|
||||
'compute-01', 'certificate request'
|
||||
);
|
||||
is( $certificate, 'scalar certificate',
|
||||
'delegated signer accepts the serialized xCAT response shape' );
|
||||
is( $error, undef, 'successful delegated request has no error' );
|
||||
is( $xCAT::Client::host, '192.0.2.10:3001',
|
||||
'delegated request targets site.master' );
|
||||
is_deeply(
|
||||
$xCAT::Client::request,
|
||||
{
|
||||
command => ['signx509cert'],
|
||||
arg => ['compute-01'],
|
||||
csr => ['certificate request'],
|
||||
},
|
||||
'delegated request carries the node and CSR'
|
||||
);
|
||||
|
||||
reset_client();
|
||||
$xCAT::TableUtils::master = '2001:db8::10';
|
||||
@xCAT::Client::responses = (
|
||||
{
|
||||
data => [
|
||||
{
|
||||
desc => 'x509cert',
|
||||
content => ['array certificate'],
|
||||
}
|
||||
]
|
||||
},
|
||||
);
|
||||
($certificate, $error) = xCAT_plugin::credentials::_request_x509_from_master(
|
||||
'compute-01', 'certificate request'
|
||||
);
|
||||
is( $certificate, 'array certificate',
|
||||
'delegated signer tolerates native array values' );
|
||||
is( $xCAT::Client::host, '[2001:db8::10]:3001',
|
||||
'IPv6 management address is bracketed' );
|
||||
|
||||
reset_client();
|
||||
@xCAT::Client::responses = (
|
||||
{ error => ['Delegated certificate request denied'], errorcode => [1] }
|
||||
);
|
||||
($certificate, $error) = xCAT_plugin::credentials::_request_x509_from_master(
|
||||
'compute-01', 'certificate request'
|
||||
);
|
||||
is( $certificate, undef, 'management node rejection returns no certificate' );
|
||||
like( $error, qr/Delegated certificate request denied\z/,
|
||||
'management node error is returned to the service node' );
|
||||
like( $xCAT::MsgUtils::messages[-1]->[3],
|
||||
qr/Delegated certificate request denied\z/,
|
||||
'management node error is logged on the service node' );
|
||||
|
||||
reset_client();
|
||||
$xCAT::Client::exception = "TLS handshake failed\n";
|
||||
($certificate, $error) = xCAT_plugin::credentials::_request_x509_from_master(
|
||||
'compute-01', 'certificate request'
|
||||
);
|
||||
like( $error, qr/TLS handshake failed\z/,
|
||||
'client exception is preserved in the delegated error' );
|
||||
like( $xCAT::MsgUtils::messages[-1]->[3], qr/TLS handshake failed\z/,
|
||||
'client exception is logged on the service node' );
|
||||
|
||||
reset_client();
|
||||
$xCAT::Client::trigger_timeout = 1;
|
||||
($certificate, $error) = xCAT_plugin::credentials::_request_x509_from_master(
|
||||
'compute-01', 'certificate request'
|
||||
);
|
||||
like( $error, qr/request timed out after 30 seconds\z/,
|
||||
'stalled management node request is bounded by a timeout' );
|
||||
|
||||
reset_client();
|
||||
($certificate, $error) = xCAT_plugin::credentials::_request_x509_from_master(
|
||||
'compute-01', 'certificate request'
|
||||
);
|
||||
like( $error, qr/management node returned no certificate\z/,
|
||||
'empty management node response has a specific error' );
|
||||
|
||||
reset_client();
|
||||
$xCAT::TableUtils::master = undef;
|
||||
($certificate, $error) = xCAT_plugin::credentials::_request_x509_from_master(
|
||||
'compute-01', 'certificate request'
|
||||
);
|
||||
is( $error, 'The management node is not configured',
|
||||
'missing site.master has a specific error' );
|
||||
|
||||
$xCAT::Table::servicenode = 'service-a';
|
||||
@xCAT::MsgUtils::messages = ();
|
||||
my @callback;
|
||||
{
|
||||
no warnings 'redefine';
|
||||
local *xCAT_plugin::credentials::_sign_x509_certificate = sub {
|
||||
return 'signed certificate';
|
||||
};
|
||||
xCAT_plugin::credentials::process_request(
|
||||
{
|
||||
command => ['signx509cert'],
|
||||
arg => ['compute-01'],
|
||||
csr => ['certificate request'],
|
||||
_xcat_authname => ['root'],
|
||||
_xcat_clienthost => ['service-a'],
|
||||
_xcat_clientip => ['192.0.2.21'],
|
||||
},
|
||||
sub { push @callback, shift; }
|
||||
);
|
||||
}
|
||||
is_deeply(
|
||||
\@callback,
|
||||
[
|
||||
{
|
||||
data => [
|
||||
{
|
||||
content => ['signed certificate'],
|
||||
desc => ['x509cert'],
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
'authorized delegated request returns only the signed certificate'
|
||||
);
|
||||
is_deeply(
|
||||
$xCAT::MsgUtils::messages[-1],
|
||||
[
|
||||
'xCAT::MsgUtils',
|
||||
0,
|
||||
'I',
|
||||
'credentials: signed certificate for compute-01 requested by 192.0.2.21',
|
||||
],
|
||||
'successful delegated signing records the node and service node address'
|
||||
);
|
||||
|
||||
@callback = ();
|
||||
xCAT_plugin::credentials::process_request(
|
||||
{
|
||||
command => ['signx509cert'],
|
||||
arg => ['compute-01'],
|
||||
csr => ['certificate request'],
|
||||
_xcat_authname => ['root'],
|
||||
_xcat_clienthost => ['service-c'],
|
||||
_xcat_clientip => ['192.0.2.23'],
|
||||
},
|
||||
sub { push @callback, shift; }
|
||||
);
|
||||
is( $callback[0]->{errorcode}->[0], 1,
|
||||
'unauthorized delegated request returns an error' );
|
||||
|
||||
%xCAT::NodeRange::ranges = ( 'compute-01' => ['compute-01'] );
|
||||
$xCAT::Utils::service_node = 1;
|
||||
@callback = ();
|
||||
{
|
||||
no warnings 'redefine';
|
||||
local *xCAT_plugin::credentials::ok_with_node = sub { return 1; };
|
||||
local *xCAT_plugin::credentials::_request_x509_from_master = sub {
|
||||
return ('delegated certificate', undef);
|
||||
};
|
||||
xCAT_plugin::credentials::process_request(
|
||||
{
|
||||
command => ['getcredentials'],
|
||||
arg => ['x509cert'],
|
||||
csr => ['certificate request'],
|
||||
callback_port => [123],
|
||||
_xcat_clienthost => ['compute-01'],
|
||||
},
|
||||
sub { push @callback, shift; }
|
||||
);
|
||||
}
|
||||
is( $callback[0]->{data}->[0]->{content}->[0], 'delegated certificate',
|
||||
'service-node getcredentials path returns the delegated certificate' );
|
||||
|
||||
@callback = ();
|
||||
{
|
||||
no warnings 'redefine';
|
||||
local *xCAT_plugin::credentials::ok_with_node = sub { return 1; };
|
||||
local *xCAT_plugin::credentials::_request_x509_from_master = sub {
|
||||
return (undef, 'management node rejected the request');
|
||||
};
|
||||
xCAT_plugin::credentials::process_request(
|
||||
{
|
||||
command => ['getcredentials'],
|
||||
arg => ['x509cert'],
|
||||
csr => ['certificate request'],
|
||||
callback_port => [123],
|
||||
_xcat_clienthost => ['compute-01'],
|
||||
},
|
||||
sub { push @callback, shift; }
|
||||
);
|
||||
}
|
||||
is( $callback[0]->{error}->[0], 'management node rejected the request',
|
||||
'service-node getcredentials path returns the delegated error' );
|
||||
|
||||
SKIP: {
|
||||
my $openssl = 0;
|
||||
if (open(my $version, '-|', 'openssl', 'version')) {
|
||||
<$version>;
|
||||
$openssl = close($version);
|
||||
}
|
||||
skip 'openssl is not available', 3 unless $openssl;
|
||||
|
||||
my $directory = tempdir(CLEANUP => 1);
|
||||
my $key = File::Spec->catfile($directory, 'test.key');
|
||||
open(my $saved_stderr, '>&', \*STDERR) or die "cannot save stderr";
|
||||
open(STDERR, '>', File::Spec->devnull()) or die "cannot redirect stderr";
|
||||
my $key_status = system('openssl', 'genrsa', '-out', $key, '2048');
|
||||
open(STDERR, '>&', $saved_stderr) or die "cannot restore stderr";
|
||||
close($saved_stderr);
|
||||
$key_status == 0 or die "cannot create test key";
|
||||
my @subjects = (
|
||||
[ valid => '/CN=compute-01' ],
|
||||
[ multiple => '/CN=root/CN=compute-01' ],
|
||||
[ extra => '/CN=compute-01/OU=cluster' ],
|
||||
);
|
||||
my %requests;
|
||||
foreach my $subject (@subjects) {
|
||||
my ($name, $distinguished_name) = @{$subject};
|
||||
my $request = File::Spec->catfile($directory, "$name.csr");
|
||||
system('openssl', 'req', '-new', '-key', $key,
|
||||
'-subj', $distinguished_name, '-out', $request) == 0
|
||||
or die "cannot create test CSR";
|
||||
$requests{$name} = $request;
|
||||
}
|
||||
|
||||
ok( xCAT_plugin::credentials::_csr_subject_matches_node(
|
||||
$requests{valid}, 'compute-01'
|
||||
),
|
||||
'single exact CN is accepted' );
|
||||
ok( !xCAT_plugin::credentials::_csr_subject_matches_node(
|
||||
$requests{multiple}, 'compute-01'
|
||||
),
|
||||
'multiple CN values are rejected' );
|
||||
ok( !xCAT_plugin::credentials::_csr_subject_matches_node(
|
||||
$requests{extra}, 'compute-01'
|
||||
),
|
||||
'additional subject attributes are rejected' );
|
||||
}
|
||||
|
||||
done_testing();
|
||||
Reference in New Issue
Block a user