#! /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;