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
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-12-10 01:03:10 +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
|
|
|
|
}
|
2021-12-10 01:03:10 +08:00
|
|
|
$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 {
|
2020-12-31 00:10:30 +08:00
|
|
|
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;
|
2023-10-02 16:39:56 +08:00
|
|
|
|
|
|
|
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
|
|
|
}
|
2023-10-02 16:39:56 +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(
|
2023-10-02 16:39:56 +08:00
|
|
|
"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(
|
2023-10-02 20:36:12 +08:00
|
|
|
<<"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) {
|
2023-10-02 20:36:12 +08:00
|
|
|
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';
|
2023-10-02 20:36:12 +08:00
|
|
|
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';
|
|
|
|
}
|
2022-11-30 21:16:34 +08:00
|
|
|
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) {
|
2021-03-20 01:50:14 +08:00
|
|
|
my $bastionName = OVH::Bastion::config('bastionName')->value;
|
2022-06-30 21:00:29 +08:00
|
|
|
OVH::Bastion::osh_crit(
|
2023-10-02 20:36:12 +08:00
|
|
|
"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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-20 01:50:14 +08:00
|
|
|
# if strict host key checking is enabled, be nice and explain how to remove this error
|
2022-11-30 21:16:34 +08:00
|
|
|
if ($header =~ /you have requested strict checking/) {
|
2020-10-16 00:32:37 +08:00
|
|
|
my $bastionName = OVH::Bastion::config('bastionName')->value;
|
2021-03-20 01:50:14 +08:00
|
|
|
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
|
|
|
}
|
|
|
|
}
|
2023-10-02 16:39:56 +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);
|