From 95f9117394603b984bd09aba03a84f7e011b241d Mon Sep 17 00:00:00 2001 From: jon4hz Date: Fri, 26 Sep 2025 17:36:52 +0200 Subject: [PATCH 01/49] feat: implement proxy jump for egress connections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jonah Zürcher --- bin/helper/osh-accountModifyPersonalAccess | 10 +- bin/helper/osh-groupAddServer | 14 +- bin/plugin/group-aclkeeper/groupAddServer | 39 ++- .../group-aclkeeper/groupAddServer.json | 13 +- bin/plugin/group-aclkeeper/groupDelServer | 49 +++- .../group-aclkeeper/groupDelServer.json | 13 +- .../restricted/accountAddPersonalAccess | 41 ++- .../restricted/accountAddPersonalAccess.json | 14 +- .../restricted/accountDelPersonalAccess | 51 +++- .../restricted/accountDelPersonalAccess.json | 14 +- bin/plugin/restricted/selfAddPersonalAccess | 45 ++- .../restricted/selfAddPersonalAccess.json | 14 +- bin/plugin/restricted/selfDelPersonalAccess | 51 +++- .../restricted/selfDelPersonalAccess.json | 14 +- bin/shell/osh.pl | 1 + lib/perl/OVH/Bastion.pm | 16 +- lib/perl/OVH/Bastion/Plugin.pm | 40 ++- lib/perl/OVH/Bastion/Plugin/ACL.pm | 32 ++- lib/perl/OVH/Bastion/allowdeny.inc | 270 ++++++++++++++++-- lib/perl/OVH/Bastion/allowkeeper.inc | 33 +++ tests/unit/tests/is_access_granted_proxy.t | 217 ++++++++++++++ 21 files changed, 859 insertions(+), 132 deletions(-) create mode 100644 tests/unit/tests/is_access_granted_proxy.t diff --git a/bin/helper/osh-accountModifyPersonalAccess b/bin/helper/osh-accountModifyPersonalAccess index 5ec2574..4392747 100755 --- a/bin/helper/osh-accountModifyPersonalAccess +++ b/bin/helper/osh-accountModifyPersonalAccess @@ -32,7 +32,7 @@ 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); eval { local $SIG{__WARN__} = sub { push @optwarns, shift }; $result = GetOptions( @@ -46,6 +46,8 @@ 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] }, ); }; if ($@) { die $@ } @@ -81,7 +83,9 @@ if (not grep { $action eq $_ } qw{ add del }) { #CODE -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) + ->value; my $plugin = ($target eq 'self' ? 'self' : 'account') . 'AddPersonalAccess'; @@ -115,6 +119,8 @@ $fnret = OVH::Bastion::access_modify( forcePassword => $forcePassword, comment => $comment, widestV4Prefix => $widestV4Prefix, + proxyIp => $proxyIp, + proxyPort => $proxyPort, ); if ($fnret->err eq 'OK') { my $ttlmsg = $ttl ? ' (expires in ' . OVH::Bastion::duration2human(seconds => $ttl)->value->{'human'} . ')' : ''; diff --git a/bin/helper/osh-groupAddServer b/bin/helper/osh-groupAddServer index 867fa64..93a8d59 100755 --- a/bin/helper/osh-groupAddServer +++ b/bin/helper/osh-groupAddServer @@ -19,7 +19,7 @@ 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); eval { local $SIG{__WARN__} = sub { push @optwarns, shift }; $result = GetOptions( @@ -33,6 +33,8 @@ eval { "force-key=s" => sub { $forceKey //= $_[1] }, "ttl=i" => sub { $ttl //= $_[1] }, "comment=s" => sub { $comment //= $_[1] }, + "proxy-host=s" => sub { $proxyIp //= $_[1] }, + "proxy-port=i" => sub { $proxyPort //= $_[1] }, ); }; @@ -86,7 +88,9 @@ $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; +my $machine = + OVH::Bastion::machine_display(ip => $ip, port => $port, user => $user, proxyIp => $proxyIp, proxyPort => $proxyPort) + ->value; # access_modify validates all its parameters, don't do it ourselves here for clarity $fnret = OVH::Bastion::access_modify( @@ -100,6 +104,8 @@ $fnret = OVH::Bastion::access_modify( forceKey => $forceKey, ttl => $ttl, comment => $comment, + proxyIp => $proxyIp, + proxyPort => $proxyPort, ); if ($fnret->err eq 'OK') { my $ttlmsg = $ttl ? ' (expires in ' . OVH::Bastion::duration2human(seconds => $ttl)->value->{'human'} . ')' : ''; @@ -114,7 +120,9 @@ if ($fnret->err eq 'OK') { forcePassword => $forcePassword, forceKey => $forceKey, ttl => $ttl, - comment => $comment + comment => $comment, + proxyIp => $proxyIp, + proxyPort => $proxyPort, }, 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..ef63a71 100755 --- a/bin/plugin/group-aclkeeper/groupAddServer +++ b/bin/plugin/group-aclkeeper/groupAddServer @@ -21,6 +21,8 @@ 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, # undocumented/compatibility: "user-any" => \my $userAny, "port-any" => \my $portAny, @@ -55,6 +57,8 @@ Usage: --osh SCRIPT_NAME --group GROUP --host HOST --user USER|* --port PORT|* [ --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) Examples:: @@ -76,15 +80,32 @@ 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::is_valid_ip(ip => $proxyHost, allowSubnets => 0); + if ($fnret) { + $proxyIp = $fnret->value->{'ip'}; + } else { + $fnret = OVH::Bastion::get_ip(host => $proxyHost); + if ($fnret) { + $proxyIp = $fnret->value->{'ip'}; + } else { + $proxyIp = $proxyHost; + } + } +} + $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, ); if (!$fnret) { help(); @@ -170,5 +191,7 @@ 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; 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..7f2195e 100644 --- a/bin/plugin/group-aclkeeper/groupAddServer.json +++ b/bin/plugin/group-aclkeeper/groupAddServer.json @@ -4,11 +4,18 @@ "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+" , {"ac" : ["--port", "--port-any", "--proxy-host", "--proxy-port"]}, "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+)" , {"ac" : ["--user", "--user-any", "--proxy-host", "--proxy-port"]}, + "groupAddServer +--group +\\S+ +--host +\\S+ +--proxy-host" , {"pr" : ["", ""]}, + "groupAddServer +--group +\\S+ +--host +\\S+ +--proxy-host +\\d+" , {"ac" : ["--port", "--port-any", "--user", "--user-any", "--proxy-port"]}, + "groupAddServer +--group +\\S+ +--host +\\S+ +--proxy-port" , {"pr" : [""]}, + "groupAddServer +--group +\\S+ +--host +\\S+ +--proxy-port +\\d+" , {"ac" : ["--port", "--port-any", "--user", "--user-any", "--proxy-host"]}, "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+)" , {"ac" : ["", "--force-password", "--force", "--proxy-host", "--proxy-port"]}, + "groupAddServer +--group +\\S+ +--host +\\S+ +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\d+) +--(user(-any| +\\S+)|proxy-host +\\S+|proxy-port +\\d+)" , {"ac" : ["", "--force-password", "--force", "--proxy-host", "--proxy-port", "--port", "--port-any", "--user", "--user-any"]}, + "groupAddServer +--group +\\S+ +--host +\\S+ +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\d+|user(-any| +\\S+)) +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\d+|user(-any| +\\S+)) +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\d+|user(-any| +\\S+))" , {"ac" : ["", "--force-password", "--force", "--proxy-host", "--proxy-port", "--port", "--port-any", "--user", "--user-any"]}, + "groupAddServer +--group +\\S+ +--host +\\S+ +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\d+|user(-any| +\\S+)) +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\d+|user(-any| +\\S+)) +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\d+|user(-any| +\\S+)) +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\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" : [""]} diff --git a/bin/plugin/group-aclkeeper/groupDelServer b/bin/plugin/group-aclkeeper/groupDelServer index 3f58d8f..b2ef0a0 100755 --- a/bin/plugin/group-aclkeeper/groupDelServer +++ b/bin/plugin/group-aclkeeper/groupDelServer @@ -17,6 +17,8 @@ my $remainingOptions = OVH::Bastion::Plugin::begin( "group=s" => \my $group, "protocol=s" => \my $protocol, "force" => \my $force, + "proxy-host=s" => \my $proxyHost, + "proxy-port=s" => \my $proxyPort, # undocumented/compatibility: "user-any" => \my $userAny, "port-any" => \my $portAny, @@ -45,6 +47,8 @@ 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 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,15 +69,32 @@ 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::is_valid_ip(ip => $proxyHost, allowSubnets => 0); + if ($fnret) { + $proxyIp = $fnret->value->{'ip'}; + } else { + $fnret = OVH::Bastion::get_ip(host => $proxyHost); + if ($fnret) { + $proxyIp = $fnret->value->{'ip'}; + } else { + $proxyIp = $proxyHost; + } + } +} + $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, ); if (!$fnret) { help(); @@ -100,10 +121,12 @@ $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; 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..07103f4 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+" , {"ac" : ["--port", "--port-any", "--proxy-host", "--proxy-port"]}, "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+)" , {"ac" : ["--user", "--user-any", "--proxy-host", "--proxy-port"]}, + "groupDelServer +--group +\\S+ +--host +\\S+ +--proxy-host" , {"pr" : ["", ""]}, + "groupDelServer +--group +\\S+ +--host +\\S+ +--proxy-host +\\d+" , {"ac" : ["--port", "--port-any", "--user", "--user-any", "--proxy-port"]}, + "groupDelServer +--group +\\S+ +--host +\\S+ +--proxy-port" , {"pr" : [""]}, + "groupDelServer +--group +\\S+ +--host +\\S+ +--proxy-port +\\d+" , {"ac" : ["--port", "--port-any", "--user", "--user-any", "--proxy-host"]}, "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+ +--port(-any| +\\d+) +--user(-any| +\\S+)" , {"ac" : ["", "--force", "--proxy-host", "--proxy-port"]}, + "groupDelServer +--group +\\S+ +--host +\\S+ +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\d+) +--(user(-any| +\\S+)|proxy-host +\\S+|proxy-port +\\d+)" , {"ac" : ["", "--force", "--proxy-host", "--proxy-port", "--port", "--port-any", "--user", "--user-any"]}, + "groupDelServer +--group +\\S+ +--host +\\S+ +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\d+|user(-any| +\\S+)) +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\d+|user(-any| +\\S+)) +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\d+|user(-any| +\\S+))" , {"ac" : ["", "--force", "--proxy-host", "--proxy-port", "--port", "--port-any", "--user", "--user-any"]}, + "groupDelServer +--group +\\S+ +--host +\\S+ +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\d+|user(-any| +\\S+)) +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\d+|user(-any| +\\S+)) +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\d+|user(-any| +\\S+)) +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\d+|user(-any| +\\S+))" , {"pr" : ["", "--force"]} ], "master_only": true } diff --git a/bin/plugin/restricted/accountAddPersonalAccess b/bin/plugin/restricted/accountAddPersonalAccess index feb7d19..a0a83ea 100755 --- a/bin/plugin/restricted/accountAddPersonalAccess +++ b/bin/plugin/restricted/accountAddPersonalAccess @@ -21,6 +21,8 @@ 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, # undocumented/compatibility: "user-any" => \my $userAny, "port-any" => \my $portAny, @@ -54,6 +56,8 @@ 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) 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,15 +98,32 @@ if (!$ip) { osh_exit 'ERR_MISSING_PARAMETER', "Missing parameter 'host' or didn't resolve correctly"; } +my $proxyIp; +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); + if ($fnret) { + $proxyIp = $fnret->value->{'ip'}; + } else { + $proxyIp = $proxyHost; + } + } +} + $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, ); if (!$fnret) { help(); @@ -176,6 +197,8 @@ $fnret = OVH::Bastion::access_modify( forcePassword => $forcePassword, comment => $comment, widestV4Prefix => ($pluginConfig ? $pluginConfig->{'widest_v4_prefix'} : undef), + proxyIp => $proxyIp, + proxyPort => $proxyPort, ); $fnret or osh_exit($fnret); @@ -194,5 +217,7 @@ 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; osh_exit OVH::Bastion::helper(cmd => \@command); diff --git a/bin/plugin/restricted/accountAddPersonalAccess.json b/bin/plugin/restricted/accountAddPersonalAccess.json index 5b5a559..83660b5 100644 --- a/bin/plugin/restricted/accountAddPersonalAccess.json +++ b/bin/plugin/restricted/accountAddPersonalAccess.json @@ -4,12 +4,18 @@ "accountAddPersonalAccess +--account" , {"ac" : [""]}, "accountAddPersonalAccess +--account +\\S+" , {"ac" : ["--host"]}, "accountAddPersonalAccess +--account +\\S+ +--host" , {"pr" : ["", "", ""]}, - "accountAddPersonalAccess +--account +\\S+ +--host +\\S+" , {"ac" : ["", "--user", "--port"]}, + "accountAddPersonalAccess +--account +\\S+ +--host +\\S+" , {"ac" : ["", "--user", "--port", "--proxy-host", "--proxy-port"]}, "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +.*--user" , {"pr" : [""]}, "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+ +.*--proxy-host" , {"pr" : ["", ""]}, + "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +.*--proxy-port" , {"pr" : [""]}, + "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--user +\\S+" , {"ac" : ["", "--port", "--proxy-host", "--proxy-port"]}, + "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--port +\\S+" , {"ac" : ["", "--user", "--proxy-host", "--proxy-port"]}, + "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--proxy-host +\\S+" , {"ac" : ["", "--user", "--port", "--proxy-port"]}, + "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--proxy-port +\\d+" , {"ac" : ["", "--user", "--port", "--proxy-host"]}, + "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+" , {"ac" : ["--force-key","--force-password","--ttl","--proxy-host","--proxy-port",""]}, + "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+" , {"ac" : ["--force-key","--force-password","--ttl","--proxy-host","--proxy-port",""]}, + "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\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" : [""]}, diff --git a/bin/plugin/restricted/accountDelPersonalAccess b/bin/plugin/restricted/accountDelPersonalAccess index 5f08ed7..acdfa9f 100755 --- a/bin/plugin/restricted/accountDelPersonalAccess +++ b/bin/plugin/restricted/accountDelPersonalAccess @@ -16,6 +16,8 @@ my $remainingOptions = OVH::Bastion::Plugin::begin( options => { "account=s" => \my $account, "protocol=s" => \my $protocol, + "proxy-host=s" => \my $proxyHost, + "proxy-port=s" => \my $proxyPort, # undocumented/compatibility: "user-any" => \my $userAny, "port-any" => \my $portAny, @@ -44,6 +46,8 @@ 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 EOF ); @@ -54,15 +58,32 @@ if (!$ip) { osh_exit 'ERR_MISSING_PARAMETER', "Missing parameter 'host' or didn't resolve correctly"; } +my $proxyIp; +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); + if ($fnret) { + $proxyIp = $fnret->value->{'ip'}; + } else { + $proxyIp = $proxyHost; + } + } +} + $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, ); if (!$fnret) { help(); @@ -82,11 +103,13 @@ $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; osh_exit OVH::Bastion::helper(cmd => \@command); diff --git a/bin/plugin/restricted/accountDelPersonalAccess.json b/bin/plugin/restricted/accountDelPersonalAccess.json index 2d1118f..c6f8075 100644 --- a/bin/plugin/restricted/accountDelPersonalAccess.json +++ b/bin/plugin/restricted/accountDelPersonalAccess.json @@ -4,12 +4,18 @@ "accountDelPersonalAccess +--account" , {"ac" : [""]}, "accountDelPersonalAccess +--account +\\S+" , {"ac" : ["--host"]}, "accountDelPersonalAccess +--account +\\S+ +--host" , {"pr" : ["", "", ""]}, - "accountDelPersonalAccess +--account +\\S+ +--host +\\S+" , {"ac" : ["", "--user", "--port"]}, + "accountDelPersonalAccess +--account +\\S+ +--host +\\S+" , {"ac" : ["", "--user", "--port", "--proxy-host", "--proxy-port"]}, "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +.*--user" , {"pr" : [""]}, "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+ +.*--proxy-host" , {"pr" : ["", ""]}, + "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +.*--proxy-port" , {"pr" : [""]}, + "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +--user +\\S+" , {"ac" : ["", "--port", "--proxy-host", "--proxy-port"]}, + "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +--port +\\S+" , {"ac" : ["", "--user", "--proxy-host", "--proxy-port"]}, + "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +--proxy-host +\\S+" , {"ac" : ["", "--user", "--port", "--proxy-port"]}, + "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +--proxy-port +\\d+" , {"ac" : ["", "--user", "--port", "--proxy-host"]}, + "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+" , {"ac" : ["", "--proxy-host", "--proxy-port"]}, + "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+" , {"ac" : ["", "--proxy-host", "--proxy-port", "--port", "--user"]}, + "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+" , {"pr" : [""]} ], "master_only": true } diff --git a/bin/plugin/restricted/selfAddPersonalAccess b/bin/plugin/restricted/selfAddPersonalAccess index e8f5dd4..277c5c8 100755 --- a/bin/plugin/restricted/selfAddPersonalAccess +++ b/bin/plugin/restricted/selfAddPersonalAccess @@ -21,6 +21,8 @@ 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, # undocumented/compatibility: "user-any" => \my $userAny, "port-any" => \my $portAny, @@ -54,6 +56,8 @@ 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) EOF ); @@ -90,15 +94,32 @@ if (!$ip) { osh_exit 'ERR_MISSING_PARAMETER', "Missing parameter 'host' or didn't resolve correctly"; } +my $proxyIp; +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); + if ($fnret) { + $proxyIp = $fnret->value->{'ip'}; + } else { + $proxyIp = $proxyHost; + } + } +} + $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, ); if (!$fnret) { help(); @@ -162,6 +183,8 @@ $fnret = OVH::Bastion::access_modify( forceKey => $forceKey, forcePassword => $forcePassword, comment => $comment, + proxyIp => $proxyIp, + proxyPort => $proxyPort, widestV4Prefix => ($pluginConfig ? $pluginConfig->{'widest_v4_prefix'} : undef), ); $fnret or osh_exit($fnret); @@ -173,7 +196,9 @@ if (not $force) { port => $port, ip => $ip, forceKey => $forceKey, - forcePassword => $forcePassword + forcePassword => $forcePassword, + proxyIp => $proxyIp, + proxyPort => $proxyPort, ); if ($fnret->is_ok and $fnret->err ne 'OK') { # we have something to say, say it @@ -200,5 +225,7 @@ 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; osh_exit OVH::Bastion::helper(cmd => \@command); diff --git a/bin/plugin/restricted/selfAddPersonalAccess.json b/bin/plugin/restricted/selfAddPersonalAccess.json index 47ecc48..392cff2 100644 --- a/bin/plugin/restricted/selfAddPersonalAccess.json +++ b/bin/plugin/restricted/selfAddPersonalAccess.json @@ -2,12 +2,18 @@ "interactive": [ "selfAddPersonalAccess" , {"ac" : ["--host"]}, "selfAddPersonalAccess +--host" , {"pr" : ["", "", ""]}, - "selfAddPersonalAccess +--host +\\S+" , {"ac" : ["", "--user", "--port"]}, + "selfAddPersonalAccess +--host +\\S+" , {"ac" : ["", "--user", "--port", "--proxy-host", "--proxy-port"]}, "selfAddPersonalAccess +--host +\\S+ +.*--user" , {"pr" : [""]}, "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+ +.*--proxy-host" , {"pr" : ["", ""]}, + "selfAddPersonalAccess +--host +\\S+ +.*--proxy-port" , {"pr" : [""]}, + "selfAddPersonalAccess +--host +\\S+ +--user +\\S+" , {"ac" : ["", "--port", "--proxy-host", "--proxy-port"]}, + "selfAddPersonalAccess +--host +\\S+ +--port +\\S+" , {"ac" : ["", "--user", "--proxy-host", "--proxy-port"]}, + "selfAddPersonalAccess +--host +\\S+ +--proxy-host +\\S+" , {"ac" : ["", "--user", "--port", "--proxy-port"]}, + "selfAddPersonalAccess +--host +\\S+ +--proxy-port +\\d+" , {"ac" : ["", "--user", "--port", "--proxy-host"]}, + "selfAddPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+" , {"ac" : ["--force-key","--force-password","--ttl","--proxy-host","--proxy-port",""]}, + "selfAddPersonalAccess +--host +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+" , {"ac" : ["--force-key","--force-password","--ttl","--proxy-host","--proxy-port",""]}, + "selfAddPersonalAccess +--host +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\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" : [""]}, diff --git a/bin/plugin/restricted/selfDelPersonalAccess b/bin/plugin/restricted/selfDelPersonalAccess index 803c10d..1b1ac9f 100755 --- a/bin/plugin/restricted/selfDelPersonalAccess +++ b/bin/plugin/restricted/selfDelPersonalAccess @@ -15,6 +15,8 @@ my $remainingOptions = OVH::Bastion::Plugin::begin( userAllowWildcards => 1, options => { "protocol=s" => \my $protocol, + "proxy-host=s" => \my $proxyHost, + "proxy-port=s" => \my $proxyPort, # undocumented/compatibility: "user-any" => \my $userAny, "port-any" => \my $portAny, @@ -42,6 +44,8 @@ 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 EOF ); @@ -52,15 +56,32 @@ if (!$ip) { osh_exit 'ERR_MISSING_PARAMETER', "Missing parameter 'host' or didn't resolve correctly"; } +my $proxyIp; +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); + if ($fnret) { + $proxyIp = $fnret->value->{'ip'}; + } else { + $proxyIp = $proxyHost; + } + } +} + $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, ); if (!$fnret) { help(); @@ -71,11 +92,13 @@ $port = $fnret->value->{'port'}; 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; osh_exit OVH::Bastion::helper(cmd => \@command); diff --git a/bin/plugin/restricted/selfDelPersonalAccess.json b/bin/plugin/restricted/selfDelPersonalAccess.json index 729bbf3..085f9dc 100644 --- a/bin/plugin/restricted/selfDelPersonalAccess.json +++ b/bin/plugin/restricted/selfDelPersonalAccess.json @@ -2,12 +2,18 @@ "interactive": [ "selfDelPersonalAccess" , {"ac" : ["--host"]}, "selfDelPersonalAccess +--host" , {"pr" : ["", "", ""]}, - "selfDelPersonalAccess +--host +\\S+" , {"ac" : ["", "--user", "--port"]}, + "selfDelPersonalAccess +--host +\\S+" , {"ac" : ["", "--user", "--port", "--proxy-host", "--proxy-port"]}, "selfDelPersonalAccess +--host +\\S+ +.*--user" , {"pr" : [""]}, "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+ +.*--proxy-host" , {"pr" : ["", ""]}, + "selfDelPersonalAccess +--host +\\S+ +.*--proxy-port" , {"pr" : [""]}, + "selfDelPersonalAccess +--host +\\S+ +--user +\\S+" , {"ac" : ["", "--port", "--proxy-host", "--proxy-port"]}, + "selfDelPersonalAccess +--host +\\S+ +--port +\\S+" , {"ac" : ["", "--user", "--proxy-host", "--proxy-port"]}, + "selfDelPersonalAccess +--host +\\S+ +--proxy-host +\\S+" , {"ac" : ["", "--user", "--port", "--proxy-port"]}, + "selfDelPersonalAccess +--host +\\S+ +--proxy-port +\\d+" , {"ac" : ["", "--user", "--port", "--proxy-host"]}, + "selfDelPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+" , {"ac" : ["", "--proxy-host", "--proxy-port"]}, + "selfDelPersonalAccess +--host +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+" , {"ac" : ["", "--proxy-host", "--proxy-port", "--port", "--user"]}, + "selfDelPersonalAccess +--host +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+" , {"pr" : [""]} ], "master_only": true } diff --git a/bin/shell/osh.pl b/bin/shell/osh.pl index 9434f99..fc33c1e 100755 --- a/bin/shell/osh.pl +++ b/bin/shell/osh.pl @@ -576,6 +576,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 diff --git a/lib/perl/OVH/Bastion.pm b/lib/perl/OVH/Bastion.pm index d065c2d..da8b8a8 100644 --- a/lib/perl/OVH/Bastion.pm +++ b/lib/perl/OVH/Bastion.pm @@ -762,15 +762,23 @@ sub is_valid_remote_user { } 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 $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; + $machine = "$machine via $proxy"; + } + return R('OK', value => $machine); } diff --git a/lib/perl/OVH/Bastion/Plugin.pm b/lib/perl/OVH/Bastion/Plugin.pm index b5819b4..646b1b2 100644 --- a/lib/perl/OVH/Bastion/Plugin.pm +++ b/lib/perl/OVH/Bastion/Plugin.pm @@ -13,9 +13,13 @@ $| = 1; use Exporter 'import'; ## no critic (ProhibitPackageVars) -our ($user, $ip, $host, $port, $scriptName, $self, $sysself, $realm, $remoteself, $HOME, $savedArgs, $pluginConfig); +our ( + $user, $ip, $host, $port, $scriptName, + $self, $sysself, $realm, $remoteself, $HOME, $savedArgs, $pluginConfig +); ## no critic (ProhibitAutomaticExportation) -our @EXPORT = qw( $user $ip $host $port $scriptName $self $sysself $realm $remoteself $HOME $savedArgs $pluginConfig ); +our @EXPORT = + qw( $user $ip $host $port $scriptName $self $sysself $realm $remoteself $HOME $savedArgs $pluginConfig ); our @EXPORT_OK = qw( help ); my $_helptext; @@ -95,6 +99,38 @@ sub validate_tuple { undef $host if $host eq ''; } + # handle proxy host resolution + # if (exists $params{'proxyHost'}) { + # $proxyHost = $params{'proxyHost'}; + # if ($proxyHost) { + # if ($proxyHost !~ m{^[a-zA-Z0-9._/:-]+$}) { + # # can be an IP (v4 or v6) or hostname + # osh_exit('KO_INVALID_PROXY_HOST', msg => "Proxy host name '$proxyHost' seems invalid"); + # } + # $fnret = OVH::Bastion::get_ip(host => $proxyHost); + # if (!$fnret) { + # osh_exit('KO_INVALID_PROXY_HOST', msg => "Proxy host name '$proxyHost' couldn't be resolved"); + # } + # else { + # $proxyIp = $fnret->value->{'ip'}; + # } + # } + # undef $proxyHost if $proxyHost eq ''; + # } + + # # handle proxy port validation + # if (exists $params{'proxyPort'}) { + # $proxyPort = $params{'proxyPort'}; + # if (defined $proxyPort && $proxyPort ne '') { + # $fnret = OVH::Bastion::is_valid_port(port => $proxyPort); + # $fnret or osh_exit $fnret; + # $proxyPort = $fnret->value; + # } + # else { + # undef $proxyPort; + # } + # } + return R('OK'); } diff --git a/lib/perl/OVH/Bastion/Plugin/ACL.pm b/lib/perl/OVH/Bastion/Plugin/ACL.pm index cd9eb48..5cbb72b 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) = + @params{qw{ port portAny user userAny scpUp scpDown sftp protocol proxyIp proxyPort }}; if ($user and $userAny) { return R('ERR_INCOMPATIBLE_PARAMETERS', @@ -84,11 +84,37 @@ sub check { ); } + # check proxy-host and proxy-port parameters + osh_debug("Checking proxy parameters: proxyIp='$proxyIp' proxyPort='$proxyPort'"); + if ($proxyIp) { + if (!$proxyPort) { + return R('ERR_MISSING_PARAMETER', msg => "When --proxy-host is specified, --proxy-port becomes mandatory"); + } + + # validate proxy host format (same as regular host validation) + if ($proxyIp !~ m{^[a-zA-Z0-9._/:-]+$}) { + return R('ERR_INVALID_PARAMETER', msg => "Proxy host name '$proxyIp' seems invalid"); + } + } + + 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 '*'; - return R('OK', value => {user => $user, port => $port, protocol => $protocol}); + return R('OK', + value => {user => $user, port => $port, protocol => $protocol, proxyIp => $proxyIp, proxyPort => $proxyPort}); } 1; diff --git a/lib/perl/OVH/Bastion/allowdeny.inc b/lib/perl/OVH/Bastion/allowdeny.inc index 8b352b7..78899b5 100644 --- a/lib/perl/OVH/Bastion/allowdeny.inc +++ b/lib/perl/OVH/Bastion/allowdeny.inc @@ -84,6 +84,9 @@ sub is_access_way_granted { 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 $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 @@ -101,9 +104,12 @@ sub is_access_way_granted { $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 exactIpMatch=$exactIpMatch exactPortMatch=$exactPortMatch exactUserMatch=$exactUserMatch"; + if (defined $wantedProxyIp) { + $check_debug_msg .= " proxyIp=$wantedProxyIp proxyPort=$wantedProxyPort"; + } + osh_debug($check_debug_msg); my %match; foreach my $entry (@acl) { @@ -111,14 +117,21 @@ 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'}) { + $check_debug_msg .= + " proxyIp=" . ($entry->{'proxyIp'} // '') . " proxyPort=" . ($entry->{'proxyPort'} // ''); + } + osh_debug($check_debug_msg); $entry->{'ip'} or next; # can't be empty @@ -213,16 +226,51 @@ sub is_access_way_granted { my $isIPv6 = (index($entry->{'ip'}, ':') != -1); my $isNetblock = (index($entry->{'ip'}, '/') != -1); + # 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"); + } + if (defined $wantedProxyPort && !defined $wantedProxyIp) { + return R('ERR_INVALID_PARAMETER', msg => "If proxyPort is given, proxyIp must be given too"); + } + # if we want an exact match, it's a simple strcmp() for IPv4 if ($exactIpMatch && !$isIPv6) { next if ($entry->{'ip'} ne $wantedIp); + # check proxy parameters + # If $wantedProxyIp is defined, we assume $wantedProxyPort is defined too + + # Case 1: proxy parameters are requested + if (defined $wantedProxyIp || defined $wantedProxyPort) { + # if proxy parameters are requested, the entry must have proxy info + if (!defined $entry->{'proxyIp'} && !defined $entry->{'proxyPort'}) { + next; + } + + # check proxy IP match + if (defined $wantedProxyIp) { + next if (!defined $entry->{'proxyIp'} || $entry->{'proxyIp'} ne $wantedProxyIp); + } + + # check proxy port match + if (defined $wantedProxyPort) { + next if (!defined $entry->{'proxyPort'} || $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 +284,43 @@ 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 || defined $wantedProxyPort) { + # 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 + if (defined $wantedProxyIp) { + $proxyMatches = 0 + if (!defined $entry->{'proxyIp'} || $entry->{'proxyIp'} ne $wantedProxyIp); + } + + # check proxy port match + if (defined $wantedProxyPort && $proxyMatches) { + $proxyMatches = 0 + if (!defined $entry->{'proxyPort'} || $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 +331,47 @@ 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 || defined $wantedProxyPort) { + # 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 + if (defined $wantedProxyIp) { + $proxyMatches = 0 + if (!defined $entry->{'proxyIp'} || $entry->{'proxyIp'} ne $wantedProxyIp); + } + + # check proxy port match + if (defined $wantedProxyPort && $proxyMatches) { + $proxyMatches = 0 + if (!defined $entry->{'proxyPort'} || $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 +385,8 @@ sub is_access_way_granted { forceKey => $match{'forceKey'}, forcePassword => $match{'forcePassword'}, comment => $match{'comment'}, + proxyIp => $match{'proxyIp'}, + proxyPort => $match{'proxyPort'}, } ); } @@ -581,7 +695,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 +711,8 @@ sub print_acls { $addedDate = substr($addedDate, 0, 10); my $forceKey = $entry->{'forceKey'} || '-'; my $forcePassword = $entry->{'forcePassword'} || '-'; + my $via = ($entry->{'proxyIp'} ? $entry->{'proxyIp'} : '-') + . ($entry->{'proxyPort'} ? ":$entry->{'proxyPort'}" : ''); my $expiry = $entry->{'expiry'} ? (duration2human(seconds => ($entry->{'expiry'} - time()))->value->{'human'}) : '-'; @@ -610,7 +726,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 +846,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 + # if proxyPort is defined, proxyIp must be defined too + 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 +866,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 +881,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 +912,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 +1079,13 @@ 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 + )->value; return R('KO_ACCESS_DENIED', msg => "Access denied for $account to $machine"); } @@ -938,9 +1094,11 @@ 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 $forceKey = $params{'forceKey'}; my $fnret; @@ -962,6 +1120,16 @@ 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; + } + $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 +1197,31 @@ 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; + push @proxyCommand, "-W", "%h:%p"; + push @proxyCommand, "-l", $user, $proxyIp; + my $proxyCommandStr = join(' ', map { /\s/ ? "'$_'" : $_ } @proxyCommand); + push @command, "-o", "ProxyCommand=\"$proxyCommandStr\""; + } + + 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 +1481,8 @@ 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 ); # extract comment if any @@ -1382,6 +1572,24 @@ 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/# COMMENT=<([^>]+)>//) { $userComment = $1; } @@ -1404,6 +1612,8 @@ sub _get_acl_from_file { addedDate => $addedDate, userComment => $userComment, comment => $extra, + proxyIp => $proxyIp, + proxyPort => $proxyPort, }; } diff --git a/lib/perl/OVH/Bastion/allowkeeper.inc b/lib/perl/OVH/Bastion/allowkeeper.inc index 2fb0a0c..25c4f26 100644 --- a/lib/perl/OVH/Bastion/allowkeeper.inc +++ b/lib/perl/OVH/Bastion/allowkeeper.inc @@ -316,6 +316,9 @@ sub access_modify { my $forceKey = $params{'forceKey'}; my $forcePassword = $params{'forcePassword'}; + my $proxyIp = $params{'proxyIp'}; + my $proxyPort = $params{'proxyPort'}; + my $dryrun = $params{'dryrun'}; # don't do anything, just check params and prereqs my $sudo = $params{'sudo'}; # passed as-is to subs we use @@ -445,6 +448,26 @@ sub access_modify { } } + if (defined $proxyIp) { + $fnret = OVH::Bastion::is_valid_ip(ip => $proxyIp, allowSubnets => 0); + return $fnret unless $fnret; + $proxyIp = $fnret->value->{'ip'}; + } + + if (defined $proxyPort) { + $fnret = OVH::Bastion::is_valid_port(port => $proxyPort); + return $fnret unless $fnret; + $proxyPort = $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 @@ -525,6 +548,8 @@ sub access_modify { way => $way, group => $shortGroup, account => $account, + proxyIp => $proxyIp, + proxyPort => $proxyPort, exactMatch => 1, # we're checking if the exact right we're asked to modify exists or not ); osh_debug("... result is $fnret"); @@ -607,6 +632,12 @@ sub access_modify { if ($ttl) { $entry .= " # EXPIRY=" . (time() + $ttl); } + if ($proxyIp) { + $entry .= " # PROXYHOST=" . $proxyIp; + } + if ($proxyPort) { + $entry .= " # PROXYPORT=" . $proxyPort; + } if ($comment) { $comment =~ s{[#<>\\"']}{_}g; $entry .= " # COMMENT=<" . $comment . ">"; @@ -680,6 +711,8 @@ sub access_modify { ['force_key', $params{'forceKey'}], ['force_password', $params{'forcePassword'}], ['comment', $params{'comment'}], + ['proxy_ip', $proxyIp], + ['proxy_port', $proxyPort], ] ); return R('OK', msg => $returnmsg) if $returnmsg; 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..59775ce --- /dev/null +++ b/tests/unit/tests/is_access_granted_proxy.t @@ -0,0 +1,217 @@ +#! /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", + "198.51.100.0/24:22 # PROXYHOST=10.0.0.1 # PROXYPORT=2222", + # 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: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 - should only work with exact proxy match +$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'; +$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 +$want{"192.0.2.30"}{"22"}{"me"}{"10.0.0.2"}{"3333"} = 'OK'; +$want{"192.0.2.30"}{"22"}{"me"}{"10.0.0.1"}{"2222"} = 'KO_ACCESS_DENIED'; # wrong proxy + +# Test 4: Subnet access with proxy (198.51.100.0/24 covers 198.51.100.0 - 198.51.100.255) +$want{"198.51.100.100"}{"22"}{"me"}{"10.0.0.1"}{"2222"} = 'OK'; # subnet match with proxy +$want{"198.51.100.200"}{"22"}{"me"}{"10.0.0.1"}{"2222"} = 'OK'; # subnet match with proxy +$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 +$want{"192.0.2.50"}{"22"}{"me"}{"10.0.0.4"}{"4444"} = 'OK'; # port wildcard with proxy +$want{"192.0.2.50"}{"80"}{"me"}{"10.0.0.4"}{"4444"} = 'OK'; # port wildcard with proxy +$want{"192.0.2.50"}{"22"}{"me"}{"10.0.0.4"}{"5555"} = 'KO_ACCESS_DENIED'; # wrong proxy port + +# Test 6: User wildcard with proxy - this tests a specific edge case +$want{"192.0.2.60"}{"22"}{"root"}{"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"} = 'OK'; # user wildcard should match any user with exact proxy +$want{"192.0.2.60"}{"22"}{"root"}{"10.0.0.5"}{"6666"} = 'KO_ACCESS_DENIED'; # user wildcard but wrong proxy port + +# Test 7: 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 8: 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 9: IPv6 access with specific proxy - should only work with exact proxy match +$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"}{"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 10: IPv6 different proxy configuration +$want{"2001:db8::30"}{"80"}{"me"}{"2001:db8:cafe::2"}{"3333"} = 'OK'; +$want{"2001:db8::30"}{"80"}{"me"}{"2001:db8:cafe::1"}{"2222"} = 'KO_ACCESS_DENIED'; # wrong proxy + +# Test 11: IPv6 user wildcard with proxy +$want{"2001:db8::40"}{"22"}{"root"}{"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"} = 'OK'; # user wildcard should match any user with exact proxy +$want{"2001:db8::40"}{"22"}{"root"}{"2001:db8:cafe::4"}{"5555"} = 'KO_ACCESS_DENIED'; # user wildcard but wrong proxy port + +# Test 12: IPv6 subnet access with proxy (2001:aaaa::/64 covers 2001:db8::0 - 2001:aaaa::ffff:ffff:ffff:ffff) +$want{"2001:aaaa::100"}{"22"}{"me"}{"2001:db8:cafe::1"}{"2222"} = 'OK'; # subnet match with proxy +$want{"2001:aaaa::200"}{"22"}{"me"}{"2001:db8:cafe::1"}{"2222"} = 'OK'; # subnet match with proxy +$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 13: 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.99 + 2001:db8::10 + 2001:db8::11 + 2001:db8::20 + 2001:db8::30 + 2001:db8::40 + 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") { + # Skip combinations that don't make sense (proxy port without proxy IP) + next if (!defined $proxyIp || $proxyIp eq $undef) && (defined $proxyPort && $proxyPort ne $undef); + + my $expected = $want{$ip}{$port}{$user}{$proxyIp // $undef}{$proxyPort // $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) { + $params{proxyIp} = $proxyIp; + } + if (defined $proxyPort && $proxyPort ne $undef) { + $params{proxyPort} = $proxyPort; + } + + my $result = OVH::Bastion::is_access_granted(%params); + + my $test_desc = sprintf( + "is_access_granted with %s@%s:%s proxy=%s:%s", + $user, $ip, $port, + $proxyIp // '', + $proxyPort // '' + ); + + is($result->err, $expected, $test_desc); + + # If access is granted, verify proxy information is returned + _verify_proxy_information($expected, $proxyIp, $proxyPort, $result, $test_desc); + } + } + } + } +} + +sub _verify_proxy_information { + my ($expected, $proxyIp, $proxyPort, $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"); + } + last; + } + ok($found_proxy, "$test_desc - proxy IP returned in grant"); + return; +} + +done_testing(); From aabbbf959a0af8f542c760cdc00b52a7ac86c510 Mon Sep 17 00:00:00 2001 From: jon4hz Date: Tue, 7 Oct 2025 21:02:44 +0200 Subject: [PATCH 02/49] feat(osh): parse proxyjump options correctly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jonah Zürcher --- bin/shell/osh.pl | 115 ++++++++++++++++++++++++++++- lib/perl/OVH/Bastion.pm | 1 + lib/perl/OVH/Bastion/allowdeny.inc | 5 +- 3 files changed, 116 insertions(+), 5 deletions(-) diff --git a/bin/shell/osh.pl b/bin/shell/osh.pl index fc33c1e..b7aeb1e 100755 --- a/bin/shell/osh.pl +++ b/bin/shell/osh.pl @@ -237,6 +237,45 @@ else { # my @saved_argv = @ARGV; +# Check if this is a ProxyJump connection that should be executed directly +if ($ENV{'OSH_PROXYJUMP_CONNECTION'}) { + osh_debug("Detected ProxyJump connection, executing command directly"); + + # Extract the command from the realOptions or ARGV + my $proxy_command; + if (@ARGV && $ARGV[0] eq '-c' && $ARGV[1]) { + $proxy_command = $ARGV[1]; + } else { + $proxy_command = join(' ', @ARGV); + } + + osh_debug("ProxyJump command: $proxy_command"); + + # Execute the proxy command directly without further validation + if ($proxy_command) { + # Parse the command to extract program and arguments + my @cmd_parts = split(/\s+/, $proxy_command); + if (!@cmd_parts) { + main_exit(OVH::Bastion::EXIT_EXEC_FAILED, "exec_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, "exec_failed", "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, "exec_failed", "Failed to execute proxy command: $!"); + } else { + main_exit(OVH::Bastion::EXIT_EXEC_FAILED, "exec_failed", "No proxy command provided"); + } +} + # these options are the ones on shell definition of user calling osh.pl, # the user-passed commands are stringified after "-c" (as in sh -c) # it's possible to define the shell as osh.pl --debug, to force debug @@ -382,6 +421,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(); @@ -502,9 +542,11 @@ if ($interactive and not $ENV{'OSH_IN_INTERACTIVE_SESSION'}) { # hmm, we are under mosh, mosh needs something to exec, so let's # re-exec ourselves in interactive mode + # TODO: does this work with proxyjump? exec(@toExecute, $0, '-c', $realOptions); } + # TODO: log proxy info my $logret = OVH::Bastion::log_access_insert( account => $self, cmdtype => 'interactive', @@ -600,6 +642,40 @@ else { } } +my $proxyIp = undef; +my $proxyPort = 22; +# Parse proxyjump args if specified +if ($proxyJump) { + if ($proxyJump =~ /^(\[?[a-zA-Z0-9._-]+\]?)(?::(\d+))?$/) { + $proxyIp = $1; + $proxyPort = $2 if $2; + osh_debug("parsed proxyjump: host=$proxyIp port=$proxyPort"); + } + else { + main_exit OVH::Bastion::EXIT_INVALID_PROXYJUMP, 'invalid_proxyjump', + "Invalid proxyjump specification '$proxyJump', should be host[:port]"; + } + + $fnret = OVH::Bastion::get_ip(host => $proxyIp, allowSubnets => 0); + if (!$fnret && (($osh_command && $host) || !$osh_command)) { + 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; + } + } + $proxyIp = $fnret->value->{'ip'}; + osh_debug("Proxyjump host $proxyIp resolved to IP " . $fnret->value->{'ip'}); + + $ENV{'OSH_PROXYJUMP_HOST'} = $proxyIp; + $ENV{'OSH_PROXYJUMP_PORT'} = $proxyPort; + $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))) { @@ -749,6 +825,7 @@ my %ingressRealm = ( my $pivEffectivePolicyEnabled = OVH::Bastion::is_effective_piv_account_policy_enabled(account => $self); # if we're coming from a realm, we're receiving a connection from another bastion, keep all the traces: +# TODO: do we need to do something here regarding proxyjump? my @previous_bastion_details; if ($realm && $ENV{'LC_BASTION_DETAILS'}) { my $decoded_details; @@ -814,6 +891,8 @@ osh_debug("self : " . (defined $host ? $host : '') . "\n" . "port : " . (defined $port ? $port : '') . "\n" + . "proxyJump : " + . (defined $proxyJump ? $proxyJump : '') . "\n" . "verbose : " . (defined $verbose ? $verbose : '') . "\n" . "tty : " @@ -829,6 +908,7 @@ my $hostto = OVH::Bastion::ip2host($host)->value || $host; # Special case: adminSudo for ssh connection as another user if ($sshAs) { $fnret = OVH::Bastion::is_admin(account => $self); + # TODO: log proxyjump info my $logret = OVH::Bastion::log_access_insert( account => $self, cmdtype => 'sshas', @@ -1178,6 +1258,8 @@ else { ipfrom => $ipfrom, ip => $ip, port => $port, + proxyIp => $proxyIp ? $proxyIp : undef, + proxyPort => $proxyPort ? $proxyPort : undef, details => 1 ); } @@ -1192,6 +1274,7 @@ if (!$fnret) { $message .= " (tried with remote user '$user')"; # "root is not the default login anymore" } + # TODO: log proxyjump info my $logret = OVH::Bastion::log_access_insert( account => $self, cmdtype => $telnet ? 'telnet' : 'ssh', @@ -1226,6 +1309,7 @@ if ($osh_debug) { } # build ttyrec command that'll prefix the real command +# TODO: support proxyjump properly here my $ttyrec_fnret = OVH::Bastion::build_ttyrec_cmdline_part1of2( ip => $ip, port => $port, @@ -1400,6 +1484,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'; @@ -1453,7 +1538,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 { @@ -1471,6 +1556,34 @@ else { push @command, '-T' if $notty; push @command, '-o', "ConnectTimeout=$timeout" if $timeout; + + if ($proxyJump) { + # 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', $user, '-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); diff --git a/lib/perl/OVH/Bastion.pm b/lib/perl/OVH/Bastion.pm index da8b8a8..8518c24 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 { diff --git a/lib/perl/OVH/Bastion/allowdeny.inc b/lib/perl/OVH/Bastion/allowdeny.inc index 78899b5..82410c6 100644 --- a/lib/perl/OVH/Bastion/allowdeny.inc +++ b/lib/perl/OVH/Bastion/allowdeny.inc @@ -230,9 +230,6 @@ sub is_access_way_granted { if (defined $wantedProxyIp && !defined $wantedProxyPort) { return R('ERR_INVALID_PARAMETER', msg => "If proxyIp is given, proxyPort must be given too"); } - if (defined $wantedProxyPort && !defined $wantedProxyIp) { - return R('ERR_INVALID_PARAMETER', msg => "If proxyPort is given, proxyIp must be given too"); - } # if we want an exact match, it's a simple strcmp() for IPv4 if ($exactIpMatch && !$isIPv6) { @@ -336,7 +333,7 @@ sub is_access_way_granted { my $proxyMatches = 1; # Case 1: proxy parameters are requested - if (defined $wantedProxyIp || defined $wantedProxyPort) { + if (defined $wantedProxyIp) { # if proxy parameters are requested, the entry must have proxy info if (!defined $entry->{'proxyIp'} && !defined $entry->{'proxyPort'}) { $proxyMatches = 0; From 030b334b20192d0b1d670d3f4fe0161f7ae28eff Mon Sep 17 00:00:00 2001 From: jon4hz Date: Wed, 8 Oct 2025 15:16:35 +0200 Subject: [PATCH 03/49] feat(scp): support proxyjump MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jonah Zürcher --- bin/plugin/open/scp | 90 +++++++++++++++++--- lib/perl/OVH/Bastion/Plugin/otherProtocol.pm | 40 ++++++--- 2 files changed, 106 insertions(+), 24 deletions(-) diff --git a/bin/plugin/open/scp b/bin/plugin/open/scp index cedccaf..b398023 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="$SELF" 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 @@ -165,7 +176,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) @@ -221,7 +236,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 @@ -282,6 +301,34 @@ EOF # code # my $fnret; +my $proxyIp = undef; +my $proxyPort = 22; +# Parse proxyjump args if specified +if ($proxyJump) { + if ($proxyJump =~ /^(\[?[a-zA-Z0-9._-]+\]?)(?::(\d+))?$/) { + $proxyIp = $1; + $proxyPort = $2 if $2; + osh_debug("parsed proxyjump: host=$proxyIp port=$proxyPort"); + } + else { + osh_exit 'ERR_INVALID_PARAMETER', "Invalid proxyjump specification '$proxyJump', should be 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 (not $host) { help(); @@ -314,11 +361,13 @@ $port ||= 22; # scp uses 22 if not $user ||= $self; # same for 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, ); $fnret or osh_exit($fnret); @@ -334,6 +383,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', $user, '-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/lib/perl/OVH/Bastion/Plugin/otherProtocol.pm b/lib/perl/OVH/Bastion/Plugin/otherProtocol.pm index 206a452..50e67df 100644 --- a/lib/perl/OVH/Bastion/Plugin/otherProtocol.pm +++ b/lib/perl/OVH/Bastion/Plugin/otherProtocol.pm @@ -15,29 +15,39 @@ 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 $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 + )->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, + details => 1 ); if (not $fnret) { @@ -60,6 +70,8 @@ sub has_protocol_access { ipfrom => $ipfrom, ip => $ip, port => $port, + proxyIp => $proxyIp, + proxyPort => $proxyPort, exactUserMatch => 1, details => 1 ); From e710b006adaf723e40839c5352d97ad826e38682 Mon Sep 17 00:00:00 2001 From: jon4hz Date: Wed, 8 Oct 2025 15:19:52 +0200 Subject: [PATCH 04/49] chore: adjust machine display to include proxy info MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jonah Zürcher --- bin/shell/osh.pl | 12 ++++++++++-- lib/perl/OVH/Bastion/allowkeeper.inc | 16 ++++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/bin/shell/osh.pl b/bin/shell/osh.pl index b7aeb1e..a1b4a21 100755 --- a/bin/shell/osh.pl +++ b/bin/shell/osh.pl @@ -1236,10 +1236,18 @@ $user = $user || $config->{'defaultLogin'} || $remoteself || $sysself; # 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 + )->value, + ); if (!$quiet) { diff --git a/lib/perl/OVH/Bastion/allowkeeper.inc b/lib/perl/OVH/Bastion/allowkeeper.inc index 25c4f26..2c46e12 100644 --- a/lib/perl/OVH/Bastion/allowkeeper.inc +++ b/lib/perl/OVH/Bastion/allowkeeper.inc @@ -659,7 +659,13 @@ 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 + )->value; my $ttlmsg = $ttl ? (' (expires in ' . OVH::Bastion::duration2human(seconds => $ttl)->value->{'human'} . ')') : ''; $returnmsg = "Access to $machine successfully added$ttlmsg"; @@ -684,7 +690,13 @@ 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 + )->value; $returnmsg = "Access to $machine successfully removed"; } else { From 157a7a18825c7c2be555022ef718f71bd10efe41 Mon Sep 17 00:00:00 2001 From: jon4hz Date: Wed, 8 Oct 2025 15:20:01 +0200 Subject: [PATCH 05/49] chore: run perl tidy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jonah Zürcher --- bin/plugin/group-aclkeeper/groupAddServer | 6 +- bin/plugin/group-aclkeeper/groupDelServer | 16 +++-- .../restricted/accountAddPersonalAccess | 6 +- .../restricted/accountDelPersonalAccess | 14 ++-- bin/plugin/restricted/selfAddPersonalAccess | 6 +- bin/plugin/restricted/selfDelPersonalAccess | 12 ++-- bin/shell/osh.pl | 69 ++++++++++--------- lib/perl/OVH/Bastion/Plugin.pm | 8 +-- lib/perl/OVH/Bastion/Plugin/groupSetRole.pm | 2 +- lib/perl/OVH/Bastion/allowdeny.inc | 2 +- 10 files changed, 77 insertions(+), 64 deletions(-) diff --git a/bin/plugin/group-aclkeeper/groupAddServer b/bin/plugin/group-aclkeeper/groupAddServer index ef63a71..33706ec 100755 --- a/bin/plugin/group-aclkeeper/groupAddServer +++ b/bin/plugin/group-aclkeeper/groupAddServer @@ -85,11 +85,13 @@ if ($proxyHost) { $fnret = OVH::Bastion::is_valid_ip(ip => $proxyHost, allowSubnets => 0); if ($fnret) { $proxyIp = $fnret->value->{'ip'}; - } else { + } + else { $fnret = OVH::Bastion::get_ip(host => $proxyHost); if ($fnret) { $proxyIp = $fnret->value->{'ip'}; - } else { + } + else { $proxyIp = $proxyHost; } } diff --git a/bin/plugin/group-aclkeeper/groupDelServer b/bin/plugin/group-aclkeeper/groupDelServer index b2ef0a0..7cfcb08 100755 --- a/bin/plugin/group-aclkeeper/groupDelServer +++ b/bin/plugin/group-aclkeeper/groupDelServer @@ -14,11 +14,11 @@ 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, - "proxy-host=s" => \my $proxyHost, - "proxy-port=s" => \my $proxyPort, + "group=s" => \my $group, + "protocol=s" => \my $protocol, + "force" => \my $force, + "proxy-host=s" => \my $proxyHost, + "proxy-port=s" => \my $proxyPort, # undocumented/compatibility: "user-any" => \my $userAny, "port-any" => \my $portAny, @@ -74,11 +74,13 @@ if ($proxyHost) { $fnret = OVH::Bastion::is_valid_ip(ip => $proxyHost, allowSubnets => 0); if ($fnret) { $proxyIp = $fnret->value->{'ip'}; - } else { + } + else { $fnret = OVH::Bastion::get_ip(host => $proxyHost); if ($fnret) { $proxyIp = $fnret->value->{'ip'}; - } else { + } + else { $proxyIp = $proxyHost; } } diff --git a/bin/plugin/restricted/accountAddPersonalAccess b/bin/plugin/restricted/accountAddPersonalAccess index a0a83ea..fc724c1 100755 --- a/bin/plugin/restricted/accountAddPersonalAccess +++ b/bin/plugin/restricted/accountAddPersonalAccess @@ -103,11 +103,13 @@ if ($proxyHost) { $fnret = OVH::Bastion::is_valid_ip(ip => $proxyHost, allowSubnets => 0); if ($fnret) { $proxyIp = $fnret->value->{'ip'}; - } else { + } + else { $fnret = OVH::Bastion::get_ip(host => $proxyHost); if ($fnret) { $proxyIp = $fnret->value->{'ip'}; - } else { + } + else { $proxyIp = $proxyHost; } } diff --git a/bin/plugin/restricted/accountDelPersonalAccess b/bin/plugin/restricted/accountDelPersonalAccess index acdfa9f..697ea74 100755 --- a/bin/plugin/restricted/accountDelPersonalAccess +++ b/bin/plugin/restricted/accountDelPersonalAccess @@ -14,10 +14,10 @@ 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, - "proxy-host=s" => \my $proxyHost, - "proxy-port=s" => \my $proxyPort, + "account=s" => \my $account, + "protocol=s" => \my $protocol, + "proxy-host=s" => \my $proxyHost, + "proxy-port=s" => \my $proxyPort, # undocumented/compatibility: "user-any" => \my $userAny, "port-any" => \my $portAny, @@ -63,11 +63,13 @@ if ($proxyHost) { $fnret = OVH::Bastion::is_valid_ip(ip => $proxyHost, allowSubnets => 0); if ($fnret) { $proxyIp = $fnret->value->{'ip'}; - } else { + } + else { $fnret = OVH::Bastion::get_ip(host => $proxyHost); if ($fnret) { $proxyIp = $fnret->value->{'ip'}; - } else { + } + else { $proxyIp = $proxyHost; } } diff --git a/bin/plugin/restricted/selfAddPersonalAccess b/bin/plugin/restricted/selfAddPersonalAccess index 277c5c8..0ad856c 100755 --- a/bin/plugin/restricted/selfAddPersonalAccess +++ b/bin/plugin/restricted/selfAddPersonalAccess @@ -99,11 +99,13 @@ if ($proxyHost) { $fnret = OVH::Bastion::is_valid_ip(ip => $proxyHost, allowSubnets => 0); if ($fnret) { $proxyIp = $fnret->value->{'ip'}; - } else { + } + else { $fnret = OVH::Bastion::get_ip(host => $proxyHost); if ($fnret) { $proxyIp = $fnret->value->{'ip'}; - } else { + } + else { $proxyIp = $proxyHost; } } diff --git a/bin/plugin/restricted/selfDelPersonalAccess b/bin/plugin/restricted/selfDelPersonalAccess index 1b1ac9f..a2de6f7 100755 --- a/bin/plugin/restricted/selfDelPersonalAccess +++ b/bin/plugin/restricted/selfDelPersonalAccess @@ -14,9 +14,9 @@ my $remainingOptions = OVH::Bastion::Plugin::begin( header => "removing personal access to a server from an account", userAllowWildcards => 1, options => { - "protocol=s" => \my $protocol, - "proxy-host=s" => \my $proxyHost, - "proxy-port=s" => \my $proxyPort, + "protocol=s" => \my $protocol, + "proxy-host=s" => \my $proxyHost, + "proxy-port=s" => \my $proxyPort, # undocumented/compatibility: "user-any" => \my $userAny, "port-any" => \my $portAny, @@ -61,11 +61,13 @@ if ($proxyHost) { $fnret = OVH::Bastion::is_valid_ip(ip => $proxyHost, allowSubnets => 0); if ($fnret) { $proxyIp = $fnret->value->{'ip'}; - } else { + } + else { $fnret = OVH::Bastion::get_ip(host => $proxyHost); if ($fnret) { $proxyIp = $fnret->value->{'ip'}; - } else { + } + else { $proxyIp = $proxyHost; } } diff --git a/bin/shell/osh.pl b/bin/shell/osh.pl index a1b4a21..a4b7594 100755 --- a/bin/shell/osh.pl +++ b/bin/shell/osh.pl @@ -240,17 +240,18 @@ my @saved_argv = @ARGV; # Check if this is a ProxyJump connection that should be executed directly if ($ENV{'OSH_PROXYJUMP_CONNECTION'}) { osh_debug("Detected ProxyJump connection, executing command directly"); - + # Extract the command from the realOptions or ARGV my $proxy_command; if (@ARGV && $ARGV[0] eq '-c' && $ARGV[1]) { $proxy_command = $ARGV[1]; - } else { + } + else { $proxy_command = join(' ', @ARGV); } - + osh_debug("ProxyJump command: $proxy_command"); - + # Execute the proxy command directly without further validation if ($proxy_command) { # Parse the command to extract program and arguments @@ -258,7 +259,7 @@ if ($ENV{'OSH_PROXYJUMP_CONNECTION'}) { if (!@cmd_parts) { main_exit(OVH::Bastion::EXIT_EXEC_FAILED, "exec_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; @@ -268,10 +269,12 @@ if ($ENV{'OSH_PROXYJUMP_CONNECTION'}) { if ($cmd_parts[0] ne 'ssh') { main_exit(OVH::Bastion::EXIT_EXEC_FAILED, "exec_failed", "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, "exec_failed", "Failed to execute proxy command: $!"); - } else { + exec(@cmd_parts) + or main_exit(OVH::Bastion::EXIT_EXEC_FAILED, "exec_failed", "Failed to execute proxy command: $!"); + } + else { main_exit(OVH::Bastion::EXIT_EXEC_FAILED, "exec_failed", "No proxy command provided"); } } @@ -642,12 +645,12 @@ else { } } -my $proxyIp = undef; +my $proxyIp = undef; my $proxyPort = 22; # Parse proxyjump args if specified if ($proxyJump) { if ($proxyJump =~ /^(\[?[a-zA-Z0-9._-]+\]?)(?::(\d+))?$/) { - $proxyIp = $1; + $proxyIp = $1; $proxyPort = $2 if $2; osh_debug("parsed proxyjump: host=$proxyIp port=$proxyPort"); } @@ -668,11 +671,11 @@ if ($proxyJump) { main_exit OVH::Bastion::EXIT_HOST_NOT_FOUND, 'host_not_found', $fnret->msg; } } - $proxyIp = $fnret->value->{'ip'}; osh_debug("Proxyjump host $proxyIp resolved to IP " . $fnret->value->{'ip'}); + $proxyIp = $fnret->value->{'ip'}; - $ENV{'OSH_PROXYJUMP_HOST'} = $proxyIp; - $ENV{'OSH_PROXYJUMP_PORT'} = $proxyPort; + $ENV{'OSH_PROXYJUMP_HOST'} = $proxyIp; + $ENV{'OSH_PROXYJUMP_PORT'} = $proxyPort; $ENV{'OSH_PROXYJUMP_CONNECTION'} = 1; } @@ -1030,6 +1033,7 @@ if ($osh_command) { # Then test for rights $fnret = OVH::Bastion::can_account_execute_plugin(account => $self, plugin => $osh_command); + # TODO: log proxyjump info my $logret = OVH::Bastion::log_access_insert( account => $self, cmdtype => 'osh', @@ -1261,14 +1265,14 @@ if ($fnret and $fnret->value() =~ /yes/) { } else { $fnret = OVH::Bastion::is_access_granted( - account => $self, - user => $user, - ipfrom => $ipfrom, - ip => $ip, - port => $port, - proxyIp => $proxyIp ? $proxyIp : undef, - proxyPort => $proxyPort ? $proxyPort : undef, - details => 1 + account => $self, + user => $user, + ipfrom => $ipfrom, + ip => $ip, + port => $port, + proxyIp => $proxyIp, + proxyPort => $proxyPort, + details => 1 ); } @@ -1564,17 +1568,16 @@ else { push @command, '-T' if $notty; push @command, '-o', "ConnectTimeout=$timeout" if $timeout; - if ($proxyJump) { # 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', $user, '-W', '%h:%p', $proxyIp; @@ -1584,11 +1587,11 @@ else { } } 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"); } @@ -2027,12 +2030,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/lib/perl/OVH/Bastion/Plugin.pm b/lib/perl/OVH/Bastion/Plugin.pm index 646b1b2..c6b8ca9 100644 --- a/lib/perl/OVH/Bastion/Plugin.pm +++ b/lib/perl/OVH/Bastion/Plugin.pm @@ -13,13 +13,9 @@ $| = 1; use Exporter 'import'; ## no critic (ProhibitPackageVars) -our ( - $user, $ip, $host, $port, $scriptName, - $self, $sysself, $realm, $remoteself, $HOME, $savedArgs, $pluginConfig -); +our ($user, $ip, $host, $port, $scriptName, $self, $sysself, $realm, $remoteself, $HOME, $savedArgs, $pluginConfig); ## no critic (ProhibitAutomaticExportation) -our @EXPORT = - qw( $user $ip $host $port $scriptName $self $sysself $realm $remoteself $HOME $savedArgs $pluginConfig ); +our @EXPORT = qw( $user $ip $host $port $scriptName $self $sysself $realm $remoteself $HOME $savedArgs $pluginConfig ); our @EXPORT_OK = qw( help ); my $_helptext; diff --git a/lib/perl/OVH/Bastion/Plugin/groupSetRole.pm b/lib/perl/OVH/Bastion/Plugin/groupSetRole.pm index a7ac915..e27d0c3 100644 --- a/lib/perl/OVH/Bastion/Plugin/groupSetRole.pm +++ b/lib/perl/OVH/Bastion/Plugin/groupSetRole.pm @@ -209,7 +209,7 @@ sub act { my $machine = OVH::Bastion::machine_display( ip => $access->{'ip'}, port => $access->{'port'}, - user => $access->{'user'} + user => $access->{'user'}, )->value; $fnret = OVH::Bastion::Plugin::groupSetRole::act( account => $account, diff --git a/lib/perl/OVH/Bastion/allowdeny.inc b/lib/perl/OVH/Bastion/allowdeny.inc index 82410c6..5332705 100644 --- a/lib/perl/OVH/Bastion/allowdeny.inc +++ b/lib/perl/OVH/Bastion/allowdeny.inc @@ -1198,7 +1198,7 @@ sub ssh_test_access_way { # add proxyjump when specified if (defined $proxyIp) { - my @proxyCommand = qw{ ssh -o ConnectTimeout=5 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no }; + my @proxyCommand = qw{ ssh -o ConnectTimeout=5 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no }; push @proxyCommand, '-o', 'PreferredAuthentications=' . $preferredAuthentications; foreach (@keyList) { push @proxyCommand, "-i", $_; From 186826112940c20bf0c1497dd5aa6990ef37a10e Mon Sep 17 00:00:00 2001 From: jon4hz Date: Fri, 10 Oct 2025 16:33:37 +0200 Subject: [PATCH 06/49] fix: handle access check correctly with proxy options --- lib/perl/OVH/Bastion/allowdeny.inc | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/perl/OVH/Bastion/allowdeny.inc b/lib/perl/OVH/Bastion/allowdeny.inc index 5332705..bede4aa 100644 --- a/lib/perl/OVH/Bastion/allowdeny.inc +++ b/lib/perl/OVH/Bastion/allowdeny.inc @@ -235,11 +235,8 @@ sub is_access_way_granted { if ($exactIpMatch && !$isIPv6) { next if ($entry->{'ip'} ne $wantedIp); - # check proxy parameters - # If $wantedProxyIp is defined, we assume $wantedProxyPort is defined too - # Case 1: proxy parameters are requested - if (defined $wantedProxyIp || defined $wantedProxyPort) { + if (defined $wantedProxyIp) { # if proxy parameters are requested, the entry must have proxy info if (!defined $entry->{'proxyIp'} && !defined $entry->{'proxyPort'}) { next; @@ -286,7 +283,7 @@ sub is_access_way_granted { my $proxyMatches = 1; # Case 1: proxy parameters are requested - if (defined $wantedProxyIp || defined $wantedProxyPort) { + if (defined $wantedProxyIp) { # if proxy parameters are requested, the entry must have proxy info if (!defined $entry->{'proxyIp'} && !defined $entry->{'proxyPort'}) { $proxyMatches = 0; From 9da71bd46e497fff4475fc9dedc36c97397afd64 Mon Sep 17 00:00:00 2001 From: jon4hz Date: Mon, 27 Oct 2025 21:34:26 +0100 Subject: [PATCH 07/49] fix: reset proxy connection env var --- bin/shell/osh.pl | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/shell/osh.pl b/bin/shell/osh.pl index a4b7594..d7e2628 100755 --- a/bin/shell/osh.pl +++ b/bin/shell/osh.pl @@ -239,6 +239,7 @@ my @saved_argv = @ARGV; # Check if this is a ProxyJump connection that should be executed directly if ($ENV{'OSH_PROXYJUMP_CONNECTION'}) { + $ENV{'OSH_PROXYJUMP_CONNECTION'} = 0; # make sure nothing else gets interpreted as proxyjump osh_debug("Detected ProxyJump connection, executing command directly"); # Extract the command from the realOptions or ARGV From f79d4a03d5aa019be395cbb2871db281b8bd55d8 Mon Sep 17 00:00:00 2001 From: jon4hz Date: Mon, 27 Oct 2025 21:35:42 +0100 Subject: [PATCH 08/49] fix: handle proxy connection in access test --- lib/perl/OVH/Bastion/allowdeny.inc | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/perl/OVH/Bastion/allowdeny.inc b/lib/perl/OVH/Bastion/allowdeny.inc index bede4aa..71b1aca 100644 --- a/lib/perl/OVH/Bastion/allowdeny.inc +++ b/lib/perl/OVH/Bastion/allowdeny.inc @@ -1200,11 +1200,17 @@ sub ssh_test_access_way { foreach (@keyList) { push @proxyCommand, "-i", $_; } - push @proxyCommand, "-p", $proxyPort if $proxyPort; - push @proxyCommand, "-W", "%h:%p"; - push @proxyCommand, "-l", $user, $proxyIp; + push @proxyCommand, "-p", $proxyPort if $proxyPort && $proxyPort != 22; + push @proxyCommand, '-l', $user, '-W', '%h:%p', $proxyIp; + my $proxyCommandStr = join(' ', map { /\s/ ? "'$_'" : $_ } @proxyCommand); - push @command, "-o", "ProxyCommand=\"$proxyCommandStr\""; + osh_debug("Testing with ProxyCommand: $proxyCommandStr"); + + push @command, '-o', "ProxyCommand=$proxyCommandStr"; + + $ENV{'OSH_PROXYJUMP_HOST'} = $proxyIp; + $ENV{'OSH_PROXYJUMP_PORT'} = $proxyPort; + $ENV{'OSH_PROXYJUMP_CONNECTION'} = 1; } push @command, '-T', '--', 'true'; From df6791bdc94a143e63f4bf46ab1a50f21a8832cb Mon Sep 17 00:00:00 2001 From: jon4hz Date: Wed, 29 Oct 2025 10:54:07 +0100 Subject: [PATCH 09/49] fix: return proxyIP and proxyPort in json output --- bin/helper/osh-accountModifyPersonalAccess | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/bin/helper/osh-accountModifyPersonalAccess b/bin/helper/osh-accountModifyPersonalAccess index 4392747..8b715e6 100755 --- a/bin/helper/osh-accountModifyPersonalAccess +++ b/bin/helper/osh-accountModifyPersonalAccess @@ -127,13 +127,15 @@ if ($fnret->err eq 'OK') { 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, }, msg => $action eq 'add' ? "Access to $machine was added to account $account$ttlmsg" From 11df17b62467add6cc6f05183e6cab1621bad903 Mon Sep 17 00:00:00 2001 From: jon4hz Date: Wed, 29 Oct 2025 16:08:42 +0100 Subject: [PATCH 10/49] fix: correct proxy parameter in groupAddServer helper --- bin/helper/osh-groupAddServer | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/helper/osh-groupAddServer b/bin/helper/osh-groupAddServer index 93a8d59..28a2afb 100755 --- a/bin/helper/osh-groupAddServer +++ b/bin/helper/osh-groupAddServer @@ -33,7 +33,7 @@ eval { "force-key=s" => sub { $forceKey //= $_[1] }, "ttl=i" => sub { $ttl //= $_[1] }, "comment=s" => sub { $comment //= $_[1] }, - "proxy-host=s" => sub { $proxyIp //= $_[1] }, + "proxy-ip=s" => sub { $proxyIp //= $_[1] }, "proxy-port=i" => sub { $proxyPort //= $_[1] }, ); From 0f753c98ed66074d46bdb88cc91bba4f65a56078 Mon Sep 17 00:00:00 2001 From: jon4hz Date: Thu, 30 Oct 2025 01:14:13 +0100 Subject: [PATCH 11/49] chore: more tests --- tests/functional/tests.d/347-proxyjump.sh | 212 ++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 tests/functional/tests.d/347-proxyjump.sh diff --git a/tests/functional/tests.d/347-proxyjump.sh b/tests/functional/tests.d/347-proxyjump.sh new file mode 100644 index 0000000..3a07bdd --- /dev/null +++ b/tests/functional/tests.d/347-proxyjump.sh @@ -0,0 +1,212 @@ +# 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 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 --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 --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 --force + json .command selfAddPersonalAccess .error_code KO_INVALID_IP + + # 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 --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 --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 --force + json .command selfAddPersonalAccess .error_code ERR_INVALID_PARAMETER + + # + # 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 + 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 + 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 + json .command accountAddPersonalAccess .error_code ERR_MISSING_PARAMETER + + # + # 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 --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 --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 --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 --force + json .command groupAddServer .error_code KO_INVALID_IP + + # + # 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 --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 + contain '"ip":"192.168.1.200"' + contain '"port":"2222"' + contain '"user":"listtest"' + contain '"proxyIp":"10.0.0.5"' + contain '"proxyPort":"5555"' + + # 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 + 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 + 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 + contain '"ip":"192.168.2.100"' + contain '"port":"22"' + contain '"user":"testuser"' + contain '"proxyIp":"10.0.0.2"' + contain '"proxyPort":"22"' + + # 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 + 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 --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 + contain '"ip":"192.168.3.100"' + contain '"port":"22"' + contain '"user":"testuser"' + contain '"proxyIp":"10.0.0.3"' + contain '"proxyPort":"22"' + + # 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 + 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 From 115ca262f94e80a9a17041e0e9139b8a3d96f0e4 Mon Sep 17 00:00:00 2001 From: jon4hz Date: Thu, 30 Oct 2025 01:31:16 +0100 Subject: [PATCH 12/49] fix: use delete to clear env var --- bin/shell/osh.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/shell/osh.pl b/bin/shell/osh.pl index d7e2628..ecb01c9 100755 --- a/bin/shell/osh.pl +++ b/bin/shell/osh.pl @@ -239,7 +239,7 @@ my @saved_argv = @ARGV; # Check if this is a ProxyJump connection that should be executed directly if ($ENV{'OSH_PROXYJUMP_CONNECTION'}) { - $ENV{'OSH_PROXYJUMP_CONNECTION'} = 0; # make sure nothing else gets interpreted as proxyjump + delete $ENV{'OSH_PROXYJUMP_CONNECTION'}; # make sure nothing else gets interpreted as proxyjump osh_debug("Detected ProxyJump connection, executing command directly"); # Extract the command from the realOptions or ARGV From 59068ee70dba4eef70d4e4203448c4bc3ca9b831 Mon Sep 17 00:00:00 2001 From: jon4hz Date: Mon, 3 Nov 2025 19:55:28 +0100 Subject: [PATCH 13/49] fix: remove unnecessary scp parameter --- bin/plugin/open/scp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/plugin/open/scp b/bin/plugin/open/scp index b398023..0bbb6c1 100755 --- a/bin/plugin/open/scp +++ b/bin/plugin/open/scp @@ -177,7 +177,7 @@ fi # shellcheck disable=SC2086 [ "$BASTION_SCP_DEBUG" = 1 ] && set -x 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" + $BASTION_CMD -t $BASTION_SSH_EXTRA_ARGS -- --osh scp --host "$REMOTE_HOST" --port "$REMOTE_PORT" --user "$REMOTE_USER" --proxy-jump "$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 @@ -237,7 +237,7 @@ scpcmd=$(echo "$3" | sed -e 's/#/##/g;s/ /#/g') [ "$BASTION_SCP_DEBUG" = 1 ] && set -x EOF 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" + echo "exec $BASTION_CMD -T \$sshcmdline $BASTION_SSH_EXTRA_ARGS -- --user \"\$REMOTE_USER\" --port \"\$REMOTE_PORT\" --host \"\$REMOTE_HOST\" --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 From 2f03a82deafe72c68413f1d13fae281c7fc03a87 Mon Sep 17 00:00:00 2001 From: jon4hz Date: Mon, 3 Nov 2025 22:31:11 +0100 Subject: [PATCH 14/49] feat: add proxy-user parameter --- bin/helper/osh-accountModifyPersonalAccess | 7 +- bin/helper/osh-groupAddServer | 7 +- bin/plugin/group-aclkeeper/groupAddServer | 69 +++--- bin/plugin/group-aclkeeper/groupDelServer | 16 +- bin/plugin/open/scp | 19 +- .../restricted/accountAddPersonalAccess | 25 ++- .../restricted/accountDelPersonalAccess | 16 +- bin/plugin/restricted/selfAddPersonalAccess | 27 ++- bin/plugin/restricted/selfDelPersonalAccess | 16 +- bin/shell/osh.pl | 28 ++- lib/perl/OVH/Bastion.pm | 2 + lib/perl/OVH/Bastion/Plugin.pm | 32 --- lib/perl/OVH/Bastion/Plugin/ACL.pm | 30 ++- lib/perl/OVH/Bastion/allowdeny.inc | 113 ++++++++-- lib/perl/OVH/Bastion/allowkeeper.inc | 30 ++- tests/functional/tests.d/347-proxyjump.sh | 199 +++++++++++++++--- tests/unit/tests/is_access_granted_proxy.t | 185 +++++++++++----- 17 files changed, 629 insertions(+), 192 deletions(-) diff --git a/bin/helper/osh-accountModifyPersonalAccess b/bin/helper/osh-accountModifyPersonalAccess index 8b715e6..a438306 100755 --- a/bin/helper/osh-accountModifyPersonalAccess +++ b/bin/helper/osh-accountModifyPersonalAccess @@ -32,7 +32,7 @@ use OVH::Bastion::Helper; # Fetch command options my $fnret; my ($result, @optwarns); -my ($account, $ip, $user, $port, $action, $ttl, $forceKey, $forcePassword, $target, $comment, $proxyIp, $proxyPort); +my ($account, $ip, $user, $port, $action, $ttl, $forceKey, $forcePassword, $target, $comment, $proxyIp, $proxyPort, $proxyUser); eval { local $SIG{__WARN__} = sub { push @optwarns, shift }; $result = GetOptions( @@ -48,6 +48,7 @@ eval { "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 $@ } @@ -84,7 +85,7 @@ if (not grep { $action eq $_ } qw{ add del }) { #>CODE my $machine = - OVH::Bastion::machine_display(ip => $ip, port => $port, user => $user, proxyIp => $proxyIp, proxyPort => $proxyPort) + 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'; @@ -121,6 +122,7 @@ $fnret = OVH::Bastion::access_modify( 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'} . ')' : ''; @@ -136,6 +138,7 @@ if ($fnret->err eq 'OK') { 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 28a2afb..a7f5463 100755 --- a/bin/helper/osh-groupAddServer +++ b/bin/helper/osh-groupAddServer @@ -19,7 +19,7 @@ use OVH::Bastion::Helper; # Fetch command options my $fnret; my ($result, @optwarns); -my ($group, $user, $ip, $port, $action, $force, $forcePassword, $forceKey, $ttl, $comment, $proxyIp, $proxyPort); +my ($group, $user, $ip, $port, $action, $force, $forcePassword, $forceKey, $ttl, $comment, $proxyIp, $proxyPort, $proxyUser); eval { local $SIG{__WARN__} = sub { push @optwarns, shift }; $result = GetOptions( @@ -35,6 +35,7 @@ eval { "comment=s" => sub { $comment //= $_[1] }, "proxy-ip=s" => sub { $proxyIp //= $_[1] }, "proxy-port=i" => sub { $proxyPort //= $_[1] }, + "proxy-user=s" => sub { $proxyUser //= $_[1] }, ); }; @@ -89,7 +90,7 @@ $fnret or HEXIT($fnret); #>CODE my $machine = - OVH::Bastion::machine_display(ip => $ip, port => $port, user => $user, proxyIp => $proxyIp, proxyPort => $proxyPort) + 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 @@ -106,6 +107,7 @@ $fnret = OVH::Bastion::access_modify( 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'} . ')' : ''; @@ -123,6 +125,7 @@ if ($fnret->err eq 'OK') { 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 33706ec..95b7681 100755 --- a/bin/plugin/group-aclkeeper/groupAddServer +++ b/bin/plugin/group-aclkeeper/groupAddServer @@ -23,6 +23,7 @@ my $remainingOptions = OVH::Bastion::Plugin::begin( "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, @@ -35,30 +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. - --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) + --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:: @@ -97,6 +100,12 @@ if ($proxyHost) { } } +if ($proxyUser) { + $fnret = OVH::Bastion::is_valid_remote_user(user => $proxyUser, allowWildcards => 1); + $fnret or osh_exit $fnret; + $proxyUser = $fnret->value; +} + $fnret = OVH::Bastion::Plugin::ACL::check( user => $user, userAny => $userAny, @@ -108,13 +117,15 @@ $fnret = OVH::Bastion::Plugin::ACL::check( 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); @@ -161,7 +172,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 @@ -195,5 +209,6 @@ 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/groupDelServer b/bin/plugin/group-aclkeeper/groupDelServer index 7cfcb08..e9d8bff 100755 --- a/bin/plugin/group-aclkeeper/groupDelServer +++ b/bin/plugin/group-aclkeeper/groupDelServer @@ -19,6 +19,7 @@ my $remainingOptions = OVH::Bastion::Plugin::begin( "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, @@ -49,6 +50,8 @@ Usage: --osh SCRIPT_NAME --group GROUP --host HOST --user USER --port PORT [OPTI 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`` @@ -86,6 +89,12 @@ if ($proxyHost) { } } +if ($proxyUser) { + $fnret = OVH::Bastion::is_valid_remote_user(user => $proxyUser, allowWildcards => 1); + $fnret or osh_exit $fnret; + $proxyUser = $fnret->value; +} + $fnret = OVH::Bastion::Plugin::ACL::check( user => $user, userAny => $userAny, @@ -97,13 +106,15 @@ $fnret = OVH::Bastion::Plugin::ACL::check( 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); @@ -130,5 +141,6 @@ 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/open/scp b/bin/plugin/open/scp index 0bbb6c1..482c7e8 100755 --- a/bin/plugin/open/scp +++ b/bin/plugin/open/scp @@ -303,15 +303,17 @@ EOF my $fnret; my $proxyIp = undef; my $proxyPort = 22; +my $proxyUser = undef; # Parse proxyjump args if specified if ($proxyJump) { - if ($proxyJump =~ /^(\[?[a-zA-Z0-9._-]+\]?)(?::(\d+))?$/) { - $proxyIp = $1; - $proxyPort = $2 if $2; - osh_debug("parsed proxyjump: host=$proxyIp port=$proxyPort"); + 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 host[:port]"; + osh_exit 'ERR_INVALID_PARAMETER', "Invalid proxyjump specification '$proxyJump', should be [user@]host[:port]"; } $fnret = OVH::Bastion::get_ip(host => $proxyIp, allowSubnets => 0); @@ -328,6 +330,10 @@ if ($proxyJump) { } $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) { @@ -359,6 +365,7 @@ 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 +$proxyUser ||= $user; # same for proxy user $fnret = OVH::Bastion::Plugin::otherProtocol::has_protocol_access( account => $self, @@ -395,7 +402,7 @@ if ($proxyIp) { } push @proxyCommand, '-p', $proxyPort if $proxyPort && $proxyPort != 22; - push @proxyCommand, '-l', $user, '-W', '%h:%p', $proxyIp; + push @proxyCommand, '-l', $proxyUser, '-W', '%h:%p', $proxyIp; # Quote arguments that contain spaces and build the command string my $proxyCommandStr = join(' ', map { /\s/ ? "'$_'" : $_ } @proxyCommand); diff --git a/bin/plugin/restricted/accountAddPersonalAccess b/bin/plugin/restricted/accountAddPersonalAccess index fc724c1..6628a04 100755 --- a/bin/plugin/restricted/accountAddPersonalAccess +++ b/bin/plugin/restricted/accountAddPersonalAccess @@ -23,6 +23,7 @@ my $remainingOptions = OVH::Bastion::Plugin::begin( "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, @@ -58,6 +59,8 @@ Usage: --osh SCRIPT_NAME --account ACCOUNT --host HOST --user USER --port PORT [ --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``. @@ -115,6 +118,12 @@ if ($proxyHost) { } } +if ($proxyUser) { + $fnret = OVH::Bastion::is_valid_remote_user(user => $proxyUser, allowWildcards => 1); + $fnret or osh_exit $fnret; + $proxyUser = $fnret->value; +} + $fnret = OVH::Bastion::Plugin::ACL::check( user => $user, userAny => $userAny, @@ -126,13 +135,15 @@ $fnret = OVH::Bastion::Plugin::ACL::check( 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); @@ -178,6 +189,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) { @@ -201,6 +220,7 @@ $fnret = OVH::Bastion::access_modify( widestV4Prefix => ($pluginConfig ? $pluginConfig->{'widest_v4_prefix'} : undef), proxyIp => $proxyIp, proxyPort => $proxyPort, + proxyUser => $proxyUser, ); $fnret or osh_exit($fnret); @@ -221,5 +241,6 @@ 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/accountDelPersonalAccess b/bin/plugin/restricted/accountDelPersonalAccess index 697ea74..555a827 100755 --- a/bin/plugin/restricted/accountDelPersonalAccess +++ b/bin/plugin/restricted/accountDelPersonalAccess @@ -18,6 +18,7 @@ my $remainingOptions = OVH::Bastion::Plugin::begin( "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, @@ -48,6 +49,8 @@ Usage: --osh SCRIPT_NAME --account ACCOUNT --host HOST --user USER --port PORT [ 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 ); @@ -75,6 +78,12 @@ if ($proxyHost) { } } +if ($proxyUser) { + $fnret = OVH::Bastion::is_valid_remote_user(user => $proxyUser, allowWildcards => 1); + $fnret or osh_exit $fnret; + $proxyUser = $fnret->value; +} + $fnret = OVH::Bastion::Plugin::ACL::check( user => $user, userAny => $userAny, @@ -86,13 +95,15 @@ $fnret = OVH::Bastion::Plugin::ACL::check( 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(); @@ -113,5 +124,6 @@ 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/selfAddPersonalAccess b/bin/plugin/restricted/selfAddPersonalAccess index 0ad856c..52a9965 100755 --- a/bin/plugin/restricted/selfAddPersonalAccess +++ b/bin/plugin/restricted/selfAddPersonalAccess @@ -23,6 +23,7 @@ my $remainingOptions = OVH::Bastion::Plugin::begin( "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, @@ -58,6 +59,9 @@ Usage: --osh SCRIPT_NAME --host HOST --user USER --port PORT [OPTIONS] --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 ); @@ -111,6 +115,12 @@ if ($proxyHost) { } } +if ($proxyUser) { + $fnret = OVH::Bastion::is_valid_remote_user(user => $proxyUser, allowWildcards => 1); + $fnret or osh_exit $fnret; + $proxyUser = $fnret->value; +} + $fnret = OVH::Bastion::Plugin::ACL::check( user => $user, userAny => $userAny, @@ -122,13 +132,15 @@ $fnret = OVH::Bastion::Plugin::ACL::check( 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); @@ -165,6 +177,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) { @@ -187,6 +207,7 @@ $fnret = OVH::Bastion::access_modify( comment => $comment, proxyIp => $proxyIp, proxyPort => $proxyPort, + proxyUser => $proxyUser, widestV4Prefix => ($pluginConfig ? $pluginConfig->{'widest_v4_prefix'} : undef), ); $fnret or osh_exit($fnret); @@ -201,6 +222,7 @@ if (not $force) { forcePassword => $forcePassword, proxyIp => $proxyIp, proxyPort => $proxyPort, + proxyUser => $proxyUser, ); if ($fnret->is_ok and $fnret->err ne 'OK') { # we have something to say, say it @@ -229,5 +251,6 @@ 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/selfDelPersonalAccess b/bin/plugin/restricted/selfDelPersonalAccess index a2de6f7..c013ecc 100755 --- a/bin/plugin/restricted/selfDelPersonalAccess +++ b/bin/plugin/restricted/selfDelPersonalAccess @@ -23,6 +23,7 @@ my $remainingOptions = OVH::Bastion::Plugin::begin( "scpup" => \my $scpUp, "scpdown" => \my $scpDown, "sftp" => \my $sftp, + "proxy-user=s" => \my $proxyUser, }, helptext => <<'EOF', Remove a personal server access from your account @@ -46,6 +47,8 @@ Usage: --osh SCRIPT_NAME --host HOST --user USER --port PORT [OPTIONS] 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 ); @@ -73,6 +76,12 @@ if ($proxyHost) { } } +if ($proxyUser) { + $fnret = OVH::Bastion::is_valid_remote_user(user => $proxyUser, allowWildcards => 1); + $fnret or osh_exit $fnret; + $proxyUser = $fnret->value; +} + $fnret = OVH::Bastion::Plugin::ACL::check( user => $user, userAny => $userAny, @@ -84,13 +93,15 @@ $fnret = OVH::Bastion::Plugin::ACL::check( 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'; @@ -102,5 +113,6 @@ 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/shell/osh.pl b/bin/shell/osh.pl index ecb01c9..054939e 100755 --- a/bin/shell/osh.pl +++ b/bin/shell/osh.pl @@ -648,16 +648,18 @@ else { my $proxyIp = undef; my $proxyPort = 22; +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._-]+\]?)(?::(\d+))?$/) { - $proxyIp = $1; - $proxyPort = $2 if $2; - osh_debug("parsed proxyjump: host=$proxyIp port=$proxyPort"); + 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 { main_exit OVH::Bastion::EXIT_INVALID_PROXYJUMP, 'invalid_proxyjump', - "Invalid proxyjump specification '$proxyJump', should be host[:port]"; + "Invalid proxyjump specification '$proxyJump', should be [user@]host[:port]"; } $fnret = OVH::Bastion::get_ip(host => $proxyIp, allowSubnets => 0); @@ -675,8 +677,14 @@ if ($proxyJump) { 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_HOST'} = $proxyIp; $ENV{'OSH_PROXYJUMP_PORT'} = $proxyPort; + $ENV{'OSH_PROXYJUMP_USER'} = $proxyUser; $ENV{'OSH_PROXYJUMP_CONNECTION'} = 1; } @@ -1163,6 +1171,7 @@ if ($osh_command) { } # build ttyrec command that'll prefix the real command + # TODO: support proxy info $fnret = OVH::Bastion::build_ttyrec_cmdline( ip => $osh_command, port => 0, @@ -1238,6 +1247,9 @@ 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"); @@ -1250,7 +1262,8 @@ my $displayLine = sprintf( port => $port, user => $user, proxyIp => $proxyIp, - proxyPort => $proxyPort + proxyPort => $proxyPort, + proxyUser => $proxyUser, )->value, ); @@ -1273,6 +1286,7 @@ else { port => $port, proxyIp => $proxyIp, proxyPort => $proxyPort, + proxyUser => $proxyUser, details => 1 ); } @@ -1580,7 +1594,7 @@ else { } push @proxyCommand, '-p', $proxyPort if $proxyPort && $proxyPort != 22; - push @proxyCommand, '-l', $user, '-W', '%h:%p', $proxyIp; + push @proxyCommand, '-l', $proxyUser, '-W', '%h:%p', $proxyIp; if ($verbose) { foreach (1 .. $verbose) { diff --git a/lib/perl/OVH/Bastion.pm b/lib/perl/OVH/Bastion.pm index 8518c24..bfa1097 100644 --- a/lib/perl/OVH/Bastion.pm +++ b/lib/perl/OVH/Bastion.pm @@ -769,6 +769,7 @@ sub machine_display { 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; @@ -777,6 +778,7 @@ sub machine_display { if ($proxyIp) { my $proxy = (index($proxyIp, ':') >= 0 ? "[$proxyIp]" : $proxyIp); $proxy .= ":$proxyPort" if $proxyPort; + $proxy = $proxyUser . '@' . $proxy if $proxyUser; $machine = "$machine via $proxy"; } diff --git a/lib/perl/OVH/Bastion/Plugin.pm b/lib/perl/OVH/Bastion/Plugin.pm index c6b8ca9..b5819b4 100644 --- a/lib/perl/OVH/Bastion/Plugin.pm +++ b/lib/perl/OVH/Bastion/Plugin.pm @@ -95,38 +95,6 @@ sub validate_tuple { undef $host if $host eq ''; } - # handle proxy host resolution - # if (exists $params{'proxyHost'}) { - # $proxyHost = $params{'proxyHost'}; - # if ($proxyHost) { - # if ($proxyHost !~ m{^[a-zA-Z0-9._/:-]+$}) { - # # can be an IP (v4 or v6) or hostname - # osh_exit('KO_INVALID_PROXY_HOST', msg => "Proxy host name '$proxyHost' seems invalid"); - # } - # $fnret = OVH::Bastion::get_ip(host => $proxyHost); - # if (!$fnret) { - # osh_exit('KO_INVALID_PROXY_HOST', msg => "Proxy host name '$proxyHost' couldn't be resolved"); - # } - # else { - # $proxyIp = $fnret->value->{'ip'}; - # } - # } - # undef $proxyHost if $proxyHost eq ''; - # } - - # # handle proxy port validation - # if (exists $params{'proxyPort'}) { - # $proxyPort = $params{'proxyPort'}; - # if (defined $proxyPort && $proxyPort ne '') { - # $fnret = OVH::Bastion::is_valid_port(port => $proxyPort); - # $fnret or osh_exit $fnret; - # $proxyPort = $fnret->value; - # } - # else { - # undef $proxyPort; - # } - # } - return R('OK'); } diff --git a/lib/perl/OVH/Bastion/Plugin/ACL.pm b/lib/perl/OVH/Bastion/Plugin/ACL.pm index 5cbb72b..8241a4d 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, $proxyIp, $proxyPort) = - @params{qw{ port portAny user userAny scpUp scpDown sftp protocol proxyIp proxyPort }}; + 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', @@ -85,7 +85,7 @@ sub check { } # check proxy-host and proxy-port parameters - osh_debug("Checking proxy parameters: proxyIp='$proxyIp' proxyPort='$proxyPort'"); + 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"); @@ -95,6 +95,10 @@ sub check { if ($proxyIp !~ m{^[a-zA-Z0-9._/:-]+$}) { return R('ERR_INVALID_PARAMETER', msg => "Proxy host name '$proxyIp' seems invalid"); } + + if (!$proxyUser) { + return R('ERR_MISSING_PARAMETER', msg => "When --proxy-host is specified, --proxy-user becomes mandatory"); + } } if ($proxyPort) { @@ -110,11 +114,21 @@ sub check { } # now, remap port and user '*' back to undef - undef $user if $user eq '*'; - undef $port if $port eq '*'; - - return R('OK', - value => {user => $user, port => $port, protocol => $protocol, proxyIp => $proxyIp, proxyPort => $proxyPort}); + 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/allowdeny.inc b/lib/perl/OVH/Bastion/allowdeny.inc index 71b1aca..12295c9 100644 --- a/lib/perl/OVH/Bastion/allowdeny.inc +++ b/lib/perl/OVH/Bastion/allowdeny.inc @@ -86,6 +86,7 @@ sub is_access_way_granted { 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 @@ -96,8 +97,9 @@ 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); @@ -106,9 +108,6 @@ sub is_access_way_granted { my $check_debug_msg = "checking way $way/$account/$group with ignorePort=$ignorePort ignoreUser=$ignoreUser exactIpMatch=$exactIpMatch exactPortMatch=$exactPortMatch exactUserMatch=$exactUserMatch"; - if (defined $wantedProxyIp) { - $check_debug_msg .= " proxyIp=$wantedProxyIp proxyPort=$wantedProxyPort"; - } osh_debug($check_debug_msg); my %match; @@ -128,8 +127,17 @@ sub is_access_way_granted { . ($entry->{'port'} // ''); if (defined $entry->{'proxyIp'}) { + undef $entry->{'proxyUser'} if (defined $entry->{'proxyUser'} && $entry->{'proxyUser'} eq '*'); + $check_debug_msg .= - " proxyIp=" . ($entry->{'proxyIp'} // '') . " proxyPort=" . ($entry->{'proxyPort'} // ''); + "with wanted proxy " + . ($wantedProxyUser // '') . '@' + . ($wantedProxyIp // '') . ':' + . ($wantedProxyPort // '') + . " against " + . ($entry->{'proxyUser'} // '') . '@' + . ($entry->{'proxyIp'} // '') . ':' + . ($entry->{'proxyPort'} // ''); } osh_debug($check_debug_msg); @@ -222,15 +230,63 @@ sub is_access_way_granted { } } - # then, check IP - my $isIPv6 = (index($entry->{'ip'}, ':') != -1); - my $isNetblock = (index($entry->{'ip'}, '/') != -1); - # 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"); } + # check proxy user if we have a proxy ip + if (defined $wantedProxyIp) { + 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); + # if we want an exact match, it's a simple strcmp() for IPv4 if ($exactIpMatch && !$isIPv6) { next if ($entry->{'ip'} ne $wantedIp); @@ -705,8 +761,12 @@ sub print_acls { $addedDate = substr($addedDate, 0, 10); my $forceKey = $entry->{'forceKey'} || '-'; my $forcePassword = $entry->{'forcePassword'} || '-'; - my $via = ($entry->{'proxyIp'} ? $entry->{'proxyIp'} : '-') - . ($entry->{'proxyPort'} ? ":$entry->{'proxyPort'}" : ''); + + 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'}) : '-'; @@ -842,7 +902,7 @@ sub is_access_granted { 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 - # if proxyPort is defined, proxyIp must be defined 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 @@ -1078,7 +1138,8 @@ sub is_access_granted { port => $port, user => $user, proxyIp => $proxyIp, - proxyPort => $proxyPort + proxyPort => $proxyPort, + proxyUser => $proxyUser )->value; return R('KO_ACCESS_DENIED', msg => "Access denied for $account to $machine"); } @@ -1093,6 +1154,7 @@ sub ssh_test_access_way { my $user = $params{'user'}; my $proxyIp = $params{'proxyIp'}; my $proxyPort = $params{'proxyPort'}; + my $proxyUser = $params{'proxyUser'}; my $forceKey = $params{'forceKey'}; my $fnret; @@ -1122,6 +1184,11 @@ sub ssh_test_access_way { $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; @@ -1201,7 +1268,7 @@ sub ssh_test_access_way { push @proxyCommand, "-i", $_; } push @proxyCommand, "-p", $proxyPort if $proxyPort && $proxyPort != 22; - push @proxyCommand, '-l', $user, '-W', '%h:%p', $proxyIp; + push @proxyCommand, '-l', $proxyUser, '-W', '%h:%p', $proxyIp; my $proxyCommandStr = join(' ', map { /\s/ ? "'$_'" : $_ } @proxyCommand); osh_debug("Testing with ProxyCommand: $proxyCommandStr"); @@ -1210,6 +1277,7 @@ sub ssh_test_access_way { $ENV{'OSH_PROXYJUMP_HOST'} = $proxyIp; $ENV{'OSH_PROXYJUMP_PORT'} = $proxyPort; + $ENV{'OSH_PROXYJUMP_USER'} = $proxyUser; $ENV{'OSH_PROXYJUMP_CONNECTION'} = 1; } @@ -1481,8 +1549,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, $proxyIp, $proxyPort + $ip, $user, $port, $comment, $forceKey, + $forcePassword, $expiry, $addedBy, $addedDate, $extra, + $comment, $userComment, $proxyIp, $proxyPort, $proxyUser ); # extract comment if any @@ -1590,6 +1659,15 @@ sub _get_acl_from_file { $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; } @@ -1614,6 +1692,7 @@ sub _get_acl_from_file { 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 2c46e12..86f1656 100644 --- a/lib/perl/OVH/Bastion/allowkeeper.inc +++ b/lib/perl/OVH/Bastion/allowkeeper.inc @@ -318,6 +318,7 @@ sub access_modify { 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 @@ -347,8 +348,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') { @@ -448,18 +450,27 @@ 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"); @@ -550,6 +561,7 @@ sub access_modify { 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"); @@ -638,6 +650,9 @@ sub access_modify { if ($proxyPort) { $entry .= " # PROXYPORT=" . $proxyPort; } + if ($proxyUser) { + $entry .= " # PROXYUSER=" . $proxyUser; + } if ($comment) { $comment =~ s{[#<>\\"']}{_}g; $entry .= " # COMMENT=<" . $comment . ">"; @@ -664,7 +679,8 @@ sub access_modify { port => $port, user => $user, proxyIp => $proxyIp, - proxyPort => $proxyPort + proxyPort => $proxyPort, + proxyUser => $proxyUser, )->value; my $ttlmsg = $ttl ? (' (expires in ' . OVH::Bastion::duration2human(seconds => $ttl)->value->{'human'} . ')') : ''; @@ -695,7 +711,8 @@ sub access_modify { port => $port, user => $user, proxyIp => $proxyIp, - proxyPort => $proxyPort + proxyPort => $proxyPort, + proxyUser => $proxyUser, )->value; $returnmsg = "Access to $machine successfully removed"; } @@ -723,8 +740,9 @@ sub access_modify { ['force_key', $params{'forceKey'}], ['force_password', $params{'forcePassword'}], ['comment', $params{'comment'}], - ['proxy_ip', $proxyIp], - ['proxy_port', $proxyPort], + ['proxy_ip', $params{'proxyIp'}], + ['proxy_port', $params{'proxyPort'}], + ['proxy_user', $params{'proxyUser'}], ] ); return R('OK', msg => $returnmsg) if $returnmsg; diff --git a/tests/functional/tests.d/347-proxyjump.sh b/tests/functional/tests.d/347-proxyjump.sh index 3a07bdd..7fdaafe 100644 --- a/tests/functional/tests.d/347-proxyjump.sh +++ b/tests/functional/tests.d/347-proxyjump.sh @@ -23,15 +23,15 @@ testsuite_proxyjump() # # 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 --force + 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 --force + 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 --force + 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 KO_INVALID_IP # Test proxy-port without proxy-host @@ -39,27 +39,55 @@ testsuite_proxyjump() 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 --force + 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 --force + 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 --force + 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 + 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 + 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 @@ -67,19 +95,43 @@ testsuite_proxyjump() 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 + 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 --force + 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 --force + 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 @@ -87,60 +139,99 @@ testsuite_proxyjump() 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 --force + 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 --force + 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 KO_INVALID_IP + # 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 # @@ -148,7 +239,7 @@ testsuite_proxyjump() # # 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 --force + 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 @@ -161,11 +252,27 @@ testsuite_proxyjump() contain '"proxyPort":"5555"' # 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 + 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 + contain '"ip":"192.168.1.201"' + contain '"port":"2222"' + contain '"user":"listtest"' + contain '"proxyIp":"10.0.0.5"' + contain '"proxyPort":"5555"' + contain '"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 + 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 @@ -178,11 +285,29 @@ testsuite_proxyjump() contain '"proxyPort":"22"' # 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 + 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 + contain '"ip":"192.168.2.200"' + contain '"port":"22"' + contain '"user":"testuser"' + contain '"proxyIp":"10.0.0.2"' + contain '"proxyPort":"22"' + contain '"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 --force + 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 @@ -195,7 +320,25 @@ testsuite_proxyjump() contain '"proxyPort":"22"' # 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 + 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 + contain '"ip":"192.168.3.200"' + contain '"port":"22"' + contain '"user":"testuser"' + contain '"proxyIp":"10.0.0.3"' + contain '"proxyPort":"22"' + contain '"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 success a0_delete_group1 $a0 --osh groupDelete --group $group1 --no-confirm diff --git a/tests/unit/tests/is_access_granted_proxy.t b/tests/unit/tests/is_access_granted_proxy.t index 59775ce..4ce1506 100644 --- a/tests/unit/tests/is_access_granted_proxy.t +++ b/tests/unit/tests/is_access_granted_proxy.t @@ -23,13 +23,19 @@ OVH::Bastion::set_mock_data( "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", ], }, @@ -51,66 +57,126 @@ $want{"192.0.2.10"}{"22"}{"me"}{"10.0.0.1"}{"2222"} = 'KO_ACCESS_DENIED'; # p $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 - should only work with exact proxy match +# 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'; +$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 +# 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 (198.51.100.0/24 covers 198.51.100.0 - 198.51.100.255) +# 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 +# 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 - this tests a specific edge case -$want{"192.0.2.60"}{"22"}{"root"}{"10.0.0.5"}{"5555"} = 'OK'; # user wildcard should match any user with exact proxy +# 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 7: Negative cases - hosts not in ACL +# 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 8: Regular IPv6 access without proxy - should work as before +# 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 9: IPv6 access with specific proxy - should only work with exact proxy match +# 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 10: IPv6 different proxy configuration +# 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 11: IPv6 user wildcard with proxy -$want{"2001:db8::40"}{"22"}{"root"}{"2001:db8:cafe::4"}{"4444"} = 'OK'; # user wildcard should match any user with exact 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 12: IPv6 subnet access with proxy (2001:aaaa::/64 covers 2001:db8::0 - 2001:aaaa::ffff:ffff:ffff:ffff) +# 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 13: IPv6 negative cases - hosts not in ACL +# 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'; @@ -125,12 +191,19 @@ foreach my $ip ( 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 @@ -147,41 +220,53 @@ foreach my $ip ( ) { foreach my $proxyPort ($undef, "2222", "3333", "1234", "4444", "5555", "6666") { - # Skip combinations that don't make sense (proxy port without proxy IP) - next if (!defined $proxyIp || $proxyIp eq $undef) && (defined $proxyPort && $proxyPort ne $undef); - - my $expected = $want{$ip}{$port}{$user}{$proxyIp // $undef}{$proxyPort // $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) { - $params{proxyIp} = $proxyIp; - } - if (defined $proxyPort && $proxyPort ne $undef) { - $params{proxyPort} = $proxyPort; - } + 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) { + $params{proxyIp} = $proxyIp; + } + if (defined $proxyPort && $proxyPort ne $undef) { + $params{proxyPort} = $proxyPort; + } + if (defined $proxyUser && $proxyUser ne $undef) { + $params{proxyUser} = $proxyUser; + } - my $result = OVH::Bastion::is_access_granted(%params); + my $result = OVH::Bastion::is_access_granted(%params); - my $test_desc = sprintf( - "is_access_granted with %s@%s:%s proxy=%s:%s", - $user, $ip, $port, - $proxyIp // '', - $proxyPort // '' - ); + 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); + is($result->err, $expected, $test_desc); - # If access is granted, verify proxy information is returned - _verify_proxy_information($expected, $proxyIp, $proxyPort, $result, $test_desc); + # If access is granted, verify proxy information is returned + _verify_proxy_information($expected, $proxyIp, $proxyPort, $proxyUser, $result, $test_desc); + } } } } @@ -189,7 +274,7 @@ foreach my $ip ( } sub _verify_proxy_information { - my ($expected, $proxyIp, $proxyPort, $result, $test_desc) = @_; + my ($expected, $proxyIp, $proxyPort, $proxyUser, $result, $test_desc) = @_; # Early return if access is not granted or no proxy expected return if $expected ne 'OK'; @@ -208,6 +293,12 @@ sub _verify_proxy_information { 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"); From 0e4a51a77834cf1fcf5ee4c2191871c619388616 Mon Sep 17 00:00:00 2001 From: jon4hz Date: Tue, 4 Nov 2025 23:32:45 +0100 Subject: [PATCH 15/49] chore: remove unused env vars --- bin/shell/osh.pl | 3 --- lib/perl/OVH/Bastion/allowdeny.inc | 3 --- 2 files changed, 6 deletions(-) diff --git a/bin/shell/osh.pl b/bin/shell/osh.pl index 054939e..70c054e 100755 --- a/bin/shell/osh.pl +++ b/bin/shell/osh.pl @@ -682,9 +682,6 @@ if ($proxyJump) { main_exit OVH::Bastion::EXIT_INVALID_REMOTE_USER, 'invalid_proxy_user', "Proxy user name '$proxyUser' seems invalid"; } - $ENV{'OSH_PROXYJUMP_HOST'} = $proxyIp; - $ENV{'OSH_PROXYJUMP_PORT'} = $proxyPort; - $ENV{'OSH_PROXYJUMP_USER'} = $proxyUser; $ENV{'OSH_PROXYJUMP_CONNECTION'} = 1; } diff --git a/lib/perl/OVH/Bastion/allowdeny.inc b/lib/perl/OVH/Bastion/allowdeny.inc index 12295c9..7d96edf 100644 --- a/lib/perl/OVH/Bastion/allowdeny.inc +++ b/lib/perl/OVH/Bastion/allowdeny.inc @@ -1275,9 +1275,6 @@ sub ssh_test_access_way { push @command, '-o', "ProxyCommand=$proxyCommandStr"; - $ENV{'OSH_PROXYJUMP_HOST'} = $proxyIp; - $ENV{'OSH_PROXYJUMP_PORT'} = $proxyPort; - $ENV{'OSH_PROXYJUMP_USER'} = $proxyUser; $ENV{'OSH_PROXYJUMP_CONNECTION'} = 1; } From 2ebeead61cd532387367caa4eaa2551214c063ce Mon Sep 17 00:00:00 2001 From: jon4hz Date: Tue, 4 Nov 2025 23:37:56 +0100 Subject: [PATCH 16/49] fix: support proxyUser in protocol access --- bin/plugin/open/scp | 5 ++++- lib/perl/OVH/Bastion/Plugin/otherProtocol.pm | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/bin/plugin/open/scp b/bin/plugin/open/scp index 482c7e8..7eef844 100755 --- a/bin/plugin/open/scp +++ b/bin/plugin/open/scp @@ -365,7 +365,9 @@ 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 -$proxyUser ||= $user; # same for proxy user +if ($proxyIp) { + $proxyUser ||= $user; +} $fnret = OVH::Bastion::Plugin::otherProtocol::has_protocol_access( account => $self, @@ -375,6 +377,7 @@ $fnret = OVH::Bastion::Plugin::otherProtocol::has_protocol_access( protocol => $protocol, proxyIp => $proxyIp, proxyPort => $proxyPort, + proxyUser => $proxyUser, ); $fnret or osh_exit($fnret); diff --git a/lib/perl/OVH/Bastion/Plugin/otherProtocol.pm b/lib/perl/OVH/Bastion/Plugin/otherProtocol.pm index 50e67df..03ce660 100644 --- a/lib/perl/OVH/Bastion/Plugin/otherProtocol.pm +++ b/lib/perl/OVH/Bastion/Plugin/otherProtocol.pm @@ -23,6 +23,7 @@ sub has_protocol_access { 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) { @@ -34,7 +35,8 @@ sub has_protocol_access { port => $port, user => $user, proxyIp => $proxyIp, - proxyPort => $proxyPort + proxyPort => $proxyPort, + proxyUser => $proxyUser )->value; my %keys; @@ -47,6 +49,7 @@ sub has_protocol_access { port => $port, proxyIp => $proxyIp, proxyPort => $proxyPort, + proxyUser => $proxyUser, details => 1 ); @@ -72,6 +75,7 @@ sub has_protocol_access { port => $port, proxyIp => $proxyIp, proxyPort => $proxyPort, + proxyUser => $proxyUser, exactUserMatch => 1, details => 1 ); From 5d580a50917b25751b851668757e98be2b16dfda Mon Sep 17 00:00:00 2001 From: jon4hz Date: Wed, 5 Nov 2025 20:55:51 +0100 Subject: [PATCH 17/49] fix: correct proxy info if proxyUser is defined --- lib/perl/OVH/Bastion/allowdeny.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/perl/OVH/Bastion/allowdeny.inc b/lib/perl/OVH/Bastion/allowdeny.inc index 7d96edf..91cf619 100644 --- a/lib/perl/OVH/Bastion/allowdeny.inc +++ b/lib/perl/OVH/Bastion/allowdeny.inc @@ -764,7 +764,7 @@ sub print_acls { my $via = '-'; if ($entry->{'proxyIp'}) { - $via = $entry->{'proxyUser'} ? $entry->{'proxyUser'} : '*' . "\@$entry->{'proxyIp'}:$entry->{'proxyPort'}"; + $via = ($entry->{'proxyUser'} ? $entry->{'proxyUser'} : '*' ) . "\@$entry->{'proxyIp'}:$entry->{'proxyPort'}"; } my $expiry = From 6ebb4e4b72fc68929c6810fa506a062b65f75f10 Mon Sep 17 00:00:00 2001 From: jon4hz Date: Wed, 5 Nov 2025 20:57:23 +0100 Subject: [PATCH 18/49] chore: run perl tidy --- bin/helper/osh-accountModifyPersonalAccess | 16 +- bin/helper/osh-groupAddServer | 16 +- bin/plugin/group-aclkeeper/groupDelServer | 2 +- .../restricted/accountDelPersonalAccess | 2 +- bin/plugin/restricted/selfDelPersonalAccess | 12 +- bin/shell/osh.pl | 6 +- lib/perl/OVH/Bastion/allowdeny.inc | 11 +- tests/unit/tests/is_access_granted_proxy.t | 171 +++++++++--------- 8 files changed, 128 insertions(+), 108 deletions(-) diff --git a/bin/helper/osh-accountModifyPersonalAccess b/bin/helper/osh-accountModifyPersonalAccess index a438306..3e7478e 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, $proxyIp, $proxyPort, $proxyUser); +my ( + $account, $ip, $user, $port, $action, $ttl, $forceKey, + $forcePassword, $target, $comment, $proxyIp, $proxyPort, $proxyUser +); eval { local $SIG{__WARN__} = sub { push @optwarns, shift }; $result = GetOptions( @@ -84,9 +87,14 @@ if (not grep { $action eq $_ } qw{ add del }) { #CODE -my $machine = - OVH::Bastion::machine_display(ip => $ip, port => $port, user => $user, proxyIp => $proxyIp, proxyPort => $proxyPort, proxyUser => $proxyUser) - ->value; +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'; diff --git a/bin/helper/osh-groupAddServer b/bin/helper/osh-groupAddServer index a7f5463..fd10ec7 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, $proxyIp, $proxyPort, $proxyUser); +my ( + $group, $user, $ip, $port, $action, $force, $forcePassword, + $forceKey, $ttl, $comment, $proxyIp, $proxyPort, $proxyUser +); eval { local $SIG{__WARN__} = sub { push @optwarns, shift }; $result = GetOptions( @@ -89,9 +92,14 @@ $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, proxyIp => $proxyIp, proxyPort => $proxyPort, proxyUser => $proxyUser) - ->value; +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( diff --git a/bin/plugin/group-aclkeeper/groupDelServer b/bin/plugin/group-aclkeeper/groupDelServer index e9d8bff..4890b0d 100755 --- a/bin/plugin/group-aclkeeper/groupDelServer +++ b/bin/plugin/group-aclkeeper/groupDelServer @@ -19,7 +19,7 @@ my $remainingOptions = OVH::Bastion::Plugin::begin( "force" => \my $force, "proxy-host=s" => \my $proxyHost, "proxy-port=s" => \my $proxyPort, - "proxy-user=s" => \my $proxyUser, + "proxy-user=s" => \my $proxyUser, # undocumented/compatibility: "user-any" => \my $userAny, "port-any" => \my $portAny, diff --git a/bin/plugin/restricted/accountDelPersonalAccess b/bin/plugin/restricted/accountDelPersonalAccess index 555a827..78a9d51 100755 --- a/bin/plugin/restricted/accountDelPersonalAccess +++ b/bin/plugin/restricted/accountDelPersonalAccess @@ -18,7 +18,7 @@ my $remainingOptions = OVH::Bastion::Plugin::begin( "protocol=s" => \my $protocol, "proxy-host=s" => \my $proxyHost, "proxy-port=s" => \my $proxyPort, - "proxy-user=s" => \my $proxyUser, + "proxy-user=s" => \my $proxyUser, # undocumented/compatibility: "user-any" => \my $userAny, "port-any" => \my $portAny, diff --git a/bin/plugin/restricted/selfDelPersonalAccess b/bin/plugin/restricted/selfDelPersonalAccess index c013ecc..9f468e1 100755 --- a/bin/plugin/restricted/selfDelPersonalAccess +++ b/bin/plugin/restricted/selfDelPersonalAccess @@ -18,12 +18,12 @@ my $remainingOptions = OVH::Bastion::Plugin::begin( "proxy-host=s" => \my $proxyHost, "proxy-port=s" => \my $proxyPort, # undocumented/compatibility: - "user-any" => \my $userAny, - "port-any" => \my $portAny, - "scpup" => \my $scpUp, - "scpdown" => \my $scpDown, - "sftp" => \my $sftp, - "proxy-user=s" => \my $proxyUser, + "user-any" => \my $userAny, + "port-any" => \my $portAny, + "scpup" => \my $scpUp, + "scpdown" => \my $scpDown, + "sftp" => \my $sftp, + "proxy-user=s" => \my $proxyUser, }, helptext => <<'EOF', Remove a personal server access from your account diff --git a/bin/shell/osh.pl b/bin/shell/osh.pl index 70c054e..9332f9f 100755 --- a/bin/shell/osh.pl +++ b/bin/shell/osh.pl @@ -648,7 +648,7 @@ else { my $proxyIp = undef; my $proxyPort = 22; -my $proxyUser = $user; # user might be undef. We'll handle that later +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+))?$/) { @@ -677,9 +677,9 @@ if ($proxyJump) { 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"; + main_exit OVH::Bastion::EXIT_INVALID_REMOTE_USER, 'invalid_proxy_user', + "Proxy user name '$proxyUser' seems invalid"; } $ENV{'OSH_PROXYJUMP_CONNECTION'} = 1; diff --git a/lib/perl/OVH/Bastion/allowdeny.inc b/lib/perl/OVH/Bastion/allowdeny.inc index 91cf619..243f615 100644 --- a/lib/perl/OVH/Bastion/allowdeny.inc +++ b/lib/perl/OVH/Bastion/allowdeny.inc @@ -761,12 +761,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'}"; + $via = + ($entry->{'proxyUser'} ? $entry->{'proxyUser'} : '*') . "\@$entry->{'proxyIp'}:$entry->{'proxyPort'}"; } - + my $expiry = $entry->{'expiry'} ? (duration2human(seconds => ($entry->{'expiry'} - time()))->value->{'human'}) : '-'; @@ -1185,8 +1186,8 @@ sub ssh_test_access_way { $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); + $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; } diff --git a/tests/unit/tests/is_access_granted_proxy.t b/tests/unit/tests/is_access_granted_proxy.t index 4ce1506..7523b99 100644 --- a/tests/unit/tests/is_access_granted_proxy.t +++ b/tests/unit/tests/is_access_granted_proxy.t @@ -58,75 +58,75 @@ $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 +$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 +$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 +$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 +$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 +$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 +$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 +$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 +$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 +$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 +$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'; @@ -140,41 +140,41 @@ $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 +$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 +$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 +$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 +$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 +$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'; @@ -221,16 +221,19 @@ foreach my $ip ( { 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" + $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); + 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}; + my $expected = + $want{$ip}{$port}{$user}{$proxyIp // $undef}{$proxyPort // $undef}{$proxyUser // $undef}; next unless defined $expected; my %params = ( From 82663e503614d8c6f652486a6acc2835b37b4d45 Mon Sep 17 00:00:00 2001 From: jon4hz Date: Wed, 5 Nov 2025 21:14:00 +0100 Subject: [PATCH 19/49] revert: scp parameter is still necessary This reverts commit 59068ee70dba4eef70d4e4203448c4bc3ca9b831. --- bin/plugin/open/scp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/plugin/open/scp b/bin/plugin/open/scp index 7eef844..8df7bf5 100755 --- a/bin/plugin/open/scp +++ b/bin/plugin/open/scp @@ -177,7 +177,7 @@ fi # shellcheck disable=SC2086 [ "$BASTION_SCP_DEBUG" = 1 ] && set -x if [ -n "$PROXY_JUMP" ]; then - $BASTION_CMD -t $BASTION_SSH_EXTRA_ARGS -- --osh scp --host "$REMOTE_HOST" --port "$REMOTE_PORT" --user "$REMOTE_USER" --proxy-jump "$PROXY_JUMP" --generate-mfa-token | tee "$t" + $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 @@ -237,7 +237,7 @@ scpcmd=$(echo "$3" | sed -e 's/#/##/g;s/ /#/g') [ "$BASTION_SCP_DEBUG" = 1 ] && set -x EOF if [ -n "$PROXY_JUMP" ]; then - echo "exec $BASTION_CMD -T \$sshcmdline $BASTION_SSH_EXTRA_ARGS -- --user \"\$REMOTE_USER\" --port \"\$REMOTE_PORT\" --host \"\$REMOTE_HOST\" --osh scp --proxy-jump \"$PROXY_JUMP\" --scp-cmd \"\$scpcmd\" --mfa-token $token" >> "$t" + 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 From 213f28cc4cb7d45433af5e73f8662d02c8c1b2ad Mon Sep 17 00:00:00 2001 From: jon4hz Date: Wed, 5 Nov 2025 21:15:48 +0100 Subject: [PATCH 20/49] chore: improve log output --- lib/perl/OVH/Bastion/allowdeny.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/perl/OVH/Bastion/allowdeny.inc b/lib/perl/OVH/Bastion/allowdeny.inc index 243f615..9e9ebc7 100644 --- a/lib/perl/OVH/Bastion/allowdeny.inc +++ b/lib/perl/OVH/Bastion/allowdeny.inc @@ -130,7 +130,7 @@ sub is_access_way_granted { undef $entry->{'proxyUser'} if (defined $entry->{'proxyUser'} && $entry->{'proxyUser'} eq '*'); $check_debug_msg .= - "with wanted proxy " + " with wanted proxy " . ($wantedProxyUser // '') . '@' . ($wantedProxyIp // '') . ':' . ($wantedProxyPort // '') From f8a9a622733357cd03b9f58ac490a68dd6ad9cd0 Mon Sep 17 00:00:00 2001 From: jon4hz Date: Wed, 5 Nov 2025 21:47:04 +0100 Subject: [PATCH 21/49] feat: proxy support for guest accesses --- bin/helper/osh-accountAddGroupServer | 46 +++---- .../group-gatekeeper/groupAddGuestAccess | 111 +++++++++++------ .../group-gatekeeper/groupDelGuestAccess | 99 +++++++++++----- lib/perl/OVH/Bastion/Plugin/groupSetRole.pm | 104 ++++++++++------ tests/functional/tests.d/347-proxyjump.sh | 112 ++++++++++++++++++ 5 files changed, 345 insertions(+), 127 deletions(-) diff --git a/bin/helper/osh-accountAddGroupServer b/bin/helper/osh-accountAddGroupServer index c5b180b..a09b621 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 $@ } @@ -59,15 +62,18 @@ if (not grep { $action eq $_ } qw{ add del }) { #>CODE # 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/plugin/group-gatekeeper/groupAddGuestAccess b/bin/plugin/group-gatekeeper/groupAddGuestAccess index 800d725..661a714 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,49 @@ 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::is_valid_ip(ip => $proxyHost, allowSubnets => 0); + if ($fnret) { + $proxyIp = $fnret->value->{'ip'}; + } + else { + $fnret = OVH::Bastion::get_ip(host => $proxyHost); + if ($fnret) { + $proxyIp = $fnret->value->{'ip'}; + } + else { + $proxyIp = $proxyHost; + } + } +} + +if ($proxyUser) { + $fnret = OVH::Bastion::is_valid_remote_user(user => $proxyUser, allowWildcards => 1); + $fnret or osh_exit $fnret; + $proxyUser = $fnret->value; +} + $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 => $proxyHost, + 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 +142,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/groupDelGuestAccess b/bin/plugin/group-gatekeeper/groupDelGuestAccess index 2966faa..522baa5 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,49 @@ 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::is_valid_ip(ip => $proxyHost, allowSubnets => 0); + if ($fnret) { + $proxyIp = $fnret->value->{'ip'}; + } + else { + $fnret = OVH::Bastion::get_ip(host => $proxyHost); + if ($fnret) { + $proxyIp = $fnret->value->{'ip'}; + } + else { + $proxyIp = $proxyHost; + } + } +} + +if ($proxyUser) { + $fnret = OVH::Bastion::is_valid_remote_user(user => $proxyUser, allowWildcards => 1); + $fnret or osh_exit $fnret; + $proxyUser = $fnret->value; +} + $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 +128,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/lib/perl/OVH/Bastion/Plugin/groupSetRole.pm b/lib/perl/OVH/Bastion/Plugin/groupSetRole.pm index e27d0c3..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/tests/functional/tests.d/347-proxyjump.sh b/tests/functional/tests.d/347-proxyjump.sh index 7fdaafe..c028355 100644 --- a/tests/functional/tests.d/347-proxyjump.sh +++ b/tests/functional/tests.d/347-proxyjump.sh @@ -341,6 +341,118 @@ testsuite_proxyjump() 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_INVALID_PARAMETER + + # 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 From ee6e8f0217464dc352b313c09acb21e4c6f0f345 Mon Sep 17 00:00:00 2001 From: jon4hz Date: Wed, 5 Nov 2025 23:38:30 +0100 Subject: [PATCH 22/49] fix: include proxy info in ttyrec filename --- bin/shell/osh.pl | 11 +++-- lib/perl/OVH/Bastion.pm | 36 ++++++++++++--- lib/perl/OVH/Bastion/configuration.inc | 3 +- tests/unit/tests/base.t | 62 ++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 11 deletions(-) diff --git a/bin/shell/osh.pl b/bin/shell/osh.pl index 9332f9f..232dbbd 100755 --- a/bin/shell/osh.pl +++ b/bin/shell/osh.pl @@ -546,7 +546,6 @@ if ($interactive and not $ENV{'OSH_IN_INTERACTIVE_SESSION'}) { # hmm, we are under mosh, mosh needs something to exec, so let's # re-exec ourselves in interactive mode - # TODO: does this work with proxyjump? exec(@toExecute, $0, '-c', $realOptions); } @@ -1168,7 +1167,6 @@ if ($osh_command) { } # build ttyrec command that'll prefix the real command - # TODO: support proxy info $fnret = OVH::Bastion::build_ttyrec_cmdline( ip => $osh_command, port => 0, @@ -1181,6 +1179,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" @@ -1333,7 +1334,6 @@ if ($osh_debug) { } # build ttyrec command that'll prefix the real command -# TODO: support proxyjump properly here my $ttyrec_fnret = OVH::Bastion::build_ttyrec_cmdline_part1of2( ip => $ip, port => $port, @@ -1345,7 +1345,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; diff --git a/lib/perl/OVH/Bastion.pm b/lib/perl/OVH/Bastion.pm index bfa1097..5959565 100644 --- a/lib/perl/OVH/Bastion.pm +++ b/lib/perl/OVH/Bastion.pm @@ -1153,15 +1153,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)/) { @@ -1170,6 +1179,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{/}{_}; @@ -1180,7 +1197,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/configuration.inc b/lib/perl/OVH/Bastion/configuration.inc index 4696527..01daffd 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}, 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')"); From 743867cf7936f252544c33721d47fb57b4f72420 Mon Sep 17 00:00:00 2001 From: jon4hz Date: Thu, 6 Nov 2025 21:30:58 +0100 Subject: [PATCH 23/49] feat: log proxy info --- bin/plugin/open/selfListSessions | 42 ++++++++++++++------- bin/shell/osh.pl | 45 ++++++++++++++++------ lib/perl/OVH/Bastion/log.inc | 65 +++++++++++++++++++++++++++++--- 3 files changed, 122 insertions(+), 30 deletions(-) diff --git a/bin/plugin/open/selfListSessions b/bin/plugin/open/selfListSessions index 974095c..80945d3 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, + "proxyuser=s" => \my $proxyUser, + "proxyip=s" => \my $proxyIp, + "proxyport=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 + --proxyuser USER Only sessions that used proxy USER + --proxyip HOST Only sessions that used proxy IP + --proxyport 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/shell/osh.pl b/bin/shell/osh.pl index 232dbbd..80ee719 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); @@ -549,7 +553,6 @@ if ($interactive and not $ENV{'OSH_IN_INTERACTIVE_SESSION'}) { exec(@toExecute, $0, '-c', $realOptions); } - # TODO: log proxy info my $logret = OVH::Bastion::log_access_insert( account => $self, cmdtype => 'interactive', @@ -567,7 +570,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) { @@ -833,7 +840,6 @@ my %ingressRealm = ( my $pivEffectivePolicyEnabled = OVH::Bastion::is_effective_piv_account_policy_enabled(account => $self); # if we're coming from a realm, we're receiving a connection from another bastion, keep all the traces: -# TODO: do we need to do something here regarding proxyjump? my @previous_bastion_details; if ($realm && $ENV{'LC_BASTION_DETAILS'}) { my $decoded_details; @@ -912,11 +918,14 @@ 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) { $fnret = OVH::Bastion::is_admin(account => $self); - # TODO: log proxyjump info my $logret = OVH::Bastion::log_access_insert( account => $self, cmdtype => 'sshas', @@ -934,7 +943,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", @@ -1038,7 +1051,6 @@ if ($osh_command) { # Then test for rights $fnret = OVH::Bastion::can_account_execute_plugin(account => $self, plugin => $osh_command); - # TODO: log proxyjump info my $logret = OVH::Bastion::log_access_insert( account => $self, cmdtype => 'osh', @@ -1056,7 +1068,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) { @@ -1299,7 +1315,6 @@ if (!$fnret) { $message .= " (tried with remote user '$user')"; # "root is not the default login anymore" } - # TODO: log proxyjump info my $logret = OVH::Bastion::log_access_insert( account => $self, cmdtype => $telnet ? 'telnet' : 'ssh', @@ -1315,7 +1330,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); @@ -1731,7 +1750,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); 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; From e5521cbf1a7dcf2e156f1eada6cbe65979720f07 Mon Sep 17 00:00:00 2001 From: jon4hz Date: Thu, 6 Nov 2025 21:39:26 +0100 Subject: [PATCH 24/49] fix: only set proxyPort default value if proxyJump is defined --- bin/shell/osh.pl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/shell/osh.pl b/bin/shell/osh.pl index 80ee719..88c0e2c 100755 --- a/bin/shell/osh.pl +++ b/bin/shell/osh.pl @@ -653,14 +653,14 @@ else { } my $proxyIp = undef; -my $proxyPort = 22; +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 if $3; + $proxyPort = $3 ? $3 : 22; osh_debug("parsed proxyjump: host=$proxyIp port=$proxyPort user=$proxyUser"); } else { From e3647227acfeadcc216743288fa594a2aa84db4c Mon Sep 17 00:00:00 2001 From: jon4hz Date: Thu, 6 Nov 2025 21:47:37 +0100 Subject: [PATCH 25/49] feat: whoHasAccessTo can now check for proxy info --- bin/plugin/restricted/whoHasAccessTo | 34 +++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/bin/plugin/restricted/whoHasAccessTo b/bin/plugin/restricted/whoHasAccessTo index 5a96435..9efaab8 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,29 @@ if (!$ip) { osh_exit 'ERR_MISSING_PARAMETER', "Missing parameter host (or it didn't resolve correctly)"; } +my $proxyIp; +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); + if ($fnret) { + $proxyIp = $fnret->value->{'ip'}; + } + else { + $proxyIp = $proxyHost; + } + } +} + +if ($proxyUser) { + $fnret = OVH::Bastion::is_valid_remote_user(user => $proxyUser, allowWildcards => 1); + $fnret or osh_exit $fnret; + $proxyUser = $fnret->value; +} + $fnret = OVH::Bastion::get_account_list(); $fnret or osh_exit $fnret; @@ -72,6 +101,9 @@ sub process_account { 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 From dfb5b4a6dd00d43079afb866da801dc127e30c30 Mon Sep 17 00:00:00 2001 From: jon4hz Date: Thu, 6 Nov 2025 21:52:31 +0100 Subject: [PATCH 26/49] fix: add ignoreProxyUser to is_access_granted --- bin/plugin/restricted/whoHasAccessTo | 23 ++++++++++++----------- lib/perl/OVH/Bastion/allowdeny.inc | 9 +++++---- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/bin/plugin/restricted/whoHasAccessTo b/bin/plugin/restricted/whoHasAccessTo index 9efaab8..8820215 100755 --- a/bin/plugin/restricted/whoHasAccessTo +++ b/bin/plugin/restricted/whoHasAccessTo @@ -96,17 +96,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, - 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 + 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/lib/perl/OVH/Bastion/allowdeny.inc b/lib/perl/OVH/Bastion/allowdeny.inc index 9e9ebc7..0770f06 100644 --- a/lib/perl/OVH/Bastion/allowdeny.inc +++ b/lib/perl/OVH/Bastion/allowdeny.inc @@ -77,8 +77,9 @@ 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 @@ -107,7 +108,7 @@ sub is_access_way_granted { my @acl = @{$fnret->value || []}; my $check_debug_msg = - "checking way $way/$account/$group with ignorePort=$ignorePort ignoreUser=$ignoreUser exactIpMatch=$exactIpMatch exactPortMatch=$exactPortMatch exactUserMatch=$exactUserMatch"; + "checking way $way/$account/$group with ignorePort=$ignorePort ignoreUser=$ignoreUser ignoreProxyUser=$ignoreProxyUser exactIpMatch=$exactIpMatch exactPortMatch=$exactPortMatch exactUserMatch=$exactUserMatch"; osh_debug($check_debug_msg); my %match; @@ -236,7 +237,7 @@ sub is_access_way_granted { } # check proxy user if we have a proxy ip - if (defined $wantedProxyIp) { + if (defined $wantedProxyIp && not $ignoreProxyUser) { if ($exactUserMatch) { # we want an exact match if (not defined $entry->{'proxyUser'}) { From 42c2c8653397ea56198ca945ad9b9ac7377e95f0 Mon Sep 17 00:00:00 2001 From: jon4hz Date: Thu, 6 Nov 2025 22:24:35 +0100 Subject: [PATCH 27/49] chore: autocompletions --- .../group-aclkeeper/groupAddServer.json | 31 +++++++++-------- .../group-aclkeeper/groupDelServer.json | 24 ++++++------- .../group-gatekeeper/groupAddGuestAccess.json | 10 +++++- .../group-gatekeeper/groupDelGuestAccess.json | 8 ++++- .../restricted/accountAddPersonalAccess.json | 34 ++++++++----------- .../restricted/accountDelPersonalAccess.json | 20 +++++------ .../restricted/selfAddPersonalAccess.json | 34 ++++++++----------- .../restricted/selfDelPersonalAccess.json | 20 +++++------ bin/plugin/restricted/whoHasAccessTo.json | 11 +++--- 9 files changed, 99 insertions(+), 93 deletions(-) diff --git a/bin/plugin/group-aclkeeper/groupAddServer.json b/bin/plugin/group-aclkeeper/groupAddServer.json index 7f2195e..72c00e2 100644 --- a/bin/plugin/group-aclkeeper/groupAddServer.json +++ b/bin/plugin/group-aclkeeper/groupAddServer.json @@ -4,21 +4,22 @@ "groupAddServer +--group" , {"ac" : [""]}, "groupAddServer +--group +\\S+" , {"ac" : ["--host"]}, "groupAddServer +--group +\\S+ +--host" , {"pr" : ["", "", ""]}, - "groupAddServer +--group +\\S+ +--host +\\S+" , {"ac" : ["--port", "--port-any", "--proxy-host", "--proxy-port"]}, - "groupAddServer +--group +\\S+ +--host +\\S+ +--port" , {"pr" : [""]}, - "groupAddServer +--group +\\S+ +--host +\\S+ +--port(-any| +\\d+)" , {"ac" : ["--user", "--user-any", "--proxy-host", "--proxy-port"]}, - "groupAddServer +--group +\\S+ +--host +\\S+ +--proxy-host" , {"pr" : ["", ""]}, - "groupAddServer +--group +\\S+ +--host +\\S+ +--proxy-host +\\d+" , {"ac" : ["--port", "--port-any", "--user", "--user-any", "--proxy-port"]}, - "groupAddServer +--group +\\S+ +--host +\\S+ +--proxy-port" , {"pr" : [""]}, - "groupAddServer +--group +\\S+ +--host +\\S+ +--proxy-port +\\d+" , {"ac" : ["--port", "--port-any", "--user", "--user-any", "--proxy-host"]}, - "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", "--proxy-host", "--proxy-port"]}, - "groupAddServer +--group +\\S+ +--host +\\S+ +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\d+) +--(user(-any| +\\S+)|proxy-host +\\S+|proxy-port +\\d+)" , {"ac" : ["", "--force-password", "--force", "--proxy-host", "--proxy-port", "--port", "--port-any", "--user", "--user-any"]}, - "groupAddServer +--group +\\S+ +--host +\\S+ +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\d+|user(-any| +\\S+)) +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\d+|user(-any| +\\S+)) +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\d+|user(-any| +\\S+))" , {"ac" : ["", "--force-password", "--force", "--proxy-host", "--proxy-port", "--port", "--port-any", "--user", "--user-any"]}, - "groupAddServer +--group +\\S+ +--host +\\S+ +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\d+|user(-any| +\\S+)) +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\d+|user(-any| +\\S+)) +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\d+|user(-any| +\\S+)) +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\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.json b/bin/plugin/group-aclkeeper/groupDelServer.json index 07103f4..4cfba0a 100644 --- a/bin/plugin/group-aclkeeper/groupDelServer.json +++ b/bin/plugin/group-aclkeeper/groupDelServer.json @@ -4,18 +4,18 @@ "groupDelServer +--group" , {"ac" : [""]}, "groupDelServer +--group +\\S+" , {"ac" : ["--host"]}, "groupDelServer +--group +\\S+ +--host" , {"pr" : ["", "", ""]}, - "groupDelServer +--group +\\S+ +--host +\\S+" , {"ac" : ["--port", "--port-any", "--proxy-host", "--proxy-port"]}, - "groupDelServer +--group +\\S+ +--host +\\S+ +--port" , {"pr" : [""]}, - "groupDelServer +--group +\\S+ +--host +\\S+ +--port(-any| +\\d+)" , {"ac" : ["--user", "--user-any", "--proxy-host", "--proxy-port"]}, - "groupDelServer +--group +\\S+ +--host +\\S+ +--proxy-host" , {"pr" : ["", ""]}, - "groupDelServer +--group +\\S+ +--host +\\S+ +--proxy-host +\\d+" , {"ac" : ["--port", "--port-any", "--user", "--user-any", "--proxy-port"]}, - "groupDelServer +--group +\\S+ +--host +\\S+ +--proxy-port" , {"pr" : [""]}, - "groupDelServer +--group +\\S+ +--host +\\S+ +--proxy-port +\\d+" , {"ac" : ["--port", "--port-any", "--user", "--user-any", "--proxy-host"]}, - "groupDelServer +--group +\\S+ +--host +\\S+ +--port(-any| +\\d+) +--user" , {"pr" : [""]}, - "groupDelServer +--group +\\S+ +--host +\\S+ +--port(-any| +\\d+) +--user(-any| +\\S+)" , {"ac" : ["", "--force", "--proxy-host", "--proxy-port"]}, - "groupDelServer +--group +\\S+ +--host +\\S+ +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\d+) +--(user(-any| +\\S+)|proxy-host +\\S+|proxy-port +\\d+)" , {"ac" : ["", "--force", "--proxy-host", "--proxy-port", "--port", "--port-any", "--user", "--user-any"]}, - "groupDelServer +--group +\\S+ +--host +\\S+ +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\d+|user(-any| +\\S+)) +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\d+|user(-any| +\\S+)) +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\d+|user(-any| +\\S+))" , {"ac" : ["", "--force", "--proxy-host", "--proxy-port", "--port", "--port-any", "--user", "--user-any"]}, - "groupDelServer +--group +\\S+ +--host +\\S+ +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\d+|user(-any| +\\S+)) +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\d+|user(-any| +\\S+)) +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\d+|user(-any| +\\S+)) +--(port(-any| +\\d+)|proxy-host +\\S+|proxy-port +\\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.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.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/restricted/accountAddPersonalAccess.json b/bin/plugin/restricted/accountAddPersonalAccess.json index 83660b5..630c748 100644 --- a/bin/plugin/restricted/accountAddPersonalAccess.json +++ b/bin/plugin/restricted/accountAddPersonalAccess.json @@ -4,28 +4,22 @@ "accountAddPersonalAccess +--account" , {"ac" : [""]}, "accountAddPersonalAccess +--account +\\S+" , {"ac" : ["--host"]}, "accountAddPersonalAccess +--account +\\S+ +--host" , {"pr" : ["", "", ""]}, - "accountAddPersonalAccess +--account +\\S+ +--host +\\S+" , {"ac" : ["", "--user", "--port", "--proxy-host", "--proxy-port"]}, + "accountAddPersonalAccess +--account +\\S+ +--host +\\S+" , {"ac" : ["", "--user", "--port"]}, "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +.*--user" , {"pr" : [""]}, "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +.*--port" , {"pr" : [""]}, - "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +.*--proxy-host" , {"pr" : ["", ""]}, - "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +.*--proxy-port" , {"pr" : [""]}, - "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--user +\\S+" , {"ac" : ["", "--port", "--proxy-host", "--proxy-port"]}, - "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--port +\\S+" , {"ac" : ["", "--user", "--proxy-host", "--proxy-port"]}, - "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--proxy-host +\\S+" , {"ac" : ["", "--user", "--port", "--proxy-port"]}, - "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--proxy-port +\\d+" , {"ac" : ["", "--user", "--port", "--proxy-host"]}, - "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+" , {"ac" : ["--force-key","--force-password","--ttl","--proxy-host","--proxy-port",""]}, - "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+" , {"ac" : ["--force-key","--force-password","--ttl","--proxy-host","--proxy-port",""]}, - "accountAddPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\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+ +--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" : ["--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.json b/bin/plugin/restricted/accountDelPersonalAccess.json index c6f8075..68b3077 100644 --- a/bin/plugin/restricted/accountDelPersonalAccess.json +++ b/bin/plugin/restricted/accountDelPersonalAccess.json @@ -4,18 +4,18 @@ "accountDelPersonalAccess +--account" , {"ac" : [""]}, "accountDelPersonalAccess +--account +\\S+" , {"ac" : ["--host"]}, "accountDelPersonalAccess +--account +\\S+ +--host" , {"pr" : ["", "", ""]}, - "accountDelPersonalAccess +--account +\\S+ +--host +\\S+" , {"ac" : ["", "--user", "--port", "--proxy-host", "--proxy-port"]}, + "accountDelPersonalAccess +--account +\\S+ +--host +\\S+" , {"ac" : ["", "--user", "--port"]}, "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +.*--user" , {"pr" : [""]}, "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +.*--port" , {"pr" : [""]}, - "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +.*--proxy-host" , {"pr" : ["", ""]}, - "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +.*--proxy-port" , {"pr" : [""]}, - "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +--user +\\S+" , {"ac" : ["", "--port", "--proxy-host", "--proxy-port"]}, - "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +--port +\\S+" , {"ac" : ["", "--user", "--proxy-host", "--proxy-port"]}, - "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +--proxy-host +\\S+" , {"ac" : ["", "--user", "--port", "--proxy-port"]}, - "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +--proxy-port +\\d+" , {"ac" : ["", "--user", "--port", "--proxy-host"]}, - "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+" , {"ac" : ["", "--proxy-host", "--proxy-port"]}, - "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+" , {"ac" : ["", "--proxy-host", "--proxy-port", "--port", "--user"]}, - "accountDelPersonalAccess +--account +\\S+ +--host +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+" , {"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+" , {"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.json b/bin/plugin/restricted/selfAddPersonalAccess.json index 392cff2..a6052eb 100644 --- a/bin/plugin/restricted/selfAddPersonalAccess.json +++ b/bin/plugin/restricted/selfAddPersonalAccess.json @@ -2,28 +2,22 @@ "interactive": [ "selfAddPersonalAccess" , {"ac" : ["--host"]}, "selfAddPersonalAccess +--host" , {"pr" : ["", "", ""]}, - "selfAddPersonalAccess +--host +\\S+" , {"ac" : ["", "--user", "--port", "--proxy-host", "--proxy-port"]}, + "selfAddPersonalAccess +--host +\\S+" , {"ac" : ["", "--user", "--port"]}, "selfAddPersonalAccess +--host +\\S+ +.*--user" , {"pr" : [""]}, "selfAddPersonalAccess +--host +\\S+ +.*--port" , {"pr" : [""]}, - "selfAddPersonalAccess +--host +\\S+ +.*--proxy-host" , {"pr" : ["", ""]}, - "selfAddPersonalAccess +--host +\\S+ +.*--proxy-port" , {"pr" : [""]}, - "selfAddPersonalAccess +--host +\\S+ +--user +\\S+" , {"ac" : ["", "--port", "--proxy-host", "--proxy-port"]}, - "selfAddPersonalAccess +--host +\\S+ +--port +\\S+" , {"ac" : ["", "--user", "--proxy-host", "--proxy-port"]}, - "selfAddPersonalAccess +--host +\\S+ +--proxy-host +\\S+" , {"ac" : ["", "--user", "--port", "--proxy-port"]}, - "selfAddPersonalAccess +--host +\\S+ +--proxy-port +\\d+" , {"ac" : ["", "--user", "--port", "--proxy-host"]}, - "selfAddPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+" , {"ac" : ["--force-key","--force-password","--ttl","--proxy-host","--proxy-port",""]}, - "selfAddPersonalAccess +--host +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+" , {"ac" : ["--force-key","--force-password","--ttl","--proxy-host","--proxy-port",""]}, - "selfAddPersonalAccess +--host +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\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+ +--user +\\S+" , {"ac" : ["", "--port"]}, + "selfAddPersonalAccess +--host +\\S+ +--port +\\S+" , {"ac" : ["", "--user"]}, + "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.json b/bin/plugin/restricted/selfDelPersonalAccess.json index 085f9dc..bca492d 100644 --- a/bin/plugin/restricted/selfDelPersonalAccess.json +++ b/bin/plugin/restricted/selfDelPersonalAccess.json @@ -2,18 +2,18 @@ "interactive": [ "selfDelPersonalAccess" , {"ac" : ["--host"]}, "selfDelPersonalAccess +--host" , {"pr" : ["", "", ""]}, - "selfDelPersonalAccess +--host +\\S+" , {"ac" : ["", "--user", "--port", "--proxy-host", "--proxy-port"]}, + "selfDelPersonalAccess +--host +\\S+" , {"ac" : ["", "--user", "--port"]}, "selfDelPersonalAccess +--host +\\S+ +.*--user" , {"pr" : [""]}, "selfDelPersonalAccess +--host +\\S+ +.*--port" , {"pr" : [""]}, - "selfDelPersonalAccess +--host +\\S+ +.*--proxy-host" , {"pr" : ["", ""]}, - "selfDelPersonalAccess +--host +\\S+ +.*--proxy-port" , {"pr" : [""]}, - "selfDelPersonalAccess +--host +\\S+ +--user +\\S+" , {"ac" : ["", "--port", "--proxy-host", "--proxy-port"]}, - "selfDelPersonalAccess +--host +\\S+ +--port +\\S+" , {"ac" : ["", "--user", "--proxy-host", "--proxy-port"]}, - "selfDelPersonalAccess +--host +\\S+ +--proxy-host +\\S+" , {"ac" : ["", "--user", "--port", "--proxy-port"]}, - "selfDelPersonalAccess +--host +\\S+ +--proxy-port +\\d+" , {"ac" : ["", "--user", "--port", "--proxy-host"]}, - "selfDelPersonalAccess +--host +\\S+ +--(port|user) +\\S+ +--(port|user) +\\S+" , {"ac" : ["", "--proxy-host", "--proxy-port"]}, - "selfDelPersonalAccess +--host +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+" , {"ac" : ["", "--proxy-host", "--proxy-port", "--port", "--user"]}, - "selfDelPersonalAccess +--host +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+ +--(port|user|proxy-host|proxy-port) +\\S+" , {"pr" : [""]} + "selfDelPersonalAccess +--host +\\S+ +--user +\\S+" , {"ac" : ["", "--port"]}, + "selfDelPersonalAccess +--host +\\S+ +--port +\\S+" , {"ac" : ["", "--user"]}, + "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.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" : [""]} ] } From cebf8976791e704a552c96420843d6741c69fb09 Mon Sep 17 00:00:00 2001 From: jon4hz Date: Thu, 6 Nov 2025 22:51:14 +0100 Subject: [PATCH 28/49] refactor: function to validate proxy options --- bin/plugin/group-aclkeeper/groupAddServer | 23 +++------- bin/plugin/group-aclkeeper/groupDelServer | 23 +++------- .../group-gatekeeper/groupAddGuestAccess | 23 +++------- .../group-gatekeeper/groupDelGuestAccess | 23 +++------- .../restricted/accountAddPersonalAccess | 23 +++------- .../restricted/accountDelPersonalAccess | 23 +++------- bin/plugin/restricted/selfAddPersonalAccess | 23 +++------- bin/plugin/restricted/selfDelPersonalAccess | 23 +++------- bin/plugin/restricted/whoHasAccessTo | 23 +++------- lib/perl/OVH/Bastion.pm | 43 +++++++++++++++++++ 10 files changed, 88 insertions(+), 162 deletions(-) diff --git a/bin/plugin/group-aclkeeper/groupAddServer b/bin/plugin/group-aclkeeper/groupAddServer index 95b7681..c25b86a 100755 --- a/bin/plugin/group-aclkeeper/groupAddServer +++ b/bin/plugin/group-aclkeeper/groupAddServer @@ -85,25 +85,12 @@ if (not $group or not $ip) { my $proxyIp; 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); - if ($fnret) { - $proxyIp = $fnret->value->{'ip'}; - } - else { - $proxyIp = $proxyHost; - } - } -} - -if ($proxyUser) { - $fnret = OVH::Bastion::is_valid_remote_user(user => $proxyUser, allowWildcards => 1); + $fnret = + OVH::Bastion::validate_proxy_params(proxyHost => $proxyHost, proxyPort => $proxyPort, proxyUser => $proxyUser); $fnret or osh_exit $fnret; - $proxyUser = $fnret->value; + $proxyIp = $fnret->value->{'proxyIp'}; + $proxyPort = $fnret->value->{'proxyPort'}; + $proxyUser = $fnret->value->{'proxyUser'}; } $fnret = OVH::Bastion::Plugin::ACL::check( diff --git a/bin/plugin/group-aclkeeper/groupDelServer b/bin/plugin/group-aclkeeper/groupDelServer index 4890b0d..43ded19 100755 --- a/bin/plugin/group-aclkeeper/groupDelServer +++ b/bin/plugin/group-aclkeeper/groupDelServer @@ -74,25 +74,12 @@ if (not $group or not $ip) { my $proxyIp; 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); - if ($fnret) { - $proxyIp = $fnret->value->{'ip'}; - } - else { - $proxyIp = $proxyHost; - } - } -} - -if ($proxyUser) { - $fnret = OVH::Bastion::is_valid_remote_user(user => $proxyUser, allowWildcards => 1); + $fnret = + OVH::Bastion::validate_proxy_params(proxyHost => $proxyHost, proxyPort => $proxyPort, proxyUser => $proxyUser); $fnret or osh_exit $fnret; - $proxyUser = $fnret->value; + $proxyIp = $fnret->value->{'proxyIp'}; + $proxyPort = $fnret->value->{'proxyPort'}; + $proxyUser = $fnret->value->{'proxyUser'}; } $fnret = OVH::Bastion::Plugin::ACL::check( diff --git a/bin/plugin/group-gatekeeper/groupAddGuestAccess b/bin/plugin/group-gatekeeper/groupAddGuestAccess index 661a714..e56c7b4 100755 --- a/bin/plugin/group-gatekeeper/groupAddGuestAccess +++ b/bin/plugin/group-gatekeeper/groupAddGuestAccess @@ -85,25 +85,12 @@ if (not $ip and $host) { my $proxyIp; 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); - if ($fnret) { - $proxyIp = $fnret->value->{'ip'}; - } - else { - $proxyIp = $proxyHost; - } - } -} - -if ($proxyUser) { - $fnret = OVH::Bastion::is_valid_remote_user(user => $proxyUser, allowWildcards => 1); + $fnret = + OVH::Bastion::validate_proxy_params(proxyHost => $proxyHost, proxyPort => $proxyPort, proxyUser => $proxyUser); $fnret or osh_exit $fnret; - $proxyUser = $fnret->value; + $proxyIp = $fnret->value->{'proxyIp'}; + $proxyPort = $fnret->value->{'proxyPort'}; + $proxyUser = $fnret->value->{'proxyUser'}; } $fnret = OVH::Bastion::Plugin::ACL::check( diff --git a/bin/plugin/group-gatekeeper/groupDelGuestAccess b/bin/plugin/group-gatekeeper/groupDelGuestAccess index 522baa5..9320397 100755 --- a/bin/plugin/group-gatekeeper/groupDelGuestAccess +++ b/bin/plugin/group-gatekeeper/groupDelGuestAccess @@ -78,25 +78,12 @@ if (not $ip and $host) { my $proxyIp; 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); - if ($fnret) { - $proxyIp = $fnret->value->{'ip'}; - } - else { - $proxyIp = $proxyHost; - } - } -} - -if ($proxyUser) { - $fnret = OVH::Bastion::is_valid_remote_user(user => $proxyUser, allowWildcards => 1); + $fnret = + OVH::Bastion::validate_proxy_params(proxyHost => $proxyHost, proxyPort => $proxyPort, proxyUser => $proxyUser); $fnret or osh_exit $fnret; - $proxyUser = $fnret->value; + $proxyIp = $fnret->value->{'proxyIp'}; + $proxyPort = $fnret->value->{'proxyPort'}; + $proxyUser = $fnret->value->{'proxyUser'}; } $fnret = OVH::Bastion::Plugin::ACL::check( diff --git a/bin/plugin/restricted/accountAddPersonalAccess b/bin/plugin/restricted/accountAddPersonalAccess index 6628a04..21760dd 100755 --- a/bin/plugin/restricted/accountAddPersonalAccess +++ b/bin/plugin/restricted/accountAddPersonalAccess @@ -103,25 +103,12 @@ if (!$ip) { my $proxyIp; 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); - if ($fnret) { - $proxyIp = $fnret->value->{'ip'}; - } - else { - $proxyIp = $proxyHost; - } - } -} - -if ($proxyUser) { - $fnret = OVH::Bastion::is_valid_remote_user(user => $proxyUser, allowWildcards => 1); + $fnret = + OVH::Bastion::validate_proxy_params(proxyHost => $proxyHost, proxyPort => $proxyPort, proxyUser => $proxyUser); $fnret or osh_exit $fnret; - $proxyUser = $fnret->value; + $proxyIp = $fnret->value->{'proxyIp'}; + $proxyPort = $fnret->value->{'proxyPort'}; + $proxyUser = $fnret->value->{'proxyUser'}; } $fnret = OVH::Bastion::Plugin::ACL::check( diff --git a/bin/plugin/restricted/accountDelPersonalAccess b/bin/plugin/restricted/accountDelPersonalAccess index 78a9d51..c6cdb99 100755 --- a/bin/plugin/restricted/accountDelPersonalAccess +++ b/bin/plugin/restricted/accountDelPersonalAccess @@ -63,25 +63,12 @@ if (!$ip) { my $proxyIp; 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); - if ($fnret) { - $proxyIp = $fnret->value->{'ip'}; - } - else { - $proxyIp = $proxyHost; - } - } -} - -if ($proxyUser) { - $fnret = OVH::Bastion::is_valid_remote_user(user => $proxyUser, allowWildcards => 1); + $fnret = + OVH::Bastion::validate_proxy_params(proxyHost => $proxyHost, proxyPort => $proxyPort, proxyUser => $proxyUser); $fnret or osh_exit $fnret; - $proxyUser = $fnret->value; + $proxyIp = $fnret->value->{'proxyIp'}; + $proxyPort = $fnret->value->{'proxyPort'}; + $proxyUser = $fnret->value->{'proxyUser'}; } $fnret = OVH::Bastion::Plugin::ACL::check( diff --git a/bin/plugin/restricted/selfAddPersonalAccess b/bin/plugin/restricted/selfAddPersonalAccess index 52a9965..252109e 100755 --- a/bin/plugin/restricted/selfAddPersonalAccess +++ b/bin/plugin/restricted/selfAddPersonalAccess @@ -100,25 +100,12 @@ if (!$ip) { my $proxyIp; 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); - if ($fnret) { - $proxyIp = $fnret->value->{'ip'}; - } - else { - $proxyIp = $proxyHost; - } - } -} - -if ($proxyUser) { - $fnret = OVH::Bastion::is_valid_remote_user(user => $proxyUser, allowWildcards => 1); + $fnret = + OVH::Bastion::validate_proxy_params(proxyHost => $proxyHost, proxyPort => $proxyPort, proxyUser => $proxyUser); $fnret or osh_exit $fnret; - $proxyUser = $fnret->value; + $proxyIp = $fnret->value->{'proxyIp'}; + $proxyPort = $fnret->value->{'proxyPort'}; + $proxyUser = $fnret->value->{'proxyUser'}; } $fnret = OVH::Bastion::Plugin::ACL::check( diff --git a/bin/plugin/restricted/selfDelPersonalAccess b/bin/plugin/restricted/selfDelPersonalAccess index 9f468e1..d48bd83 100755 --- a/bin/plugin/restricted/selfDelPersonalAccess +++ b/bin/plugin/restricted/selfDelPersonalAccess @@ -61,25 +61,12 @@ if (!$ip) { my $proxyIp; 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); - if ($fnret) { - $proxyIp = $fnret->value->{'ip'}; - } - else { - $proxyIp = $proxyHost; - } - } -} - -if ($proxyUser) { - $fnret = OVH::Bastion::is_valid_remote_user(user => $proxyUser, allowWildcards => 1); + $fnret = + OVH::Bastion::validate_proxy_params(proxyHost => $proxyHost, proxyPort => $proxyPort, proxyUser => $proxyUser); $fnret or osh_exit $fnret; - $proxyUser = $fnret->value; + $proxyIp = $fnret->value->{'proxyIp'}; + $proxyPort = $fnret->value->{'proxyPort'}; + $proxyUser = $fnret->value->{'proxyUser'}; } $fnret = OVH::Bastion::Plugin::ACL::check( diff --git a/bin/plugin/restricted/whoHasAccessTo b/bin/plugin/restricted/whoHasAccessTo index 8820215..11a5714 100755 --- a/bin/plugin/restricted/whoHasAccessTo +++ b/bin/plugin/restricted/whoHasAccessTo @@ -62,25 +62,12 @@ if (!$ip) { my $proxyIp; 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); - if ($fnret) { - $proxyIp = $fnret->value->{'ip'}; - } - else { - $proxyIp = $proxyHost; - } - } -} - -if ($proxyUser) { - $fnret = OVH::Bastion::is_valid_remote_user(user => $proxyUser, allowWildcards => 1); + $fnret = + OVH::Bastion::validate_proxy_params(proxyHost => $proxyHost, proxyPort => $proxyPort, proxyUser => $proxyUser); $fnret or osh_exit $fnret; - $proxyUser = $fnret->value; + $proxyIp = $fnret->value->{'proxyIp'}; + $proxyPort = $fnret->value->{'proxyPort'}; + $proxyUser = $fnret->value->{'proxyUser'}; } $fnret = OVH::Bastion::get_account_list(); diff --git a/lib/perl/OVH/Bastion.pm b/lib/perl/OVH/Bastion.pm index 5959565..9846bc4 100644 --- a/lib/perl/OVH/Bastion.pm +++ b/lib/perl/OVH/Bastion.pm @@ -762,6 +762,49 @@ 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); + if ($fnret) { + $proxyIp = $fnret->value->{'ip'}; + } + else { + $proxyIp = $proxyHost; + } + } + } + + if ($proxyPort) { + $fnret = OVH::Bastion::is_valid_port(port => $proxyPort); + if (!$fnret) { + return R('ERR_INVALID_PARAMETER', msg => "Proxy port '$proxyPort' is invalid: " . $fnret->msg); + } + $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'}; From dbbd0e43fe1cd980e59b3a29efa0ca91b2b88fc2 Mon Sep 17 00:00:00 2001 From: jon4hz Date: Thu, 13 Nov 2025 19:37:00 +0100 Subject: [PATCH 29/49] fix: handle proxy options correctly when deleting accesses --- lib/perl/OVH/Bastion/allowkeeper.inc | 32 +++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/lib/perl/OVH/Bastion/allowkeeper.inc b/lib/perl/OVH/Bastion/allowkeeper.inc index 86f1656..ba17c3e 100644 --- a/lib/perl/OVH/Bastion/allowkeeper.inc +++ b/lib/perl/OVH/Bastion/allowkeeper.inc @@ -692,9 +692,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; } From b45c8560871fbad71429a04a1598c44f785a3dba Mon Sep 17 00:00:00 2001 From: jon4hz Date: Thu, 4 Dec 2025 11:23:19 +0100 Subject: [PATCH 30/49] chore: fix perlcritic warnings --- lib/perl/OVH/Bastion/allowdeny.inc | 2 +- tests/unit/tests/is_access_granted_proxy.t | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/perl/OVH/Bastion/allowdeny.inc b/lib/perl/OVH/Bastion/allowdeny.inc index 0770f06..7b1abbe 100644 --- a/lib/perl/OVH/Bastion/allowdeny.inc +++ b/lib/perl/OVH/Bastion/allowdeny.inc @@ -237,7 +237,7 @@ sub is_access_way_granted { } # check proxy user if we have a proxy ip - if (defined $wantedProxyIp && not $ignoreProxyUser) { + if (defined $wantedProxyIp && !$ignoreProxyUser) { if ($exactUserMatch) { # we want an exact match if (not defined $entry->{'proxyUser'}) { diff --git a/tests/unit/tests/is_access_granted_proxy.t b/tests/unit/tests/is_access_granted_proxy.t index 7523b99..ab36461 100644 --- a/tests/unit/tests/is_access_granted_proxy.t +++ b/tests/unit/tests/is_access_granted_proxy.t @@ -245,13 +245,13 @@ foreach my $ip ( ); # Add proxy parameters if they are defined - if (defined $proxyIp && $proxyIp ne $undef) { + if (defined $proxyIp && $proxyIp ne $undef) { ## no critic (ProhibitDeepNests) $params{proxyIp} = $proxyIp; } - if (defined $proxyPort && $proxyPort ne $undef) { + if (defined $proxyPort && $proxyPort ne $undef) { ## no critic (ProhibitDeepNests) $params{proxyPort} = $proxyPort; } - if (defined $proxyUser && $proxyUser ne $undef) { + if (defined $proxyUser && $proxyUser ne $undef) { ## no critic (ProhibitDeepNests) $params{proxyUser} = $proxyUser; } @@ -276,7 +276,7 @@ foreach my $ip ( } } -sub _verify_proxy_information { +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 From 9be9f67c2058307b1bdcccc35c724d28388aeefc Mon Sep 17 00:00:00 2001 From: jon4hz Date: Thu, 22 Jan 2026 10:37:40 +0100 Subject: [PATCH 31/49] fix: move proxy parsing to mosh and improve exit codes --- bin/shell/osh.pl | 76 ++++++++++++++++++++---------------------------- 1 file changed, 32 insertions(+), 44 deletions(-) diff --git a/bin/shell/osh.pl b/bin/shell/osh.pl index 88c0e2c..27f947e 100755 --- a/bin/shell/osh.pl +++ b/bin/shell/osh.pl @@ -241,49 +241,6 @@ else { # my @saved_argv = @ARGV; -# 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 - osh_debug("Detected ProxyJump connection, executing command directly"); - - # Extract the command from the realOptions or ARGV - my $proxy_command; - if (@ARGV && $ARGV[0] eq '-c' && $ARGV[1]) { - $proxy_command = $ARGV[1]; - } - else { - $proxy_command = join(' ', @ARGV); - } - - osh_debug("ProxyJump command: $proxy_command"); - - # Execute the proxy command directly without further validation - if ($proxy_command) { - # Parse the command to extract program and arguments - my @cmd_parts = split(/\s+/, $proxy_command); - if (!@cmd_parts) { - main_exit(OVH::Bastion::EXIT_EXEC_FAILED, "exec_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, "exec_failed", "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, "exec_failed", "Failed to execute proxy command: $!"); - } - else { - main_exit(OVH::Bastion::EXIT_EXEC_FAILED, "exec_failed", "No proxy command provided"); - } -} - # these options are the ones on shell definition of user calling osh.pl, # the user-passed commands are stringified after "-c" (as in sh -c) # it's possible to define the shell as osh.pl --debug, to force debug @@ -315,9 +272,40 @@ 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 + 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 (.+?) '--' (.*)/) { From 30d7be598725d031a50cd8c298a7dd261d3b1bae Mon Sep 17 00:00:00 2001 From: Jonah Date: Thu, 22 Jan 2026 10:39:01 +0100 Subject: [PATCH 32/49] fix: remove unneccessary osh_command check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Stéphane Lesimple --- bin/shell/osh.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/shell/osh.pl b/bin/shell/osh.pl index 27f947e..df64a7d 100755 --- a/bin/shell/osh.pl +++ b/bin/shell/osh.pl @@ -657,7 +657,7 @@ if ($proxyJump) { } $fnret = OVH::Bastion::get_ip(host => $proxyIp, allowSubnets => 0); - if (!$fnret && (($osh_command && $host) || !$osh_command)) { + if (!$fnret) { if ($fnret->err eq 'ERR_DNS_DISABLED') { main_exit OVH::Bastion::EXIT_DNS_DISABLED, 'dns_disabled', $fnret->msg; } From 08d440717e1c2a018543dee191969ea3bde1b440 Mon Sep 17 00:00:00 2001 From: Jonah Date: Thu, 22 Jan 2026 10:40:12 +0100 Subject: [PATCH 33/49] fix: use quotemeta to join command parts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Stéphane Lesimple --- bin/shell/osh.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/shell/osh.pl b/bin/shell/osh.pl index df64a7d..28cd6cc 100755 --- a/bin/shell/osh.pl +++ b/bin/shell/osh.pl @@ -1611,7 +1611,7 @@ else { push @proxyCommand, '-o', "ConnectTimeout=$timeout" if $timeout; # Quote arguments that contain spaces and build the command string - my $proxyCommandStr = join(' ', map { /\s/ ? "'$_'" : $_ } @proxyCommand); + my $proxyCommandStr = join(' ', map { quotemeta } @proxyCommand); push @command, '-o', "ProxyCommand=$proxyCommandStr"; osh_debug("ProxyCommand: $proxyCommandStr"); From 053aad1922ecc6705f737c6ff9e7ed8e1d2233d7 Mon Sep 17 00:00:00 2001 From: Jonah Date: Thu, 22 Jan 2026 10:43:58 +0100 Subject: [PATCH 34/49] fix: proxyIP can't be a subnet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Stéphane Lesimple --- lib/perl/OVH/Bastion/Plugin/ACL.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/perl/OVH/Bastion/Plugin/ACL.pm b/lib/perl/OVH/Bastion/Plugin/ACL.pm index 8241a4d..2d9fc94 100644 --- a/lib/perl/OVH/Bastion/Plugin/ACL.pm +++ b/lib/perl/OVH/Bastion/Plugin/ACL.pm @@ -92,7 +92,7 @@ sub check { } # validate proxy host format (same as regular host validation) - if ($proxyIp !~ m{^[a-zA-Z0-9._/:-]+$}) { + if ($proxyIp !~ m{^[a-zA-Z0-9._:-]+$}) { return R('ERR_INVALID_PARAMETER', msg => "Proxy host name '$proxyIp' seems invalid"); } From 8f87388c7d5f8308c57a0c2c7a312be361df27d6 Mon Sep 17 00:00:00 2001 From: jon4hz Date: Thu, 22 Jan 2026 10:48:25 +0100 Subject: [PATCH 35/49] fix: only check for valid ip in acl check --- lib/perl/OVH/Bastion/Plugin/ACL.pm | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/perl/OVH/Bastion/Plugin/ACL.pm b/lib/perl/OVH/Bastion/Plugin/ACL.pm index 2d9fc94..d89ff96 100644 --- a/lib/perl/OVH/Bastion/Plugin/ACL.pm +++ b/lib/perl/OVH/Bastion/Plugin/ACL.pm @@ -91,9 +91,10 @@ sub check { return R('ERR_MISSING_PARAMETER', msg => "When --proxy-host is specified, --proxy-port becomes mandatory"); } - # validate proxy host format (same as regular host validation) - if ($proxyIp !~ m{^[a-zA-Z0-9._:-]+$}) { - return R('ERR_INVALID_PARAMETER', msg => "Proxy host name '$proxyIp' seems invalid"); + # 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) { From 84e6fbf7bd17217c2847c5f7e4e477f2a68291ca Mon Sep 17 00:00:00 2001 From: jon4hz Date: Thu, 22 Jan 2026 11:26:20 +0100 Subject: [PATCH 36/49] Revert "fix: use quotemeta to join command parts" This reverts commit 08d440717e1c2a018543dee191969ea3bde1b440. --- bin/shell/osh.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/shell/osh.pl b/bin/shell/osh.pl index 28cd6cc..df64a7d 100755 --- a/bin/shell/osh.pl +++ b/bin/shell/osh.pl @@ -1611,7 +1611,7 @@ else { push @proxyCommand, '-o', "ConnectTimeout=$timeout" if $timeout; # Quote arguments that contain spaces and build the command string - my $proxyCommandStr = join(' ', map { quotemeta } @proxyCommand); + my $proxyCommandStr = join(' ', map { /\s/ ? "'$_'" : $_ } @proxyCommand); push @command, '-o', "ProxyCommand=$proxyCommandStr"; osh_debug("ProxyCommand: $proxyCommandStr"); From ffcbd5d68eadcd3824d11fb3b807e31264b5c28e Mon Sep 17 00:00:00 2001 From: Jonah Date: Thu, 22 Jan 2026 11:30:25 +0100 Subject: [PATCH 37/49] chore: add missing closing bracket MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Stéphane Lesimple --- bin/plugin/restricted/whoHasAccessTo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/plugin/restricted/whoHasAccessTo b/bin/plugin/restricted/whoHasAccessTo index 11a5714..0b3b4e2 100755 --- a/bin/plugin/restricted/whoHasAccessTo +++ b/bin/plugin/restricted/whoHasAccessTo @@ -34,7 +34,7 @@ Usage: --osh SCRIPT_NAME --host SERVER [OPTIONS] --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-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 From 14187eef49bba7194317e02365e502d13c8e2882 Mon Sep 17 00:00:00 2001 From: Jonah Date: Thu, 22 Jan 2026 11:30:53 +0100 Subject: [PATCH 38/49] fix: correctly check json output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Stéphane Lesimple --- tests/functional/tests.d/347-proxyjump.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/tests.d/347-proxyjump.sh b/tests/functional/tests.d/347-proxyjump.sh index c028355..f66a2c7 100644 --- a/tests/functional/tests.d/347-proxyjump.sh +++ b/tests/functional/tests.d/347-proxyjump.sh @@ -260,7 +260,7 @@ testsuite_proxyjump() # Check that selfListAccesses shows the proxy-user information success selfListAccesses_shows_proxy_user $a0 --osh selfListAccesses json .command selfListAccesses .error_code OK - contain '"ip":"192.168.1.201"' + json .value[0].ip 192.168.1.201 contain '"port":"2222"' contain '"user":"listtest"' contain '"proxyIp":"10.0.0.5"' From f6bc78707088d69a6663a276aa017b777608f295 Mon Sep 17 00:00:00 2001 From: jon4hz Date: Thu, 22 Jan 2026 13:13:11 +0100 Subject: [PATCH 39/49] fix: consistent flag naming in selfListSessions --- bin/plugin/open/selfListSessions | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bin/plugin/open/selfListSessions b/bin/plugin/open/selfListSessions index 80945d3..393d378 100755 --- a/bin/plugin/open/selfListSessions +++ b/bin/plugin/open/selfListSessions @@ -26,9 +26,9 @@ my $remainingOptions = OVH::Bastion::Plugin::begin( "via-port=i" => \my $viaPort, "to-port=i" => \my $toPort, "limit=i" => \my $limit, - "proxyuser=s" => \my $proxyUser, - "proxyip=s" => \my $proxyIp, - "proxyport=i" => \my $proxyPort, + "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 @@ -50,9 +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 - --proxyuser USER Only sessions that used proxy USER - --proxyip HOST Only sessions that used proxy IP - --proxyport PORT Only sessions that used proxy 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. From 004e8ce19b8fa9d92633a8f021fdb523a35578bd Mon Sep 17 00:00:00 2001 From: jon4hz Date: Thu, 22 Jan 2026 13:14:10 +0100 Subject: [PATCH 40/49] fix: placement of proxyUser option --- bin/plugin/restricted/selfDelPersonalAccess | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/plugin/restricted/selfDelPersonalAccess b/bin/plugin/restricted/selfDelPersonalAccess index d48bd83..3b1a682 100755 --- a/bin/plugin/restricted/selfDelPersonalAccess +++ b/bin/plugin/restricted/selfDelPersonalAccess @@ -17,13 +17,13 @@ my $remainingOptions = OVH::Bastion::Plugin::begin( "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, "scpup" => \my $scpUp, "scpdown" => \my $scpDown, "sftp" => \my $sftp, - "proxy-user=s" => \my $proxyUser, }, helptext => <<'EOF', Remove a personal server access from your account From 97cbea114fb008225ed2f3ec1ce70ef29da9f8c0 Mon Sep 17 00:00:00 2001 From: jon4hz Date: Thu, 22 Jan 2026 15:03:31 +0100 Subject: [PATCH 41/49] feat: make proxyJump and opt-in feature --- bin/helper/osh-accountAddGroupServer | 9 +++++++++ bin/helper/osh-accountModifyPersonalAccess | 9 +++++++++ bin/helper/osh-groupAddServer | 9 +++++++++ bin/shell/osh.pl | 13 +++++++++++++ etc/bastion/bastion.conf.dist | 7 ++++++- lib/perl/OVH/Bastion/configuration.inc | 1 + 6 files changed, 47 insertions(+), 1 deletion(-) diff --git a/bin/helper/osh-accountAddGroupServer b/bin/helper/osh-accountAddGroupServer index a09b621..23c75f5 100755 --- a/bin/helper/osh-accountAddGroupServer +++ b/bin/helper/osh-accountAddGroupServer @@ -60,6 +60,15 @@ 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', diff --git a/bin/helper/osh-accountModifyPersonalAccess b/bin/helper/osh-accountModifyPersonalAccess index 3e7478e..ed7efe0 100755 --- a/bin/helper/osh-accountModifyPersonalAccess +++ b/bin/helper/osh-accountModifyPersonalAccess @@ -87,6 +87,15 @@ 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"); +} + my $machine = OVH::Bastion::machine_display( ip => $ip, port => $port, diff --git a/bin/helper/osh-groupAddServer b/bin/helper/osh-groupAddServer index fd10ec7..9df3522 100755 --- a/bin/helper/osh-groupAddServer +++ b/bin/helper/osh-groupAddServer @@ -92,6 +92,15 @@ $fnret = OVH::Bastion::Helper::acquire_lock($lock_fh); $fnret or HEXIT($fnret); #>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"); +} + my $machine = OVH::Bastion::machine_display( ip => $ip, port => $port, diff --git a/bin/shell/osh.pl b/bin/shell/osh.pl index df64a7d..fc30340 100755 --- a/bin/shell/osh.pl +++ b/bin/shell/osh.pl @@ -275,6 +275,13 @@ osh_debug("user-passed options : $realOptions"); # 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 @@ -1591,6 +1598,12 @@ else { 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)); diff --git a/etc/bastion/bastion.conf.dist b/etc/bastion/bastion.conf.dist index 0961fe4..7236bb9 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. @@ -503,7 +508,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/configuration.inc b/lib/perl/OVH/Bastion/configuration.inc index 01daffd..4e0bf1a 100644 --- a/lib/perl/OVH/Bastion/configuration.inc +++ b/lib/perl/OVH/Bastion/configuration.inc @@ -277,6 +277,7 @@ sub load_configuration { interactiveModeAllowed readOnlySlaveMode sshClientHasOptionE ingressKeysFromAllowOverride moshAllowed debug keyboardInteractiveAllowed passwordAllowed telnetAllowed remoteCommandEscapeByDefault accountExternalValidationDenyOnFailure ingressRequirePIV IPv6Allowed sshAddKeysToAgentAllowed + egressProxyJumpAllowed } ], } From a471dc055d0c333a591d085a831471e591122f6d Mon Sep 17 00:00:00 2001 From: jon4hz Date: Thu, 22 Jan 2026 15:04:45 +0100 Subject: [PATCH 42/49] chore: perl tidy --- bin/helper/osh-accountAddGroupServer | 3 +-- bin/helper/osh-accountModifyPersonalAccess | 3 +-- bin/helper/osh-groupAddServer | 3 +-- bin/plugin/open/selfListSessions | 24 ++++++++++----------- bin/plugin/restricted/selfDelPersonalAccess | 10 ++++----- bin/shell/osh.pl | 6 +++--- 6 files changed, 23 insertions(+), 26 deletions(-) diff --git a/bin/helper/osh-accountAddGroupServer b/bin/helper/osh-accountAddGroupServer index 23c75f5..da21fe6 100755 --- a/bin/helper/osh-accountAddGroupServer +++ b/bin/helper/osh-accountAddGroupServer @@ -65,8 +65,7 @@ $fnret or main_exit(OVH::Bastion::EXIT_CONFIGURATION_FAILURE, "configuration_fai my $config = $fnret->value; if ($action eq 'add' && $proxyIp && !$config->{'egressProxyJumpAllowed'}) { - HEXIT('ERR_INVALID_PARAMETER', - msg => "ProxyJump egress connections are disabled by policy"); + 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 diff --git a/bin/helper/osh-accountModifyPersonalAccess b/bin/helper/osh-accountModifyPersonalAccess index ed7efe0..a56de54 100755 --- a/bin/helper/osh-accountModifyPersonalAccess +++ b/bin/helper/osh-accountModifyPersonalAccess @@ -92,8 +92,7 @@ $fnret or main_exit(OVH::Bastion::EXIT_CONFIGURATION_FAILURE, "configuration_fai my $config = $fnret->value; if ($action eq 'add' && $proxyIp && !$config->{'egressProxyJumpAllowed'}) { - HEXIT('ERR_INVALID_PARAMETER', - msg => "ProxyJump egress connections are disabled by policy"); + HEXIT('ERR_INVALID_PARAMETER', msg => "ProxyJump egress connections are disabled by policy"); } my $machine = OVH::Bastion::machine_display( diff --git a/bin/helper/osh-groupAddServer b/bin/helper/osh-groupAddServer index 9df3522..f5216e7 100755 --- a/bin/helper/osh-groupAddServer +++ b/bin/helper/osh-groupAddServer @@ -97,8 +97,7 @@ $fnret or main_exit(OVH::Bastion::EXIT_CONFIGURATION_FAILURE, "configuration_fai my $config = $fnret->value; if ($action eq 'add' && $proxyIp && !$config->{'egressProxyJumpAllowed'}) { - HEXIT('ERR_INVALID_PARAMETER', - msg => "ProxyJump egress connections are disabled by policy"); + HEXIT('ERR_INVALID_PARAMETER', msg => "ProxyJump egress connections are disabled by policy"); } my $machine = OVH::Bastion::machine_display( diff --git a/bin/plugin/open/selfListSessions b/bin/plugin/open/selfListSessions index 393d378..03fcc5f 100755 --- a/bin/plugin/open/selfListSessions +++ b/bin/plugin/open/selfListSessions @@ -14,18 +14,18 @@ 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, diff --git a/bin/plugin/restricted/selfDelPersonalAccess b/bin/plugin/restricted/selfDelPersonalAccess index 3b1a682..9108784 100755 --- a/bin/plugin/restricted/selfDelPersonalAccess +++ b/bin/plugin/restricted/selfDelPersonalAccess @@ -19,11 +19,11 @@ my $remainingOptions = OVH::Bastion::Plugin::begin( "proxy-port=s" => \my $proxyPort, "proxy-user=s" => \my $proxyUser, # undocumented/compatibility: - "user-any" => \my $userAny, - "port-any" => \my $portAny, - "scpup" => \my $scpUp, - "scpdown" => \my $scpDown, - "sftp" => \my $sftp, + "user-any" => \my $userAny, + "port-any" => \my $portAny, + "scpup" => \my $scpUp, + "scpdown" => \my $scpDown, + "sftp" => \my $sftp, }, helptext => <<'EOF', Remove a personal server access from your account diff --git a/bin/shell/osh.pl b/bin/shell/osh.pl index fc30340..5e2367f 100755 --- a/bin/shell/osh.pl +++ b/bin/shell/osh.pl @@ -275,12 +275,12 @@ osh_debug("user-passed options : $realOptions"); # 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"; + "Sorry $self, egress proxy-jump connections have been disabled by policy"; } osh_debug("Detected ProxyJump connection, executing command directly"); @@ -1601,7 +1601,7 @@ else { # 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"; + "Sorry $self, egress proxy-jump connections have been disabled by policy"; } # Build ProxyCommand with same options as main SSH command From 147265352cd2bdd31207281cae08352df987a529 Mon Sep 17 00:00:00 2001 From: jon4hz Date: Tue, 10 Feb 2026 11:53:56 +0100 Subject: [PATCH 43/49] chore: fix tests with proxyjump feature opt-in --- tests/functional/tests.d/347-proxyjump.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/functional/tests.d/347-proxyjump.sh b/tests/functional/tests.d/347-proxyjump.sh index f66a2c7..80b2598 100644 --- a/tests/functional/tests.d/347-proxyjump.sh +++ b/tests/functional/tests.d/347-proxyjump.sh @@ -18,6 +18,15 @@ testsuite_proxyjump() 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 # From 96f902a54c959f05134c3cf22b96004dc5ffafb7 Mon Sep 17 00:00:00 2001 From: jon4hz Date: Tue, 10 Feb 2026 14:18:32 +0100 Subject: [PATCH 44/49] fix: use proxyIp and not proxyHost --- bin/plugin/group-gatekeeper/groupAddGuestAccess | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/plugin/group-gatekeeper/groupAddGuestAccess b/bin/plugin/group-gatekeeper/groupAddGuestAccess index e56c7b4..ad7c11f 100755 --- a/bin/plugin/group-gatekeeper/groupAddGuestAccess +++ b/bin/plugin/group-gatekeeper/groupAddGuestAccess @@ -102,7 +102,7 @@ $fnret = OVH::Bastion::Plugin::ACL::check( scpDown => $scpDown, sftp => $sftp, protocol => $protocol, - proxyIp => $proxyHost, + proxyIp => $proxyIp, proxyPort => $proxyPort, proxyUser => $proxyUser, ); From 61eda562b67460b0aa4bd8e0930c4becaf658961 Mon Sep 17 00:00:00 2001 From: jon4hz Date: Tue, 10 Feb 2026 14:19:20 +0100 Subject: [PATCH 45/49] fix: improve error handling in proxy validation subroutine --- lib/perl/OVH/Bastion.pm | 12 +--- tests/functional/tests.d/347-proxyjump.sh | 75 ++++++++++++----------- 2 files changed, 42 insertions(+), 45 deletions(-) diff --git a/lib/perl/OVH/Bastion.pm b/lib/perl/OVH/Bastion.pm index 9846bc4..63e1137 100644 --- a/lib/perl/OVH/Bastion.pm +++ b/lib/perl/OVH/Bastion.pm @@ -779,20 +779,14 @@ sub validate_proxy_params { } else { $fnret = OVH::Bastion::get_ip(host => $proxyHost); - if ($fnret) { - $proxyIp = $fnret->value->{'ip'}; - } - else { - $proxyIp = $proxyHost; - } + $fnret or return $fnret; + $proxyIp = $fnret->value->{'ip'}; } } if ($proxyPort) { $fnret = OVH::Bastion::is_valid_port(port => $proxyPort); - if (!$fnret) { - return R('ERR_INVALID_PARAMETER', msg => "Proxy port '$proxyPort' is invalid: " . $fnret->msg); - } + $fnret or return $fnret; $proxyPort = $fnret->value; } diff --git a/tests/functional/tests.d/347-proxyjump.sh b/tests/functional/tests.d/347-proxyjump.sh index 80b2598..f9da0c5 100644 --- a/tests/functional/tests.d/347-proxyjump.sh +++ b/tests/functional/tests.d/347-proxyjump.sh @@ -41,7 +41,7 @@ testsuite_proxyjump() # 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 KO_INVALID_IP + 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 @@ -153,7 +153,7 @@ testsuite_proxyjump() # 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 KO_INVALID_IP + 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 @@ -254,11 +254,12 @@ testsuite_proxyjump() # Check that selfListAccesses shows the proxy information success selfListAccesses_shows_proxy $a0 --osh selfListAccesses json .command selfListAccesses .error_code OK - contain '"ip":"192.168.1.200"' - contain '"port":"2222"' - contain '"user":"listtest"' - contain '"proxyIp":"10.0.0.5"' - contain '"proxyPort":"5555"' + 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 @@ -269,12 +270,12 @@ testsuite_proxyjump() # 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].ip 192.168.1.201 - contain '"port":"2222"' - contain '"user":"listtest"' - contain '"proxyIp":"10.0.0.5"' - contain '"proxyPort":"5555"' - contain '"proxyUser":"proxyuser"' + 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 @@ -287,11 +288,12 @@ testsuite_proxyjump() # Check that accountListAccesses shows the proxy information success accountListAccesses_shows_proxy $a0 --osh accountListAccesses --account $account2 json .command accountListAccesses .error_code OK - contain '"ip":"192.168.2.100"' - contain '"port":"22"' - contain '"user":"testuser"' - contain '"proxyIp":"10.0.0.2"' - contain '"proxyPort":"22"' + 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 @@ -304,12 +306,12 @@ testsuite_proxyjump() # Check that accountListAccesses shows the proxy-user information success accountListAccesses_shows_proxy_user $a0 --osh accountListAccesses --account $account2 json .command accountListAccesses .error_code OK - contain '"ip":"192.168.2.200"' - contain '"port":"22"' - contain '"user":"testuser"' - contain '"proxyIp":"10.0.0.2"' - contain '"proxyPort":"22"' - contain '"proxyUser":"proxyuser"' + 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 @@ -322,11 +324,12 @@ testsuite_proxyjump() # Check that groupListServers shows the proxy information success groupListServers_shows_proxy $a1 --osh groupListServers --group $group1 json .command groupListServers .error_code OK - contain '"ip":"192.168.3.100"' - contain '"port":"22"' - contain '"user":"testuser"' - contain '"proxyIp":"10.0.0.3"' - contain '"proxyPort":"22"' + 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 @@ -339,12 +342,12 @@ testsuite_proxyjump() # Check that groupListServers shows the proxy-user information success groupListServers_shows_proxy_user $a1 --osh groupListServers --group $group1 json .command groupListServers .error_code OK - contain '"ip":"192.168.3.200"' - contain '"port":"22"' - contain '"user":"testuser"' - contain '"proxyIp":"10.0.0.3"' - contain '"proxyPort":"22"' - contain '"proxyUser":"proxyuser"' + 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 @@ -383,7 +386,7 @@ testsuite_proxyjump() # 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_INVALID_PARAMETER + 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 From 1a5a2bf35213b61e36a3d7d4925fa3b4ed639d99 Mon Sep 17 00:00:00 2001 From: jon4hz Date: Tue, 10 Feb 2026 16:34:51 +0100 Subject: [PATCH 46/49] fix: move proxy option check out of loop --- lib/perl/OVH/Bastion/allowdeny.inc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/perl/OVH/Bastion/allowdeny.inc b/lib/perl/OVH/Bastion/allowdeny.inc index 7b1abbe..84cb589 100644 --- a/lib/perl/OVH/Bastion/allowdeny.inc +++ b/lib/perl/OVH/Bastion/allowdeny.inc @@ -111,6 +111,11 @@ sub is_access_way_granted { "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) { # normalize '*' to undef @@ -231,11 +236,6 @@ sub is_access_way_granted { } } - # 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"); - } - # check proxy user if we have a proxy ip if (defined $wantedProxyIp && !$ignoreProxyUser) { if ($exactUserMatch) { From 1eb96e4f208b79cfc486b242c5d45e1a05549ec3 Mon Sep 17 00:00:00 2001 From: Jonah Date: Tue, 10 Feb 2026 16:38:22 +0100 Subject: [PATCH 47/49] fix: use logical or when checking for mandatory params MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Stéphane Lesimple --- lib/perl/OVH/Bastion/allowdeny.inc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/perl/OVH/Bastion/allowdeny.inc b/lib/perl/OVH/Bastion/allowdeny.inc index 84cb589..444bd58 100644 --- a/lib/perl/OVH/Bastion/allowdeny.inc +++ b/lib/perl/OVH/Bastion/allowdeny.inc @@ -295,7 +295,7 @@ sub is_access_way_granted { # 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'}) { + if (!defined $entry->{'proxyIp'} || !defined $entry->{'proxyPort'}) { next; } @@ -342,7 +342,7 @@ sub is_access_way_granted { # 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'}) { + if (!defined $entry->{'proxyIp'} || !defined $entry->{'proxyPort'}) { $proxyMatches = 0; } else { From 741b675d3ed80154798cf6cf9820f73d691824c7 Mon Sep 17 00:00:00 2001 From: Jonah Date: Tue, 10 Feb 2026 16:40:48 +0100 Subject: [PATCH 48/49] fix: simplify checks wanted variables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Stéphane Lesimple --- lib/perl/OVH/Bastion/allowdeny.inc | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/lib/perl/OVH/Bastion/allowdeny.inc b/lib/perl/OVH/Bastion/allowdeny.inc index 444bd58..f61dd2e 100644 --- a/lib/perl/OVH/Bastion/allowdeny.inc +++ b/lib/perl/OVH/Bastion/allowdeny.inc @@ -300,14 +300,10 @@ sub is_access_way_granted { } # check proxy IP match - if (defined $wantedProxyIp) { - next if (!defined $entry->{'proxyIp'} || $entry->{'proxyIp'} ne $wantedProxyIp); - } + next if ($entry->{'proxyIp'} ne $wantedProxyIp); # check proxy port match - if (defined $wantedProxyPort) { - next if (!defined $entry->{'proxyPort'} || $entry->{'proxyPort'} ne $wantedProxyPort); - } + 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'}) { @@ -347,16 +343,10 @@ sub is_access_way_granted { } else { # check proxy IP match - if (defined $wantedProxyIp) { - $proxyMatches = 0 - if (!defined $entry->{'proxyIp'} || $entry->{'proxyIp'} ne $wantedProxyIp); - } + $proxyMatches = 0 if ($entry->{'proxyIp'} ne $wantedProxyIp); # check proxy port match - if (defined $wantedProxyPort && $proxyMatches) { - $proxyMatches = 0 - if (!defined $entry->{'proxyPort'} || $entry->{'proxyPort'} ne $wantedProxyPort); - } + $proxyMatches = 0 if ($proxyMatches && $entry->{'proxyPort'} ne $wantedProxyPort); } } # Case 2: no proxy requested, but entry has proxy info - should not match From 0353c0523800a7c840239b581b4698d261cb1ee5 Mon Sep 17 00:00:00 2001 From: jon4hz Date: Tue, 10 Feb 2026 16:46:35 +0100 Subject: [PATCH 49/49] fix: simplify more checks wanted variables --- lib/perl/OVH/Bastion/allowdeny.inc | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/lib/perl/OVH/Bastion/allowdeny.inc b/lib/perl/OVH/Bastion/allowdeny.inc index f61dd2e..708fb54 100644 --- a/lib/perl/OVH/Bastion/allowdeny.inc +++ b/lib/perl/OVH/Bastion/allowdeny.inc @@ -384,16 +384,10 @@ sub is_access_way_granted { } else { # check proxy IP match - if (defined $wantedProxyIp) { - $proxyMatches = 0 - if (!defined $entry->{'proxyIp'} || $entry->{'proxyIp'} ne $wantedProxyIp); - } + next if ($entry->{'proxyIp'} ne $wantedProxyIp); # check proxy port match - if (defined $wantedProxyPort && $proxyMatches) { - $proxyMatches = 0 - if (!defined $entry->{'proxyPort'} || $entry->{'proxyPort'} ne $wantedProxyPort); - } + next if ($entry->{'proxyPort'} ne $wantedProxyPort); } } # Case 2: no proxy requested, but entry has proxy info - should not match