mirror of https://github.com/ovh/the-bastion
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
150 lines
5.3 KiB
150 lines
5.3 KiB
#! /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;
|