2
0
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:
Vinícius Ferrão
2026-08-20 19:33:23 -03:00
committed by GitHub
5 changed files with 739 additions and 56 deletions
@@ -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
+227 -53
View File
@@ -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;
+9 -3
View File
@@ -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])) {
+493
View File
@@ -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();