mirror of
https://github.com/ovh/the-bastion.git
synced 2025-02-26 00:24:12 +08:00
379 lines
12 KiB
Perl
Executable file
379 lines
12 KiB
Perl
Executable file
#! /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 = <<'END_OF_SCRIPT';
|
|
#! /usr/bin/env bash
|
|
set -u
|
|
shopt -s nocasematch
|
|
# <custom-section>
|
|
SELF="%SELF%"
|
|
BASTION_CMD="%BASTION_CMD%"
|
|
VERSION="%VERSION%"
|
|
# </custom_section>
|
|
|
|
: "${BASTION_SFTP_DEBUG:=}"
|
|
[ "$BASTION_SFTP_DEBUG" = 1 ] && echo "sftpwrapper: args: $*" >&2
|
|
|
|
BASTION_SSH_EXTRA_ARGS=""
|
|
BASTION_SFTP_EXTRA_ARGS=""
|
|
|
|
usage() {
|
|
cat >&2 <<EOF
|
|
usage: $0 [-i identity_file] [-P port] [-o ssh_option] [-b batchfile] [-F ssh_config] [-l limit] [-afprv] destination
|
|
EOF
|
|
exit 1
|
|
}
|
|
|
|
while [ -n "${1:-}" ]; do
|
|
case "$1" in
|
|
'-a'|'-f'|'-p'|'-r')
|
|
BASTION_SFTP_EXTRA_ARGS="$BASTION_SFTP_EXTRA_ARGS $1"
|
|
shift;;
|
|
'-v')
|
|
BASTION_SSH_EXTRA_ARGS="$BASTION_SSH_EXTRA_ARGS $1"
|
|
BASTION_SFTP_EXTRA_ARGS="$BASTION_SFTP_EXTRA_ARGS $1"
|
|
shift;;
|
|
'-i'|'-o'|'-F')
|
|
if [ -z "${2:-}" ]; then
|
|
echo "sftpwrapper: missing argument after '$1'" >&2
|
|
exit 1
|
|
fi
|
|
BASTION_SSH_EXTRA_ARGS="$BASTION_SSH_EXTRA_ARGS $1 $2"
|
|
BASTION_SFTP_EXTRA_ARGS="$BASTION_SFTP_EXTRA_ARGS $1 $2"
|
|
shift 2;;
|
|
'-b'|'-l')
|
|
if [ -z "${2:-}" ]; then
|
|
echo "sftpwrapper: missing argument after '$1'" >&2
|
|
exit 1
|
|
fi
|
|
BASTION_SFTP_EXTRA_ARGS="$BASTION_SFTP_EXTRA_ARGS $1 $2"
|
|
shift 2;;
|
|
"-P")
|
|
if [ -z "${2:-}" ]; then
|
|
echo "sftpwrapper: missing argument after '$1'" >&2
|
|
exit 1
|
|
fi
|
|
REMOTE_PORT="$2"
|
|
shift 2;;
|
|
"-*")
|
|
echo "sftpwrapper: unsupported option '$1'" >&2
|
|
exit 1;;
|
|
*) break;;
|
|
esac
|
|
done
|
|
|
|
# here, we should have only destination in $1
|
|
[ $# -ne 1 ] && usage
|
|
|
|
dst="${1:-}"
|
|
if [[ $dst =~ ^(sftp://)?(([^@:/]+)@)?([^@:/]+)(:([0-9]+))?(/(.+))?$ ]]; then
|
|
REMOTE_USER="${BASH_REMATCH[3]:-$SELF}"
|
|
REMOTE_HOST="${BASH_REMATCH[4]}"
|
|
REMOTE_PORT="${BASH_REMATCH[6]:-22}"
|
|
REMOTE_PATH="${BASH_REMATCH[7]}"
|
|
[ "$BASTION_SFTP_DEBUG" = 1 ] && echo "sftpwrapper: parsed user=$REMOTE_USER host=$REMOTE_HOST port=$REMOTE_PORT path=$REMOTE_PATH" >&2
|
|
else
|
|
echo "sftpwrapper: couldn't parse destination '$dst'" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [ -z "${REMOTE_HOST:-}" ]; then
|
|
echo "sftpwrapper: no remote host found in your command '$dst'" >&2
|
|
exit 1
|
|
fi
|
|
|
|
t=$(mktemp)
|
|
# shellcheck disable=SC2064
|
|
trap "rm -f $t" EXIT
|
|
|
|
# shellcheck disable=SC2086
|
|
[ "$BASTION_SFTP_DEBUG" = 1 ] && set -x
|
|
$BASTION_CMD -t $BASTION_SSH_EXTRA_ARGS -- --osh sftp --host "$REMOTE_HOST" --port "$REMOTE_PORT" --user "$REMOTE_USER" --generate-mfa-token | tee "$t"
|
|
[ "$BASTION_SFTP_DEBUG" = 1 ] && set +x
|
|
token=$(grep -Eo '^MFA_TOKEN=[a-zA-Z0-9,]+' "$t" | tail -n 1 | cut -d= -f2)
|
|
|
|
if [ -z "$token" ]; then
|
|
echo "sftpwrapper: Couldn't get an MFA token, aborting." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# now craft the wrapper to be used by sftp through -S
|
|
cat >"$t" <<'EOF'
|
|
#! /usr/bin/env bash
|
|
shopt -s nocasematch
|
|
|
|
REMOTE_USER=${USER:-}
|
|
REMOTE_PORT=22
|
|
sshcmdline=""
|
|
|
|
[ "$BASTION_SFTP_DEBUG" = 1 ] && echo "sftphelper: args: $*" >&2
|
|
while ! [ "${1:-}" = "--" ] ; do
|
|
# handle several ways of specifying remote user
|
|
if [ "${1:-}" = "-l" ] ; then
|
|
REMOTE_USER="${2:-}"
|
|
shift 2
|
|
elif [[ ${1:-} =~ ^-oUser[=\ ]([^\ ]+)$ ]] ; then
|
|
REMOTE_USER="${BASH_REMATCH[1]}"
|
|
shift
|
|
elif [ "${1:-}" = "-o" ] && [[ ${2:-} =~ ^user=([0-9]+)$ ]] ; then
|
|
REMOTE_USER="${BASH_REMATCH[1]}"
|
|
shift 2
|
|
|
|
# handle several ways of specifying remote port
|
|
elif [ "${1:-}" = "-p" ] ; then
|
|
REMOTE_PORT="${2:-22}"
|
|
shift 2
|
|
elif [[ ${1:-} =~ ^-oPort[=\ ]([0-9]+)$ ]] ; then
|
|
REMOTE_PORT="${BASH_REMATCH[1]}"
|
|
shift
|
|
elif [ "${1:-}" = "-o" ] && [[ $2 =~ ^port=([0-9]+)$ ]] ; then
|
|
REMOTE_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
|
|
|
|
[ "$BASTION_SFTP_DEBUG" = 1 ] && echo "sftphelper: remaining args: $*" >&2
|
|
|
|
# sane default
|
|
: "${REMOTE_USER:-$USER}"
|
|
[ -z "$REMOTE_USER" ] && REMOTE_USER="$(whoami)"
|
|
|
|
# after '--', remaining args are always host then 'sftp'
|
|
REMOTE_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 REMOTE_USER@REMOTE_HOST, split it
|
|
if [[ $REMOTE_HOST =~ @ ]]; then
|
|
REMOTE_USER="${REMOTE_HOST%@*}"
|
|
REMOTE_HOST="${REMOTE_HOST#*@}"
|
|
fi
|
|
|
|
# and go
|
|
[ "$BASTION_SFTP_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 sftp --mfa-token $token" >> "$t"
|
|
chmod +x "$t"
|
|
|
|
# don't use exec below, because we need the trap to be executed on exit
|
|
export BASTION_SFTP_DEBUG
|
|
[ "$BASTION_SFTP_DEBUG" = 1 ] && set -x
|
|
sftp $BASTION_SFTP_EXTRA_ARGS -S "$t" sftp://"$REMOTE_USER"@"$REMOTE_HOST":"$REMOTE_PORT""$REMOTE_PATH"
|
|
END_OF_SCRIPT
|
|
|
|
$script =~ s{%BASTION_CMD%}{$bastionCommand}g;
|
|
$script =~ s{%SELF%}{$self}g;
|
|
$script =~ s{%VERSION%}{$OVH::Bastion::VERSION}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 wrapper script to use instead of
|
|
calling your sftp client directly. It'll be specific to your account and this bastion,
|
|
don't share it with others! To download your customized script, copy/paste this command:
|
|
EOF
|
|
print "\necho \"$base64\"|base64 -d|gunzip -c > ~/sftp-via-$bastionName \\\n"
|
|
. "&& chmod +x ~/sftp-via-$bastionName\n\n";
|
|
|
|
osh_info <<"EOF";
|
|
To use sftp through this bastion, use this script instead of your regular sftp command.
|
|
For example:
|
|
\$ ~/sftp-via-$bastionName login\@server:port
|
|
|
|
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 sftp command called by the script
|
|
- `BASTION_SSH_EXTRA_ARGS`: if set, the contents of this variable is added
|
|
to the resulting ssh commands used in the script
|
|
|
|
For example:
|
|
\$ BASTION_SFTP_DEBUG=1 BASTION_SSH_EXTRA_ARGS="-v" ~/sftp-via-$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;
|
|
|