# Security Hardening Guide
This document lists every security-sensitive parameter in Runbound's systemd
service file and explains what breaks silently if it is missing or misconfigured.
**Quick check:** run this after every install or update:
```bash
runbound --check-config /etc/runbound/unbound.conf
```
---
## Capabilities
| `CAP_NET_BIND_SERVICE` | Cannot bind to port 53 — server fails to start |
| `CAP_NET_RAW` | XDP disabled silently — server runs normally on SO_REUSEPORT fallback |
| `CAP_NET_ADMIN` | XDP disabled silently |
| `CAP_BPF` | XDP disabled silently |
The recommended way to grant capabilities to the binary (without running as root):
```bash
sudo setcap cap_net_bind_service,cap_net_raw,cap_net_admin,cap_bpf+eip $(which runbound)
```
Or via systemd unit:
```ini
[Service]
AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_NET_ADMIN CAP_BPF
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_NET_ADMIN CAP_BPF
```
---
## Address families
| `AF_INET AF_INET6` | Server cannot open UDP/TCP sockets — fails to start |
| `AF_UNIX` | API socket unavailable |
| `AF_XDP` | XDP disabled silently — server runs normally without it |
Correct `RestrictAddressFamilies` line for a server with XDP:
```ini
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_XDP
```
---
## Locked memory
| `LimitMEMLOCK=infinity` | AF/XDP UMEM allocation fails — XDP disabled silently |
```ini
[Service]
LimitMEMLOCK=infinity
```
This is required because the AF/XDP UMEM region is a large shared memory area
that the kernel must pin (lock) in RAM. Without this limit, `mmap(MAP_LOCKED)`
fails with `ENOMEM` and the XDP path is silently skipped.
---
## eBPF / kernel hardening
| `MemoryDenyWriteExecute` | `false` | eBPF JIT compilation blocked — XDP disabled silently |
| `ProtectKernelModules` | `false` | eBPF program loading blocked — XDP disabled silently |
```ini
[Service]
MemoryDenyWriteExecute=false
ProtectKernelModules=false
```
These two directives are commonly set to `true` in hardened systemd profiles.
They must be relaxed for the eBPF XDP program to load. All other kernel hardening
directives (`ProtectKernelTunables`, `ProtectKernelLogs`, `ProtectClock`, etc.)
can remain enabled.
---
## Configuration file
| `rate-limit: 0` | Rate limiting disabled — all source IPs have unlimited query rate |
| `rate-limit: 200` | Recommended default for home / residential resolvers |
| `rate-limit: 5000` | Recommended for shared or production resolvers |
> **Version note:** in v0.4.6 and below, `rate-limit: 0` set the bucket to 0 tokens
> and refused every query. Fixed in v0.4.7 — `0` now correctly means unlimited.
---
## Verifying security at startup
Check the following lines appear in logs after every install or update:
```bash
Expected output:
```
INFO runbound: CPU affinity enabled — physical cores (HT excluded) cores=N
INFO runbound::dns::server: cache size auto-sized from MemAvailable cache_size=N
INFO runbound::dns::server: DNS UDP listening (SO_REUSEPORT) addr=0.0.0.0:53 sockets=N
INFO runbound: XDP kernel-bypass fast path active iface=ethX
INFO runbound::dns::server: rate limiting disabled ← or: rate limiting enabled limit=N
```
**If `XDP kernel-bypass fast path active` is absent**, check the following in order:
1. `runbound --check-config /etc/runbound/unbound.conf` — will identify the exact missing parameter
2. Capabilities: `cat /proc/$(pgrep runbound)/status | grep CapEff`
3. `AF_XDP` in `RestrictAddressFamilies`
4. `LimitMEMLOCK=infinity` in the service file
5. `MemoryDenyWriteExecute=false` and `ProtectKernelModules=false`
---
## Complete hardened service file (with XDP)
```ini
[Unit]
Description=Runbound DNS Server
After=network.target
Wants=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/runbound /etc/runbound/unbound.conf
Restart=on-failure
RestartSec=5
User=runbound
Group=runbound
# Capabilities — port 53 + XDP kernel-bypass
AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_NET_ADMIN CAP_BPF
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_NET_ADMIN CAP_BPF
NoNewPrivileges=yes
# XDP requires AF_XDP + unlocked memory + eBPF JIT
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_XDP
LimitMEMLOCK=infinity
MemoryDenyWriteExecute=false
ProtectKernelModules=false
# General hardening (compatible with XDP)
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ProtectKernelTunables=yes
ProtectKernelLogs=yes
ProtectClock=yes
ProtectControlGroups=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
[Install]
WantedBy=multi-user.target
```
---
See [security.md](security.md) for the full security model and audit findings.