NagiosEnterprises / ncpa

Nagios Cross-Platform Agent
Other
176 stars 95 forks source link

Feature Request: systemd service file hardening #1047

Open pittagurneyi opened 7 months ago

pittagurneyi commented 7 months ago

This is what I came up with in the last couple of hours of testing and it seems to work - though I haven't done a lot of checks yet:

[Unit]
Description=NCPA
Documentation=https://www.nagios.org/ncpa
# https://www.freedesktop.org/wiki/Software/systemd/NetworkTarget/
After=network-online.target local-fs.target
Wants=network-online.target

[Service]
Type=simple
Restart=on-abort

RuntimeDirectory=ncpa
RuntimeDirectoryMode=0755

User=nagios
Group=nagios

# Is this correct or does it now conflict with the config.cfg pidfile?
PIDFile=/usr/local/ncpa/var/run/ncpa.pid

ReadWritePaths=/usr/local/ncpa/var

ExecStart=/usr/local/ncpa/ncpa -n
# Unsure if this is implemented in NCPA.
ExecReload=/bin/kill -HUP $MAINPID
ExecStopPost=/bin/rm -f /usr/local/ncpa/var/run/ncpa.pid

# https://www.freedesktop.org/software/systemd/man/systemd.exec.html
# systemd-analyze security ncpa.service
# Entire system mounted read-only except for /dev/, /proc/ and /sys/.
ProtectSystem=strict
# We don't need anything from $HOME.
ProtectHome=yes
# Separate tmpfs /tmp directory for this process.
PrivateTmp=true
# Only current User= is seen.
# In conflict with sysctl hardening that disables user namespaces.
#PrivateUsers=true
# Any changes in mounts don't affect the main system.
PrivateMounts=true
# We have to allow access to the disks, so this can't be true.
#PrivateDevices=true
# We have to allow network access, so this can't be true, as it would only allow "lo" access.
#PrivateNetwork=true
# Writes to the hardware clock or system clock will be denied.
ProtectClock=true
# Protect cgroups.
ProtectControlGroups=yes
# Protect kernel modules.
ProtectKernelModules=yes
# Protect kernel tunables found in /sys.
ProtectKernelTunables=yes
# Protect kernel logs, not to be confused with user-space logging.
ProtectKernelLogs=yes
# Don't allow hostname to be changed
ProtectHostname=yes
# Can't be restricted if users want to look at existing running processes via
# means other than systemctl status, i.e. ps aux.
#
# Processes owned by other users are hidden from /proc/.
#ProtectProc=invisible
# Can't be activated, because zpool status, etc. don't work anymore as they need the non-process information available via /proc.
# All files and directories not directly associated with process management and introspection are made invisible in the /proc/ file system configured for the unit's processes.
#ProcSubset=pid
# Only allow these types of network access.
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
# No access to namespaces but the one that is created for it.
RestrictNamespaces=yes
# Prevent the process from hogging the CPU cores.
RestrictRealtime=yes
# Setting suid or sgid on any files or directories is denied.
RestrictSUIDSGID=yes
# Restrict system calls
# @clock @cpu-emulation @debug @module @mount @obsolete @privileged @raw-io @reboot @resources @swap
SystemCallFilter=@system-service

# We have a tmpfs mounted temporary directory due to PivateTmp that can
# be used instead of /dev/shm, as that is, due to the hardening herein,
# not available.
#Environment=TMPDIR=/tmp
# This didn't work, because 
# https://github.com/python/cpython/blob/main/Lib/multiprocessing/heap.py#L73
# hard-codes directory candidates and only /dev/shm is available, not respecting
# the environment variable TMPDIR.
# Instead we are mounting tmpfs rw on /dev/shm for this processes namespace.
# Doesn't work either. Abandon hardening of memory filesystems for now.
#TemporaryFileSystem=/dev/shm:rw
# Can't be restricted as python multiprocessing requires it.
# Prevent memory mappings that are both writable and executable. Could lead to problems for processes with dynamic code.
#MemoryDenyWriteExecute=yes
#InaccessiblePaths=/dev/shm
#SystemCallFilter=~memfd_create

# Restrict Capability Bounding Set
# man 7 capabilities
# CAP_AUDIT_CONTROL CAP_AUDIT_READ CAP_AUDIT_WRITE CAP_BLOCK_SUSPEND CAP_CHOWN CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER CAP_FSETID CAP_IPC_LOCK CAP_IPC_OWNER CAP_KILL CAP_LEASE CAP_LINUX_IMMUTABLE CAP_MAC_ADMIN CAP_MAC_OVERRIDE CAP_MKNOD CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_BROADCAST CAP_NET_RAW CAP_SETFCAP CAP_SETGID CAP_SETPCAP CAP_SETUID CAP_SYS_ADMIN CAP_SYS_BOOT CAP_SYS_CHROOT CAP_SYS_NICE CAP_SYS_PACCT CAP_SYS_PTRACE CAP_SYS_RAWIO CAP_SYS_RESOURCE CAP_SYS_TTY_CONFIG
# We need to allow CAP_NET_RAW for ping to work.
CapabilityBoundingSet=CAP_NET_RAW
# Only allow native ABI code to be executed
SystemCallArchitectures=native
# SysV IPC objects can't be left around
RemoveIPC=true
# Prevent kernel system call to change "personality" from the default one.
LockPersonality=yes
# Firewall
# Allow IP access to local subnets and exclude everything else.
IPAddressAllow=localhost
IPAddressAllow=10.0.0.0/8
IPAddressDeny=any
# We have a state directory so that other services might access the state of this one.
StateDirectory=ncpa
# pam_tally as part of sudo requires write access
#ReadWritePaths=/var/run/faillock
# Disallow privilege escalation.
# This prevents any use of sudo, as may other options here, that imply NoNewPrivileges=yes,
# so setting it to 'no' here is not sufficient for sudo to work again.
NoNewPrivileges=yes
# Set the SELinux context for this process. If SELinux is turned off, this is ignored.
#SELinuxContext=
LimitNOFILE=5000
UMask=0077

# Unclear if NCPA uses sendmail ...
# So sendmail works:
RestrictAddressFamilies=AF_NETLINK
ReadWritePaths=-/var/spool/postfix/maildrop
SupplementaryGroups=postdrop

[Install]
WantedBy=multi-user.target

The firewall part might be a bit too restrictive for some users, i.e. needs to be inactive by default and activated by modifying the .service file or adding a ncpa.service.d/something.conf file in /etc/systemd/system.

Also this prevents - at least to my knowledge - any kind of sudo usage, for example by plugins that can be called. I don't believe I need any such checks, but I guess I'll find out.

So I'd say this is a work-in-progress and depends on whether the user is fine with that and wants a more hardenend NCPA service or not.

I thought I'd share it here for anyone who wishes to do some hardening for NCPA.

pittagurneyi commented 6 months ago

From here https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html it seems a PIDFile is not required:

Note that PID files should be avoided in modern projects. Use Type=notify, Type=notify-reload or Type=simple where possible, which does not require use of PID files to determine the main process of a service and avoids needless forking.

I already used Type=simple. So now the question is if the internal logic of ncpa uses a pid file in any way or if we can offload that task to systemd.

Furthermore, normally a PIDFile should be placed in the RuntimeDirectory, typically placed at /run/ncpa or /var/run/ncpa, depending on the systemd system-wide configuration, which gets wiped (or only the innermost subdirectories ?!) after every service stop.

This would all resolve the problem of handling the pid file on systemd services.

The unit file would then look like this - I haven't tested it yet:

[Unit]
Description=NCPA
Documentation=https://www.nagios.org/ncpa
# https://www.freedesktop.org/wiki/Software/systemd/NetworkTarget/
After=network-online.target local-fs.target
Wants=network-online.target

[Service]
Type=simple
Restart=on-abort

RuntimeDirectory=ncpa
RuntimeDirectoryMode=0755

User=nagios
Group=nagios

ReadWritePaths=/usr/local/ncpa/var

ExecStart=/usr/local/ncpa/ncpa -n

# https://www.freedesktop.org/software/systemd/man/systemd.exec.html
# systemd-analyze security ncpa.service
# Entire system mounted read-only except for /dev/, /proc/ and /sys/.
ProtectSystem=strict
# We don't need anything from $HOME.
ProtectHome=yes
# Separate tmpfs /tmp directory for this process.
PrivateTmp=true
# Only current User= is seen.
# In conflict with sysctl hardening that disables user namespaces.
#PrivateUsers=true
# Any changes in mounts don't affect the main system.
PrivateMounts=true
# We have to allow access to the disks, so this can't be true.
#PrivateDevices=true
# We have to allow network access, so this can't be true, as it would only allow "lo" access.
#PrivateNetwork=true
# Writes to the hardware clock or system clock will be denied.
ProtectClock=true
# Protect cgroups.
ProtectControlGroups=yes
# Protect kernel modules.
ProtectKernelModules=yes
# Protect kernel tunables found in /sys.
ProtectKernelTunables=yes
# Protect kernel logs, not to be confused with user-space logging.
ProtectKernelLogs=yes
# Don't allow hostname to be changed
ProtectHostname=yes
# Can't be restricted if users want to look at existing running processes via
# means other than systemctl status, i.e. ps aux.
#
# Processes owned by other users are hidden from /proc/.
#ProtectProc=invisible
# Can't be activated, because zpool status, etc. don't work anymore as they need the non-process information available via /proc.
# All files and directories not directly associated with process management and introspection are made invisible in the /proc/ file system configured for the unit's processes.
#ProcSubset=pid
# Only allow these types of network access.
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
# No access to namespaces but the one that is created for it.
RestrictNamespaces=yes
# Prevent the process from hogging the CPU cores.
RestrictRealtime=yes
# Setting suid or sgid on any files or directories is denied.
RestrictSUIDSGID=yes
# Restrict system calls
# @clock @cpu-emulation @debug @module @mount @obsolete @privileged @raw-io @reboot @resources @swap
SystemCallFilter=@system-service

# We have a tmpfs mounted temporary directory due to PivateTmp that can
# be used instead of /dev/shm, as that is, due to the hardening herein,
# not available.
#Environment=TMPDIR=/tmp
# This didn't work, because 
# https://github.com/python/cpython/blob/main/Lib/multiprocessing/heap.py#L73
# hard-codes directory candidates and only /dev/shm is available, not respecting
# the environment variable TMPDIR.
# Instead we are mounting tmpfs rw on /dev/shm for this processes namespace.
# Doesn't work either. Abandon hardening of memory filesystems for now.
#TemporaryFileSystem=/dev/shm:rw
# Can't be restricted as python multiprocessing requires it.
# Prevent memory mappings that are both writable and executable. Could lead to problems for processes with dynamic code.
#MemoryDenyWriteExecute=yes
#InaccessiblePaths=/dev/shm
#SystemCallFilter=~memfd_create

# Restrict Capability Bounding Set
# man 7 capabilities
# CAP_AUDIT_CONTROL CAP_AUDIT_READ CAP_AUDIT_WRITE CAP_BLOCK_SUSPEND CAP_CHOWN CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER CAP_FSETID CAP_IPC_LOCK CAP_IPC_OWNER CAP_KILL CAP_LEASE CAP_LINUX_IMMUTABLE CAP_MAC_ADMIN CAP_MAC_OVERRIDE CAP_MKNOD CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_BROADCAST CAP_NET_RAW CAP_SETFCAP CAP_SETGID CAP_SETPCAP CAP_SETUID CAP_SYS_ADMIN CAP_SYS_BOOT CAP_SYS_CHROOT CAP_SYS_NICE CAP_SYS_PACCT CAP_SYS_PTRACE CAP_SYS_RAWIO CAP_SYS_RESOURCE CAP_SYS_TTY_CONFIG
# We need to allow CAP_NET_RAW for ping to work.
CapabilityBoundingSet=CAP_NET_RAW
# Only allow native ABI code to be executed
SystemCallArchitectures=native
# SysV IPC objects can't be left around
RemoveIPC=true
# Prevent kernel system call to change "personality" from the default one.
LockPersonality=yes
# Firewall
# Allow IP access to local subnets and exclude everything else.
IPAddressAllow=localhost
IPAddressAllow=10.0.0.0/8
IPAddressDeny=any
# We have a state directory so that other services might access the state of this one.
StateDirectory=ncpa
# pam_tally as part of sudo requires write access
#ReadWritePaths=/var/run/faillock
# Disallow privilege escalation.
# This prevents any use of sudo, as may other options here, that imply NoNewPrivileges=yes,
# so setting it to 'no' here is not sufficient for sudo to work again.
NoNewPrivileges=yes
# Set the SELinux context for this process. If SELinux is turned off, this is ignored.
#SELinuxContext=
LimitNOFILE=5000
UMask=0077

# Unclear if NCPA uses sendmail ...
# So sendmail works:
RestrictAddressFamilies=AF_NETLINK
ReadWritePaths=-/var/spool/postfix/maildrop
SupplementaryGroups=postdrop

[Install]
WantedBy=multi-user.target

The problematic part in the agent code is then here I'd guess:

https://github.com/NagiosEnterprises/ncpa/blob/master/agent/ncpa.py#L567