runbound 0.4.14

A DNS server. Just for fun.
[Unit]
Description=Runbound DNS Server
Documentation=https://github.com/runbound/runbound
After=network-online.target
Wants=network-online.target
# Restart up to 5 times within 5 minutes before giving up
StartLimitIntervalSec=300
StartLimitBurst=5

[Service]
Type=simple
ExecStart=/usr/local/sbin/runbound /etc/runbound/unbound.conf
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5s

# User / group created by install.sh (useradd -r runbound)
User=runbound
Group=runbound

# API key persistence — set here to survive restarts without regeneration.
# Leave commented to auto-generate on first start (saved to /etc/runbound/api.key).
# EnvironmentFile=-/etc/runbound/environment
Environment="RUST_LOG=runbound=info"

WorkingDirectory=/var/lib/runbound

# ── Systemd hardening (defence-in-depth) ────────────────────────────────────

# No privilege escalation from runbound user
NoNewPrivileges=true

# Private /tmp and /dev — the server has no reason to touch shared temp files
PrivateTmp=true
PrivateDevices=true

# Filesystem: read-only except for the two directories we actually need
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/etc/runbound /var/lib/runbound

# Kernel hardening
ProtectKernelTunables=true
# eBPF program loading requires kernel module access — disabled for XDP.
# Re-enable (=true) if not using XDP.
ProtectKernelModules=false
ProtectKernelLogs=true
ProtectControlGroups=true
ProtectClock=true
ProtectHostname=true

# Namespace isolation
RestrictNamespaces=true
LockPersonality=true

# Syscall filter — DNS server only needs inet sockets and file I/O
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_XDP
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM

# eBPF JIT requires executable memory — disable this guard when XDP is active.
# Re-enable (=true) if not using XDP.
MemoryDenyWriteExecute=false

# No realtime scheduling (unnecessary, prevents priority inversion attacks)
RestrictRealtime=true
RestrictSUIDSGID=true

# Clean up IPC objects on exit
RemoveIPC=true

# ── Capabilities ─────────────────────────────────────────────────────────────
# CAP_NET_BIND_SERVICE: bind to port 53 (< 1024) as non-root
# CAP_NET_RAW + CAP_NET_ADMIN + CAP_BPF: required for AF_XDP kernel-bypass
# Remove the XDP caps if not using bare metal Intel NICs (ixgbe/i40e/ice/igc)
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_NET_ADMIN CAP_BPF
AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_NET_ADMIN CAP_BPF

# ── Resource limits ──────────────────────────────────────────────────────────
# 512 MB hard cap — the memory guard already purges caches at 80 % system RAM,
# but this systemd limit is a belt-and-suspenders last line of defence.
MemoryMax=512M
# File descriptors: 32 SO_REUSEPORT UDP sockets + TCP + DoT + feeds HTTP
LimitNOFILE=65536
# AF_XDP UMEM requires locked memory — default limit (~64 KB) is insufficient
LimitMEMLOCK=infinity
# Tokio thread pool + hickory tasks
TasksMax=4096

[Install]
WantedBy=multi-user.target