Forty .ovpn files for one country arrive from a provider, and the plan looks obvious: copy a random one into place, then start the tunnel. Written as a pipeline inside ExecStart, that plan dies at boot with a status code that explains nothing. The unit file is not a script, and the repair is either to bring a shell in explicitly or to stop choosing files at all.

The symptom: a unit that starts and immediately dies

The status shows the service failed in a fraction of a second, and the journal line names a command that no shell would ever have received. Nothing about the tunnel appears, because the tunnel never began.

Reading the exact command line from the journal is the fastest way to confirm this. systemd echoes what it executed, and the string will show the pipeline characters as literal arguments rather than as operators.

Why ExecStart cannot run a pipeline

systemd splits the command line itself, applies its own quoting and variable substitution, and then executes the result directly. Pipes, redirection, globbing by a shell and command substitution simply are not part of that grammar. This is a design choice rather than an omission: a unit is declarative about what to run, and shell logic belongs in a script that the unit invokes. Both repairs follow from that, and neither requires giving the unit more privilege.

It also explains the misleading exit code. The first argument is treated as the binary, so a malformed line fails before any of your logic runs.

Do the selection in ExecStartPre

The direct repair is a preparation step that calls a shell explicitly, keeping the selection and the start in the same unit so a failure in one cannot leave the other running alone.

[Service]
Type=notify
ExecStartPre=/bin/bash -c "ls /etc/openvpn/profiles/au* | shuf -n 1 | xargs -r -I{} cp -f {} /etc/openvpn/%i.conf"
ExecStart=/usr/sbin/openvpn --config /etc/openvpn/%i.conf

Three details decide whether this survives contact with a real machine. Naming is the first trap: systemd substitutes %i while parsing the unit, long before bash runs, so the specifier has to sit outside single quotes. xargs -r prevents cp from running with no argument when the glob matches nothing. And a failing preparation step stops the unit, which is the correct outcome: starting with yesterday's configuration is worse than not starting. A random pick that always chooses the same file is usually not bad luck. Check whether the copied file is being overwritten by a package update, or whether the glob is matching a single stale entry.

Or let OpenVPN choose the endpoint

OpenVPN can already hold several endpoints in one profile and pick among them at random, which removes the shell, the copy and the write access from the boot path entirely.

client
remote-random
<connection>
remote au1.example.net 1194 udp
</connection>
<connection>
remote au2.example.net 1194 udp
</connection>

This form keeps server choice inside the file the provider authored, so a rotation that renames files does not break the unit. It also makes the candidate list auditable, which a glob over a directory never is. The shuffle still earns its place when profiles differ in more than an endpoint: different certificates, different routing directives, different cipher policy. In that case the choice is between whole configurations and a shell step is the honest way to express it.

Read back the instance and the file it used

Confirm which unit actually runs before debugging its contents. The packaged meta service and the template are infrastructure; the units that correspond to your files are the instances named after them.

systemctl list-units 'openvpn*'
systemctl cat [email protected]
journalctl -u openvpn@au -n 30 --no-pager

An instance nobody remembers enabling usually traces back to a stray configuration file in the package directory. Turn off that named instance instead of the template, otherwise the next package upgrade brings it back. After the change, start the unit and read the command line the journal reports, then confirm the configuration path it actually opened. That one check separates a working random pick from a service that is quietly reusing a file from last month.

ተዛመድቲ ጽሑፋት