Two least-privilege recipes pasted together do not produce a stricter deployment. One model lets the daemon start privileged and drop to an unprivileged account after initialization; the other has the service manager launch it under a restricted identity from the first instruction. Each works on its own, and the combination often produces a service that cannot do either job cleanly. The rest of this walks the decision in order.

Two models, one owner of the switch

The first model belongs to the daemon. Community OpenVPN's user and group options change identity after the tunnel and keys are set up, and persist-key plus persist-tun keep those resources usable afterwards.

The second model belongs to the service manager. A systemd User= or Group= setting decides the identity the process starts under, and capability directives decide what it may hold. Choosing between the models is a packaging question before it is a security question, and the package maintainer already answered it. Your job is to find that answer rather than to invent a third option.

Both are legitimate. The mistake is treating them as layers of one design, because the second model can make the first one's identity switch an operation the process is no longer allowed to perform.

Decide where the identity changes

If the unit already starts as a dedicated account, adding a daemon instruction to become a different account asks for a setuid transition that a hardened service was never meant to make. The failure appears as a permission error at startup rather than as a clear policy message. It reads like a missing capability, which sends people toward a wider bounding set instead of toward the identity question.

Read what the installed package actually ships before you edit anything, including its drop-in directory. Some units deliberately omit the daemon's own drop because the manager already handles it, and some rely on those options for the post-initialization switch. Removing either without knowing which model you inherited is how a working service stops starting.

Write the chosen model in one line: manager-owned or daemon-owned. Every later directive has to be consistent with that sentence, and a mixed unit should be treated as a bug rather than a compromise.

Capabilities are limits, not grants

A capability bounding set says what a process may hold at most. It is a ceiling, and listing a capability there grants nothing by itself, which is why a unit with a generous bounding set can still fail to create a tunnel.

What actually places a capability into the ambient set for a service is AmbientCapabilities=. The systemd execution reference documents the two directives separately, and the distinction explains most of the confusing capability advice online.

Add a capability only after the failing operation identifies it. A wider bounding set hides the real cause and leaves the service more privileged than the design intended. It also makes the unit harder to review, because the ceiling no longer describes the job. Remove the extra capability once the real fault is fixed.

Map authority across the lifecycle

List what the process must do at each stage: startup, steady state, reauthentication and teardown. Interface creation, route changes, reading a protected key and invoking a helper do not all happen at the same moment or under the same requirements. Sort the list by stage before you sort it by capability.

Persistence options preserve resources across renegotiation, and they do not grant permissions that were never held. A key that the process cannot read at startup will still be unreadable later, whatever the persistence flags say. The flags preserve what already worked.

Use a dedicated account rather than a shared one. Upstream OpenVPN warns against the habit of using nobody as a generic example, because other services on the host may already run as that account and the isolation you think you have is not there.

Features that change the local workload, such as data-channel offload or a build that links differently, can shift which operations are needed. Revisit the map when the package changes, not only when something breaks.

Write the plan down and keep recovery access

A deployment record should name the manager, the startup identity, the intended steady-state identity and each grant with the operation that requires it. That record is what makes the next change reviewable instead of experimental.

Keep console access available while you edit, because a unit that fails at startup is difficult to repair remotely. Test a restart and a full reboot before you call the change complete. A unit that survives a restart can still fail before networking is up.

Success is an understood minimum-authority lifecycle, not a process that happens to be running under a non-root name.

Winndannde jowitiiɗe heen