Harden a systemd Service with systemd-analyze security and Sandboxing

Reduce the damage a compromised service can do by using systemd's built-in security analysis and a careful first pass of sandboxing controls.

systemdService hardeningLinux sandboxing
Illustrated guide cover for Harden a systemd Service with systemd-analyze security and Sandboxing
systemd • Service hardening • Linux sandboxing
What you learn

How to score a unit with systemd-analyze security, add a drop-in override, apply a small set of safer restrictions, and re-test the service instead of blindly pasting a giant hardening checklist.

Best for

Operators already comfortable with basic unit files who want a better default security posture for long-running services.

Risk to watch

The wrong restriction can break file access, networking, or startup behavior. Start small and reversible.

Before you begin

  • A service unit you already understand at a basic level.
  • Root or sudo access on the host.
  • A way to test the service after each change.
  • Log access through journalctl.

The current systemd documentation describes systemd-analyze as both an analysis and verification tool, and one of its most practical subcommands for operators is systemd-analyze security. That command does not magically harden a service for you, but it does expose where a unit is still running with broad privileges and few containment controls. The right workflow is to inspect, apply a small reversible change, and test again.

Warning: Hardening is not a copy-paste contest. A strict profile that breaks your service silently is worse than a modest profile you understand and can operate safely.

Step 1: Score the current unit

Start by checking the unit's current exposure:

systemd-analyze security myapp.service

This output highlights categories such as filesystem access, privilege boundaries, capabilities, and sandboxing features. Treat the score as a guide, not a badge. A worse score does not automatically mean the app is insecure in context, and a better score does not prove the app is safe. What it does give you is a concrete list of hardening levers you can evaluate.

Also review the current unit and any existing overrides:

systemctl cat myapp.service
systemctl status myapp.service --no-pager

Understanding the current unit shape matters because some restrictions are only safe for services that do not need write access, raw networking, or privileged filesystem paths.

Step 2: Use a drop-in override, not a direct edit

Do not edit packaged unit files in place if you can avoid it. Use a drop-in override so your changes are easier to audit and less likely to be lost during package updates.

sudo systemctl edit myapp.service

This opens an override file under /etc/systemd/system/myapp.service.d/override.conf. That is the right place for your first hardening pass.

Why this matters: Drop-ins make it easier to compare your hardening layer against the vendor unit and to back out one file if the service stops behaving correctly.

Step 3: Apply a safe first hardening pass

Start with controls that are broadly useful and usually low-risk for many web apps and background services:

[Service]
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
ProtectControlGroups=true
ProtectKernelModules=true
ProtectKernelTunables=true
RestrictSUIDSGID=true
LockPersonality=true
MemoryDenyWriteExecute=true

Not every one of these is safe for every service, but as a first pass they are a practical review set. The key is that they are understandable:

  • NoNewPrivileges=true prevents the service from gaining new privileges through exec transitions.
  • PrivateTmp=true isolates its temporary directory view.
  • ProtectSystem=full makes much of the system tree read-only.
  • ProtectHome=true blocks casual access to user home directories.

If the service needs a writable location, provide it explicitly instead of leaving the whole filesystem broad by default. For example:

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

This is where real service knowledge matters. A safe hardening pass narrows access deliberately rather than pretending the service can run with no state at all.

Step 4: Restart and verify behavior

After saving the drop-in, reload systemd and restart the service:

sudo systemctl daemon-reload
sudo systemctl restart myapp.service
sudo systemctl status myapp.service --no-pager
journalctl -u myapp.service -n 100 --no-pager

Then test the service from the perspective that matters. If it is a web app, make an HTTP request. If it is a worker, run a controlled task. If it writes to disk, verify that write path still works.

Finally, rescore the unit:

systemd-analyze security myapp.service

You are looking for two things at once: a tighter security profile and a service that still behaves correctly.

Step 5: Tighten based on real service needs

Once the first pass works, you can go further based on what the service actually does. Examples include:

  • ProtectClock=true if the service does not need to adjust time.
  • PrivateDevices=true if it does not need device access.
  • CapabilityBoundingSet= to strip Linux capabilities it should not keep.
  • RestrictAddressFamilies= to limit the socket families it can use.

These stronger controls are where many breakages begin, so add them one by one. The systemd tools give you inspection, but you still have to reason about the application.

Rollback and recovery notes

If the service fails after a hardening change, remove the last restriction first instead of tearing down the whole hardening layer blindly. The clean rollback path is the override file itself.

sudo systemctl revert myapp.service
sudo systemctl daemon-reload
sudo systemctl restart myapp.service

If you only need to undo one new directive, edit the drop-in, remove that line, reload, and test again. Keep a copy of the working override in version control or an ops repo so good hardening changes do not become one-off shell history.

Troubleshooting common systemd hardening problems

The service starts failing with permission errors.
Check whether ProtectSystem, ProtectHome, or missing ReadWritePaths blocked a location the app actually needs.

The unit no longer reaches the network correctly.
Review restrictions such as RestrictAddressFamilies or capability changes if you added them. Start from the last known-good step, not from zero.

The score improved, but I do not understand why.
Read the systemd-analyze security categories alongside the actual directives in your drop-in. The point is operating understanding, not chasing a prettier number.

The packaged unit changed after an update.
That is exactly why the drop-in approach matters. Use systemctl cat to review the new combined unit and confirm your override still makes sense.

Expected outcome: You now have a repeatable hardening workflow: inspect, override, test, re-score, and iterate without turning the service into an unreadable pile of restrictions.

What to do next

Once a service is durable and more contained, the next useful step is protecting its browser-facing admin path. Continue with Protect Internal Dashboards with Authelia, Nginx, and an Identity-Aware Gate.