diff --git a/bin/admin/fixrights.sh b/bin/admin/fixrights.sh index f804a72..9048cbf 100755 --- a/bin/admin/fixrights.sh +++ b/bin/admin/fixrights.sh @@ -53,6 +53,8 @@ chmod 0755 "$basedir"/docker/entrypoint.sh \ "$basedir"/tests/functional/proxy/remote-daemon \ "$basedir"/tests/functional/fake_ttyrec.sh +find "$basedir"/contrib/nrpe/probes -type f -print0 | xargs -r0 chmod 0755 + find "$basedir"/tests/unit -type f -name "*.pl" -print0 | xargs -r0 chmod 0755 while IFS= read -r -d '' plugin diff --git a/contrib/nrpe/README.md b/contrib/nrpe/README.md new file mode 100644 index 0000000..1d85cf5 --- /dev/null +++ b/contrib/nrpe/README.md @@ -0,0 +1,15 @@ +NRPE Probes +=========== + +A few NRPE probes are available in the ``probes/`` subdirectory. + +Some of these probes might need to have elevated rights, an example of sudoers file is included. + +You might want to also use the nice ``check_logfiles`` probe, courtesy of +Consol Labs (https://labs.consol.de/nagios/check_logfiles/index.html), to ensure +that the cron scripts behave correctly and that no error is happening during the backup process, +the encrypt & rsync process, the HA synchronization daemon, etc. + +The configuration of the ``check_logfiles`` probe can be found in ``etc/nagios/plugins.d``. + +The bastion-side NRPE daemon configuration for these probes can be found in the ``etc/nagios/nrpe.d``. diff --git a/contrib/nrpe/etc/nagios/nrpe.d/check_bastion_healthcheck.cfg b/contrib/nrpe/etc/nagios/nrpe.d/check_bastion_healthcheck.cfg new file mode 100644 index 0000000..880e5fc --- /dev/null +++ b/contrib/nrpe/etc/nagios/nrpe.d/check_bastion_healthcheck.cfg @@ -0,0 +1 @@ +command[check_bastion_healthcheck]=/usr/bin/sudo -u healthcheck /opt/bastion/contrib/nrpe/probes/bastion-healthcheck --host 127.0.0.1 --port 22 --account healthcheck --keyfile /home/healthcheck/.ssh/id_healthcheck diff --git a/contrib/nrpe/etc/nagios/nrpe.d/check_bastion_http_proxy.cfg b/contrib/nrpe/etc/nagios/nrpe.d/check_bastion_http_proxy.cfg new file mode 100644 index 0000000..82dc234 --- /dev/null +++ b/contrib/nrpe/etc/nagios/nrpe.d/check_bastion_http_proxy.cfg @@ -0,0 +1 @@ +command[check_bastion_http_proxy]=/opt/bastion/contrib/nrpe/probes/bastion-http-proxy --host 127.0.0.1 --port 8443 --disabled-ok diff --git a/contrib/nrpe/etc/nagios/nrpe.d/check_bastion_locked.cfg b/contrib/nrpe/etc/nagios/nrpe.d/check_bastion_locked.cfg new file mode 100644 index 0000000..f19d2ea --- /dev/null +++ b/contrib/nrpe/etc/nagios/nrpe.d/check_bastion_locked.cfg @@ -0,0 +1 @@ +command[check_bastion_locked]=/opt/bastion/contrib/nrpe/probes/bastion-locked diff --git a/contrib/nrpe/etc/nagios/nrpe.d/check_bastion_root_connected_too_long.cfg b/contrib/nrpe/etc/nagios/nrpe.d/check_bastion_root_connected_too_long.cfg new file mode 100644 index 0000000..0444872 --- /dev/null +++ b/contrib/nrpe/etc/nagios/nrpe.d/check_bastion_root_connected_too_long.cfg @@ -0,0 +1 @@ +command[check_bastion_root_connected_too_long]=/opt/bastion/contrib/nrpe/probes/bastion-root-connected-too-long --warn-after-minutes 60 --crit-after-minutes 180 diff --git a/contrib/nrpe/etc/nagios/nrpe.d/check_bastion_sync_daemon.cfg b/contrib/nrpe/etc/nagios/nrpe.d/check_bastion_sync_daemon.cfg new file mode 100644 index 0000000..a04e843 --- /dev/null +++ b/contrib/nrpe/etc/nagios/nrpe.d/check_bastion_sync_daemon.cfg @@ -0,0 +1 @@ +command[check_bastion_sync_daemon]=/usr/lib/nagios/plugins/check_dummy 0 'osh-sync-watcher is not intended to run on slave bastions' diff --git a/contrib/nrpe/etc/nagios/nrpe.d/check_bastion_version.cfg b/contrib/nrpe/etc/nagios/nrpe.d/check_bastion_version.cfg new file mode 100644 index 0000000..9afcf75 --- /dev/null +++ b/contrib/nrpe/etc/nagios/nrpe.d/check_bastion_version.cfg @@ -0,0 +1 @@ +command[check_bastion_version]=/opt/bastion/contrib/nrpe/probes/bastion-version diff --git a/contrib/nrpe/etc/nagios/nrpe.d/check_log_bastion_backup.cfg b/contrib/nrpe/etc/nagios/nrpe.d/check_log_bastion_backup.cfg new file mode 100644 index 0000000..4611526 --- /dev/null +++ b/contrib/nrpe/etc/nagios/nrpe.d/check_log_bastion_backup.cfg @@ -0,0 +1 @@ +command[check_log_bastion_backup]=/usr/bin/sudo -u root /usr/lib/nagios/check_logfiles -f /etc/nagios/plugins.d/ovh/check_logfiles.cfg --searches=bastion_backup diff --git a/contrib/nrpe/etc/nagios/nrpe.d/check_log_bastion_encrypt_rsync.cfg b/contrib/nrpe/etc/nagios/nrpe.d/check_log_bastion_encrypt_rsync.cfg new file mode 100644 index 0000000..481fc6b --- /dev/null +++ b/contrib/nrpe/etc/nagios/nrpe.d/check_log_bastion_encrypt_rsync.cfg @@ -0,0 +1 @@ +command[check_log_bastion_encrypt_rsync]=/usr/bin/sudo -u root /usr/lib/nagios/check_logfiles -f /etc/nagios/plugins.d/ovh/check_logfiles.cfg --searches=bastion_encrypt_rsync diff --git a/contrib/nrpe/etc/nagios/nrpe.d/check_log_bastion_guest_key_cleanup.cfg b/contrib/nrpe/etc/nagios/nrpe.d/check_log_bastion_guest_key_cleanup.cfg new file mode 100644 index 0000000..b51f0cb --- /dev/null +++ b/contrib/nrpe/etc/nagios/nrpe.d/check_log_bastion_guest_key_cleanup.cfg @@ -0,0 +1 @@ +command[check_log_bastion_guest_key_cleanup]=/usr/bin/sudo -u root /usr/lib/nagios/check_logfiles -f /etc/nagios/plugins.d/ovh/check_logfiles.cfg --searches=bastion_guest_key_cleanup diff --git a/contrib/nrpe/etc/nagios/nrpe.d/check_log_bastion_misc.cfg b/contrib/nrpe/etc/nagios/nrpe.d/check_log_bastion_misc.cfg new file mode 100644 index 0000000..00fdcaf --- /dev/null +++ b/contrib/nrpe/etc/nagios/nrpe.d/check_log_bastion_misc.cfg @@ -0,0 +1 @@ +command[check_log_bastion_misc]=/usr/bin/sudo -u root /usr/lib/nagios/check_logfiles -f /etc/nagios/plugins.d/ovh/check_logfiles.cfg --searches=bastion_misc diff --git a/contrib/nrpe/etc/nagios/nrpe.d/check_log_bastion_orphaned_homedir.cfg b/contrib/nrpe/etc/nagios/nrpe.d/check_log_bastion_orphaned_homedir.cfg new file mode 100644 index 0000000..55a9a41 --- /dev/null +++ b/contrib/nrpe/etc/nagios/nrpe.d/check_log_bastion_orphaned_homedir.cfg @@ -0,0 +1 @@ +command[check_log_bastion_orphaned_homedir]=/usr/bin/sudo -u root /usr/lib/nagios/check_logfiles -f /etc/nagios/plugins.d/ovh/check_logfiles.cfg --searches=bastion_orphaned_homedir diff --git a/contrib/nrpe/etc/nagios/nrpe.d/check_log_bastion_piv_grace.cfg b/contrib/nrpe/etc/nagios/nrpe.d/check_log_bastion_piv_grace.cfg new file mode 100644 index 0000000..38eeca4 --- /dev/null +++ b/contrib/nrpe/etc/nagios/nrpe.d/check_log_bastion_piv_grace.cfg @@ -0,0 +1 @@ +command[check_log_bastion_piv_grace]=/usr/bin/sudo -u root /usr/lib/nagios/check_logfiles -f /etc/nagios/plugins.d/ovh/check_logfiles.cfg --searches=bastion_piv_grace diff --git a/contrib/nrpe/etc/nagios/nrpe.d/check_log_bastion_secondaries_sync.cfg b/contrib/nrpe/etc/nagios/nrpe.d/check_log_bastion_secondaries_sync.cfg new file mode 100644 index 0000000..e3fda77 --- /dev/null +++ b/contrib/nrpe/etc/nagios/nrpe.d/check_log_bastion_secondaries_sync.cfg @@ -0,0 +1 @@ +command[check_log_bastion_secondaries_sync]=/usr/bin/sudo -u root /usr/lib/nagios/check_logfiles -f /etc/nagios/plugins.d/ovh/check_logfiles.cfg --searches=bastion_secondaries_sync diff --git a/contrib/nrpe/etc/nagios/plugins.d/check_logfiles.cfg b/contrib/nrpe/etc/nagios/plugins.d/check_logfiles.cfg new file mode 100644 index 0000000..289f68f --- /dev/null +++ b/contrib/nrpe/etc/nagios/plugins.d/check_logfiles.cfg @@ -0,0 +1,56 @@ +# where the state information will be saved. +$seekfilesdir = '/var/cache/nagios'; + +# where protocols with found patterns will be stored. +$protocolsdir = $seekfilesdir; + +@searches = ( + { + tag => 'bastion_backup', + logfile => '/var/log/bastion/bastion-scripts.log', + criticalpatterns => ["will not be encrypted", "ERROR:"], + okpatterns => ["Done, got 0 error"], + options => 'allyoucaneat, sticky=86400, syslogclient=osh-backup-acl-keys.sh', + }, + { + tag => 'bastion_encrypt_rsync', + logfile => '/var/log/bastion/bastion-scripts.log', + criticalpatterns => ["ERROR:"], + okpatterns => ["Done, got 0 error"], + options => 'allyoucaneat, sticky=86400, syslogclient=osh-encrypt-rsync.pl', + }, + { + tag => 'bastion_orphaned_homedir', + logfile => '/var/log/bastion/bastion-scripts.log', + criticalpatterns => ["ERROR:"], + okpatterns => ["Done, got 0 error"], + options => 'allyoucaneat, sticky=900, syslogclient=osh-orphaned-homedir.sh', + }, + { + tag => 'bastion_piv_grace', + logfile => '/var/log/bastion/bastion-scripts.log', + criticalpatterns => ["ERROR:"], + okpatterns => ["Done, got 0 error"], + options => 'allyoucaneat, sticky=900, syslogclient=osh-piv-grace-reaper.pl', + }, + { + tag => 'bastion_guest_key_cleanup', + logfile => '/var/log/bastion/bastion-scripts.log', + criticalpatterns => ["ERROR:"], + okpatterns => ["Done, got 0 error"], + options => 'allyoucaneat, sticky=900, syslogclient=osh-cleanup-guest-key-access.pl', + }, + { + tag => 'bastion_misc', + logfile => '/var/log/bastion/bastion-scripts.log', + criticalpatterns => ["osh-lingering-sessions-reaper.sh.*ERROR:", "osh-rotate-ttyrec.sh.*ERROR:"], + options => 'allyoucaneat, sticky=900', + }, + { + tag => 'bastion_secondaries_sync', + logfile => '/var/log/bastion/bastion-scripts.log', + criticalpatterns => ["ERROR:"], + okpatterns => ["All secondaries have been synchronized successfully"], + options => 'allyoucaneat, sticky=900, syslogclient=osh-sync-watcher.sh, criticalthreshold=6', + }, +); diff --git a/contrib/nrpe/probes/bastion-healthcheck b/contrib/nrpe/probes/bastion-healthcheck new file mode 100755 index 0000000..15da09e --- /dev/null +++ b/contrib/nrpe/probes/bastion-healthcheck @@ -0,0 +1,107 @@ +#! /usr/bin/env perl +# vim: set filetype=perl ts=4 sw=4 sts=4 et: +# +# DESC: Check that the bastion code works (healtheck) + +use strict; +use warnings; +use File::Basename; +use Getopt::Long; + +my $PROBE_NAME = basename($0); +my $debug; + +## no critic (Subroutines::RequireArgUnpacking) +## no critic (Subroutines::RequireFinalReturn) + +sub _out { + my ($criticity, $msg) = @_; + printf "%s %4s - %s\n", $PROBE_NAME, $criticity, $msg; +} + +sub _dbg { _out('dbg', $_[0]) if $debug; } +sub _info { _out('info', $_[0]); } +sub _warn { _out('WARN', $_[0]); } +sub _err { _out('ERR!', $_[0]); } + +sub success { my $msg = shift; _info($msg) if $msg; _info("status=OK"); exit(0); } +sub warning { my $msg = shift; _warn($msg) if $msg; _info("status=WARN"); exit(1); } +sub failure { my $msg = shift; _err($msg) if $msg; _info("status=FAILURE"); exit(2); } +sub unknown { my $msg = shift; _err($msg) if $msg; _info("status=UNKNOWN"); exit(3); } + +# OPTIONS + +my $host = "127.0.0.1"; +my $port = 22; +my $account = 'healthcheck'; +my $keyfile = '/home/healthcheck/.ssh/id_healthcheck'; +my $kbdinteractive = 0; + +GetOptions( + "help" => \my $help, + "debug!" => \$debug, + "host=s" => \$host, + "port=i" => \$port, + "account=s" => \$account, + "keyfile=s" => \$keyfile, + "kbd-interactive" => \$kbdinteractive, +) or unknown("Failed parsing command-line"); + +# HELP + +if ($help) { + print <<"EOF"; + +$PROBE_NAME [options] + + --help This help message + --debug Increase verbosity of logs + --host HOST Host to connect to. Default: $host + --port PORT Port to connect to. Default: $port + --account ACCOUNT Account name to use to authenticate. Default: $account + --keyfile PATH Path to the private SSH key file to authenticate. Defaut: $keyfile + --kbd-interactive Allow keyboard-interactive authentication. Default: $kbdinteractive + + Note: don't specify an other option than --help to get the proper default values. + +EOF + unknown(); +} + +# CODE + +if ($account !~ /^[a-zA-Z0-9._-]+$/) { + unknown("Specified account is invalid ($account)"); +} + +if ($host !~ /^[a-zA-Z0-9._-]+$/) { + unknown("Specified host is invalid ($host)"); +} + +if ($port <= 0 || $port > 65535) { + unknown("Specified port is invalid ($port)"); +} + +if (!-f -r $keyfile) { + unknown("Specified keyfile '$keyfile' is not readable or not a file"); +} + +# first; check that sudo is healthy +_dbg("Checking sudo viability..."); +my $sysret = system(qw{ sudo -n -v }); +if ($sysret != 0) { + critical("sudo is broken!"); +} + +my @cmd = ('ssh', '-l', $account, '-i', $keyfile, '-p', $port); +if ($kbdinteractive) { + push @cmd, ('-o', 'KbdInteractiveAuthentication=yes', '-o', 'PreferredAuthentications=publickey,keyboard-interactive'); +} +push @cmd, ($host, '--', '-q', '--osh', 'info'); +_dbg("Executing: " . join(" ", @cmd)); + +$sysret = system(@cmd); +if ($sysret == 0) { + success("Connection worked and ended successfully"); +} +failure("Connection failed (SSH return code = " . ($sysret >> 8) . ")"); diff --git a/contrib/nrpe/probes/bastion-http-proxy b/contrib/nrpe/probes/bastion-http-proxy new file mode 100755 index 0000000..74a2009 --- /dev/null +++ b/contrib/nrpe/probes/bastion-http-proxy @@ -0,0 +1,133 @@ +#! /usr/bin/env perl +# vim: set filetype=perl ts=4 sw=4 sts=4 et: +# +# DESC: Warn if the bastion HTTPS proxy is down + +use strict; +use warnings; +use File::Basename; +use Getopt::Long; +use LWP::UserAgent; +use IO::Socket::SSL; +use JSON; + +my $PROBE_NAME = basename($0); +my $debug; + +## no critic (Subroutines::RequireArgUnpacking) +## no critic (Subroutines::RequireFinalReturn) + +sub _out { + my ($criticity, $msg) = @_; + printf "%s %4s - %s\n", $PROBE_NAME, $criticity, $msg; +} + +sub _dbg { _out('dbg', $_[0]) if $debug; } +sub _info { _out('info', $_[0]); } +sub _warn { _out('WARN', $_[0]); } +sub _err { _out('ERR!', $_[0]); } + +sub success { my $msg = shift; _info($msg) if $msg; _info("status=OK"); exit(0); } +sub warning { my $msg = shift; _warn($msg) if $msg; _info("status=WARN"); exit(1); } +sub failure { my $msg = shift; _err($msg) if $msg; _info("status=FAILURE"); exit(2); } +sub unknown { my $msg = shift; _err($msg) if $msg; _info("status=UNKNOWN"); exit(3); } + +# OPTIONS + +my $host = "127.0.0.1"; +my $DEFAULT_PORT = 8443; +my $disabledOk = 0; # don't warn if proxy is disabled +my $port; + +GetOptions( + "help" => \my $help, + "debug!" => \$debug, + "host=s" => \$host, + "port=i" => \$port, + "disabled-ok" => \$disabledOk, +) or unknown("Failed parsing command-line"); + +# attempt to get a better shot at the default port +my $json_data; +if (open(my $conf, "<", "/etc/bastion/osh-http-proxy.conf")) { + _dbg("opened https bastion config"); + local $/ = undef; + $json_data = <$conf>; + close($conf); + + $json_data =~ s/#.*//g; + my $json; + eval { $json = decode_json($json_data); }; + if ($@) { + _dbg("error decoding json ($@), keeping default port to $DEFAULT_PORT, and assuming proxy is enabled"); + $json->{'enabled'} = 1; + } + + # if config has a port and no port is specified on cmdline + if ($json->{'port'} && !$port) { + $port = $json->{'port'}; + _dbg("will use port $port as default, from config"); + } + + # proxy is disabled by config + if (!$json->{'enabled'}) { + if ($disabledOk) { + success("Proxy is disabled, and got --disabled-ok"); + } + else { + _warn("Proxy is disabled, but didn't get --disabled-ok, attempting to test nevertheless"); + } + } + + close($conf); +} +else { + if ($disabledOk) { + success("Specified --disabled-ok but couldn't find config file, assuming it's not installed"); + } + _dbg("Couldn't open https bastion config, keeping default port to $DEFAULT_PORT"); +} + +$port = $DEFAULT_PORT if not defined $port; + +# HELP + +if ($help) { + print <<"EOF"; + +$PROBE_NAME [options] + + --help This help message + --debug Increase verbosity of logs + --host HOST Host to connect to. Default: $host + --port PORT Port to connect to. Default: $port (tentatively + autodected from the HTTPS Bastion proxy configuration) + --disabled-ok Return success even if Proxy is disabled (from config) + +EOF + unknown(); +} + +# CODE + +# verify_hostname == 0 is ok because that's not what we're verifying here +my $ua = LWP::UserAgent->new( + agent => 'NRPE', + ssl_opts => { + verify_hostname => 0, + SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE + } +); +my $result = $ua->get("https://$host:$port/bastion-health-check"); + +_info("Got HTTP result code " . $result->code); + +if ($result->code == 200) { + success("> $_") for split /\n/, $result->decoded_content; +} +elsif ($result->code == 202) { + warning("> $_") for split /\n/, $result->decoded_content; # daemon should be reloaded +} +else { + failure("> $_") for split /\n/, $result->decoded_content; +} diff --git a/contrib/nrpe/probes/bastion-locked b/contrib/nrpe/probes/bastion-locked new file mode 100755 index 0000000..eb993d9 --- /dev/null +++ b/contrib/nrpe/probes/bastion-locked @@ -0,0 +1,60 @@ +#! /usr/bin/env perl +# vim: set filetype=perl ts=4 sw=4 sts=4 et: +# +# DESC: Warn if bastion is locked (/home encrypted) + +use strict; +use warnings; +use File::Basename; +use Getopt::Long; + +my $PROBE_NAME = basename($0); +my $debug; + +## no critic (Subroutines::RequireArgUnpacking) +## no critic (Subroutines::RequireFinalReturn) + +sub _out { + my ($criticity, $msg) = @_; + printf "%s %4s - %s\n", $PROBE_NAME, $criticity, $msg; +} + +sub _dbg { _out('dbg', $_[0]) if $debug; } +sub _info { _out('info', $_[0]); } +sub _warn { _out('WARN', $_[0]); } +sub _err { _out('ERR!', $_[0]); } + +sub success { my $msg = shift; _info($msg) if $msg; _info("status=OK"); exit(0); } +sub warning { my $msg = shift; _warn($msg) if $msg; _info("status=WARN"); exit(1); } +sub failure { my $msg = shift; _err($msg) if $msg; _info("status=FAILURE"); exit(2); } +sub unknown { my $msg = shift; _err($msg) if $msg; _info("status=UNKNOWN"); exit(3); } + +# OPTIONS + +GetOptions( + "help" => \my $help, + "debug!" => \$debug, +) or unknown("Failed parsing command-line"); + +# HELP + +if ($help) { + print <<"EOF"; + +$PROBE_NAME [options] + + --help This help message + --debug Increase verbosity of logs + +EOF + unknown(); +} + +# CODE + +if (-d "/home/allowkeeper") { + _dbg("/home/allowkeeper exists and is a directory"); + success("bastion /home is unlocked"); +} +_dbg("/home/allowkeeper doesn't exists or is not a directory"); +failure("bastion /home is locked!"); diff --git a/contrib/nrpe/probes/bastion-root-connected-too-long b/contrib/nrpe/probes/bastion-root-connected-too-long new file mode 100755 index 0000000..10a8f90 --- /dev/null +++ b/contrib/nrpe/probes/bastion-root-connected-too-long @@ -0,0 +1,192 @@ +#! /usr/bin/env perl +# vim: set filetype=perl ts=4 sw=4 sts=4 et: +# +# DESC: Check that a process with attached tty running as root +# is not there since more than X hours + +use strict; +use warnings; +use File::Basename; +use List::Util qw/first/; +use IPC::Open3 'open3'; +use Getopt::Long; +$SIG{'CHLD'} = 'IGNORE'; # don't bother using waitpid on this short-lived probe + +my $PROBE_NAME = basename($0); +my $debug; + +## no critic (Subroutines::RequireArgUnpacking) +## no critic (Subroutines::RequireFinalReturn) + +sub _out { + my ($criticity, $msg) = @_; + printf "%s %4s - %s\n", $PROBE_NAME, $criticity, $msg; +} + +sub _dbg { _out('dbg', $_[0]) if $debug; } +sub _info { _out('info', $_[0]); } +sub _warn { _out('WARN', $_[0]); } +sub _err { _out('ERR!', $_[0]); } + +sub success { my $msg = shift; _info($msg) if $msg; _info("status=OK"); exit(0); } +sub warning { my $msg = shift; _warn($msg) if $msg; _info("status=WARN"); exit(1); } +sub failure { my $msg = shift; _err($msg) if $msg; _info("status=FAILURE"); exit(2); } +sub unknown { my $msg = shift; _err($msg) if $msg; _info("status=UNKNOWN"); exit(3); } + +# OPTIONS + +my $warnAfterMinutes = 30; +my $critAfterMinutes = 120; + +GetOptions( + "help" => \my $help, + "debug!" => \$debug, + "warn-after-minutes=i" => \$warnAfterMinutes, + "crit-after-minutes=i" => \$critAfterMinutes, +) or unknown("Failed parsing command-line"); + +# HELP + +if ($help) { + print <<"EOF"; + +$PROBE_NAME [options] + + --help This help message + --debug Increase verbosity of logs + --warn-after-minutes NB Exit with a WARN exit code after a root process has been logged in for more than + this amount of minutes. Use 0 to never WARN. Default: $warnAfterMinutes + --crit-after-minutes NB Exit with a CRIT exit code after a root process has been logged in for more than + this amount of minutes. Use 0 to never CRIT. Default: $critAfterMinutes + + Note: don't specify an other option than --help to get the proper default values. + +EOF + unknown(); +} + +# CODE + +_dbg("Getting system clock tick"); +my ($stdin, $stdout); +eval { open3($stdin, $stdout, '>&STDERR', qw{ getconf CLK_TCK }); }; +if ($@) { + unknown("Couldn't start 'getconf' process"); +} +close($stdin); + +my $clockTick = <$stdout>; +close($stdout); +chomp($clockTick); +_dbg("clocktick is $clockTick"); + +_dbg("Getting uptime"); +open(my $fh, '<', "/proc/uptime") or unknown("Cannot open /proc/uptime: $!"); +my $uptimeData = <$fh>; +close $fh; +my $uptime; +if ($uptimeData =~ /^(\d+)/) { + $uptime = $1; +} +else { + unknown("Cannot parse uptime! '$uptimeData'"); +} + +_dbg("Uptime is $uptime seconds"); + +_dbg('Getting the list of processes that have a tty'); +$stdin = $stdout = undef; +eval { open3($stdin, $stdout, '>&STDERR', qw{ ps aho pid }); }; +if ($@) { + unknown("Couldn't start 'ps' process"); +} +close($stdin); +my @pidlist = <$stdout>; +close($stdout); +s/^\s+|\s+$//g for @pidlist; +_dbg('Found ' . (scalar @pidlist) . ' PIDs having a tty'); + +my $criticalCount = 0; +my $warningCount = 0; + +PID: foreach my $pid (@pidlist) { + next if $pid !~ /^\d+$/; + my $fh; + if (not open($fh, '<', "/proc/$pid/status")) { + _dbg("Couldn't open /proc/$pid/status ($!), probably a disappeared process (race condition)"); + next; + } + while (<$fh>) { + next if (not /^[UG]id:/); # parse Uid / Gid numbers + my ($id1, $id2, undef, $id4) = /(\d+)/g; + next PID if (not grep { $_ == 0 } ($id1, $id2, $id4)); # Root detected + _dbg("process $pid running as root, analyzing tty"); + + # Checking if exe is agetty, as it triggers the probe but is NOT a security issue + my $binary = readlink("/proc/$pid/exe"); + chomp($binary); + _dbg("Binary is $binary"); + + # The regex with 'deleted' handles upgrade of binaries, which are tagged deleted in proc/exe + next PID if ($binary =~ m{^(/usr)?/s?bin/agetty( \(deleted\))?$}); + next PID if ($binary =~ m{^/usr/bin/sudo( \(deleted\))?$}); + next PID if ($binary =~ m{^(/usr)?/bin/minijail0( \(deleted\))?$}); + + _dbg("check age of $pid"); + my $stat; + if (not open($stat, '<', "/proc/$pid/stat")) { + _dbg("couldn't open /proc/$pid/stat ($!), probably a disappeared process (race condition), getting to the next one"); + next PID; + } + my @stats = split(/\s+/, <$stat>); + close $stat; + + my $startTime = $stats[21]; # in %llu, number of clock ticks, see man 5 proc + my $processUptime = $uptime - ($startTime / $clockTick); + $processUptime = int($processUptime / 60); # minutes conversion + + _dbg("$pid Up since $processUptime minutes"); + + # Get guilty Admin + my $guilty = '??UNKNOWN??'; + if (open(my $envfh, "<", "/proc/$pid/environ")) { + my $guiltyEnv = first { /LC_BASTION=(\w+)/ } <$envfh>; + if (defined $guiltyEnv and $guiltyEnv =~ /LC_BASTION=(\w+)/) { + $guilty = $1; + } + close $envfh; + } + + # Get cmdline + my $cmdline = '??UNKNOWN??'; + if (open(my $envfh, "<", "/proc/$pid/cmdline")) { + $cmdline = <$envfh>; + chomp $cmdline; + close $envfh; + + # Just in case the cmdline contains sensitive info, we just keep the first word + $cmdline =~ s/ .+//; + } + + if ($critAfterMinutes > 0 && $processUptime > $critAfterMinutes) { + _info "Root process $pid ($cmdline) by $guilty, up for $processUptime minutes (> than $critAfterMinutes min)"; + $criticalCount += 1; + } + elsif ($warnAfterMinutes > 0 && $processUptime > $warnAfterMinutes) { + _info "Root process $pid ($cmdline) by $guilty, up for $processUptime minutes (> than $warnAfterMinutes min)"; + $warningCount += 1; + } + } + close $fh; +} + +# check Results +if ($criticalCount) { + failure("$criticalCount critical cases found"); +} +elsif ($warningCount) { + warning("$warningCount warning cases found"); +} + +# is ok +success("No long-lived root process found"); diff --git a/contrib/nrpe/probes/bastion-version b/contrib/nrpe/probes/bastion-version new file mode 100755 index 0000000..6f55eda --- /dev/null +++ b/contrib/nrpe/probes/bastion-version @@ -0,0 +1,131 @@ +#! /usr/bin/env perl +# vim: set filetype=perl ts=4 sw=4 sts=4 et: +# +# DESC: Warn if a more recent bastion version is available + +use strict; +use warnings; +use File::Basename; +use Getopt::Long; + +my $PROBE_NAME = basename($0); +my $debug; + +## no critic (Subroutines::RequireArgUnpacking) +## no critic (Subroutines::RequireFinalReturn) +## no critic (InputOutput::ProhibitBacktickOperators) + +sub _out { + my ($criticity, $msg) = @_; + printf "%s %4s - %s\n", $PROBE_NAME, $criticity, $msg; +} + +sub _dbg { _out('dbg', $_[0]) if $debug; } +sub _info { _out('info', $_[0]); } +sub _warn { _out('WARN', $_[0]); } +sub _err { _out('ERR!', $_[0]); } + +sub success { my $msg = shift; _info($msg) if $msg; _info("status=OK"); exit(0); } +sub warning { my $msg = shift; _warn($msg) if $msg; _info("status=WARN"); exit(1); } +sub failure { my $msg = shift; _err($msg) if $msg; _info("status=FAILURE"); exit(2); } +sub unknown { my $msg = shift; _err($msg) if $msg; _info("status=UNKNOWN"); exit(3); } + +# OPTIONS + +GetOptions( + "help" => \my $help, + "debug!" => \$debug, + "basedir=s" => \my $basedir, + "no-warn-on-diff" => \my $noWarnOnDiff, +) or unknown("Failed parsing command-line"); + +# HELP + +if ($help) { + print <<"EOF"; + +$PROBE_NAME [options] + + --help This help message + --debug Increase verbosity of logs + --basedir DIR Specify the base directory of The Bastion (default: /opt/bastion) + --no-warn-on-diff Never return a WARN code even if we find a git diff + +EOF + unknown(); +} + +$basedir ||= "/opt/bastion"; + +# CODE + +# get current version +my $current_version; +if (open(my $fh, '<', "$basedir/lib/perl/OVH/Bastion.pm")) { + while (<$fh>) { + if (m{^\s*our\s+\$VERSION\s*=\s*.([0-9a-zA-Z.-]+)}) { + $current_version = $1; + } + } + close($fh); + if ($current_version) { + _info("Bastion version $current_version found"); + } + else { + unknown("Couldn't find version in Bastion.pm file!"); + } +} +else { + unknown("Couldn't find current bastion version ($!)"); +} + +my @out; +my $ret; + +if (!chdir("$basedir")) { + unknown("Couldn't chdir to $basedir!"); +} + +@out = qx{git rev-parse --abbrev-ref HEAD}; +$ret = $?; +if ($ret != 0) { + _info("Bastion main path is not a git repo, or failed to rev-parse"); +} +else { + _dbg("output: $_") for @out; + my $branch = $out[0]; + chomp $branch; + _info("Bastion is on branch $branch"); +} + +@out = qx{git rev-parse HEAD}; +$ret = $?; +if ($ret != 0) { + _info("Bastion main path is not a git repo, or failed to rev-parse"); +} +else { + _dbg("output: $_") for @out; + my $commit = $out[0]; + chomp $commit; + _info("Bastion is on commit $commit"); +} + +@out = qx{git diff}; +if ($ret != 0) { + _info("Bastion main path is not a git repo, or failed to diff"); +} +else { + _dbg("output: $_") for @out; + my $difflines = @out; + if ($difflines > 0) { + if ($noWarnOnDiff) { + success("Found $difflines lines of diff"); + } + else { + warning("Found $difflines lines of diff"); + } + } + else { + success("Found no git diff"); + } +} diff --git a/contrib/nrpe/sudoers.example b/contrib/nrpe/sudoers.example new file mode 100644 index 0000000..34154b8 --- /dev/null +++ b/contrib/nrpe/sudoers.example @@ -0,0 +1,5 @@ +# This is a sudoers example file for The Bastion contrib NRPE probes. Adjust to your system. + +nagios ALL=(healthcheck) NOPASSWD: /opt/bastion/contrib/nrpe/probes/bastion-healthcheck +nagios ALL=(root) NOPASSWD: /opt/bastion/contrib/nrpe/probes/bastion-version +