Run systemd-analyze security [UNIT...] to check other available protections.

Examples

Create the user with $HOME at /run/<username> and configure RuntimeDirectory, so that systemd will create and chown the directory automatically.

Set RuntimeDirectoryPreserve to no to discard its content when service stops or restarts. If you need more persistence, use StateDirectory and /var/lib/<username> instead.

Grant CAP_NET_BIND_SERVICE so that the service could bind to well-known ports (0 to 1023).

[Unit]
Description=<description>
After=network.target

[Service]
Type=simple
ExecStart=<start-command>
User=<user>
Group=<user>

# Grant user writable access to home and working directory that persists until system reboot,
# because /run is a mount point of "tmpfs".
WorkingDirectory=~
RuntimeDirectory=<username>
RuntimeDirectoryPreserve=yes

# Hardening
NoNewPrivileges=true
RestrictNamespaces=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictSUIDSGID=yes

# User isolation
PrivateTmp=yes
PrivateUsers=yes
ProtectHome=yes

# Mount entire file system hierarchy read-only as much as possible
ProtectSystem=strict
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectControlGroups=yes
ProtectProc=invisible

# Limit capabilities
CapabilityBoundingSet=CAP_NET_BIND_SERVICE

# Grant capabilities
AmbientCapabilities=CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target

To use BindPaths=, ProtectHome=tmpfs should be used instead and DynamicUser= should be avoided.

# Hardening
NoNewPrivileges=true
RestrictNamespaces=yes
RestrictAddressFamilies=AF_INET AF_INET6
RestrictSUIDSGID=yes

# User isolation
RemoveIPC=yes
PrivateTmp=yes
PrivateUsers=yes
ProtectHome=tmpfs
BindPaths=/home/<user> /home/other/storage

# Mount entire file system hierarchy read-only as much as possible
ProtectSystem=strict
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectControlGroups=yes
ProtectProc=invisible

# Limit capabilities
CapabilityBoundingSet=