Skip to content

Commit

Permalink
feat: add rsync support to --protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
speed47 committed Sep 17, 2024
1 parent cbd945a commit 69f83c9
Show file tree
Hide file tree
Showing 31 changed files with 1,049 additions and 836 deletions.
65 changes: 35 additions & 30 deletions bin/plugin/group-gatekeeper/groupAddGuestAccess
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,17 @@ 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,
"account=s" => \my $account,
"user-any" => \my $userAny,
"port-any" => \my $portAny,
"scpup" => \my $scpUp,
"scpdown" => \my $scpDown,
"sftp" => \my $sftp,
"rsync" => \my $rsync,
"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,
# undocumented/compatibility:
"user-any" => \my $userAny,
"port-any" => \my $portAny,
"scpup" => \my $scpUp,
"scpdown" => \my $scpDown,
"sftp" => \my $sftp,
},
helptext => <<'EOF',
Add a specific group server access to an account
Expand All @@ -36,18 +37,22 @@ Usage: --osh SCRIPT_NAME --group GROUP --account ACCOUNT [OPTIONS]
to the USER/HOST/PORT tuple you'll specify with the options below.
--host HOST|IP|NET/CIDR Host(s) to add access to, either a HOST which will be resolved to an IP immediately,
or an IP, or a whole network using the NET/CIDR notation
--user USER Specify which remote user should be allowed to connect as.
--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.
--user-any Synonym of '--user *', allows connecting as any remote user.
--port PORT Remote port allowed to connect to
--port-any Allow access to any remote port
--scpup Allow SCP upload, you--bastion-->server (omit --user in this case)
--scpdown Allow SCP download, you<--bastion--server (omit --user in this case)
--sftp Allow usage of the SFTP subsystem, you<--bastion-->server (omit --user in this case)
--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.
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
--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
This command adds, to an existing bastion account, access to the egress keys of a group,
Expand All @@ -72,36 +77,36 @@ if (not $ip and $host) {
}

$fnret = OVH::Bastion::Plugin::ACL::check(
user => $user,
userAny => $userAny,
port => $port,
portAny => $portAny,
scpUp => $scpUp,
scpDown => $scpDown,
sftp => $sftp,
rsync => $rsync,
user => $user,
userAny => $userAny,
port => $port,
portAny => $portAny,
scpUp => $scpUp,
scpDown => $scpDown,
sftp => $sftp,
protocol => $protocol,
);
if (!$fnret) {
help();
osh_exit($fnret);
}
$user = $fnret->value->{'user'};
$port = $fnret->value->{'port'};

if (defined $ttl) {
$fnret = OVH::Bastion::is_valid_ttl(ttl => $ttl);
$fnret or osh_exit $fnret;
$ttl = $fnret->value->{'seconds'};
}

# act() will also call preconditions() which will check all the params
$fnret = OVH::Bastion::Plugin::groupSetRole::act(
account => $account,
group => $group,
action => 'add',
type => 'guest',
user => $user,
userAny => $userAny,
port => $port,
portAny => $portAny,
host => ($ip || $host),
ttl => $ttl,
comment => $comment,
Expand Down
58 changes: 31 additions & 27 deletions bin/plugin/group-gatekeeper/groupDelGuestAccess
Original file line number Diff line number Diff line change
Expand Up @@ -15,34 +15,39 @@ 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,
"account=s" => \my $account,
"user-any" => \my $userAny,
"port-any" => \my $portAny,
"scpup" => \my $scpUp,
"scpdown" => \my $scpDown,
"sftp" => \my $sftp,
"rsync" => \my $rsync,
"group=s" => \my $group,
"protocol=s" => \my $protocol,
"account=s" => \my $account,
# undocumented/compatibility:
"user-any" => \my $userAny,
"port-any" => \my $portAny,
"scpup" => \my $scpUp,
"scpdown" => \my $scpDown,
"sftp" => \my $sftp,
},
helptext => <<'EOF',
Remove a specific group server access from an account
Usage: --osh SCRIPT_NAME --group GROUP --account ACCOUNT [OPTIONS]
--group GROUP Specify which group to remove the guest access to ACCOUNT from
--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|NET/CIDR Host(s) to remove access from, either a HOST which will be resolved to an IP immediately,
or an IP, or a whole network using the NET/CIDR notation
--user USER Specify which remote user was allowed to connect as.
--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.
--user-any Synonym of '--user *', allowed connecting as any remote user.
--port PORT Remote port that was allowed to connect to
--port-any Use when access was allowed to any remote port
--scpup Remove SCP upload right, you--bastion-->server (omit --user in this case)
--scpdown Remove SCP download right, you<--bastion--server (omit --user in this case)
--sftp Remove usage of the SFTP subsystem, you<--bastion-->server (omit --user in this case)
--rsync Remove usage of rsync through the bastion
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:
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
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``
Expand All @@ -65,30 +70,29 @@ if (not $ip and $host) {
}

$fnret = OVH::Bastion::Plugin::ACL::check(
user => $user,
userAny => $userAny,
port => $port,
portAny => $portAny,
scpUp => $scpUp,
scpDown => $scpDown,
sftp => $sftp,
rsync => $rsync,
user => $user,
userAny => $userAny,
port => $port,
portAny => $portAny,
scpUp => $scpUp,
scpDown => $scpDown,
sftp => $sftp,
protocol => $protocol,
);
if (!$fnret) {
help();
osh_exit($fnret);
}
$user = $fnret->value->{'user'};
$port = $fnret->value->{'port'};

$fnret = OVH::Bastion::Plugin::groupSetRole::act(
account => $account,
group => $group,
action => 'del',
type => 'guest',
user => $user,
userAny => $userAny,
port => $port,
portAny => $portAny,
host => ($ip || $host),
sudo => 0,
silentoverride => 0,
Expand Down
149 changes: 149 additions & 0 deletions bin/plugin/open/rsync
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;

use File::Basename;
use lib dirname(__FILE__) . '/../../../lib/perl';
use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT );
use OVH::Bastion::Plugin::otherProtocol;
# stdout is used by rsync, so ensure we output everything through stderr
local $ENV{'FORCE_STDERR'} = 1;

# don't output fancy stuff, this can get digested by rsync and we get garbage output
local $ENV{'PLUGIN_QUIET'} = 1;

# rsync will craft a command-line for our plugin like this one (to upload):
# -l REMOTE_USER REMOTE_HOST rsync --server -vlogDtpre.iLsfxC . REMOTE_DIR
# and like this (to download)
# -l REMOTE_USER REMOTE_HOST rsync --server --sender -vlogDtpre.iLsfxC . REMOTE_DIR
#
# we parse the REMOTE_USER thanks to "options" below, and the remaining of the command-line
# is left untouched thanks to allowUnknownOptions==1
my $remainingOptions = OVH::Bastion::Plugin::begin(
argv => \@ARGV,
header => undef,
allowUnknownOptions => 1,
options => {
"l=s" => \my $opt_user,
},
helptext => <<'EOF',
rsync passthrough using the bastion
Usage examples:
rsync -va --rsh "ssh -T BASTION_USER@BASTION_HOST -p BASTION_PORT -- --osh rsync --" /srcdir remoteuser@remotehost:/dest/
rsync -va --rsh "ssh -T BASTION_USER@BASTION_HOST -p BASTION_PORT -- --osh rsync --" remoteuser@remotehost:/srcdir /dest/
Note that you'll need to be specifically granted to use rsync on the remote server,
in addition to being granted normal SSH access to it.
EOF
);

# validate $opt_user and export it as $user
OVH::Bastion::Plugin::validate_tuple(user => $opt_user);

# validate host passed by rsync and export it as $host/$ip
if (ref $remainingOptions eq 'ARRAY' && @$remainingOptions) {
my $opt_host = shift(@$remainingOptions);
OVH::Bastion::Plugin::validate_tuple(host => $opt_host);
}
else {
osh_exit 'ERR_INVALID_COMMAND',
"No host found, this plugin should be called by rsync.\nUse \`--osh rsync --help\` for more information.";
}

if (ref $remainingOptions eq 'ARRAY' && @$remainingOptions && $remainingOptions->[0] eq 'rsync') {
; # ok, we'll pass all the remaining options as options to the remote server, which will start rsync
}
else {
osh_exit 'ERR_INVALID_COMMAND',
"This plugin should be called by rsync.\nUse \`--osh rsync --help\` for more information.";
}

#
# code
#
my $fnret;

if (not $host) {
help();
osh_exit;
}

if (not $ip) {
# note that the calling-side rsync will not passthrough this exit code, but most probably "1" instead.
osh_exit 'ERR_HOST_NOT_FOUND', "Sorry, couldn't resolve the host you specified ('$host'), aborting.";
}

$port ||= 22; # rsync 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

$fnret = OVH::Bastion::Plugin::otherProtocol::has_protocol_access(
account => $self,
user => $user,
ip => $ip,
port => $port,
protocol => 'rsync',
);
$fnret or osh_exit($fnret);

my $machine = $fnret->value->{'machine'};
my @keys = @{$fnret->value->{'keys'} || []};
my $mfaRequired = $fnret->value->{'mfaRequired'};

# if we have an mfaRequired here, we have a problem because as we're run by rsync on the client side,
# it's too late to ask for any interactive user input now, as we don't have access to the terminal:
# we can only bail out if MFA was required for this host/group.
if ($mfaRequired) {
# is this account exempt from MFA?
my $hasMfaPasswordBypass =
OVH::Bastion::is_user_in_group(account => $sysself, group => OVH::Bastion::MFA_PASSWORD_BYPASS_GROUP);
my $hasMfaTOTPBypass =
OVH::Bastion::is_user_in_group(account => $sysself, group => OVH::Bastion::MFA_TOTP_BYPASS_GROUP);

if ($mfaRequired eq 'password' && $hasMfaPasswordBypass) {
print STDERR "This host requires password MFA but your account has password MFA bypass, allowing...\n";
}
elsif ($mfaRequired eq 'totp' && $hasMfaTOTPBypass) {
print STDERR "This host requires TOTP MFA but your account has TOTP MFA bypass, allowing...\n";
}
elsif ($mfaRequired eq 'any' && $hasMfaPasswordBypass && $hasMfaTOTPBypass) {
print STDERR "This host requires MFA but your account has MFA bypass, allowing...\n";
}
else {
osh_exit('KO_MFA_REQUIRED', "MFA is required for this host, which is not supported by rsync.");
}
}

# now build the command

my @cmd = qw{ ssh -x -oForwardAgent=no -oPermitLocalCommand=no -oClearAllForwardings=yes };
push @cmd, ('-p', $port) if $port;
push @cmd, ('-l', $user) if $user;

foreach my $key (@keys) {
push @cmd, ('-i', $key);
}

push @cmd, "--", $ip, @$remainingOptions;

print STDERR ">>> Hello $self, running rsync through the bastion on $machine...\n";

$fnret = OVH::Bastion::execute(cmd => \@cmd, expects_stdin => 1, is_binary => 1);
if ($fnret->err ne 'OK') {
osh_exit 'ERR_TRANSFER_FAILED', "Error launching transfer: $fnret";
}
print STDERR sprintf(
">>> Done, %d bytes uploaded, %d bytes downloaded\n",
$fnret->value->{'bytesnb'}{'stdin'} + 0,
$fnret->value->{'bytesnb'}{'stdout'} + 0
);

if ($fnret->value->{'sysret'} != 0) {
print STDERR ">>> On bastion side, rsync exited with return code " . $fnret->value->{'sysret'} . ".\n";
}

# don't use osh_exit() to avoid getting a footer
exit OVH::Bastion::EXIT_OK;
4 changes: 4 additions & 0 deletions bin/plugin/open/rsync.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"stealth_stdout": true,
"force_stderr": true
}
6 changes: 3 additions & 3 deletions bin/plugin/open/sftp
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT );
use OVH::Bastion::Plugin::otherProtocol;

# stdout is used by scp, so ensure we output everything through stderr
# stdout is used by sftp, so ensure we output everything through stderr
local $ENV{'FORCE_STDERR'} = 1;

# don't output fancy stuff, this can get digested by scp and we get garbage output
# don't output fancy stuff, this can get digested by sftp and we get garbage output
local $ENV{'PLUGIN_QUIET'} = 1;

my $remainingOptions = OVH::Bastion::Plugin::begin(
Expand Down Expand Up @@ -278,7 +278,7 @@ if (not $ip) {
osh_exit 'ERR_HOST_NOT_FOUND', "Sorry, couldn't resolve the host you specified ('$host'), aborting.";
}

$port ||= 22; # scp uses 22 if not specified, so we need to test access to that port and not any port (aka undef)
$port ||= 22; # sftp 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

$fnret = OVH::Bastion::Plugin::otherProtocol::has_protocol_access(
Expand Down
Loading

0 comments on commit 69f83c9

Please sign in to comment.