The service starts, reads the profile without complaint, and then dies on a line that looks unrelated: it cannot open ca.crt. Nothing about the certificate changed, and the profile path in the unit file is absolute. That combination is the tell. The path you passed locates one file, and the names inside that file are resolved somewhere else entirely, so a launch that looks fully qualified can still be reading its keys from a directory nobody intended.

The failure has a small number of causes, and they are worth separating in order: the base directory the process actually had, the launch context of the service, and the permissions or sandbox around the file once the path is right. Each one leaves a different trace, in the unit, in the process and on the filesystem.

The profile path is not a base directory

Picture a process that starts in /tmp and is handed /etc/vpn/prod/tenant-a.ovpn. Inside that profile, the line ca ca.crt names a relative path. Nothing in the argument that located the profile rewrites that name, so the process looks for /tmp/ca.crt and reports a missing file.

The confusing part is the ordering of the errors. OpenVPN parses the configuration first and opens referenced files as it reaches them, so a typo in the profile is reported before any key is touched while a missing certificate appears only once parsing gets that far. A profile that loads is therefore evidence that the syntax is fine, not that the files it names were found.

What the manual separates: --config and --cd

The OpenVPN 2.6 manual describes two distinct options. One loads a configuration file. The other changes the process directory before configuration, key, script and other files are read, and it requires an absolute directory with no reference to the current one.

openvpn --cd /etc/vpn/prod --config tenant-a.ovpn

That invocation is a shape, not an instruction to launch someone else's profile. What it buys you is determinism: the base for every relative name in the chain is stated in the command rather than inherited from whatever shell or manager started the process. The alternative is equally valid and often better for a service: leave the working directory alone and make every referenced file absolute inside the profile.

Whichever you pick, audit the whole chain rather than the first failure. A profile can pull in another configuration file, and the included file's relative names are resolved against the same process directory, not against the directory of the file that included them. Log paths, authentication files and helper scripts follow the same rule.

The service starts somewhere else

An interactive shell in the profile directory proves nothing about a packaged unit. Managers set their own working directory, and a wrapper script may change it again before the binary runs. The unit that fails is the only launch worth reasoning about.

systemctl show -p WorkingDirectory -p ExecStart --value openvpn@tenant-a
ls -l /proc/$(pgrep -n openvpn)/cwd

The second command shows the directory the live process actually holds, which is more trustworthy than the unit file when a wrapper is involved. If the two disagree, the wrapper is your answer and the unit is a red herring.

After the lookup: permissions, chroot and sandboxing

A correct base directory can still produce the same message, because resolution and access are separate steps. The process needs execute permission on every parent directory and read permission on the file, and a service unit that hides part of the filesystem changes what the process can see at all.

Three common variants produce an identical error. The file is readable only by root while the service drops privileges before opening it. The unit sets a protective filesystem view that does not include the profile directory. The daemon runs with a chroot, so the path that exists on the host does not exist inside that view.

Diagnose with the file's ownership and the process identity, not by loosening permissions. Making a private key world-readable to silence this error trades a loud failure for a quiet one, and the key is the part of the directory you least want exposed.

Prove the fix with the owned launch

Restart the real unit from a directory unrelated to the profile and watch the same startup line. Then confirm the process is holding the intended directory while it runs, and that the certificate it loaded is the one you expect.

systemctl restart openvpn@tenant-a
systemctl status openvpn@tenant-a --no-pager
journalctl -u openvpn@tenant-a -n 20 --no-pager

When path loading succeeds, authentication and routing are still separate stages with their own failures, and a connection that comes up but passes no traffic is a different investigation. The fix here is only about which directory the files came from, and it should be visible in the invocation rather than remembered by whoever last ran it by hand.

If the profile is deployed by a configuration manager, put the base directory or the absolute file names there as well. A manual fix on one host is the version of this problem that returns after the next rebuild.

ບົດຄວາມທີ່ກ່ຽວຂ້ອງ