the-bastion/bin/plugin/open/sftp

263 lines
8.2 KiB
Text
Raw Normal View History

2023-03-03 19:26:51 +08:00
#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;
use MIME::Base64;
use IO::Compress::Gzip qw{ gzip };
use Sys::Hostname ();
use File::Basename;
use lib dirname(__FILE__) . '/../../../lib/perl';
use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT );
# stdout is used by scp, 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
local $ENV{'PLUGIN_QUIET'} = 1;
my $remainingOptions = OVH::Bastion::Plugin::begin(
argv => \@ARGV,
header => undef,
options => {},
help => \&help,
);
sub help {
delete $ENV{'FORCE_STDERR'};
delete $ENV{'PLUGIN_QUIET'};
osh_header("sftp");
my $bastionCommand = OVH::Bastion::config('bastionCommand')->value;
my $bastionName = OVH::Bastion::config('bastionName')->value;
$bastionCommand =~ s/USER|ACCOUNT/$self/g;
$bastionCommand =~ s/CACHENAME|BASTIONNAME/$bastionName/g;
my $hostname = Sys::Hostname::hostname();
$bastionCommand =~ s/HOSTNAME/$hostname/g;
# for sftp, if the bastionCommand contains -t, we need to get rid of it
$bastionCommand =~ s/ -t( |$)/$1/;
# same thing for --
$bastionCommand =~ s/ --/ /;
my $script = <<'EOF';
#! /usr/bin/env bash
shopt -s nocasematch
[ "$BASTION_SFTP_DEBUG" ] && echo "sftpwrapper: args: $*" >&2
while ! [ "$1" = "--" ] ; do
# user
if [ "$1" = "-l" ] ; then
remoteuser="--user $2"
shift 2
elif [[ $1 =~ ^-oUser[=\ ]([^\ ]+)$ ]] ; then
remoteuser="--user ${BASH_REMATCH[1]}"
shift
elif [ "$1" = "-o" ] && [[ $2 =~ ^user=([0-9]+)$ ]] ; then
remoteuser="--user ${BASH_REMATCH[1]}"
shift 2
# port
elif [ "$1" = "-p" ] ; then
remoteport="--port $2"
shift 2
elif [[ $1 =~ ^-oPort[=\ ]([0-9]+)$ ]] ; then
remoteport="--port ${BASH_REMATCH[1]}"
shift
elif [ "$1" = "-o" ] && [[ $2 =~ ^port=([0-9]+)$ ]] ; then
remoteport="--port ${BASH_REMATCH[1]}"
shift 2
# other '-oFoo Bar'
elif [[ $1 =~ ^-o([^\ ]+)\ (.+)$ ]] ; then
sshcmdline="$sshcmdline -o${BASH_REMATCH[1]}=${BASH_REMATCH[2]}"
shift
# don't forward -s
elif [ "$1" = "-s" ]; then
shift
# other stuff passed directly to ssh
else
sshcmdline="$sshcmdline $1"
shift
fi
done
# after '--', remaining args are always host then 'sftp'
host="$2"
subsystem="$3"
if [ "$subsystem" != sftp ]; then
echo "Unknown subsystem requested '$subsystem', expected 'sftp'" >&2
exit 1
fi
# if host is in the form remoteuser@remotehost, split it
if [[ $host =~ @ ]]; then
remoteuser="--user ${host%@*}"
host=${host#*@}
fi
# and go
[ "$BASTION_SFTP_DEBUG" ] && echo "sftpwrapper: exec #BASTIONCMD# -T $sshcmdline $BASTION_SFTP_EXTRA_ARGS -- $remoteuser $remoteport --host $host --osh sftp" >&2
exec #BASTIONCMD# -T $sshcmdline $BASTION_SFTP_EXTRA_ARGS -- $remoteuser $remoteport --host $host --osh sftp
EOF
$script =~ s{#BASTIONCMD#}{$bastionCommand}g;
my $compressed = '';
gzip \$script => \$compressed;
my $base64 = encode_base64($compressed);
chomp $base64;
osh_info <<"EOF";
Description:
Transfers files to/from a host through the bastion
Usage:
To use sftp through the bastion, you need a helper script to use with
your sftp client. It'll be specific to your account, don't share it with others!
To download your customized script, copy/paste this command:
EOF
print "\necho \"$base64\"|base64 -d|\\\ngunzip -c > ~/sftp_$bastionName && chmod +x ~/sftp_$bastionName\n\n";
osh_info <<"EOF";
To use sftp through this bastion, add `-S ~/sftp_$bastionName` to your regular sftp command.
For example:
\$ sftp -S ~/sftp_$bastionName login\@server
The following environment variables modify the behavior of the script:
- `BASTION_SFTP_DEBUG`: if set to 1, debug info is printed on the console
- `BASTION_SFTP_EXTRA_ARGS`: if set, the contents of this variable is added
to the resulting ssh command called by the script
For example:
\$ BASTION_SFTP_DEBUG=1 BASTION_SFTP_EXTRA_ARGS="-v" sftp -S ~/sftp_$bastionName login\@server
Please note that you need to be granted to be allowed to use sftp to the remote host,
in addition to having the right to SSH to it.
For a group, the right should be added with --sftp of the groupAddServer command.
For a personal access, the right should be added with --sftp of the selfAddPersonalAccess command.
EOF
osh_ok({script => $base64, "content-encoding" => 'base64-gzip'});
return 0;
}
#
# code
#
my $fnret;
if (not $host) {
help();
osh_exit;
}
if (not $ip) {
# note that the calling-side sftp 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.";
}
my $machine = $ip;
$machine = "$user\@$ip" if $user;
$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
$machine .= ":$port";
my %keys;
osh_debug("Checking access 1/2 of $self to $machine...");
$fnret = OVH::Bastion::is_access_granted(
account => $self,
user => $user,
ipfrom => $ENV{'OSH_IP_FROM'},
ip => $ip,
port => $port,
details => 1
);
if (not $fnret) {
osh_exit 'ERR_ACCESS_DENIED', "Sorry, but you don't seem to have access to $machine";
}
# get the keys we would try
foreach my $access (@{$fnret->value || []}) {
foreach my $key (@{$access->{'sortedKeys'} || []}) {
my $keyfile = $access->{'keys'}{$key}{'fullpath'};
$keys{$keyfile}++ if -r $keyfile;
osh_debug("Checking access 1/2 keyfile: $keyfile");
}
}
my $userToCheck = '!sftp';
osh_debug("Checking access 2/2 of $self to $userToCheck of $machine...");
$fnret = OVH::Bastion::is_access_granted(
account => $self,
user => $userToCheck,
ipfrom => $ENV{'OSH_IP_FROM'},
ip => $ip,
port => $port,
exactUserMatch => 1,
details => 1
);
if (not $fnret) {
osh_exit 'ERR_ACCESS_DENIED',
"Sorry, but even if you have ssh access to $machine, you still need to be granted specifically for sftp";
}
# get the keys we would try too
foreach my $access (@{$fnret->value || []}) {
foreach my $key (@{$access->{'sortedKeys'} || []}) {
my $keyfile = $access->{'keys'}{$key}{'fullpath'};
$keys{$keyfile}++ if -r $keyfile;
osh_debug("Checking access 2/2 keyfile: $keyfile");
}
}
# 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;
push @cmd, '-s';
my $atleastonekey = 0;
foreach my $keyfile (keys %keys) {
# only use the key if it has been seen in both allow_deny() calls, this is to avoid
# a security bypass where a user would have group access to a server, but not to the
# !sftp special user, and we would add himself this access through selfAddPrivateAccess.
# in that case both allow_deny would return OK, but with different keys.
# we'll only use the keys that matched BOTH calls.
next unless $keys{$keyfile} == 2;
push @cmd, ('-i', $keyfile);
$atleastonekey = 1;
}
if (not $atleastonekey) {
osh_exit('KO_ACCESS_DENIED',
"Sorry, you seem to have access through ssh and through sftp but by different and distinct means (distinct keys)."
. " The intersection between your rights for ssh and for sftp needs to be at least one.");
}
push @cmd, "--", $ip, 'sftp';
print STDERR ">>> Hello $self, starting up sftp subsystem to $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 ">>> Done, "
. $fnret->value->{'bytesnb'}{'stdin'}
. " bytes uploaded, "
. $fnret->value->{'bytesnb'}{'stdout'}
. " bytes downloaded.\n";
if ($fnret->value->{'sysret'} != 0) {
print STDERR ">>> On bastion side, sftp exited with return code " . $fnret->value->{'sysret'} . ".\n";
}
# don't use osh_exit() to avoid getting a footer
exit OVH::Bastion::EXIT_OK;