1#!/usr/bin/perl -T 2# -*- perl -*- 3# 4# Copyright and license: see bottom of file 5# 6 7use strict; 8use warnings; 9 10# Trust PERL5LIB from environment 11use lib map { /(.*)/ } split(/:/, ($ENV{PERL5LIB} || '')); 12 13use English qw(-no_match_vars); 14use File::Temp; 15use Getopt::Long; 16 17use Munin::Common::Defaults; 18use Munin::Node::Config; 19use Munin::Node::OS; 20use Munin::Node::Service; 21 22my $services; 23my $servicedir; 24my $conffile = "$Munin::Common::Defaults::MUNIN_CONFDIR/munin-node.conf"; 25my $DEBUG = 0; 26my $PIDEBUG = 0; 27my $paranoia = 0; 28my $ignore_systemd_properties = 0; 29# The following parameters of "systemd-run" require rather recent systemd versions: 30# --wait: 232 or later 31# --pipe: 235 or later 32# --collect: 236 or later 33 34my $REQUIRED_SYSTEMD_VERSION = 236; 35 36# See "man systemd.exec" for the list of all systemd properties. 37# The following properties belong to the relevant sections "Capabilities", 38# "Security", "Mandatory Access Control", "Process Properties", 39# "Sandboxing" and "System Call Filtering". 40# These properties will be imported from the specification of 41# "munin-node.service" if systemd is enabled. 42# See "--ignore-systemd-properties" for details. 43# See "get_systemd_hardening_flags" for a few exceptions from the list below. 44my @SYSTEMD_PROPERTY_IMPORT_PATTERNS = qw( 45 AmbientCapabilities 46 AppArmorProfile 47 CapabilityBoundingSet 48 DynamicUser 49 Environment 50 EnvironmentFile 51 Group 52 Limit\w+ 53 LockPersonality 54 MemoryDenyWriteExecute 55 MountFlags 56 NetworkNamespacePath 57 NoNewPrivileges 58 PassEnvironment 59 Private\w+ 60 Protect\w+ 61 Restrict\w+ 62 SecureBits 63 SELinuxContext 64 SmackProcessLabel 65 SystemCallArchitectures 66 SystemCallFilter 67 TemporaryFileSystem 68 UMask 69 UnsetEnvironment 70 User 71 \w+Directory 72 \w+Paths 73); 74 75# The following environment variables are assigned automatically by 76# systemd-run (see "man systemd.exec"). We should not override them 77# when calling "systemd-run". 78# See "--ignore-systemd-properties" for details. 79my %ENVIRONMENT_IGNORE_HASH = map { $_ => 1 } qw( 80 PATH 81 LANG 82 USER 83 HOME 84 SHELL 85 LOGNAME 86 INVOCATION_ID 87 XDG_RUNTIME_DIR 88 RUNTIME_DIRECTORY 89 STATE_DIRECTORY 90 CACHE_DIRECTORY 91 LOGS_DIRECTORY 92 CONFIGURATION_DIRECTORY 93 MAINPID 94 MANAGERPID 95 LISTEN_FDS 96 LISTEN_PID 97 LISTEN_FDNAMES 98 NOTIFY_SOCKET 99 WATCHDOG_PID 100 WATCHDOG_USEC 101 TERM 102 JOURNAL_STREAM 103 SERVICE_RESULT 104 EXIT_CODE 105 EXIT_STATUS 106 PIDFILE 107); 108 109my $config = Munin::Node::Config->instance(); 110 111 112sub main 113{ 114 # "Clean" environment to disable taint-checking on the environment. We _know_ 115 # that the environment is insecure, but we want to let admins shoot themselves 116 # in the foot with it, if they want to. 117 foreach my $key (keys %ENV) { 118 $ENV{$key} =~ /^(.*)$/s; 119 $ENV{$key} = $1; 120 } 121 # plugins run in taint mode because the uid is changed, so the path 122 # must not contain writable directories. 123 $ENV{PATH}='/bin:/sbin:/usr/bin:/usr/sbin:%%PREFIX%%/bin:%%PREFIX%%/sbin'; 124 125 $0 =~ /^(.*)$/; 126 $0 = $1; 127 128 my @original_argv = @ARGV; 129 my ($plugin, $arg) = parse_args(); 130 131 # Loads the settings from munin-node.conf. 132 # Ensures that, where options can be set both in the config and in 133 # @ARGV, the latter takes precedence. 134 $paranoia = $config->{paranoia}; 135 136 my $config = Munin::Node::Config->instance(); 137 $config->parse_config_from_file($conffile); 138 139 # Run directly or execute recursively via "systemd-run". 140 if (($ignore_systemd_properties) || (! -d "/run/systemd/system")) { 141 return execute_plugin($plugin, $arg); 142 } elsif (!check_systemd_run_permissions()) { 143 print STDERR "# Skipping systemd properties simulation due to lack of permissions.\n" if $config->{DEBUG}; 144 return execute_plugin($plugin, $arg); 145 } else { 146 my $systemd_version = get_systemd_version(); 147 if ((not defined $systemd_version) or ($systemd_version < $REQUIRED_SYSTEMD_VERSION)) { 148 print STDERR "# Skipping systemd properties simulation due to required systemd version ($REQUIRED_SYSTEMD_VERSION)\n" if $config->{DEBUG}; 149 return execute_plugin($plugin, $arg); 150 } else { 151 my @munin_node_hardening_flags; 152 my $parse_flags_success = 0; 153 eval { 154 @munin_node_hardening_flags = get_systemd_hardening_flags(); 155 $parse_flags_success = 1; 156 }; 157 if ($parse_flags_success) { 158 return run_via_systemd(\@munin_node_hardening_flags, 159 \@original_argv, $config->{DEBUG}); 160 } else { 161 # Failed to retrieve systemd properties of munin-node service. 162 # Probable causes: systemd is not installed/enabled or the 163 # service unit does not exist. 164 return execute_plugin($plugin, $arg); 165 } 166 } 167 } 168} 169 170 171sub check_systemd_run_permissions { 172 # verify whether systemd-run can be exected (e.g. unprivileged users cannot execute it) 173 return system("systemd-run --collect --pipe --quiet --wait -- true </dev/null >/dev/null 2>&1") == 0; 174} 175 176 177# Retrieve the locally configured hardening flags for the "munin-node" systemd 178# service. 179# The result of the function is a list of strings like the following: 180# ProtectHome=yes 181sub get_systemd_hardening_flags { 182 # retrieve all active properties except for soft (runtime) limits 183 my @munin_service_properties = grep !/^Limit\w+Soft=/, `systemctl show munin-node 2>/dev/null`; 184 die "no systemd enabled or failed to retrieve unit properties" unless ($CHILD_ERROR >> 8 == 0); 185 my $flag_name_regex = '^((?:' . join("|", @SYSTEMD_PROPERTY_IMPORT_PATTERNS) . ')=.*)$'; 186 my @flag_list; 187 foreach my $property_definition (@munin_service_properties) { 188 # The effect of files referenced in "DropInPaths" (e.g. files overriding the properties of 189 # a service) is already applied to the output of "systemd-show". 190 # Thus we can safely ignore this property (which is not accepted by "systemd-run" anyway). 191 next if ($property_definition =~ /^DropInPaths=/); 192 # "systemd show" does not output the EnvironmentFile property in a readable format. 193 # See https://github.com/systemd/systemd/issues/14723. 194 next if ($property_definition =~ /^EnvironmentFiles?=(.*) \(ignore_errors=(yes|no)\)$/); 195 push @flag_list, $1 if $property_definition =~ /$flag_name_regex/; 196 } 197 return @flag_list; 198} 199 200 201# "man systemd.exec" describes the quoting rules for EnvironmentFile. 202# We apply the following steps: 203# 1) escape all double quotes with a backslash 204# 2) surround the value with double quotes 205# This combination ensures that even line breaks are properly parsed by 206# systemd-run. 207sub quote_for_systemd_environment_file { 208 my ($key, $value) = @_; 209 # escape double quotes 210 $value =~ s/"/\\"/; 211 return $key . '="' . $value . "\"\n"; 212} 213 214 215sub get_systemd_version { 216 my @version_output = `systemd-run --version 2>/dev/null`; 217 foreach my $line (@version_output) { 218 if ($line =~ /^systemd(?:-run)?\s+(\d+).*$/) { 219 return int($1); 220 } 221 } 222 return; 223} 224 225 226# Recursively execute this script ("munin-run") via "systemd-run". 227# This allows to apply the hardening properties defined in "munin-node.service". 228# Thus the behavior of "munin-run" should be the same as the behavior of 229# munin-node service itself. This is less surprising for users. 230sub run_via_systemd { 231 my ($systemd_properties_ref, $original_argv_ref, $debug_enabled) = @_; 232 my @call_args; 233 push @call_args, "systemd-run"; 234 # discard the transient service even in case of errors 235 push @call_args, "--collect"; 236 # use our stdin/stdout/stderr for the created process 237 push @call_args, "--pipe"; 238 push @call_args, "--quiet"; 239 # wait for the end of the command execution 240 push @call_args, "--wait"; 241 # Preserve the environment (e.g. manual plugin configuration applied by 242 # the user). 243 # We use systemd-run's property "EnvironmentFile" for transferring the 244 # environment of the current process to the new process. The following 245 # simpler approaches ("properties") are not suitable: 246 # * Environment: would expose the private environment of the calling 247 # user to all other users (via the commandline). 248 # * PassEnvironment: the variables are only transferred from the 249 # system manager (PID 1) instead of the calling process. 250 # This approach causes a problem 251 my $environment_file = File::Temp->new(); 252 # The order of systemd-run's environment variable processing may cause 253 # problems, if "munin-node.service" specifies "Environment" properties, 254 # which exist in the caller's environment. Such variables (being written 255 # to the temporary EnvironmentFile) take precedence over the ones defined 256 # in "munin-node.service". There does not seem to be a clean generic 257 # workaround for this issue. 258 foreach my $key (keys %ENV) { 259 next if exists($ENVIRONMENT_IGNORE_HASH{$key}); 260 print $environment_file quote_for_systemd_environment_file($key, $ENV{$key}); 261 } 262 push @call_args, "--property"; 263 push @call_args, "EnvironmentFile=" . $environment_file->filename; 264 # enable the hardening flags of the munin-node service 265 foreach my $key_value (@$systemd_properties_ref) { 266 push @call_args, "--property"; 267 push @call_args, $key_value; 268 } 269 push @call_args, "--"; 270 # append the untainted name/path of "munin-run" itself 271 $0 =~ /^(.*)$/s; 272 push @call_args, $1; 273 push @call_args, "--ignore-systemd-properties"; 274 foreach my $arg (@$original_argv_ref) { 275 # untaint our arguments 276 $arg =~ /^(.*)$/s; 277 push @call_args, $1; 278 } 279 if ($debug_enabled) { 280 print STDERR ("# Running 'munin-run' via 'systemd-run' with systemd " 281 . "properties based on 'munin-node.service'.\n"); 282 my $command_printable = ""; 283 foreach my $token (@call_args) { 284 $command_printable .= " " if ($command_printable); 285 if ($token =~ /\s/) { 286 $command_printable .= "'$token'"; 287 } else { 288 $command_printable .= "$token"; 289 } 290 } 291 print STDERR "# Command invocation: $command_printable\n"; 292 } 293 # We need to use "system" instead of "exec in order to remove the EnvironmentFile 294 # afterwards. This is indirectly handled by the object cleanup from File::Temp. 295 my $result = system(@call_args); 296 if ($result == -1) { 297 die "Failed to execute the 'systemd-run' wrapper. Maybe try '--ignore-systemd-properties'."; 298 } else { 299 my $exitcode = $result >> 8; 300 if ($exitcode != 0) { 301 # Sadly problems with "systemd-run" are only visible in the log (no error output). 302 print STDERR ("Warning: the execution of 'munin-run' via 'systemd-run' returned an " 303 . "error. This may either be caused by a problem with the plugin to be " 304 . "executed or a failure of the 'systemd-run' wrapper. Details of the " 305 . "latter can be found via 'journalctl'.\n"); 306 } 307 return $exitcode; 308 } 309} 310 311 312sub execute_plugin { 313 my ($plugin, $arg) = @_; 314 315 $services = Munin::Node::Service->new( 316 servicedir => $servicedir, 317 defuser => $config->{defuser}, 318 defgroup => $config->{defgroup}, 319 pidebug => $PIDEBUG, 320 ); 321 322 $config->reinitialize({ 323 %$config, 324 paranoia => $paranoia, 325 }); 326 327 unless ($services->is_a_runnable_service($plugin)) { 328 print STDERR "# Unknown service '$plugin'\n"; 329 exit 1; 330 } 331 332 $services->prepare_plugin_environment($plugin); 333 334 # no need for a timeout -- the user can kill this process any 335 # time they want. 336 $services->exec_service($plugin, $arg); 337 338 # Never reached, but just in case... 339 print STDERR "# FATAL: Failed to exec.\n"; 340 exit 42; 341} 342 343 344sub parse_args 345{ 346 # Default configuration values 347 my $sconfdir = "$Munin::Common::Defaults::MUNIN_CONFDIR/plugin-conf.d"; 348 my $sconffile; 349 350 my ($plugin, $arg); 351 352 print_usage_and_exit() unless GetOptions( 353 "config=s" => \$conffile, 354 "debug!" => \$DEBUG, 355 "pidebug!" => \$PIDEBUG, 356 "servicedir=s" => \$servicedir, 357 "sconfdir=s" => \$sconfdir, 358 "sconffile=s" => \$sconffile, 359 "paranoia!" => \$paranoia, 360 "ignore-systemd-properties" => \$ignore_systemd_properties, 361 "version" => \&print_version_and_exit, 362 "help" => \&print_usage_and_exit, 363 ); 364 365 print_usage_and_exit() unless $ARGV[0]; 366 367 # Detaint the plugin name 368 ($plugin) = ($ARGV[0] =~ m/^([-\w.:]+)$/) or die "# ERROR: Invalid plugin name '$ARGV[0].\n"; 369 if ($ARGV[1]) { 370 ($arg) = ($ARGV[1] =~ m/^(\w+)$/) 371 or die "# ERROR: Invalid characters in argument '$ARGV[1]'.\n"; 372 } 373 374 # Detaint service directory. FIXME: do more strict detainting? 375 if ($servicedir) { 376 $servicedir =~ /(.*)/; 377 $servicedir = $1; 378 } 379 380 # Update the config 381 $config->reinitialize({ 382 %$config, 383 384 sconfdir => $sconfdir, 385 conffile => $conffile, 386 sconffile => $sconffile, 387 DEBUG => $DEBUG, 388 paranoia => $paranoia, 389 }); 390 391 return ($plugin, $arg); 392} 393 394 395sub print_usage_and_exit 396{ 397 require Pod::Usage; 398 Pod::Usage::pod2usage(-verbose => 1); 399} 400 401 402sub print_version_and_exit 403{ 404 require Pod::Usage; 405 Pod::Usage::pod2usage( 406 -verbose => 99, 407 -sections => 'VERSION|COPYRIGHT', 408 ); 409} 410 411 412exit main() unless caller; 413 414 4151; 416 417__END__ 418 419=head1 NAME 420 421munin-run - A program to run Munin plugins from the command line 422 423=head1 SYNOPSIS 424 425munin-run [options] <plugin> [ config | autoconf | snmpconf | suggest ] 426 427=head1 DESCRIPTION 428 429munin-run is a script to run Munin plugins from the command-line. 430It's useful when debugging plugins, as they are run in the same conditions 431as they are under munin-node. 432 433=head1 OPTIONS 434 435=over 5 436 437=item B<< --config <configfile> >> 438 439Use E<lt>fileE<gt> as configuration file. [@@CONFDIR@@/munin-node.conf] 440 441=item B<< --servicedir <dir> >> 442 443Use E<lt>dirE<gt> as plugin dir. [@@CONFDIR@@/plugins/] 444 445=item B<< --sconfdir <dir> >> 446 447Use E<lt>dirE<gt> as plugin configuration dir. [@@CONFDIR@@/plugin-conf.d/] 448 449=item B<< --sconffile <file> >> 450 451Use E<lt>fileE<gt> as plugin configuration. Overrides sconfdir. [undefined] 452 453=item B<--paranoia > 454 455Only run plugins owned by root and check permissions. [disabled] 456 457=item B<--ignore-systemd-properties > 458 459Do not try to detect and enforce the locally configured hardening flags of the 460"munin-node" service unit. This detection is skipped, if systemd is not enabled. 461The hardening flags may cause subtile surprises. 462For example "ProtectHome=yes" prevents the "df" plugin from determining the 463state of the "home" partition. [disabled] 464 465=item B<--help > 466 467View this help message. 468 469=item B<--debug > 470 471Print debug messages. Debug messages are sent to STDOUT and are 472prefixed with "#" (this makes it easier for other parts of munin to 473use munin-run and still have --debug on). Only errors go to STDERR. 474 475=item B<--pidebug > 476 477Plugin debug. Sets the environment variable MUNIN_DEBUG to 1 so 478that plugins may enable debugging. [disabled] 479 480=item B<--version > 481 482Show version information. 483 484=back 485 486=head1 NOTES FOR SYSTEMD USERS 487 488The "munin-node" service is usually started by systemd via a 489"munin-node.service" definition. Some distributions enable hardening 490settings in this service file in order to restrict the allowed set of 491activities for the "munin-node" process. 492This may cause surprising differences between the result of "munin-run" 493and the real "munin-node" service. 494 495A popular example of such a surprising restriction is "ProtectHome=yes" 496combined with the "df" plugin. The restriction silently prevents the 497plugin from determining the status of mountpoints below /home. 498 499"munin-run" tries to mimic this behavior of "munin-node" automatically. 500Thus the execution of "munin-run df" should provide the same output as 501"echo fetch df | nc localhost munin". 502 503If you want to debug potential issues of systemd restrictions, then you 504may want to use the parameters "--ignore-systemd-properties" and 505"--debug". Permanent overrides of systemd properties can be configured 506locally via "systemctl edit munin-node". 507See "man systemd.exec" for the documentation of systemd's properties. 508 509=head1 FILES 510 511 @@CONFDIR@@/munin-node.conf 512 @@CONFDIR@@/plugins/* 513 @@CONFDIR@@/plugin-conf.d/* 514 @@STATEDIR@@/munin-node.pid 515 @@LOGDIR@@/munin-node.log 516 517=head1 VERSION 518 519This is munin-run (munin-node) v@@VERSION@@ 520 521=head1 AUTHORS 522 523Audun Ytterdal, Jimmy Olsen, Tore Anderson, Nicolai Langfeldt, 524Lars Kruse. 525 526=head1 BUGS 527 528Please see L<http://munin-monitoring.org/report/1>. 529 530=head1 COPYRIGHT 531 532Copyright (C) 2002-2009 Audun Ytterdal, Jimmy Olsen, Tore Anderson, 533Nicolai Langfeldt / Linpro AS. 534Copyright (C) 2020 Lars Kruse 535 536This program is free software; you can redistribute it and/or 537modify it under the terms of the GNU General Public License 538as published by the Free Software Foundation; version 2 dated June, 5391991. 540 541This program is distributed in the hope that it will be useful, 542but WITHOUT ANY WARRANTY; without even the implied warranty of 543MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 544GNU General Public License for more details. 545 546You should have received a copy of the GNU General Public License 547along with this program; if not, write to the Free Software 548Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 549MA 02110-1301 USA. 550 551=cut 552 553# vim: sw=4 : ts=4 : expandtab 554