mirror of
https://github.com/ovh/the-bastion.git
synced 2024-11-14 12:17:32 +08:00
1633 lines
63 KiB
Perl
Executable file
1633 lines
63 KiB
Perl
Executable file
#! /usr/bin/env perl
|
|
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
|
|
use 5.010;
|
|
use common::sense;
|
|
|
|
use File::Basename;
|
|
use lib dirname(__FILE__) . '/../../lib/perl';
|
|
use OVH::Result;
|
|
use OVH::Bastion;
|
|
|
|
use Getopt::Long qw(GetOptionsFromString :config pass_through no_ignore_case no_auto_abbrev);
|
|
use Sys::Hostname;
|
|
use POSIX qw(strftime);
|
|
use Term::ANSIColor;
|
|
use JSON;
|
|
|
|
$| = 1;
|
|
my $fnret;
|
|
|
|
#
|
|
# Signals
|
|
#
|
|
|
|
$SIG{$_} = \&exit_sig for qw{ INT TERM SEGV HUP PIPE };
|
|
|
|
#
|
|
# Do just what is needed before the first call to main_exit in the code flow
|
|
#
|
|
|
|
# safe umask
|
|
umask(0027);
|
|
|
|
# sanitize user for taint mode
|
|
my $self = OVH::Bastion::get_user_from_env()->value;
|
|
my $home = OVH::Bastion::get_home_from_env()->value;
|
|
my ($sysself, $realm, $remoteself); # to handle realm cases, will be filled later, look for # REALM below
|
|
|
|
# both needs to be there because in case of SIG, we need them in the handler
|
|
my $log_db_name = undef;
|
|
my $log_insert_id = undef;
|
|
|
|
# set a uniqid that will be used in syslog, both sqls, and ttyrec name, so we can search for the same event
|
|
my $log_uniq_id = OVH::Bastion::generate_uniq_id()->value;
|
|
$ENV{'UNIQID'} = $log_uniq_id; # some modules need it, also used in warn/die handler
|
|
|
|
# fetch basic connection info
|
|
my ($ipfrom, $portfrom, $bastionip, $bastionport) = split(/\s/, $ENV{'SSH_CONNECTION'});
|
|
my $hostfrom = OVH::Bastion::ip2host($ipfrom)->value || $ipfrom;
|
|
my $bastionhost = OVH::Bastion::ip2host($bastionip)->value || $bastionip;
|
|
|
|
# sub used to exit from this shell, also handles logs for early exits
|
|
sub main_exit {
|
|
my ($retcode, $comment, $msg) = @_;
|
|
|
|
# if, this is an early exit, we didn't log anything yet in the sql, do it now
|
|
OVH::Bastion::log_access_insert(
|
|
account => $self,
|
|
cmdtype => 'abort',
|
|
allowed => undef,
|
|
ipfrom => $ipfrom,
|
|
hostfrom => $hostfrom,
|
|
portfrom => $portfrom,
|
|
bastionhost => $bastionhost,
|
|
bastionip => $bastionip,
|
|
bastionport => $bastionport,
|
|
ipto => undef,
|
|
hostto => undef,
|
|
portto => undef,
|
|
user => undef,
|
|
plugin => undef,
|
|
params => join('^', @ARGV),
|
|
comment => $comment,
|
|
uniqid => $log_uniq_id
|
|
) if (not defined $log_db_name or not defined $log_insert_id);
|
|
|
|
my $R = R($retcode eq OVH::Bastion::EXIT_OK ? 'OK' : 'KO_' . uc($comment), msg => $msg);
|
|
|
|
OVH::Bastion::osh_crit($R->msg) if not $R;
|
|
OVH::Bastion::json_output($R) if $ENV{'PLUGIN_JSON'};
|
|
|
|
exit $retcode;
|
|
}
|
|
|
|
# Safeness check
|
|
|
|
if (not defined $self) {
|
|
|
|
# wow, that's weird, stop here
|
|
$self = '<none>';
|
|
main_exit(OVH::Bastion::EXIT_EXEC_FAILED, "security_violation", "USER is not defined");
|
|
}
|
|
|
|
#
|
|
# Now, load config
|
|
#
|
|
|
|
$fnret = OVH::Bastion::load_configuration();
|
|
$fnret or main_exit(OVH::Bastion::EXIT_CONFIGURATION_FAILURE, "configuration_failure", $fnret->msg);
|
|
my $config = $fnret->value;
|
|
|
|
my $bastionName = $config->{'bastionName'};
|
|
my $osh_debug = $config->{'debug'};
|
|
|
|
# REALM case: somebody from another realm (named xyz) connects with the realm_xyz account here,
|
|
# and the real remote account name (which doesn't have an account here because it's from another realm)
|
|
# is passed through LC_BASTION
|
|
if ($self =~ /^realm_([a-zA-Z0-9_.-]+)/) {
|
|
if ($ENV{'LC_BASTION'}) {
|
|
|
|
# don't overwrite $self just yet because it might end up being invalid, and when we'll call main_exit 2 lines down,
|
|
# we won't log to the proper place if sql logs or access logs are enabled per account.
|
|
my $potentialSelf = sprintf("%s/%s", $1, $ENV{'LC_BASTION'});
|
|
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $potentialSelf, realmOnly => 1);
|
|
$fnret or main_exit(OVH::Bastion::EXIT_ACCOUNT_INVALID, "account_invalid", "The realm-scoped account '$self' is invalid (" . $fnret->msg . ")");
|
|
|
|
# $potentialSelf is valid, we can use it
|
|
$self = $potentialSelf;
|
|
}
|
|
else {
|
|
main_exit(OVH::Bastion::EXIT_ACCOUNT_INVALID, "account_invalid", "Attempted to use a realm account but not from another bastion");
|
|
}
|
|
}
|
|
else {
|
|
# non-realm case
|
|
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $self);
|
|
$fnret or main_exit(OVH::Bastion::EXIT_ACCOUNT_INVALID, "account_invalid", "The account is invalid (" . $fnret->msg . ")");
|
|
}
|
|
{
|
|
my %values = %{$fnret->value};
|
|
($sysself, $self, $realm, $remoteself) = @values{qw{ sysaccount account realm remoteaccount }};
|
|
}
|
|
|
|
#
|
|
# First Check : is USER valid ?
|
|
#
|
|
my $activenessDenyOnFailure = OVH::Bastion::config("accountExternalValidationDenyOnFailure")->value;
|
|
my $msg_to_print_delayed; # if set, will be osh_warn()'ed if we're connecting through ssh (i.e. not scp, it breaks it)
|
|
$fnret = OVH::Bastion::is_account_active(account => $self);
|
|
if ($fnret) {
|
|
; # OK
|
|
}
|
|
elsif ($fnret->is_ko || ($activenessDenyOnFailure && $fnret->is_err)) {
|
|
main_exit OVH::Bastion::EXIT_ACCOUNT_INACTIVE, "account_inactive", "Your account is inactive, $self, sorry";
|
|
}
|
|
else {
|
|
$msg_to_print_delayed = $fnret->msg;
|
|
}
|
|
|
|
#
|
|
# Now : are we in maintenance mode ?
|
|
#
|
|
if (-e '/home/allowkeeper/maintenance') {
|
|
osh_crit "This bastion is currently in maintenance mode, new connections are not allowed.";
|
|
my $maintenance_message = '(unknown)';
|
|
if (open(my $fh, '<', '/home/allowkeeper/maintenance')) {
|
|
local $/ = undef;
|
|
$maintenance_message = <$fh>;
|
|
close($fh);
|
|
}
|
|
osh_warn "The maintenance reason is as follows: $maintenance_message";
|
|
if (OVH::Bastion::is_admin()) {
|
|
osh_warn "You are a bastion admin, allowing anyway, but it's really because it's you.";
|
|
}
|
|
else {
|
|
main_exit(OVH::Bastion::EXIT_MAINTENANCE_MODE, "maintenance_mode", $maintenance_message);
|
|
}
|
|
}
|
|
|
|
#
|
|
# Does the user have a TTL, and if yes, has it expired?
|
|
#
|
|
|
|
$fnret = OVH::Bastion::account_config(account => $self, key => "account_ttl");
|
|
if ($fnret) {
|
|
if ($fnret->value !~ /^\d+$/) {
|
|
main_exit(OVH::Bastion::EXIT_TTL_EXPIRED, "ttl_expired", "Your TTL has an invalid value, access denied. Check with an administrator.");
|
|
}
|
|
my $ttl = $fnret->value;
|
|
|
|
$fnret = OVH::Bastion::account_config(account => $self, key => "creation_timestamp");
|
|
if ($fnret->value !~ /^\d+$/) {
|
|
main_exit(OVH::Bastion::EXIT_TTL_EXPIRED,
|
|
"ttl_expired", "Your account creation date has an invalid value, and you have a TTL set, access denied. Check with an administrator.");
|
|
}
|
|
my $created = $fnret->value;
|
|
|
|
if ($created + $ttl < time()) {
|
|
main_exit(OVH::Bastion::EXIT_TTL_EXPIRED, "ttl_expired", "Sorry $self, your account has expired.");
|
|
}
|
|
}
|
|
|
|
#
|
|
# Second check : has account logged-in recently enough to be allowed ?
|
|
#
|
|
$fnret = OVH::Bastion::is_account_nonexpired(sysaccount => $sysself, remoteaccount => $remoteself);
|
|
if ($fnret->is_err) {
|
|
|
|
# internal error, warn and pass
|
|
osh_warn($fnret);
|
|
}
|
|
elsif ($fnret->is_ko) {
|
|
|
|
# expired
|
|
main_exit OVH::Bastion::EXIT_ACCOUNT_EXPIRED, 'account_expired', $fnret->msg;
|
|
}
|
|
my $lastlog_filepath = $fnret->value->{'filepath'};
|
|
|
|
my $lastlogmsg = sprintf("Welcome to $bastionName, $self, this is your first connection");
|
|
if ($fnret && $fnret->value && $fnret->value->{'seconds'}) {
|
|
my $lastloginfo = $fnret->value->{'info'} ? " from " . $fnret->value->{'info'} : "";
|
|
$fnret = OVH::Bastion::duration2human(seconds => $fnret->value->{'seconds'}, tense => "past");
|
|
$lastlogmsg = sprintf("Welcome to $bastionName, $self, your last login was %s ago (%s)%s", $fnret->value->{'duration'}, $fnret->value->{'date'}, $lastloginfo);
|
|
}
|
|
|
|
# ok not expired, so we update lastlog
|
|
if ($lastlog_filepath && open(my $lastlogfh, '>', $lastlog_filepath)) {
|
|
print $lastlogfh sprintf("%s(%s)", $ipfrom, $hostfrom);
|
|
close($lastlogfh);
|
|
}
|
|
else {
|
|
osh_warn "Couldn't update your lastlog ($lastlog_filepath: $!), contact a bastion admin";
|
|
}
|
|
|
|
#
|
|
# Fetch command options
|
|
#
|
|
my @saved_argv = @ARGV;
|
|
|
|
# these options are the ones on shell definition of user calling osh.pl,
|
|
# the user-passed commands are stringified after "-c" (as in sh -c)
|
|
# it's possible to define the shell as osh.pl --debug, to force debug
|
|
my $realOptions;
|
|
my $opt_debug;
|
|
my $result = GetOptions(
|
|
"c=s" => \$realOptions, # user command under -c '...'
|
|
"debug" => \$opt_debug,
|
|
);
|
|
if (not $result) {
|
|
help();
|
|
main_exit OVH::Bastion::EXIT_UNKNOWN_COMMAND, "unknown_command", "Bad command";
|
|
}
|
|
|
|
$osh_debug = 1 if $opt_debug; # osh_debug was already 1 if specified in config file
|
|
|
|
# per-user debug ?
|
|
$fnret = OVH::Bastion::account_config(account => $self, key => "debug");
|
|
if ($fnret and $fnret->value() =~ /yes/) {
|
|
$osh_debug = 1;
|
|
}
|
|
|
|
$ENV{'OSH_DEBUG'} = 1 if $osh_debug;
|
|
|
|
osh_debug("self=$self home=$home realm=$realm remoteself=$remoteself sysself=$sysself");
|
|
osh_debug("user-passed options : $realOptions");
|
|
|
|
#
|
|
# Command params
|
|
#
|
|
|
|
my $port = 22; # can be override by special port
|
|
my @toExecute;
|
|
|
|
# special case: mosh, in that case we have something like this in $realOptions
|
|
# mosh-server 'new' '-s' '-c' '256' '-l' 'LANG=en_US.UTF-8' '-l' 'LANGUAGE=en_US' '--' '--osh' 'info'
|
|
if (defined $realOptions && $realOptions =~ /^mosh-server (.+?) '--' (.*)/) {
|
|
osh_debug("MOSH DETECTED (with params)");
|
|
|
|
# remove mosh stuff and save it for later
|
|
my $mosh = $1;
|
|
$realOptions = $2;
|
|
$ENV{'MOSH_SERVER_NETWORK_TMOUT'} = OVH::Bastion::config('moshTimeoutNetwork')->value();
|
|
$ENV{'MOSH_SERVER_SIGNAL_TMOUT'} = OVH::Bastion::config('moshTimeoutSignal')->value();
|
|
|
|
# get @toExecute params from the stuff we got from mosh-client (stored in $mosh) ?
|
|
# or maybe not... I don't trust users, and we need to override some things anyway (such as ports)
|
|
@toExecute = qw{ mosh-server new -s -l LANG=en_US.UTF-8 -l LANGUAGE=en_US };
|
|
|
|
# add what has been specified in the config
|
|
my @moshCommandLine = split(/\s+/, OVH::Bastion::config('moshCommandLine')->value());
|
|
push @toExecute, @moshCommandLine if @moshCommandLine;
|
|
|
|
# okay, just extract the -c 256 / -c 8 from the command because it depends on the user terminal spec
|
|
my $colors = 8; # by default
|
|
if ($mosh =~ m/'-c' '(\d+)'/) {
|
|
$colors = $1;
|
|
}
|
|
push @toExecute, ('-c', $colors, '--');
|
|
|
|
# mosh has the bad habit of surrounding every param with simple quotes ('), and escaping ' by '\'',
|
|
# because it thinks we are a POSIX shell, but we're not. So get around that
|
|
osh_debug("mosh params: $2");
|
|
|
|
# now unescape mosh params... yay regexes!
|
|
$realOptions =~ s/(?<!\\)'//g;
|
|
$realOptions =~ s/\\'/'/g;
|
|
osh_debug("after mosh parsing, remaining realOptions: <$realOptions>");
|
|
if (not $config->{'moshAllowed'}) {
|
|
main_exit OVH::Bastion::EXIT_MOSH_DISABLED, "mosh_disabled", "Mosh support has been disabled on this bastion";
|
|
}
|
|
}
|
|
elsif (defined $realOptions && $realOptions =~ /^mosh-server /) {
|
|
osh_debug("MOSH DETECTED (without any param)");
|
|
|
|
# we won't really use mosh, as we'll exit later with the bastion help anyway
|
|
$realOptions = '';
|
|
}
|
|
|
|
# If there is a '--' in command line, protect all the end of the command line
|
|
# in order to let it in one block after command line parsing
|
|
|
|
my $beforeOptions;
|
|
my $afterOptions;
|
|
|
|
if (defined $realOptions && $realOptions =~ /^(.*?) -- (.*)$/) {
|
|
$beforeOptions = $1;
|
|
$afterOptions = $2;
|
|
osh_debug("before <$beforeOptions> after <$afterOptions>");
|
|
if (($config->{'remoteCommandEscapeByDefault'} and not $beforeOptions =~ /(^| )--never-escape( |$)/)
|
|
or $beforeOptions =~ /(^| )--always-escape( |$)/)
|
|
{
|
|
# ugly / legacy mode: escape ' with \'
|
|
$afterOptions =~ s/'/\\'/g;
|
|
osh_debug("afterOptions after legacy voodoo is <$afterOptions>");
|
|
}
|
|
else {
|
|
osh_debug("afterOptions without legacy voodoo is <$afterOptions>");
|
|
}
|
|
}
|
|
else {
|
|
# we have no -- delimiter, either there was no remote command (that's fine),
|
|
# or it's indistinguishable from the bastion options, in that case GetOptionsFromString
|
|
# will leave what it doesn't recognize, will also fuck up "" and '', but users are warned
|
|
# to always use -- anyway, and we'll use that as a remote command
|
|
$beforeOptions = $realOptions;
|
|
$afterOptions = undef; # will contain the GetOptionsFromString leftovers
|
|
}
|
|
|
|
my $remainingOptions;
|
|
($result, $remainingOptions) = GetOptionsFromString(
|
|
$beforeOptions,
|
|
"port|p=i" => \my $optPort,
|
|
"verbose+" => \my $verbose,
|
|
"tty|t" => \my $tty,
|
|
"no-tty|T" => \my $notty,
|
|
"user|u=s" => \my $user,
|
|
"osh=s" => \my $osh_command,
|
|
"telnet|e" => \my $telnet,
|
|
"password=s" => \my $passwordFile,
|
|
"self-password|P" => \my $selfPassword,
|
|
"host|h=s" => \my $host,
|
|
"help" => \my $help,
|
|
"long-help" => \my $longHelp,
|
|
"quiet|q" => \my $quiet,
|
|
"timeout=i" => \my $timeout,
|
|
"bind=s" => \my $bind,
|
|
"debug" => \my $debug,
|
|
"json" => \my $json,
|
|
"json-greppable" => \my $json_greppable,
|
|
"json-pretty" => \my $json_pretty,
|
|
"always-escape" => \my $_dummy1, # not used as corresponding option has already been ninja-used above
|
|
"never-escape" => \my $_dummy2, # not used as corresponding option has already been ninja-used above
|
|
"interactive|i" => \my $interactive,
|
|
"netconf" => \my $netconf,
|
|
"wait" => \my $wait,
|
|
"ssh-as=s" => \my $sshAs,
|
|
"use-key=s" => \my $useKey,
|
|
"kbd-interactive" => \my $userKbdInteractive,
|
|
"proactive-mfa" => \my $proactiveMfa,
|
|
"fallback-password-delay=i" => \my $fallbackPasswordDelay,
|
|
);
|
|
if (not defined $realOptions) {
|
|
help();
|
|
if (OVH::Bastion::config('interactiveModeByDefault')->value) {
|
|
|
|
# nothing specified by the user, let's drop them to the interactive mode
|
|
osh_warn("No command specified, entering interactive mode by default");
|
|
$interactive = 1;
|
|
}
|
|
else {
|
|
main_exit OVH::Bastion::EXIT_UNKNOWN_COMMAND, "unknown_command", "Missing command";
|
|
}
|
|
}
|
|
|
|
if (!$quiet && $realm && !$ENV{'OSH_IN_INTERACTIVE_SESSION'}) {
|
|
my $welcome =
|
|
"You are now connected to " . colored($bastionName, "yellow") . ". Welcome, " . colored($remoteself, "yellow") . ", citizen of the " . colored($realm, "yellow") . " realm!";
|
|
print colored("-" x (length($welcome) - 3 * 9) . "\n", "bold yellow");
|
|
print $welcome. "\n";
|
|
print colored("-" x (length($welcome) - 3 * 9) . "\n", "bold yellow");
|
|
print "\n";
|
|
}
|
|
osh_debug("remainingOptions <" . join('/', @$remainingOptions) . ">");
|
|
|
|
if (defined $afterOptions and @$remainingOptions > 1) {
|
|
|
|
# user specified -- but there are more than 1 unrecognized param (the 1 should be the user@host)
|
|
# so we warn that we didn't understood
|
|
osh_warn "WARN : I couldn't parse some of your options before the '--' delimiter, things are probably about to go very wrong\n";
|
|
}
|
|
if (not defined $afterOptions and @$remainingOptions > 1 and not $osh_command) {
|
|
osh_warn "WARN : You did not use the '--' delimiter to pass your remote command, maybe something crazy will happen !\n";
|
|
}
|
|
|
|
if ($afterOptions) {
|
|
push @$remainingOptions, split(/ /, $afterOptions);
|
|
osh_debug("remainingOptionsAfterAdd <" . join('/', @$remainingOptions) . ">");
|
|
}
|
|
|
|
if ($json_pretty) {
|
|
$ENV{'PLUGIN_JSON'} = 'PRETTY';
|
|
}
|
|
elsif ($json_greppable) {
|
|
$ENV{'PLUGIN_JSON'} = 'GREP';
|
|
}
|
|
elsif ($json) {
|
|
$ENV{'PLUGIN_JSON'} = 'DEFAULT';
|
|
}
|
|
|
|
if ($quiet || $json || $json_pretty || $json_greppable) {
|
|
|
|
# remove colors
|
|
$ENV{'ANSI_COLORS_DISABLED'} = 1; # cf Term::ANSIColor;
|
|
}
|
|
|
|
if (!$result) {
|
|
help();
|
|
main_exit OVH::Bastion::EXIT_GETOPTS_FAILED, 'getopts_failed', "Error parsing command line options";
|
|
}
|
|
|
|
if ($help and not $osh_command) {
|
|
help();
|
|
main_exit OVH::Bastion::EXIT_OK, 'help', '';
|
|
}
|
|
|
|
if ($longHelp) {
|
|
long_help();
|
|
main_exit OVH::Bastion::EXIT_OK, 'long_help', '';
|
|
}
|
|
|
|
if ($bind) {
|
|
$fnret = OVH::Bastion::get_bastion_ips();
|
|
if ($fnret) {
|
|
if (not grep { $bind eq $_ } @{$fnret->value}) {
|
|
main_exit OVH::Bastion::EXIT_CONFLICTING_OPTIONS, "invalid_bind", "Invalid binding IP specified ($bind)";
|
|
}
|
|
}
|
|
}
|
|
|
|
# if proactive MFA has been requested, do it here, before the code diverts to either
|
|
# handling interactive session, plugins/osh commands, or a connection request
|
|
if ($proactiveMfa) {
|
|
print "As proactive MFA has been requested, entering MFA phase.\n";
|
|
$fnret = OVH::Bastion::do_pamtester(self => $self, sysself => $sysself);
|
|
$fnret or main_exit(OVH::Bastion::EXIT_MFA_FAILED, 'mfa_failed', $fnret->msg);
|
|
|
|
# if we're still here, it succeeded
|
|
$ENV{'OSH_PROACTIVE_MFA'} = 1;
|
|
}
|
|
|
|
if ($interactive and not $ENV{'OSH_IN_INTERACTIVE_SESSION'}) {
|
|
if (not $config->{'interactiveModeAllowed'}) {
|
|
main_exit OVH::Bastion::EXIT_INTERACTIVE_DISABLED, "interactive_disabled", "Interactive mode has been disabled on this bastion";
|
|
}
|
|
if ($osh_command) {
|
|
main_exit OVH::Bastion::EXIT_CONFLICTING_OPTIONS, "conflicting_options", "Incompatible options specified: --interactive and --osh";
|
|
}
|
|
if (@toExecute) {
|
|
|
|
# hmm, we are under mosh, mosh needs something to exec, so let's
|
|
# re-exec ourselves in interactive mode
|
|
exec(@toExecute, $0, '-c', $realOptions);
|
|
}
|
|
|
|
my $logret = OVH::Bastion::log_access_insert(
|
|
account => $self,
|
|
cmdtype => 'interactive',
|
|
allowed => 1,
|
|
ipfrom => $ipfrom,
|
|
hostfrom => $hostfrom,
|
|
portfrom => $portfrom,
|
|
bastionhost => $bastionhost,
|
|
bastionip => $bastionip,
|
|
bastionport => $bastionport,
|
|
ipto => undef,
|
|
hostto => undef,
|
|
portto => undef,
|
|
user => undef,
|
|
plugin => undef,
|
|
params => undef,
|
|
comment => undef,
|
|
uniqid => $log_uniq_id
|
|
);
|
|
if ($logret) {
|
|
|
|
# needed for the log_access_update func after we're done with the command
|
|
$log_insert_id = $logret->value->{'insert_id'};
|
|
$log_db_name = $logret->value->{'db_name'};
|
|
}
|
|
else {
|
|
osh_warn($logret->msg);
|
|
}
|
|
|
|
OVH::Bastion::interactive(realOptions => $realOptions, timeoutHandler => \&exit_sig, self => $self);
|
|
|
|
# this functions may never return, especially in case of idle timeout exit
|
|
|
|
if (defined $log_insert_id and defined $log_db_name) {
|
|
$logret = OVH::Bastion::log_access_update(
|
|
account => $self,
|
|
insert_id => $log_insert_id,
|
|
db_name => $log_db_name,
|
|
uniq_id => $log_uniq_id,
|
|
returnvalue => undef,
|
|
plugin_stdout => undef,
|
|
plugin_stderr => undef
|
|
);
|
|
$logret or osh_warn($logret->msg);
|
|
}
|
|
main_exit OVH::Bastion::EXIT_OK, 'interactive', '';
|
|
}
|
|
|
|
# If it's an osh command
|
|
# we'll pass the remaining options to the plugin
|
|
my $remainingOptionsCounter = scalar(@$remainingOptions);
|
|
my $command;
|
|
if ($osh_command) {
|
|
($help) and $ENV{'PLUGIN_HELP'} = 1;
|
|
($quiet) and $ENV{'PLUGIN_QUIET'} = 1;
|
|
($osh_debug) and $ENV{'PLUGIN_DEBUG'} = 1;
|
|
($debug) and $ENV{'PLUGIN_DEBUG'} = 1;
|
|
osh_debug('Going got pass the following supplement args to plugin: ' . join('^', @$remainingOptions));
|
|
}
|
|
else {
|
|
# it's ssh or telnet => it may remain at least 'host' or 'user@host'
|
|
osh_debug("Remaining options " . join('/', @$remainingOptions));
|
|
if ($remainingOptionsCounter == 0) {
|
|
if (!$host) {
|
|
help();
|
|
main_exit OVH::Bastion::EXIT_NO_HOST, 'no_host', "No osh command specified and no host to connect to";
|
|
}
|
|
else {
|
|
; # we have an host with option -h
|
|
}
|
|
}
|
|
else {
|
|
$host = shift(@{$remainingOptions});
|
|
if ($host eq '-osh' || $host eq '--osh') {
|
|
|
|
# special case when using -osh without argument
|
|
$osh_command = 'help';
|
|
$host = '';
|
|
}
|
|
else {
|
|
$remainingOptionsCounter--;
|
|
osh_debug("host = $host");
|
|
if ($host =~ /^([\S\d\w\.\-_]+)\@([\S\d\w\.\-_]+)$/) {
|
|
$user = $1;
|
|
$host = $2;
|
|
}
|
|
osh_debug("user $user host $host");
|
|
}
|
|
}
|
|
|
|
if ($remainingOptionsCounter > 0) {
|
|
$command .= join(' ', @$remainingOptions);
|
|
osh_debug("Going to add extra command $command");
|
|
}
|
|
}
|
|
|
|
if ($user && !OVH::Bastion::is_valid_remote_user(user => $user)) {
|
|
main_exit OVH::Bastion::EXIT_INVALID_REMOTE_USER, 'invalid_remote_user', "Remote user name '$user' seems invalid";
|
|
}
|
|
if ($host && $host !~ m{^[a-zA-Z0-9._/:-]+$}) {
|
|
|
|
# can be an IP (v4 or v6), hostname, or prefix (with a /)
|
|
main_exit OVH::Bastion::EXIT_INVALID_REMOTE_HOST, 'invalid_remote_host', "Remote host name '$host' seems invalid";
|
|
}
|
|
|
|
# Get real ip from host
|
|
$fnret = R('ERR_INTERNAL', silent => 1);
|
|
my $ip = undef;
|
|
|
|
# if: avoid loading Net::IP and BigInt if there's no host specified
|
|
if ($host) {
|
|
|
|
# probably this "host" is in fact an option, but we didn't parse it because it's an unknown one,
|
|
# so we call the long_help() for the user, before exiting
|
|
if ($host =~ m{^--}) {
|
|
long_help();
|
|
main_exit OVH::Bastion::EXIT_GETOPTS_FAILED, 'getopts_failed', "Couldn't parse option '$host'";
|
|
}
|
|
|
|
# otherwise, resolve the host
|
|
$fnret = OVH::Bastion::get_ip(host => $host);
|
|
}
|
|
if (!$fnret) {
|
|
|
|
# exit error when not osh ...
|
|
if (!$osh_command) {
|
|
main_exit OVH::Bastion::EXIT_HOST_NOT_FOUND, 'host_not_found', "Unable to resolve host '$host' ($fnret)";
|
|
}
|
|
elsif ($host && $host !~ m{^[0-9.:]+/\d+$}) # in some osh plugins, ip/mask is accepted, don't yell.
|
|
{
|
|
osh_warn("I was unable to resolve host '$host'. Something shitty might happen.");
|
|
}
|
|
}
|
|
else {
|
|
$ip = $fnret->value->{'ip'};
|
|
}
|
|
|
|
osh_debug("will work on IP $ip");
|
|
|
|
# Check if we got a telnet or ssh password user
|
|
my $userPasswordClue;
|
|
my $userPasswordContext;
|
|
if (defined $user and $user =~ /^(telnet|ssh)-passw(or)?d-([^-]+)(-([^-]+))?$/) {
|
|
my $method = $1;
|
|
|
|
# update user
|
|
$user = $3;
|
|
|
|
if ($4) {
|
|
$userPasswordClue = $5;
|
|
}
|
|
else {
|
|
$userPasswordClue = $user;
|
|
}
|
|
|
|
if ($method eq 'telnet') {
|
|
|
|
# as if user specified -e aka --telnet
|
|
$telnet = 1;
|
|
}
|
|
|
|
$userPasswordContext = 'group';
|
|
}
|
|
elsif ($passwordFile) {
|
|
$userPasswordClue = $passwordFile;
|
|
$userPasswordContext = 'group';
|
|
}
|
|
elsif ($selfPassword) {
|
|
$userPasswordClue = $self;
|
|
$userPasswordContext = 'self';
|
|
}
|
|
|
|
osh_debug("Will use password file $userPasswordClue with user $user under context $userPasswordContext") if $userPasswordClue;
|
|
|
|
if ($optPort) {
|
|
$port = $optPort;
|
|
}
|
|
elsif ($telnet) {
|
|
$port = 23;
|
|
}
|
|
else {
|
|
$port = 22;
|
|
}
|
|
|
|
if ($telnet && !$config->{'telnetAllowed'}) {
|
|
main_exit OVH::Bastion::EXIT_ACCESS_DENIED, 'telnet_denied', "Sorry, the telnet protocol has been disabled by policy";
|
|
}
|
|
|
|
if ($userKbdInteractive && !$config->{'keyboardInteractiveAllowed'}) {
|
|
main_exit OVH::Bastion::EXIT_CONFLICTING_OPTIONS, 'kbd_interactive_denied', "Sorry, the keyboard-interactive egress authentication scheme has been disabled by policy";
|
|
}
|
|
$ENV{'OSH_KBD_INTERACTIVE'} = 1 if $userKbdInteractive; # useful for plugins that need to call ssh by themselves (for example to test a connection, i.e. groupAddServer)
|
|
|
|
# MFA enforcing for ingress connection, either on global bastion config, or on specific account config
|
|
my $mfaPolicy = OVH::Bastion::config('accountMFAPolicy')->value;
|
|
my $isMfaPasswordConfigured = OVH::Bastion::is_user_in_group(account => $sysself, group => OVH::Bastion::MFA_PASSWORD_CONFIGURED_GROUP);
|
|
my $isMfaTOTPConfigured = OVH::Bastion::is_user_in_group(account => $sysself, group => OVH::Bastion::MFA_TOTP_CONFIGURED_GROUP);
|
|
my $isMfaPasswordRequired = OVH::Bastion::is_user_in_group(account => $sysself, group => OVH::Bastion::MFA_PASSWORD_REQUIRED_GROUP);
|
|
my $hasMfaPasswordBypass = OVH::Bastion::is_user_in_group(account => $sysself, group => OVH::Bastion::MFA_PASSWORD_BYPASS_GROUP);
|
|
my $isMfaTOTPRequired = OVH::Bastion::is_user_in_group(account => $sysself, group => OVH::Bastion::MFA_TOTP_REQUIRED_GROUP);
|
|
my $hasMfaTOTPBypass = OVH::Bastion::is_user_in_group(account => $sysself, group => OVH::Bastion::MFA_TOTP_BYPASS_GROUP);
|
|
|
|
# MFA information from a potential ingress realm:
|
|
my $remoteMfaValidated = 0;
|
|
my $remoteMfaPassword = 0;
|
|
my $remoteMfaTOTP = 0;
|
|
my $remoteHasPIV = 0;
|
|
|
|
my $pivEffectivePolicyEnabled = OVH::Bastion::is_effective_piv_account_policy_enabled(account => $self);
|
|
|
|
# if we're coming from a realm, we're receiving a connection from another bastion, keep all the traces:
|
|
my @previous_bastion_details;
|
|
if ($realm && $ENV{'LC_BASTION_DETAILS'}) {
|
|
my $decoded_details;
|
|
eval { $decoded_details = decode_json($ENV{'LC_BASTION_DETAILS'}); };
|
|
if (!$@) {
|
|
@previous_bastion_details = @$decoded_details;
|
|
|
|
# if the remote bastion did validate MFA, trust it
|
|
$remoteMfaValidated = $decoded_details->[0]{'mfa'}{'validated'} ? 1 : 0;
|
|
$remoteMfaPassword = $decoded_details->[0]{'mfa'}{'type'}{'password'} ? 1 : 0;
|
|
$remoteMfaTOTP = $decoded_details->[0]{'mfa'}{'type'}{'totp'} ? 1 : 0;
|
|
|
|
# also get the PIV status
|
|
if (ref $decoded_details->[0]{'piv'} eq 'HASH') {
|
|
$remoteHasPIV = $decoded_details->[0]{'piv'}{'enforced'} ? 1 : 0;
|
|
|
|
# if remote PIV is not enforced AND we enforce PIV locally (either by global policy or account-scoped policy),
|
|
# we must refuse the connection.
|
|
if ($pivEffectivePolicyEnabled && !$remoteHasPIV) {
|
|
my $otherSideName = $decoded_details->[0]{'via'}{'name'} || $decoded_details->[0]{'via'}{'host'};
|
|
main_exit(OVH::Bastion::EXIT_PIV_REQUIRED, 'piv_required',
|
|
"Sorry $self, but the $bastionName bastion policy requires that you use a PIV key to connect, please set a PIV key up on your local bastion ($otherSideName).");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($mfaPolicy ne 'disabled' && !grep { $osh_command eq $_ } qw{ selfMFASetupPassword selfMFASetupTOTP help info }) {
|
|
|
|
if (($mfaPolicy eq 'password-required' && !$hasMfaPasswordBypass) || $isMfaPasswordRequired) {
|
|
main_exit(OVH::Bastion::EXIT_MFA_PASSWORD_SETUP_REQUIRED,
|
|
'mfa_password_setup_required',
|
|
"Sorry, but you need to setup the Multi-Factor Authentication before using this bastion, please use the `--osh selfMFASetupPassword' option to do so")
|
|
if (!$isMfaPasswordConfigured && !$remoteMfaPassword);
|
|
}
|
|
|
|
if (($mfaPolicy eq 'totp-required' && !$hasMfaTOTPBypass) || $isMfaTOTPRequired) {
|
|
main_exit(OVH::Bastion::EXIT_MFA_TOTP_SETUP_REQUIRED,
|
|
'mfa_totp_setup_required',
|
|
"Sorry, but you need to setup the Multi-Factor Authentication before using this bastion, please use the `--osh selfMFASetupTOTP' option to do so")
|
|
if !($isMfaTOTPConfigured && !$remoteMfaTOTP);
|
|
}
|
|
|
|
if ($mfaPolicy eq 'any-required' && (!$isMfaPasswordConfigured && !$hasMfaPasswordBypass) && (!$isMfaTOTPConfigured && !$hasMfaTOTPBypass) && !$remoteMfaValidated) {
|
|
main_exit(OVH::Bastion::EXIT_MFA_ANY_SETUP_REQUIRED, 'mfa_any_setup_required',
|
|
"Sorry, but you need to setup the Multi-Factor Authentication before using this bastion, please use either the `--osh selfMFASetupPassword' or the `--osh selfMFASetupTOTP' option, at your discretion, to do so"
|
|
);
|
|
}
|
|
}
|
|
|
|
# /MFA enforcing
|
|
|
|
osh_debug("self : "
|
|
. (defined $self ? $self : '<undef>') . "\n"
|
|
. "user : "
|
|
. (defined $user ? $user : '<undef>') . "\n"
|
|
. "host : "
|
|
. (defined $host ? $host : '<undef>') . "\n"
|
|
. "port : "
|
|
. (defined $port ? $port : '<undef>') . "\n"
|
|
. "verbose : "
|
|
. (defined $verbose ? $verbose : '<undef>') . "\n"
|
|
. "tty : "
|
|
. (defined $tty ? $tty : '<undef>') . "\n"
|
|
. "osh : "
|
|
. (defined $osh_command ? $osh_command : '<undef>') . "\n"
|
|
. "command : "
|
|
. (defined $command ? $command : '<undef>')
|
|
. "\n");
|
|
|
|
my $hostto = OVH::Bastion::ip2host($host)->value || $host;
|
|
|
|
# Special case: adminSudo for ssh connection as another user
|
|
if ($sshAs) {
|
|
$fnret = OVH::Bastion::is_admin(account => $self);
|
|
my $logret = OVH::Bastion::log_access_insert(
|
|
account => $self,
|
|
cmdtype => 'sshas',
|
|
allowed => $fnret ? 1 : 0,
|
|
ipfrom => $ipfrom,
|
|
hostfrom => $hostfrom,
|
|
portfrom => $portfrom,
|
|
bastionhost => $bastionhost,
|
|
bastionip => $bastionip,
|
|
bastionport => $bastionport,
|
|
ipto => $ip,
|
|
hostto => $hostto,
|
|
portto => $optPort,
|
|
user => $user,
|
|
plugin => undef,
|
|
params => join(' ', @$remainingOptions),
|
|
comment => undef,
|
|
uniqid => $log_uniq_id
|
|
);
|
|
if (!$fnret) {
|
|
main_exit OVH::Bastion::EXIT_RESTRICTED_COMMAND, "sshas_denied", "Sorry, this feature is reserved to bastion administrators. Your attempt has been logged.";
|
|
}
|
|
if ($osh_command) {
|
|
main_exit OVH::Bastion::EXIT_CONFLICTING_OPTIONS, "conflicting_options",
|
|
"Can't use --ssh-as and --osh together. If you want to run a plugin as another user, use --osh adminSudo";
|
|
}
|
|
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $sshAs);
|
|
$fnret or main_exit OVH::Bastion::EXIT_ACCESS_DENIED, 'invalid_account', "Sorry, the specified account is invalid";
|
|
|
|
my @cmd = qw( sudo -n -u );
|
|
push @cmd, $sshAs;
|
|
push @cmd, qw( -- /usr/bin/env perl );
|
|
push @cmd, $OVH::Bastion::BASEPATH . '/bin/shell/osh.pl';
|
|
push @cmd, '-c';
|
|
|
|
my @forwardOptions;
|
|
push @forwardOptions, "--user", $user if $user;
|
|
push @forwardOptions, "--host", $host if $host;
|
|
push @forwardOptions, "--port", $port if $port;
|
|
push @forwardOptions, @$remainingOptions if ($remainingOptions and @$remainingOptions);
|
|
|
|
if (not @forwardOptions) {
|
|
main_exit OVH::Bastion::EXIT_NO_HOST, 'no_host', "No osh command specified and no host to connect to";
|
|
}
|
|
|
|
push @cmd, join(" ", @forwardOptions);
|
|
|
|
OVH::Bastion::syslogFormatted(
|
|
criticity => 'info',
|
|
type => 'security',
|
|
fields => [['type', 'admin-ssh-as'], ['account' => $self], ['sudo-as', $sshAs], ['plugin', 'ssh'], ['params', join(" ", @forwardOptions)]]
|
|
);
|
|
|
|
osh_warn("ADMIN SUDO: $self, you'll now impersonate $sshAs, this has been logged.");
|
|
|
|
exec(@cmd) or main_exit(OVH::Bastion::EXIT_EXEC_FAILED, "ssh_as_failed", "Couldn't start a session under the account $sshAs ($!)");
|
|
}
|
|
|
|
# This will be filled with details we might want to pass on to the remote machine as a json-encoded envvar
|
|
my %bastion_details = (
|
|
piv => {
|
|
enforced => $pivEffectivePolicyEnabled ? \1 : \0,
|
|
reason => $pivEffectivePolicyEnabled->msg,
|
|
},
|
|
);
|
|
|
|
#
|
|
# First case. We have an OSH command
|
|
#
|
|
if ($osh_command) {
|
|
|
|
# For backward compatibility, accept old names of plugins
|
|
my %legacy2new = qw(
|
|
accountAddFullGroupAccess groupAddMember
|
|
accountDelFullGroupAccess groupDelMember
|
|
accountAddPartialGroupAccess groupAddGuestAccess
|
|
accountDelPartialGroupAccess groupDelGuestAccess
|
|
accountListPartialGroupAccess groupListGuestAccesses
|
|
selfListKeys selfListIngressKeys
|
|
selfAddKey selfAddIngressKey
|
|
selfDelKey selfDelIngressKey
|
|
selfListBastionKeys selfListEgressKeys
|
|
selfGenerateBastionKey selfGenerateEgressKey
|
|
selfAddPrivateAccess selfAddPersonalAccess
|
|
selfDelPrivateAccess selfDelPersonalAccess
|
|
accountAddPrivateAccess accountAddPersonalAccess
|
|
accountDelPrivateAccess accountDelPersonalAccess
|
|
accountListBastionKeys accountListEgressKeys
|
|
accountListKeys accountListIngressKeys
|
|
accountResetKeys accountResetIngressKeys
|
|
helloWorld info
|
|
groupGenerateEgressPassword groupGeneratePassword
|
|
groupListEgressPasswords groupListPasswords
|
|
selfListEgressPasswords selfListPasswords
|
|
);
|
|
$osh_command = $legacy2new{$osh_command} if $legacy2new{$osh_command};
|
|
|
|
# Then test for rights
|
|
$fnret = OVH::Bastion::can_account_execute_plugin(account => $self, plugin => $osh_command);
|
|
|
|
my $logret = OVH::Bastion::log_access_insert(
|
|
account => $self,
|
|
cmdtype => 'osh',
|
|
allowed => ($fnret ? 1 : 0),
|
|
ipfrom => $ipfrom,
|
|
hostfrom => $hostfrom,
|
|
portfrom => $portfrom,
|
|
bastionhost => $bastionhost,
|
|
bastionip => $bastionip,
|
|
bastionport => $bastionport,
|
|
ipto => $ip,
|
|
hostto => $hostto,
|
|
portto => $optPort,
|
|
user => $user,
|
|
plugin => $osh_command,
|
|
params => join(' ', @$remainingOptions),
|
|
comment => 'plugin-' . ($fnret->value ? $fnret->value->{'type'} : 'UNDEF'),
|
|
uniqid => $log_uniq_id
|
|
);
|
|
if ($logret) {
|
|
|
|
# needed for the log_access_update func after we're done with the command
|
|
$log_insert_id = $logret->value->{'insert_id'};
|
|
$log_db_name = $logret->value->{'db_name'};
|
|
}
|
|
else {
|
|
warn_syslog("Failed to insert access log: " . $logret->msg);
|
|
if ($ip eq '127.0.0.1') {
|
|
osh_warn("Would deny access on out of space condition but you're root\@127.0.0.1, I hope you're here to fix me!");
|
|
}
|
|
else {
|
|
main_exit OVH::Bastion::EXIT_OUT_OF_SPACE, 'out_of_space', "Bastion is out of space, admin intervention is needed! (" . $logret->msg . ")";
|
|
}
|
|
}
|
|
|
|
if ($fnret) {
|
|
my @cmd = ($fnret->value->{'fullpath'}, $user, $ip, $host, $optPort, @$remainingOptions);
|
|
|
|
# is plugin explicitly disabled?
|
|
my $isDisabled = OVH::Bastion::plugin_config(plugin => $osh_command, key => "disabled");
|
|
|
|
# plugin is enabled by default if not explicitly disabled
|
|
if ($isDisabled and $isDisabled->value()) {
|
|
main_exit OVH::Bastion::EXIT_RESTRICTED_COMMAND, "plugin_disabled", "Sorry, this plugin has been disabled by policy.";
|
|
}
|
|
if ($isDisabled->is_err && $isDisabled->err ne 'KO_NO_SUCH_FILE') {
|
|
warn_syslog("Failed to tell whether the '$osh_command' plugin is enabled or not (" . $isDisabled->msg . ")");
|
|
main_exit OVH::Bastion::EXIT_RESTRICTED_COMMAND, "plugin_disabled",
|
|
"Sorry, a configuration error prevents us to check whether this plugin is enabled, warn your sysadmin!";
|
|
}
|
|
|
|
# check if we need JIT MFA to call this plugin, this can be configured per-plugin
|
|
# TODO: autodetect if the MFA check is done outside of the code by sshd+PAM, to avoid re-asking for it here
|
|
my $MFArequiredForPlugin = OVH::Bastion::plugin_config(plugin => $osh_command, key => "mfa_required")->value;
|
|
$MFArequiredForPlugin ||= 'none'; # no config means none
|
|
# some plugins need an explicit MFA check before being called (mainly plugins manipulating authentication factors)
|
|
# if the user wants to reset one of its MFA tokens, force require MFA
|
|
if ((grep { $osh_command eq $_ } qw{ selfMFAResetPassword selfMFAResetTOTP }) && ($MFArequiredForPlugin eq 'none')) {
|
|
|
|
# enforce MFA in those cases, even if it's not configured
|
|
$MFArequiredForPlugin = 'any';
|
|
}
|
|
|
|
# if the user wants to setup TOTP, if it happens to be already set (or any other factor), require it too
|
|
# note: this is not needed for selfMFASetupPassword, because `passwd` does the job of asking the previous password
|
|
elsif ($osh_command eq 'selfMFASetupTOTP' && ($isMfaTOTPConfigured || $isMfaPasswordConfigured) && ($MFArequiredForPlugin eq 'none')) {
|
|
$MFArequiredForPlugin = 'any';
|
|
}
|
|
|
|
if (!grep { $MFArequiredForPlugin eq $_ } qw{ password totp any none }) {
|
|
main_exit(OVH::Bastion::EXIT_MFA_FAILED, 'mfa_plugin_configuration_failed', "MFA configuration is incorrect for this plugin, report to your sysadmin!");
|
|
}
|
|
my $skipMFA = 0;
|
|
if ($MFArequiredForPlugin eq 'password' && !$isMfaPasswordConfigured) {
|
|
if ($hasMfaPasswordBypass) {
|
|
$skipMFA = 1;
|
|
}
|
|
else {
|
|
main_exit(OVH::Bastion::EXIT_MFA_PASSWORD_SETUP_REQUIRED,
|
|
'mfa_password_setup_required',
|
|
"Sorry, but you need to setup the Multi-Factor Authentication before using this command,\n" . "please use the `--osh selfMFASetupPassword' option to do so");
|
|
}
|
|
}
|
|
elsif ($MFArequiredForPlugin eq 'totp' && !$isMfaTOTPConfigured) {
|
|
if ($hasMfaTOTPBypass) {
|
|
$skipMFA = 1;
|
|
}
|
|
else {
|
|
main_exit(OVH::Bastion::EXIT_MFA_TOTP_SETUP_REQUIRED,
|
|
'mfa_totp_setup_required',
|
|
"Sorry, but you need to setup the Multi-Factor Authentication before using this command,\n" . "please use the `--osh selfMFASetupTOTP' option to do so");
|
|
}
|
|
}
|
|
elsif ($MFArequiredForPlugin eq 'any' && !$isMfaTOTPConfigured && !$isMfaPasswordConfigured) {
|
|
if ($hasMfaPasswordBypass && $hasMfaTOTPBypass) {
|
|
$skipMFA = 1;
|
|
}
|
|
else {
|
|
main_exit(OVH::Bastion::EXIT_MFA_ANY_SETUP_REQUIRED, 'mfa_any_setup_required',
|
|
"Sorry, but you need to setup the Multi-Factor Authentication before using this command,\n"
|
|
. "please use either the `--osh selfMFASetupPassword' or the `--osh selfMFASetupTOTP' option, at your discretion, to do so");
|
|
}
|
|
}
|
|
|
|
# and start the MFA phase if needed
|
|
if ($MFArequiredForPlugin ne 'none' && !$skipMFA) {
|
|
print "As this is required to run this plugin, entering MFA phase.\n";
|
|
if ($ENV{'OSH_PROACTIVE_MFA'}) {
|
|
print "... you already validated MFA proactively.\n";
|
|
}
|
|
else {
|
|
$fnret = OVH::Bastion::do_pamtester(self => $self, sysself => $sysself);
|
|
$fnret or main_exit(OVH::Bastion::EXIT_MFA_FAILED, 'mfa_failed', $fnret->msg);
|
|
}
|
|
|
|
# so that the remote server, which can be a bastion in case we're chaining, can enforce its own policy:
|
|
$bastion_details{'mfa'} = {
|
|
validated => \1,
|
|
reason => 'mfa_required_for_plugin',
|
|
type => {
|
|
password => $isMfaPasswordConfigured ? \1 : \0,
|
|
totp => $isMfaTOTPConfigured ? \1 : \0,
|
|
}
|
|
};
|
|
}
|
|
|
|
OVH::Bastion::set_terminal_mode_for_plugin(plugin => $osh_command, action => 'set');
|
|
|
|
# get the execution mode required by the plugin
|
|
my $is_binary;
|
|
my $system;
|
|
$fnret = OVH::Bastion::plugin_config(plugin => $osh_command, key => "execution_mode_on_$^O");
|
|
if (!$fnret || !$fnret->value) {
|
|
$fnret = OVH::Bastion::plugin_config(plugin => $osh_command, key => "execution_mode");
|
|
}
|
|
if ($fnret && $fnret->value) {
|
|
$system = 1 if $fnret->value eq 'system';
|
|
$is_binary = 1 if $fnret->value eq 'binary';
|
|
}
|
|
$ENV{'OSH_IP_FROM'} = $ipfrom; # used in some plugins for is_access_granted()
|
|
$fnret = OVH::Bastion::execute(
|
|
cmd => \@cmd,
|
|
noisy_stdout => 1,
|
|
noisy_stderr => 1,
|
|
expects_stdin => 1,
|
|
system => $system,
|
|
is_binary => $is_binary,
|
|
);
|
|
OVH::Bastion::set_terminal_mode_for_plugin(plugin => $osh_command, action => 'restore');
|
|
|
|
if (defined $log_insert_id and defined $log_db_name) {
|
|
$logret = OVH::Bastion::log_access_update(
|
|
account => $self,
|
|
insert_id => $log_insert_id,
|
|
db_name => $log_db_name,
|
|
uniq_id => $log_uniq_id,
|
|
returnvalue => $fnret->value ? $fnret->value->{'sysret_raw'} : undef,
|
|
plugin_stdout => $fnret->value ? $fnret->value->{'stdout'} : undef,
|
|
plugin_stderr => $fnret->value ? $fnret->value->{'stderr'} : undef
|
|
);
|
|
$logret or osh_warn($logret->msg);
|
|
}
|
|
exit($fnret->value ? $fnret->value->{'status'} : OVH::Bastion::EXIT_EXEC_FAILED);
|
|
}
|
|
else {
|
|
if ($fnret->err eq 'KO_UNKNOWN_PLUGIN') {
|
|
help();
|
|
main_exit OVH::Bastion::EXIT_UNKNOWN_COMMAND, "unknown_command", $fnret->msg;
|
|
}
|
|
main_exit OVH::Bastion::EXIT_RESTRICTED_COMMAND, "restricted_command", $fnret->msg;
|
|
}
|
|
}
|
|
|
|
#
|
|
# Else, it's a ttyrec ssh or telnet connection
|
|
#
|
|
|
|
if (!$quiet) {
|
|
if ($config->{'displayLastLogin'}) {
|
|
osh_info($lastlogmsg);
|
|
print "\n";
|
|
}
|
|
|
|
osh_warn($msg_to_print_delayed) if defined $msg_to_print_delayed; # set if we had an error to print previously
|
|
}
|
|
|
|
# if no user yet, fix it to remote user
|
|
# do that here, cause sometimes we do not want to pass user to osh
|
|
$user = $user || $config->{'defaultLogin'} || $remoteself || $sysself;
|
|
|
|
# log request
|
|
osh_debug("final request : " . "$user\@$ip -p $port -- $command'\n");
|
|
|
|
my $displayLine = "$hostfrom:$portfrom => $self\@$bastionhost:$bastionport => $user\@$hostto:$port";
|
|
|
|
if (!$quiet) {
|
|
print "$displayLine ...\n";
|
|
}
|
|
|
|
# before doing stuff, check if we have the right to connect somewhere (some users are locked only to osh commands)
|
|
$fnret = OVH::Bastion::account_config(account => $self, key => OVH::Bastion::OPT_ACCOUNT_OSH_ONLY);
|
|
if ($fnret and $fnret->value() =~ /yes/) {
|
|
$fnret = R('KO_ACCESS_DENIED', msg => "You don't have the right to connect anywhere");
|
|
}
|
|
else {
|
|
$fnret = OVH::Bastion::is_access_granted(account => $self, user => $user, ipfrom => $ipfrom, ip => $ip, port => $port, wantKeys => 1);
|
|
}
|
|
|
|
# so in the end, can we access the requested user@host machine ?
|
|
my $JITMFARequired;
|
|
if (!$fnret) {
|
|
|
|
# User is not allowed, exit
|
|
my $message = $fnret->msg;
|
|
if ($user eq $self) {
|
|
$message .= " (tried with remote user '$user')"; # "root is not the default login anymore"
|
|
}
|
|
|
|
my $logret = OVH::Bastion::log_access_insert(
|
|
account => $self,
|
|
cmdtype => $telnet ? 'telnet' : 'ssh',
|
|
allowed => 0,
|
|
ipfrom => $ipfrom,
|
|
hostfrom => $hostfrom,
|
|
portfrom => $portfrom,
|
|
bastionhost => $bastionhost,
|
|
bastionip => $bastionip,
|
|
bastionport => $bastionport,
|
|
ipto => $ip,
|
|
hostto => $hostto,
|
|
portto => $port,
|
|
user => $user,
|
|
params => $command,
|
|
uniqid => $log_uniq_id
|
|
);
|
|
if (!$logret) {
|
|
osh_warn($logret);
|
|
}
|
|
|
|
main_exit OVH::Bastion::EXIT_ACCESS_DENIED, 'access_denied', $message;
|
|
}
|
|
|
|
# else, keep calm and carry on
|
|
# build ttyrec command that'll prefix the real command
|
|
my $ttyrec_fnret = OVH::Bastion::build_ttyrec_cmdline(
|
|
ip => $ip,
|
|
port => $port,
|
|
user => $user,
|
|
account => $self,
|
|
uniqid => $log_uniq_id,
|
|
home => $home,
|
|
realm => $realm,
|
|
remoteaccount => $remoteself,
|
|
debug => $osh_debug,
|
|
tty => $tty,
|
|
notty => $notty
|
|
);
|
|
main_exit(OVH::Bastion::EXIT_TTYREC_CMDLINE_FAILED, "ttyrec_failed", $ttyrec_fnret->msg) if !$ttyrec_fnret;
|
|
|
|
my @ttyrec = @{$ttyrec_fnret->value->{'cmd'}};
|
|
my $saveFile = $ttyrec_fnret->value->{'saveFile'};
|
|
|
|
print " allowed ... log on($saveFile)\n\n" if !$quiet;
|
|
|
|
# now build the real command
|
|
my @command;
|
|
|
|
# if we are doing a password login, find the password(s)
|
|
|
|
# for autologin, "-1" means "try the main password, then try the fallbacks", 0 means "try only the main password", and N means "try only the Nth fallback password"
|
|
my $forcePasswordId = -1;
|
|
|
|
if ($userPasswordClue) {
|
|
|
|
# locate main password file
|
|
my $fnretpass = OVH::Bastion::get_passfile(hint => $userPasswordClue, context => $userPasswordContext, self => ($remoteself || $sysself), tryLegacy => 1);
|
|
if (!$fnretpass) {
|
|
main_exit OVH::Bastion::EXIT_PASSFILE_NOT_FOUND, "passfile-not-found", $fnretpass->msg;
|
|
}
|
|
$passwordFile = $fnretpass->value;
|
|
|
|
# check if a specific password is forced
|
|
foreach my $grant (@{$fnret->value}) {
|
|
|
|
# only keep the grant matching the password clue and context AND with a forced password
|
|
if (
|
|
$grant->{'forcePassword'}
|
|
&& ( ($userPasswordContext eq 'self' && $grant->{'type'} eq 'personal')
|
|
|| ($userPasswordContext eq 'group' && $grant->{'type'} =~ /^group-(member|guest)$/ && $grant->{'group'} eq $userPasswordClue))
|
|
)
|
|
{
|
|
|
|
# FIXME: force-password and force-key don't work yet for guest accesses, see #256
|
|
# fetch the hashes of the main password and all its fallbacks
|
|
my $fnrethashes;
|
|
if ($userPasswordContext eq 'self') {
|
|
$fnrethashes = OVH::Bastion::get_hashes_list(context => 'account', account => $userPasswordClue);
|
|
}
|
|
else {
|
|
$fnrethashes = OVH::Bastion::get_hashes_list(context => 'group', group => $userPasswordClue);
|
|
}
|
|
|
|
if (!$fnrethashes) {
|
|
main_exit(OVH::Bastion::EXIT_GET_HASH_FAILED, "get_hashes_list", $fnrethashes->msg);
|
|
}
|
|
|
|
# is our forced password's hash one of them ?
|
|
for my $id (0 .. $#{$fnrethashes->value}) {
|
|
foreach my $hash (values(%{$fnrethashes->value->[$id]->{'hashes'}})) {
|
|
if ($grant->{'forcePassword'} eq $hash) {
|
|
$forcePasswordId = $id;
|
|
print " forcing password with hash: " . $grant->{'forcePassword'} . "\n\n" unless $quiet;
|
|
}
|
|
}
|
|
}
|
|
|
|
# if the password was not found, abort
|
|
if ($forcePasswordId == -1) {
|
|
main_exit(OVH::Bastion::EXIT_PASSFILE_NOT_FOUND, "forced-password-not-found", "The forced password could not be found");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# if we want telnet (not ssh)
|
|
if ($telnet) {
|
|
|
|
# TELNET PASSWORD AUTOLOGIN
|
|
if ($userPasswordClue) {
|
|
osh_debug("going to use telnet with this password file : $passwordFile");
|
|
print " will use TELNET with password autologin\n\n" unless $quiet;
|
|
push @command, $OVH::Bastion::BASEPATH . '/bin/shell/autologin', 'telnet', $user, $ip, $port,
|
|
$passwordFile, $forcePasswordId, ($timeout ? $timeout : 45), ($fallbackPasswordDelay // 3);
|
|
}
|
|
|
|
# TELNET PASSWORD INTERACTIVE
|
|
else {
|
|
print " will use TELNET with interactive password login\n\n" unless $quiet;
|
|
push @command, '/usr/bin/telnet', '-l', $user, $host, $port;
|
|
}
|
|
}
|
|
|
|
# if we want ssh (not telnet)
|
|
else {
|
|
my @preferredAuths;
|
|
|
|
# SSH PASSWORD AUTOLOGIN
|
|
if ($userPasswordClue) {
|
|
|
|
push @preferredAuths, 'keyboard-interactive';
|
|
push @preferredAuths, 'password';
|
|
|
|
osh_debug("going to use ssh with this password file : $passwordFile");
|
|
print " will use SSH with password autologin\n\n" unless $quiet;
|
|
push @command, $OVH::Bastion::BASEPATH . '/bin/shell/autologin', 'ssh', $user, $ip, $port,
|
|
$passwordFile, $forcePasswordId, ($timeout ? $timeout : 45), ($fallbackPasswordDelay // 3);
|
|
}
|
|
|
|
# SSH EGRESS KEYS (and maybe password interactive as a fallback if passwordAllowed)
|
|
else {
|
|
|
|
# ssh by key
|
|
push @preferredAuths, 'publickey';
|
|
|
|
# also set kbdinteractive if allowed in bastion config (needed for e.g. TOTP)
|
|
push @preferredAuths, 'keyboard-interactive' if ($config->{'keyboardInteractiveAllowed'} && $userKbdInteractive);
|
|
|
|
# also set password if allowed in bastion config (to allow users to enter a remote password interactively)
|
|
push @preferredAuths, 'password' if $config->{'passwordAllowed'};
|
|
|
|
push @command, '/usr/bin/ssh', $ip, '-l', $user, '-p', $port;
|
|
|
|
my @keysToTry;
|
|
print " will try the following accesses you have: \n" unless $quiet;
|
|
foreach my $access (@{$fnret->value || []}) {
|
|
foreach my $key (@{$access->{'sortedKeys'} || []}) {
|
|
my $keyinfo = $access->{'keys'}{$key};
|
|
my $type = $access->{'type'} . " of " . $access->{'group'};
|
|
if ($access->{'type'} =~ /^group/) {
|
|
$type = colored($access->{'type'}, $access->{'type'} eq 'group-member' ? 'green' : 'yellow');
|
|
$type .= " of " . colored($access->{'group'}, 'blue bold');
|
|
}
|
|
elsif ($access->{'type'} =~ /^personal/) {
|
|
$type = colored($access->{'type'}, 'red') . ' access';
|
|
}
|
|
my $generated = strftime("[%Y/%m/%d]", localtime($keyinfo->{'mtime'}));
|
|
|
|
if ((not $useKey) || ($useKey eq $keyinfo->{'fingerprint'})) {
|
|
my $forced = ' ';
|
|
if ($useKey) {
|
|
$forced = colored(' (KEY FORCED ON CMDLINE)', 'bold red');
|
|
}
|
|
elsif ($access->{'forceKey'}) {
|
|
$forced = colored(' (KEY FORCED IN ACL)', 'bold red');
|
|
}
|
|
if ($access->{'mfaRequired'} && $access->{'mfaRequired'} ne 'none') {
|
|
$forced .= colored(' (MFA REQUIRED: ' . uc($access->{'mfaRequired'}) . ')', 'bold red');
|
|
$JITMFARequired = $access->{'mfaRequired'};
|
|
}
|
|
printf(" - %s with %s-%s key %s %s%s\n", $type, $keyinfo->{'family'}, $keyinfo->{'size'}, $keyinfo->{'fingerprint'}, $generated, $forced) unless $quiet;
|
|
push @keysToTry, $keyinfo->{'fullpath'} if not(grep { $_ eq $keyinfo->{'fullpath'} } @keysToTry);
|
|
}
|
|
}
|
|
}
|
|
if ($useKey and not @keysToTry) {
|
|
print " >>> No key matched the fingerprint you gave me ($useKey), connection will fail!\n";
|
|
}
|
|
print "\n" unless $quiet;
|
|
|
|
foreach (@keysToTry) {
|
|
if (-r) {
|
|
osh_debug("Got a group key $_");
|
|
push @command, '-i', $_;
|
|
}
|
|
else {
|
|
osh_warn("Weird, key file $_ is not accessible");
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($verbose) {
|
|
foreach (1 .. $verbose) {
|
|
push @command, '-v';
|
|
}
|
|
}
|
|
push @command, '-q' if $quiet;
|
|
push @command, '-t' if $tty;
|
|
push @command, '-T' if $notty;
|
|
push @command, '-o', "ConnectTimeout=$timeout" if $timeout;
|
|
|
|
if (not $quiet) {
|
|
$fnret = OVH::Bastion::account_config(account => $self, key => OVH::Bastion::OPT_ACCOUNT_IDLE_IGNORE, public => 1);
|
|
if ($fnret && $fnret->value =~ /yes/) {
|
|
osh_debug("Account is immune to idle");
|
|
}
|
|
else {
|
|
if ($config->{'idleLockTimeout'}) {
|
|
print(" /!\\ Your session will be locked after " . $config->{'idleLockTimeout'} . " seconds of inactivity, use `--osh unlock' to unlock it\n");
|
|
}
|
|
if ($config->{'idleKillTimeout'}) {
|
|
print(" /!\\ Your session will be killed after " . $config->{'idleKillTimeout'} . " seconds of inactivity.\n");
|
|
}
|
|
print "\n" if ($config->{'idleLockTimeout'} || $config->{'idleKillTimeout'});
|
|
}
|
|
}
|
|
|
|
push @command, '-o', 'PreferredAuthentications=' . (join(',', @preferredAuths));
|
|
|
|
if ($config->{'sshClientHasOptionE'}) {
|
|
push @command, '-E', $saveFile . '.sshdebug';
|
|
}
|
|
|
|
if ($config->{'sshClientDebugLevel'}) {
|
|
foreach (1 .. $config->{'sshClientDebugLevel'}) {
|
|
push @command, '-v';
|
|
}
|
|
}
|
|
|
|
if ($netconf) {
|
|
|
|
# in netconf mode, we must ask our ssh to request remote netconf subsystem
|
|
push @command, '-s', 'netconf';
|
|
}
|
|
elsif ($command) {
|
|
|
|
# the '--' is to force ssh (started by ttyrec (started by us)) to stop processing its options and pass the rest to remote shell
|
|
push @command, '--', $command;
|
|
}
|
|
}
|
|
|
|
# add current account name as LC_BASTION to be passed via ssh
|
|
$ENV{'LC_BASTION'} = $self;
|
|
|
|
$bastion_details{'mfa'}{'validated'} //= \0;
|
|
$bastion_details{'mfa'}{'type'}{'password'} //= \0;
|
|
$bastion_details{'mfa'}{'type'}{'totp'} //= \0;
|
|
$bastion_details{'piv'}{'enforced'} //= \0;
|
|
$bastion_details{'from'} = {addr => $ipfrom, host => $hostfrom, port => $portfrom + 0};
|
|
$bastion_details{'via'} = {addr => $bastionip, host => $bastionhost, port => $bastionport + 0, name => $bastionName};
|
|
$bastion_details{'to'} = {addr => $ip, host => $hostto, port => $port + 0, user => $user};
|
|
$bastion_details{'account'} = $self;
|
|
$bastion_details{'uniqid'} = $log_uniq_id;
|
|
$bastion_details{'version'} = $OVH::Bastion::VERSION;
|
|
|
|
if (!@command) {
|
|
main_exit OVH::Bastion::EXIT_UNKNOWN_COMMAND, "empty_command", "Found no command to execute!";
|
|
}
|
|
else {
|
|
|
|
# the '--' is to force ttyrec (started by us) to stop processing its options and execute the rest as is
|
|
push @ttyrec, '--', @command;
|
|
}
|
|
|
|
# add binding IP if specified
|
|
# works for ssh *and* telnet
|
|
if ($bind) {
|
|
push @command, '-b', $bind;
|
|
}
|
|
|
|
osh_debug("about to exec: " . join(' ', @ttyrec));
|
|
|
|
# if --wait is specified, we wait for the host to be alive before connecting
|
|
if ($wait) {
|
|
my $startedat = time();
|
|
osh_info "Pinging $host, will connect as soon as it's alive...";
|
|
while (1) {
|
|
my @pingcmd = qw{ fping -- };
|
|
push @pingcmd, $host;
|
|
|
|
my $fnretexec = OVH::Bastion::execute(cmd => \@pingcmd, noisy_stdout => 1, noisy_stderr => 1);
|
|
$fnretexec or exit(OVH::Bastion::EXIT_EXEC_FAILED);
|
|
if ($fnretexec->value->{'sysret'} == 0) {
|
|
osh_info "Alive after waiting for " . (time() - $startedat) . " seconds, connecting...";
|
|
sleep 2 if (time() > $startedat + 1); # so that ssh has the time to startup... hopefully
|
|
last;
|
|
}
|
|
sleep 1;
|
|
}
|
|
}
|
|
|
|
my $logret = OVH::Bastion::log_access_insert(
|
|
account => $self,
|
|
cmdtype => $telnet ? 'telnet' : 'ssh',
|
|
allowed => 1,
|
|
ipfrom => $ipfrom,
|
|
hostfrom => $hostfrom,
|
|
portfrom => $portfrom,
|
|
bastionhost => $bastionhost,
|
|
bastionip => $bastionip,
|
|
bastionport => $bastionport,
|
|
ipto => $ip,
|
|
hostto => $hostto,
|
|
portto => $port,
|
|
user => $user,
|
|
params => join(' ', @ttyrec),
|
|
ttyrecfile => $saveFile,
|
|
uniqid => $log_uniq_id
|
|
);
|
|
if (!$logret) {
|
|
osh_warn($logret);
|
|
if ($ip eq '127.0.0.1') {
|
|
osh_warn("Would deny access on out of space condition but you're root\@127.0.0.1, I hope you're here to fix me!");
|
|
}
|
|
else {
|
|
main_exit OVH::Bastion::EXIT_OUT_OF_SPACE, 'out_of_space', "Bastion is out of space, admin intervention is needed! (" . $logret->msg . ")";
|
|
}
|
|
$logret->{'value'} = {};
|
|
}
|
|
|
|
# if we have JIT MFA, do it now
|
|
if ($JITMFARequired) {
|
|
my $skipMFA = 0;
|
|
my $realmMFA = 0;
|
|
if ($proactiveMfa) {
|
|
print "As proactive MFA has been requested, entering MFA phase.\n";
|
|
}
|
|
else {
|
|
print "As this is required for this host, entering MFA phase.\n";
|
|
}
|
|
if ($JITMFARequired eq 'totp' && !$isMfaTOTPConfigured) {
|
|
if ($hasMfaTOTPBypass) {
|
|
$skipMFA = 1;
|
|
}
|
|
elsif ($remoteMfaTOTP && $remoteMfaValidated) {
|
|
$realmMFA = 1;
|
|
}
|
|
else {
|
|
main_exit(OVH::Bastion::EXIT_MFA_TOTP_SETUP_REQUIRED,
|
|
'mfa_totp_setup_required',
|
|
"Sorry, but you need to setup the Multi-Factor Authentication before connecting to this host,\nplease use the `--osh selfMFASetupTOTP' option to do so");
|
|
}
|
|
}
|
|
elsif ($JITMFARequired eq 'password' && !$isMfaPasswordConfigured) {
|
|
if ($hasMfaPasswordBypass) {
|
|
$skipMFA = 1;
|
|
}
|
|
elsif ($remoteMfaPassword && $remoteMfaValidated) {
|
|
$realmMFA = 1;
|
|
}
|
|
else {
|
|
main_exit(OVH::Bastion::EXIT_MFA_PASSWORD_SETUP_REQUIRED,
|
|
'mfa_password_setup_required',
|
|
"Sorry, but you need to setup the Multi-Factor Authentication before connecting to this host,\nplease use the `--osh selfMFASetupPassword' option to do so");
|
|
}
|
|
}
|
|
elsif ($JITMFARequired eq 'any' && !$isMfaTOTPConfigured && !$isMfaPasswordConfigured) {
|
|
if ($hasMfaPasswordBypass || $hasMfaTOTPBypass) {
|
|
|
|
# FIXME: should actually be $hasMFABypassAll (not yet implemented)
|
|
$skipMFA = 1;
|
|
}
|
|
elsif ($remoteMfaValidated) {
|
|
$realmMFA = 1;
|
|
}
|
|
else {
|
|
main_exit(OVH::Bastion::EXIT_MFA_ANY_SETUP_REQUIRED, 'mfa_any_setup_required',
|
|
"Sorry, but you need to setup the Multi-Factor Authentication before connecting to this host,\nplease use either the `--osh selfMFASetupPassword' or the `--osh selfMFASetupTOTP' option, at your discretion, to do so"
|
|
);
|
|
}
|
|
}
|
|
|
|
if ($skipMFA) {
|
|
print "... skipping as your account is exempt from MFA.\n";
|
|
}
|
|
elsif ($realmMFA) {
|
|
print "... you already validated MFA on the bastion you're coming from.\n";
|
|
}
|
|
elsif ($ENV{'OSH_PROACTIVE_MFA'}) {
|
|
print "... you already validated MFA proactively.\n";
|
|
}
|
|
else {
|
|
$fnret = OVH::Bastion::do_pamtester(self => $self, sysself => $sysself);
|
|
$fnret or main_exit(OVH::Bastion::EXIT_MFA_FAILED, 'mfa_failed', $fnret->msg);
|
|
|
|
# so that the remote server, which can be a bastion in case we're chaining, can enforce its own policy
|
|
$bastion_details{'mfa'} = {
|
|
validated => \1,
|
|
reason => ($proactiveMfa ? 'proactive_mfa' : 'mfa_required_for_host'),
|
|
type => {
|
|
password => $isMfaPasswordConfigured ? \1 : \0,
|
|
totp => $isMfaTOTPConfigured ? \1 : \0,
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
# now that we're about to connect, convert the bastion_details to a json envvar:
|
|
my @details_json = (\%bastion_details);
|
|
|
|
# if we have data from a previous bastion (due to a realm connection), include it on top:
|
|
push @details_json, @previous_bastion_details if @previous_bastion_details;
|
|
|
|
# then convert to json:
|
|
$ENV{'LC_BASTION_DETAILS'} = encode_json(\@details_json);
|
|
|
|
# here is a nice hack to drastically improve the memory footprint of an
|
|
# heavily used bastion. we exec() another script that is way lighter, see
|
|
# comments in the connect.pl file for more information.
|
|
|
|
if (!$quiet) {
|
|
print "Connecting...\n";
|
|
}
|
|
|
|
push @toExecute, $OVH::Bastion::BASEPATH . '/bin/shell/connect.pl';
|
|
exec(
|
|
@toExecute, $ip, $port, $config->{'sshClientHasOptionE'},
|
|
$userPasswordClue, $saveFile,
|
|
$logret->value->{'insert_id'},
|
|
$logret->value->{'db_name'},
|
|
$logret->value->{'uniq_id'},
|
|
$self, @ttyrec
|
|
) or exit(OVH::Bastion::EXIT_EXEC_FAILED);
|
|
|
|
exit OVH::Bastion::EXIT_OK;
|
|
|
|
#
|
|
# FUNCTIONS follow
|
|
#
|
|
|
|
#
|
|
# On SIG, still try to log in db
|
|
#
|
|
sub exit_sig {
|
|
my ($sig) = @_;
|
|
if (defined $log_insert_id and defined $log_db_name) {
|
|
OVH::Bastion::log_access_update(
|
|
account => $self,
|
|
insert_id => $log_insert_id,
|
|
db_name => $log_db_name,
|
|
uniq_id => $log_uniq_id,
|
|
signal => $sig,
|
|
);
|
|
}
|
|
exit OVH::Bastion::EXIT_OK;
|
|
}
|
|
|
|
#
|
|
# Display help message
|
|
#
|
|
sub help {
|
|
|
|
=cut just to debug memory fingerprint
|
|
use Devel::Size qw[total_size]; # pragma optional module
|
|
my %siz;
|
|
foreach (keys %::main::main::)
|
|
{
|
|
push @{ $siz{ total_size( $::main::main::{ $_ } ) } }, $_;
|
|
}
|
|
foreach (sort { $a <=> $b } keys %siz)
|
|
{
|
|
printf "%9d: %s\n", $_, join(' ', @{$siz{$_}});
|
|
}
|
|
exit OVH::Bastion::EXIT_OK;
|
|
=cut
|
|
|
|
print STDERR <<"EOF" ;
|
|
|
|
The Bastion v$OVH::Bastion::VERSION quick usage examples:
|
|
|
|
Connect to a server: $bastionName admin\@srv1.example.org
|
|
Run a command on a server: $bastionName admin\@srv1.example.org -- uname -a
|
|
|
|
List the osh commands: $bastionName --osh help
|
|
Help on a specific osh command: $bastionName --osh OSH_COMMAND --help
|
|
Enter interactive mode for osh: $bastionName -i
|
|
|
|
Get more complete help: $bastionName --long-help
|
|
|
|
EOF
|
|
return;
|
|
}
|
|
|
|
sub long_help {
|
|
print STDERR <<"EOF" ;
|
|
|
|
Usage (ssh): $bastionName [OPTIONS] [user\@]host [-- REMOTE_COMMAND]
|
|
Usage (telnet): $bastionName -e [OPTIONS] [user\@]host
|
|
Usage (osh cmd): $bastionName --osh [OSH_COMMAND] [OSH_OPTIONS]
|
|
|
|
[OPTIONS]
|
|
--host, -h HOST Host to connect to
|
|
--user, -u USER Remote host user to connect as
|
|
--port, -p PORT Port to use
|
|
--telnet, -e Use telnet instead of ssh
|
|
--timeout DELAY Specify a timeout for ssh or telnet egress connection
|
|
--bind IP Force binding of the egress ssh connection to a specified local IP
|
|
--password GROUP Use a group egress password instead of ssh keys to login (via ssh or telnet)
|
|
--self-password, -P Use your own personal account egress password instead of ssh keys to login (via ssh or telnet)
|
|
--osh Use an osh command (see --osh help to get a list)
|
|
--interactive, -i Enter interactive mode (useful to use multiple osh commands)
|
|
--quiet, -q Disable most messages and colors, useful for scripts
|
|
--always-escape Bypass config and force the bugged behavior of old bastions for REMOTE_COMMAND escaping. Don't use.
|
|
--never-escape Bypass config and force the new behavior of new bastions for REMOTE_COMMAND escaping. Don't use.
|
|
--wait Ping the host before connecting to it (useful to ssh just after a reboot!)
|
|
--long-help Print this
|
|
|
|
[REMOTE_COMMAND]
|
|
You can pass a command to execute on the remote machine. For complex commands, don't forget
|
|
that your shell will eat one level of quotes and backslashes. One working example:
|
|
$bastionName srv1.example.org -- "perl -e 'use Data::Dumper; print Dumper(\\\@ARGV)' one 'two is 2' three"
|
|
|
|
[OPTIONS (ssh)]
|
|
--verbose, -v Enable verbose ssh
|
|
--tty, -t Force tty allocation
|
|
--no-tty, -T Prevent tty allocation
|
|
--use-key FP Explicitly specify the fingerprint of the egress key you want to use
|
|
--kbd-interactive Enable the keyboard-interactive authentication scheme on egress connection
|
|
--netconf Request to use netconf subsystem
|
|
--fallback-password-delay S Amount of seconds to wait between subsequent tries in the SSH password autologin fallback mechanism (3).
|
|
|
|
[OPTIONS (osh cmd)]
|
|
--json Return data in json format between JSON_START and JSON_END tags
|
|
--json-pretty Prettify returned json, useful for debug / human reading
|
|
--json-greppable Return data in json format squashed on one line starting with JSON_DATA=
|
|
|
|
[OSH_COMMAND]
|
|
These are used to interact with the bastion configuration, accesses,
|
|
keys, accounts and groups. To get a list,
|
|
use: $bastionName --osh help
|
|
|
|
[OSH_OPTIONS]
|
|
Those options are specific for each OSH_COMMAND, to get help on those,
|
|
use: $bastionName --osh OSH_COMMAND --help
|
|
|
|
EOF
|
|
if (OVH::Bastion::is_admin(account => $self)) {
|
|
print STDERR <<"EOF" ;
|
|
[ADMIN_OPTIONS]
|
|
--ssh-as ACCOUNT Impersonate another account to ssh connect somewhere on his or her behalf. This is logged.
|
|
|
|
EOF
|
|
}
|
|
return;
|
|
}
|