There is a persistent myth that security is a product you buy or a clever technique you deploy. In practice, most real-world compromise is stopped — or enabled — by configuration that nobody finds interesting: which user a process runs as, which directories it can write to, whether it can gain new privileges. This is the unglamorous layer, and it is where the majority of practical defence actually lives. A breached web application matters far less if the process behind it cannot read your home directory, cannot escalate to root, and cannot write anywhere outside a single data directory.
This article walks through hardening a single Linux service the boring, effective way. The tools are old, standard, and free: a dedicated user, systemd sandboxing, key-only SSH, a default-deny firewall, and automatic updates. Nothing here requires a security budget. It requires about an hour and the discipline to do it before you need it, not after.
Least privilege starts with the account
The first question for any service is: who runs it? Far too many services still run as root, or as a shared account with a login shell and a home directory, because that was the path of least resistance. The correct answer is a dedicated, non-login system user that owns nothing but the service's own data.
Create it with no shell and no home login. On most distributions useradd --system --no-create-home --shell /usr/sbin/nologin appsvc does the job. This account should not be able to log in interactively, should not appear in sudoers, and should own only the directories the service genuinely needs to write. If an attacker gains code execution inside the process, they inherit exactly these limits — which is the whole point. Least privilege is not a slogan; it is the difference between an incident scoped to one data directory and an incident scoped to your entire host.
Defence-in-depth with systemd
Running as an unprivileged user is necessary but not sufficient. Modern systemd ships a rich set of sandboxing directives that layer kernel-level restrictions on top of ordinary Unix permissions. They are, in effect, a lightweight container built from namespaces, mount restrictions, and seccomp — configured in the unit file, with no extra runtime. Here is a representative unit for a small web service:
[Service]
User=appsvc
Group=appsvc
ExecStart=/usr/local/bin/appsvc
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/lib/appsvc
RestrictSUIDSGID=true
LockPersonality=true
CapabilityBoundingSet=
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
SystemCallFilter=@system-service
Each line earns its place. NoNewPrivileges=true stops the process — and everything it spawns — from ever gaining privileges through setuid binaries or file capabilities; it is one of the highest-value directives you can set. ProtectSystem=strict mounts the entire filesystem read-only for the service, and ProtectHome=true makes /home, /root, and /run/user inaccessible. PrivateTmp=true gives the service its own /tmp, closing a whole class of predictable-temp-file and cross-service leak attacks.
Against that read-only baseline, ReadWritePaths=/var/lib/appsvc carves out the one directory the service may write — a tight allowlist rather than a broad denylist. RestrictSUIDSGID=true forbids creating setuid/setgid files; LockPersonality=true blocks personality changes that are sometimes used to weaken address-space protections. Finally, an empty CapabilityBoundingSet= strips every Linux capability, so even a process that somehow reached uid 0 could not bind low ports, load kernel modules, or change file ownership. If your service genuinely needs one capability — say CAP_NET_BIND_SERVICE to bind port 443 directly — grant that single one explicitly and nothing more.
You do not have to guess whether this is working. Run systemd-analyze security appsvc.service and systemd will score the unit and list every restriction you have not yet applied. Treat a bad score as a to-do list. Tighten, restart, and re-check — and watch the logs, because the failure mode of over-restriction is a service that cannot write where it needs to, which is far easier to diagnose than a silent breach.
The host basics that surround it
A hardened unit sits on a host that also needs the boring treatment. None of this is novel, which is precisely why it works.
- SSH, key-only. Set
PasswordAuthentication noandPermitRootLogin no. Password auth on a public SSH port is the single most reliably attacked surface on the internet; key-only authentication removes brute-forcing as a threat entirely. - Default-deny firewall. With
nftablesorufw, drop everything inbound, then allow only the ports you actually serve — typically 22, 80, and 443. A default-deny posture means a service accidentally listening on a management port is not automatically exposed. - fail2ban. It watches your logs and temporarily bans addresses that fail repeatedly. It is not a substitute for key-only auth, but it trims noise and slows credential-stuffing against any web login you expose.
- nginx with TLS. Terminate TLS at a reverse proxy, keep the application bound to localhost, and use a certificate from Let's Encrypt or your CA of choice. This gives you one place to enforce modern ciphers, HSTS, and sane headers.
- Automatic security updates. On Debian and Ubuntu,
unattended-upgradesscoped to the security pocket; on RHEL-family systems,dnf-automatic. Most exploited vulnerabilities have a patch available before they are widely abused. Applying it automatically closes the window without waiting on a human.
Why this matters, and what to do next
The regulatory direction of travel makes the boring approach a professional baseline rather than a nice-to-have. Under NIS2 — whose national transposition deadline was 17 October 2024, though several member states ran late — essential and important entities are expected to manage risk with basic cyber hygiene, and management can be held accountable for failing to. The Cyber Resilience Act, in force since December 2024 with its main obligations applying from around 11 December 2027 (and vulnerability-reporting duties from roughly September 2026), pushes secure-by-default configuration onto products with digital elements. Least privilege and defence-in-depth are exactly what these frameworks mean by a reasonable state of the art.
What this means for you: if you run even one internet-facing Linux service, spend the hour. Give it a dedicated non-login user, wrap the unit in the sandboxing directives above, run systemd-analyze security and drive the score down, and put the SSH, firewall, and auto-update basics in place around it. None of it is glamorous, and that is the point — boring, well-understood configuration is the security that actually holds when something goes wrong.
This is the sort of unshowy, durable work I take on for small teams and solo operators: getting the fundamentals right so the exciting parts of the roadmap can be built on something that will not quietly fall over.