Suggestions for systemd service file

Wouldn’t these lists normally be stored in /etc/pihole? What conditions would lead the lists to being stored in /home?

Thanks for posting your file, we are on fairly similar tracks but you do have some differences to mine. I am running on systemd v249 primarily (with a test box on v257) so that should cover a decent range.

One thing I did notice yesterday is that PrivateUsers= prevents FTL from binding on port 53:

pihole pihole-FTL[10720]: dnsmasq: failed to create listening socket for port 53: Permission denied

Is this perhaps related to the start script running with elevated privileges (root?) or something similar?

My file is as below:

[Unit]

Description=Pi-hole FTL [OVERRIDE IN EFFECT]
After=time-sync.target
Wants=time-sync.target

[Service]

# Prevents the pihole-FTL process from acquiring more capabilities than the following
AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_NET_ADMIN CAP_SYS_NICE CAP_IPC_LOCK CAP_CHOWN
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_NET_ADMIN CAP_SYS_NICE CAP_IPC_LOCK CAP_CHOWN

# Let the pihole-FTL process see a read-only view of /opt/pihole and root directory for web interface
# when TemporaryFileSystem= is hiding the content of /opt and /var
BindReadOnlyPaths=/opt/pihole /var/www/html/admin

# Gives access to /etc/pihole
ConfigurationDirectory=pihole

ConfigurationDirectoryMode=755
InaccessiblePaths=/boot -/etc/dkimkeys -/etc/duo -/etc/ssh /etc/systemd -/etc/wireguard -/lost+found /root
LockPersonality=true

# Gives access to /var/log/pihole
LogsDirectory=pihole

NoNewPrivileges=true
PrivateDevices=true
PrivateIPC=true

# Gives pihole-FTL a private (0700), not shared, per-execution, tmp folder
PrivateTmp=true

# This breaks CPU/RAM reporting under System Settings
#ProcSubset=pid

ProtectClock=true
ProtectControlGroups=true
ProtectHome=true
ProtectHostname=true
ProtectKernelLogs=true
ProtectKernelModules=true
ProtectKernelTunables=true

# Hides processes owned by other users from pihole-FTL view
ProtectProc=invisible
# Mount the entire file system as read-only. Write access to required folders is provided via
# {Configuration,Logs,Runtime}Directory= settings
ProtectSystem=strict

RemoveIPC=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_NETLINK AF_UNIX
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true

# Gives access to /run/pihole, cleared when service is stopped
RuntimeDirectory=pihole

SystemCallArchitectures=native
SystemCallErrorNumber=EPERM
SystemCallFilter=@basic-io @file-system @io-event @ipc @network-io @process @signal @sync @timer ioctl umask getpriority setpriority sysinfo uname flock mremap capget fchown32 chown32 sendfile64

# Hides the content of the following folders, which may contain user data, from pihole-FTL view
TemporaryFileSystem=/media /mnt /opt /srv /var

@robgill I have some questions:

  • Why did you include the @debugdebug group? Is that needed?
  • UMask 0077 as opposed to 0007? I guess you wanted to lock it down a bit further?

My file is not perfect and still needs some work; bear in mind I do not use the DHCP or NTP functionality in Pi-hole.

I really like your usage of NoExecPaths=, really nice touch! Will experiment with this myself. One thing I must still test is MemoryDenyWriteExecute=, doubt this will work but worth a shot.

To answer your actual question, I have no objections to submitting a PR but it will only be in the new year, and only then if there is enough interest and an indication from the team that it would be welcomed. So far, this discussion has been really productive from my perspective, so my thanks to everyone for their contributions.

@MichaIng I was wrong with my earlier statement: ConfigurationDirectory= etc. seems to work properly on v249 at least without any additional ReadWritePaths=, maybe the documentation was just never updated prior to v252? Not saying it really changes anything in terms of what is feasible, just pointing it out in the interests of accuracy.

Debian Bullseye would be 247. However, maybe I mixed it up with earlier systemd versions where not all of these directives existed yet, e.g. LogsDirectory was not availalable yet at some point while RuntimeDirectory was already or so, and with PrivateTmp which at some point did not guarantee write access, but it is also a different type of directive.

I would also discourage everyone from using /home or /root for anything used by system services, hence make ProtectHome possible to enforce a general principle. Such services have /etc and /var/lib and /opt and if needed /mnt and /media etc for their files/files they need access to. IMO no user (aside of root) must have access to any other user's home dir. Those anyway have 0700 mode by default to enforce exactly that, so ProtectHome is actually just another layer in case UNIX permissions got messed, and since such access for system services with often web interfaces or similar poses an additional risk, compared to local UNIX users.

Home dirs are IMO meant for their owner only, and the daemons that run as this user, nothing else.

But this is my opinion and UNIX distro+systemd convention, while admins might decide differently. The question is whether Pi-hole wants to enforce such convention, causing a breaking change in case, or not.

1 Like

When adding an allowlist/blocklist it is possible to subscribe to a local file, the same way as it is to subscribe to a list via http. (eg, file:///home/rob/kidswhitelist.list). Pi-Hole stores its copy of the list in /etc/pihole, but the user's version of the file that they modify and maintain over time can be stored anywhere. Pi-Hole needs access (readonly) to the local file when it updates lists via the WebUI.

I begin to think it may not be needed, there is not ptrace() presently in the code.

Yes.

PrivateUsers places the capabilities you are granting (such as CAP_NET_BIND_SERVICE) within the new private namespaces, essentially negating them.

1 Like

Thank you for explaining, I get it now. Mine runs without the @debug calls permitted so I also suspect that group is not needed.

I take your point on local lists, but I agree that /home is not really the place for this. I would store it in /etc/pihole or in /opt rather; I agree with MichaIng as the purpose and I would want to avoid anything reading my home directory if at all possible. My two cents for whatever it is worth.

Regardless of anything, any such changes would need to be introduced gradually to minimise the risk of breaking changes. I have no objection to doing so, but it will need to be in January once I am back from holiday.

I definitely agree that for more advanced users who want a tighter system (and don't configure any non-standard file locations and also not making use of DHCP or NTP) there are certainly a more areas that could be tightened up to around 2.5 on systemd-analyze security.

Experienced users like those could handle this via an override in /etc/systemd, or by setting /etc/systemd/system/pihole-FTL.service as immutable to prevent any overwrites from future upgrades.

My concerns over older versions of systemd look like they were overcautious, the newest directive I have included requires v240, which dates from 2018. I incorporated a some changes from your file and a few more on top, and have a reasonably hardened (3.3 in systemd-analyze security) .service file passing all the tox tests in the Pihole repo. It is running at the moment on four real world systems. I plan to review the logs next week to see if anything is amiss but for now I would say it looks good.

2 Likes

Thanks @robgill, looking forward to hearing the outcome. I shall be away next week but will catch up where and when I can.

1 Like

You dont have to do allot to beat the others :wink:

$ systemd-analyze --no-pager security apparmor.service cron.service dbus.service NetworkManager.service ssh.service unbound.service wpa_supplicant.service
[..]
UNIT                   EXPOSURE PREDICATE HAPPY
apparmor.service            9.5 UNSAFE    😨
cron.service                9.6 UNSAFE    😨
dbus.service                9.6 UNSAFE    😨
NetworkManager.service      7.8 EXPOSED   🙁
ssh.service                 9.6 UNSAFE    😨
unbound.service             9.6 UNSAFE    😨
wpa_supplicant.service      9.6 UNSAFE    😨

Though, this should not be seen as a competition :smile:, and e.g. an SSH server naturally requires elevated access, to allow multiple users to login and see their own data etc.

What I generally like about applying sandboxing directives as developer/maintainer is that one is forced to test/review/learn which actual access something really requires for which purpose. We found quite some surprises about badly implemented things, or features we (me) were not aware about among the software install options we provide.

1 Like

So this is about as strict as it can be without interfering with Pi-hole's NTP functionality, configurability or the ability for users to subscibe to lists they maintain on the local filesystem.

# The approaches here aim to work with Pi-hole's existing
# feature set and configurability.
#
# Discussion at https://discourse.pi-hole.net/t/suggestions-for-systemd-service-file
#
# There are notes at the end, and on some options throughout
# about some specific hardening that can not take place while
# maintaining this compatibility. (Advanced users may wish to
# enable these options in overrides).
#
# This passes all of the current tests in pi-hole's tox test suite
# on all of the systemd distributions.
#
# Overall these changes reduce the results of 
# systemd-analyze security pihole-FTL (performed on trixie)
# from → Overall exposure level for pihole-FTL.service: 9.0 UNSAFE 😨
# to   → Overall exposure level for pihole-FTL.service: 3.3 OK 🙂
#
# The newest enabled features require systemd v240 or greater
# (December 2018) this should not be a concern considering the versions
# found in the oldest currently supported distros:
#
# Centos Stream 9 shipped with 249
# Debian Bullseye shipped with 247 (Even buster was 241)
# Fedora 40 shipped with 252
# Ubuntu 20.04 shipped with 245
#
# Note that these restrictions apply only to FTL running as daemon
# /service, and do not have any effect upon command-line execution 
# of pihole-FTL
#

[Unit]
Description=Pi-hole FTL
# This unit is supposed to indicate when network functionality is available, but it is only
# very weakly defined what that is supposed to mean, with one exception: at shutdown, a unit
# that is ordered after network-online.target will be stopped before the network
Wants=network-online.target
After=network-online.target
# A target that should be used as synchronization point for all host/network name service lookups.
# All services for which the availability of full host/network name resolution is essential should
# be ordered after this target, but not pull it in.
Wants=nss-lookup.target
Before=nss-lookup.target

# Limit (re)start loop to 5 within 1 minute
StartLimitBurst=5
StartLimitIntervalSec=60s

[Service]
User=pihole

# Prestart and Poststop scripts
# (+ indicates run prestart with elevated permissions)
ExecStartPre=+/opt/pihole/pihole-FTL-prestart.sh
ExecStart=/usr/bin/pihole-FTL -f
Restart=on-failure
RestartSec=5s
ExecReload=/bin/kill -HUP $MAINPID
ExecStopPost=+/opt/pihole/pihole-FTL-poststop.sh

# Use graceful shutdown with a reasonable timeout
TimeoutStopSec=60s

# Service Hardening
#
# The aims of these sections are to provide no more privilege than is 
# required for Pi-hole to function
#
# Discussion at https://discourse.pi-hole.net/t/suggestions-for-systemd-service-file

# Set capabilities and bound to initial set
AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_NET_ADMIN CAP_SYS_NICE CAP_IPC_LOCK CAP_CHOWN CAP_SYS_TIME
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_NET_ADMIN CAP_SYS_NICE CAP_IPC_LOCK CAP_CHOWN CAP_SYS_TIME
SecureBits=keep-caps

# Limit allowable system calls
SystemCallFilter=~@known
SystemCallFilter=@system-service
SystemCallFilter=@clock

# Only permit native system calls
SystemCallArchitectures=native
SystemCallErrorNumber=EPERM

# Ignore setuid and setgid bits, and prevent them from being set by Pi-hole
NoNewPrivileges=yes
RestrictSUIDSGID=yes

# Prevent loading any additional kernel modules
ProtectKernelModules=yes
# Deny access to kernel logs
ProtectKernelLogs=yes

# Restrict address families
RestrictAddressFamilies=AF_INET AF_INET6 AF_LOCAL AF_NETLINK

# Make /usr, /boot, /etc and possibly some more folders read-only...
ProtectSystem=full
# ... except /etc/pihole
# This merely retains r/w access rights, it does not add any new.
# Must still be writable on the host!
ReadWriteDirectories=/etc/pihole
# Prevent writing to /home hierarchy
# Read access is permitted to allow users to have locally maintained lists
# which are updated via gravity
# BREAKAGE - Setting ProtectHome=true will break the ability to access lists
# BREAKAGE - a user has stored under /home
ProtectHome=read-only

# Limit which files the service may execute
# Allow the gravity update to execute via web-UI
NoExecPaths=/
#ExecPaths=/usr/bin/pihole-FTL
ExecPaths=/opt/pihole/gravity.sh

# Let the pihole-FTL process see a read-only view of Pi-hole's local repo
ReadOnlyPaths=/opt/pihole

# Device Policies - restrict Pi-Hole to pseudo devices (eg /dev/null, /dev/random, /dev/urandom etc)
# and those devices specifically permitted below
# BREAKAGE - (Replacing this with the older PrivateDevices=yes
# BREAKAGE - will prevent Pi-hole's NTP features from working)
DevicePolicy=closed
# Allow access to RTC devices to adjust the system's real time clock
# BREAKAGE - (Removing these devices will prevent Pi-hole's NTP features
# BREAKAGE - from working)
DeviceAllow=/dev/efirtc*
DeviceAllow=/dev/misc/efirtc*
DeviceAllow=/dev/rtc*
DeviceAllow=/dev/misc/rtc*

# Limit access to /proc 
# Make most of /proc readonly, and deny access to /proc/kallsyms and /proc/kcore
ProtectKernelTunables=yes
# Permit access to /proc entries from other tasks

# Don't create globally or group readable files by default
UMask=0077

# Isolate Pi-hole's temporary files
PrivateTmp=yes

# Prevent modification to CGROUP hierarchies
ProtectControlGroups=yes

# Restrict nameservices
RestrictNamespaces=yes

# Prevent switching personality
LockPersonality=yes

# Prevent access to keyring
KeyringMode=private

# Prevent altering the system's hostname or NIS domain name
ProtectHostname=yes

# Protect IPC
PrivateIPC=true
# Clear IPC at unload
RemoveIPC=yes

# Prevent creation of memory blocks that are simultaneously writeable 
# and executable (see note below)
MemoryDenyWriteExecute=yes


### BREAKAGE notes
### In addition to issues listed above.

### The following can't be set without causing potential breakage to people
### who have altered file paths in the pi-hole configuration
#
# LogsDirectory=pihole
#
# Log file locations are configureable
# via files.log.ftl, files.log.dnsmasq, files.log.webserver
# and can appear at any location
#
# ReadOnlyPaths=/var/www/html/admin
#
# Web UI file location is configurable
# via webserver.paths.webroot and webserver.paths.webhome
# There is also the option to serve other content in /var/www
# via webserver.serve_all
#
# RuntimeDirectory=pihole
#
# Pid file location is configurable
# via files.pid
#
# ProtectSystem=strict
#
# As any of the above files can be specified for any location
# which may be write protected by strict
#
# Eliminating/replacing Prestart and poststop files
# 
# For the same reasons, there seems to be no realistic way around 
# needing prestart without drastically reducing Pi-hole's 
# configurability there is no clean way to get the customized file
# locations into the systemd service file.
#

### NTP Incompatibility
#
# ProtectProc=invisible
#
# Setting this to 'invisible' will prevent Pi-hole
# from detecting existing disciplining NTP clients
#
# ProtectClock=yes
# Setting this option to "yes" will prevent Pi-hole's NTP time 
# synchronisation features from working
#
# (Also see notes under /dev and /proc in main file body)
#
# NTP client requires the ability to adjust the system clock to work
# So the SystemCallFilter needs to include @clock, on a system
# not using NTP it could be ommited or replaced with the negator
#SystemCallFilter=~@clock

### General incompatibility 
#
# InaccessiblePaths=/dev/shm
# (This is often recommended together with MemoryDenyWriteExecute and 
# Systemcallfilter=~memfd_create to prevent a circumvention route via dev shm or 
# memfd_create)
#
# However, enabling this option is unworkable given FTL's use of /dev/shm overall.
#

### Not enabled yet - requires systemd v258
# (In practical terms, Debian Trixie is on v257 as of 2025-12,
# so it will be some time (years) until it would be available on all supported distros)
#
# Limit access to BPF
# PrivateBPF=yes

[Install]
WantedBy=multi-user.target
2 Likes

This looks really good. Would you like to create the PR @robgill? Since you have done the bulk of the work here.

I notice you are not setting ProtectProc, does that break something?

2 Likes

Protectproc is another that causes problems with NTP. It prevents FTL from detecting if there is another NTP client running. (Net result, two different programs setting the time, so it will bounce around and never be accurate).

PR will be made shortly

2 Likes

Thanks for the update and explanation. I was intending to get to this next week or the week after, circumstances permitting - but I also don’t feel right taking credit for work which is mostly yours :grinning_face_with_smiling_eyes: