diff --git a/bin/helper/osh-accountAddGroupServer b/bin/helper/osh-accountAddGroupServer index c5b180b..da21fe6 100755 --- a/bin/helper/osh-accountAddGroupServer +++ b/bin/helper/osh-accountAddGroupServer @@ -17,19 +17,22 @@ use OVH::Bastion::Helper; # Fetch command options my $fnret; my ($result, @optwarns); -my ($account, $group, $ip, $user, $port, $action, $ttl, $comment, $forceKey); +my ($account, $group, $ip, $user, $port, $proxyIp, $proxyPort, $proxyUser, $action, $ttl, $comment, $forceKey); eval { local $SIG{__WARN__} = sub { push @optwarns, shift }; $result = GetOptions( - "account=s" => sub { $account //= $_[1] }, - "group=s" => sub { $group //= $_[1] }, - "ip=s" => sub { $ip //= $_[1] }, - "user=s" => sub { $user //= $_[1] }, - "port=i" => sub { $port //= $_[1] }, - "action=s" => sub { $action //= $_[1] }, - "ttl=i" => sub { $ttl //= $_[1] }, - "comment=s" => sub { $comment //= $_[1] }, - "force-key=s" => sub { $forceKey //= $_[1] }, + "account=s" => sub { $account //= $_[1] }, + "group=s" => sub { $group //= $_[1] }, + "ip=s" => sub { $ip //= $_[1] }, + "user=s" => sub { $user //= $_[1] }, + "port=i" => sub { $port //= $_[1] }, + "action=s" => sub { $action //= $_[1] }, + "ttl=i" => sub { $ttl //= $_[1] }, + "comment=s" => sub { $comment //= $_[1] }, + "force-key=s" => sub { $forceKey //= $_[1] }, + "proxy-ip=s" => sub { $proxyIp //= $_[1] }, + "proxy-port=i" => sub { $proxyPort //= $_[1] }, + "proxy-user=s" => sub { $proxyUser //= $_[1] }, ); }; if ($@) { die $@ } @@ -57,17 +60,28 @@ if (not grep { $action eq $_ } qw{ add del }) { #CODE +$fnret = OVH::Bastion::load_configuration(); +$fnret or main_exit(OVH::Bastion::EXIT_CONFIGURATION_FAILURE, "configuration_failure", $fnret->msg); +my $config = $fnret->value; + +if ($action eq 'add' && $proxyIp && !$config->{'egressProxyJumpAllowed'}) { + HEXIT('ERR_INVALID_PARAMETER', msg => "ProxyJump egress connections are disabled by policy"); +} + # access_modify validates all its parameters, don't do it ourselves here for clarity $fnret = OVH::Bastion::access_modify( - way => 'groupguest', - account => $account, - group => $group, - action => $action, - user => $user, - ip => $ip, - port => $port, - ttl => $ttl, - comment => $comment, - forceKey => $forceKey + way => 'groupguest', + account => $account, + group => $group, + action => $action, + user => $user, + ip => $ip, + port => $port, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser, + ttl => $ttl, + comment => $comment, + forceKey => $forceKey ); HEXIT($fnret); diff --git a/bin/helper/osh-accountModifyPersonalAccess b/bin/helper/osh-accountModifyPersonalAccess index 5ec2574..a56de54 100755 --- a/bin/helper/osh-accountModifyPersonalAccess +++ b/bin/helper/osh-accountModifyPersonalAccess @@ -32,7 +32,10 @@ use OVH::Bastion::Helper; # Fetch command options my $fnret; my ($result, @optwarns); -my ($account, $ip, $user, $port, $action, $ttl, $forceKey, $forcePassword, $target, $comment); +my ( + $account, $ip, $user, $port, $action, $ttl, $forceKey, + $forcePassword, $target, $comment, $proxyIp, $proxyPort, $proxyUser +); eval { local $SIG{__WARN__} = sub { push @optwarns, shift }; $result = GetOptions( @@ -46,6 +49,9 @@ eval { "force-password=s" => sub { $forcePassword //= $_[1] }, "target=s" => sub { $target //= $_[1] }, "comment=s" => sub { $comment //= $_[1] }, + "proxy-ip=s" => sub { $proxyIp //= $_[1] }, + "proxy-port=i" => sub { $proxyPort //= $_[1] }, + "proxy-user=s" => sub { $proxyUser //= $_[1] }, ); }; if ($@) { die $@ } @@ -81,7 +87,22 @@ if (not grep { $action eq $_ } qw{ add del }) { #CODE -my $machine = OVH::Bastion::machine_display(ip => $ip, port => $port, user => $user)->value; +$fnret = OVH::Bastion::load_configuration(); +$fnret or main_exit(OVH::Bastion::EXIT_CONFIGURATION_FAILURE, "configuration_failure", $fnret->msg); +my $config = $fnret->value; + +if ($action eq 'add' && $proxyIp && !$config->{'egressProxyJumpAllowed'}) { + HEXIT('ERR_INVALID_PARAMETER', msg => "ProxyJump egress connections are disabled by policy"); +} + +my $machine = OVH::Bastion::machine_display( + ip => $ip, + port => $port, + user => $user, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser +)->value; my $plugin = ($target eq 'self' ? 'self' : 'account') . 'AddPersonalAccess'; @@ -115,19 +136,25 @@ $fnret = OVH::Bastion::access_modify( forcePassword => $forcePassword, comment => $comment, widestV4Prefix => $widestV4Prefix, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser, ); if ($fnret->err eq 'OK') { my $ttlmsg = $ttl ? ' (expires in ' . OVH::Bastion::duration2human(seconds => $ttl)->value->{'human'} . ')' : ''; HEXIT( 'OK', value => { - action => $action, - account => $account, - ip => $ip, - user => $user, - port => $port, - ttl => $ttl, - comment => $comment + action => $action, + account => $account, + ip => $ip, + user => $user, + port => $port, + ttl => $ttl, + comment => $comment, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser, }, msg => $action eq 'add' ? "Access to $machine was added to account $account$ttlmsg" diff --git a/bin/helper/osh-groupAddServer b/bin/helper/osh-groupAddServer index 867fa64..f5216e7 100755 --- a/bin/helper/osh-groupAddServer +++ b/bin/helper/osh-groupAddServer @@ -19,7 +19,10 @@ use OVH::Bastion::Helper; # Fetch command options my $fnret; my ($result, @optwarns); -my ($group, $user, $ip, $port, $action, $force, $forcePassword, $forceKey, $ttl, $comment); +my ( + $group, $user, $ip, $port, $action, $force, $forcePassword, + $forceKey, $ttl, $comment, $proxyIp, $proxyPort, $proxyUser +); eval { local $SIG{__WARN__} = sub { push @optwarns, shift }; $result = GetOptions( @@ -33,6 +36,9 @@ eval { "force-key=s" => sub { $forceKey //= $_[1] }, "ttl=i" => sub { $ttl //= $_[1] }, "comment=s" => sub { $comment //= $_[1] }, + "proxy-ip=s" => sub { $proxyIp //= $_[1] }, + "proxy-port=i" => sub { $proxyPort //= $_[1] }, + "proxy-user=s" => sub { $proxyUser //= $_[1] }, ); }; @@ -86,7 +92,22 @@ $fnret = OVH::Bastion::Helper::acquire_lock($lock_fh); $fnret or HEXIT($fnret); #>CODE -my $machine = OVH::Bastion::machine_display(ip => $ip, port => $port, user => $user)->value; +$fnret = OVH::Bastion::load_configuration(); +$fnret or main_exit(OVH::Bastion::EXIT_CONFIGURATION_FAILURE, "configuration_failure", $fnret->msg); +my $config = $fnret->value; + +if ($action eq 'add' && $proxyIp && !$config->{'egressProxyJumpAllowed'}) { + HEXIT('ERR_INVALID_PARAMETER', msg => "ProxyJump egress connections are disabled by policy"); +} + +my $machine = OVH::Bastion::machine_display( + ip => $ip, + port => $port, + user => $user, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser +)->value; # access_modify validates all its parameters, don't do it ourselves here for clarity $fnret = OVH::Bastion::access_modify( @@ -100,6 +121,9 @@ $fnret = OVH::Bastion::access_modify( forceKey => $forceKey, ttl => $ttl, comment => $comment, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser, ); if ($fnret->err eq 'OK') { my $ttlmsg = $ttl ? ' (expires in ' . OVH::Bastion::duration2human(seconds => $ttl)->value->{'human'} . ')' : ''; @@ -114,7 +138,10 @@ if ($fnret->err eq 'OK') { forcePassword => $forcePassword, forceKey => $forceKey, ttl => $ttl, - comment => $comment + comment => $comment, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser, }, msg => $action eq 'add' ? "Entry $machine was added to group $shortGroup$ttlmsg" diff --git a/bin/plugin/group-aclkeeper/groupAddServer b/bin/plugin/group-aclkeeper/groupAddServer index 60a4a05..c25b86a 100755 --- a/bin/plugin/group-aclkeeper/groupAddServer +++ b/bin/plugin/group-aclkeeper/groupAddServer @@ -21,6 +21,9 @@ my $remainingOptions = OVH::Bastion::Plugin::begin( "force-password=s" => \my $forcePassword, "ttl=s" => \my $ttl, "comment=s" => \my $comment, + "proxy-host=s" => \my $proxyHost, + "proxy-port=s" => \my $proxyPort, + "proxy-user=s" => \my $proxyUser, # undocumented/compatibility: "user-any" => \my $userAny, "port-any" => \my $portAny, @@ -33,28 +36,32 @@ Add an IP or IP block to a group's servers list Usage: --osh SCRIPT_NAME --group GROUP --host HOST --user USER|* --port PORT|* [OPTIONS] - --group GROUP Specify which group this machine should be added to - --host HOST|IP|SUBNET Host(s) to add access to, either a HOST which will be resolved to an IP immediately, - or an IP, or a whole subnet using the PREFIX/SIZE notation - --user USER|PATTERN|* Specify which remote user should be allowed to connect as. - Globbing characters '*' and '?' are supported, so you can specify a pattern - that will be matched against the actual remote user name. - To allow any user, use '--user *' (you might need to escape '*' from your shell) - --port PORT|* Remote port allowed to connect to - To allow any port, use '--port *' (you might need to escape '*' from your shell) - --protocol PROTO Specify that a special protocol should be allowed for this HOST:PORT tuple, note that you - must not specify --user in that case. However, for this protocol to be usable under a given - remote user, access to the USER@HOST:PORT tuple must also be allowed. - PROTO must be one of: - scpup allow SCP upload, you--bastion-->server - scpdown allow SCP download, you<--bastion--server - sftp allow usage of the SFTP subsystem, through the bastion - rsync allow usage of rsync, through the bastion - --force Don't try the ssh connection, just add the host to the group blindly - --force-key FINGERPRINT Only use the key with the specified fingerprint to connect to the server (cf groupInfo) - --force-password HASH Only use the password with the specified hash to connect to the server (cf groupListPasswords) - --ttl SECONDS|DURATION Specify a number of seconds (or a duration string, such as "1d7h8m") after which the access will automatically expire - --comment "'ANY TEXT'" Add a comment alongside this server. Quote it twice as shown if you're under a shell. + --group GROUP Specify which group this machine should be added to + --host HOST|IP|SUBNET Host(s) to add access to, either a HOST which will be resolved to an IP immediately, + or an IP, or a whole subnet using the PREFIX/SIZE notation + --user USER|PATTERN|* Specify which remote user should be allowed to connect as. + Globbing characters '*' and '?' are supported, so you can specify a pattern + that will be matched against the actual remote user name. + To allow any user, use '--user *' (you might need to escape '*' from your shell) + --port PORT|* Remote port allowed to connect to + To allow any port, use '--port *' (you might need to escape '*' from your shell) + --protocol PROTO Specify that a special protocol should be allowed for this HOST:PORT tuple, note that you + must not specify --user in that case. However, for this protocol to be usable under a given + remote user, access to the USER@HOST:PORT tuple must also be allowed. + PROTO must be one of: + scpup allow SCP upload, you--bastion-->server + scpdown allow SCP download, you<--bastion--server + sftp allow usage of the SFTP subsystem, through the bastion + rsync allow usage of rsync, through the bastion + --force Don't try the ssh connection, just add the host to the group blindly + --force-key FINGERPRINT Only use the key with the specified fingerprint to connect to the server (cf groupInfo) + --force-password HASH Only use the password with the specified hash to connect to the server (cf groupListPasswords) + --ttl SECONDS|DURATION Specify a number of seconds (or a duration string, such as "1d7h8m") after which the access will automatically expire + --comment "'ANY TEXT'" Add a comment alongside this server. Quote it twice as shown if you're under a shell. + --proxy-host HOST|IP Use this host as a proxy/jump host to reach the target server + --proxy-port PORT Proxy host port to connect to (mandatory when --proxy-host is specified) + --proxy-user USER|PATTERN|* Proxy user to connect as (mandatory when --proxy-host is specified). + Globbing characters '*' and '?' are supported for pattern matching. Examples:: @@ -76,22 +83,36 @@ if (not $group or not $ip) { "Missing mandatory parameter 'host' or 'group' (or host didn't resolve correctly)"; } +my $proxyIp; +if ($proxyHost) { + $fnret = + OVH::Bastion::validate_proxy_params(proxyHost => $proxyHost, proxyPort => $proxyPort, proxyUser => $proxyUser); + $fnret or osh_exit $fnret; + $proxyIp = $fnret->value->{'proxyIp'}; + $proxyPort = $fnret->value->{'proxyPort'}; + $proxyUser = $fnret->value->{'proxyUser'}; +} + $fnret = OVH::Bastion::Plugin::ACL::check( - user => $user, - userAny => $userAny, - port => $port, - portAny => $portAny, - scpUp => $scpUp, - scpDown => $scpDown, - sftp => $sftp, - protocol => $protocol, + user => $user, + userAny => $userAny, + port => $port, + portAny => $portAny, + scpUp => $scpUp, + scpDown => $scpDown, + sftp => $sftp, + protocol => $protocol, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser, ); if (!$fnret) { help(); osh_exit($fnret); } -$user = $fnret->value->{'user'}; -$port = $fnret->value->{'port'}; +$user = $fnret->value->{'user'}; +$port = $fnret->value->{'port'}; +$proxyUser = $fnret->value->{'proxyUser'}; $fnret = OVH::Bastion::is_valid_group_and_existing(group => $group, groupType => "key"); $fnret or osh_exit($fnret); @@ -138,7 +159,10 @@ if (not $force) { port => $port, ip => $ip, forceKey => $forceKey, - forcePassword => $forcePassword + forcePassword => $forcePassword, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser, ); if ($fnret->is_ok and $fnret->err ne 'OK') { # we have something to say, say it @@ -170,5 +194,8 @@ push @command, '--force-key', $forceKey if $forceKey; push @command, '--force-password', $forcePassword if $forcePassword; push @command, '--ttl', $ttl if $ttl; push @command, '--comment', $comment if $comment; +push @command, '--proxy-ip', $proxyIp if $proxyIp; +push @command, '--proxy-port', $proxyPort if $proxyPort; +push @command, '--proxy-user', $proxyUser if $proxyUser; osh_exit OVH::Bastion::helper(cmd => \@command); diff --git a/bin/plugin/group-aclkeeper/groupAddServer.json b/bin/plugin/group-aclkeeper/groupAddServer.json index c52cfc5..72c00e2 100644 --- a/bin/plugin/group-aclkeeper/groupAddServer.json +++ b/bin/plugin/group-aclkeeper/groupAddServer.json @@ -4,14 +4,22 @@ "groupAddServer +--group" , {"ac" : [""]}, "groupAddServer +--group +\\S+" , {"ac" : ["--host"]}, "groupAddServer +--group +\\S+ +--host" , {"pr" : ["", "", ""]}, - "groupAddServer +--group +\\S+ +--host +\\S+" , {"ac" : ["--port", "--port-any"]}, - "groupAddServer +--group +\\S+ +--host +\\S+ +--port" , {"pr" : [""]}, - "groupAddServer +--group +\\S+ +--host +\\S+ +--port(-any| +\\d+)" , {"ac" : ["--user", "--user-any"]}, - "groupAddServer +--group +\\S+ +--host +\\S+ +--port(-any| +\\d+) +--user" , {"pr" : [""]}, - "groupAddServer +--group +\\S+ +--host +\\S+ +--port(-any| +\\d+) +--user(-any| +\\S+)" , {"ac" : ["", "--force-password", "--force"]}, - "groupAddServer +--group +\\S+ +--host +\\S+ +--port(-any| +\\d+) +--user(-any| +\\S+) +--force-password" , {"pr" : [""]}, - "groupAddServer +--group +\\S+ +--host +\\S+ +--port(-any| +\\d+) +--user(-any| +\\S+) +--force-password +\\S+" , {"ac" : ["", "--force"]}, - "groupAddServer +--group +\\S+ +--host +\\S+ +--port(-any| +\\d+) +--user(-any| +\\S+) +--force-password +\\S+ +--force" , {"pr" : [""]} + "groupAddServer +--group +\\S+ +--host +\\S+" , {"ac" : ["", "--user", "--port"]}, + "groupAddServer +--group +\\S+ +--host +\\S+ +.*--user" , {"pr" : [""]}, + "groupAddServer +--group +\\S+ +--host +\\S+ +.*--port" , {"pr" : [""]}, + "groupAddServer +--group +\\S+ +--host +\\S+ +--user +\\S+" , {"ac" : ["", "--port"]}, + "groupAddServer +--group +\\S+ +--host +\\S+ +--port +\\S+" , {"ac" : ["", "--user"]}, + "groupAddServer +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+" , {"ac" : ["--proxy-host", "--force-key", "--force-password", "--ttl", "--comment", "--force", ""]}, + "groupAddServer +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host" , {"pr" : ["", ""]}, + "groupAddServer +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+" , {"ac" : ["--proxy-port"]}, + "groupAddServer +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port" , {"pr" : [""]}, + "groupAddServer +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port +\\S+" , {"ac" : ["--proxy-user"]}, + "groupAddServer +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port +\\S+ +.*--proxy-user" , {"pr" : [""]}, + "groupAddServer +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port +\\S+ +.*--proxy-user +\\S+" , {"ac" : ["--force-key", "--force-password", "--ttl", "--comment", "--force", ""]}, + "groupAddServer +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--ttl" , {"pr" : [""]}, + "groupAddServer +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--force-key" , {"pr" : [""]}, + "groupAddServer +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--force-password" , {"pr" : [""]}, + "groupAddServer +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--comment" , {"pr" : [""]} ], "master_only": true } diff --git a/bin/plugin/group-aclkeeper/groupDelServer b/bin/plugin/group-aclkeeper/groupDelServer index 3f58d8f..43ded19 100755 --- a/bin/plugin/group-aclkeeper/groupDelServer +++ b/bin/plugin/group-aclkeeper/groupDelServer @@ -14,9 +14,12 @@ my $remainingOptions = OVH::Bastion::Plugin::begin( header => "removing a server from a group", userAllowWildcards => 1, options => { - "group=s" => \my $group, - "protocol=s" => \my $protocol, - "force" => \my $force, + "group=s" => \my $group, + "protocol=s" => \my $protocol, + "force" => \my $force, + "proxy-host=s" => \my $proxyHost, + "proxy-port=s" => \my $proxyPort, + "proxy-user=s" => \my $proxyUser, # undocumented/compatibility: "user-any" => \my $userAny, "port-any" => \my $portAny, @@ -45,6 +48,10 @@ Usage: --osh SCRIPT_NAME --group GROUP --host HOST --user USER --port PORT [OPTI scpdown allow SCP download, you<--bastion--server sftp allow usage of the SFTP subsystem, through the bastion rsync allow usage of rsync, through the bastion + --proxy-host HOST|IP Specify which host was used as a proxy/jump host to reach the target server + --proxy-port PORT Proxy port that was used to reach the target server + --proxy-user USER|PATTERN|* Proxy user that was configured for this access (mandatory when --proxy-host is specified). + Globbing characters '*' and '?' are supported for pattern matching. This command adds, to an existing bastion account, access to a given server, using the egress keys of the group. The list of eligible servers for a given group is given by ``groupListServers`` @@ -65,22 +72,36 @@ if (not $group or not $ip) { "Missing mandatory parameter 'host' or 'group' (or host didn't resolve correctly)"; } +my $proxyIp; +if ($proxyHost) { + $fnret = + OVH::Bastion::validate_proxy_params(proxyHost => $proxyHost, proxyPort => $proxyPort, proxyUser => $proxyUser); + $fnret or osh_exit $fnret; + $proxyIp = $fnret->value->{'proxyIp'}; + $proxyPort = $fnret->value->{'proxyPort'}; + $proxyUser = $fnret->value->{'proxyUser'}; +} + $fnret = OVH::Bastion::Plugin::ACL::check( - user => $user, - userAny => $userAny, - port => $port, - portAny => $portAny, - scpUp => $scpUp, - scpDown => $scpDown, - sftp => $sftp, - protocol => $protocol, + user => $user, + userAny => $userAny, + port => $port, + portAny => $portAny, + scpUp => $scpUp, + scpDown => $scpDown, + sftp => $sftp, + protocol => $protocol, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser, ); if (!$fnret) { help(); osh_exit($fnret); } -$user = $fnret->value->{'user'}; -$port = $fnret->value->{'port'}; +$user = $fnret->value->{'user'}; +$port = $fnret->value->{'port'}; +$proxyUser = $fnret->value->{'proxyUser'}; $fnret = OVH::Bastion::is_valid_group_and_existing(group => $group, groupType => "key"); $fnret or osh_exit($fnret); @@ -100,10 +121,13 @@ $fnret my @command = qw{ sudo -n -u }; push @command, ($group, '--', '/usr/bin/env', 'perl', '-T', $OVH::Bastion::BASEPATH . '/bin/helper/osh-groupAddServer'); -push @command, '--group', $group; -push @command, '--action', 'del'; -push @command, '--ip', $ip; -push @command, '--user', $user if $user; -push @command, '--port', $port if $port; +push @command, '--group', $group; +push @command, '--action', 'del'; +push @command, '--ip', $ip; +push @command, '--user', $user if $user; +push @command, '--port', $port if $port; +push @command, '--proxy-ip', $proxyIp if $proxyIp; +push @command, '--proxy-port', $proxyPort if $proxyPort; +push @command, '--proxy-user', $proxyUser if $proxyUser; osh_exit OVH::Bastion::helper(cmd => \@command); diff --git a/bin/plugin/group-aclkeeper/groupDelServer.json b/bin/plugin/group-aclkeeper/groupDelServer.json index 94fe0a6..4cfba0a 100644 --- a/bin/plugin/group-aclkeeper/groupDelServer.json +++ b/bin/plugin/group-aclkeeper/groupDelServer.json @@ -4,11 +4,18 @@ "groupDelServer +--group" , {"ac" : [""]}, "groupDelServer +--group +\\S+" , {"ac" : ["--host"]}, "groupDelServer +--group +\\S+ +--host" , {"pr" : ["", "", ""]}, - "groupDelServer +--group +\\S+ +--host +\\S+" , {"ac" : ["--port", "--port-any"]}, - "groupDelServer +--group +\\S+ +--host +\\S+ +--port" , {"pr" : [""]}, - "groupDelServer +--group +\\S+ +--host +\\S+ +--port(-any| +\\d+)" , {"ac" : ["--user", "--user-any"]}, - "groupDelServer +--group +\\S+ +--host +\\S+ +--port(-any| +\\d+) +--user" , {"pr" : [""]}, - "groupDelServer +--group +\\S+ +--host +\\S+ +--port(-any| +\\d+) +--user(-any| +\\S+)" , {"pr" : ["", "--force"]} + "groupDelServer +--group +\\S+ +--host +\\S+" , {"ac" : ["", "--user", "--port"]}, + "groupDelServer +--group +\\S+ +--host +\\S+ +.*--user" , {"pr" : [""]}, + "groupDelServer +--group +\\S+ +--host +\\S+ +.*--port" , {"pr" : [""]}, + "groupDelServer +--group +\\S+ +--host +\\S+ +--user +\\S+" , {"ac" : ["", "--port"]}, + "groupDelServer +--group +\\S+ +--host +\\S+ +--port +\\S+" , {"ac" : ["", "--user"]}, + "groupDelServer +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+" , {"ac" : ["--proxy-host", ""]}, + "groupDelServer +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host" , {"pr" : ["", ""]}, + "groupDelServer +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+" , {"ac" : ["--proxy-port"]}, + "groupDelServer +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port" , {"pr" : [""]}, + "groupDelServer +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port +\\S+" , {"ac" : ["--proxy-user"]}, + "groupDelServer +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port +\\S+ +.*--proxy-user" , {"pr" : [""]}, + "groupDelServer +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port +\\S+ +.*--proxy-user +\\S+" , {"pr" : [""]} ], "master_only": true } diff --git a/bin/plugin/group-gatekeeper/groupAddGuestAccess b/bin/plugin/group-gatekeeper/groupAddGuestAccess index 800d725..ad7c11f 100755 --- a/bin/plugin/group-gatekeeper/groupAddGuestAccess +++ b/bin/plugin/group-gatekeeper/groupAddGuestAccess @@ -15,11 +15,14 @@ my $remainingOptions = OVH::Bastion::Plugin::begin( header => "add access to one server of a group to an account", userAllowWildcards => 1, options => { - "group=s" => \my $group, - "protocol=s" => \my $protocol, - "account=s" => \my $account, - "ttl=s" => \my $ttl, - "comment=s" => \my $comment, + "group=s" => \my $group, + "protocol=s" => \my $protocol, + "account=s" => \my $account, + "ttl=s" => \my $ttl, + "comment=s" => \my $comment, + "proxy-host=s" => \my $proxyHost, + "proxy-port=s" => \my $proxyPort, + "proxy-user=s" => \my $proxyUser, # undocumented/compatibility: "user-any" => \my $userAny, "port-any" => \my $portAny, @@ -32,28 +35,32 @@ Add a specific group server access to an account Usage: --osh SCRIPT_NAME --group GROUP --account ACCOUNT [OPTIONS] - --account ACCOUNT Name of the other bastion account to add access to, they'll be given access to the GROUP key - --group GROUP Group to add the guest access to, note that this group should already have access - to the USER/HOST/PORT tuple you'll specify with the options below. - --host HOST|IP|SUBNET Host(s) to add access to, either a HOST which will be resolved to an IP immediately, - or an IP, or a whole subnet using the PREFIX/SIZE notation - --user USER|PATTERN|* Specify which remote user should be allowed to connect as. - Globbing characters '*' and '?' are supported, so you can specify a pattern - that will be matched against the actual remote user name. - To allow any user, use '--user *' (you might need to escape '*' from your shell) - --port PORT|* Remote port allowed to connect to - To allow any port, use '--port *' (you might need to escape '*' from your shell) - --protocol PROTO Specify that a special protocol should be allowed for this HOST:PORT tuple, note that you - must not specify --user in that case. However, for this protocol to be usable under a given - remote user, access to the USER@HOST:PORT tuple must also be allowed. - PROTO must be one of: - scpupload allow SCP upload, you--bastion-->server - scpdownload allow SCP download, you<--bastion--server - sftp allow usage of the SFTP subsystem, through the bastion - rsync allow usage of rsync, through the bastion - --ttl SECONDS|DURATION Specify a number of seconds after which the access will automatically expire - --comment '"ANY TEXT"' Add a comment alongside this access. Quote it twice as shown if you're under a shell. - If omitted, we'll use the closest preexisting group access' comment as seen in groupListServers + --account ACCOUNT Name of the other bastion account to add access to, they'll be given access to the GROUP key + --group GROUP Group to add the guest access to, note that this group should already have access + to the USER/HOST/PORT tuple you'll specify with the options below. + --host HOST|IP|SUBNET Host(s) to add access to, either a HOST which will be resolved to an IP immediately, + or an IP, or a whole subnet using the PREFIX/SIZE notation + --user USER|PATTERN|* Specify which remote user should be allowed to connect as. + Globbing characters '*' and '?' are supported, so you can specify a pattern + that will be matched against the actual remote user name. + To allow any user, use '--user *' (you might need to escape '*' from your shell) + --port PORT|* Remote port allowed to connect to + To allow any port, use '--port *' (you might need to escape '*' from your shell) + --protocol PROTO Specify that a special protocol should be allowed for this HOST:PORT tuple, note that you + must not specify --user in that case. However, for this protocol to be usable under a given + remote user, access to the USER@HOST:PORT tuple must also be allowed. + PROTO must be one of: + scpupload allow SCP upload, you--bastion-->server + scpdownload allow SCP download, you<--bastion--server + sftp allow usage of the SFTP subsystem, through the bastion + rsync allow usage of rsync, through the bastion + --ttl SECONDS|DURATION Specify a number of seconds after which the access will automatically expire + --comment '"ANY TEXT"' Add a comment alongside this access. Quote it twice as shown if you're under a shell. + If omitted, we'll use the closest preexisting group access' comment as seen in groupListServers + --proxy-host HOST|IP Use this host as a proxy/jump host to reach the target server + --proxy-port PORT Proxy host port to connect to (mandatory when --proxy-host is specified) + --proxy-user USER|PATTERN|* Proxy user to connect as (mandatory when --proxy-host is specified). + Globbing characters '*' and '?' are supported for pattern matching. This command adds, to an existing bastion account, access to the egress keys of a group, but only to accessing one or several given servers, instead of all the servers of this group. @@ -76,22 +83,36 @@ if (not $ip and $host) { "Specified host ($host) didn't resolve correctly, fix your DNS or specify the IP instead"; } +my $proxyIp; +if ($proxyHost) { + $fnret = + OVH::Bastion::validate_proxy_params(proxyHost => $proxyHost, proxyPort => $proxyPort, proxyUser => $proxyUser); + $fnret or osh_exit $fnret; + $proxyIp = $fnret->value->{'proxyIp'}; + $proxyPort = $fnret->value->{'proxyPort'}; + $proxyUser = $fnret->value->{'proxyUser'}; +} + $fnret = OVH::Bastion::Plugin::ACL::check( - user => $user, - userAny => $userAny, - port => $port, - portAny => $portAny, - scpUp => $scpUp, - scpDown => $scpDown, - sftp => $sftp, - protocol => $protocol, + user => $user, + userAny => $userAny, + port => $port, + portAny => $portAny, + scpUp => $scpUp, + scpDown => $scpDown, + sftp => $sftp, + protocol => $protocol, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser, ); if (!$fnret) { help(); osh_exit($fnret); } -$user = $fnret->value->{'user'}; -$port = $fnret->value->{'port'}; +$user = $fnret->value->{'user'}; +$port = $fnret->value->{'port'}; +$proxyUser = $fnret->value->{'proxyUser'}; if (defined $ttl) { $fnret = OVH::Bastion::is_valid_ttl(ttl => $ttl); @@ -108,6 +129,9 @@ $fnret = OVH::Bastion::Plugin::groupSetRole::act( user => $user, port => $port, host => ($ip || $host), + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser, ttl => $ttl, comment => $comment, sudo => 0, diff --git a/bin/plugin/group-gatekeeper/groupAddGuestAccess.json b/bin/plugin/group-gatekeeper/groupAddGuestAccess.json index 2a9654c..0467181 100644 --- a/bin/plugin/group-gatekeeper/groupAddGuestAccess.json +++ b/bin/plugin/group-gatekeeper/groupAddGuestAccess.json @@ -11,7 +11,15 @@ "groupAddGuestAccess +--account +\\S+ +--group +\\S+ +--host +\\S+ +.*--port" , {"pr" : [""]}, "groupAddGuestAccess +--account +\\S+ +--group +\\S+ +--host +\\S+ +--user +\\S+" , {"ac" : ["", "--port"]}, "groupAddGuestAccess +--account +\\S+ +--group +\\S+ +--host +\\S+ +--port +\\S+" , {"ac" : ["", "--user"]}, - "groupAddGuestAccess +--account +\\S+ +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+" , {"pr" : [""]} + "groupAddGuestAccess +--account +\\S+ +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+" , {"ac" : ["--proxy-host", "--ttl", "--comment", ""]}, + "groupAddGuestAccess +--account +\\S+ +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host" , {"pr" : ["", ""]}, + "groupAddGuestAccess +--account +\\S+ +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+" , {"ac" : ["--proxy-port"]}, + "groupAddGuestAccess +--account +\\S+ +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port" , {"pr" : [""]}, + "groupAddGuestAccess +--account +\\S+ +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port +\\S+" , {"ac" : ["--proxy-user"]}, + "groupAddGuestAccess +--account +\\S+ +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port +\\S+ +.*--proxy-user" , {"pr" : [""]}, + "groupAddGuestAccess +--account +\\S+ +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port +\\S+ +.*--proxy-user +\\S+" , {"ac" : ["--ttl", "--comment", ""]}, + "groupAddGuestAccess +--account +\\S+ +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--ttl" , {"pr" : [""]}, + "groupAddGuestAccess +--account +\\S+ +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--comment" , {"pr" : [""]} ], "master_only": true } diff --git a/bin/plugin/group-gatekeeper/groupDelGuestAccess b/bin/plugin/group-gatekeeper/groupDelGuestAccess index 2966faa..9320397 100755 --- a/bin/plugin/group-gatekeeper/groupDelGuestAccess +++ b/bin/plugin/group-gatekeeper/groupDelGuestAccess @@ -15,9 +15,12 @@ my $remainingOptions = OVH::Bastion::Plugin::begin( header => "remove access from one server of a group from an account", userAllowWildcards => 1, options => { - "group=s" => \my $group, - "protocol=s" => \my $protocol, - "account=s" => \my $account, + "group=s" => \my $group, + "protocol=s" => \my $protocol, + "account=s" => \my $account, + "proxy-host=s" => \my $proxyHost, + "proxy-port=s" => \my $proxyPort, + "proxy-user=s" => \my $proxyUser, # undocumented/compatibility: "user-any" => \my $userAny, "port-any" => \my $portAny, @@ -30,24 +33,28 @@ Remove a specific group server access from an account Usage: --osh SCRIPT_NAME --group GROUP --account ACCOUNT [OPTIONS] - --account ACCOUNT Bastion account remove the guest access from - --group GROUP Specify which group to remove the guest access to ACCOUNT from - --host HOST|IP|SUBNET Host(s) to remove access from, either a HOST which will be resolved to an IP immediately, - or an IP, or a whole subnet using the PREFIX/SIZE notation - --user USER|PATTERN|* Specify which remote user was allowed to connect as. - Globbing characters '*' and '?' are supported, so you can specify a pattern - that will be matched against the actual remote user name. - If any user was allowed, use '--user *' (you might need to escape '*' from your shell) - --port PORT|* Remote port that was allowed to connect to - If any user was allowed, use '--port *' (you might need to escape '*' from your shell) - --protocol PROTO Specify that a special protocol was allowed for this HOST:PORT tuple, note that you - must not specify --user in that case. However, for this protocol to be usable under a given - remote user, access to the USER@HOST:PORT tuple must also be allowed. - PROTO must be one of: - scpupload allow SCP upload, you--bastion-->server - scpdownload allow SCP download, you<--bastion--server - sftp allow usage of the SFTP subsystem, through the bastion - rsync allow usage of rsync, through the bastion + --account ACCOUNT Bastion account remove the guest access from + --group GROUP Specify which group to remove the guest access to ACCOUNT from + --host HOST|IP|SUBNET Host(s) to remove access from, either a HOST which will be resolved to an IP immediately, + or an IP, or a whole subnet using the PREFIX/SIZE notation + --user USER|PATTERN|* Specify which remote user was allowed to connect as. + Globbing characters '*' and '?' are supported, so you can specify a pattern + that will be matched against the actual remote user name. + If any user was allowed, use '--user *' (you might need to escape '*' from your shell) + --port PORT|* Remote port that was allowed to connect to + If any user was allowed, use '--port *' (you might need to escape '*' from your shell) + --protocol PROTO Specify that a special protocol was allowed for this HOST:PORT tuple, note that you + must not specify --user in that case. However, for this protocol to be usable under a given + remote user, access to the USER@HOST:PORT tuple must also be allowed. + PROTO must be one of: + scpupload allow SCP upload, you--bastion-->server + scpdownload allow SCP download, you<--bastion--server + sftp allow usage of the SFTP subsystem, through the bastion + rsync allow usage of rsync, through the bastion + --proxy-host HOST|IP Use this host as a proxy/jump host to reach the target server + --proxy-port PORT Proxy host port to connect to (mandatory when --proxy-host is specified) + --proxy-user USER|PATTERN|* Proxy user to connect as (mandatory when --proxy-host is specified). + Globbing characters '*' and '?' are supported for pattern matching. This command removes, from an existing bastion account, access to a given server, using the egress keys of the group. The list of such servers is given by ``groupListGuestAccesses`` @@ -69,22 +76,36 @@ if (not $ip and $host) { "Specified host ($host) didn't resolve correctly, fix your DNS or specify the IP instead"; } +my $proxyIp; +if ($proxyHost) { + $fnret = + OVH::Bastion::validate_proxy_params(proxyHost => $proxyHost, proxyPort => $proxyPort, proxyUser => $proxyUser); + $fnret or osh_exit $fnret; + $proxyIp = $fnret->value->{'proxyIp'}; + $proxyPort = $fnret->value->{'proxyPort'}; + $proxyUser = $fnret->value->{'proxyUser'}; +} + $fnret = OVH::Bastion::Plugin::ACL::check( - user => $user, - userAny => $userAny, - port => $port, - portAny => $portAny, - scpUp => $scpUp, - scpDown => $scpDown, - sftp => $sftp, - protocol => $protocol, + user => $user, + userAny => $userAny, + port => $port, + portAny => $portAny, + scpUp => $scpUp, + scpDown => $scpDown, + sftp => $sftp, + protocol => $protocol, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser, ); if (!$fnret) { help(); osh_exit($fnret); } -$user = $fnret->value->{'user'}; -$port = $fnret->value->{'port'}; +$user = $fnret->value->{'user'}; +$port = $fnret->value->{'port'}; +$proxyUser = $fnret->value->{'proxyUser'}; $fnret = OVH::Bastion::Plugin::groupSetRole::act( account => $account, @@ -94,6 +115,9 @@ $fnret = OVH::Bastion::Plugin::groupSetRole::act( user => $user, port => $port, host => ($ip || $host), + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser, sudo => 0, silentoverride => 0, self => $self, diff --git a/bin/plugin/group-gatekeeper/groupDelGuestAccess.json b/bin/plugin/group-gatekeeper/groupDelGuestAccess.json index 024cc6b..8a8ce6c 100644 --- a/bin/plugin/group-gatekeeper/groupDelGuestAccess.json +++ b/bin/plugin/group-gatekeeper/groupDelGuestAccess.json @@ -11,7 +11,13 @@ "groupDelGuestAccess +--account +\\S+ +--group +\\S+ +--host +\\S+ +.*--port" , {"pr" : [""]}, "groupDelGuestAccess +--account +\\S+ +--group +\\S+ +--host +\\S+ +--user +\\S+" , {"ac" : ["", "--port"]}, "groupDelGuestAccess +--account +\\S+ +--group +\\S+ +--host +\\S+ +--port +\\S+" , {"ac" : ["", "--user"]}, - "groupDelGuestAccess +--account +\\S+ +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+" , {"pr" : [""]} + "groupDelGuestAccess +--account +\\S+ +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+" , {"ac" : ["--proxy-host", ""]}, + "groupDelGuestAccess +--account +\\S+ +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host" , {"pr" : ["", ""]}, + "groupDelGuestAccess +--account +\\S+ +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+" , {"ac" : ["--proxy-port"]}, + "groupDelGuestAccess +--account +\\S+ +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port" , {"pr" : [""]}, + "groupDelGuestAccess +--account +\\S+ +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port +\\S+" , {"ac" : ["--proxy-user"]}, + "groupDelGuestAccess +--account +\\S+ +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port +\\S+ +.*--proxy-user" , {"pr" : [""]}, + "groupDelGuestAccess +--account +\\S+ +--group +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port +\\S+ +.*--proxy-user +\\S+" , {"pr" : [""]} ], "master_only": true } diff --git a/bin/plugin/open/scp b/bin/plugin/open/scp index b2d01c1..7335984 100755 --- a/bin/plugin/open/scp +++ b/bin/plugin/open/scp @@ -23,8 +23,11 @@ my ($scpCmd); my $remainingOptions = OVH::Bastion::Plugin::begin( argv => \@ARGV, header => undef, - options => {'scp-cmd=s' => \$scpCmd}, - help => \&help, + options => { + 'scp-cmd=s' => \$scpCmd, + 'proxy-jump=s' => \my $proxyJump, + }, + help => \&help, ); sub help { @@ -65,10 +68,11 @@ REMOTE_HOST="" REMOTE_PORT=22 REMOTE_USER="" REMOTE_PATH="" +PROXY_JUMP="" usage() { cat >&2 <&2 + exit 1 + fi + PROXY_JUMP="$2" + shift 2;; "-P"|"-l") if [ -z "${2:-}" ]; then echo "scpwrapper: missing argument after '$1'" >&2 @@ -170,7 +181,11 @@ fi # shellcheck disable=SC2086 [ "$BASTION_SCP_DEBUG" = 1 ] && set -x -$BASTION_CMD -t $BASTION_SSH_EXTRA_ARGS -- --osh scp --host "$REMOTE_HOST" --port "$REMOTE_PORT" --user "$REMOTE_USER" --generate-mfa-token | tee "$t" +if [ -n "$PROXY_JUMP" ]; then + $BASTION_CMD -t $BASTION_SSH_EXTRA_ARGS -- --osh scp --host "$REMOTE_HOST" --port "$REMOTE_PORT" --user "$REMOTE_USER" -J "$PROXY_JUMP" --generate-mfa-token | tee "$t" +else + $BASTION_CMD -t $BASTION_SSH_EXTRA_ARGS -- --osh scp --host "$REMOTE_HOST" --port "$REMOTE_PORT" --user "$REMOTE_USER" --generate-mfa-token | tee "$t" +fi [ "$BASTION_SCP_DEBUG" = 1 ] && set +x token=$(grep -Eo '^MFA_TOKEN=[a-zA-Z0-9,]+' "$t" | tail -n 1 | cut -d= -f2) @@ -229,7 +244,11 @@ scpcmd=$(echo "$3" | sed -e 's/#/##/g;s/ /#/g') # and go [ "$BASTION_SCP_DEBUG" = 1 ] && set -x EOF -echo "exec $BASTION_CMD -T \$sshcmdline $BASTION_SSH_EXTRA_ARGS -- --user \"\$REMOTE_USER\" --port \"\$REMOTE_PORT\" --host \"\$REMOTE_HOST\" --osh scp --scp-cmd \"\$scpcmd\" --mfa-token $token" >> "$t" +if [ -n "$PROXY_JUMP" ]; then + echo "exec $BASTION_CMD -T \$sshcmdline $BASTION_SSH_EXTRA_ARGS -- --user \"\$REMOTE_USER\" --port \"\$REMOTE_PORT\" --host \"\$REMOTE_HOST\" -J \"$PROXY_JUMP\" --osh scp --proxy-jump \"$PROXY_JUMP\" --scp-cmd \"\$scpcmd\" --mfa-token $token" >> "$t" +else + echo "exec $BASTION_CMD -T \$sshcmdline $BASTION_SSH_EXTRA_ARGS -- --user \"\$REMOTE_USER\" --port \"\$REMOTE_PORT\" --host \"\$REMOTE_HOST\" --osh scp --scp-cmd \"\$scpcmd\" --mfa-token $token" >> "$t" +fi chmod +x "$t" # don't use exec below, because we need the trap to be executed on exit @@ -290,6 +309,40 @@ EOF # code # my $fnret; +my $proxyIp = undef; +my $proxyPort = 22; +my $proxyUser = undef; +# Parse proxyjump args if specified +if ($proxyJump) { + if ($proxyJump =~ /^(?:([a-zA-Z0-9._@!-]{1,128})@)?(\[?[a-zA-Z0-9._-]+\]?)(?::(\d+))?$/) { + $proxyUser = $1 if $1; + $proxyIp = $2; + $proxyPort = $3 if $3; + osh_debug("parsed proxyjump: host=$proxyIp port=$proxyPort user=$proxyUser"); + } + else { + osh_exit 'ERR_INVALID_PARAMETER', "Invalid proxyjump specification '$proxyJump', should be [user@]host[:port]"; + } + + $fnret = OVH::Bastion::get_ip(host => $proxyIp, allowSubnets => 0); + if (!$fnret) { + if ($fnret->err eq 'ERR_DNS_DISABLED') { + osh_exit 'ERR_DNS_DISABLED', $fnret->msg; + } + elsif ($fnret->err eq 'ERR_IP_VERSION_DISABLED') { + osh_exit 'ERR_IP_VERSION_DISABLED', $fnret->msg; + } + else { + osh_exit 'ERR_HOST_NOT_FOUND', $fnret->msg; + } + } + $proxyIp = $fnret->value->{'ip'}; + osh_debug("Proxyjump host resolved to IP: $proxyIp"); + + if ($proxyUser && !OVH::Bastion::is_valid_remote_user(user => $proxyUser, allowWildcards => 0)) { + osh_exit 'EXIT_INVALID_REMOTE_USER', 'invalid_proxy_user', "Proxy user name '$proxyUser' seems invalid"; + } +} if (not $host) { help(); @@ -320,13 +373,19 @@ if ($decoded =~ m{[\`\$\;><\|\&]}) { $port ||= 22; # scp uses 22 if not specified, so we need to test access to that port and not any port (aka undef) $user ||= $self; # same for user +if ($proxyIp) { + $proxyUser ||= $user; +} $fnret = OVH::Bastion::Plugin::otherProtocol::has_protocol_access( - account => $self, - user => $user, - ip => $ip, - port => $port, - protocol => $protocol, + account => $self, + user => $user, + ip => $ip, + port => $port, + protocol => $protocol, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser, ); $fnret or osh_exit($fnret); @@ -342,6 +401,27 @@ foreach my $key (@keys) { push @cmd, ('-i', $key); } +# Add ProxyCommand if proxy is specified +if ($proxyIp) { + # Build ProxyCommand similar to how osh.pl does it + my @proxyCommand = ('ssh'); + push @proxyCommand, '-o', 'ForwardAgent=no', '-o', 'PermitLocalCommand=no', '-o', 'ClearAllForwardings=yes'; + + # Add the same SSH keys to the proxy command + foreach my $key (@keys) { + push @proxyCommand, ('-i', $key); + } + + push @proxyCommand, '-p', $proxyPort if $proxyPort && $proxyPort != 22; + push @proxyCommand, '-l', $proxyUser, '-W', '%h:%p', $proxyIp; + + # Quote arguments that contain spaces and build the command string + my $proxyCommandStr = join(' ', map { /\s/ ? "'$_'" : $_ } @proxyCommand); + push @cmd, '-o', "ProxyCommand=$proxyCommandStr"; + + osh_debug("ProxyCommand: $proxyCommandStr"); +} + push @cmd, "--", $ip, $decoded; print STDERR ">>> Hello $self, transferring your file through the bastion " diff --git a/bin/plugin/open/selfListSessions b/bin/plugin/open/selfListSessions index 974095c..03fcc5f 100755 --- a/bin/plugin/open/selfListSessions +++ b/bin/plugin/open/selfListSessions @@ -14,18 +14,21 @@ my $remainingOptions = OVH::Bastion::Plugin::begin( argv => \@ARGV, header => "your past sessions list", options => { - "detailed" => \my $detailed, - "id=s" => \my $id, - "type=s" => \my $type, - "allowed" => \my $allowed, - "denied" => \my $denied, - "after=s" => \my $after, - "before=s" => \my $before, - "from=s" => \my $from, - "via=s" => \my $via, - "via-port=i" => \my $viaPort, - "to-port=i" => \my $toPort, - "limit=i" => \my $limit, + "detailed" => \my $detailed, + "id=s" => \my $id, + "type=s" => \my $type, + "allowed" => \my $allowed, + "denied" => \my $denied, + "after=s" => \my $after, + "before=s" => \my $before, + "from=s" => \my $from, + "via=s" => \my $via, + "via-port=i" => \my $viaPort, + "to-port=i" => \my $toPort, + "limit=i" => \my $limit, + "proxy-user=s" => \my $proxyUser, + "proxy-ip=s" => \my $proxyIp, + "proxy-port=i" => \my $proxyPort, }, helptext => <<'EOF', List the few past sessions of your account @@ -47,6 +50,9 @@ Usage: --osh SCRIPT_NAME [OPTIONS] --user USER Only sessions connecting using remote USER --via HOST Only sessions that connected through bastion IP HOST --via-port PORT Only sessions that connected through bastion PORT + --proxy-user USER Only sessions that used proxy USER + --proxy-ip HOST Only sessions that used proxy IP + --proxy-port PORT Only sessions that used proxy PORT Note that only the sessions that happened on this precise bastion instance will be shown, not the sessions from its possible cluster siblings. @@ -95,7 +101,10 @@ $fnret = OVH::Bastion::log_access_get( bastionport => $viaPort, toPort => $toPort, user => $user, - limit => $limit + limit => $limit, + proxyuser => $proxyUser, + proxyip => $proxyIp, + proxyport => $proxyPort, ); $fnret or osh_exit $fnret; @@ -148,6 +157,12 @@ else { $r->{user} || $r->{ipto} || $r->{portto} || $r->{hostto} ? sprintf(' to %s@%s:%s(%s)', $r->{'user'}, $r->{'ipto'}, $r->{'portto'}, $r->{'hostto'}) : ''; + + if ($r->{proxyip}) { + $to .= sprintf(' via proxy %s@%s:%s(%s)', + $r->{'proxyuser'}, $r->{'proxyip'}, $r->{'proxyport'}, $r->{'proxyhost'}); + } + $r->{params} = undef if ($r->{cmdtype} ne 'osh'); $r->{returnvalue} = $r->{comment} if $r->{returnvalue} < 0; @@ -177,6 +192,7 @@ else { from => {ip => $r->{ipfrom}, host => $r->{hostfrom}, port => $r->{portfrom}}, via => {ip => $r->{bastionip}, port => $r->{bastionport}, user => $r->{account}}, to => {ip => $r->{ipto}, port => $r->{portto}, host => $r->{hostto}}, + proxy => {ip => $r->{proxyip}, port => $r->{proxyport}, host => $r->{proxyhost}}, timestamp_started => $r->{timestamp} + $r->{timestampusec} / 1_000_000, timestamp_ended => $r->{timestampend} + $r->{timestampendusec} / 1_000_000, type => $r->{cmdtype}, diff --git a/bin/plugin/restricted/accountAddPersonalAccess b/bin/plugin/restricted/accountAddPersonalAccess index feb7d19..21760dd 100755 --- a/bin/plugin/restricted/accountAddPersonalAccess +++ b/bin/plugin/restricted/accountAddPersonalAccess @@ -21,6 +21,9 @@ my $remainingOptions = OVH::Bastion::Plugin::begin( "force-password=s" => \my $forcePassword, "ttl=s" => \my $ttl, "comment=s" => \my $comment, + "proxy-host=s" => \my $proxyHost, + "proxy-port=s" => \my $proxyPort, + "proxy-user=s" => \my $proxyUser, # undocumented/compatibility: "user-any" => \my $userAny, "port-any" => \my $portAny, @@ -54,6 +57,10 @@ Usage: --osh SCRIPT_NAME --account ACCOUNT --host HOST --user USER --port PORT [ --force-password HASH Only use the password with the specified hash to connect to the server (cf accountListPasswords) --ttl SECONDS|DURATION Specify a number of seconds (or a duration string, such as "1d7h8m") after which the access will automatically expire --comment "'ANY TEXT'" Add a comment alongside this server. Quote it twice as shown if you're under a shell. + --proxy-host HOST|IP Use this host as a proxy/jump host to reach the target server + --proxy-port PORT Proxy host port to connect to (mandatory when --proxy-host is specified) + --proxy-user USER|PATTERN|* Proxy user to connect as (mandatory when --proxy-host is specified). + Globbing characters '*' and '?' are supported for pattern matching. The access will work only if one of the account's personal egress public key has been copied to the remote server. To get the list of an account's personal egress public keys, see ``accountListEgressKeyss`` and ``selfListEgressKeys``. @@ -94,22 +101,36 @@ if (!$ip) { osh_exit 'ERR_MISSING_PARAMETER', "Missing parameter 'host' or didn't resolve correctly"; } +my $proxyIp; +if ($proxyHost) { + $fnret = + OVH::Bastion::validate_proxy_params(proxyHost => $proxyHost, proxyPort => $proxyPort, proxyUser => $proxyUser); + $fnret or osh_exit $fnret; + $proxyIp = $fnret->value->{'proxyIp'}; + $proxyPort = $fnret->value->{'proxyPort'}; + $proxyUser = $fnret->value->{'proxyUser'}; +} + $fnret = OVH::Bastion::Plugin::ACL::check( - user => $user, - userAny => $userAny, - port => $port, - portAny => $portAny, - scpUp => $scpUp, - scpDown => $scpDown, - sftp => $sftp, - protocol => $protocol, + user => $user, + userAny => $userAny, + port => $port, + portAny => $portAny, + scpUp => $scpUp, + scpDown => $scpDown, + sftp => $sftp, + protocol => $protocol, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser, ); if (!$fnret) { help(); osh_exit($fnret); } -$user = $fnret->value->{'user'}; -$port = $fnret->value->{'port'}; +$user = $fnret->value->{'user'}; +$port = $fnret->value->{'port'}; +$proxyUser = $fnret->value->{'proxyUser'}; if (defined $ttl) { $fnret = OVH::Bastion::is_valid_ttl(ttl => $ttl); @@ -155,6 +176,14 @@ if ($pluginConfig && $pluginConfig->{'self_remote_user_only'}) { } } +if ($pluginConfig && $pluginConfig->{'self_remote_user_only'}) { + if ($proxyIp && (!$proxyUser || $proxyUser ne $account)) { + osh_exit('ERR_INVALID_PARAMETER', + msg => "This bastion policy forces the remote user of personal accesses to match\n" + . "the account name: you may retry with --proxy-user $account"); + } +} + # if no comment is specified, but we're adding the server by hostname, # use it to craft a comment if (!$comment && $host ne $ip) { @@ -176,6 +205,9 @@ $fnret = OVH::Bastion::access_modify( forcePassword => $forcePassword, comment => $comment, widestV4Prefix => ($pluginConfig ? $pluginConfig->{'widest_v4_prefix'} : undef), + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser, ); $fnret or osh_exit($fnret); @@ -194,5 +226,8 @@ push @command, '--force-key', $forceKey if $forceKey; push @command, '--force-password', $forcePassword if $forcePassword; push @command, '--ttl', $ttl if $ttl; push @command, '--comment', $comment if $comment; +push @command, '--proxy-ip', $proxyIp if $proxyIp; +push @command, '--proxy-port', $proxyPort if $proxyPort; +push @command, '--proxy-user', $proxyUser if $proxyUser; osh_exit OVH::Bastion::helper(cmd => \@command); diff --git a/bin/plugin/restricted/accountAddPersonalAccess.json b/bin/plugin/restricted/accountAddPersonalAccess.json index 5b5a559..630c748 100644 --- a/bin/plugin/restricted/accountAddPersonalAccess.json +++ b/bin/plugin/restricted/accountAddPersonalAccess.json @@ -9,17 +9,17 @@ "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +.*--port" , {"pr" : [""]}, "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--user +\\S+" , {"ac" : ["", "--port"]}, "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--port +\\S+" , {"ac" : ["", "--user"]}, - "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+" , {"ac" : ["--force-key","--force-password","--ttl",""]}, - "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +--ttl" , {"pr" : [""]}, - "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +--ttl \\S+" , {"ac" : ["--force-key","--force-password",""]}, - "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +--ttl \\S+ --force-key" , {"pr" : [""]}, - "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +--ttl \\S+ --force-password" , {"pr" : [""]}, - "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +--ttl \\S+ --force-(key|password) \\S+" , {"pr" : [""]}, - "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +--force-key" , {"pr" : [""]}, - "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +--force-password" , {"pr" : [""]}, - "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +--force-(key|password) +\\S+" , {"ac" : ["--ttl",""]}, - "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +--force-(key|password) +\\S+ +--ttl" , {"pr" : [""]}, - "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +--force-(key|password) +\\S+ +--ttl +\\S+" , {"pr" : [""]} + "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+" , {"ac" : ["--proxy-host", "--force-key", "--force-password", "--ttl", "--comment", ""]}, + "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host" , {"pr" : ["", ""]}, + "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+" , {"ac" : ["--proxy-port"]}, + "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port" , {"pr" : [""]}, + "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port +\\S+" , {"ac" : ["--proxy-user"]}, + "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port +\\S+ +.*--proxy-user" , {"pr" : [""]}, + "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port +\\S+ +.*--proxy-user +\\S+" , {"ac" : ["--force-key", "--force-password", "--ttl", "--comment", ""]}, + "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--ttl" , {"pr" : [""]}, + "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--force-key" , {"pr" : [""]}, + "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--force-password" , {"pr" : [""]}, + "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--comment" , {"pr" : [""]} ], "master_only": true } diff --git a/bin/plugin/restricted/accountDelPersonalAccess b/bin/plugin/restricted/accountDelPersonalAccess index 5f08ed7..c6cdb99 100755 --- a/bin/plugin/restricted/accountDelPersonalAccess +++ b/bin/plugin/restricted/accountDelPersonalAccess @@ -14,8 +14,11 @@ my $remainingOptions = OVH::Bastion::Plugin::begin( header => "removing personal access to a server from an account", userAllowWildcards => 1, options => { - "account=s" => \my $account, - "protocol=s" => \my $protocol, + "account=s" => \my $account, + "protocol=s" => \my $protocol, + "proxy-host=s" => \my $proxyHost, + "proxy-port=s" => \my $proxyPort, + "proxy-user=s" => \my $proxyUser, # undocumented/compatibility: "user-any" => \my $userAny, "port-any" => \my $portAny, @@ -44,6 +47,10 @@ Usage: --osh SCRIPT_NAME --account ACCOUNT --host HOST --user USER --port PORT [ scpdownload allow SCP download, you<--bastion--server sftp allow usage of the SFTP subsystem, through the bastion rsync allow usage of rsync, through the bastion + --proxy-host HOST|IP Specify which host was used as a proxy/jump host to reach the target server + --proxy-port PORT Proxy port that was used to reach the target server + --proxy-user USER|PATTERN|* Proxy user that was configured for this access (mandatory when --proxy-host is specified). + Globbing characters '*' and '?' are supported for pattern matching. EOF ); @@ -54,22 +61,36 @@ if (!$ip) { osh_exit 'ERR_MISSING_PARAMETER', "Missing parameter 'host' or didn't resolve correctly"; } +my $proxyIp; +if ($proxyHost) { + $fnret = + OVH::Bastion::validate_proxy_params(proxyHost => $proxyHost, proxyPort => $proxyPort, proxyUser => $proxyUser); + $fnret or osh_exit $fnret; + $proxyIp = $fnret->value->{'proxyIp'}; + $proxyPort = $fnret->value->{'proxyPort'}; + $proxyUser = $fnret->value->{'proxyUser'}; +} + $fnret = OVH::Bastion::Plugin::ACL::check( - user => $user, - userAny => $userAny, - port => $port, - portAny => $portAny, - scpUp => $scpUp, - scpDown => $scpDown, - sftp => $sftp, - protocol => $protocol, + user => $user, + userAny => $userAny, + port => $port, + portAny => $portAny, + scpUp => $scpUp, + scpDown => $scpDown, + sftp => $sftp, + protocol => $protocol, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser, ); if (!$fnret) { help(); osh_exit($fnret); } -$user = $fnret->value->{'user'}; -$port = $fnret->value->{'port'}; +$user = $fnret->value->{'user'}; +$port = $fnret->value->{'port'}; +$proxyUser = $fnret->value->{'proxyUser'}; if (not $account) { help(); @@ -82,11 +103,14 @@ $account = $fnret->value->{'account'}; my @command = qw{ sudo -n -u allowkeeper -- /usr/bin/env perl -T }; push @command, $OVH::Bastion::BASEPATH . '/bin/helper/osh-accountModifyPersonalAccess'; -push @command, '--target', 'any'; -push @command, '--action', 'del'; -push @command, '--account', $account; -push @command, '--ip', $ip; -push @command, '--user', $user if $user; -push @command, '--port', $port if $port; +push @command, '--target', 'any'; +push @command, '--action', 'del'; +push @command, '--account', $account; +push @command, '--ip', $ip; +push @command, '--user', $user if $user; +push @command, '--port', $port if $port; +push @command, '--proxy-ip', $proxyIp if $proxyIp; +push @command, '--proxy-port', $proxyPort if $proxyPort; +push @command, '--proxy-user', $proxyUser if $proxyUser; osh_exit OVH::Bastion::helper(cmd => \@command); diff --git a/bin/plugin/restricted/accountDelPersonalAccess.json b/bin/plugin/restricted/accountDelPersonalAccess.json index 2d1118f..68b3077 100644 --- a/bin/plugin/restricted/accountDelPersonalAccess.json +++ b/bin/plugin/restricted/accountDelPersonalAccess.json @@ -9,7 +9,13 @@ "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +.*--port" , {"pr" : [""]}, "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +--user +\\S+" , {"ac" : ["", "--port"]}, "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +--port +\\S+" , {"ac" : ["", "--user"]}, - "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+" , {"pr" : [""]} + "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+" , {"ac" : ["--proxy-host", ""]}, + "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host" , {"pr" : ["", ""]}, + "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+" , {"ac" : ["--proxy-port"]}, + "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port" , {"pr" : [""]}, + "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port +\\S+" , {"ac" : ["--proxy-user"]}, + "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port +\\S+ +.*--proxy-user" , {"pr" : [""]}, + "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port +\\S+ +.*--proxy-user +\\S+" , {"pr" : [""]} ], "master_only": true } diff --git a/bin/plugin/restricted/selfAddPersonalAccess b/bin/plugin/restricted/selfAddPersonalAccess index e8f5dd4..252109e 100755 --- a/bin/plugin/restricted/selfAddPersonalAccess +++ b/bin/plugin/restricted/selfAddPersonalAccess @@ -21,6 +21,9 @@ my $remainingOptions = OVH::Bastion::Plugin::begin( "force" => \my $force, "ttl=s" => \my $ttl, "comment=s" => \my $comment, + "proxy-host=s" => \my $proxyHost, + "proxy-port=s" => \my $proxyPort, + "proxy-user=s" => \my $proxyUser, # undocumented/compatibility: "user-any" => \my $userAny, "port-any" => \my $portAny, @@ -54,6 +57,11 @@ Usage: --osh SCRIPT_NAME --host HOST --user USER --port PORT [OPTIONS] --force-password HASH Only use the password with the specified hash to connect to the server (cf selfListPasswords) --ttl SECONDS|DURATION Specify a number of seconds (or a duration string, such as "1d7h8m") after which the access will automatically expire --comment "'ANY TEXT'" Add a comment alongside this server. Quote it twice as shown if you're under a shell. + --proxy-host HOST|IP Use this host as a proxy/jump host to reach the target server + --proxy-port PORT Proxy host port to connect to (mandatory when --proxy-host is specified) + --proxy-user USER|PATTERN|* Proxy user to connect as (mandatory when --proxy-host is specified). + Globbing characters '*' and '?' are supported for pattern matching. + When connecting via SSH (not plugins), defaults to --user value for convenience. EOF ); @@ -90,22 +98,36 @@ if (!$ip) { osh_exit 'ERR_MISSING_PARAMETER', "Missing parameter 'host' or didn't resolve correctly"; } +my $proxyIp; +if ($proxyHost) { + $fnret = + OVH::Bastion::validate_proxy_params(proxyHost => $proxyHost, proxyPort => $proxyPort, proxyUser => $proxyUser); + $fnret or osh_exit $fnret; + $proxyIp = $fnret->value->{'proxyIp'}; + $proxyPort = $fnret->value->{'proxyPort'}; + $proxyUser = $fnret->value->{'proxyUser'}; +} + $fnret = OVH::Bastion::Plugin::ACL::check( - user => $user, - userAny => $userAny, - port => $port, - portAny => $portAny, - scpUp => $scpUp, - scpDown => $scpDown, - sftp => $sftp, - protocol => $protocol, + user => $user, + userAny => $userAny, + port => $port, + portAny => $portAny, + scpUp => $scpUp, + scpDown => $scpDown, + sftp => $sftp, + protocol => $protocol, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser, ); if (!$fnret) { help(); osh_exit($fnret); } -$user = $fnret->value->{'user'}; -$port = $fnret->value->{'port'}; +$user = $fnret->value->{'user'}; +$port = $fnret->value->{'port'}; +$proxyUser = $fnret->value->{'proxyUser'}; if (defined $ttl) { $fnret = OVH::Bastion::is_valid_ttl(ttl => $ttl); @@ -142,6 +164,14 @@ if ($pluginConfig && $pluginConfig->{'self_remote_user_only'}) { } } +if ($pluginConfig && $pluginConfig->{'self_remote_user_only'}) { + if ($proxyIp && (!$proxyUser || $proxyUser ne $self)) { + osh_exit('ERR_INVALID_PARAMETER', + msg => "This bastion policy forces the remote user of personal accesses to match\n" + . "the account name: you may retry with --proxy-user $self"); + } +} + # if no comment is specified, but we're adding the server by hostname, # use it to craft a comment if (!$comment && $host ne $ip) { @@ -162,6 +192,9 @@ $fnret = OVH::Bastion::access_modify( forceKey => $forceKey, forcePassword => $forcePassword, comment => $comment, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser, widestV4Prefix => ($pluginConfig ? $pluginConfig->{'widest_v4_prefix'} : undef), ); $fnret or osh_exit($fnret); @@ -173,7 +206,10 @@ if (not $force) { port => $port, ip => $ip, forceKey => $forceKey, - forcePassword => $forcePassword + forcePassword => $forcePassword, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser, ); if ($fnret->is_ok and $fnret->err ne 'OK') { # we have something to say, say it @@ -200,5 +236,8 @@ push @command, '--force-key', $forceKey if $forceKey; push @command, '--force-password', $forcePassword if $forcePassword; push @command, '--ttl', $ttl if $ttl; push @command, '--comment', $comment if $comment; +push @command, '--proxy-ip', $proxyIp if $proxyIp; +push @command, '--proxy-port', $proxyPort if $proxyPort; +push @command, '--proxy-user', $proxyUser if $proxyUser; osh_exit OVH::Bastion::helper(cmd => \@command); diff --git a/bin/plugin/restricted/selfAddPersonalAccess.json b/bin/plugin/restricted/selfAddPersonalAccess.json index 47ecc48..a6052eb 100644 --- a/bin/plugin/restricted/selfAddPersonalAccess.json +++ b/bin/plugin/restricted/selfAddPersonalAccess.json @@ -7,17 +7,17 @@ "selfAddPersonalAccess +--host +\\S+ +.*--port" , {"pr" : [""]}, "selfAddPersonalAccess +--host +\\S+ +--user +\\S+" , {"ac" : ["", "--port"]}, "selfAddPersonalAccess +--host +\\S+ +--port +\\S+" , {"ac" : ["", "--user"]}, - "selfAddPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+" , {"ac" : ["--force-key","--force-password","--ttl",""]}, - "selfAddPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +--ttl" , {"pr" : [""]}, - "selfAddPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +--ttl +\\S+" , {"ac" : ["--force-key","--force-password",""]}, - "selfAddPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +--ttl +\\S+ --force-key" , {"pr" : [""]}, - "selfAddPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +--ttl +\\S+ --force-password" , {"pr" : [""]}, - "selfAddPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +--ttl +\\S+ --force-(key|password) \\S+" , {"pr" : [""]}, - "selfAddPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +--force-key" , {"pr" : [""]}, - "selfAddPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +--force-password" , {"pr" : [""]}, - "selfAddPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +--force-(key|password) +\\S+" , {"ac" : ["--ttl",""]}, - "selfAddPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +--force-(key|password) +\\S+ +--ttl" , {"pr" : [""]}, - "selfAddPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +--force-(key|password) +\\S+ +--ttl +\\S+" , {"ac" : [""]} + "selfAddPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+" , {"ac" : ["--proxy-host", "--force-key", "--force-password", "--ttl", "--force", "--comment", ""]}, + "selfAddPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host" , {"pr" : ["", ""]}, + "selfAddPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+" , {"ac" : ["--proxy-port"]}, + "selfAddPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port" , {"pr" : [""]}, + "selfAddPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port +\\S+" , {"ac" : ["--proxy-user"]}, + "selfAddPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port +\\S+ +.*--proxy-user" , {"pr" : [""]}, + "selfAddPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port +\\S+ +.*--proxy-user +\\S+" , {"ac" : ["--force-key", "--force-password", "--ttl", "--force", "--comment", ""]}, + "selfAddPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--ttl" , {"pr" : [""]}, + "selfAddPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--force-key" , {"pr" : [""]}, + "selfAddPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--force-password" , {"pr" : [""]}, + "selfAddPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--comment" , {"pr" : [""]} ], "master_only": true } diff --git a/bin/plugin/restricted/selfDelPersonalAccess b/bin/plugin/restricted/selfDelPersonalAccess index 803c10d..9108784 100755 --- a/bin/plugin/restricted/selfDelPersonalAccess +++ b/bin/plugin/restricted/selfDelPersonalAccess @@ -14,7 +14,10 @@ my $remainingOptions = OVH::Bastion::Plugin::begin( header => "removing personal access to a server from an account", userAllowWildcards => 1, options => { - "protocol=s" => \my $protocol, + "protocol=s" => \my $protocol, + "proxy-host=s" => \my $proxyHost, + "proxy-port=s" => \my $proxyPort, + "proxy-user=s" => \my $proxyUser, # undocumented/compatibility: "user-any" => \my $userAny, "port-any" => \my $portAny, @@ -42,6 +45,10 @@ Usage: --osh SCRIPT_NAME --host HOST --user USER --port PORT [OPTIONS] scpdownload allow SCP download, you<--bastion--server sftp allow usage of the SFTP subsystem, through the bastion rsync allow usage of rsync, through the bastion + --proxy-host HOST|IP Specify which host was used as a proxy/jump host to reach the target server + --proxy-port PORT Proxy port that was used to reach the target server + --proxy-user USER|PATTERN|* Proxy user that was configured for this access (mandatory when --proxy-host is specified). + Globbing characters '*' and '?' are supported for pattern matching. EOF ); @@ -52,30 +59,47 @@ if (!$ip) { osh_exit 'ERR_MISSING_PARAMETER', "Missing parameter 'host' or didn't resolve correctly"; } +my $proxyIp; +if ($proxyHost) { + $fnret = + OVH::Bastion::validate_proxy_params(proxyHost => $proxyHost, proxyPort => $proxyPort, proxyUser => $proxyUser); + $fnret or osh_exit $fnret; + $proxyIp = $fnret->value->{'proxyIp'}; + $proxyPort = $fnret->value->{'proxyPort'}; + $proxyUser = $fnret->value->{'proxyUser'}; +} + $fnret = OVH::Bastion::Plugin::ACL::check( - user => $user, - userAny => $userAny, - port => $port, - portAny => $portAny, - scpUp => $scpUp, - scpDown => $scpDown, - sftp => $sftp, - protocol => $protocol, + user => $user, + userAny => $userAny, + port => $port, + portAny => $portAny, + scpUp => $scpUp, + scpDown => $scpDown, + sftp => $sftp, + protocol => $protocol, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser, ); if (!$fnret) { help(); osh_exit($fnret); } -$user = $fnret->value->{'user'}; -$port = $fnret->value->{'port'}; +$user = $fnret->value->{'user'}; +$port = $fnret->value->{'port'}; +$proxyUser = $fnret->value->{'proxyUser'}; my @command = qw{ sudo -n -u allowkeeper -- /usr/bin/env perl -T }; push @command, $OVH::Bastion::BASEPATH . '/bin/helper/osh-accountModifyPersonalAccess'; -push @command, '--target', 'self'; -push @command, '--action', 'del'; -push @command, '--account', $self; -push @command, '--ip', $ip; -push @command, '--user', $user if $user; -push @command, '--port', $port if $port; +push @command, '--target', 'self'; +push @command, '--action', 'del'; +push @command, '--account', $self; +push @command, '--ip', $ip; +push @command, '--user', $user if $user; +push @command, '--port', $port if $port; +push @command, '--proxy-ip', $proxyIp if $proxyIp; +push @command, '--proxy-port', $proxyPort if $proxyPort; +push @command, '--proxy-user', $proxyUser if $proxyUser; osh_exit OVH::Bastion::helper(cmd => \@command); diff --git a/bin/plugin/restricted/selfDelPersonalAccess.json b/bin/plugin/restricted/selfDelPersonalAccess.json index 729bbf3..bca492d 100644 --- a/bin/plugin/restricted/selfDelPersonalAccess.json +++ b/bin/plugin/restricted/selfDelPersonalAccess.json @@ -7,7 +7,13 @@ "selfDelPersonalAccess +--host +\\S+ +.*--port" , {"pr" : [""]}, "selfDelPersonalAccess +--host +\\S+ +--user +\\S+" , {"ac" : ["", "--port"]}, "selfDelPersonalAccess +--host +\\S+ +--port +\\S+" , {"ac" : ["", "--user"]}, - "selfDelPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+" , {"pr" : [""]} + "selfDelPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+" , {"ac" : ["--proxy-host", ""]}, + "selfDelPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host" , {"pr" : ["", ""]}, + "selfDelPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+" , {"ac" : ["--proxy-port"]}, + "selfDelPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port" , {"pr" : [""]}, + "selfDelPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port +\\S+" , {"ac" : ["--proxy-user"]}, + "selfDelPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port +\\S+ +.*--proxy-user" , {"pr" : [""]}, + "selfDelPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+ +.*--proxy-host +\\S+ +.*--proxy-port +\\S+ +.*--proxy-user +\\S+" , {"pr" : [""]} ], "master_only": true } diff --git a/bin/plugin/restricted/whoHasAccessTo b/bin/plugin/restricted/whoHasAccessTo index 5a96435..0b3b4e2 100755 --- a/bin/plugin/restricted/whoHasAccessTo +++ b/bin/plugin/restricted/whoHasAccessTo @@ -12,11 +12,14 @@ use OVH::Bastion::Plugin qw( :DEFAULT help ); # globally allow sys_getpw* and sys_getgr* cache use $ENV{'PW_GR_CACHE'} = 1; -my (@ignoreGroups, $ignorePersonal, $showWildcards); +my ($proxyUser, $proxyHost, $proxyPort, @ignoreGroups, $ignorePersonal, $showWildcards); my $remainingOptions = OVH::Bastion::Plugin::begin( argv => \@ARGV, header => "who has access to this?", options => { + "proxy-user=s" => \$proxyUser, + "proxy-host=s" => \$proxyHost, + "proxy-port=i" => \$proxyPort, "ignore-group=s" => \@ignoreGroups, "ignore-private" => \$ignorePersonal, # deprecated name "ignore-personal" => \$ignorePersonal, @@ -30,6 +33,9 @@ Usage: --osh SCRIPT_NAME --host SERVER [OPTIONS] --host SERVER List declared accesses to this server --user USER Remote user allowed (if not specified, ignore user specifications) --port PORT Remote port allowed (if not specified, ignore port specifications) + --proxy-user USER Proxy user allowed (if egress connection goes through a proxyjump) + --proxy-host HOST Proxy host allowed (if egress connection goes through a proxyjump) + --proxy-port PORT Proxy port allowed (if egress connection goes through a proxyjump) --ignore-personal Don't check accounts' personal accesses (i.e. only check groups) --ignore-group GROUP Ignore accesses by this group, if you know GROUP public key is in fact not present on remote server but bastion thinks it is @@ -54,6 +60,16 @@ if (!$ip) { osh_exit 'ERR_MISSING_PARAMETER', "Missing parameter host (or it didn't resolve correctly)"; } +my $proxyIp; +if ($proxyHost) { + $fnret = + OVH::Bastion::validate_proxy_params(proxyHost => $proxyHost, proxyPort => $proxyPort, proxyUser => $proxyUser); + $fnret or osh_exit $fnret; + $proxyIp = $fnret->value->{'proxyIp'}; + $proxyPort = $fnret->value->{'proxyPort'}; + $proxyUser = $fnret->value->{'proxyUser'}; +} + $fnret = OVH::Bastion::get_account_list(); $fnret or osh_exit $fnret; @@ -67,14 +83,18 @@ sub process_account { my $account = $params{'account'}; $fnret = OVH::Bastion::is_access_granted( - account => $account, - user => $user, - ip => $ip, - ipfrom => $ENV{'OSH_IP_FROM'}, - port => $port, - cache => 1, - ignorePort => ($port ? 0 : 1), # return accesses without checking for the specified port - ignoreUser => ($user ? 0 : 1), # return accesses without checking for the specified remote user + account => $account, + user => $user, + ip => $ip, + ipfrom => $ENV{'OSH_IP_FROM'}, + port => $port, + proxyUser => $proxyUser, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + cache => 1, + ignorePort => ($port ? 0 : 1), # return accesses without checking for the specified port + ignoreUser => ($user ? 0 : 1), # return accesses without checking for the specified remote user + ignoreProxyUser => ($proxyUser ? 0 : 1), # return accesses without checking for the specified proxy user ); if ($fnret) { my $byPersonal = 0; diff --git a/bin/plugin/restricted/whoHasAccessTo.json b/bin/plugin/restricted/whoHasAccessTo.json index bca1a1f..47b9965 100644 --- a/bin/plugin/restricted/whoHasAccessTo.json +++ b/bin/plugin/restricted/whoHasAccessTo.json @@ -2,9 +2,12 @@ "interactive": [ "whoHasAccessTo" , {"ac" : ["--host"]}, "whoHasAccessTo +--host" , {"pr" : [""]}, - "whoHasAccessTo +--host +\\S+ +(.*--(user|port|ignore-group) +\\S+| +.*--(ignore-wildcard|ignore-private))?$" , {"ac" : ["--user","--port","--ignore-wildcard","--ignore-private","--ignore-group",""]}, - "whoHasAccessTo +--host +\\S+ +.*--user" , {"pr" : [""]}, - "whoHasAccessTo +--host +\\S+ +.*--port" , {"pr" : [""]}, - "whoHasAccessTo +--host +\\S+ +.*--ignore-group" , {"ac" : [""]} + "whoHasAccessTo +--host +\\S+" , {"ac" : ["--user", "--port", "--proxy-host", "--proxy-port", "--proxy-user", "--ignore-group", "--ignore-personal", "--show-wildcards", ""]}, + "whoHasAccessTo +--host +\\S+ +.*--user" , {"pr" : [""]}, + "whoHasAccessTo +--host +\\S+ +.*--port" , {"pr" : [""]}, + "whoHasAccessTo +--host +\\S+ +.*--proxy-host" , {"pr" : ["", ""]}, + "whoHasAccessTo +--host +\\S+ +.*--proxy-port" , {"pr" : [""]}, + "whoHasAccessTo +--host +\\S+ +.*--proxy-user" , {"pr" : [""]}, + "whoHasAccessTo +--host +\\S+ +.*--ignore-group" , {"ac" : [""]} ] } diff --git a/bin/shell/osh.pl b/bin/shell/osh.pl index c06e701..9bbfa4c 100755 --- a/bin/shell/osh.pl +++ b/bin/shell/osh.pl @@ -70,7 +70,11 @@ sub main_exit { plugin => undef, params => join('^', @ARGV), comment => $comment, - uniqid => $log_uniq_id + uniqid => $log_uniq_id, + proxyuser => undef, + proxyip => undef, + proxyhost => undef, + proxyport => undef ) if (not defined $log_db_name or not defined $log_insert_id); my $R = R($retcode eq OVH::Bastion::EXIT_OK ? 'OK' : 'KO_' . uc($comment), msg => $msg); @@ -268,9 +272,47 @@ osh_debug("user-passed options : $realOptions"); # Command params # +# special case: check if this is a ProxyJump connection that should be executed directly +if ($ENV{'OSH_PROXYJUMP_CONNECTION'}) { + delete $ENV{'OSH_PROXYJUMP_CONNECTION'}; # make sure nothing else gets interpreted as proxyjump + + # check if poxyJump connections are allowed + # This condition should never be true if proxyJump isn't allowed, but let's double check + if (!$config->{'egressProxyJumpAllowed'}) { + main_exit OVH::Bastion::EXIT_ACCESS_DENIED, "proxyjump_not_allowed", + "Sorry $self, egress proxy-jump connections have been disabled by policy"; + } + osh_debug("Detected ProxyJump connection, executing command directly"); + + # Execute the proxy command directly without further validation + if ($realOptions) { + # Parse the command to extract program and arguments + my @cmd_parts = split(/\s+/, $realOptions); + if (!@cmd_parts) { + main_exit(OVH::Bastion::EXIT_EXEC_FAILED, "proxy_parsing_failed", "Failed to parse proxy command"); + } + + # Remove "exec" if it's the first argument (the ssh subprocess puts that there) + if ($cmd_parts[0] eq 'exec') { + shift @cmd_parts; + } + + # this should never happen, but just in case... + if ($cmd_parts[0] ne 'ssh') { + main_exit(OVH::Bastion::EXIT_EXEC_FAILED, "proxy_no_ssh_cmd", "Proxy command must start with 'ssh'"); + } + + osh_debug("Executing proxy command parts: " . join(' ', @cmd_parts)); + exec(@cmd_parts) + or main_exit(OVH::Bastion::EXIT_EXEC_FAILED, "proxy_exec_failed", "Failed to execute proxy command: $!"); + } + else { + main_exit(OVH::Bastion::EXIT_EXEC_FAILED, "proxy_no_cmd", "No proxy command provided"); + } +} + my $port = 22; # can be override by special port my @toExecute; - # special case: mosh, in that case we have something like this in $realOptions # mosh-server 'new' '-s' '-c' '256' '-l' 'LANG=en_US.UTF-8' '-l' 'LANGUAGE=en_US' '--' '--osh' 'info' if (defined $realOptions && $realOptions =~ /^mosh-server (.+?) '--' (.*)/) { @@ -382,6 +424,7 @@ my $remainingOptions; "generate-mfa-token" => \my $generateMfaToken, "mfa-token=s" => \my $mfaToken, "term-passthrough" => \my $termPassthrough, + "J=s" => \my $proxyJump, ); if (not defined $realOptions) { help(); @@ -522,7 +565,11 @@ if ($interactive and not $ENV{'OSH_IN_INTERACTIVE_SESSION'}) { plugin => undef, params => undef, comment => undef, - uniqid => $log_uniq_id + uniqid => $log_uniq_id, + proxyuser => undef, + proxyip => undef, + proxyhost => undef, + proxyport => undef ); if ($logret) { @@ -576,6 +623,7 @@ else { } else { $host = shift(@{$remainingOptions}); + osh_debug("After shift, remainingOptions " . join('/', @$remainingOptions)); if ($host eq '-osh' || $host eq '--osh') { # special case when using -osh without argument @@ -599,6 +647,45 @@ else { } } +my $proxyIp = undef; +my $proxyPort = undef; +my $proxyUser = $user; # user might be undef. We'll handle that later +# Parse proxyjump args if specified +if ($proxyJump) { + if ($proxyJump =~ /^(?:([a-zA-Z0-9._@!-]{1,128})@)?(\[?[a-zA-Z0-9._-]+\]?)(?::(\d+))?$/) { + $proxyUser = $1 if $1; + $proxyIp = $2; + $proxyPort = $3 ? $3 : 22; + osh_debug("parsed proxyjump: host=$proxyIp port=$proxyPort user=$proxyUser"); + } + else { + main_exit OVH::Bastion::EXIT_INVALID_PROXYJUMP, 'invalid_proxyjump', + "Invalid proxyjump specification '$proxyJump', should be [user@]host[:port]"; + } + + $fnret = OVH::Bastion::get_ip(host => $proxyIp, allowSubnets => 0); + if (!$fnret) { + if ($fnret->err eq 'ERR_DNS_DISABLED') { + main_exit OVH::Bastion::EXIT_DNS_DISABLED, 'dns_disabled', $fnret->msg; + } + elsif ($fnret->err eq 'ERR_IP_VERSION_DISABLED') { + main_exit OVH::Bastion::EXIT_IP_VERSION_DISABLED, 'ip_version_disabled', $fnret->msg; + } + else { + main_exit OVH::Bastion::EXIT_HOST_NOT_FOUND, 'host_not_found', $fnret->msg; + } + } + osh_debug("Proxyjump host $proxyIp resolved to IP " . $fnret->value->{'ip'}); + $proxyIp = $fnret->value->{'ip'}; + + if ($proxyUser && !OVH::Bastion::is_valid_remote_user(user => $proxyUser, allowWildcards => 0)) { + main_exit OVH::Bastion::EXIT_INVALID_REMOTE_USER, 'invalid_proxy_user', + "Proxy user name '$proxyUser' seems invalid"; + } + + $ENV{'OSH_PROXYJUMP_CONNECTION'} = 1; +} + # for plugins (osh_command), do a first check with allowWildcards, it'll be re-done in Plugin::start with # either allowWildcards set to 0 or 1 depending on the plugin configuration that we don't have at this stage yet if ($user && !OVH::Bastion::is_valid_remote_user(user => $user, allowWildcards => ($osh_command ? 1 : 0))) { @@ -813,6 +900,8 @@ osh_debug("self : " . (defined $host ? $host : '') . "\n" . "port : " . (defined $port ? $port : '') . "\n" + . "proxyJump : " + . (defined $proxyJump ? $proxyJump : '') . "\n" . "verbose : " . (defined $verbose ? $verbose : '') . "\n" . "tty : " @@ -824,6 +913,10 @@ osh_debug("self : " . "\n"); my $hostto = OVH::Bastion::ip2host($host)->value || $host; +my $proxyhost; +if (defined $proxyIp) { + $proxyhost = OVH::Bastion::ip2host($proxyIp)->value || $proxyIp; +} # Special case: adminSudo for ssh connection as another user if ($sshAs) { @@ -845,7 +938,11 @@ if ($sshAs) { plugin => undef, params => join(' ', @$remainingOptions), comment => undef, - uniqid => $log_uniq_id + uniqid => $log_uniq_id, + proxyuser => $proxyUser, + proxyip => $proxyIp, + proxyhost => $proxyhost, + proxyport => $proxyPort ); if (!$fnret) { main_exit OVH::Bastion::EXIT_RESTRICTED_COMMAND, "sshas_denied", @@ -966,7 +1063,11 @@ if ($osh_command) { plugin => $osh_command, params => join(' ', @$remainingOptions), comment => 'plugin-' . ($fnret->value ? $fnret->value->{'type'} : 'UNDEF'), - uniqid => $log_uniq_id + uniqid => $log_uniq_id, + proxyuser => $proxyUser, + proxyip => $proxyIp, + proxyhost => $proxyhost, + proxyport => $proxyPort ); if ($logret) { @@ -1089,6 +1190,9 @@ if ($osh_command) { debug => $osh_debug, tty => $tty, notty => $notty, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser, stealth_stdout => OVH::Bastion::plugin_config( plugin => $osh_command, key => "stealth_stdout" @@ -1152,13 +1256,25 @@ if (!$quiet) { # do that here, cause sometimes we do not want to pass user to osh $user = $user || $config->{'defaultLogin'} || $remoteself || $sysself; +# if we have a proxyIp but no proxyUser, set it to $user +$proxyUser = $user if ($proxyIp && !$proxyUser); + # log request osh_debug("final request : " . "$user\@$ip -p $port -- $command'\n"); -my $displayLine = sprintf("%s => %s => %s", +my $displayLine = sprintf( + "%s => %s => %s", OVH::Bastion::machine_display(ip => $hostfrom, port => $portfrom)->value, OVH::Bastion::machine_display(ip => $bastionhost, port => $bastionport, user => $self)->value, - OVH::Bastion::machine_display(ip => $hostto, port => $port, user => $user)->value, + OVH::Bastion::machine_display( + ip => $hostto, + port => $port, + user => $user, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser, + )->value, + ); if (!$quiet) { @@ -1172,12 +1288,15 @@ if ($fnret and $fnret->value() =~ /yes/) { } else { $fnret = OVH::Bastion::is_access_granted( - account => $self, - user => $user, - ipfrom => $ipfrom, - ip => $ip, - port => $port, - details => 1 + account => $self, + user => $user, + ipfrom => $ipfrom, + ip => $ip, + port => $port, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser, + details => 1 ); } @@ -1206,7 +1325,11 @@ if (!$fnret) { portto => $port, user => $user, params => $command, - uniqid => $log_uniq_id + uniqid => $log_uniq_id, + proxyuser => $proxyUser, + proxyip => $proxyIp, + proxyhost => $proxyhost, + proxyport => $proxyPort ); if (!$logret) { osh_warn($logret); @@ -1236,7 +1359,10 @@ my $ttyrec_fnret = OVH::Bastion::build_ttyrec_cmdline_part1of2( remoteaccount => $remoteself, debug => $osh_debug, tty => $tty, - notty => $notty + notty => $notty, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser ); main_exit(OVH::Bastion::EXIT_TTYREC_CMDLINE_FAILED, "ttyrec_failed", $ttyrec_fnret->msg) if !$ttyrec_fnret; @@ -1399,6 +1525,7 @@ else { @ttyrec = @{$ttyrec_fnret->value->{'cmd'}}; # SSH PASSWORD AUTOLOGIN + # TODO: how tf does this work??? And how to proxyjump with this? if ($userPasswordClue) { push @preferredAuths, 'keyboard-interactive'; @@ -1452,7 +1579,7 @@ else { if ($fnret) { # add the -i key1 -i key2 etc. returned by get_details_from_access_array() push @command, @{$fnret->value->{'sshKeysArgs'}}; - # updathe the JIT MFA flag + # update the JIT MFA flag $JITMFARequired = $fnret->value->{'mfaRequired'}; } else { @@ -1470,6 +1597,39 @@ else { push @command, '-T' if $notty; push @command, '-o', "ConnectTimeout=$timeout" if $timeout; + if ($proxyJump) { + # check if poxyJump connections are allowed + if (!$config->{'egressProxyJumpAllowed'}) { + main_exit OVH::Bastion::EXIT_ACCESS_DENIED, "proxyjump_not_allowed", + "Sorry $self, egress proxy-jump connections have been disabled by policy"; + } + + # Build ProxyCommand with same options as main SSH command + my @proxyCommand = ('ssh'); + push @proxyCommand, '-o', 'PreferredAuthentications=' . (join(',', @preferredAuths)); + + # Add the same SSH keys to the proxy command + if ($fnret && $fnret->value->{'sshKeysArgs'}) { + push @proxyCommand, @{$fnret->value->{'sshKeysArgs'}}; + } + + push @proxyCommand, '-p', $proxyPort if $proxyPort && $proxyPort != 22; + push @proxyCommand, '-l', $proxyUser, '-W', '%h:%p', $proxyIp; + + if ($verbose) { + foreach (1 .. $verbose) { + push @proxyCommand, '-v'; + } + } + push @proxyCommand, '-o', "ConnectTimeout=$timeout" if $timeout; + + # Quote arguments that contain spaces and build the command string + my $proxyCommandStr = join(' ', map { /\s/ ? "'$_'" : $_ } @proxyCommand); + push @command, '-o', "ProxyCommand=$proxyCommandStr"; + + osh_debug("ProxyCommand: $proxyCommandStr"); + } + if (not $quiet) { $fnret = OVH::Bastion::account_config(account => $self, key => OVH::Bastion::OPT_ACCOUNT_IDLE_IGNORE, public => 1); @@ -1591,7 +1751,11 @@ my $logret = OVH::Bastion::log_access_insert( user => $user, params => join(' ', @ttyrec), ttyrecfile => $saveFile, - uniqid => $log_uniq_id + uniqid => $log_uniq_id, + proxyuser => $proxyUser, + proxyip => $proxyIp, + proxyhost => $proxyhost, + proxyport => $proxyPort ); if (!$logret) { osh_warn($logret); @@ -1905,12 +2069,14 @@ sub do_plugin_jit_mfa { my $remoteuser = $user || $config->{'defaultLogin'} || $remoteself || $sysself; $localfnret = OVH::Bastion::is_access_granted( - account => $self, - user => $user, - ipfrom => $ipfrom, - ip => $ip, - port => $port, - details => 1 + account => $self, + user => $user, + ipfrom => $ipfrom, + ip => $ip, + port => $port, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + details => 1 ); if (!$localfnret) { diff --git a/etc/bastion/bastion.conf.dist b/etc/bastion/bastion.conf.dist index b008c6f..fa6e9ab 100644 --- a/etc/bastion/bastion.conf.dist +++ b/etc/bastion/bastion.conf.dist @@ -123,6 +123,11 @@ # EXAMPLE: "-s -p 40000:49999" "moshCommandLine": "", # +# egressProxyJumpAllowed (boolean) +# DESC: If set to ``true``, ProxyJump (``-J``) egress connections will be allowed. +# DEFAULT: false +"egressProxyJumpAllowed": false, +# ########################### # > Global network policies # >> Those options can set a few global network policies to be applied bastion-wide. @@ -508,7 +513,7 @@ "sshClientHasOptionE": false, # # sshAddKeysToAgentAllowed (boolean) -# DESC: Set to ``true`` if you want to allow to spawn an ssh-agent and forward it over the egress session when specifically requested with the '--forward-agent' or '-x' flag, with the egress key added to the agent. Useful if you need the ssh-key for authentication on other systems (another jumpserver for example). +# DESC: Set to ``true`` if you want to allow to spawn an ssh-agent and forward it over the egress session when specifically requested with the '--forward-agent' or '-x' flag, with the egress key added to the agent. Useful if you need the ssh-key for authentication on other systems (another jumpserver for example). # DEFAULT: false "sshAddKeysToAgentAllowed": false } diff --git a/lib/perl/OVH/Bastion.pm b/lib/perl/OVH/Bastion.pm index ba12740..cc67f59 100644 --- a/lib/perl/OVH/Bastion.pm +++ b/lib/perl/OVH/Bastion.pm @@ -104,6 +104,7 @@ use constant { EXIT_ACCOUNT_FROZEN => 131, EXIT_DNS_DISABLED => 132, EXIT_IP_VERSION_DISABLED => 133, + EXIT_INVALID_PROXYJUMP => 134, }; use constant { @@ -761,16 +762,63 @@ sub is_valid_remote_user { return R('ERR_INVALID_PARAMETER', msg => "Specified user doesn't seem to be valid"); } +sub validate_proxy_params { + my %params = @_; + my $proxyHost = $params{'proxyHost'}; + my $proxyPort = $params{'proxyPort'}; + my $proxyUser = $params{'proxyUser'}; + my $allowWildcards = $params{'allowWildcards'} // 1; + + my $proxyIp; + my $fnret; + + if ($proxyHost) { + $fnret = OVH::Bastion::is_valid_ip(ip => $proxyHost, allowSubnets => 0); + if ($fnret) { + $proxyIp = $fnret->value->{'ip'}; + } + else { + $fnret = OVH::Bastion::get_ip(host => $proxyHost); + $fnret or return $fnret; + $proxyIp = $fnret->value->{'ip'}; + } + } + + if ($proxyPort) { + $fnret = OVH::Bastion::is_valid_port(port => $proxyPort); + $fnret or return $fnret; + $proxyPort = $fnret->value; + } + + if ($proxyUser) { + $fnret = OVH::Bastion::is_valid_remote_user(user => $proxyUser, allowWildcards => $allowWildcards); + $fnret or return $fnret; + $proxyUser = $fnret->value; + } + + return R('OK', value => {proxyIp => $proxyIp, proxyPort => $proxyPort, proxyUser => $proxyUser}); +} + sub machine_display { - my %params = @_; - my $ip = $params{'ip'}; - my $port = $params{'port'}; - my $user = $params{'user'}; + my %params = @_; + my $ip = $params{'ip'}; + my $port = $params{'port'}; + my $user = $params{'user'}; + my $proxyIp = $params{'proxyIp'}; + my $proxyPort = $params{'proxyPort'}; + my $proxyUser = $params{'proxyUser'}; my $machine = (index($ip, ':') >= 0 ? "[$ip]" : $ip); $machine .= ":$port" if $port; $machine = $user . '@' . $machine if $user; + if ($proxyIp) { + my $proxy = (index($proxyIp, ':') >= 0 ? "[$proxyIp]" : $proxyIp); + $proxy .= ":$proxyPort" if $proxyPort; + $proxy = $proxyUser . '@' . $proxy if $proxyUser; + $machine = "$machine via $proxy"; + } + return R('OK', value => $machine); } @@ -1142,15 +1190,24 @@ sub build_ttyrec_cmdline_part1of2 { $params{'ip'} = 'v6[' . $params{'ip'} . ']'; } + # handle ipv6 for proxyIp + if ($params{'proxyIp'} && index($params{'proxyIp'}, ':') >= 0) { + $params{'proxyIp'} =~ tr/:/./; + $params{'proxyIp'} = 'v6[' . $params{'proxyIp'} . ']'; + } + # build ttyrec filename format my $bastionName = OVH::Bastion::config('bastionName')->value; my $ttyrecFilenameFormat = OVH::Bastion::config('ttyrecFilenameFormat')->value; $ttyrecFilenameFormat =~ s/&bastionname/$bastionName/g; - $ttyrecFilenameFormat =~ s/&uniqid/$params{'uniqid'}/g if $params{'uniqid'}; - $ttyrecFilenameFormat =~ s/&ip/$params{'ip'}/g if $params{'ip'}; - $ttyrecFilenameFormat =~ s/&port/$params{'port'}/g if defined $params{'port'}; - $ttyrecFilenameFormat =~ s/&user/$params{'user'}/g if defined $params{'user'}; - $ttyrecFilenameFormat =~ s/&account/$params{'account'}/g if $params{'account'}; + $ttyrecFilenameFormat =~ s/&uniqid/$params{'uniqid'}/g if $params{'uniqid'}; + $ttyrecFilenameFormat =~ s/&ip/$params{'ip'}/g if $params{'ip'}; + $ttyrecFilenameFormat =~ s/&port/$params{'port'}/g if defined $params{'port'}; + $ttyrecFilenameFormat =~ s/&user/$params{'user'}/g if defined $params{'user'}; + $ttyrecFilenameFormat =~ s/&account/$params{'account'}/g if $params{'account'}; + $ttyrecFilenameFormat =~ s/&proxyip/$params{'proxyIp'}/g if defined $params{'proxyIp'}; + $ttyrecFilenameFormat =~ s/&proxyport/$params{'proxyPort'}/g if defined $params{'proxyPort'}; + $ttyrecFilenameFormat =~ s/&proxyuser/$params{'proxyUser'}/g if defined $params{'proxyUser'}; if ($ttyrecFilenameFormat =~ /&(bastionname|uniqid|ip|port|user|account)/) { @@ -1159,6 +1216,14 @@ sub build_ttyrec_cmdline_part1of2 { msg => "Missing bastionname, uniqid, ip, port, user or account in ttyrec cmdline building"); } + # if there is no proxyIp, remove the via part and all proxy placeholders + if (!defined $params{'proxyIp'}) { + $ttyrecFilenameFormat =~ s/\.via//g; + $ttyrecFilenameFormat =~ s/\.&proxyuser//g; + $ttyrecFilenameFormat =~ s/\.&proxyip//g; + $ttyrecFilenameFormat =~ s/\.&proxyport//g; + } + # ensure there are no '/' $ttyrecFilenameFormat =~ tr{/}{_}; @@ -1169,7 +1234,14 @@ sub build_ttyrec_cmdline_part1of2 { $saveDir .= "/" . $params{'remoteaccount'}; mkdir($saveDir); } - $saveDir .= "/" . $params{'ip'}; + + # format it like via-$proxyIp-$ip so that it's sorted by proxyIp first, then by ip if you list the directory + if ($params{'proxyIp'}) { + $saveDir .= "/via-" . $params{'proxyIp'} . "-" . $params{'ip'}; + } + else { + $saveDir .= "/" . $params{'ip'}; + } mkdir($saveDir); my $saveFileFormat = "$saveDir/$ttyrecFilenameFormat"; diff --git a/lib/perl/OVH/Bastion/Plugin/ACL.pm b/lib/perl/OVH/Bastion/Plugin/ACL.pm index cd9eb48..d89ff96 100644 --- a/lib/perl/OVH/Bastion/Plugin/ACL.pm +++ b/lib/perl/OVH/Bastion/Plugin/ACL.pm @@ -9,8 +9,8 @@ use OVH::Bastion; sub check { my %params = @_; - my ($port, $portAny, $user, $userAny, $scpUp, $scpDown, $sftp, $protocol) = - @params{qw{ port portAny user userAny scpUp scpDown sftp protocol }}; + my ($port, $portAny, $user, $userAny, $scpUp, $scpDown, $sftp, $protocol, $proxyIp, $proxyPort, $proxyUser) = + @params{qw{ port portAny user userAny scpUp scpDown sftp protocol proxyIp proxyPort proxyUser }}; if ($user and $userAny) { return R('ERR_INCOMPATIBLE_PARAMETERS', @@ -84,11 +84,52 @@ sub check { ); } - # now, remap port and user '*' back to undef - undef $user if $user eq '*'; - undef $port if $port eq '*'; + # check proxy-host and proxy-port parameters + osh_debug("Checking proxy parameters: proxyIp='$proxyIp' proxyPort='$proxyPort' proxyUser='$proxyUser'"); + if ($proxyIp) { + if (!$proxyPort) { + return R('ERR_MISSING_PARAMETER', msg => "When --proxy-host is specified, --proxy-port becomes mandatory"); + } + + # validate proxy ip + my $fntret = OVH::Bastion::is_valid_ip(ip => $proxyIp, allowSubnets => 0); + if (!$fntret) { + return R('ERR_INVALID_PARAMETER', msg => "Proxy host IP '$proxyIp' is invalid: " . $fntret->msg); + } + + if (!$proxyUser) { + return R('ERR_MISSING_PARAMETER', msg => "When --proxy-host is specified, --proxy-user becomes mandatory"); + } + } - return R('OK', value => {user => $user, port => $port, protocol => $protocol}); + if ($proxyPort) { + if (!$proxyIp) { + return R('ERR_MISSING_PARAMETER', msg => "When --proxy-port is specified, --proxy-host becomes mandatory"); + } + + # validate proxy port + my $fnret = OVH::Bastion::is_valid_port(port => $proxyPort); + if (!$fnret) { + return R('ERR_INVALID_PARAMETER', msg => "Proxy port '$proxyPort' is invalid: " . $fnret->msg); + } + } + + # now, remap port and user '*' back to undef + undef $user if $user eq '*'; + undef $port if $port eq '*'; + undef $proxyUser if $proxyUser eq '*'; + + return R( + 'OK', + value => { + user => $user, + port => $port, + protocol => $protocol, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser + } + ); } 1; diff --git a/lib/perl/OVH/Bastion/Plugin/groupSetRole.pm b/lib/perl/OVH/Bastion/Plugin/groupSetRole.pm index a7ac915..3de8d25 100644 --- a/lib/perl/OVH/Bastion/Plugin/groupSetRole.pm +++ b/lib/perl/OVH/Bastion/Plugin/groupSetRole.pm @@ -170,13 +170,17 @@ sub act { my %values = %{$fnret->value()}; my ($group, $shortGroup, $account, $type, $realm, $remoteaccount, $sysaccount) = @values{qw{ group shortGroup account type realm remoteaccount sysaccount }}; - my ($action, $self, $user, $host, $port, $ttl, $comment) = @params{qw{ action self user host port ttl comment }}; + my ($action, $self, $user, $host, $port, $proxyIp, $proxyPort, $proxyUser, $ttl, $comment) = + @params{qw{ action self user host port proxyIp proxyPort proxyUser ttl comment }}; my @command; - osh_debug( - "groupSetRole::act, $action $type $group/$account ($sysaccount/$realm/$remoteaccount) $user\@$host:$port ttl=$ttl" - ); + my $debug_msg = + "groupSetRole::act, $action $type $group/$account ($sysaccount/$realm/$remoteaccount) $user\@$host:$port ttl=$ttl"; + if ($proxyIp) { + $debug_msg .= " via proxy $proxyUser\@$proxyIp:$proxyPort"; + } + osh_debug($debug_msg); # add/del system user to system group except if we're removing a guest access (will be done after if needed) if (!($type eq 'guest' and $action eq 'del')) { @@ -207,19 +211,25 @@ sub act { # foreach guest access, delete foreach my $access (@acl) { my $machine = OVH::Bastion::machine_display( - ip => $access->{'ip'}, - port => $access->{'port'}, - user => $access->{'user'} + ip => $access->{'ip'}, + port => $access->{'port'}, + user => $access->{'user'}, + proxyIp => $access->{'proxyIp'}, + proxyPort => $access->{'proxyPort'}, + proxyUser => $access->{'proxyUser'}, )->value; $fnret = OVH::Bastion::Plugin::groupSetRole::act( - account => $account, - group => $shortGroup, - action => 'del', - type => 'guest', - user => $access->{'user'}, - port => $access->{'port'}, - host => $access->{'ip'}, - self => $self, + account => $account, + group => $shortGroup, + action => 'del', + type => 'guest', + user => $access->{'user'}, + port => $access->{'port'}, + host => $access->{'ip'}, + proxyIp => $access->{'proxyIp'}, + proxyPort => $access->{'proxyPort'}, + proxyUser => $access->{'proxyUser'}, + self => $self, ); if (!$fnret) { osh_warn("Failed removing guest access to $machine, proceeding anyway..."); @@ -253,18 +263,28 @@ sub act { # in that case, we need to handle the add/del of the guest access to $user@$host:$port # check if group has access to $user@$ip:$port - my $machine = OVH::Bastion::machine_display(ip => $host, port => $port, user => $user)->value; + my $machine = OVH::Bastion::machine_display( + ip => $host, + port => $port, + user => $user, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser + )->value; osh_debug( "groupSetRole::act, checking if group $group has access to $machine to $action $type access to $account"); if ($action eq 'add') { $fnret = OVH::Bastion::is_access_way_granted( - way => 'group', - group => $shortGroup, - user => $user, - port => $port, - ip => $host, + way => 'group', + group => $shortGroup, + user => $user, + port => $port, + ip => $host, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser, ); if (not $fnret) { osh_debug("groupSetRole::act, it doesn't! $fnret"); @@ -287,14 +307,17 @@ sub act { # Add/Del user access to user@host:port with group key @command = qw{ sudo -n -u allowkeeper -- /usr/bin/env perl -T }; push @command, $OVH::Bastion::BASEPATH . '/bin/helper/osh-accountAddGroupServer'; - push @command, '--group', $group; # must be first params, forced in sudoers.d - push @command, '--account', $account; - push @command, '--action', $action; - push @command, '--ip', $host; - push @command, '--user', $user if $user; - push @command, '--port', $port if $port; - push @command, '--ttl', $ttl if $ttl; - push @command, '--comment', $comment if $comment; + push @command, '--group', $group; # must be first params, forced in sudoers.d + push @command, '--account', $account; + push @command, '--action', $action; + push @command, '--ip', $host; + push @command, '--user', $user if $user; + push @command, '--port', $port if $port; + push @command, '--proxy-ip', $proxyIp if $proxyIp; + push @command, '--proxy-port', $proxyPort if $proxyPort; + push @command, '--proxy-user', $proxyUser if $proxyUser; + push @command, '--ttl', $ttl if $ttl; + push @command, '--comment', $comment if $comment; $fnret = OVH::Bastion::helper(cmd => \@command); $fnret or return $fnret; @@ -388,16 +411,19 @@ sub act { severity => 'info', type => 'membership', fields => [ - ['action', $action], - ['type', $type], - ['group', $shortGroup], - ['account', $account], - ['self', $self], - ['user', $user], - ['host', $host], - ['port', $port], - ['ttl', $ttl], - ['comment', $comment || ''], + ['action', $action], + ['type', $type], + ['group', $shortGroup], + ['account', $account], + ['self', $self], + ['user', $user], + ['host', $host], + ['port', $port], + ['proxyIp', $proxyIp], + ['proxyPort', $proxyPort], + ['proxyUser', $proxyUser], + ['ttl', $ttl], + ['comment', $comment || ''], ] ); } diff --git a/lib/perl/OVH/Bastion/Plugin/otherProtocol.pm b/lib/perl/OVH/Bastion/Plugin/otherProtocol.pm index 206a452..03ce660 100644 --- a/lib/perl/OVH/Bastion/Plugin/otherProtocol.pm +++ b/lib/perl/OVH/Bastion/Plugin/otherProtocol.pm @@ -15,29 +15,42 @@ use OVH::Bastion; # this requirement will be lifted once we add the "protocol type" to the whole access tuple data model # while we're at it, return whether we found that this access requires MFA sub has_protocol_access { - my %params = @_; - my $account = $params{'account'}; - my $user = $params{'user'}; - my $ipfrom = $params{'ipfrom'} || $ENV{'OSH_IP_FROM'}; - my $ip = $params{'ip'}; - my $port = $params{'port'}; - my $protocol = $params{'protocol'}; + my %params = @_; + my $account = $params{'account'}; + my $user = $params{'user'}; + my $ipfrom = $params{'ipfrom'} || $ENV{'OSH_IP_FROM'}; + my $ip = $params{'ip'}; + my $port = $params{'port'}; + my $proxyIp = $params{'proxyIp'}; + my $proxyPort = $params{'proxyPort'}; + my $proxyUser = $params{'proxyUser'}; + my $protocol = $params{'protocol'}; if (!$account || !$ipfrom || !$ip || !$protocol || !$user || !$port) { return R('ERR_MISSING_PARAMETERS', msg => "Missing mandatory parameters for has_protocol_access"); } - my $machine = OVH::Bastion::machine_display(ip => $ip, port => $port, user => $user)->value; + my $machine = OVH::Bastion::machine_display( + ip => $ip, + port => $port, + user => $user, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser + )->value; my %keys; osh_debug("Checking access 1/2 of $account to $machine..."); my $fnret = OVH::Bastion::is_access_granted( - account => $account, - user => $user, - ipfrom => $ipfrom, - ip => $ip, - port => $port, - details => 1 + account => $account, + user => $user, + ipfrom => $ipfrom, + ip => $ip, + port => $port, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser, + details => 1 ); if (not $fnret) { @@ -60,6 +73,9 @@ sub has_protocol_access { ipfrom => $ipfrom, ip => $ip, port => $port, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser, exactUserMatch => 1, details => 1 ); diff --git a/lib/perl/OVH/Bastion/allowdeny.inc b/lib/perl/OVH/Bastion/allowdeny.inc index 8b352b7..708fb54 100644 --- a/lib/perl/OVH/Bastion/allowdeny.inc +++ b/lib/perl/OVH/Bastion/allowdeny.inc @@ -77,13 +77,18 @@ sub is_access_way_granted { my $exactUserMatch = $params{'exactUserMatch'}; # $user must be explicitly allowed (user wildcards in grantfile will be ignored) my $exactMatch = $params{'exactMatch'}; # sets exactIpMatch exactPortMatch and exactUserMatch - my $ignoreUser = $params{'ignoreUser'}; # ignore remote user COMPLETELY (plop@, or root@, or @ will all match) - my $ignorePort = $params{'ignorePort'}; # ignore port COMPLETELY (port 22, 2345, or port-wildcard will all match) + my $ignoreUser = $params{'ignoreUser'}; # ignore remote user COMPLETELY (plop@, or root@, or @ will all match) + my $ignorePort = $params{'ignorePort'}; # ignore port COMPLETELY (port 22, 2345, or port-wildcard will all match) + my $ignoreProxyUser = $params{'ignoreProxyUser'}; # ignore proxy user COMPLETELY (if egress connection goes through a proxyjump) my $wantedUser = $params{'user'}; # if undef, means we look for a user-any allow my $wantedIp = $params{'ip'}; # can be a single IP or a subnet my $wantedPort = $params{'port'}; # if undef, means we look for a port-any allow + my $wantedProxyIp = $params{'proxyIp'}; # can be a single IP, no support for subnet + my $wantedProxyPort = $params{'proxyPort'}; # must be exact if defined, no support for wildcard + my $wantedProxyUser = $params{'proxyUser'}; # if undef, means we look for a user-any allow + my $way = $params{'way'}; # personal|group|groupguest|legacy my $group = $params{'group'}; # only meaningful and needed if type=group or type=groupguest my $account = $params{'account'}; # only meaningful and needed if type=personal or type=groupguest @@ -93,17 +98,23 @@ sub is_access_way_granted { $exactIpMatch = $exactPortMatch = $exactUserMatch = 1 if $exactMatch; # normalize '*' to undef - undef $wantedUser if (defined $wantedUser && $wantedUser eq '*'); - undef $wantedPort if (defined $wantedPort && $wantedPort eq '*'); + undef $wantedUser if (defined $wantedUser && $wantedUser eq '*'); + undef $wantedPort if (defined $wantedPort && $wantedPort eq '*'); + undef $wantedProxyUser if (defined $wantedProxyUser && $wantedProxyUser eq '*'); # 'group', 'account', and 'way' parameters are only useful to, and checked by, get_acl_way() $fnret = OVH::Bastion::get_acl_way(way => $way, account => $account, group => $group); $fnret or return $fnret; my @acl = @{$fnret->value || []}; - osh_debug( - "checking way $way/$account/$group with ignorePort=$ignorePort ignoreUser=$ignoreUser exactIpMatch=$exactIpMatch exactPortMatch=$exactPortMatch exactUserMatch=$exactUserMatch" - ); + my $check_debug_msg = + "checking way $way/$account/$group with ignorePort=$ignorePort ignoreUser=$ignoreUser ignoreProxyUser=$ignoreProxyUser exactIpMatch=$exactIpMatch exactPortMatch=$exactPortMatch exactUserMatch=$exactUserMatch"; + osh_debug($check_debug_msg); + + # make sure both proxyIp and proxyPort are defined or undefined + if (defined $wantedProxyIp && !defined $wantedProxyPort) { + return R('ERR_INVALID_PARAMETER', msg => "If proxyIp is given, proxyPort must be given too"); + } my %match; foreach my $entry (@acl) { @@ -111,14 +122,30 @@ sub is_access_way_granted { undef $entry->{'user'} if (defined $entry->{'user'} && $entry->{'user'} eq '*'); undef $entry->{'port'} if (defined $entry->{'port'} && $entry->{'port'} eq '*'); - osh_debug("checking wanted " - . ($wantedUser // '') . '@' - . ($wantedIp // '') . ':' - . ($wantedPort // '') - . ' against ' - . ($entry->{'user'} // '') . '@' - . ($entry->{'ip'} // '') . ':' - . ($entry->{'port'} // '')); + $check_debug_msg = + "checking wanted " + . ($wantedUser // '') . '@' + . ($wantedIp // '') . ':' + . ($wantedPort // '') + . ' against ' + . ($entry->{'user'} // '') . '@' + . ($entry->{'ip'} // '') . ':' + . ($entry->{'port'} // ''); + + if (defined $entry->{'proxyIp'}) { + undef $entry->{'proxyUser'} if (defined $entry->{'proxyUser'} && $entry->{'proxyUser'} eq '*'); + + $check_debug_msg .= + " with wanted proxy " + . ($wantedProxyUser // '') . '@' + . ($wantedProxyIp // '') . ':' + . ($wantedProxyPort // '') + . " against " + . ($entry->{'proxyUser'} // '') . '@' + . ($entry->{'proxyIp'} // '') . ':' + . ($entry->{'proxyPort'} // ''); + } + osh_debug($check_debug_msg); $entry->{'ip'} or next; # can't be empty @@ -209,6 +236,54 @@ sub is_access_way_granted { } } + # check proxy user if we have a proxy ip + if (defined $wantedProxyIp && !$ignoreProxyUser) { + if ($exactUserMatch) { + # we want an exact match + if (not defined $entry->{'proxyUser'}) { + if (not defined $wantedProxyUser) { + ; # both undefined ? ok + } + else { + next; # if only one of two is undef, it's not an exact match + } + } + else { + if (not defined $wantedProxyUser) { + next; # if only one of two is undef, it's not an exact match + } + else { + next if ($wantedProxyUser ne $entry->{'proxyUser'}); # both defined but unequal, not a match + } + } + } + else { + # we don't want an exact match (aka proxy-user-any allowed) + if (not defined $entry->{'proxyUser'}) { + ; # it's a wildcard, will always match + } + else { + if (not defined $wantedProxyUser) { + next; # we want a wildcard, but we don't have it + } + else { + # handle the case where $entry->{'proxyUser'} contains wildcards such as '?' or '*' + if (index($entry->{'proxyUser'}, '*') >= 0 || index($entry->{'proxyUser'}, '?') >= 0) { + # turn wildcards into a regexp + my $entryUserRe = quotemeta($entry->{'proxyUser'}); + $entryUserRe =~ s{\\\?}{.}g; + $entryUserRe =~ s{\\\*}{.*}g; + next if ($wantedProxyUser !~ /^$entryUserRe$/); + } + else { + # doesn't contain a wildcard, simple comparison + next if ($wantedProxyUser ne $entry->{'proxyUser'}); # both defined but unequal, not a match + } + } + } + } + } + # then, check IP my $isIPv6 = (index($entry->{'ip'}, ':') != -1); my $isNetblock = (index($entry->{'ip'}, '/') != -1); @@ -217,12 +292,32 @@ sub is_access_way_granted { if ($exactIpMatch && !$isIPv6) { next if ($entry->{'ip'} ne $wantedIp); + # Case 1: proxy parameters are requested + if (defined $wantedProxyIp) { + # if proxy parameters are requested, the entry must have proxy info + if (!defined $entry->{'proxyIp'} || !defined $entry->{'proxyPort'}) { + next; + } + + # check proxy IP match + next if ($entry->{'proxyIp'} ne $wantedProxyIp); + + # check proxy port match + next if ($entry->{'proxyPort'} ne $wantedProxyPort); + } + # Case 2: no proxy requested, but entry has proxy info - should not match + elsif (defined $entry->{'proxyIp'} || defined $entry->{'proxyPort'}) { + next; # entry has proxy info but no proxy was requested + } + # here, we got a perfect match %match = ( forceKey => $entry->{'forceKey'}, forcePassword => $entry->{'forcePassword'}, ip => $entry->{'ip'}, comment => $entry->{'userComment'}, + proxyIp => $entry->{'proxyIp'}, + proxyPort => $entry->{'proxyPort'}, size => undef, # not needed ); last; # perfect match, don't search further @@ -236,12 +331,37 @@ sub is_access_way_granted { my $ipCheck = Net::Netmask->new2($entry->{'ip'}); if ($ipCheck && ($isNetblock ? $ipCheck->contains($wantedIp) : $ipCheck->match($wantedIp))) { osh_debug("... we got a subnet match!"); - if (not defined $match{'size'} or $ipCheck->size() < $match{'size'}) { + + # check proxy parameters + my $proxyMatches = 1; + + # Case 1: proxy parameters are requested + if (defined $wantedProxyIp) { + # if proxy parameters are requested, the entry must have proxy info + if (!defined $entry->{'proxyIp'} || !defined $entry->{'proxyPort'}) { + $proxyMatches = 0; + } + else { + # check proxy IP match + $proxyMatches = 0 if ($entry->{'proxyIp'} ne $wantedProxyIp); + + # check proxy port match + $proxyMatches = 0 if ($proxyMatches && $entry->{'proxyPort'} ne $wantedProxyPort); + } + } + # Case 2: no proxy requested, but entry has proxy info - should not match + elsif (defined $entry->{'proxyIp'} || defined $entry->{'proxyPort'}) { + $proxyMatches = 0; + } + + if ($proxyMatches && (not defined $match{'size'} or $ipCheck->size() < $match{'size'})) { %match = ( forceKey => $entry->{'forceKey'}, forcePassword => $entry->{'forcePassword'}, ip => $entry->{'ip'}, comment => $entry->{'userComment'}, + proxyIp => $entry->{'proxyIp'}, + proxyPort => $entry->{'proxyPort'}, size => $ipCheck->size(), ); last if $match{'size'} == 1; # we won't get better than this @@ -252,14 +372,41 @@ sub is_access_way_granted { # it's a single ipv4, so a simple strcmp() does the trick if ($entry->{'ip'} eq $wantedIp) { osh_debug("... we got a singleip match!"); - %match = ( - forceKey => $entry->{'forceKey'}, - forcePassword => $entry->{'forcePassword'}, - ip => $entry->{'ip'}, - comment => $entry->{'userComment'}, - size => 1, - ); - last; + + # check proxy parameters + my $proxyMatches = 1; + + # Case 1: proxy parameters are requested + if (defined $wantedProxyIp) { + # if proxy parameters are requested, the entry must have proxy info + if (!defined $entry->{'proxyIp'} && !defined $entry->{'proxyPort'}) { + $proxyMatches = 0; + } + else { + # check proxy IP match + next if ($entry->{'proxyIp'} ne $wantedProxyIp); + + # check proxy port match + next if ($entry->{'proxyPort'} ne $wantedProxyPort); + } + } + # Case 2: no proxy requested, but entry has proxy info - should not match + elsif (defined $entry->{'proxyIp'} || defined $entry->{'proxyPort'}) { + $proxyMatches = 0; + } + + if ($proxyMatches) { + %match = ( + forceKey => $entry->{'forceKey'}, + forcePassword => $entry->{'forcePassword'}, + ip => $entry->{'ip'}, + comment => $entry->{'userComment'}, + proxyIp => $entry->{'proxyIp'}, + proxyPort => $entry->{'proxyPort'}, + size => 1, + ); + last; + } } } } @@ -273,6 +420,8 @@ sub is_access_way_granted { forceKey => $match{'forceKey'}, forcePassword => $match{'forcePassword'}, comment => $match{'comment'}, + proxyIp => $match{'proxyIp'}, + proxyPort => $match{'proxyPort'}, } ); } @@ -581,7 +730,7 @@ sub print_acls { # also take this opportunity to remember the longest field for each column my @printRows; my @jsonRows; - my @columnNames = qw( IP PORT USER ACCESS-BY ADDED-BY ADDED-AT EXPIRY? COMMENT FORCED-KEY FORCED-PASSWORD); + my @columnNames = qw( IP PORT USER ACCESS-BY ADDED-BY ADDED-AT EXPIRY? COMMENT FORCED-KEY FORCED-PASSWORD VIA); my @printColumnLength = map { length } @columnNames; foreach my $contextAcl (@$acls) { my $type = $contextAcl->{'type'}; @@ -597,6 +746,13 @@ sub print_acls { $addedDate = substr($addedDate, 0, 10); my $forceKey = $entry->{'forceKey'} || '-'; my $forcePassword = $entry->{'forcePassword'} || '-'; + + my $via = '-'; + if ($entry->{'proxyIp'}) { + $via = + ($entry->{'proxyUser'} ? $entry->{'proxyUser'} : '*') . "\@$entry->{'proxyIp'}:$entry->{'proxyPort'}"; + } + my $expiry = $entry->{'expiry'} ? (duration2human(seconds => ($entry->{'expiry'} - time()))->value->{'human'}) : '-'; @@ -610,7 +766,8 @@ sub print_acls { $entry->{'user'} ? $entry->{'user'} : '*', $accessType, $addedBy, $addedDate, $expiry, $entry->{'userComment'} || '-', - $forceKey, $forcePassword + $forceKey, $forcePassword, + $via ); # if we have includes or excludes, match fields against the built regex @@ -729,6 +886,10 @@ sub is_access_granted { my $listOnly = $params{'listOnly'}; # don't open the files, just return file names my $noexec = $params{'noexec'}; # passed to is_valid_public_key + my $proxyIp = $params{'proxyIp'}; # if defined, means we look for a proxyIp match too + my $proxyPort = $params{'proxyPort'}; # if defined, means we look for a proxyPort match too + my $proxyUser = $params{'proxyUser'}; # if undef, means we look for a proxy-user wildcard allow + my $details = delete $params{'details'}; # if set, look for and return ssh keys + config data along with allowed accesses delete $params{'way'}; # WE specify this parameter, not our caller @@ -745,6 +906,14 @@ sub is_access_granted { msg => "Can't connect you to $ip as it's part of the forbidden networks of this bastion (see --osh info)") if $fnret->is_ok; + if (defined $proxyIp) { + $fnret = _is_in_any_net(ip => $proxyIp, networks => $forbiddenNetworks); + return R('KO_ACCESS_DENIED', + msg => + "Can't connect you to $ip via proxy $proxyIp as it's part of the forbidden networks of this bastion (see --osh info)" + ) if $fnret->is_ok; + } + # 0b/3 check if we're not outside of the bastion allowed networks, if we are, just bail out my $allowedNetworks = OVH::Bastion::config('allowedNetworks')->value; if (@$allowedNetworks) { @@ -752,6 +921,14 @@ sub is_access_granted { return R('KO_ACCESS_DENIED', msg => "Can't connect you to $ip as it's not part of the allowed networks of this bastion (see --osh info)") if $fnret->is_ko; + + if (defined $proxyIp) { + $fnret = _is_in_any_net(ip => $proxyIp, networks => $allowedNetworks); + return R('KO_ACCESS_DENIED', + msg => + "Can't connect you to $ip via proxy $proxyIp as it's not part of the allowed networks of this bastion (see --osh info)" + ) if $fnret->is_ko; + } } # 0c/3 check if there are more complex "ingressToEgressRules" defined, and potentially bail out whether needed @@ -775,6 +952,19 @@ sub is_access_granted { warn("Denied access due to potential configuration error in ingressToEgressRules (rule #$ruleNb, egress"); return R('KO_ACCESS_DENIED', msg => "Error checking ingressToEgressRules, warn your bastion admin!"); } + if (defined $proxyIp) { + my $fnret2 = _is_in_any_net(ip => $proxyIp, networks => $outNets); + if ($fnret2->is_err) { + warn( + "Denied access due to potential configuration error in ingressToEgressRules (rule #$ruleNb, egress proxy" + ); + return R('KO_ACCESS_DENIED', msg => "Error checking ingressToEgressRules, warn your bastion admin!"); + } + if ($fnret2->is_ok) { + # proxy egress matches too, we consider the egress matches + $fnret = $fnret2; + } + } if ($policy eq 'ALLOW-EXCLUSIVE') { if ($fnret->is_ok) { @@ -929,7 +1119,14 @@ sub is_access_granted { return R('OK', value => \@grants) if @grants; - my $machine = OVH::Bastion::machine_display(ip => $ip, port => $port, user => $user)->value; + my $machine = OVH::Bastion::machine_display( + ip => $ip, + port => $port, + user => $user, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser + )->value; return R('KO_ACCESS_DENIED', msg => "Access denied for $account to $machine"); } @@ -938,9 +1135,12 @@ sub ssh_test_access_way { my $account = $params{'account'}; my $group = $params{'group'}; - my $port = $params{'port'}; - my $ip = $params{'ip'}; - my $user = $params{'user'}; + my $port = $params{'port'}; + my $ip = $params{'ip'}; + my $user = $params{'user'}; + my $proxyIp = $params{'proxyIp'}; + my $proxyPort = $params{'proxyPort'}; + my $proxyUser = $params{'proxyUser'}; my $forceKey = $params{'forceKey'}; my $fnret; @@ -962,6 +1162,21 @@ sub ssh_test_access_way { $port = $fnret->value; } + if (defined $proxyIp) { + $fnret = OVH::Bastion::is_valid_ip(ip => $proxyIp, allowSubnets => 0); + $fnret or return $fnret; + $proxyIp = $fnret->value->{'ip'}; + + $fnret = OVH::Bastion::is_valid_port(port => $proxyPort); + $fnret or return $fnret; + $proxyPort = $fnret->value; + + $proxyUser = OVH::Bastion::get_user_from_env()->value if not $proxyUser; # no proxyUser or account ? get from env then + $fnret = OVH::Bastion::is_valid_remote_user(user => $proxyUser, allowWildcards => 1); + $fnret or return $fnret; + $proxyUser = $fnret->value; + } + $user = OVH::Bastion::config("defaultLogin")->value if not $user; $user = $account if not $user; # defaultLogin empty means the user himself $user = OVH::Bastion::get_user_from_env()->value if not $user; # no user or account ? get from env then @@ -1029,9 +1244,35 @@ sub ssh_test_access_way { # add port when specified push @command, ("-p", $port) if $port; - push @command, "-l", $user, $ip, '-T', '--', 'true'; + push @command, "-l", $user, $ip; + + # add proxyjump when specified + if (defined $proxyIp) { + my @proxyCommand = qw{ ssh -o ConnectTimeout=5 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no }; + push @proxyCommand, '-o', 'PreferredAuthentications=' . $preferredAuthentications; + foreach (@keyList) { + push @proxyCommand, "-i", $_; + } + push @proxyCommand, "-p", $proxyPort if $proxyPort && $proxyPort != 22; + push @proxyCommand, '-l', $proxyUser, '-W', '%h:%p', $proxyIp; + + my $proxyCommandStr = join(' ', map { /\s/ ? "'$_'" : $_ } @proxyCommand); + osh_debug("Testing with ProxyCommand: $proxyCommandStr"); + + push @command, '-o', "ProxyCommand=$proxyCommandStr"; + + $ENV{'OSH_PROXYJUMP_CONNECTION'} = 1; + } + + push @command, '-T', '--', 'true'; - osh_info("Testing connection to $user\@$ip, please wait..."); + my $connection_info_msg = "Testing connection to $user\@$ip"; + if (defined $proxyIp) { + $connection_info_msg .= " via proxy $proxyIp"; + } + $connection_info_msg .= ", please wait..."; + + osh_info($connection_info_msg); $fnret = OVH::Bastion::execute(cmd => \@command, noisy_stderr => 1); $fnret or return $fnret; @@ -1291,8 +1532,9 @@ sub _get_acl_from_file { my @entries; foreach my $line (@lines) { my ( - $ip, $user, $port, $comment, $forceKey, $forcePassword, - $expiry, $addedBy, $addedDate, $extra, $comment, $userComment + $ip, $user, $port, $comment, $forceKey, + $forcePassword, $expiry, $addedBy, $addedDate, $extra, + $comment, $userComment, $proxyIp, $proxyPort, $proxyUser ); # extract comment if any @@ -1382,6 +1624,33 @@ sub _get_acl_from_file { $forcePassword = $fnret->value->{'hash'}; osh_debug("found a valid forced password <$forcePassword>"); } + if ($comment =~ s/# PROXYHOST=(\S+)//) { + $fnret = OVH::Bastion::is_valid_ip(ip => $1, allowSubnets => 0); + if (!$fnret) { + osh_debug("skipping line <$line> because invalid proxy host IP ($1) found"); + next; + } + $proxyIp = $fnret->value->{'ip'}; + osh_debug("found a valid proxy host <$proxyIp>"); + } + if ($comment =~ s/# PROXYPORT=(\d+)//) { + $fnret = OVH::Bastion::is_valid_port(port => $1); + if (!$fnret) { + osh_debug("skipping line <$line> because invalid proxy port ($1) found"); + next; + } + $proxyPort = $fnret->value; + osh_debug("found a valid proxy port <$proxyPort>"); + } + if ($comment =~ s/# PROXYUSER=(\S+)//) { + $fnret = OVH::Bastion::is_valid_remote_user(user => $1, allowWildcards => 1); + if (!$fnret) { + osh_debug("skipping line <$line> because invalid proxy user ($1) found"); + next; + } + $proxyUser = $fnret->value; + osh_debug("found a valid proxy user <$proxyUser>"); + } if ($comment =~ s/# COMMENT=<([^>]+)>//) { $userComment = $1; } @@ -1404,6 +1673,9 @@ sub _get_acl_from_file { addedDate => $addedDate, userComment => $userComment, comment => $extra, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser }; } diff --git a/lib/perl/OVH/Bastion/allowkeeper.inc b/lib/perl/OVH/Bastion/allowkeeper.inc index fda6058..fa3eec3 100644 --- a/lib/perl/OVH/Bastion/allowkeeper.inc +++ b/lib/perl/OVH/Bastion/allowkeeper.inc @@ -340,6 +340,10 @@ sub access_modify { my $forceKey = $params{'forceKey'}; my $forcePassword = $params{'forcePassword'}; + my $proxyIp = $params{'proxyIp'}; + my $proxyPort = $params{'proxyPort'}; + my $proxyUser = $params{'proxyUser'}; + my $dryrun = $params{'dryrun'}; # don't do anything, just check params and prereqs my $sudo = $params{'sudo'}; # passed as-is to subs we use @@ -368,8 +372,9 @@ sub access_modify { # normalize * into undef # also, due to how plugins work, sometimes user and port are just '', make them undef in those cases - undef $user if (defined $user && ($user eq '*' || $user eq '')); - undef $port if (defined $port && ($port eq '*' || $port eq '')); + undef $user if (defined $user && ($user eq '*' || $user eq '')); + undef $port if (defined $port && ($port eq '*' || $port eq '')); + undef $proxyUser if (defined $proxyUser && ($proxyUser eq '*' || $proxyUser eq '')); # check way if ($way eq 'personal') { @@ -469,6 +474,35 @@ sub access_modify { } } + # check proxy host + if (defined $proxyIp) { + $fnret = OVH::Bastion::is_valid_ip(ip => $proxyIp, allowSubnets => 0); + return $fnret unless $fnret; + $proxyIp = $fnret->value->{'ip'}; + } + + # check proxy port + if (defined $proxyPort) { + $fnret = OVH::Bastion::is_valid_port(port => $proxyPort); + return $fnret unless $fnret; + $proxyPort = $fnret->value; + } + + # check proxy user + if (defined $proxyUser) { + $fnret = OVH::Bastion::is_valid_remote_user(user => $proxyUser, allowWildcards => 1); + return $fnret unless $fnret; + $proxyUser = $fnret->value; + } + + # Validate proxy configuration: if PROXYHOST is specified, PROXYPORT must be too + if (defined $proxyIp && !defined $proxyPort) { + return R('ERR_INVALID_PARAMETER', msg => "When specifying a proxy host, proxy port must also be specified"); + } + if (!defined $proxyIp && defined $proxyPort) { + return R('ERR_INVALID_PARAMETER', msg => "When specifying a proxy port, proxy host must also be specified"); + } + # check if the caller has the right to make the change they're asking # ... 1. either $> is allowkeeper and $ENV{'SUDO_USER'} is the requesting account # ... 2. or $> is $grouptomodify and $ENV{'SUDO_USER'} is the requesting account @@ -549,6 +583,9 @@ sub access_modify { way => $way, group => $shortGroup, account => $account, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser, exactMatch => 1, # we're checking if the exact right we're asked to modify exists or not ); osh_debug("... result is $fnret"); @@ -631,6 +668,15 @@ sub access_modify { if ($ttl) { $entry .= " # EXPIRY=" . (time() + $ttl); } + if ($proxyIp) { + $entry .= " # PROXYHOST=" . $proxyIp; + } + if ($proxyPort) { + $entry .= " # PROXYPORT=" . $proxyPort; + } + if ($proxyUser) { + $entry .= " # PROXYUSER=" . $proxyUser; + } if ($comment) { $comment =~ s{[#<>\\"']}{_}g; $entry .= " # COMMENT=<" . $comment . ">"; @@ -652,7 +698,14 @@ sub access_modify { else { return R('ERR_CANNOT_OPEN_FILE', msg => "Error opening $file: $!"); } - my $machine = OVH::Bastion::machine_display(ip => $ip, port => $port, user => $user)->value; + my $machine = OVH::Bastion::machine_display( + ip => $ip, + port => $port, + user => $user, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser, + )->value; my $ttlmsg = $ttl ? (' (expires in ' . OVH::Bastion::duration2human(seconds => $ttl)->value->{'human'} . ')') : ''; $returnmsg = "Access to $machine successfully added$ttlmsg"; @@ -663,9 +716,35 @@ sub access_modify { my $found = 0; while (my $line = <$fh_file>) { if ($line =~ m{^\Q$entry\E(\s|$)}) { - chomp $line; - $line = "# $line # $comment\n"; - $found++; + # now verify that proxy options match too + my $shouldDelete = 1; + + if (defined $proxyIp) { + $shouldDelete = 0 unless $line =~ m{\# PROXYHOST=\Q$proxyIp\E(\s|$)}; + } + elsif ($line =~ m{\# PROXYHOST=}) { + $shouldDelete = 0; + } + + if ($shouldDelete && defined $proxyPort) { + $shouldDelete = 0 unless $line =~ m{\# PROXYPORT=\Q$proxyPort\E(\s|$)}; + } + elsif ($shouldDelete && $line =~ m{\# PROXYPORT=}) { + $shouldDelete = 0; + } + + if ($shouldDelete && defined $proxyUser) { + $shouldDelete = 0 unless $line =~ m{\# PROXYUSER=\Q$proxyUser\E(\s|$)}; + } + elsif ($shouldDelete && $line =~ m{\# PROXYUSER=}) { + $shouldDelete = 0; + } + + if ($shouldDelete) { + chomp $line; + $line = "# $line # $comment\n"; + $found++; + } } $newFile .= $line; } @@ -677,7 +756,14 @@ sub access_modify { if (open(my $fh_file, '>', $file)) { print $fh_file $newFile; close($fh_file); - my $machine = OVH::Bastion::machine_display(ip => $ip, port => $port, user => $user)->value; + my $machine = OVH::Bastion::machine_display( + ip => $ip, + port => $port, + user => $user, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + proxyUser => $proxyUser, + )->value; $returnmsg = "Access to $machine successfully removed"; } else { @@ -704,6 +790,9 @@ sub access_modify { ['force_key', $params{'forceKey'}], ['force_password', $params{'forcePassword'}], ['comment', $params{'comment'}], + ['proxy_ip', $params{'proxyIp'}], + ['proxy_port', $params{'proxyPort'}], + ['proxy_user', $params{'proxyUser'}], ] ); return R('OK', msg => $returnmsg) if $returnmsg; diff --git a/lib/perl/OVH/Bastion/configuration.inc b/lib/perl/OVH/Bastion/configuration.inc index cab1d0d..271d604 100644 --- a/lib/perl/OVH/Bastion/configuration.inc +++ b/lib/perl/OVH/Bastion/configuration.inc @@ -156,7 +156,8 @@ sub load_configuration { {name => 'syslogDescription', default => 'bastion', validre => qr/^([a-zA-Z0-9_.-]+)$/}, { name => 'ttyrecFilenameFormat', - default => '%Y-%m-%d.%H-%M-%S.#usec#.&uniqid.&account.&user.&ip.&port.ttyrec', + default => + '%Y-%m-%d.%H-%M-%S.#usec#.&uniqid.&account.&user.&ip.&port.via.&proxyuser.&proxyip.&proxyport.ttyrec', validre => qr/^([a-zA-Z0-9%&#_.-]+)$/ }, {name => 'accountExpiredMessage', default => '', validre => qr/^(.*)$/, emptyok => 1}, @@ -277,6 +278,7 @@ sub load_configuration { interactiveModeAllowed readOnlySlaveMode sshClientHasOptionE ingressKeysFromAllowOverride moshAllowed debug keyboardInteractiveAllowed passwordAllowed telnetAllowed remoteCommandEscapeByDefault accountExternalValidationDenyOnFailure ingressRequirePIV IPv6Allowed sshAddKeysToAgentAllowed + egressProxyJumpAllowed } ], } diff --git a/lib/perl/OVH/Bastion/log.inc b/lib/perl/OVH/Bastion/log.inc index a408a43..568a992 100644 --- a/lib/perl/OVH/Bastion/log.inc +++ b/lib/perl/OVH/Bastion/log.inc @@ -223,6 +223,29 @@ EOS # endof version==0 if ($user_version == 1) { + # add proxy columns for proxyjump feature + my $table = ($sqltype eq 'local' ? "connections" : "connections_summary"); + + $dbh->do("ALTER TABLE $table ADD COLUMN proxyuser TEXT") + or return R('KO', msg => "adding proxyuser column to $table"); + + $dbh->do("ALTER TABLE $table ADD COLUMN proxyip TEXT") + or return R('KO', msg => "adding proxyip column to $table"); + + $dbh->do("ALTER TABLE $table ADD COLUMN proxyhost TEXT") + or return R('KO', msg => "adding proxyhost column to $table"); + + $dbh->do("ALTER TABLE $table ADD COLUMN proxyport INTEGER") + or return R('KO', msg => "adding proxyport column to $table"); + + $dbh->do("PRAGMA user_version=2") + or return R('KO', msg => "setting user_version to 2"); + $user_version = 2; + } + + # endof version==1 + + if ($user_version == 2) { ; # insert here future schema modifications } @@ -254,6 +277,10 @@ sub _sql_log_insert_file { my $timestampusec = $params{'timestampusec'}; my $uniqid = $params{'uniqid'}; my $sqltype = $params{'sqltype'}; + my $proxyuser = $params{'proxyuser'}; + my $proxyip = $params{'proxyip'}; + my $proxyhost = $params{'proxyhost'}; + my $proxyport = $params{'proxyport'}; if ($sqltype ne 'local' and $sqltype ne 'global') { return R('ERR_INVALID_PARAMETER', msg => "Invalid parameter sqltype"); @@ -304,20 +331,24 @@ sub _sql_log_insert_file { my @execute; if ($sqltype eq 'local') { @execute = ( - $uniqid, $timestamp, $timestampusec, $account, $cmdtype, $allowed, $hostfrom, - $ipfrom, $portfrom, $bastionip, $bastionport, $hostto, $ipto, $portto, - $user, $plugin, $params, $comment, $ttyrecfile + $uniqid, $timestamp, $timestampusec, $account, $cmdtype, $allowed, + $hostfrom, $ipfrom, $portfrom, $bastionip, $bastionport, $hostto, + $ipto, $portto, $user, $plugin, $params, $comment, + $ttyrecfile, $proxyuser, $proxyip, $proxyhost, $proxyport ); $prepare = "INSERT INTO connections" - . "(uniqid,timestamp,timestampusec,account,cmdtype,allowed,hostfrom,ipfrom,portfrom,bastionip,bastionport,hostto,ipto,portto,user,plugin,params,comment,ttyrecfile)" + . "(uniqid,timestamp,timestampusec,account,cmdtype,allowed,hostfrom,ipfrom,portfrom,bastionip,bastionport,hostto,ipto,portto,user,plugin,params,comment,ttyrecfile,proxyuser,proxyip,proxyhost,proxyport)" . 'VALUES (' . ('?,' x (@execute - 1)) . '?)'; } elsif ($sqltype eq 'global') { - @execute = ($uniqid, $timestamp, $account, $cmdtype, $allowed, $ipfrom, $ipto, $portto, $user, $plugin); + @execute = ( + $uniqid, $timestamp, $account, $cmdtype, $allowed, $ipfrom, $ipto, + $portto, $user, $plugin, $proxyuser, $proxyhost, $proxyip, $proxyport + ); $prepare = - "INSERT INTO connections_summary (uniqid,timestamp,account,cmdtype,allowed,ipfrom,ipto,portto,user,plugin)" + "INSERT INTO connections_summary (uniqid,timestamp,account,cmdtype,allowed,ipfrom,ipto,portto,user,plugin,proxyuser,proxyip,proxyhost,proxyport)" . 'VALUES (' . ('?,' x (@execute - 1)) . '?)'; } @@ -405,6 +436,9 @@ sub log_access_insert { if (not defined $params{'hostfrom'} and defined $params{'ipfrom'}) { $params{'hostfrom'} = OVH::Bastion::ip2host($params{'ipfrom'})->value; } + if (not defined $params{'proxyhost'} and defined $params{'proxyip'}) { + $params{'proxyhost'} = OVH::Bastion::ip2host($params{'proxyip'})->value; + } my ($timestamp, $timestampusec) = Time::HiRes::gettimeofday(); $params{'timestamp'} = $timestamp; @@ -475,6 +509,10 @@ sub log_access_insert { ['accountsql', $accountsql], ['comment', $params{'comment'}], ['params', $params{'params'}], + ['proxyuser', $params{'proxyuser'}], + ['proxyip', $params{'proxyip'}], + ['proxyhost', $params{'proxyhost'}], + ['proxyport', $params{'proxyport'}], ); if (ref $custom eq 'ARRAY') { foreach my $item (@$custom) { @@ -771,6 +809,9 @@ sub _sql_log_fetch_from_file { my $portto = $params{'portto'}; my $bastionip = $params{'bastionip'}; my $bastionport = $params{'bastionport'}; + my $proxyuser = $params{'proxyuser'}; + my $proxyip = $params{'proxyip'}; + my $proxyport = $params{'proxyport'}; my $user = $params{'user'}; my $plugin = $params{'plugin'}; my $before = $params{'before'}; @@ -828,6 +869,18 @@ sub _sql_log_fetch_from_file { push @conditions, "bastionport = ?"; push @execvalues, $bastionport; } + if ($proxyuser) { + push @conditions, "proxyuser = ?"; + push @execvalues, $proxyuser; + } + if ($proxyip) { + push @conditions, "proxyip = ?"; + push @execvalues, $proxyip; + } + if ($proxyport) { + push @conditions, "proxyport = ?"; + push @execvalues, $proxyport; + } if ($before) { push @conditions, "timestamp <= ?"; push @execvalues, $before; diff --git a/tests/functional/tests.d/347-proxyjump.sh b/tests/functional/tests.d/347-proxyjump.sh new file mode 100644 index 0000000..f9da0c5 --- /dev/null +++ b/tests/functional/tests.d/347-proxyjump.sh @@ -0,0 +1,479 @@ +# vim: set filetype=sh ts=4 sw=4 sts=4 et: +# shellcheck shell=bash +# shellcheck disable=SC2317,SC2086,SC2016,SC2046 +# below: convoluted way that forces shellcheck to source our caller +# shellcheck source=tests/functional/launch_tests_on_instance.sh +. "$(dirname "${BASH_SOURCE[0]}")"/dummy + +testsuite_proxyjump() +{ + # Create test accounts + success a0_create_a1 $a0 --osh accountCreate --always-active --account $account1 --uid $uid1 --public-key "\"$(cat $account1key1file.pub)\"" + json .error_code OK .command accountCreate + + success a0_create_a2 $a0 --osh accountCreate --always-active --account $account2 --uid $uid2 --public-key "\"$(cat $account2key1file.pub)\"" + json .error_code OK .command accountCreate + + # Create a test group + success a0_create_group1 $a0 --osh groupCreate --group $group1 --owner $account1 --algo ed25519 --size 256 + json .error_code OK .command groupCreate + + # Test that proxy parameters are rejected while the feature is disabled + run selfAddPersonalAccess_proxy_feature_disabled $a0 --osh selfAddPersonalAccess --host 192.168.1.100 --user testuser --port 22 --proxy-host 10.0.0.1 --proxy-port 22 --proxy-user testuser --force + retvalshouldbe 100 + contain "ProxyJump egress connections are disabled by policy" + json .error_code ERR_INVALID_PARAMETER + + # now enable the proxyjump feature + configchg 's=^\\\\x22egressProxyJumpAllowed\\\\x22.+=\\\\x22egressProxyJumpAllowed\\\\x22:true,=' + + # + # Test selfAddPersonalAccess with proxy parameters + # + + # Test basic proxy parameter + success selfAddPersonalAccess_with_proxy_host $a0 --osh selfAddPersonalAccess --host 192.168.1.100 --user testuser --port 22 --proxy-host 10.0.0.1 --proxy-port 22 --proxy-user testuser --force + json .command selfAddPersonalAccess .error_code OK .value.ip 192.168.1.100 .value.user testuser .value.port 22 .value.proxyIp 10.0.0.1 .value.proxyPort 22 + + # Test with hostname as proxy-host + success selfAddPersonalAccess_with_proxy_hostname $a0 --osh selfAddPersonalAccess --host 192.168.1.102 --user testuser --port 22 --proxy-host localhost --proxy-port 22 --proxy-user testuser --force + json .command selfAddPersonalAccess .error_code OK .value.ip 192.168.1.102 .value.user testuser .value.port 22 .value.proxyIp 127.0.0.1 + + # Test invalid proxy-host + plgfail selfAddPersonalAccess_invalid_proxy_host $a0 --osh selfAddPersonalAccess --host 192.168.1.103 --user testuser --port 22 --proxy-host "invalid..host..name" --proxy-port 22 --proxy-user testuser --force + json .command selfAddPersonalAccess .error_code ERR_HOST_NOT_FOUND + + # Test proxy-port without proxy-host + plgfail selfAddPersonalAccess_proxy_port_without_host $a0 --osh selfAddPersonalAccess --host 192.168.1.104 --user testuser --port 22 --proxy-port 2222 --force + json .command selfAddPersonalAccess .error_code ERR_MISSING_PARAMETER + + # Test proxy-host without proxy-port + plgfail selfAddPersonalAccess_proxy_host_without_port $a0 --osh selfAddPersonalAccess --host 192.168.1.107 --user testuser --port 22 --proxy-host 10.0.0.1 --proxy-user testuser --force + json .command selfAddPersonalAccess .error_code ERR_MISSING_PARAMETER + + # Test invalid proxy-port + plgfail selfAddPersonalAccess_invalid_proxy_port $a0 --osh selfAddPersonalAccess --host 192.168.1.105 --user testuser --port 22 --proxy-host 10.0.0.1 --proxy-port abc --proxy-user testuser --force + json .command selfAddPersonalAccess .error_code ERR_INVALID_PARAMETER + + # Test invalid proxy-port (out of range) + plgfail selfAddPersonalAccess_proxy_port_out_of_range $a0 --osh selfAddPersonalAccess --host 192.168.1.106 --user testuser --port 22 --proxy-host 10.0.0.1 --proxy-port 99999 --proxy-user testuser --force + json .command selfAddPersonalAccess .error_code ERR_INVALID_PARAMETER + + # Test proxy-host and proxy-port without proxy-user (should fail) + plgfail selfAddPersonalAccess_proxy_without_user $a0 --osh selfAddPersonalAccess --host 192.168.1.108 --user testuser --port 22 --proxy-host 10.0.0.1 --proxy-port 22 --force + json .command selfAddPersonalAccess .error_code ERR_MISSING_PARAMETER + contain "When --proxy-host is specified, --proxy-user becomes mandatory" + + # + # Test selfAddPersonalAccess with proxy-user parameter + # + + # Test basic proxy-user parameter + success selfAddPersonalAccess_with_proxy_user $a0 --osh selfAddPersonalAccess --host 192.168.1.110 --user testuser --port 22 --proxy-host 10.0.0.1 --proxy-port 22 --proxy-user proxyuser --force + json .command selfAddPersonalAccess .error_code OK .value.ip 192.168.1.110 .value.user testuser .value.port 22 .value.proxyIp 10.0.0.1 .value.proxyPort 22 .value.proxyUser proxyuser + + # Test proxy-user wildcard + success selfAddPersonalAccess_with_proxy_user_wildcard $a0 --osh selfAddPersonalAccess --host 192.168.1.111 --user testuser --port 22 --proxy-host 10.0.0.1 --proxy-port 22 --proxy-user '*' --force + json .command selfAddPersonalAccess .error_code OK .value.ip 192.168.1.111 .value.user testuser .value.port 22 .value.proxyIp 10.0.0.1 .value.proxyPort 22 + + # Test invalid proxy-user + plgfail selfAddPersonalAccess_invalid_proxy_user $a0 --osh selfAddPersonalAccess --host 192.168.1.112 --user testuser --port 22 --proxy-host 10.0.0.1 --proxy-port 22 --proxy-user "inväliduse{r" --force + json .command selfAddPersonalAccess .error_code ERR_INVALID_PARAMETER + + # Clean up proxy-user test entries + success selfDelPersonalAccess_with_proxy_user $a0 --osh selfDelPersonalAccess --host 192.168.1.110 --user testuser --port 22 --proxy-host 10.0.0.1 --proxy-port 22 --proxy-user proxyuser + json .command selfDelPersonalAccess .error_code OK + + success selfDelPersonalAccess_with_proxy_user_wildcard $a0 --osh selfDelPersonalAccess --host 192.168.1.111 --user testuser --port 22 --proxy-host 10.0.0.1 --proxy-port 22 --proxy-user '*' + json .command selfDelPersonalAccess .error_code OK + + # + # Test accountAddPersonalAccess with proxy parameters + # + + # Test basic proxy-host parameter + success accountAddPersonalAccess_with_proxy_host $a0 --osh accountAddPersonalAccess --account $account2 --host 192.168.2.100 --user testuser --port 22 --proxy-host 10.0.0.2 --proxy-port 22 --proxy-user testuser + json .command accountAddPersonalAccess .error_code OK .value.ip 192.168.2.100 .value.user testuser .value.port 22 .value.proxyIp 10.0.0.2 + + # Test with proxy hostname + success accountAddPersonalAccess_with_proxy_port $a0 --osh accountAddPersonalAccess --account $account2 --host 192.168.2.101 --user testuser --port 22 --proxy-host localhost --proxy-port 3333 --proxy-user testuser + json .command accountAddPersonalAccess .error_code OK .value.ip 192.168.2.101 .value.user testuser .value.port 22 .value.proxyIp 127.0.0.1 .value.proxyPort 3333 + + # Test proxy-port without proxy-host + plgfail accountAddPersonalAccess_proxy_port_without_host $a0 --osh accountAddPersonalAccess --account $account2 --host 192.168.2.102 --user testuser --port 22 --proxy-port 3333 + json .command accountAddPersonalAccess .error_code ERR_MISSING_PARAMETER + + # Test proxy-host without proxy-port + plgfail accountAddPersonalAccess_proxy_host_without_port $a0 --osh accountAddPersonalAccess --account $account2 --host 192.168.2.102 --user testuser --port 22 --proxy-host 10.0.0.2 --proxy-user testuser + json .command accountAddPersonalAccess .error_code ERR_MISSING_PARAMETER + + # Test proxy-host and proxy-port without proxy-user (should fail) + plgfail accountAddPersonalAccess_proxy_without_user $a0 --osh accountAddPersonalAccess --account $account2 --host 192.168.2.103 --user testuser --port 22 --proxy-host 10.0.0.2 --proxy-port 22 + json .command accountAddPersonalAccess .error_code ERR_MISSING_PARAMETER + contain "When --proxy-host is specified, --proxy-user becomes mandatory" + + # + # Test accountAddPersonalAccess with proxy-user parameter + # + + # Test basic proxy-user parameter + success accountAddPersonalAccess_with_proxy_user $a0 --osh accountAddPersonalAccess --account $account2 --host 192.168.2.110 --user testuser --port 22 --proxy-host 10.0.0.2 --proxy-port 22 --proxy-user proxyuser + json .command accountAddPersonalAccess .error_code OK .value.ip 192.168.2.110 .value.user testuser .value.port 22 .value.proxyIp 10.0.0.2 .value.proxyPort 22 .value.proxyUser proxyuser + + # Test proxy-user wildcard + success accountAddPersonalAccess_with_proxy_user_wildcard $a0 --osh accountAddPersonalAccess --account $account2 --host 192.168.2.111 --user testuser --port 22 --proxy-host 10.0.0.2 --proxy-port 22 --proxy-user '*' + json .command accountAddPersonalAccess .error_code OK .value.ip 192.168.2.111 .value.user testuser .value.port 22 .value.proxyIp 10.0.0.2 .value.proxyPort 22 + + # Clean up proxy-user test entries + success accountDelPersonalAccess_with_proxy_user $a0 --osh accountDelPersonalAccess --account $account2 --host 192.168.2.110 --user testuser --port 22 --proxy-host 10.0.0.2 --proxy-port 22 --proxy-user proxyuser + json .command accountDelPersonalAccess .error_code OK + + success accountDelPersonalAccess_with_proxy_user_wildcard $a0 --osh accountDelPersonalAccess --account $account2 --host 192.168.2.111 --user testuser --port 22 --proxy-host 10.0.0.2 --proxy-port 22 --proxy-user '*' + json .command accountDelPersonalAccess .error_code OK + + # + # Test groupAddServer with proxy parameters + # + + # Test basic proxy-host parameter + success groupAddServer_with_proxy_host $a1 --osh groupAddServer --group $group1 --host 192.168.3.100 --user testuser --port 22 --proxy-host 10.0.0.3 --proxy-port 22 --proxy-user testuser --force + json .command groupAddServer .error_code OK .value.ip 192.168.3.100 .value.user testuser .value.port 22 .value.proxyIp 10.0.0.3 .value.proxyPort 22 + + # Test with proxy hostname + success groupAddServer_with_proxy_port $a1 --osh groupAddServer --group $group1 --host 192.168.3.101 --user testuser --port 22 --proxy-host localhost --proxy-port 4444 --proxy-user testuser --force + json .command groupAddServer .error_code OK .value.ip 192.168.3.101 .value.user testuser .value.port 22 .value.proxyIp 127.0.0.1 .value.proxyPort 4444 + + # Test proxy-port without proxy-host + plgfail groupAddServer_proxy_port_without_host $a1 --osh groupAddServer --group $group1 --host 192.168.3.102 --user testuser --port 22 --proxy-port 4444 --force + json .command groupAddServer .error_code ERR_MISSING_PARAMETER + + # Test proxy-host without proxy-port + plgfail groupAddServer_proxy_host_without_port $a1 --osh groupAddServer --group $group1 --host 192.168.3.102 --user testuser --port 22 --proxy-host 10.0.0.3 --proxy-user testuser --force + json .command groupAddServer .error_code ERR_MISSING_PARAMETER + + # Test invalid proxy-host + plgfail groupAddServer_invalid_proxy_host $a1 --osh groupAddServer --group $group1 --host 192.168.3.103 --user testuser --port 22 --proxy-host "bad...hostname" --proxy-port 22 --proxy-user testuser --force + json .command groupAddServer .error_code ERR_HOST_NOT_FOUND + + # Test proxy-host and proxy-port without proxy-user (should fail) + plgfail groupAddServer_proxy_without_user $a1 --osh groupAddServer --group $group1 --host 192.168.3.104 --user testuser --port 22 --proxy-host 10.0.0.3 --proxy-port 22 --force + json .command groupAddServer .error_code ERR_MISSING_PARAMETER + contain "When --proxy-host is specified, --proxy-user becomes mandatory" + + # + # Test groupAddServer with proxy-user parameter + # + + # Test basic proxy-user parameter + success groupAddServer_with_proxy_user $a1 --osh groupAddServer --group $group1 --host 192.168.3.110 --user testuser --port 22 --proxy-host 10.0.0.3 --proxy-port 22 --proxy-user proxyuser --force + json .command groupAddServer .error_code OK .value.ip 192.168.3.110 .value.user testuser .value.port 22 .value.proxyIp 10.0.0.3 .value.proxyPort 22 .value.proxyUser proxyuser + + # Test proxy-user wildcard + success groupAddServer_with_proxy_user_wildcard $a1 --osh groupAddServer --group $group1 --host 192.168.3.111 --user testuser --port 22 --proxy-host 10.0.0.3 --proxy-port 22 --proxy-user '*' --force + json .command groupAddServer .error_code OK .value.ip 192.168.3.111 .value.user testuser .value.port 22 .value.proxyIp 10.0.0.3 .value.proxyPort 22 + + # Clean up proxy-user test entries + success groupDelServer_with_proxy_user $a1 --osh groupDelServer --group $group1 --host 192.168.3.110 --user testuser --port 22 --proxy-host 10.0.0.3 --proxy-port 22 --proxy-user proxyuser + json .command groupDelServer .error_code OK + + success groupDelServer_with_proxy_user_wildcard $a1 --osh groupDelServer --group $group1 --host 192.168.3.111 --user testuser --port 22 --proxy-host 10.0.0.3 --proxy-port 22 --proxy-user '*' + json .command groupDelServer .error_code OK + + # + # Test deletion of accesses with proxy parameters + # + + # Delete selfAddPersonalAccess entries with missing proxy-port + plgfail selfDelPersonalAccess_without_proxy_port $a0 --osh selfDelPersonalAccess --host 192.168.1.100 --user testuser --port 22 --proxy-host 10.0.0.1 --proxy-user testuser + json .command selfDelPersonalAccess .error_code ERR_MISSING_PARAMETER + + # Delete selfAddPersonalAccess entries with missing proxy-host + plgfail selfDelPersonalAccess_without_proxy_host $a0 --osh selfDelPersonalAccess --host 192.168.1.100 --user testuser --port 22 --proxy-port 22 + json .command selfDelPersonalAccess .error_code ERR_MISSING_PARAMETER + + # Delete selfAddPersonalAccess entries with missing proxy-user + plgfail selfDelPersonalAccess_without_proxy_user $a0 --osh selfDelPersonalAccess --host 192.168.1.100 --user testuser --port 22 --proxy-host 10.0.0.1 --proxy-port 22 + json .command selfDelPersonalAccess .error_code ERR_MISSING_PARAMETER + contain "When --proxy-host is specified, --proxy-user becomes mandatory" + + # Delete selfAddPersonalAccess entries + success selfDelPersonalAccess_with_proxy $a0 --osh selfDelPersonalAccess --host 192.168.1.100 --user testuser --port 22 --proxy-host 10.0.0.1 --proxy-port 22 --proxy-user testuser + json .command selfDelPersonalAccess .error_code OK + + success selfDelPersonalAccess_with_proxy_hostname $a0 --osh selfDelPersonalAccess --host 192.168.1.102 --user testuser --port 22 --proxy-host localhost --proxy-port 22 --proxy-user testuser + json .command selfDelPersonalAccess .error_code OK + + # Delete accountAddPersonalAccess entries with missing proxy-port + plgfail accountDelPersonalAccess_without_proxy_port $a0 --osh accountDelPersonalAccess --account $account2 --host 192.168.2.100 --user testuser --port 22 --proxy-host 10.0.0.2 --proxy-user testuser + json .command accountDelPersonalAccess .error_code ERR_MISSING_PARAMETER + + # Delete accountAddPersonalAccess entries with missing proxy-host + plgfail accountDelPersonalAccess_without_proxy_host $a0 --osh accountDelPersonalAccess --account $account2 --host 192.168.2.100 --user testuser --port 22 --proxy-port 22 + json .command accountDelPersonalAccess .error_code ERR_MISSING_PARAMETER + + # Delete accountAddPersonalAccess entries with missing proxy-user + plgfail accountDelPersonalAccess_without_proxy_user $a0 --osh accountDelPersonalAccess --account $account2 --host 192.168.2.100 --user testuser --port 22 --proxy-host 10.0.0.2 --proxy-port 22 + json .command accountDelPersonalAccess .error_code ERR_MISSING_PARAMETER + contain "When --proxy-host is specified, --proxy-user becomes mandatory" + + # Delete accountAddPersonalAccess entries + success accountDelPersonalAccess_with_proxy $a0 --osh accountDelPersonalAccess --account $account2 --host 192.168.2.100 --user testuser --port 22 --proxy-host 10.0.0.2 --proxy-port 22 --proxy-user testuser + json .command accountDelPersonalAccess .error_code OK + + success accountDelPersonalAccess_with_proxy_hostname $a0 --osh accountDelPersonalAccess --account $account2 --host 192.168.2.101 --user testuser --port 22 --proxy-host localhost --proxy-port 3333 --proxy-user testuser + json .command accountDelPersonalAccess .error_code OK + + # Delete groupAddServer entries with missing proxy-port + plgfail groupDelServer_without_proxy_port $a1 --osh groupDelServer --group $group1 --host 192.168.3.100 --user testuser --port 22 --proxy-host 10.0.0.3 --proxy-user testuser + json .command groupDelServer .error_code ERR_MISSING_PARAMETER + + # Delete groupAddServer entries with missing proxy-host + plgfail groupDelServer_without_proxy_host $a1 --osh groupDelServer --group $group1 --host 192.168.3.100 --user testuser --port 22 --proxy-port 22 + json .command groupDelServer .error_code ERR_MISSING_PARAMETER + + # Delete groupAddServer entries with missing proxy-user + plgfail groupDelServer_without_proxy_user $a1 --osh groupDelServer --group $group1 --host 192.168.3.100 --user testuser --port 22 --proxy-host 10.0.0.3 --proxy-port 22 + json .command groupDelServer .error_code ERR_MISSING_PARAMETER + contain "When --proxy-host is specified, --proxy-user becomes mandatory" + + # Delete groupAddServer entries + success groupDelServer_with_proxy $a1 --osh groupDelServer --group $group1 --host 192.168.3.100 --user testuser --port 22 --proxy-host 10.0.0.3 --proxy-port 22 --proxy-user testuser + json .command groupDelServer .error_code OK + + success groupDelServer_with_proxy_hostname $a1 --osh groupDelServer --group $group1 --host 192.168.3.101 --user testuser --port 22 --proxy-host localhost --proxy-port 4444 --proxy-user testuser + json .command groupDelServer .error_code OK + + # + # Test that proxy information is displayed in access lists + # + + # Add self access with proxy + success add_access_for_list_check $a0 --osh selfAddPersonalAccess --host 192.168.1.200 --user listtest --port 2222 --proxy-host 10.0.0.5 --proxy-port 5555 --proxy-user listtest --force + json .command selfAddPersonalAccess .error_code OK + + # Check that selfListAccesses shows the proxy information + success selfListAccesses_shows_proxy $a0 --osh selfListAccesses + json .command selfListAccesses .error_code OK + json .value[0].acl[0].ip 192.168.1.200 + json .value[0].acl[0].port 2222 + json .value[0].acl[0].user listtest + json .value[0].acl[0].proxyIp 10.0.0.5 + json .value[0].acl[0].proxyPort 5555 + json .value[0].acl[0].proxyUser listtest + + # Clean up + success cleanup_list_test $a0 --osh selfDelPersonalAccess --host 192.168.1.200 --user listtest --port 2222 --proxy-host 10.0.0.5 --proxy-port 5555 --proxy-user listtest + json .command selfDelPersonalAccess .error_code OK # Add self access with proxy and proxy-user + success add_access_with_proxy_user_for_list_check $a0 --osh selfAddPersonalAccess --host 192.168.1.201 --user listtest --port 2222 --proxy-host 10.0.0.5 --proxy-port 5555 --proxy-user proxyuser --force + json .command selfAddPersonalAccess .error_code OK + + # Check that selfListAccesses shows the proxy-user information + success selfListAccesses_shows_proxy_user $a0 --osh selfListAccesses + json .command selfListAccesses .error_code OK + json .value[0].acl[0].ip 192.168.1.201 + json .value[0].acl[0].port 2222 + json .value[0].acl[0].user listtest + json .value[0].acl[0].proxyIp 10.0.0.5 + json .value[0].acl[0].proxyPort 5555 + json .value[0].acl[0].proxyUser proxyuser + + # Clean up + success cleanup_list_test_with_proxy_user $a0 --osh selfDelPersonalAccess --host 192.168.1.201 --user listtest --port 2222 --proxy-host 10.0.0.5 --proxy-port 5555 --proxy-user proxyuser + json .command selfDelPersonalAccess .error_code OK + + # Add account access with proxy + success add_account_access_for_list_check $a0 --osh accountAddPersonalAccess --account $account2 --host 192.168.2.100 --user testuser --port 22 --proxy-host 10.0.0.2 --proxy-port 22 --proxy-user testuser + json .command accountAddPersonalAccess .error_code OK + + # Check that accountListAccesses shows the proxy information + success accountListAccesses_shows_proxy $a0 --osh accountListAccesses --account $account2 + json .command accountListAccesses .error_code OK + json .value[0].acl[0].ip 192.168.2.100 + json .value[0].acl[0].port 22 + json .value[0].acl[0].user testuser + json .value[0].acl[0].proxyIp 10.0.0.2 + json .value[0].acl[0].proxyPort 22 + json .value[0].acl[0].proxyUser testuser + + # Clean up + success cleanup_account_list_test $a0 --osh accountDelPersonalAccess --account $account2 --host 192.168.2.100 --user testuser --port 22 --proxy-host 10.0.0.2 --proxy-port 22 --proxy-user testuser + json .command accountDelPersonalAccess .error_code OK + + # Add account access with proxy and proxy-user + success add_account_access_with_proxy_user_for_list_check $a0 --osh accountAddPersonalAccess --account $account2 --host 192.168.2.200 --user testuser --port 22 --proxy-host 10.0.0.2 --proxy-port 22 --proxy-user proxyuser + json .command accountAddPersonalAccess .error_code OK + + # Check that accountListAccesses shows the proxy-user information + success accountListAccesses_shows_proxy_user $a0 --osh accountListAccesses --account $account2 + json .command accountListAccesses .error_code OK + json .value[0].acl[0].ip 192.168.2.200 + json .value[0].acl[0].port 22 + json .value[0].acl[0].user testuser + json .value[0].acl[0].proxyIp 10.0.0.2 + json .value[0].acl[0].proxyPort 22 + json .value[0].acl[0].proxyUser proxyuser + + # Clean up + success cleanup_account_list_test_with_proxy_user $a0 --osh accountDelPersonalAccess --account $account2 --host 192.168.2.200 --user testuser --port 22 --proxy-host 10.0.0.2 --proxy-port 22 --proxy-user proxyuser + json .command accountDelPersonalAccess .error_code OK + + # Add group server with proxy + success add_group_server_for_list_check $a1 --osh groupAddServer --group $group1 --host 192.168.3.100 --user testuser --port 22 --proxy-host 10.0.0.3 --proxy-port 22 --proxy-user testuser --force + json .command groupAddServer .error_code OK + + # Check that groupListServers shows the proxy information + success groupListServers_shows_proxy $a1 --osh groupListServers --group $group1 + json .command groupListServers .error_code OK + json .value[0].ip 192.168.3.100 + json .value[0].port 22 + json .value[0].user testuser + json .value[0].proxyIp 10.0.0.3 + json .value[0].proxyPort 22 + json .value[0].proxyUser testuser + + # Clean up + success cleanup_group_list_test $a1 --osh groupDelServer --group $group1 --host 192.168.3.100 --user testuser --port 22 --proxy-host 10.0.0.3 --proxy-port 22 --proxy-user testuser + json .command groupDelServer .error_code OK + + # Add group server with proxy and proxy-user + success add_group_server_with_proxy_user_for_list_check $a1 --osh groupAddServer --group $group1 --host 192.168.3.200 --user testuser --port 22 --proxy-host 10.0.0.3 --proxy-port 22 --proxy-user proxyuser --force + json .command groupAddServer .error_code OK + + # Check that groupListServers shows the proxy-user information + success groupListServers_shows_proxy_user $a1 --osh groupListServers --group $group1 + json .command groupListServers .error_code OK + json .value[0].ip 192.168.3.200 + json .value[0].port 22 + json .value[0].user testuser + json .value[0].proxyIp 10.0.0.3 + json .value[0].proxyPort 22 + json .value[0].proxyUser proxyuser + + # Clean up + success cleanup_group_list_test_with_proxy_user $a1 --osh groupDelServer --group $group1 --host 192.168.3.200 --user testuser --port 22 --proxy-host 10.0.0.3 --proxy-port 22 --proxy-user proxyuser + json .command groupDelServer .error_code OK + + # + # Test groupAddGuestAccess with proxy parameters + # + + # Add a server to the group first so we can test guest access + success a1_add_server_to_g1 $a1 --osh groupAddServer --group $group1 --host 192.168.4.100 --user testuser --port 22 --proxy-host 10.0.0.4 --proxy-port 22 --proxy-user testuser --force + json .command groupAddServer .error_code OK + + # Test basic proxy parameter with groupAddGuestAccess + success groupAddGuestAccess_with_proxy_host $a1 --osh groupAddGuestAccess --group $group1 --account $account2 --host 192.168.4.100 --user testuser --port 22 --proxy-host 10.0.0.4 --proxy-port 22 --proxy-user testuser + json .command groupAddGuestAccess .error_code OK + + # Test with hostname as proxy-host + success a1_add_server_with_proxy_hostname $a1 --osh groupAddServer --group $group1 --host 192.168.4.101 --user testuser --port 22 --proxy-host localhost --proxy-port 2222 --proxy-user testuser --force + json .command groupAddServer .error_code OK + + success groupAddGuestAccess_with_proxy_hostname $a1 --osh groupAddGuestAccess --group $group1 --account $account2 --host 192.168.4.101 --user testuser --port 22 --proxy-host localhost --proxy-port 2222 --proxy-user testuser + json .command groupAddGuestAccess .error_code OK + + # Test proxy-port without proxy-host + plgfail groupAddGuestAccess_proxy_port_without_host $a1 --osh groupAddGuestAccess --group $group1 --account $account2 --host 192.168.4.102 --user testuser --port 22 --proxy-port 2222 + json .command groupAddGuestAccess .error_code ERR_MISSING_PARAMETER + + # Test proxy-host without proxy-port + plgfail groupAddGuestAccess_proxy_host_without_port $a1 --osh groupAddGuestAccess --group $group1 --account $account2 --host 192.168.4.103 --user testuser --port 22 --proxy-host 10.0.0.4 --proxy-user testuser + json .command groupAddGuestAccess .error_code ERR_MISSING_PARAMETER + + # Test proxy-host and proxy-port without proxy-user + plgfail groupAddGuestAccess_proxy_without_user $a1 --osh groupAddGuestAccess --group $group1 --account $account2 --host 192.168.4.104 --user testuser --port 22 --proxy-host 10.0.0.4 --proxy-port 22 + json .command groupAddGuestAccess .error_code ERR_MISSING_PARAMETER + + # Test invalid proxy-host + plgfail groupAddGuestAccess_invalid_proxy_host $a1 --osh groupAddGuestAccess --group $group1 --account $account2 --host 192.168.4.105 --user testuser --port 22 --proxy-host "badhostnäim" --proxy-port 22 --proxy-user testuser + json .command groupAddGuestAccess .error_code ERR_HOST_NOT_FOUND + + # Test guest access that requires group to have access to proxy params + success a1_add_server_no_proxy $a1 --osh groupAddServer --group $group1 --host 192.168.4.110 --user testuser --port 22 --force + json .command groupAddServer .error_code OK + + plgfail groupAddGuestAccess_group_no_access_to_proxy $a1 --osh groupAddGuestAccess --group $group1 --account $account2 --host 192.168.4.110 --user testuser --port 22 --proxy-host 10.0.0.4 --proxy-port 22 --proxy-user testuser + json .command groupAddGuestAccess .error_code ERR_GROUP_HAS_NO_ACCESS + + # + # Test groupDelGuestAccess with proxy parameters + # + + # Delete with missing proxy-port + plgfail groupDelGuestAccess_without_proxy_port $a1 --osh groupDelGuestAccess --group $group1 --account $account2 --host 192.168.4.100 --user testuser --port 22 --proxy-host 10.0.0.4 --proxy-user testuser + json .command groupDelGuestAccess .error_code ERR_MISSING_PARAMETER + + # Delete with missing proxy-host + plgfail groupDelGuestAccess_without_proxy_host $a1 --osh groupDelGuestAccess --group $group1 --account $account2 --host 192.168.4.100 --user testuser --port 22 --proxy-port 22 + json .command groupDelGuestAccess .error_code ERR_MISSING_PARAMETER + + # Delete with missing proxy-user + plgfail groupDelGuestAccess_without_proxy_user $a1 --osh groupDelGuestAccess --group $group1 --account $account2 --host 192.168.4.100 --user testuser --port 22 --proxy-host 10.0.0.4 --proxy-port 22 + json .command groupDelGuestAccess .error_code ERR_MISSING_PARAMETER + contain "When --proxy-host is specified, --proxy-user becomes mandatory" + + # Delete guest access with proxy + success groupDelGuestAccess_with_proxy $a1 --osh groupDelGuestAccess --group $group1 --account $account2 --host 192.168.4.100 --user testuser --port 22 --proxy-host 10.0.0.4 --proxy-port 22 --proxy-user testuser + json .command groupDelGuestAccess .error_code OK + + success groupDelGuestAccess_with_proxy_hostname $a1 --osh groupDelGuestAccess --group $group1 --account $account2 --host 192.168.4.101 --user testuser --port 22 --proxy-host localhost --proxy-port 2222 --proxy-user testuser + json .command groupDelGuestAccess .error_code OK + + # + # Test that proxy information is displayed in groupListGuestAccesses + # + + # Add guest access with proxy for list check + success a1_add_server_for_guest_list_check $a1 --osh groupAddServer --group $group1 --host 192.168.4.200 --user listtest --port 2222 --proxy-host 10.0.0.6 --proxy-port 6666 --proxy-user listtest --force + json .command groupAddServer .error_code OK + + success add_guest_access_for_list_check $a1 --osh groupAddGuestAccess --group $group1 --account $account2 --host 192.168.4.200 --user listtest --port 2222 --proxy-host 10.0.0.6 --proxy-port 6666 --proxy-user listtest + json .command groupAddGuestAccess .error_code OK + + # Check that groupListGuestAccesses shows the proxy information + success groupListGuestAccesses_shows_proxy $a1 --osh groupListGuestAccesses --group $group1 --account $account2 + json .command groupListGuestAccesses .error_code OK .value[0].ip 192.168.4.200 .value[0].port 2222 .value[0].user listtest .value[0].proxyIp 10.0.0.6 .value[0].proxyPort 6666 .value[0].proxyUser listtest + + # Clean up + success cleanup_guest_list_test $a1 --osh groupDelGuestAccess --group $group1 --account $account2 --host 192.168.4.200 --user listtest --port 2222 --proxy-host 10.0.0.6 --proxy-port 6666 --proxy-user listtest + json .command groupDelGuestAccess .error_code OK + + # Add guest access with proxy-user for list check + success a1_add_server_with_proxy_user_for_guest_list $a1 --osh groupAddServer --group $group1 --host 192.168.4.201 --user listtest --port 2222 --proxy-host 10.0.0.6 --proxy-port 6666 --proxy-user proxyuser --force + json .command groupAddServer .error_code OK + + success add_guest_access_with_proxy_user_for_list_check $a1 --osh groupAddGuestAccess --group $group1 --account $account2 --host 192.168.4.201 --user listtest --port 2222 --proxy-host 10.0.0.6 --proxy-port 6666 --proxy-user proxyuser + json .command groupAddGuestAccess .error_code OK + + # Clean up + success cleanup_guest_list_test_with_proxy_user $a1 --osh groupDelGuestAccess --group $group1 --account $account2 --host 192.168.4.201 --user listtest --port 2222 --proxy-host 10.0.0.6 --proxy-port 6666 --proxy-user proxyuser + json .command groupDelGuestAccess .error_code OK + + # Clean up servers added for testing + success cleanup_server_192_168_4_100 $a1 --osh groupDelServer --group $group1 --host 192.168.4.100 --user testuser --port 22 --proxy-host 10.0.0.4 --proxy-port 22 --proxy-user testuser + json .command groupDelServer .error_code OK + + success cleanup_server_192_168_4_101 $a1 --osh groupDelServer --group $group1 --host 192.168.4.101 --user testuser --port 22 --proxy-host localhost --proxy-port 2222 --proxy-user testuser + json .command groupDelServer .error_code OK + + success cleanup_server_192_168_4_110 $a1 --osh groupDelServer --group $group1 --host 192.168.4.110 --user testuser --port 22 + json .command groupDelServer .error_code OK + + success cleanup_server_192_168_4_200 $a1 --osh groupDelServer --group $group1 --host 192.168.4.200 --user listtest --port 2222 --proxy-host 10.0.0.6 --proxy-port 6666 --proxy-user listtest + json .command groupDelServer .error_code OK + + success cleanup_server_192_168_4_201 $a1 --osh groupDelServer --group $group1 --host 192.168.4.201 --user listtest --port 2222 --proxy-host 10.0.0.6 --proxy-port 6666 --proxy-user proxyuser + json .command groupDelServer .error_code OK + + success a0_delete_group1 $a0 --osh groupDelete --group $group1 --no-confirm + json .error_code OK .command groupDelete + + success a0_delete_a1 $a0 --osh accountDelete --account $account1 --no-confirm + json .error_code OK .command accountDelete + + success a0_delete_a2 $a0 --osh accountDelete --account $account2 --no-confirm + json .error_code OK .command accountDelete +} + +testsuite_proxyjump +unset -f testsuite_proxyjump diff --git a/tests/unit/tests/base.t b/tests/unit/tests/base.t index 04c61b0..834bd74 100644 --- a/tests/unit/tests/base.t +++ b/tests/unit/tests/base.t @@ -159,6 +159,68 @@ cmp_deeply( "build_ttyrec_cmdline_part2of2 cmd" ); +# Test ttyrec with proxy parameters +$fnret = OVH::Bastion::build_ttyrec_cmdline_part1of2( + ip => "192.168.1.100", + port => 22, + user => "targetuser", + account => "bastionuser", + uniqid => 'cafed00dcafe', + home => "/home/randomuser", + proxyIp => "10.0.0.1", + proxyPort => 2222, + proxyUser => "jumpi", +); +cmp_deeply( + $fnret->value->{'saveFile'}, + re( + qr{^\Q/home/randomuser/ttyrec/via-10.0.0.1-192.168.1.100/20\E\d\d-\d\d-\d\d.\d\d\-\d\d\-\d\d\.\d{6}\Q.cafed00dcafe.bastionuser.targetuser.192.168.1.100.22.via.jumpi.10.0.0.1.2222.ttyrec\E$} + ), + "build_ttyrec_cmdline_part1of2 with proxy saveFile" +); +cmp_deeply( + $fnret->value->{'cmd'}, + [ + 'ttyrec', + '-f', + $fnret->value->{'saveFile'}, + '-F', + '/home/randomuser/ttyrec/via-10.0.0.1-192.168.1.100/%Y-%m-%d.%H-%M-%S.#usec#.cafed00dcafe.bastionuser.targetuser.192.168.1.100.22.via.jumpi.10.0.0.1.2222.ttyrec' + ], + "build_ttyrec_cmdline_part1of2 with proxy cmd" +); + +# Test ttyrec with IPv6 proxy +$fnret = OVH::Bastion::build_ttyrec_cmdline_part1of2( + ip => "192.168.1.200", + port => 22, + user => "targetuser", + account => "bastionuser", + uniqid => 'cafed00dcafe', + home => "/home/randomuser", + proxyIp => "2001:db8::1", + proxyPort => 22, + proxyUser => "jumpi", +); +cmp_deeply( + $fnret->value->{'saveFile'}, + re( + qr{^\Q/home/randomuser/ttyrec/via-v6[2001.db8..1]-192.168.1.200/20\E\d\d-\d\d-\d\d.\d\d\-\d\d\-\d\d\.\d{6}\Q.cafed00dcafe.bastionuser.targetuser.192.168.1.200.22.via.jumpi.v6[2001.db8..1].22.ttyrec\E$} + ), + "build_ttyrec_cmdline_part1of2 with IPv6 proxy saveFile" +); +cmp_deeply( + $fnret->value->{'cmd'}, + [ + 'ttyrec', + '-f', + $fnret->value->{'saveFile'}, + '-F', + '/home/randomuser/ttyrec/via-v6[2001.db8..1]-192.168.1.200/%Y-%m-%d.%H-%M-%S.#usec#.cafed00dcafe.bastionuser.targetuser.192.168.1.200.22.via.jumpi.v6[2001.db8..1].22.ttyrec' + ], + "build_ttyrec_cmdline_part1of2 with IPv6 proxy cmd" +); + is(OVH::Bastion::config("bastionName")->value, "mock", "bastion name is mocked"); ok(OVH::Bastion::is_account_valid(account => "azerty")->is_ok, "is_account_valid('azerty')"); diff --git a/tests/unit/tests/is_access_granted_proxy.t b/tests/unit/tests/is_access_granted_proxy.t new file mode 100644 index 0000000..ab36461 --- /dev/null +++ b/tests/unit/tests/is_access_granted_proxy.t @@ -0,0 +1,311 @@ +#! /usr/bin/env perl +# vim: set filetype=perl ts=4 sw=4 sts=4 et: +use common::sense; +use Test::More; + +use File::Basename; +use lib dirname(__FILE__) . '/../../../lib/perl'; +use OVH::Bastion; +use OVH::Result; + +OVH::Bastion::enable_mocking(); +OVH::Bastion::set_mock_data( + { + "accounts" => { + "me" => { + "uid" => 99982, + "gid" => 99982, + "personal_accesses" => [ + "me\@192.0.2.10:22", + "me\@192.0.2.11", + "me\@192.0.2.20:22 # PROXYHOST=10.0.0.1 # PROXYPORT=2222", + "me\@192.0.2.21:80 # PROXYHOST=10.0.0.1 # PROXYPORT=2222", + "me\@192.0.2.30:22 # PROXYHOST=10.0.0.2 # PROXYPORT=3333", + "me\@192.0.2.50 # PROXYHOST=10.0.0.4 # PROXYPORT=4444", + "192.0.2.60:22 # PROXYHOST=10.0.0.5 # PROXYPORT=5555", + "192.0.2.61:22 # PROXYHOST=10.0.0.5 # PROXYPORT=5555 # PROXYUSER=proxyuser", + "192.0.2.62:22 # PROXYHOST=10.0.0.5 # PROXYPORT=5555", + "192.0.2.63:22 # PROXYHOST=10.0.0.5 # PROXYPORT=5555 # PROXYUSER=admin*", + "192.0.2.64:22 # PROXYHOST=10.0.0.5 # PROXYPORT=5555 # PROXYUSER=user?", + "198.51.100.0/24:22 # PROXYHOST=10.0.0.1 # PROXYPORT=2222", + "198.51.200.0/24:22 # PROXYHOST=10.0.0.1 # PROXYPORT=2222 # PROXYUSER=netuser", + # IPv6 entries + "me\@[2001:db8::10]:22", + "me\@[2001:db8::11]", + "me\@[2001:db8::20]:22 # PROXYHOST=2001:db8:cafe::1 # PROXYPORT=2222", + "me\@[2001:db8::30]:80 # PROXYHOST=2001:db8:cafe::2 # PROXYPORT=3333", + "[2001:db8::40]:22 # PROXYHOST=2001:db8:cafe::4 # PROXYPORT=4444", + "[2001:db8::41]:22 # PROXYHOST=2001:db8:cafe::4 # PROXYPORT=4444 # PROXYUSER=ipv6user", + "[2001:aaaa::/64]:22 # PROXYHOST=2001:db8:cafe::1 # PROXYPORT=2222", + ], + }, + }, + } +); +OVH::Bastion::load_configuration( + mock_data => { + bastionName => "mock", + } +); + +my %want; # truth table +my $undef = '_none_'; # can't use undef as a hash key, so we'll use this special value instead + +# Test 1: Regular access without proxy - should work as before +$want{"192.0.2.10"}{"22"}{"me"}{$undef}{$undef} = 'OK'; +$want{"192.0.2.10"}{"22"}{"me"}{"10.0.0.1"}{"2222"} = 'KO_ACCESS_DENIED'; # proxy requested but not configured +$want{"192.0.2.11"}{"22"}{"me"}{$undef}{$undef} = 'OK'; +$want{"192.0.2.11"}{"80"}{"me"}{$undef}{$undef} = 'OK'; + +# Test 2: Access with specific proxy (no PROXYUSER in ACL) - should work with any proxy-user +$want{"192.0.2.20"}{"22"}{"me"}{$undef}{$undef} = 'KO_ACCESS_DENIED'; # proxy required but not provided +$want{"192.0.2.20"}{"22"}{"me"}{"10.0.0.1"}{"2222"} = 'OK'; # no proxy-user specified +$want{"192.0.2.20"}{"22"}{"me"}{"10.0.0.1"}{"2222"}{"anyuser"} = 'OK'; # no PROXYUSER in ACL = accepts any proxy-user +$want{"192.0.2.20"}{"22"}{"me"}{"10.0.0.1"}{"2222"}{"testuser"} = 'OK'; # no PROXYUSER in ACL = accepts any proxy-user +$want{"192.0.2.20"}{"22"}{"me"}{"10.0.0.1"}{"2222"}{$undef} = 'OK'; # no PROXYUSER in ACL = accepts undef too +$want{"192.0.2.20"}{"22"}{"me"}{"10.0.0.1"}{"3333"} = 'KO_ACCESS_DENIED'; # wrong proxy port +$want{"192.0.2.20"}{"22"}{"me"}{"10.0.0.2"}{"2222"} = 'KO_ACCESS_DENIED'; # wrong proxy IP +$want{"192.0.2.20"}{"22"}{"me"}{"10.0.0.1"}{$undef} = 'KO_ACCESS_DENIED'; # proxy IP without port + +# Test 3: Different proxy configuration (no PROXYUSER in ACL) - accepts any proxy-user +$want{"192.0.2.30"}{"22"}{"me"}{"10.0.0.2"}{"3333"} = 'OK'; +$want{"192.0.2.30"}{"22"}{"me"}{"10.0.0.2"}{"3333"}{"anyuser"} = 'OK'; # no PROXYUSER in ACL +$want{"192.0.2.30"}{"22"}{"me"}{"10.0.0.2"}{"3333"}{$undef} = 'OK'; # no PROXYUSER in ACL +$want{"192.0.2.30"}{"22"}{"me"}{"10.0.0.1"}{"2222"} = 'KO_ACCESS_DENIED'; # wrong proxy + +# Test 4: Subnet access with proxy (no PROXYUSER in ACL) - accepts any proxy-user +$want{"198.51.100.100"}{"22"}{"me"}{"10.0.0.1"}{"2222"} = 'OK'; # subnet match with proxy +$want{"198.51.100.100"}{"22"}{"me"}{"10.0.0.1"}{"2222"}{"anyuser"} = 'OK'; # no PROXYUSER in ACL +$want{"198.51.100.100"}{"22"}{"me"}{"10.0.0.1"}{"2222"}{$undef} = 'OK'; # no PROXYUSER in ACL +$want{"198.51.100.200"}{"22"}{"me"}{"10.0.0.1"}{"2222"} = 'OK'; # subnet match with proxy +$want{"198.51.100.200"}{"22"}{"me"}{"10.0.0.1"}{"2222"}{"testuser"} = 'OK'; # no PROXYUSER in ACL +$want{"198.51.100.100"}{"22"}{"me"}{"10.0.0.1"}{"3333"} = 'KO_ACCESS_DENIED'; # subnet match, wrong proxy port +$want{"198.51.100.100"}{"22"}{"me"}{$undef}{$undef} = 'KO_ACCESS_DENIED'; # subnet match but no proxy requested when proxy required + +# Test 5: Port wildcard with proxy (no PROXYUSER in ACL) - accepts any proxy-user +$want{"192.0.2.50"}{"22"}{"me"}{"10.0.0.4"}{"4444"} = 'OK'; # port wildcard with proxy +$want{"192.0.2.50"}{"22"}{"me"}{"10.0.0.4"}{"4444"}{"anyuser"} = 'OK'; # no PROXYUSER in ACL +$want{"192.0.2.50"}{"22"}{"me"}{"10.0.0.4"}{"4444"}{$undef} = 'OK'; # no PROXYUSER in ACL +$want{"192.0.2.50"}{"80"}{"me"}{"10.0.0.4"}{"4444"} = 'OK'; # port wildcard with proxy +$want{"192.0.2.50"}{"80"}{"me"}{"10.0.0.4"}{"4444"}{"testuser"} = 'OK'; # no PROXYUSER in ACL +$want{"192.0.2.50"}{"22"}{"me"}{"10.0.0.4"}{"5555"} = 'KO_ACCESS_DENIED'; # wrong proxy port + +# Test 6: User wildcard with proxy (no PROXYUSER in ACL) - accepts any proxy-user +$want{"192.0.2.60"}{"22"}{"root"}{"10.0.0.5"}{"5555"} = 'OK'; # user wildcard, no proxy-user specified +$want{"192.0.2.60"}{"22"}{"root"}{"10.0.0.5"}{"5555"}{"anyuser"} = 'OK'; # no PROXYUSER in ACL +$want{"192.0.2.60"}{"22"}{"root"}{"10.0.0.5"}{"5555"}{$undef} = 'OK'; # no PROXYUSER in ACL +$want{"192.0.2.60"}{"22"}{"admin"}{"10.0.0.5"}{"5555"} = 'OK'; # user wildcard should match any user with exact proxy +$want{"192.0.2.60"}{"22"}{"admin"}{"10.0.0.5"}{"5555"}{"testuser"} = 'OK'; # no PROXYUSER in ACL +$want{"192.0.2.60"}{"22"}{"root"}{"10.0.0.5"}{"6666"} = 'KO_ACCESS_DENIED'; # user wildcard but wrong proxy port + +# Test 6b: User wildcard with proxy and specific proxy-user +$want{"192.0.2.61"}{"22"}{"root"}{"10.0.0.5"}{"5555"}{"proxyuser"} = 'OK'; # user wildcard, specific proxy-user match +$want{"192.0.2.61"}{"22"}{"admin"}{"10.0.0.5"}{"5555"}{"proxyuser"} = 'OK'; # user wildcard, specific proxy-user match +$want{"192.0.2.61"}{"22"}{"root"}{"10.0.0.5"}{"5555"}{"wronguser"} = 'KO_ACCESS_DENIED'; # user wildcard, wrong proxy-user +$want{"192.0.2.61"}{"22"}{"root"}{"10.0.0.5"}{"5555"}{$undef} = 'KO_ACCESS_DENIED'; # user wildcard, missing proxy-user + +# Test 6c: User wildcard with proxy but no proxy-user in ACL (allows any proxy-user) +$want{"192.0.2.62"}{"22"}{"root"}{"10.0.0.5"}{"5555"}{"anyuser"} = 'OK'; # no proxy-user in ACL = wildcard +$want{"192.0.2.62"}{"22"}{"admin"}{"10.0.0.5"}{"5555"}{"otheruser"} = 'OK'; # no proxy-user in ACL = wildcard +$want{"192.0.2.62"}{"22"}{"root"}{"10.0.0.5"}{"5555"}{$undef} = 'OK'; # no proxy-user in ACL = wildcard, undef also allowed + +# Test 6d: User wildcard with proxy-user pattern (admin*) +$want{"192.0.2.63"}{"22"}{"root"}{"10.0.0.5"}{"5555"}{"admin"} = 'OK'; # proxy-user pattern match +$want{"192.0.2.63"}{"22"}{"admin"}{"10.0.0.5"}{"5555"}{"admin123"} = 'OK'; # proxy-user pattern match +$want{"192.0.2.63"}{"22"}{"root"}{"10.0.0.5"}{"5555"}{"root"} = 'KO_ACCESS_DENIED'; # proxy-user pattern no match +$want{"192.0.2.63"}{"22"}{"root"}{"10.0.0.5"}{"5555"}{$undef} = 'KO_ACCESS_DENIED'; # missing proxy-user + +# Test 6e: User wildcard with proxy-user pattern (user?) +$want{"192.0.2.64"}{"22"}{"root"}{"10.0.0.5"}{"5555"}{"user1"} = 'OK'; # proxy-user pattern match +$want{"192.0.2.64"}{"22"}{"admin"}{"10.0.0.5"}{"5555"}{"userA"} = 'OK'; # proxy-user pattern match +$want{"192.0.2.64"}{"22"}{"root"}{"10.0.0.5"}{"5555"}{"user"} = 'KO_ACCESS_DENIED'; # proxy-user pattern no match (too short) +$want{"192.0.2.64"}{"22"}{"root"}{"10.0.0.5"}{"5555"}{"user12"} = 'KO_ACCESS_DENIED'; # proxy-user pattern no match (too long) +$want{"192.0.2.64"}{"22"}{"root"}{"10.0.0.5"}{"5555"}{$undef} = 'KO_ACCESS_DENIED'; # missing proxy-user + +# Test 7: Subnet with proxy-user +$want{"198.51.200.100"}{"22"}{"me"}{"10.0.0.1"}{"2222"}{"netuser"} = 'OK'; # subnet match with specific proxy-user +$want{"198.51.200.200"}{"22"}{"me"}{"10.0.0.1"}{"2222"}{"netuser"} = 'OK'; # subnet match with specific proxy-user +$want{"198.51.200.100"}{"22"}{"me"}{"10.0.0.1"}{"2222"}{"other"} = 'KO_ACCESS_DENIED'; # subnet match, wrong proxy-user +$want{"198.51.200.100"}{"22"}{"me"}{"10.0.0.1"}{"2222"}{$undef} = 'KO_ACCESS_DENIED'; # subnet match, missing proxy-user + +# Test 8: Negative cases - hosts not in ACL +$want{"192.0.2.99"}{"22"}{"me"}{$undef}{$undef} = 'KO_ACCESS_DENIED'; +$want{"192.0.2.99"}{"22"}{"me"}{"10.0.0.1"}{"2222"} = 'KO_ACCESS_DENIED'; + +# IPv6 Tests +# Test 9: Regular IPv6 access without proxy - should work as before +$want{"2001:db8::10"}{"22"}{"me"}{$undef}{$undef} = 'OK'; +$want{"2001:db8::10"}{"22"}{"me"}{"2001:db8:cafe::1"}{"2222"} = 'KO_ACCESS_DENIED'; # proxy requested but not configured +$want{"2001:db8::11"}{"22"}{"me"}{$undef}{$undef} = 'OK'; +$want{"2001:db8::11"}{"80"}{"me"}{$undef}{$undef} = 'OK'; + +# Test 10: IPv6 access with specific proxy (no PROXYUSER in ACL) - accepts any proxy-user +$want{"2001:db8::20"}{"22"}{"me"}{$undef}{$undef} = 'KO_ACCESS_DENIED'; # proxy required but not provided +$want{"2001:db8::20"}{"22"}{"me"}{"2001:db8:cafe::1"}{"2222"} = 'OK'; +$want{"2001:db8::20"}{"22"}{"me"}{"2001:db8:cafe::1"}{"2222"}{"anyuser"} = 'OK'; # no PROXYUSER in ACL +$want{"2001:db8::20"}{"22"}{"me"}{"2001:db8:cafe::1"}{"2222"}{$undef} = 'OK'; # no PROXYUSER in ACL +$want{"2001:db8::20"}{"22"}{"me"}{"2001:db8:cafe::1"}{"3333"} = 'KO_ACCESS_DENIED'; # wrong proxy port +$want{"2001:db8::20"}{"22"}{"me"}{"2001:db8:cafe::2"}{"2222"} = 'KO_ACCESS_DENIED'; # wrong proxy IP + +# Test 11: IPv6 different proxy configuration (no PROXYUSER in ACL) - accepts any proxy-user +$want{"2001:db8::30"}{"80"}{"me"}{"2001:db8:cafe::2"}{"3333"} = 'OK'; +$want{"2001:db8::30"}{"80"}{"me"}{"2001:db8:cafe::2"}{"3333"}{"anyuser"} = 'OK'; # no PROXYUSER in ACL +$want{"2001:db8::30"}{"80"}{"me"}{"2001:db8:cafe::2"}{"3333"}{$undef} = 'OK'; # no PROXYUSER in ACL +$want{"2001:db8::30"}{"80"}{"me"}{"2001:db8:cafe::1"}{"2222"} = 'KO_ACCESS_DENIED'; # wrong proxy + +# Test 12: IPv6 user wildcard with proxy (no PROXYUSER in ACL) - accepts any proxy-user +$want{"2001:db8::40"}{"22"}{"root"}{"2001:db8:cafe::4"}{"4444"} = 'OK'; # user wildcard, no proxy-user specified +$want{"2001:db8::40"}{"22"}{"root"}{"2001:db8:cafe::4"}{"4444"}{"anyuser"} = 'OK'; # no PROXYUSER in ACL +$want{"2001:db8::40"}{"22"}{"root"}{"2001:db8:cafe::4"}{"4444"}{$undef} = 'OK'; # no PROXYUSER in ACL +$want{"2001:db8::40"}{"22"}{"admin"}{"2001:db8:cafe::4"}{"4444"} = 'OK'; # user wildcard should match any user with exact proxy +$want{"2001:db8::40"}{"22"}{"admin"}{"2001:db8:cafe::4"}{"4444"}{"testuser"} = 'OK'; # no PROXYUSER in ACL +$want{"2001:db8::40"}{"22"}{"root"}{"2001:db8:cafe::4"}{"5555"} = 'KO_ACCESS_DENIED'; # user wildcard but wrong proxy port + +# Test 12b: IPv6 user wildcard with proxy and specific proxy-user +$want{"2001:db8::41"}{"22"}{"root"}{"2001:db8:cafe::4"}{"4444"}{"ipv6user"} = 'OK'; # IPv6 user wildcard, specific proxy-user match +$want{"2001:db8::41"}{"22"}{"admin"}{"2001:db8:cafe::4"}{"4444"}{"ipv6user"} = 'OK'; # IPv6 user wildcard, specific proxy-user match +$want{"2001:db8::41"}{"22"}{"root"}{"2001:db8:cafe::4"}{"4444"}{"wronguser"} = 'KO_ACCESS_DENIED'; # IPv6 user wildcard, wrong proxy-user +$want{"2001:db8::41"}{"22"}{"root"}{"2001:db8:cafe::4"}{"4444"}{$undef} = 'KO_ACCESS_DENIED'; # IPv6 user wildcard, missing proxy-user + +# Test 13: IPv6 subnet access with proxy (no PROXYUSER in ACL) - accepts any proxy-user +$want{"2001:aaaa::100"}{"22"}{"me"}{"2001:db8:cafe::1"}{"2222"} = 'OK'; # subnet match with proxy +$want{"2001:aaaa::100"}{"22"}{"me"}{"2001:db8:cafe::1"}{"2222"}{"anyuser"} = 'OK'; # no PROXYUSER in ACL +$want{"2001:aaaa::100"}{"22"}{"me"}{"2001:db8:cafe::1"}{"2222"}{$undef} = 'OK'; # no PROXYUSER in ACL +$want{"2001:aaaa::200"}{"22"}{"me"}{"2001:db8:cafe::1"}{"2222"} = 'OK'; # subnet match with proxy +$want{"2001:aaaa::200"}{"22"}{"me"}{"2001:db8:cafe::1"}{"2222"}{"testuser"} = 'OK'; # no PROXYUSER in ACL +$want{"2001:aaaa::100"}{"22"}{"me"}{"2001:db8:cafe::1"}{"3333"} = 'KO_ACCESS_DENIED'; # subnet match, wrong proxy port +$want{"2001:aaaa::100"}{"22"}{"me"}{$undef}{$undef} = 'KO_ACCESS_DENIED'; # subnet match but no proxy requested when proxy required + +# Test 14: IPv6 negative cases - hosts not in ACL +$want{"2001:ffff::999"}{"22"}{"me"}{$undef}{$undef} = 'KO_ACCESS_DENIED'; +$want{"2001:ffff::999"}{"22"}{"me"}{"2001:db8:cafe::1"}{"2222"} = 'KO_ACCESS_DENIED'; + +# Run all the tests +foreach my $ip ( + qw{ + 192.0.2.10 + 192.0.2.11 + 192.0.2.20 + 192.0.2.30 + 198.51.100.100 + 198.51.100.200 + 192.0.2.50 + 192.0.2.60 + 192.0.2.61 + 192.0.2.62 + 192.0.2.63 + 192.0.2.64 + 192.0.2.99 + 198.51.200.100 + 198.51.200.200 + 2001:db8::10 + 2001:db8::11 + 2001:db8::20 + 2001:db8::30 + 2001:db8::40 + 2001:db8::41 + 2001:aaaa::100 + 2001:aaaa::200 + 2001:ffff::999 + } + ) +{ + + foreach my $port (qw{22 80}) { + foreach my $user (qw{me root admin}) { + foreach my $proxyIp ( + $undef, "10.0.0.1", "10.0.0.2", "10.0.0.3", + "10.0.0.4", "10.0.0.5", "2001:db8:cafe::1", "2001:db8:cafe::2", + "2001:db8:cafe::4" + ) + { + foreach my $proxyPort ($undef, "2222", "3333", "1234", "4444", "5555", "6666") { + foreach my $proxyUser ( + $undef, "proxyuser", "anyuser", "otheruser", "wronguser", "testuser", + "admin", "admin123", "root", "user1", "userA", "user", + "user12", "netuser", "other", "ipv6user" + ) + { + # Skip combinations that don't make sense + next + if (!defined $proxyIp || $proxyIp eq $undef) && (defined $proxyPort && $proxyPort ne $undef); + next + if (!defined $proxyIp || $proxyIp eq $undef) && (defined $proxyUser && $proxyUser ne $undef); + + my $expected = + $want{$ip}{$port}{$user}{$proxyIp // $undef}{$proxyPort // $undef}{$proxyUser // $undef}; + next unless defined $expected; + + my %params = ( + ipfrom => "127.0.0.1", + account => "me", + user => $user, + ip => $ip, + port => $port, + ); + + # Add proxy parameters if they are defined + if (defined $proxyIp && $proxyIp ne $undef) { ## no critic (ProhibitDeepNests) + $params{proxyIp} = $proxyIp; + } + if (defined $proxyPort && $proxyPort ne $undef) { ## no critic (ProhibitDeepNests) + $params{proxyPort} = $proxyPort; + } + if (defined $proxyUser && $proxyUser ne $undef) { ## no critic (ProhibitDeepNests) + $params{proxyUser} = $proxyUser; + } + + my $result = OVH::Bastion::is_access_granted(%params); + + my $test_desc = sprintf( + "is_access_granted with %s@%s:%s proxy=%s@%s:%s", + $user, $ip, $port, + $proxyUser // '', + $proxyIp // '', + $proxyPort // '' + ); + + is($result->err, $expected, $test_desc); + + # If access is granted, verify proxy information is returned + _verify_proxy_information($expected, $proxyIp, $proxyPort, $proxyUser, $result, $test_desc); + } + } + } + } + } +} + +sub _verify_proxy_information { ## no critic (ProhibitManyArgs) + my ($expected, $proxyIp, $proxyPort, $proxyUser, $result, $test_desc) = @_; + + # Early return if access is not granted or no proxy expected + return if $expected ne 'OK'; + return if !defined $proxyIp || $proxyIp eq $undef; + + my $value = $result->value; + return if ref $value ne 'ARRAY' || @$value == 0; + + # Check if any of the returned grants has proxy info + my $found_proxy = 0; + foreach my $grant (@$value) { + next if !defined $grant->{proxyIp}; + next if $grant->{proxyIp} ne $proxyIp; + + $found_proxy = 1; + if (defined $proxyPort && $proxyPort ne $undef) { + is($grant->{proxyPort}, $proxyPort, "$test_desc - proxy port returned"); + } + if (defined $proxyUser && $proxyUser ne $undef) { + # Note: proxyUser in grant might be undef if wildcard, so we only check if explicitly set + if (defined $grant->{proxyUser}) { + ok(1, "$test_desc - proxy user returned"); + } + } + last; + } + ok($found_proxy, "$test_desc - proxy IP returned in grant"); + return; +} + +done_testing();