The service starts, a client associates, and no route ever appears on the server side. Or the daemon refuses to start and names a command it could not execute. Both outcomes follow the same three lines, which are usually copied together from a hardening guide: drop to an unprivileged account, drop the group, and change the filesystem root. Only one of the three hides files, and only one of them removes the privilege to change the routing table. Splitting them is how you learn which line you actually want.

What the jail removes that the daemon still needs

Changing the root happens after startup, so the daemon keeps whatever it already opened and loses everything it would open later. Any helper the connection runs after that point has to exist inside the new root, along with the shared libraries it loads. A jail built from a configuration file and a certificate has neither.

Dropping to an unprivileged account is a separate restriction. A process that has given up root may be unable to alter routes or bring an interface up even when the binary is right there, so the two failures have to be diagnosed apart.

Three conditions decide which fix you need

The first condition is how your build installs routes. Recent OpenVPN releases configure the interface and routing table in-process, while older packages and custom setups rely on external tools or on up and down scripts. If the log names a command, an external tool is involved and the jail is hiding it. If the log shows a permission error instead, the account is the problem.

The second is maintenance. A jail with copied binaries has to be refreshed whenever the package that provides them is upgraded, or the first upgrade leaves a broken tunnel behind. The third is whether a specific control actually requires a chroot; if the requirement is really about privilege separation, dropping the account may satisfy it alone.

Signal in the logOther side of the boundaryRepair direction
Command not found after startupBinary and its libraries absent inside the new rootPopulate the jail, or drop the chroot line
Operation not permittedAccount lost the privilege to edit routesKeep the account privileged, or pre-create routes elsewhere
No such deviceJail lacks the tun device nodeCreate the node inside, or open it before the root change

What a working jail has to contain

A jail that runs commands needs the binary, every library it links against, and any device node the daemon opens late. Copy the pieces and test them without the VPN in the picture:

ldd /usr/sbin/ip
chroot /etc/openvpn/jail /sbin/ip route show
chroot /etc/openvpn/jail /sbin/ip link set tun0 up

The first command lists the dependencies, and it is the one people skip. A copied binary that cannot find a library fails with a different message than a missing binary, and both look like an OpenVPN fault when read from the client side. Running the command inside the jail by hand reproduces the failure without restarting the service.

Copy the configuration, certificates and any CRL into the jail as well, because a file the daemon expects to re-read after startup will not be found once the root changed. Record what you copied, since the next upgrade of the networking package replaces the originals and leaves the jail on the old copy.

Shrinking the hardening instead

If the jail exists because a guide said so, removing that one line while keeping the account restrictions is a defensible outcome. The daemon keeps its filesystem view and loses the problem, and the privilege drop still removes root from the long-lived process.

Write down what remains, and why. A configuration that quietly lost a control without a note is how hardening drifts, and the next audit will ask why the line disappeared.

Bisect the three lines before choosing

Copy the configuration, disable all three directives, and confirm that a client connects and that the expected routes appear. Re-enable the account lines only, then the chroot alone. The step where the tunnel breaks identifies the boundary, and you avoid rebuilding a jail that was never the cause.

Once it routes again, run one client session at higher verbosity and read the line naming the command or the permission that failed. That line is worth more than the timeout the client eventually reports.

Ingingo zihuye