Skip to content

safe-reload script runs as root operating on potentially untrusted data found in icinga PID file #10527

Closed

Description

@mgerstner

Describe the bug

The safe-reload script is invoked upon systemctl reload icinga2.service as root (see ExecReload in service unit file).

The icinga processes typically run with lowered privileges, e.g. as icinga:icinga on openSUSE Tumbleweed. The PID file there is found in this location:

root# ls -lhd /run/icinga2/icinga2.pid
-rw-r--r-- 1 icinga icinga 5 Jul 31 10:20 /run/icinga2/icinga2.pid
root# ls -lhd /run/icinga2/
drwxr-xr-x 3 icinga icingacmd 80 Jul 31 10:20 /run/icinga2/

The safe-reload script performs the following operation based on the pidfile data:

pid=`cat "$ICINGA2_PID_FILE"`
if ! kill -HUP "$pid" >/dev/null 2>&1; then
	echo "Error: Icinga not running"
	exit 7
fi

This effectively means that the safe-reload script processes data controlled by the icinga service user. The PID file could be replaced by a symbolic link, or a named pipe, which would lead to a local denial-of-service against the reload invocation.

Furthermore an arbitrary PID can be placed in the file, which would cause safe-reload to send the SIGHUP to an arbitrary other process in the system, causing integrity violation and/or denial-of-service.

This is a defense-in-depth issue, because the isolation between the icinga service user and root is lacking. It is not directly exploitable, but becomes a problem when the icinga service user account is compromised.

To fix this the safe-reload script could be invoked with service user credentials from the very beginning, or parts of the script could use a utility like sudo, to drop to service user privileges.

Activity

  1. mgerstner commented on Jul 31, 2025

    @mgerstner
    Author

    Note that there exists a variant of the same issue in the logrotate drop-in configuration file:

    	postrotate
    		/bin/kill -USR1 $(cat @ICINGA2_INITRUNDIR@/icinga2.pid 2> /dev/null) 2> /dev/null || true
    	endscript
    

    This postrotate script runs as root and operates on the PID file. It can similarly be fixed by using sudo. Or, even better, fix the systemctl reload case and the call systemctl reload icinga2.service in the logrotate configuration file.

  2. self-assigned this
    on Aug 1, 2025
  3. added theissue type on Aug 1, 2025
  4. julianbrost commented on Aug 1, 2025

    @julianbrost
    Member

    Thank you for the report!

    To fix this the safe-reload script could be invoked with service user credentials from the very beginning, or parts of the script could use a utility like sudo, to drop to service user privileges.

    sudo sounds like overkill for the task, something like setpriv from util-linux sounds more appropriate for that job. However, from a previous and very similar issue (see #5991 and #5850), there is already the icinga2 internal signal subcommand that drops privileges to the daemon user and then sends a signal. So I'd go for that as it's already there and not Linux-specific.

    Or, even better, fix the systemctl reload case and the call systemctl reload icinga2.service in the logrotate configuration file.

    No, those do different things, SIGHUP performs a full reload, loading the (potentially changed) configuration, whereas SIGUSR1 only reopens the log files.

    This is a defense-in-depth issue, because the isolation between the icinga service user and root is lacking. It is not directly exploitable, but becomes a problem when the icinga service user account is compromised.

    May I ask if you reported this here because you weren't aware of our security contact address (see https://icinga.com/company/contact/security-issues/, though that could probably be communicated better here on GitHub)?

  5. julianbrost commented on Aug 1, 2025

    @julianbrost
    Member

    Please have a look at my proposed fix in #10530. Note that this is mostly targeted at fixing the root problem, I think more can still be done, like for example setting User=icinga in the systemd unit so systemd doesn't even start things as root (except ExecStartPre which probably needs more permissions as it initializes directories) or using $MAINPID when invoked from systemd, removing the need for even reading the PID file.

  6. mgerstner commented on Aug 6, 2025

    @mgerstner
    Author

    This is a defense-in-depth issue, because the isolation between the icinga service user and root is lacking. It is not directly exploitable, but becomes a problem when the icinga service user account is compromised.

    May I ask if you reported this here because you weren't aware of our security contact address (see https://icinga.com/company/contact/security-issues/, though that could probably be communicated better here on GitHub)?

    I checked whether it was possible to create a confidential GitHub issue, but that wasn't available, because there's no security policy setup in your repository.

    Apologies if you would have preferred coordinated disclosure. We usually offer it, but for defense-in-depth issues like this one upstream projects often like to handle them publicly from the beginning. As I'm currently dealing with a large amount of similar issues I chose the quick and simple route to save some time.

  7. mgerstner commented on Aug 6, 2025

    @mgerstner
    Author

    Please have a look at my proposed fix in #10530. Note that this is mostly targeted at fixing the root problem, I think more can still be done, like for example setting User=icinga in the systemd unit so systemd doesn't even start things as root (except ExecStartPre which probably needs more permissions as it initializes directories) or using $MAINPID when invoked from systemd, removing the need for even reading the PID file.

    I just had a look. I guess the interesting part now is what icinga2 internal signal --sig <signal> --pid <pid> does. Judging from the strace output it's a lot. Many threads are created, sockets are created, network interface information is retrieved via Netlink, file descriptors in /proc/self/fd are inspected, one thread sends SIGRT_1 to another thread.

    Besides all that, all threads also seem to call setuid(), setgid() and setgroups(). icinga2 and icingacmd group ownership is retained. Only then the desired signal is sent to the target <pid>. So I guess it is okay. I couldn't see any dangerous file operations in the trace.

  8. julianbrost commented on Aug 6, 2025

    @julianbrost
    Member

    Threads being started is mostly an artifact that this is just part of the one icinga2 binary, which is also used for running the daemon and initializes threads quite early. Those don't really do anything useful for that particular call. So what happens here is pretty much the same as it would when starting icinga2 daemon as root with it dropping the privileges itself.

    That SIGRT_1 sending and following syscalls are just the glibc implementation of setuid(): in the kernel, credentials are per thread not per process, so glibc does that dance to issue the corresponding syscalls in every thread.

  9. Al2Klimov commented on Oct 28, 2025

    @Al2Klimov
    Member

    This postrotate script runs as root

    Oh, does it? #10617

    I thought it includes LOGROTATE_USE_SU which is su ${ICINGA2_USER} ${ICINGA2_GROUP}.

  10. mgerstner commented on Oct 29, 2025

    @mgerstner
    Author

    This postrotate script runs as root

    Oh, does it? #10617

    I thought it includes LOGROTATE_USE_SU which is su ${ICINGA2_USER} ${ICINGA2_GROUP}.

    What is this LOGROTATE_USE_SU exactly refering to? Where is this supposed to be found?

  11. yhabteab commented on Oct 29, 2025

    @yhabteab
    Member

    What is this LOGROTATE_USE_SU exactly refering to? Where is this supposed to be found?

    It's a CMake variable set conditionally here and might not always be available.

    icinga2/CMakeLists.txt

    Lines 155 to 172 in f42510f

    if(NOT DEFINED LOGROTATE_HAS_SU)
    set(LOGROTATE_HAS_SU OFF)
    find_program(LOGROTATE_BINARY logrotate)
    execute_process(COMMAND ${LOGROTATE_BINARY} ERROR_VARIABLE LOGROTATE_OUTPUT)
    if(LOGROTATE_OUTPUT)
    string(REGEX REPLACE "^logrotate ([0-9.]*).*" "\\1" LOGROTATE_VERSION
    ${LOGROTATE_OUTPUT})
    message(STATUS "Found logrotate (found version \"${LOGROTATE_VERSION}\")")
    if("${LOGROTATE_VERSION}" VERSION_GREATER "3.7.9")
    set(LOGROTATE_HAS_SU ON)
    endif()
    endif()
    endif()
    if(LOGROTATE_HAS_SU)
    set(LOGROTATE_USE_SU "\n\tsu ${ICINGA2_USER} ${ICINGA2_GROUP}")
    else()
    set(LOGROTATE_CREATE "\n\tcreate 644 ${ICINGA2_USER} ${ICINGA2_GROUP}")
    endif()

  12. mgerstner commented on Oct 29, 2025

    @mgerstner
    Author

    What is this LOGROTATE_USE_SU exactly refering to? Where is this supposed to be found?

    It's a CMake variable set conditionally here and might not always be available.

    Ah, I see. It results in a su <user> <group> logrotate configuration line.

    Alas, this configuration directive only determines which credentials logrotated uses for its internal log rotation logic. Embedded scripts like prerotate and postrotate always run with full root privileges.

    We covered this in a blog post a while ago, if you're interested:

    https://security.opensuse.org/2025/10/01/summer-spotlight.html#problems-with-scripts-embedded-in-configuration

  13. yhabteab commented on Oct 29, 2025

    @yhabteab
    Member

    Embedded scripts like prerotate and postrotate always run with full root privileges.

    Indeed! In its man page states also this:

    Scripts are run as the invoking user and group, irrespective of any su directive.

    Thus, the OP of #10617 probably changed the systemd unit file to start logrotate as the icinga user. There's nothing to do here, thanks for sharing the link!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area/setupInstallation, systemd, sample files

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions