Long uptime proves only that startup succeeded with the privileges the daemon held at that moment. The failures that matter here arrive later: a new client connects, a revocation list is replaced, a helper needs to write a file. Each of those is a fresh open performed by the unprivileged account, with its own permission question and its own error message. Three decisions separate a configuration that keeps working from one that fails on the next event.

Separate what was loaded early from what is opened later

A descriptor is a handle on an open file description, and the access mode was fixed when that description was created. Changing the process user afterwards does not close the handle, which is why a key file read as root keeps working after the drop. The mode does not change either: a handle opened for reading does not become writable, and a writable handle is not revoked just because the account lost its rights to the path. Those two facts explain most of the surprising behaviour in these reports.

The other half is memory. A daemon can parse a certificate, keep the parsed material, and close the file; what remains in use is data, not an open file. Continuing traffic therefore proves neither that a descriptor is open nor that the pathname is reachable. It proves that the material needed for the current session was obtained at some earlier point.

A later open is a different transaction. It runs under the current credentials, the current security context and the current mount view, so a path that was readable at boot can be refused at the next connection. Rotation adds a trap: writing a new file over the same pathname leaves an existing descriptor attached to the old object, so a reload can appear to succeed while the daemon still uses the previous contents. Compare the inode the process holds with the one on disk before believing a rotation took effect.

Name the three runtime reads and when each fires

Three resources are normally opened well after initialization, and they fail at different moments. A client-specific configuration file is read when that particular client connects, so it can break for one user while everyone else continues. Revocation data is consulted during peer authentication and again when the file is replaced. A temporary directory is touched only when a script or plugin writes something.

None of the three belongs to the startup path, which is why a server that has been healthy for weeks can fail on the next connection. The category also names the owner of the fix: the first two are read permissions on configuration under the daemon account, and the third is a writable location with a narrow purpose.

Path reopened laterWhat triggers the openFirst sign when it fails
Client configuration fileA connection for that client identityOne client rejected while others connect
Revocation dataPeer authentication, including after replacementRevoked and valid peers both refused
Script or plugin temporary directoryA helper needs to create a fileThe helper exits with a path error

persist-key answers a narrower question than most people assume. It keeps the key file open across the documented SIGUSR1 and ping-restart resets, and it does nothing for CCD files, revocation data, helper executables or a full process restart. Those still follow their own open paths, under whatever account is running at the time.

Confirm the account and the view it actually has

Ask the running daemon which account it is using instead of trusting the unit file, then compare the answer with what the unit sets. A mismatch is the first thing to resolve, because every later permission check runs as whoever the process really is. Then walk the path one component at a time; traversal is evaluated at each level, so a readable file inside an unreadable directory is still unreadable. The final file mode is the last question, not the first.

Namespaces and service sandboxes add boundaries that ownership and mode bits do not describe. A path visible in your shell can be absent or read-only inside a chroot, a mount namespace, or a unit that hides home directories. Security modules add one more layer, so a file that looks readable can still be denied when it is opened.

ps -o user:12,pid,args -C openvpn
systemctl show -p User -p Group -p ProtectHome -p RootDirectory [email protected]
sudo -u openvpn namei -l /etc/openvpn/ccd/alice

A successful read in the administrator's shell tests none of those combined conditions. Run the check as the daemon account, with the same namespace and sandbox, or accept that the result describes your shell rather than the server. The third command is the cheap version of that test: it prints the mode of each component and fails on the first one the daemon account cannot traverse.

Grant the runtime write in a directory of its own

Reading is only half of the inventory. A helper script may need to create a status file, a plugin may need to write state next to its configuration, and those writes are not covered by making the configuration tree writable. Give the specific directory the write permission its function needs, with an owner and a mode you can explain. Prefer read-only access for everything that is only read.

Keep the private key's permissions separate from the policy files. A key that must be re-read after a reset has a different requirement from a client file read on each connection, and one permissive mode across both hands out more access than the problem asks for. A dedicated daemon account with narrow ownership is easier to audit than a broad group. When the key has to survive resets, persist-key removes the re-read instead of widening the directory.

Decide from the event you can reproduce

A live tunnel proves one path through the code. The event that needs the file is the only test that covers the open you care about, whether that is a fresh client connection, a replacement revocation file, a permitted soft reset or a planned restart. Pick the event, run it, and keep the first error rather than the last.

For a soft reset, one signal and the unit's log show whether the key was read again. Send the signal, then read the last lines the unit wrote:

systemctl kill -s SIGUSR1 [email protected]
journalctl -u [email protected] -n 40 --no-pager

Do not conclude that a compromised daemon can no longer modify files simply because privileges were dropped. What it can still do depends on its remaining credentials, its capabilities, the directories it can write and any writable descriptor it still holds. Privilege reduction narrows authority, and the inventory of runtime opens is what shows how far. Write that boundary next to the configuration so the next person does not have to rediscover it.

Nuŋlɔɖi siwo ku ɖe eŋu