Skip to content
vast-cow's blog
Go back

Designing a Secure Directory Layout for Services That Start as Root and Then Drop Privileges

Edit page

Services that start as root, perform a small set of privileged operations, and then drop privileges and run long-term as an unprivileged user can be made significantly more robust by enforcing a clear rule:

Strictly separate “paths writable by root” from “paths writable by the post-drop service user,” and minimize writable locations after privilege drop.

A practical way to achieve this is to follow the Filesystem Hierarchy Standard (FHS) and design your file placement so that only a small, explicitly intended set of directories remains writable after the service becomes unprivileged.

1) Read-Mostly Configuration (Managed by Root)

/etc/<svc>/

Use this for static configuration files that are read at startup (YAML/TOML/INI, etc.), not modified during runtime.

Typical permissions:

Key rule: Do not place files under /etc that the service updates while running. Runtime writes to /etc blur the privilege boundary, invite configuration corruption, and increase operational risk.

Additional note (why this matters operationally): If the service user can write anything under /etc/<svc>, then a compromise after the privilege drop can become a persistent compromise (e.g., rewriting config to execute a different binary, change endpoints, or disable auth). Treat /etc as root-owned policy, not mutable state.

1b) Service Code, Libraries, and Helper Binaries (Root-Owned, Read-Only in Practice)

/usr/lib/<svc>/ or /usr/libexec/<svc>/

Use this for:

These directories should be read-only in practice (writable only by root).

Added clarification: /usr/lib/<svc> vs /usr/libexec/<svc> Both are commonly used for “service-owned implementation artifacts,” but they have different intent:

In practice, distributions vary, and you will see either directory used for both roles. The key security property is consistent: root-owned and not writable by the post-drop user.

Security note (supply-chain and persistence): If the post-drop user can write into /usr locations, then an attacker can replace binaries/modules and gain persistent code execution on restart. Keeping /usr root-owned and read-only is a core part of maintaining a trustworthy runtime.

2) Runtime-Only (Volatile) State

/run/<svc>/ (historically /var/run)

Use this for:

Typical permissions:

Because /run is cleared on reboot, never store persistent data here.

Added guidance (ownership patterns):

Safety note (socket/lock handling): UNIX sockets and lockfiles can become privilege boundary choke points. Ensure:

3) Persistent Service Data (Writable After Privilege Drop)

/var/lib/<svc>/

Use this for:

Typical ownership/mode:

Design principle: treat this as the primary (and ideally only) “core” writable area after privileges are dropped.

Added guidance (state integrity): Persistent state is frequently the highest-value target after compromise. Consider:

4) Cache (Rebuildable, Can Be Deleted)

/var/cache/<svc>/

Use this for:

Typical ownership/mode:

Added guidance (cache poisoning): Caches are often fed by external inputs. If cache contents influence execution (templates, bytecode, plugins, dynamically loaded assets), treat them closer to code than data. In that case, consider placing them under root-managed directories or using verification (hash/signature) before use.

5) Logs

/var/log/<svc>/ (or prefer journald)

Best practice is to centralize logs in journald and reduce file-based logging.

If writing to files:

Typical ownership/mode (varies with approach):

Added guidance (log permissions and rotation): Two common safe patterns:

  1. Service writes to stdout/stderr; journald collects

    • minimal filesystem writable surface
    • avoids logrotate permission complications
  2. Service writes to files

    • ensure the service only needs append, not arbitrary rewrite
    • coordinate with logrotate using copytruncate or create with correct ownership (depending on your application’s reopen behavior)
    • avoid letting the service user write logs in a directory that also contains root-written logs unless permissions are carefully designed

6) Temporary Files

Added guidance (why /run/<svc>/tmp is often best): It is:

Defining the Privilege Boundary in “Root Start → Drop Privileges”

A. Keep Root-Only Work to the Minimum

Common examples that may require root briefly:

After completing privileged steps, drop privileges in the usual sequence:

After dropping privileges, the service should not write to /etc or /usr.

Added implementation note (defense-in-depth): Dropping privileges is necessary but not sufficient. Pair it with filesystem policy so that even if an attacker gets code execution as <svcuser>, they cannot:

B. Constrain Writes to a Small, Dedicated Set of Paths

A strong default policy is:

This is a practical approach to enforce least privilege at the filesystem level.

Added guidance (separate “data” from “inputs”): If the service accepts user-controlled uploads, consider separating them from core state:

systemd Implementation Notes (Operationally Strong and Low-Risk)

If you use systemd, declare directories and permissions in the unit file to reduce drift and eliminate manual setup errors.

Recommended unit directives:

Hardening examples:

This effectively turns writable locations into a whitelist, which is one of the strongest operational controls for services that drop privileges.

Added note (why the systemd directives matter): Using StateDirectory= / RuntimeDirectory= etc. also enforces consistent ownership and mode at start, which reduces the risk of:

A Typical “Final Form” Example (myservice)

A clean, production-friendly layout might look like:

Avoiding “Root Start” Entirely (Often the Best Option)

If the only reason for starting as root is “listen on 80/443,” consider safer alternatives:

These approaches reduce attack surface substantially by eliminating long-lived root execution.

Added note (principle of least privilege, applied): The goal is not only “drop privileges quickly,” but also “design the deployment so privileges are never needed.” When that is feasible, it typically yields the simplest, most auditable security posture.


Edit page
Share this post:

Comments


Previous Post
Capturing DHCP-Assigned Network Parameters Using a Temporary macvlan in a Linux Network Namespace
Next Post
Generate Reverse DNS (PTR) Names from IP Addresses via Python CLI