Current Path : /usr/sbin/ |
Current File : //usr/sbin/munin-run |
#!/usr/bin/perl -T # -*- perl -*- # # Copyright and license: see bottom of file # use strict; use warnings; # Trust PERL5LIB from environment use lib map { /(.*)/ } split(/:/, ($ENV{PERL5LIB} || '')); use English qw(-no_match_vars); use File::Temp; use Getopt::Long; use Munin::Common::Defaults; use Munin::Node::Config; use Munin::Node::OS; use Munin::Node::Service; my $services; my $servicedir; my $conffile = "$Munin::Common::Defaults::MUNIN_CONFDIR/munin-node.conf"; my $DEBUG = 0; my $PIDEBUG = 0; my $paranoia = 0; my $ignore_systemd_properties = 0; # The following parameters of "systemd-run" require rather recent systemd versions: # --wait: 232 or later # --pipe: 235 or later # --collect: 236 or later my $REQUIRED_SYSTEMD_VERSION = 999; # See "man systemd.exec" for the list of all systemd properties. # The following properties belong to the relevant sections "Capabilities", # "Security", "Mandatory Access Control", "Process Properties", # "Sandboxing" and "System Call Filtering". # These properties will be imported from the specification of # "munin-node.service" if systemd is enabled. # See "--ignore-systemd-properties" for details. # See "get_systemd_hardening_flags" for a few exceptions from the list below. my @SYSTEMD_PROPERTY_IMPORT_PATTERNS = qw( AmbientCapabilities AppArmorProfile CapabilityBoundingSet DynamicUser Environment EnvironmentFile Group Limit\w+ MemoryDenyWriteExecute MountFlags NetworkNamespacePath NoNewPrivileges PassEnvironment Private\w+ Protect\w+ Restrict\w+ SecureBits SELinuxContext SmackProcessLabel SystemCallArchitectures SystemCallFilter TemporaryFileSystem UMask UnsetEnvironment User \w+Directory \w+Paths ); # The following environment variables are assigned automatically by # systemd-run (see "man systemd.exec"). We should not override them # when calling "systemd-run". # See "--ignore-systemd-properties" for details. my %ENVIRONMENT_IGNORE_HASH = map { $_ => 1 } qw( PATH LANG USER HOME SHELL LOGNAME INVOCATION_ID XDG_RUNTIME_DIR RUNTIME_DIRECTORY STATE_DIRECTORY CACHE_DIRECTORY LOGS_DIRECTORY CONFIGURATION_DIRECTORY MAINPID MANAGERPID LISTEN_FDS LISTEN_PID LISTEN_FDNAMES NOTIFY_SOCKET WATCHDOG_PID WATCHDOG_USEC TERM JOURNAL_STREAM SERVICE_RESULT EXIT_CODE EXIT_STATUS PIDFILE ); my $config = Munin::Node::Config->instance(); sub main { # "Clean" environment to disable taint-checking on the environment. We _know_ # that the environment is insecure, but we want to let admins shoot themselves # in the foot with it, if they want to. foreach my $key (keys %ENV) { $ENV{$key} =~ /^(.*)$/s; $ENV{$key} = $1; } $0 =~ /^(.*)$/; $0 = $1; my @original_argv = @ARGV; my ($plugin, $arg) = parse_args(); # Loads the settings from munin-node.conf. # Ensures that, where options can be set both in the config and in # @ARGV, the latter takes precedence. $paranoia = $config->{paranoia}; my $config = Munin::Node::Config->instance(); $config->parse_config_from_file($conffile); # Run directly or execute recursively via "systemd-run". if (($ignore_systemd_properties) || (! -d "/run/systemd/system")) { return execute_plugin($plugin, $arg); } elsif (!check_systemd_run_permissions()) { print STDERR "# Skipping systemd properties simulation due to lack of permissions.\n" if $config->{DEBUG}; return execute_plugin($plugin, $arg); } else { my $systemd_version = get_systemd_version(); if ((not defined $systemd_version) or ($systemd_version < $REQUIRED_SYSTEMD_VERSION)) { print STDERR "# Skipping systemd properties simulation due to required systemd version ($REQUIRED_SYSTEMD_VERSION)\n" if $config->{DEBUG}; return execute_plugin($plugin, $arg); } else { my @munin_node_hardening_flags; my $parse_flags_success = 0; eval { @munin_node_hardening_flags = get_systemd_hardening_flags(); $parse_flags_success = 1; }; if ($parse_flags_success) { return run_via_systemd(\@munin_node_hardening_flags, \@original_argv, $config->{DEBUG}); } else { # Failed to retrieve systemd properties of munin-node service. # Probable causes: systemd is not installed/enabled or the # service unit does not exist. return execute_plugin($plugin, $arg); } } } } sub check_systemd_run_permissions { # verify whether systemd-run can be exected (e.g. unprivileged users cannot execute it) return system("systemd-run --collect --pipe --quiet --wait -- true </dev/null >/dev/null 2>&1") == 0; } # Retrieve the locally configured hardening flags for the "munin-node" systemd # service. # The result of the function is a list of strings like the following: # ProtectHome=yes sub get_systemd_hardening_flags { # retrieve all active properties except for soft (runtime) limits my @munin_service_properties = grep !/^Limit\w+Soft=/, `systemctl show munin-node 2>/dev/null`; die "no systemd enabled or failed to retrieve unit properties" unless ($CHILD_ERROR >> 8 == 0); my $flag_name_regex = '^((?:' . join("|", @SYSTEMD_PROPERTY_IMPORT_PATTERNS) . ')=.*)$'; my @flag_list; foreach my $property_definition (@munin_service_properties) { # The effect of files referenced in "DropInPaths" (e.g. files overriding the properties of # a service) is already applied to the output of "systemd-show". # Thus we can safely ignore this property (which is not accepted by "systemd-run" anyway). next if ($property_definition =~ /^DropInPaths=/); # "systemd show" does not output the EnvironmentFile property in a readable format. # See https://github.com/systemd/systemd/issues/14723. next if ($property_definition =~ /^EnvironmentFiles?=(.*) \(ignore_errors=(yes|no)\)$/); next if ($property_definition =~ /^RestrictSUIDSGID=/); push @flag_list, $1 if $property_definition =~ /$flag_name_regex/; } return @flag_list; } # "man systemd.exec" describes the quoting rules for EnvironmentFile. # We apply the following steps: # 1) escape all double quotes with a backslash # 2) surround the value with double quotes # This combination ensures that even line breaks are properly parsed by # systemd-run. sub quote_for_systemd_environment_file { my ($key, $value) = @_; # escape double quotes $value =~ s/"/\\"/; return $key . '="' . $value . "\"\n"; } sub get_systemd_version { my @version_output = `systemd-run --version 2>/dev/null`; foreach my $line (@version_output) { if ($line =~ /^systemd(?:-run)?\s+(\d+).*$/) { return int($1); } } return; } # Recursively execute this script ("munin-run") via "systemd-run". # This allows to apply the hardening properties defined in "munin-node.service". # Thus the behavior of "munin-run" should be the same as the behavior of # munin-node service itself. This is less surprising for users. sub run_via_systemd { my ($systemd_properties_ref, $original_argv_ref, $debug_enabled) = @_; my @call_args; push @call_args, "systemd-run"; # discard the transient service even in case of errors push @call_args, "--collect"; # use our stdin/stdout/stderr for the created process push @call_args, "--pipe"; push @call_args, "--quiet"; # wait for the end of the command execution push @call_args, "--wait"; # Preserve the environment (e.g. manual plugin configuration applied by # the user). # We use systemd-run's property "EnvironmentFile" for transferring the # environment of the current process to the new process. The following # simpler approaches ("properties") are not suitable: # * Environment: would expose the private environment of the calling # user to all other users (via the commandline). # * PassEnvironment: the variables are only transferred from the # system manager (PID 1) instead of the calling process. # This approach causes a problem my $environment_file = File::Temp->new(); # The order of systemd-run's environment variable processing may cause # problems, if "munin-node.service" specifies "Environment" properties, # which exist in the caller's environment. Such variables (being written # to the temporary EnvironmentFile) take precedence over the ones defined # in "munin-node.service". There does not seem to be a clean generic # workaround for this issue. foreach my $key (keys %ENV) { next if exists($ENVIRONMENT_IGNORE_HASH{$key}); print $environment_file quote_for_systemd_environment_file($key, $ENV{$key}); } push @call_args, "--property"; push @call_args, "EnvironmentFile=" . $environment_file->filename; # enable the hardening flags of the munin-node service foreach my $key_value (@$systemd_properties_ref) { push @call_args, "--property"; push @call_args, $key_value; } push @call_args, "--"; # append the untainted name/path of "munin-run" itself $0 =~ /^(.*)$/s; push @call_args, $1; push @call_args, "--ignore-systemd-properties"; foreach my $arg (@$original_argv_ref) { # untaint our arguments $arg =~ /^(.*)$/s; push @call_args, $1; } if ($debug_enabled) { print STDERR ("# Running 'munin-run' via 'systemd-run' with systemd " . "properties based on 'munin-node.service'.\n"); my $command_printable = ""; foreach my $token (@call_args) { $command_printable .= " " if ($command_printable); if ($token =~ /\s/) { $command_printable .= "'$token'"; } else { $command_printable .= "$token"; } } print STDERR "# Command invocation: $command_printable\n"; } # We need to use "system" instead of "exec in order to remove the EnvironmentFile # afterwards. This is indirectly handled by the object cleanup from File::Temp. my $result = system(@call_args); if ($result == -1) { die "Failed to execute the 'systemd-run' wrapper. Maybe try '--ignore-systemd-properties'."; } else { my $exitcode = $result >> 8; if ($exitcode != 0) { # Sadly problems with "systemd-run" are only visible in the log (no error output). print STDERR ("Warning: the execution of 'munin-run' via 'systemd-run' returned an " . "error. This may either be caused by a problem with the plugin to be " . "executed or a failure of the 'systemd-run' wrapper. Details of the " . "latter can be found via 'journalctl'.\n"); } return $exitcode; } } sub execute_plugin { my ($plugin, $arg) = @_; $services = Munin::Node::Service->new( servicedir => $servicedir, defuser => $config->{defuser}, defgroup => $config->{defgroup}, pidebug => $PIDEBUG, ); $config->reinitialize({ %$config, paranoia => $paranoia, }); unless ($services->is_a_runnable_service($plugin)) { print STDERR "# Unknown service '$plugin'\n"; exit 1; } $services->prepare_plugin_environment($plugin); # no need for a timeout -- the user can kill this process any # time they want. $services->exec_service($plugin, $arg); # Never reached, but just in case... print STDERR "# FATAL: Failed to exec.\n"; exit 42; } sub parse_args { # Default configuration values my $sconfdir = "$Munin::Common::Defaults::MUNIN_CONFDIR/plugin-conf.d"; my $sconffile; my ($plugin, $arg); print_usage_and_exit() unless GetOptions( "config=s" => \$conffile, "debug!" => \$DEBUG, "pidebug!" => \$PIDEBUG, "servicedir=s" => \$servicedir, "sconfdir=s" => \$sconfdir, "sconffile=s" => \$sconffile, "paranoia!" => \$paranoia, "ignore-systemd-properties" => \$ignore_systemd_properties, "version" => \&print_version_and_exit, "help" => \&print_usage_and_exit, ); print_usage_and_exit() unless $ARGV[0]; # Detaint the plugin name ($plugin) = ($ARGV[0] =~ m/^([-\w.:]+)$/) or die "# ERROR: Invalid plugin name '$ARGV[0].\n"; if ($ARGV[1]) { ($arg) = ($ARGV[1] =~ m/^(\w+)$/) or die "# ERROR: Invalid characters in argument '$ARGV[1]'.\n"; } # Detaint service directory. FIXME: do more strict detainting? if ($servicedir) { $servicedir =~ /(.*)/; $servicedir = $1; } # Update the config $config->reinitialize({ %$config, sconfdir => $sconfdir, conffile => $conffile, sconffile => $sconffile, DEBUG => $DEBUG, paranoia => $paranoia, }); return ($plugin, $arg); } sub print_usage_and_exit { require Pod::Usage; Pod::Usage::pod2usage(-verbose => 1); } sub print_version_and_exit { require Pod::Usage; Pod::Usage::pod2usage( -verbose => 99, -sections => 'VERSION|COPYRIGHT', ); } exit main() unless caller; 1; __END__ =head1 NAME munin-run - A program to run Munin plugins from the command line =head1 SYNOPSIS munin-run [options] <plugin> [ config | autoconf | snmpconf | suggest ] =head1 DESCRIPTION munin-run is a script to run Munin plugins from the command-line. It's useful when debugging plugins, as they are run in the same conditions as they are under munin-node. =head1 OPTIONS =over 5 =item B<< --config <configfile> >> Use E<lt>fileE<gt> as configuration file. [/etc/munin/munin-node.conf] =item B<< --servicedir <dir> >> Use E<lt>dirE<gt> as plugin dir. [/etc/munin/plugins/] =item B<< --sconfdir <dir> >> Use E<lt>dirE<gt> as plugin configuration dir. [/etc/munin/plugin-conf.d/] =item B<< --sconffile <file> >> Use E<lt>fileE<gt> as plugin configuration. Overrides sconfdir. [undefined] =item B<--paranoia > Only run plugins owned by root and check permissions. [disabled] =item B<--ignore-systemd-properties > Do not try to detect and enforce the locally configured hardening flags of the "munin-node" service unit. This detection is skipped, if systemd is not enabled. The hardening flags may cause subtile surprises. For example "ProtectHome=yes" prevents the "df" plugin from determining the state of the "home" partition. [disabled] =item B<--help > View this help message. =item B<--debug > Print debug messages. Debug messages are sent to STDOUT and are prefixed with "#" (this makes it easier for other parts of munin to use munin-run and still have --debug on). Only errors go to STDERR. =item B<--pidebug > Plugin debug. Sets the environment variable MUNIN_DEBUG to 1 so that plugins may enable debugging. [disabled] =item B<--version > Show version information. =back =head1 NOTES FOR SYSTEMD USERS The "munin-node" service is usually started by systemd via a "munin-node.service" definition. Some distributions enable hardening settings in this service file in order to restrict the allowed set of activities for the "munin-node" process. This may cause surprising differences between the result of "munin-run" and the real "munin-node" service. A popular example of such a surprising restriction is "ProtectHome=yes" combined with the "df" plugin. The restriction silently prevents the plugin from determining the status of mountpoints below /home. "munin-run" tries to mimic this behavior of "munin-node" automatically. Thus the execution of "munin-run df" should provide the same output as "echo fetch df | nc localhost munin". If you want to debug potential issues of systemd restrictions, then you may want to use the parameters "--ignore-systemd-properties" and "--debug". Permanent overrides of systemd properties can be configured locally via "systemctl edit munin-node". See "man systemd.exec" for the documentation of systemd's properties. =head1 FILES /etc/munin/munin-node.conf /etc/munin/plugins/* /etc/munin/plugin-conf.d/* /var/run/munin/munin-node.pid /var/log/munin/munin-node.log =head1 VERSION This is munin-run (munin-node) v2.0.66 =head1 AUTHORS Audun Ytterdal, Jimmy Olsen, Tore Anderson, Nicolai Langfeldt, Lars Kruse. =head1 BUGS Please see L<http://munin-monitoring.org/report/1>. =head1 COPYRIGHT Copyright (C) 2002-2009 Audun Ytterdal, Jimmy Olsen, Tore Anderson, Nicolai Langfeldt / Linpro AS. Copyright (C) 2020 Lars Kruse This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 dated June, 1991. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. =cut # vim: sw=4 : ts=4 : expandtab