Systemd service for reporting received email as spam
- Runs as daemon
- Natively Systemd-compliant
- Runs as notify-type
- Deploys in own Python venv to be immune of system changes
- Asynchronous / event-driven
- Secure:
- SElinux-policy
- Daemon uses Linux capabilities to restrict privileges
- Configurable with separate file in
/etc
For docs, see: https://dbus.freedesktop.org/doc/dbus-python/tutorial.html#connecting-to-the-bus
- Per user SessionBus,
dbus-send --session - Global SystemBus,
dbus-send --system
Note: On a request, default is --session.
Policy install (as root):
- Copy file
spamreporter-dbus.confinto directory/etc/dbus-1/system.d/ - Make policy change effective:
systemctl reload dbus - List available services:
Response will contain published interface:
dbus-send \ --system \ --print-reply \ --type=method_call \ --dest=org.freedesktop.DBus \ /org/freedesktop/DBus org.freedesktop.DBus.ListNames
method return time=123.456 sender=org.freedesktop.DBus -> destination=:1.1234 serial=3 reply_serial=2 array [ string "org.freedesktop.DBus" string "fi.hqcodeshop.SpamReporter" ... ] - Verify published service details:
Response will contain published interface:
busctl introspect fi.hqcodeshop.SpamReporter /fi/hqcodeshop/SpamReporter fi.hqcodeshop.SpamReporter
NAME TYPE SIGNATURE RESULT/VALUE FLAGS .Ping method - s - .ReportFile method s s - - Test service with a ping:
Response will contain a greeting to the caller:
dbus-send --print-reply \ --system \ --type=method_call \ --dest=fi.hqcodeshop.SpamReporter \ /fi/hqcodeshop/SpamReporter fi.hqcodeshop.SpamReporter.Ping
method return time=1647618215.603226 sender=:1.250 -> destination=:1.252 serial=6 reply_serial=2 string "Hi Joe User in system-bus! pong" - Done!
When service is running, see if D-Bus works.
Run:
dbus-send \
--print-reply \
--type=method_call \
--dest=fi.hqcodeshop.SpamReporter \
/fi/hqcodeshop/SpamReporter fi.hqcodeshop.SpamReporter.PingResponse (based on user who sent the ping):
string "Hi Joe Nobody! pong"
When service is running, following command will report file named -FILENAME-HERE- as spam.
dbus-send \
--system \
--print-reply \
--type=method_call \
--dest=--dest=fi.hqcodeshop.SpamReporter \
/fi/hqcodeshop/SpamReporter fi.hqcodeshop.SpamReporter.ReportFile "string:-FILENAME-HERE-"Docs: https://www.freedesktop.org/software/systemd/man/systemd.service.html
For best results, this type of service runs as type notify. Practically this means, the Python code needs to ping Linux every 30 seconds as keep-alive pulse. Miss one ping and Linux will restart the service.
Pinging / notifying is done using asynchronous calls to reduce idle CPU-load to absolute minimum.
.service definition has following:
CapabilityBoundingSet=CAP_AUDIT_WRITE CAP_DAC_READ_SEARCH CAP_IPC_LOCK CAP_SYS_NICE
man 7 capabilities @ https://man7.org/linux/man-pages/man7/capabilities.7.html
- CAP_AUDIT_WRITE (since Linux 2.6.11)
- Write records to kernel auditing log.
- CAP_DAC_READ_SEARCH
- Bypass file read permission checks and directory read and execute permission checks;
- invoke open_by_handle_at(2);
- use the linkat(2) AT_EMPTY_PATH flag to create a link to a file referred to by a file descriptor.
- CAP_IPC_LOCK
- Lock memory (mlock(2), mlockall(2), mmap(2), shmctl(2));
- Allocate memory using huge pages (memfd_create(2), mmap(2), shmctl(2)).
- CAP_SYS_NICE
- Lower the process nice value (nice(2), setpriority(2)) and change the nice value for arbitrary processes;
- set real-time scheduling policies for calling process, and set scheduling policies and priorities for arbitrary processes (sched_setscheduler(2), sched_setparam(2), sched_setattr(2));
- set CPU affinity for arbitrary processes (sched_setaffinity(2));
- set I/O scheduling class and priority for arbitrary processes (ioprio_set(2));
- apply migrate_pages(2) to arbitrary processes and allow processes to be migrated to arbitrary nodes;
- apply move_pages(2) to arbitrary processes;
- use the MPOL_MF_MOVE_ALL flag with mbind(2) and move_pages(2).
Service is run as root.
When running as non-root, capability CAP_DAC_READ_SEARCH does allow reading of other users' files, but doesn't allow traversing directories or querying for file existence. Thus, basic operation of this daemon is unavailable.
For security reasons, it would be sensible for daemon to run as non-root, however, there doesn't seem to be suitable capability to allow iterating directory contents or traversing filesystem while ignoring permissions. Such functionality is critical, feasible options for non-root operation are lacking, so daemon runs as root.
When running system D-Bus, it is possible only as root. For security reasons it would be advisable to run as non-root, but in D-Bus that is not possible.
- Find PID,
systemctl status spammer-reporter - Run command:
gawk '/^CapEff/ {print $2}' /proc/<PID-OF-PROCESS>/status | xargs -n1 -I {} capsh --decode={} - Expected output:
0x0000000020804004=cap_dac_read_search,cap_ipc_lock,cap_sys_nice,cap_audit_write
Note: See man-page for definitions of: Permitted, Inheritable, Effective, Bounding and Ambient capability sets.
See subdirectory SElinux/ for details.
- Use SElinux-tool
runcon. Set system_u:system_r:spammerblock_t:s0 as security context.
runcon system_u:system_r:spammerblock_t:s0 \
/usr/libexec/spammer-block/bin/python \
spammer_block_commands/reporter_service.py \
system \
--config /etc/sysconfig/spammer-reporter.tomlsemodule -DBNow all possible deny-rules are logged and can be traced with audit2allow.