Who Watches Watchmen? – Integrating Elixir Applications with Systemd
hauleth.dev
hauleth.dev
I wish that every systemd example/sample/template came with _extensive_ hardening, since I find it quite confusing. I've used systemd-analyze security <SERVICE> to try to figure out what was needed. For Elixir, I've come up with:
UMask=077
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=full
ProtectHome=yes
ProtectControlGroups=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
RestrictNamespaces=yes
RestrictSUIDSGID=yes
LockPersonality=yes
PrivateDevices=yes
PrivateUsers=yes
ProtectClock=yes
ProtectKernelLogs=yes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
Plus the use of TemporaryFileSystem and BindPaths to limit the file system.'systemd-analyze security' is a fine thing, just make sure to use the latest version of systemd because it was really buggy in the past. For example, the version that ships in RHEL 8 is so buggy it's practically useless.
I came up with this for most of my services that do require a JIT compiler (so Java, dotnet, etc):
DynamicUser=yes
CapabilityBoundingSet=
DevicePolicy=closed
InaccessiblePaths=-/usr/bin /usr/sbin /mnt /media /var/www
LockPersonality=yes
NoNewPrivileges=yes
PrivateDevices=yes
PrivateMounts=yes
PrivateTmp=yes
PrivateUsers=yes
ProtectClock=yes
ProtectControlGroups=yes
ProtectHome=yes
ProtectHostname=yes
ProtectKernelLogs=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
ProtectProc=invisible
ProtectSystem=strict
RemoveIPC=yes
RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
SystemCallArchitectures=native
SystemCallFilter=~@clock @cpu-emulation @privileged @module @raw-io @reboot @mount @obsolete @swap @debug
You might want to throw away InaccessiblePaths if your application calls external binaries. The stuff I typically write shouldn't do it.Some of these flags are not strictly necessary because they should be enabled by other switches, but I prefer to keep them to make configuration more obvious and to mitigate possible bugs (there were some in the past).
If your application needs to store anything locally, add some combination of these:
RuntimeDirectory=appname # adds /var/run/appname
StateDirectory=appname # adds /var/lib/appname
CacheDirectory=appname # adds /var/cache/appname
LogsDirectory=appname # adds /var/log/appname
ConfigurationDirectory=appname # adds /etc/appname
and you can read the resulting path in environment variables RUNTIME_DIRECTORY / STATE_DIRECTORY / CACHE_DIRECTORY / LOGS_DIRECTORY / CONFIGURATION_DIRECTORY if your systemd is new enough.systemd will make sure that your limited user can read and write these paths, including their content.
Add this if your application does not use a JIT compiler:
MemoryDenyWriteExecute=yes
And this to prevent it from listening on wrong ports in an event of misconfiguration. SocketBindDeny=any
SocketBindAllow=tcp:5000
These firewalling flags can be useful if your service does not do much networking to external APIs: IPAddressDeny=any
IPAddressAllow=localhost
IPAddressAllow=10.3.42.0/24However it may be nice follow-up article where I will describe full hardening process.
Does this require a good understanding of D-Bus to understand? Because I got completely lost on this...
D-Bus is not merged in to systemd. When systemd notices that a service it started is named "dbus", it then registers itself as a D-Bus service; programs (like `systemctl` or `poweroff` then use use D-Bus to send it commands. For comparison, sysvinit accepts commands via a named pipe (`/run/initctl`, or `/dev/initctl` for older versions of sysvinit).
Most services are not made to be D-Bus daemons. A service is only made to be a D-Bus daemon if its unit file says `Type=dbus`; none of the examples in the article say this. You can use D-Bus to ask systemd to start or stop a given service; same as you could use `telinit` to use /run/initctl to tell sysvinit to change the runlevel; this does not make a service started this way a D-Bus service.
The NOTIFY_SOCKET and watchdog functionality that the article discusses... this functionality has nothing to do with D-Bus.
I find the design incredibly charming !
This is pretty neat and I love how cleanly the application code reads. I’m curious: is the Erlang VM super fast? I would have expected VM startup time to dominate the overall time to start.
[0]: https://podman.io/
Erlang VM startup is ok, but it is not ultra fast, and it can be easily slowed down with many modules in releases or slow applications. Additionally as it was said - Erlang works best in case of long-running instances where the VM is handling spawning and managing of short lived internal processes.
First draft of this article also included the socket activation and FD passing section, but these were making the article way to long, so I moved them to the Part 2 where I will have more space for them. With socket activation Erlang VM startup time is negligible problem.
:systemd.ready(),
:systemd.set_status(down: [status: "drained"]),
{Plug.Cowboy.Drainer, refs: :all, shutdown: 10_000},
:systemd.set_status(down: [status: "draining"])
Shouldn’t the status be set to “draining” first and then, after drain is complete, to “drained”?Is there a standard protocol for either health probes or watchdog timers? It seems like people aren’t defaulting to SNMP like they used to, and I haven’t heard much about RFC 6241 NETCONF which might be intended to replace it. At work we just probe health with trivial RPCs, but it’d be better for the industry to converge on something.