Security Hardening

Lock down your systemd services with security options.

Quick Hardening

The --hardening flag enables a sensible set of security options:

mkunit service myapp \
  --exec "./server" \
  --hardening \
  --install

This adds the following to your unit file:

[Service]
# Filesystem protection
ProtectSystem=strict
ProtectHome=read-only
PrivateTmp=yes
ReadWritePaths=/var/lib/myapp

# Privilege restrictions
NoNewPrivileges=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes

# Network restrictions (if no network needed)
# PrivateNetwork=yes

# Additional isolation
ProtectHostname=yes
ProtectClock=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
RemoveIPC=yes

What Each Option Does

Filesystem Protection

Option Effect
ProtectSystem=strict Makes /usr, /boot, /efi, and /etc read-only
ProtectHome=read-only Makes /home, /root, /run/user read-only
PrivateTmp=yes Private /tmp and /var/tmp directories
ReadWritePaths= Explicitly allow writing to specific paths

Privilege Restrictions

Option Effect
NoNewPrivileges=yes Prevents gaining new privileges via setuid/setgid
PrivateDevices=yes No access to physical devices
ProtectKernelTunables=yes No access to /proc and /sys kernel variables
ProtectKernelModules=yes Cannot load kernel modules

Additional Isolation

Option Effect
ProtectHostname=yes Cannot change hostname
ProtectClock=yes Cannot change system clock
RestrictRealtime=yes Cannot use realtime scheduling
PrivateNetwork=yes Isolated network namespace (no network access)

Customizing Hardening

After creating a service with --hardening, you can edit it to fine-tune the security options:

# Create with hardening
mkunit service myapp --exec "./server" --hardening --install

# Edit to customize
mkunit edit myapp

Allow Network Access

By default, --hardening keeps network access. To explicitly deny:

[Service]
PrivateNetwork=yes

Allow Writing to Specific Directories

[Service]
ProtectSystem=strict
ReadWritePaths=/var/lib/myapp /var/log/myapp

Allow Device Access

For services that need hardware access:

[Service]
PrivateDevices=no
DeviceAllow=/dev/ttyUSB0 rw

Capability-Based Security

For services that need specific privileges without running as root:

mkunit service myapp \
  --exec "./server" \
  --user myapp \
  --cap-add NET_BIND_SERVICE \
  --cap-drop ALL \
  --install

This generates:

[Service]
User=myapp
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE

Common Capability Needs

Capability Use Case
CAP_NET_BIND_SERVICE Bind to ports below 1024
CAP_NET_RAW Raw network access (ping, packet capture)
CAP_SYS_TIME Set system clock
CAP_CHOWN Change file ownership

Security Score

Check your service's security score with systemd-analyze:

# Check security exposure score (lower is better)
systemd-analyze security myapp.service

# See detailed breakdown
systemd-analyze security myapp.service --no-pager

A well-hardened service should score below 2.0.

Example: Hardened Web Application

sudo mkunit service webapp \
  --exec "/opt/webapp/bin/server" \
  --workdir /opt/webapp \
  --user webapp \
  --group webapp \
  --hardening \
  --system \
  --install

Then edit to allow specific paths:

[Service]
# Allow writing to data and log directories
ReadWritePaths=/var/lib/webapp /var/log/webapp

# Allow reading SSL certificates
ReadOnlyPaths=/etc/ssl/certs

Example: Hardened Database Backup

sudo mkunit service db-backup \
  --exec "/usr/local/bin/backup-db.sh" \
  --type oneshot \
  --user backup \
  --hardening \
  --system \
  --install
[Service]
# Allow writing to backup directory
ReadWritePaths=/var/backups/db

# Allow reading database socket
ReadOnlyPaths=/var/run/postgresql
Start Strict, Relax as Needed

It's easier to start with --hardening and add permissions as needed than to try to lock down an already-permissive service. Check logs when the service fails - permission errors will indicate what needs to be allowed.

See Also