Hooks report success, and the same path still looks empty from a shell. Both statements can be true at once, because a systemd service may live in its own mount namespace with a private temporary directory. Once that is understood, the interesting question becomes whether the write succeeded at all.

Two temporary directories, one path

The isolation setting gives the unit a private view of the temporary directories rather than making them read-only. Processes outside the unit cannot see what it writes there, and anything it leaves behind is discarded when the unit stops. A path printed by the hook and a path checked from a shell can therefore refer to different objects with the same name.

That is a visibility problem, and it looks like a data loss problem. Nothing was deleted, because the file is simply in a namespace the shell is not a member of. The same command behaves differently when run inside the unit, which is the observation that usually resolves the argument. Read the unit's settings before you conclude anything about the write.

Separate invisible from unwritable

A successful write and a failed open produce different evidence, and the distinction decides the repair. If the hook says the file was created but an outside reader cannot find it, you have a namespace question. If the hook reports a permission error or a failed open, the file was never written and no namespace change will help.

systemctl show [email protected] -p PrivateTmp -p User -p Group -p RuntimeDirectory
systemctl show [email protected] -p MainPID --value
sudo ls -l /proc/PID/root/tmp/client-state

The process-root path exposes the service's own filesystem view, subject to permission checks on that process while it is still running. Read the journal for the unit in the same session, because a write error and a missing file are easy to conflate once somebody has already decided the cause.

Give shared data an explicit location

Data that only the service needs belongs in its private temporary directory. Data another process must read needs a path defined for both of them, with a stated owner, a mode and an expiry. Decide which of the two you have before editing the unit. Most of these incidents come from treating a private file as a shared one.

[Service]
RuntimeDirectory=openvpn-hook-office
RuntimeDirectoryMode=0750

A runtime directory places the folder under the system runtime area, creates it for the unit's configured user and group, and removes it when the unit stops. Add it through a drop-in so the package's unit stays intact, then reload the manager and restart that instance, accepting that its clients will reconnect.

Keep any runtime directory the installed unit already requires. Replacing the list instead of adding to it is a common way to break an unrelated feature that nobody connects to the change.

Two privilege layers can disagree

The service manager creates the directory for the unit's configured user, while the VPN daemon can separately drop to its own account before running a connection hook. Those are different layers, and they can name different identities.

A root-owned directory with a restrictive mode is perfectly visible to a daemon that has already dropped privileges, and still unwritable by it. The error then reads as a permission problem even though the path exists exactly where you expected. Compare the directory's owner with the identity the hook reports. When they differ, the mode was never the question.

Check the identity the hook really runs as before changing ownership. Changing the service's execution user to fix a directory can alter device access and network privileges elsewhere, which is a large price for a small problem.

Verify the whole handoff

Confirm four things at once: the hook writes under its real identity, the intended reader can consume the file, unrelated users cannot, and stopping the unit cleans up as designed. A file appearing in the host's shared temporary directory is not a requirement, and it is usually a sign that the exchange never got its own path. Test the reader after a restart, not only during the first run. Cleanup behaviour is part of the design, so observe it rather than assuming it.

Persistent state belongs somewhere with a matching lifetime, since a runtime directory is meant to disappear. Choose the location by lifetime rather than by convenience. That habit prevents the next version of this same investigation.

ХӀокхунах тера йаззамаш