Provisioning scripts that report an error and keep going are worse than scripts that never run at all. Client setup touches protected keys, an identity at the gateway, and a service that may already be serving traffic. Each of those needs a stop condition, and the script needs to know which stage it reached when it stopped.

Treat the whole thing as a transaction with five checkpoints, and make every checkpoint refuse to continue on its own.

Fix the inputs before touching the filesystem

Start with an explicit contract: one identity, one profile source, one intended service owner. Verify that the profile exists, that the certificate and key it references are readable, and that the target directory belongs to the expected package. A path that resolves is not evidence that the material behind it is complete.

Reject empty variables early. An unset $DEVICE that expands to nothing turns install -m 400 $KEY /etc/openvpn/client/ into a copy aimed at a directory, and the failure arrives later as a service that cannot read its key.

Assemble the profile set in a staging directory

Build the complete set somewhere private first: profile, CA, client certificate, key, and any helper the profile calls. Copy with install -m 400 -o root -g root so permissions are set at creation rather than afterwards. Then check the references inside the profile, because a relative ca or key line resolves against the unit's working directory once the service starts. Nothing should be written into the live directory until the whole set validates.

Record a non-secret fingerprint of what this run produced, for example sha256sum office.conf stored beside a version marker. That single value is what lets a rerun tell an update from a no-op.

Swap atomically and keep the previous set

Move the staged set into place as one operation where the filesystem allows it, and keep the reviewed previous set under a timestamped name. A partial copy that leaves a new profile beside an old key produces a service authenticating with the wrong identity and logging nothing useful about why. If the swap cannot be atomic, order the moves so the profile lands last.

When the target already exists, decide explicitly whether this run is an update, an identical rerun, or a conflict with another device's profile. Silently overwriting a profile that belongs to a different identity is how two machines end up sharing one certificate. Refuse the run and name the conflicting owner instead.

Activate one owner and prove it

Pick one activation path, either the systemd instance or NetworkManager, and use it consistently. Starting an unmanaged openvpn process beside an enabled unit gives two tunnels, competing DNS updates, and no clear answer to which one a stop command targets. After activation, check systemctl is-active openvpn-client@office, then the route to the remote prefix, then resolvectl status for the resolver the tunnel installed. Finish by reaching one resource the tunnel exists for.

A running process and a green tray icon are process facts, not evidence that the identity and traffic policy survived the handoff.

Rerun and retirement are separate workflows

A second run should converge on the same reviewed files and the same owner, not create a second account or a second daemon. Retirement is its own procedure: stop the exact owner, archive or delete the local profile per policy, and revoke the identity at the gateway. Removing a local key file does not revoke a copy that already left the machine.

Keep a recovery path that does not depend on the tunnel, such as console access or a second management route. A provisioning change that locks you out of the box is a failed change even when every stage reported success.

The script worth keeping is a transaction around ownership and evidence. A short list of privileged copy commands is not the same thing.

Mikanda mikatakata