Harden Linux Services with `systemd-analyze security`: From Score to Enforceable Policy
Source: Dev.to
Introduction
If you run long‑lived services on Linux, hardening often gets postponed because it feels risky: “What if I break production?”
A practical way to avoid guesswork is to use systemd-analyze security as a baseline, then apply hardening in small, testable drop‑ins. This guide walks through a repeatable workflow you can use on Debian/Ubuntu/Fedora/Arch hosts that run systemd.
systemd-analyze security evaluates a unit against sandboxing and privilege‑related settings and reports an exposure score with detailed findings. It helps you prioritize what to harden first.
Important: the score is a heuristic, not a proof of safety. Use it to guide improvements, then validate behavior with service‑level tests.
Prerequisites
- Linux host with systemd
- Root or sudo access
- A non‑critical service to practice on first
Check your versions:
systemd --version
systemd-analyze --version
Inspect the Current Exposure
Pick a service (example: nginx.service) and inspect its current exposure:
sudo systemd-analyze security --no-pager nginx.service
Capture the output so you can compare before/after:
sudo mkdir -p /var/log/systemd-hardening
sudo systemd-analyze security --no-pager nginx.service \
| sudo tee /var/log/systemd-hardening/nginx.before.txt
Create a Drop‑In
Never edit vendor units directly. Use a drop‑in:
sudo systemctl edit nginx.service
Paste the following into the editor that opens:
[Service]
# Privilege/sandbox basics
NoNewPrivileges=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectSystem=full
ProtectHome=read-only
ProtectControlGroups=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
LockPersonality=yes
RestrictSUIDSGID=yes
# Networking/process constraints (keep AF_INET/AF_INET6 if web‑facing)
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
RestrictNamespaces=yes
# Resource guardrails (tune for your workload)
CPUQuota=80%
MemoryHigh=400M
MemoryMax=600M
TasksMax=512
Reload and Test
sudo systemctl daemon-reload
sudo systemctl restart nginx.service
sudo systemctl --no-pager --full status nginx.service
sudo journalctl -u nginx.service -n 100 --no-pager
curl -I http://127.0.0.1
If something breaks, revert quickly:
sudo rm -f /etc/systemd/system/nginx.service.d/override.conf
sudo systemctl daemon-reload
sudo systemctl restart nginx.service
Compare Before/After
sudo systemd-analyze security --no-pager nginx.service \
| sudo tee /var/log/systemd-hardening/nginx.after.txt
diff -u /var/log/systemd-hardening/nginx.before.txt \
/var/log/systemd-hardening/nginx.after.txt || true
You should see fewer risky findings and a lower exposure score.
Automate Audits
Create /usr/local/sbin/systemd-security-audit.sh:
#!/usr/bin/env bash
set -euo pipefail
OUT_DIR="/var/log/systemd-hardening"
mkdir -p "$OUT_DIR"
UNITS=(
nginx.service
ssh.service
docker.service
)
for u in "${UNITS[@]}"; do
echo "==> Auditing $u"
if systemctl list-unit-files --type=service | awk '{print $1}' | grep -qx "$u"; then
TS="$(date -u +%Y%m%dT%H%M%SZ)"
systemd-analyze security --no-pager "$u" > "$OUT_DIR/${u}.${TS}.txt" || true
else
echo "WARN: unit not found: $u"
fi
done
Make it executable and run:
sudo install -m 0755 /usr/local/sbin/systemd-security-audit.sh /usr/local/sbin/systemd-security-audit.sh
sudo /usr/local/sbin/systemd-security-audit.sh
Optional Daily Timer
Create the service unit /etc/systemd/system/systemd-security-audit.service:
[Unit]
Description=Run systemd security audit for selected services
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/systemd-security-audit.sh
Create the timer unit /etc/systemd/system/systemd-security-audit.timer:
[Unit]
Description=Daily systemd security audit
[Timer]
OnCalendar=daily
RandomizedDelaySec=20m
Persistent=true
[Install]
WantedBy=timers.target
Enable and start the timer:
sudo systemctl daemon-reload
sudo systemctl enable --now systemd-security-audit.timer
sudo systemctl list-timers --all | grep systemd-security-audit
Practical Tips
- Start with
ProtectSystem=fullbefore trying stricter settings. - Don’t blindly set
PrivateNetwork=yesfor network services. - Prefer
MemoryHighas the main throttle andMemoryMaxas the hard limit. - Hardening is service‑specific: apply incrementally, validate each change, and keep rollback steps ready.
References
- Systemd‑analyze(1) – security command
- systemd.exec(5) – sandboxing and execution options
- systemd.resource-control(5) – CPU/memory/task limits
- systemd.unit(5) and drop‑ins (systemctl edit workflow)
- Red Hat docs on cgroups v2 + systemd resource controls