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