the-bastion/bin/shell/connect.pl

266 lines
10 KiB
Perl
Raw Normal View History

2020-10-16 00:32:37 +08:00
#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;
# this line absolutely needs to be sync with the exec() of osh.pl
# that is launching us. we don't use GetOpts or such, as this is not
# user-modifiable anyway. We're mainly passing parameters we will need
# in this short script. some of them can sometimes be undef. this is normal.
2022-06-30 21:00:29 +08:00
my ($ip, $port, $sshClientHasOptionE, $userPasswordClue, $saveFile, $insert_id, $db_name, $uniq_id, $self, @command) =
@ARGV;
2020-10-16 00:32:37 +08:00
# on signal (HUP happens a lot), still try to log in db
sub exit_sig {
my ($sig) = @_;
if (defined $insert_id and defined $db_name) {
# at that point, we might not have required the proper libs yet, do it
require File::Basename;
require '' ## no critic (BarewordIncludes) ## I trust __FILE__, no worries
. File::Basename::dirname(__FILE__) . '/../../lib/perl/OVH/Bastion.pm';
# and log
OVH::Bastion::log_access_update(
feat: revamp logs All connections and plugin executions emit two logs, an 'open' and a 'close' log. We now add all the details of the connection to the 'close' logs, those that were previously only available in the corresponding 'open' log. This way, it is no longer required to correlate both logs with their uniqid to have all the data: the 'close' log should suffice. The 'open' log is still there if for some reason the 'close' log can't be emitted (kill -9, system crash, etc.), or if the 'open' and the 'close' log are several hours, days or months appart. An additional field "duration" has been added to the 'close' logs, this represents the number of seconds (with millisecond precision) the connection lasted. Two new fields "globalsql" and "accountsql" have been added to the 'open'-type logs. These will contain either "ok" if we successfully logged to the corresponding log database, "no" if it is disabled, or "error $aDetailedMessage" if we got an error trying to insert the row. The 'close'-type log also has the new "accountsql_close" field, but misses the "globalsql_close" field as we never update the global database on this event. On the 'close' log, we can also have the value "missing", indicating that we couldn't update the access log row in the database, as the corresponding 'open' log couldn't insert it. The "ttyrecsize" log field for the 'close'-type logs has been removed, as it was never completely implemented, and contains bogus data if ttyrec log rotation occurs. It has also been removed from the sqlite log databases. The 'open' and 'close' events are now pushed to our own log files, in addition to syslog, if logging to those files is enabled (see ``enableGlobalAccesssLog`` and ``enableAccountAccessLog``), previously the 'close' events were only pushed to syslog. The /home/osh.log is no longer used for ``enableGlobalAccessLog``, the global log is instead written to /home/logkeeper/global-log-YYYYMM.log. The global sql file, enabled with ``enableGlobalSqlLog``, is now split by year-month instead of by year, to /home/logkeeper/global-log-YYYYMM.sqlite.
2020-12-15 23:39:20 +08:00
account => $self,
insert_id => $insert_id,
db_name => $db_name,
uniq_id => $uniq_id,
signal => $sig,
2020-10-16 00:32:37 +08:00
);
}
# nullify my own handlers so that they don't get re-executed when my parent
# exits because of the signal, and I get sent back a SIGHUP (see Prctl below)
$SIG{$_} = 'IGNORE' for qw{ INT HUP TERM SEGV };
2020-10-16 00:32:37 +08:00
# signal my current process group
kill $sig, 0;
exit(117); # EXIT_GOT_SIGNAL
}
$SIG{$_} = \&exit_sig for qw{ INT HUP TERM SEGV };
2020-10-16 00:32:37 +08:00
# beautify for ps
local $0 = '' . __FILE__ . ' ' . join(' ', @command);
# set signal for when my parent dies (Linux only)
eval {
require Linux::Prctl; # pragma optional module
2020-10-16 00:32:37 +08:00
# 1 is SIGHUP
Linux::Prctl::set_pdeathsig(1);
};
# As we're going to system() something passed to us via @ARGV,
# we want to be sure we're being called by something we know.
# Yes. I'm fucking paranoid.
if (open(my $fh, '<', "/proc/" . getppid() . '/cmdline')) {
my $cmdline = do { local $/ = undef; <$fh> };
close($fh);
my @pargv = split(/\x00/, $cmdline);
# now check our parent infos.
# regular case: ssh
if (@pargv == 1 and $pargv[0] =~ /^sshd: /) {
; # ok, our parent is sshd, legitimate use
}
# pingssh case
elsif (@pargv == 4 and $pargv[0] =~ m{/perl$} and $pargv[1] =~ m{/osh\.pl} and $pargv[2] eq '-c') {
; # ok pingssh case
}
# admin debug case: local su
elsif (@pargv == 5 and $pargv[0] eq 'su' and $pargv[1] eq '-l' and $pargv[3] eq '-c') {
2022-06-30 21:00:29 +08:00
print STDERR "\n\nHmm, hijack of "
. $pargv[2]
. " by root detected... debug I guess... okay, but it's really because it's you.\n\n";
2020-10-16 00:32:37 +08:00
}
# mosh
elsif ($pargv[0] eq 'mosh-server') {
; # we're being called by mosh-server, alrighty
}
# clush plugin
elsif ($pargv[1] =~ m{^/opt/bastion/bin/plugin/(open|restricted)/clush$}) {
; # we're being called by the clush plugin, ok
}
# interactive mode: our parent is osh.pl
elsif ($pargv[0] eq 'perl' and $pargv[1] eq '/opt/bastion/bin/shell/osh.pl') {
; # we're being called by the interactive mode of osh.pl, ok
}
# else: it sucks.
else {
#foreach (@pargv) { print "<".$_.">\n" };
die("SECURITY VIOLATION, ABORTING.");
}
}
else {
; # grsec can deny us this. if that's the case, nevermind ... bypass this check
}
# in any case, force this
2020-11-17 21:57:34 +08:00
if (-e '/usr/local/bin/ttyrec') {
$command[0] = '/usr/local/bin/ttyrec';
}
else {
$command[0] = '/usr/bin/ttyrec';
}
2020-10-16 00:32:37 +08:00
# then finally launch the command !
my $sysret = system(@command);
# ... days or months may have passed once we arrive here, which is
# why we only used common::sense above (which is known to be light).
# using other packages would just waste memory for months as we would
# only really use them AFTER the command above has exited.
# so. now, we can require those files we need, rejoice, we have
# saved a lot of RAM in the meantime !
# special case for Time::HiRes, use a `use' instead of a `require'
# in an attempt to fix a strange 'Undefined subroutine &Time::HiRes::gettimeofday'
# that happens one every 10K connections or so
use Time::HiRes qw{ gettimeofday };
my ($timestamp, $timestampusec) = gettimeofday();
require File::Basename;
require '' ## no critic (BarewordIncludes) ## I trust __FILE__, no worries
. File::Basename::dirname(__FILE__) . '/../../lib/perl/OVH/Bastion.pm';
# ssh -E also silences normal errors on console, print them eventually
if ($sshClientHasOptionE) {
if (open(my $sshdebug, '<', $saveFile . '.sshdebug')) {
while (<$sshdebug>) {
print
2022-06-30 21:00:29 +08:00
unless
/^debug|^key_load_public:|OpenSSL|^Authenticated to|^Transferred:|^Bytes per second:|^\s*$|client-session/;
2020-10-16 00:32:37 +08:00
}
close($sshdebug);
}
}
# now guessify if the ssh worked or not
my @comments;
my $header;
if (-e $saveFile) {
if (-z _) {
push @comments, 'ttyrec_empty';
}
else {
if (open(my $fh_ttyrec, '<', $saveFile)) {
read $fh_ttyrec, $header, 2000; # 2K if there's the host key changed warning
close($fh_ttyrec);
}
}
2020-10-16 00:32:37 +08:00
}
elsif (-e "$saveFile.zst") {
if (-z _) {
push @comments, 'ttyrec_empty';
}
else {
my $fnret = OVH::Bastion::execute(
cmd => ['zstd', '-d', '-c', "$saveFile.zst"],
max_stdout_bytes => 2000,
must_succeed => 1
);
$header = join("\n", @{$fnret->value->{'stdout'} || []}) if $fnret;
}
}
else {
push @comments, 'ttyrec_none';
2020-10-16 00:32:37 +08:00
}
if ($header) {
if ($header =~ /Permission denied \(publickey/) {
push @comments, 'permission_denied';
2022-06-30 21:00:29 +08:00
OVH::Bastion::osh_crit(
"BASTION SAYS: The remote server ($ip) refused all the keys we tried (see the list just above), "
. "there are FOUR things to verify:");
2020-10-16 00:32:37 +08:00
OVH::Bastion::osh_warn(
<<"EOS"
1) Check the remote account's authorized_keys on $ip, did you add the proper key there? (personal key or group key)
2) Did you tell the bastion you added a key to the remote server, so it knows it has to use it? See the actually used keys just above. If you didn't, do it with selfAddPersonalAccess or groupAddServer.
3) Check the from="" part of the remote account's authorized_keys' keyline. Are all the bastion IPs present? Master and slave(s)? See groupInfo or selfListEgressKeys to get the proper keyline to copy/paste.
4) Did you check the 3 above points carefully? Really? Because if you did, you wouldn't be reading this 4th bullet point, as your problem would already be fixed ;)
EOS
2020-10-16 00:32:37 +08:00
);
}
if ($header =~ /Permission denied \(keyboard-interactive/) {
push @comments, 'permission_denied';
2021-09-02 16:21:34 +08:00
if (!OVH::Bastion::config('keyboardInteractiveAllowed')->value) {
OVH::Bastion::osh_crit("BASTION SAYS: The remote server ($ip) wanted to use keyboard-interactive "
. "authentication, but it's not enabled on this bastion!");
2021-09-02 16:21:34 +08:00
}
2020-10-16 00:32:37 +08:00
}
if ($header =~ /Too many authentication failures/) {
push @comments, 'too_many_auth_fail';
OVH::Bastion::osh_crit("BASTION SAYS: The remote server ($ip) disconnected us before we got "
. "a chance to try all the keys we wanted to (see the list just above).");
OVH::Bastion::osh_warn("This usually happens if there are too many keys to try, for example if you have "
. "numerous personal keys of if $ip is in many groups you have access to.");
OVH::Bastion::osh_warn("Either reduce the number of keys to try, or modify $ip\'s "
. "sshd \"MaxAuthTries\" configuration option.");
2020-10-16 00:32:37 +08:00
}
push @comments, 'hostkey_changed' if $header =~ /IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY/;
push @comments, 'hostkey_saved' if $header =~ /Warning: Permanently added /;
if ($header =~ /ssh: connect to host \S+ port \d+: Connection timed out/) {
push @comments, 'connection_timeout';
}
elsif ($header =~ /ssh: connect to host \S+ port \d+: Connection refused/) {
push @comments, 'connection_refused';
}
elsif ($header =~ /ssh: connect to host \S+ port \d+: /) {
push @comments, 'connection_error';
}
elsif ($header =~ /authentication is disabled to avoid man-in-the-middle attacks/) {
2020-10-16 00:32:37 +08:00
push @comments, 'passauth_disabled';
# be nice and explain to the user cf ticket BASTION-10
if ($userPasswordClue) {
my $bastionName = OVH::Bastion::config('bastionName')->value;
2022-06-30 21:00:29 +08:00
OVH::Bastion::osh_crit(
"BASTION SAYS: Password authentication is blocked " . "because of the hostkey mismatch on $ip.");
OVH::Bastion::osh_crit("If you are aware of this change, remove the hostkey cache "
. "with `$bastionName --osh selfForgetHostKey --host $ip --port $port'");
2020-10-16 00:32:37 +08:00
}
}
# if strict host key checking is enabled, be nice and explain how to remove this error
if ($header =~ /you have requested strict checking/) {
2020-10-16 00:32:37 +08:00
my $bastionName = OVH::Bastion::config('bastionName')->value;
OVH::Bastion::osh_crit("BASTION SAYS: Connection has been blocked because of the hostkey mismatch on $ip.");
2022-06-30 21:00:29 +08:00
OVH::Bastion::osh_crit(
"If you are aware of this change, remove the hostkey cache with `$bastionName --osh selfForgetHostKey --host $ip --port $port'"
);
2020-10-16 00:32:37 +08:00
}
}
elsif (!@comments) {
# if $header is empty and we didn't push ttyrec_none or ttyrec_empty to @comments, it's weird
2020-10-16 00:32:37 +08:00
push @comments, 'ttyrec_error';
}
# update our sql line if we successfully inserted it back in osh.pl
OVH::Bastion::log_access_update(
feat: revamp logs All connections and plugin executions emit two logs, an 'open' and a 'close' log. We now add all the details of the connection to the 'close' logs, those that were previously only available in the corresponding 'open' log. This way, it is no longer required to correlate both logs with their uniqid to have all the data: the 'close' log should suffice. The 'open' log is still there if for some reason the 'close' log can't be emitted (kill -9, system crash, etc.), or if the 'open' and the 'close' log are several hours, days or months appart. An additional field "duration" has been added to the 'close' logs, this represents the number of seconds (with millisecond precision) the connection lasted. Two new fields "globalsql" and "accountsql" have been added to the 'open'-type logs. These will contain either "ok" if we successfully logged to the corresponding log database, "no" if it is disabled, or "error $aDetailedMessage" if we got an error trying to insert the row. The 'close'-type log also has the new "accountsql_close" field, but misses the "globalsql_close" field as we never update the global database on this event. On the 'close' log, we can also have the value "missing", indicating that we couldn't update the access log row in the database, as the corresponding 'open' log couldn't insert it. The "ttyrecsize" log field for the 'close'-type logs has been removed, as it was never completely implemented, and contains bogus data if ttyrec log rotation occurs. It has also been removed from the sqlite log databases. The 'open' and 'close' events are now pushed to our own log files, in addition to syslog, if logging to those files is enabled (see ``enableGlobalAccesssLog`` and ``enableAccountAccessLog``), previously the 'close' events were only pushed to syslog. The /home/osh.log is no longer used for ``enableGlobalAccessLog``, the global log is instead written to /home/logkeeper/global-log-YYYYMM.log. The global sql file, enabled with ``enableGlobalSqlLog``, is now split by year-month instead of by year, to /home/logkeeper/global-log-YYYYMM.sqlite.
2020-12-15 23:39:20 +08:00
account => $self,
2020-10-16 00:32:37 +08:00
insert_id => $insert_id,
db_name => $db_name,
uniq_id => $uniq_id,
returnvalue => $sysret,
comment => @comments ? join(' ', @comments) : undef,
timestampend => $timestamp,
timestampendusec => $timestampusec,
);
if ($sysret == -1) {
OVH::Bastion::osh_crit("Couldn't start " . join('|', @command) . ($! ? " ($!)" : ", is it installed?"));
exit($sysret);
}
exit($sysret >> 8);