diff --git a/bin/helper/osh-groupDelEgressKey b/bin/helper/osh-groupDelEgressKey new file mode 100755 index 0000000..4ec3d98 --- /dev/null +++ b/bin/helper/osh-groupDelEgressKey @@ -0,0 +1,133 @@ +#! /usr/bin/perl -T +# vim: set filetype=perl ts=4 sw=4 sts=4 et: +# KEYSUDOERS # as an owner, we can delete an egress key of the group +# KEYSUDOERS SUPEROWNERS, %%GROUP%-owner ALL=(keykeeper) NOPASSWD: /usr/bin/env perl -T %BASEPATH%/bin/helper/osh-groupDelEgressKey --group %GROUP% * +# FILEMODE 0750 +# FILEOWN 0 keykeeper + +#>HEADER +use common::sense; +use Getopt::Long; + +use File::Basename; +use lib dirname(__FILE__) . '/../../lib/perl'; +use OVH::Bastion; +use OVH::Result; +local $| = 1; + +# +# Globals +# +$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/pkg/bin'; +my ($self) = $ENV{'SUDO_USER'} =~ m{^([a-zA-Z0-9._-]+)$}; +if (not defined $self) { + if ($< == 0) { + $self = 'root'; + } + else { + HEXIT('ERR_SUDO_NEEDED', msg => 'This command must be run under sudo'); + } +} + +# Fetch command options +Getopt::Long::Configure("no_auto_abbrev"); +my $fnret; +my ($result, @optwarns); +my ($group, $id); +eval { + local $SIG{__WARN__} = sub { push @optwarns, shift }; + $result = GetOptions( + "group=s" => sub { $group //= $_[1] }, # ignore subsequent --group on cmdline (anti-sudoers-override) + "id=s" => sub { $id //= $_[1] }, + ); +}; +if ($@) { die $@ } + +if (!$result) { + local $" = ", "; + HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns"); +} + +if (!$group || !$id) { + HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'group' or 'id'"); +} + +#
PARAMS:GROUP +$fnret = OVH::Bastion::is_valid_group_and_existing(group => $group, groupType => "key"); +$fnret or HEXIT($fnret); + +# get returned untainted value +$group = $fnret->value->{'group'}; +my $shortGroup = $fnret->value->{'shortGroup'}; + +#RIGHTSCHECK +if ($self eq 'root') { + osh_debug "Real root, skipping checks of permissions"; +} +$fnret = OVH::Bastion::is_group_owner(account => $self, group => $shortGroup, superowner => 1, sudo => 1); +if (!$fnret) { + HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this, dear $self"); +} + +#CODE +$fnret = OVH::Bastion::get_group_keys(group => $group); +$fnret or HEXIT($fnret); + +my @matchingKeys = grep { $fnret->value->{'keys'}{$_}{'id'} eq $id } @{$fnret->value->{'sortedKeys'} || []}; + +if (!@matchingKeys) { + HEXIT('ERR_INVALID_PARAMETER', msg => "Couldn't find any key with the ID you specified ($id) in group $shortGroup"); +} + +my $keyToDelete = $matchingKeys[0]; +my $key = $fnret->value->{'keys'}{$keyToDelete}; + +osh_info("We're about to delete the following key:\n"); +OVH::Bastion::print_public_key(key => $key); + +# get the path to the privkey +my $fileToDelete = $fnret->value->{'keys'}{$keyToDelete}{'fullpath'}; +if (!-f $fileToDelete) { + warn_syslog("The file '$fileToDelete' doesn't exist while trying to delete this egress key from group $shortGroup"); + HEXIT('ERR_INVALID_PARAMETER', msg => "Couldn't find the key file"); +} + +my @errors; +foreach my $file ($fileToDelete, "$fileToDelete.pub") { + push @errors, "Couldn't delete '$file' in groupDelEgressKey by $self ($!)" if !unlink($file); +} + +if (@errors) { + warn_syslog($_) for @errors; + if (@errors == 2) { + HEXIT('ERR_INTERNAL', msg => "Couldn't delete the requested key, more information available in the system log"); + } + HEXIT('ERR_INTERNAL', msg => "Couldn't delete one of the files constituting the key, more information available in the system log"); +} + +OVH::Bastion::syslogFormatted( + severity => 'info', + type => 'group', + fields => [ + [action => 'delete_egress_key'], + [group => $shortGroup], + [self => $self], + [key_id => $key->{'id'}], + [key_algo => $key->{'typecode'}], + [key_algo_family => $key->{'family'}], + [key_size => $key->{'size'}], + [key_fingerprint => $key->{'fingerprint'}], + [key_comment => $key->{'comment'}], + [key_mtime => $key->{'mtime'}], + [key_path => $key->{'fullpath'}], + [key_base64 => $key->{'base64'}], + ] +); + +HEXIT('OK', value => $key, msg => "Key $id has successfully been deleted from the $shortGroup group"); diff --git a/bin/helper/osh-groupGenerateEgressKey b/bin/helper/osh-groupGenerateEgressKey new file mode 100755 index 0000000..2d9e2ae --- /dev/null +++ b/bin/helper/osh-groupGenerateEgressKey @@ -0,0 +1,138 @@ +#! /usr/bin/perl -T +# vim: set filetype=perl ts=4 sw=4 sts=4 et: +# KEYSUDOERS # as an owner, we can generate an egress key for the group +# KEYSUDOERS SUPEROWNERS, %%GROUP%-owner ALL=(root) NOPASSWD: /usr/bin/env perl -T %BASEPATH%/bin/helper/osh-groupGenerateEgressKey --group %GROUP% * +# FILEMODE 0755 +# FILEOWN 0 0 + +#>HEADER +use common::sense; +use Getopt::Long; + +use File::Basename; +use lib dirname(__FILE__) . '/../../lib/perl'; +use OVH::Result; +use OVH::Bastion; +use OVH::Bastion::Plugin::generateEgressKey; +local $| = 1; + +# +# Globals +# +$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/pkg/bin'; +my ($self) = $ENV{'SUDO_USER'} =~ m{^([a-zA-Z0-9._-]+)$}; +if (not defined $self) { + if ($< == 0) { + $self = 'root'; + } + else { + HEXIT('ERR_SUDO_NEEDED', msg => 'This command must be run under sudo'); + } +} + +# Fetch command options +my ($result, @optwarns); +my ($group, $algo, $size, $encrypted); +eval { + local $SIG{__WARN__} = sub { push @optwarns, shift }; + $result = GetOptions( + "group=s" => sub { $group //= $_[1] }, # ignore subsequent --group on cmdline (anti-sudoers-override) + "algo=s" => sub { $algo //= $_[1] }, + "size=i" => sub { $size //= $_[1] }, + "encrypted" => sub { $encrypted //= $_[1] }, + ); +}; +if ($@) { die $@ } + +if (!$result) { + local $" = ", "; + HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns"); +} + +if (!$size || !$algo || !$group) { + HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'size', 'algo' or 'group'"); +} + +#
$group, groupType => "key"); +$fnret or HEXIT($fnret); + +$fnret = OVH::Bastion::Plugin::generateEgressKey::preconditions( + context => 'group', + self => $self, + group => $group, + algo => $algo, + size => $size, + sudo => 1, +); +$fnret or HEXIT($fnret); + +# get returned untainted values +my ($shortGroup, $keyhome); +($group, $algo, $size, $shortGroup, $keyhome) = @{$fnret->value}{qw{ group algo size shortGroup keyhome}}; + +my $passphrase = ''; +if ($encrypted) { + + # read the passphrase from stdin + $passphrase = ; + + # we need to untaint it, as it's going to be passed as an arg to the array version of system(), + # it can contain anything, really, there is no shell escape possible (see generate_ssh_key in ssh.inc) + ($passphrase) = $passphrase =~ /^(.+)$/; +} + +my $keykeeper_uid = (getpwnam('keykeeper'))[2]; +my $group_gid = (getgrnam($group))[2]; + +if (!$keykeeper_uid || !$group_gid) { + warn_syslog("Couldn't get the uid of keykeeper ($keykeeper_uid) or gid of $group ($group_gid) while $self is attempting to generate a new key with algo $algo and size $size"); + HEXIT('ERR_INTERNAL', msg => "Couldn't fetch the required account or group IDs"); +} + +osh_info "Generating a new key pair, this might take a while..."; +$fnret = OVH::Bastion::generate_ssh_key( + folder => $keyhome, + prefix => $shortGroup, + algo => $algo, + size => $size, + passphrase => $passphrase, + uid => $keykeeper_uid, + gid => $group_gid, + group_readable => 1, +); +$fnret or HEXIT($fnret); + +my $filepath = $fnret->value->{'file'}; +my $mtime = (stat($filepath))[9]; + +osh_info "The new key pair has been generated:\n"; +$fnret = OVH::Bastion::get_ssh_pub_key_info(file => $filepath . ".pub", way => "egress"); +$fnret or HEXIT($fnret); + +my $key = $fnret->value; + +OVH::Bastion::syslogFormatted( + severity => 'info', + type => 'group', + fields => [ + [action => 'generate_egress_key'], + [group => $shortGroup], + [self => $self], + [key_id => $key->{'id'}], + [key_algo => $key->{'typecode'}], + [key_algo_family => $key->{'family'}], + [key_size => $key->{'size'}], + [key_fingerprint => $key->{'fingerprint'}], + [key_comment => $key->{'comment'}], + [key_mtime => $mtime], + [key_path => $filepath], + [key_base64 => $key->{'base64'}], + [key_encrypted => $encrypted ? "yes" : "no"], + ] +); + +HEXIT('OK', value => $key); diff --git a/bin/plugin/group-owner/groupDelEgressKey b/bin/plugin/group-owner/groupDelEgressKey new file mode 100755 index 0000000..acbf399 --- /dev/null +++ b/bin/plugin/group-owner/groupDelEgressKey @@ -0,0 +1,61 @@ +#! /usr/bin/env perl +# vim: set filetype=perl ts=4 sw=4 sts=4 et: +use common::sense; + +use File::Basename; +use lib dirname(__FILE__) . '/../../../lib/perl'; +use OVH::Result; +use OVH::Bastion; +use OVH::Bastion::Plugin qw( :DEFAULT help ); + +my $remainingOptions = OVH::Bastion::Plugin::begin( + argv => \@ARGV, + header => "Remove a bastion group egress key", + options => { + "group=s" => \my $group, + "id=s" => \my $id, + }, + helptext => <<"EOF", +Remove a bastion group egress key + +Usage: --osh SCRIPT_NAME <--group GROUP> <--id ID> + + --group GROUP Name of the group to delete the egress key from + --id ID Specify the key ID to delete, you can get it with groupInfo +EOF +); + +# +# code +# +my $fnret; + +if (!$id || !$group) { + help(); + osh_exit 'ERR_MISSING_PARAMETER', "Missing the --group or --id parameter"; +} + +$fnret = OVH::Bastion::is_valid_group_and_existing(group => $group, groupType => "key"); +$fnret or osh_exit($fnret); + +# get returned untainted value +$group = $fnret->value->{'group'}; +my $shortGroup = $fnret->value->{'shortGroup'}; + +if (!OVH::Bastion::is_group_owner(account => $self, group => $shortGroup, superowner => 1)) { + osh_exit 'ERR_NOT_GROUP_OWNER', "You must be an owner to delete an egress group key"; +} + +$fnret = OVH::Bastion::get_group_keys(group => $group); + +my @matchingKeys = grep { $fnret->value->{'keys'}{$_}{'id'} eq $id } @{$fnret->value->{'sortedKeys'} || []}; + +if (!@matchingKeys) { + osh_exit 'ERR_INVALID_PARAMETER', "Couldn't find any key with the ID you specified ($id) in group $shortGroup"; +} + +my @command = qw{ sudo -n -u keykeeper -- /usr/bin/env perl -T }; +push @command, $OVH::Bastion::BASEPATH . '/bin/helper/osh-groupDelEgressKey'; +push @command, "--group", $group, "--id", $id; + +osh_exit(OVH::Bastion::helper(cmd => \@command)); diff --git a/bin/plugin/group-owner/groupDelEgressKey.json b/bin/plugin/group-owner/groupDelEgressKey.json new file mode 100644 index 0000000..b97fa61 --- /dev/null +++ b/bin/plugin/group-owner/groupDelEgressKey.json @@ -0,0 +1,10 @@ +{ + "interactive": [ + "groupDelEgressKey" , {"ac" : ["--group"]}, + "groupDelEgressKey --group" , {"ac" : [""]}, + "groupDelEgressKey --group \\S+" , {"ac" : ["--id"]}, + "groupDelEgressKey --group \\S+ --id" , {"pr" : [""]}, + "groupDelEgressKey --group \\S+ --id \\d+", {"pr" : [""]} + ], + "master_only": true +} diff --git a/bin/plugin/group-owner/groupGenerateEgressKey b/bin/plugin/group-owner/groupGenerateEgressKey new file mode 100755 index 0000000..63231a4 --- /dev/null +++ b/bin/plugin/group-owner/groupGenerateEgressKey @@ -0,0 +1,72 @@ +#! /usr/bin/env perl +# vim: set filetype=perl ts=4 sw=4 sts=4 et: +use common::sense; +use Term::ReadKey; + +use File::Basename; +use lib dirname(__FILE__) . '/../../../lib/perl'; +use OVH::Result; +use OVH::Bastion; +use OVH::Bastion::Plugin qw( :DEFAULT ); +use OVH::Bastion::Plugin::generateEgressKey; + +my $remainingOptions = OVH::Bastion::Plugin::begin( + argv => \@ARGV, + header => "generating a new key pair for a group", + options => { + "group=s" => \my $group, + "algo=s" => \my $algo, + "size=i" => \my $size, + "encrypted" => \my $encrypted, + }, + help => \&OVH::Bastion::Plugin::generateEgressKey::help, +); + +# +# code +# +my $fnret; + +$fnret = OVH::Bastion::Plugin::generateEgressKey::preconditions( + context => 'group', + self => $self, + group => $group, + algo => $algo, + size => $size +); +if ($fnret->err eq 'ERR_MISSING_PARAMETER') { + OVH::Bastion::Plugin::generateEgressKey::help(); + osh_exit(R('ERR_MISSING_PARAMETER', msg => "Missing the 'algo', 'size' or 'group' parameter'")); +} +$fnret or osh_exit $fnret; + +my ($shortGroup, $keyhome); +($group, $algo, $size, $shortGroup, $keyhome) = @{$fnret->value}{qw{ group algo size shortGroup keyhome }}; + +my $passphrase = ''; # empty by default +if ($encrypted) { + $fnret = OVH::Bastion::Plugin::generateEgressKey::ask_passphrase(); + $fnret or osh_exit $fnret; + $passphrase = $fnret->value; +} + +my @command = qw{ sudo -n -u root -- /usr/bin/env perl -T }; +push @command, $OVH::Bastion::BASEPATH . '/bin/helper/osh-groupGenerateEgressKey'; +push @command, '--group', $group; +push @command, '--algo', $algo; +push @command, '--size', $size; +push @command, '--encrypted' if $encrypted; + +$fnret = OVH::Bastion::helper(cmd => \@command, stdin_str => $passphrase); +$fnret or osh_exit $fnret; + +my $key = $fnret->value; + +$fnret = OVH::Bastion::get_bastion_ips(); +$fnret or osh_exit $fnret; + +$key->{'prefix'} = 'from="' . join(',', @{$fnret->value}) . '"'; + +OVH::Bastion::print_public_key(key => $key); + +osh_ok($key); diff --git a/bin/plugin/group-owner/groupGenerateEgressKey.json b/bin/plugin/group-owner/groupGenerateEgressKey.json new file mode 100644 index 0000000..4715294 --- /dev/null +++ b/bin/plugin/group-owner/groupGenerateEgressKey.json @@ -0,0 +1,14 @@ +{ + "interactive": [ + "groupGenerateEgressKey" , {"ac" : ["--group"]}, + "groupGenerateEgressKey --group" , {"ac" : [""]}, + "groupGenerateEgressKey --group \\S+" , {"ac" : ["--algo"]}, + "groupGenerateEgressKey --group \\S+ --algo" , {"ac" : ["rsa", "ecdsa", "ed25519"]}, + "groupGenerateEgressKey --group \\S+ --algo \\S+" , {"ac" : ["--size"]}, + "groupGenerateEgressKey --group \\S+ --algo \\S+ --size" , {"pr" : [""]}, + "groupGenerateEgressKey --group \\S+ --algo \\S+ --size \\d+" , {"ac" : ["", "--encrypted"]}, + "groupGenerateEgressKey --group \\S+ --algo \\S+ --size \\d+ --encrypted", {"pr" : [""]} + ], + "master_only": true, + "terminal_mode" : "raw" +} diff --git a/bin/plugin/group-owner/groupModify b/bin/plugin/group-owner/groupModify index f043bcb..6163889 100755 --- a/bin/plugin/group-owner/groupModify +++ b/bin/plugin/group-owner/groupModify @@ -51,7 +51,7 @@ my $shortGroup = $fnret->value->{'shortGroup'}; $fnret = OVH::Bastion::is_group_owner(account => $self, group => $shortGroup, superowner => 1); if (!$fnret) { - osh_exit 'ERR_NOT_GROUP_OWNER', "You must be an owner to delete an egress group key"; + osh_exit 'ERR_NOT_GROUP_OWNER', "You must be an owner to modify this group"; } if (defined $mfaRequired && !grep { $mfaRequired eq $_ } qw{ password totp any none }) { diff --git a/bin/plugin/open/selfGenerateEgressKey b/bin/plugin/open/selfGenerateEgressKey index 107aece..1dd0a5d 100755 --- a/bin/plugin/open/selfGenerateEgressKey +++ b/bin/plugin/open/selfGenerateEgressKey @@ -8,107 +8,43 @@ use lib dirname(__FILE__) . '/../../../lib/perl'; use OVH::Result; use OVH::Bastion; use OVH::Bastion::Plugin qw( :DEFAULT ); +use OVH::Bastion::Plugin::generateEgressKey; -my ($algo, $size, $encrypted); my $remainingOptions = OVH::Bastion::Plugin::begin( argv => \@ARGV, header => "generating a new key pair for your account", options => { - "algo=s" => \$algo, - "size=i" => \$size, - "encrypted" => \$encrypted, + "algo=s" => \my $algo, + "size=i" => \my $size, + "encrypted" => \my $encrypted, }, - help => \&help, + help => \&OVH::Bastion::Plugin::generateEgressKey::help, ); -sub help { - require Term::ANSIColor; - my $fnret = OVH::Bastion::get_supported_ssh_algorithms_list(way => 'egress'); - my @algoList = @{$fnret->value}; - my $algos = Term::ANSIColor::colored(uc join(' ', @algoList), 'green'); - my $helpAlgoSize = '--algo rsa --size 4096'; - if (grep { $_ eq 'ecdsa' } @algoList) { - $helpAlgoSize = '--algo ecdsa --size 521'; - } - if (grep { $_ eq 'ed25519' } @algoList) { - $helpAlgoSize = '--algo ed25519'; - } - osh_info <<"EOF"; -Create a new egress public + private key pair. The private key will stay on your account on this bastion. - -Usage: --osh $scriptName $helpAlgoSize [--encrypted] - - --algo ALGO Specifies the algo of the key, either rsa, ecdsa or ed25519. - - --size SIZE Specifies the size of the key to be generated. - For RSA, choose between 2048 and 8192 (4096 is good). - For ECDSA, choose either 256, 384 or 521. - For ED25519, size is always 256. - - --encrypted if specified, a passphrase will be prompted for the new key - -With the policy and SSH version on this bastion, -the following algorithms are supported: $algos - -algo size strength speed compatibility -------- ---- ---------- -------- ----------------------- -RSA 4096 good slow works everywhere -ECDSA 521 strong fast debian7+ (OpenSSH 5.7+) -ED25519 256 verystrong veryfast debian8+ (OpenSSH 6.5+) -EOF - return 0; -} - # # code # my $fnret; -if (!$algo) { - help(); - osh_exit 'ERR_MISSING_PARAMETER', "Parameter 'algo' is missing"; -} - -# check if algo is supported by system -$fnret = OVH::Bastion::get_supported_ssh_algorithms_list(way => 'egress'); -my @algoList = @{$fnret->value}; -my $ok = 0; -foreach (@algoList) { - $algo =~ /^\Q$_\E/ and $ok = 1; -} -if (not $ok) { - osh_debug($algo); - osh_debug(join(' ', @algoList)); - osh_exit 'ERR_INVALID_ALGORITHM', "Only the following list of algorithms is allowed: " . join(' ', @algoList); +$fnret = OVH::Bastion::Plugin::generateEgressKey::preconditions( + context => 'account', + account => $self, + algo => $algo, + size => $size +); +if ($fnret->err eq 'ERR_MISSING_PARAMETER') { + OVH::Bastion::Plugin::generateEgressKey::help(); + osh_exit(R('ERR_MISSING_PARAMETER', msg => "Missing the 'algo' or 'size' parameter'")); } - -$size = 256 if (not $size and $algo eq 'ed25519'); -$fnret = OVH::Bastion::is_allowed_algo_and_size(algo => $algo, size => $size, way => 'egress'); $fnret or osh_exit $fnret; -$fnret = OVH::Bastion::config('bastionName'); -$fnret or osh_exit $fnret; -my $bastionName = $fnret->value; +($algo, $size) = @{$fnret->value}{qw{ algo size }}; my $passphrase = ''; # empty by default if ($encrypted) { - print "Please enter a passphrase for your new personal bastion key (not echoed): "; - ReadMode('noecho'); - chomp(my $pass1 = ); - if (length($pass1) < 5) { - - # ssh-keygen will refuse - print "\n"; - osh_exit 'ERR_PASSPHRASE_TOO_SHORT', "Passphrase needs to be at least 5 chars"; - } - print "\nPlease enter it again: "; - chomp(my $pass2 = ); - print "\n"; - ReadMode('restore'); - if ($pass1 ne $pass2) { - osh_exit 'ERR_PASSPHRASE_MISMATCH', "Passphrases don't match, please try again"; - } - $passphrase = $pass1; + $fnret = OVH::Bastion::Plugin::generateEgressKey::ask_passphrase(); + $fnret or osh_exit $fnret; + $passphrase = $fnret->value; } osh_info "Generating your key, this might take a while..."; @@ -125,6 +61,7 @@ $fnret or osh_exit $fnret; osh_info "You new key pair has been generated:\n"; $fnret = OVH::Bastion::get_ssh_pub_key_info(file => $fnret->value->{'file'} . ".pub", way => "egress"); $fnret or osh_exit $fnret; + my $key = $fnret->value; $fnret = OVH::Bastion::get_bastion_ips(); diff --git a/etc/sudoers.group.template.d/500-base.sudoers b/etc/sudoers.group.template.d/500-base.sudoers index 6848cdd..fa961ac 100644 --- a/etc/sudoers.group.template.d/500-base.sudoers +++ b/etc/sudoers.group.template.d/500-base.sudoers @@ -13,6 +13,12 @@ SUPEROWNERS, %%GROUP%-owner ALL=(root) NOPASSWD: /usr/bin/env perl - # as an owner, we can generate an egress password for the group SUPEROWNERS, %%GROUP%-owner ALL=(%GROUP%) NOPASSWD: /usr/bin/env perl -T %BASEPATH%/bin/helper/osh-groupGeneratePassword --group %GROUP% * +# as an owner, we can generate an egress key for the group +SUPEROWNERS, %%GROUP%-owner ALL=(root) NOPASSWD: /usr/bin/env perl -T %BASEPATH%/bin/helper/osh-groupGenerateEgressKey --group %GROUP% * + +# as an owner, we can delete an egress key of the group +SUPEROWNERS, %%GROUP%-owner ALL=(keykeeper) NOPASSWD: /usr/bin/env perl -T %BASEPATH%/bin/helper/osh-groupDelEgressKey --group %GROUP% * + # as a gatekeeper, we can grant/revoke membership SUPEROWNERS, %%GROUP%-gatekeeper ALL=(root) NOPASSWD: /usr/bin/env perl -T %BASEPATH%/bin/helper/osh-groupSetRole --type member --group %GROUP% * # as a gatekeeper, to be able to symlink in /home/allowkeeper/ACCOUNT the /home/%GROUP%/allowed.ip file diff --git a/lib/perl/OVH/Bastion/Plugin/generateEgressKey.pm b/lib/perl/OVH/Bastion/Plugin/generateEgressKey.pm new file mode 100644 index 0000000..5a57deb --- /dev/null +++ b/lib/perl/OVH/Bastion/Plugin/generateEgressKey.pm @@ -0,0 +1,122 @@ +package OVH::Bastion::Plugin::generateEgressKey; + +# vim: set filetype=perl ts=4 sw=4 sts=4 et: +use common::sense; + +use File::Basename; +use lib dirname(__FILE__) . '/../../../../../lib/perl'; +use OVH::Result; +use OVH::Bastion; +use OVH::Bastion::Plugin qw{ :DEFAULT }; + +sub help { + require Term::ANSIColor; + my $fnret = OVH::Bastion::get_supported_ssh_algorithms_list(way => 'egress'); + my @algoList = @{$fnret->value}; + my $algos = Term::ANSIColor::colored(uc join(' ', @algoList), 'green'); + my $helpAlgoSize = '--algo rsa --size 4096'; + if (grep { $_ eq 'ecdsa' } @algoList) { + $helpAlgoSize = '--algo ecdsa --size 521'; + } + if (grep { $_ eq 'ed25519' } @algoList) { + $helpAlgoSize = '--algo ed25519'; + } + osh_info <<"EOF"; +Create a new public + private key pair. The private key will stay on this bastion. + +Usage: --osh $scriptName $helpAlgoSize [--encrypted] + + --algo ALGO Specifies the algo of the key, either rsa, ecdsa or ed25519. + + --size SIZE Specifies the size of the key to be generated. + For RSA, choose between 2048 and 8192 (4096 is good). + For ECDSA, choose either 256, 384 or 521. + For ED25519, size is always 256. + + --encrypted if specified, a passphrase will be prompted for the new key + +With the policy and SSH version on this bastion, +the following algorithms are supported: $algos + +algo size strength speed compatibility +------- ---- ---------- -------- ----------------------- +RSA 4096 good slow works everywhere +ECDSA 521 strong fast debian7+ (OpenSSH 5.7+) +ED25519 256 verystrong veryfast debian8+ (OpenSSH 6.5+) +EOF + return 0; +} + +sub ask_passphrase { + require Term::ReadKey; + print "Please enter a passphrase for the private key that'll stay on the bastion (not echoed): "; + Term::ReadKey::ReadMode('noecho'); + chomp(my $pass1 = ); + if (length($pass1) < 5) { + + # ssh-keygen will refuse + print "\n"; + return R('ERR_PASSPHRASE_TOO_SHORT', msg => "Passphrase needs to be at least 5 chars"); + } + print "\nPlease enter it again: "; + chomp(my $pass2 = ); + print "\n"; + Term::ReadKey::ReadMode('restore'); + if ($pass1 ne $pass2) { + return R('ERR_PASSPHRASE_MISMATCH', msg => "Passphrases don't match, please try again"); + } + return R('OK', value => $pass1); +} + +sub preconditions { + my %params = @_; + my $fnret; + + my ($self, $group, $algo, $size, $account, $sudo, $context) = @params{qw{ self group algo size account sudo context}}; + + if (!$algo || !$context) { + return R('ERR_MISSING_PARAMETER', msg => "Missing argument algo[$algo] or context[$context]"); + } + + if (!grep { $context eq $_ } qw{ group account }) { + return R('ERR_INVALID_PARAMETER', msg => "Type should be group or account"); + } + + # check whether algo is supported by system + $fnret = OVH::Bastion::is_allowed_algo_and_size(algo => $algo, size => $size, way => 'egress'); + $fnret or return $fnret; + ($algo, $size) = @{$fnret->value}{qw{ algo size }}; # untaint + + # check preconditions if we're generating a key for a group + if ($context eq 'group') { + if (!$group || !$self) { + return R('ERR_MISSING_PARAMETER', msg => "Missing 'group' or 'self' parameter"); + } + $fnret = OVH::Bastion::is_valid_group_and_existing(group => $group, groupType => 'key'); + $fnret or return $fnret; + my $keyhome = $fnret->value->{'keyhome'}; + my $shortGroup = $fnret->value->{'shortGroup'}; + $group = $fnret->value->{'group'}; + + $fnret = OVH::Bastion::is_group_owner(group => $shortGroup, account => $self, superowner => 1, sudo => $sudo); + if (!$fnret) { + return R('ERR_NOT_GROUP_OWNER', msg => "Sorry, you're not an owner of group $shortGroup, which is needed to manage its egress keys ($fnret)"); + } + + return R('OK', value => {group => $group, shortGroup => $shortGroup, keyhome => $keyhome, algo => $algo, size => $size, context => $context}); + } + elsif ($context eq 'account') { + if (!$account) { + return R('ERR_MISSING_PARAMETER', msg => "Missing 'group' parameter"); + } + $fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account); + $fnret or return $fnret; + + return R('OK', value => {algo => $algo, size => $size, context => $context}); + } + else { + return R('ERR_INTERNAL'); + } +} + +1; diff --git a/lib/perl/OVH/Bastion/ssh.inc b/lib/perl/OVH/Bastion/ssh.inc index b7cc400..1fea53d 100644 --- a/lib/perl/OVH/Bastion/ssh.inc +++ b/lib/perl/OVH/Bastion/ssh.inc @@ -664,6 +664,7 @@ sub is_allowed_algo_and_size { } if ($algo eq 'rsa') { + $algo = 'rsa'; # untaint $way = ucfirst($way); $fnret = OVH::Bastion::config("minimum${way}RsaKeySize"); $fnret or return $fnret; @@ -672,14 +673,20 @@ sub is_allowed_algo_and_size { } } elsif ($algo eq 'ecdsa') { + $algo = 'ecdsa'; # untaint if (not grep { $size eq $_ } qw{ 256 384 521 }) { return R('KO_KEY_SIZE_INVALID', msg => "For the selected algorithm, valid key sizes are 256, 384, 521"); } } - elsif ($algo eq 'ed25519' && $size && $size ne '256') { - return R('KO_KEY_SIZE_INVALID', msg => "For the selected algorithm, key size must be 256"); + elsif ($algo eq 'ed25519') { + $algo = 'ed25519'; # untaint + if ($size && $size ne '256') { + return R('KO_KEY_SIZE_INVALID', msg => "For the selected algorithm, key size must be 256"); + } + $size = 256; } - return R('OK'); + ($size) = $size =~ /^(\d+)$/; # untaint + return R('OK', value => {algo => $algo, size => $size}); } sub is_valid_fingerprint { diff --git a/tests/functional/tests.d/350-groups.sh b/tests/functional/tests.d/350-groups.sh index e9c5d4d..b00517a 100644 --- a/tests/functional/tests.d/350-groups.sh +++ b/tests/functional/tests.d/350-groups.sh @@ -104,6 +104,45 @@ EOS ) # new state: g1[a1(ow,gk,acl,member)] + # create g3 with a3 as owner to test key generation of a group a3 is not an owner of, without getting the early no-owner deny + success 350-groups a0_create_g3_with_a3_as_owner $a0 --osh groupCreate --group $group3 --algo ed25519 --owner $account3 + + # test egress key generation as an owner + run 350-groups a0_generate_key_g1_fail $a0 --osh groupGenerateEgressKey --group $group1 --algo ed25519 + retvalshouldbe 106 + json .command null .error_code KO_RESTRICTED_COMMAND .value null + + plgfail 350-groups a3_generate_key_g1_fail $a3 --osh groupGenerateEgressKey --group $group1 --algo ed25519 + json .command groupGenerateEgressKey .error_code ERR_NOT_GROUP_OWNER + + success 350-groups a1_generate_key_g1 $a1 --osh groupGenerateEgressKey --group $group1 --algo ed25519 + json .command groupGenerateEgressKey .error_code OK .value.typecode ssh-ed25519 + local key1id + local key1fp + key1id=$(get_json | $jq .value.id) + key1fp=$(get_json | $jq .value.fingerprint) + + success 350-groups a0_list_group_keys_g1 $a0 --osh groupInfo --group $group1 + json .command groupInfo .error_code OK ".value.keys.\"$key1fp\".typecode" ssh-ed25519 + + run 350-groups a0_del_key_g1 $a0 --osh groupDelEgressKey --group $group1 --id $key1id + retvalshouldbe 106 + json .command null .error_code KO_RESTRICTED_COMMAND .value null + + plgfail 350-groups a3_del_key_g1 $a3 --osh groupDelEgressKey --group $group1 --id $key1id + json .command groupDelEgressKey .error_code ERR_NOT_GROUP_OWNER + + success 350-groups a1_del_key_g1 $a1 --osh groupDelEgressKey --group $group1 --id $key1id + json .command groupDelEgressKey .error_code OK .value.id "$key1id" .value.fingerprint "$key1fp" + unset key1id + unset key1fp + + grant groupDelete + script 350-groups a0_delete_g3 $a0 --osh groupDelete --group $group3 '<<<' "$group3" + retvalshouldbe 0 + revoke groupDelete + # /egress key generation + # now test all group-* commands from a2 to grant a3 on g1 => should get an early deny run groupAddOwner a2_fail_to_addowner_a3_on_g1_early_deny_owner_cmd $a2 --osh groupAddOwner --group $group1 --account $account3