Oh yes, thank you for spreading the good news.
'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/24