diff --git a/bin/helper/osh-groupModify b/bin/helper/osh-groupModify index 23c7f22..3ba5c5e 100755 --- a/bin/helper/osh-groupModify +++ b/bin/helper/osh-groupModify @@ -18,7 +18,7 @@ use OVH::Result; # Fetch command options my $fnret; my ($result, @optwarns); -my ($group, $mfaRequired, $ttl, $idleLockTimeout, $idleKillTimeout); +my ($group, $mfaRequired, $ttl, $idleLockTimeout, $idleKillTimeout, $tryPersonalKeys); eval { local $SIG{__WARN__} = sub { push @optwarns, shift }; $result = GetOptions( @@ -27,6 +27,7 @@ eval { "guest-ttl-limit=i" => \$ttl, "idle-lock-timeout=i" => \$idleLockTimeout, "idle-kill-timeout=i" => \$idleKillTimeout, + "try-personal-keys=s" => \$tryPersonalKeys, ); }; if ($@) { die $@ } @@ -42,9 +43,11 @@ if (!$group) { HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'group'"); } -if (!$mfaRequired && !defined $ttl && !defined $idleLockTimeout && !defined $idleKillTimeout) { +if (!$mfaRequired && !defined $ttl && !defined $idleLockTimeout && !defined $idleKillTimeout && !$tryPersonalKeys) { HEXIT('ERR_MISSING_PARAMETER', - msg => "Missing argument 'mfa-required', 'guest-ttl-limit', 'idle-lock-timeout' or 'idle-kill-timeout'"); + msg => + "Missing argument 'mfa-required', 'guest-ttl-limit', 'idle-lock-timeout', 'idle-kill-timeout' or 'try-personal-keys'" + ); } #
$group, + %{OVH::Bastion::OPT_GROUP_TRY_PERSONAL_KEYS()}, + value => ($tryPersonalKeys eq 'yes' ? 1 : 0) + ); + if ($fnret) { + osh_info "... done, policy is now: $tryPersonalKeys"; + } + else { + osh_warn "... error while changing try-personal-keys policy (" . $fnret->msg . ")"; + } + $result{'try_personal_keys'} = $fnret; + } + else { + osh_warn "... invalid option '$tryPersonalKeys'"; + $result{'try_personal_keys'} = R('ERR_INVALID_PARAMETER'); + } +} + my %idleTimeout = ( lock => { name => "idle lock timeout", diff --git a/bin/plugin/group-owner/groupModify b/bin/plugin/group-owner/groupModify index 0a97e82..ba42043 100755 --- a/bin/plugin/group-owner/groupModify +++ b/bin/plugin/group-owner/groupModify @@ -17,11 +17,12 @@ my $remainingOptions = OVH::Bastion::Plugin::begin( "idle-lock-timeout=s" => \my $idleLockTimeout, "idle-kill-timeout=s" => \my $idleKillTimeout, "guest-ttl-limit=s" => \my $ttl, + "try-personal-keys=s" => \my $tryPersonalKeys, }, helptext => <<'EOF', Modify the configuration of a group -Usage: --osh SCRIPT_NAME --group GROUP [--mfa-required password|totp|any|none] [--guest-ttl-limit DURATION] +Usage: --osh SCRIPT_NAME --group GROUP [--mfa-required password|totp|any|none] [--guest-ttl-limit DURATION] [--try-personal-keys yes|no] --group GROUP Name of the group to modify --mfa-required password|totp|any|none Enforce UNIX password requirement, or TOTP requirement, or any MFA requirement, when connecting to a server of the group @@ -31,6 +32,8 @@ Usage: --osh SCRIPT_NAME --group GROUP [--mfa-required password|totp|any|none] [ this group. If set to -1, remove this group override and use the global setting instead. --guest-ttl-limit DURATION This group will enforce TTL setting, on guest access creation, to be set, and not to a higher value than DURATION, set to zero to allow guest accesses creation without any TTL set (default) + --try-personal-keys yes|no When a user accesses a server through his group permission, his personal access keys will also be added + to the connection attempt (default: no) Note that `--idle-lock-timeout` and `--idle-kill-timeout` will NOT be applied for catch-all groups (having 0.0.0.0/0 in their server list). @@ -48,7 +51,12 @@ if (!$group) { help(); osh_exit 'ERR_MISSING_PARAMETER', "Missing mandatory parameter 'group'"; } -if (!$mfaRequired && !defined $ttl && !defined $idleLockTimeout && !defined $idleKillTimeout) { +if ( !$mfaRequired + && !defined $ttl + && !defined $idleLockTimeout + && !defined $idleKillTimeout + && !defined $tryPersonalKeys) +{ help(); osh_exit 'ERR_MISSING_PARAMETER', "Nothing to modify"; } @@ -81,6 +89,11 @@ if (defined $mfaRequired && !grep { $mfaRequired eq $_ } qw{ password totp any n osh_exit 'ERR_INVALID_PARAMETER', "Expected 'password', 'totp', 'any' or 'none' as parameter to --mfa-required"; } +if (defined $tryPersonalKeys && !grep { $tryPersonalKeys eq $_ } qw{ yes no }) { + help(); + osh_exit 'ERR_INVALID_PARAMETER', "Expected 'yes' or 'no' as parameter to --try-personal-keys"; +} + my @command = qw{ sudo -n -u }; push @command, $group; push @command, qw{ -- /usr/bin/env perl -T }; @@ -90,5 +103,6 @@ push @command, '--mfa-required', $mfaRequired if $mfaRequired; push @command, '--guest-ttl-limit', $ttl if defined $ttl; push @command, '--idle-lock-timeout', $idleLockTimeout if defined $idleLockTimeout; push @command, '--idle-kill-timeout', $idleKillTimeout if defined $idleKillTimeout; +push @command, '--try-personal-keys', $tryPersonalKeys if defined $tryPersonalKeys; osh_exit OVH::Bastion::helper(cmd => \@command); diff --git a/bin/plugin/open/groupInfo b/bin/plugin/open/groupInfo index d0d39da..1ae6266 100755 --- a/bin/plugin/open/groupInfo +++ b/bin/plugin/open/groupInfo @@ -205,6 +205,11 @@ foreach my $groupData (@groups) { if ($fnret && defined $fnret->value && $fnret->value =~ /^-?\d+$/) { $ret{'idle_lock_timeout'} = $fnret->value; } + + $fnret = OVH::Bastion::group_config(group => $group, %{OVH::Bastion::OPT_GROUP_TRY_PERSONAL_KEYS()}); + if ($fnret && defined $fnret->value) { + $ret{'try_personal_keys'} = ($fnret->value eq '1' ? 'yes' : 'no'); + } } # group egress keys if we've been asked those @@ -302,6 +307,17 @@ sub print_group_info { osh_warn "Specific idle kill timeout: idle sessions on servers of this group will $action"; } + if ($ret{'try_personal_keys'}) { + osh_info ' '; + if ($ret{'try_personal_keys'} eq 'yes') { + osh_info + "Personal keys: When group members access servers from this group, their personal egress keys will also be tried."; + } + else { + osh_info "Personal keys: No personal egress keys will be used."; + } + } + if ($withKeys) { osh_info ' '; if (!%{$ret{'keys'}}) { diff --git a/bin/shell/connect.pl b/bin/shell/connect.pl index fd40f80..fba8ea3 100755 --- a/bin/shell/connect.pl +++ b/bin/shell/connect.pl @@ -220,7 +220,8 @@ if ($header) { 1) Check the remote account's authorized_keys on $ip, did you add the proper key there? (personal key or group key) 2) Did you tell the bastion you added a key to the remote server, so it knows it has to use it? See the actually used keys just above. If you didn't, do it with selfAddPersonalAccess or groupAddServer. 3) Check the from="" part of the remote account's authorized_keys' keyline. Are all the bastion IPs present? Master and slave(s)? See groupInfo or selfListEgressKeys to get the proper keyline to copy/paste. - 4) Did you check the 3 above points carefully? Really? Because if you did, you wouldn't be reading this 4th bullet point, as your problem would already be fixed ;) + 4) Are you trying to access a remote server through a group permission but the remote server expects your personal access key? Ask your group owner to enable the 'try-personal-keys' policy. + 5) Did you check the 4 above points carefully? Really? Because if you did, you wouldn't be reading this 5th bullet point, as your problem would already be fixed ;) EOS ); } diff --git a/lib/perl/OVH/Bastion.pm b/lib/perl/OVH/Bastion.pm index d065c2d..149cac4 100644 --- a/lib/perl/OVH/Bastion.pm +++ b/lib/perl/OVH/Bastion.pm @@ -132,6 +132,7 @@ use constant { OPT_GROUP_IDLE_LOCK_TIMEOUT => {key => 'idle_lock_timeout'}, OPT_GROUP_IDLE_KILL_TIMEOUT => {key => 'idle_kill_timeout'}, + OPT_GROUP_TRY_PERSONAL_KEYS => {key => 'try_personal_keys'}, }; ########### diff --git a/lib/perl/OVH/Bastion/allowdeny.inc b/lib/perl/OVH/Bastion/allowdeny.inc index 8b352b7..eb996d0 100644 --- a/lib/perl/OVH/Bastion/allowdeny.inc +++ b/lib/perl/OVH/Bastion/allowdeny.inc @@ -859,6 +859,16 @@ sub is_access_granted { # normal member case, just reuse $grantedGroup osh_debug("is_access_granted: adding grantedGroup to grants because is member"); push @grants, {type => 'group-member', group => $shortGroup, %{$grantedGroup->value}}; + + # check if group has the "try-personal-keys" option enabled, and if so, add personal keys too + my $tryPersonalKeysConfig = + OVH::Bastion::group_config(group => $group, %{OVH::Bastion::OPT_GROUP_TRY_PERSONAL_KEYS()}); + if ($tryPersonalKeysConfig && $tryPersonalKeysConfig->value && $tryPersonalKeysConfig->value eq '1') { + osh_debug( + "is_access_granted: try-personal-keys is enabled for group $shortGroup, adding personal access"); + push @grants, {type => 'personal-via-group', group => $shortGroup, %{$grantedGroup->value}}; + } + } elsif (OVH::Bastion::is_group_guest(group => $shortGroup, account => $account, sudo => $params{'sudo'})) { @@ -871,6 +881,16 @@ sub is_access_granted { if ($grantedGuest && $grantedGroup) { push @grants, {type => 'group-guest', group => $shortGroup, %{$grantedGuest->value}}; osh_debug("is_access_granted: adding grantedGuest to grants because is guest and group has access"); + + # check if group has the "try-personal-keys" option enabled, and if so, add personal keys too + my $tryPersonalKeysConfig = + OVH::Bastion::group_config(group => $group, %{OVH::Bastion::OPT_GROUP_TRY_PERSONAL_KEYS()}); + if ($tryPersonalKeysConfig && $tryPersonalKeysConfig->value && $tryPersonalKeysConfig->value eq '1') { + osh_debug( + "is_access_granted: try-personal-keys is enabled for group $shortGroup, adding personal access" + ); + push @grants, {type => 'personal-via-group', group => $shortGroup, %{$grantedGroup->value}}; + } } # special legacy case; we also check if account has a legacy access for ip AND that the group ALSO has access to this ip @@ -913,6 +933,22 @@ sub is_access_granted { $data{'mfa'} = OVH::Bastion::account_config(key => "personal_egress_mfa_required", account => $sysaccount); } + elsif ($access->{'type'} eq 'personal-via-group') { + # This is personal access triggered by try-personal-keys group option + # Use personal keys but group MFA and timeout settings + $data{'keys'} = OVH::Bastion::get_personal_account_keys( + account => $sysaccount, + listOnly => $listOnly, + noexec => $noexec, + forceKey => $access->{'forceKey'} + ); + # Use group MFA and timeout settings since access is via group + $data{'mfa'} = OVH::Bastion::group_config(key => "mfa_required", group => $access->{'group'}); + $data{'idle_lock_timeout'} = OVH::Bastion::group_config(%{OVH::Bastion::OPT_GROUP_IDLE_LOCK_TIMEOUT()}, + group => $access->{'group'}); + $data{'idle_kill_timeout'} = OVH::Bastion::group_config(%{OVH::Bastion::OPT_GROUP_IDLE_KILL_TIMEOUT()}, + group => $access->{'group'}); + } else { # unknown access type? no key! warn_syslog("Unknown access type '" . $access->{'type'} . "' found, ignoring"); diff --git a/tests/functional/tests.d/350-groups.sh b/tests/functional/tests.d/350-groups.sh index fb2ca16..fdafea3 100644 --- a/tests/functional/tests.d/350-groups.sh +++ b/tests/functional/tests.d/350-groups.sh @@ -1088,6 +1088,34 @@ EOS success guest_ttl_limit $a1 --osh groupModify --group $group1 --guest-ttl-limit 0 json .command groupModify .error_code OK + # try-personal-keys tests + # test invalid parameter values for try-personal-keys + plgfail invalid_try_personal_keys_value $a1 --osh groupModify --group $group1 --try-personal-keys invalid + json .error_code ERR_INVALID_PARAMETER + + # enable try-personal-keys + success enable_try_personal_keys $a1 --osh groupModify --group $group1 --try-personal-keys yes + json .error_code OK .command groupModify .value.try_personal_keys.error_code OK + + # verify the setting is applied in groupInfo + success check_try_personal_keys_enabled $a1 --osh groupInfo --group $group1 + contain "personal egress keys will also be tried" + json .error_code OK .command groupInfo .value.try_personal_keys yes + + # disable try-personal-keys + success disable_try_personal_keys $a1 --osh groupModify --group $group1 --try-personal-keys no + json .error_code OK .command groupModify .value.try_personal_keys.error_code OK + + # verify the setting is applied in groupInfo + success check_try_personal_keys_disabled $a1 --osh groupInfo --group $group1 + contain "No personal egress keys will be used" + json .error_code OK .command groupInfo .value.try_personal_keys no + + # non-owner cannot modify try-personal-keys + run non_owner_modify_try_personal_keys $a2 --osh groupModify --group $group1 --try-personal-keys yes + retvalshouldbe 106 + json .error_code KO_RESTRICTED_COMMAND + # if we're just counting the number of tests, don't sleep [ "$COUNTONLY" != 1 ] && sleep 1 @@ -1147,6 +1175,60 @@ EOS nocontain "$group1" contain "personal access" + # try-personal-keys tests + # First, make account2 a member instead of just a guest for proper testing + success add_a2_as_member_for_try_personal_keys $a1 --osh groupAddMember --group $group1 --account $account2 + json .error_code OK .command groupAddMember + + # Without try-personal-keys enabled - should only show group access + run a2_group_access_only $a2 127.0.0.11 --user testuser + retvalshouldbe 255 + contain "allowed ... log on" + contain "group-member of $group1" + nocontain "personal-via-group" + + # Enable try-personal-keys and test access + success enable_try_personal_keys_for_access $a1 --osh groupModify --group $group1 --try-personal-keys yes + json .error_code OK .command groupModify + + # Now connection should show both group and personal access + run a2_group_and_personal_access $a2 127.0.0.11 --user testuser + retvalshouldbe 255 + contain "allowed ... log on" + contain "group-member of $group1" + contain "personal-via-group" + + # Disable try-personal-keys again + success disable_try_personal_keys_for_access $a1 --osh groupModify --group $group1 --try-personal-keys no + json .error_code OK .command groupModify + + # Should only show group access again + run a2_group_access_only_again $a2 127.0.0.11 --user testuser + retvalshouldbe 255 + contain "allowed ... log on" + contain "group-member of $group1" + nocontain "personal-via-group" + + # Enable try-personal-keys again + success enable_try_personal_keys_again $a1 --osh groupModify --group $group1 --try-personal-keys yes + json .error_code OK .command groupModify + + # Remove account2 from group members (this will also remove guest access) + success remove_a2_from_members $a1 --osh groupDelMember --group $group1 --account $account2 + json .error_code OK .command groupDelMember + + # Add account2 back as guest to test guest access only + success add_a2_as_guest_after_removal $a1 --osh groupAddGuestAccess --group $group1 --account $account2 --host 127.0.0.10 --user testuser --port 22 + json .error_code OK .command groupAddGuestAccess + + # Account2 is now guest but can still use personal access keys to connect + run a2_guest_access_only_after_member_removal $a2 127.0.0.10 --user testuser + retvalshouldbe 255 + contain "allowed ... log on" + contain "group-guest of $group1" + nocontain "group-member" + contain "personal-via-group" + # group1: a1(owner,aclkeeper,gatekeeper,member) a2(guest(127.0.0.10)) servers(127.0.0.10,127.0.0.11) # account1: perso(account1@127.0.0.11:22) plgfail transmitownership_noright1 $a3 --osh groupTransmitOwnership --group $group1 --account $account1