Scripts start VPN clients at whatever layer the author picked, and that layer is rarely the one the job needed. The vendor command line, the operating system's dialer and a mobile app intent all return at different points in the connection's life. They also handle the secret differently, and that difference decides whether the password is visible to every user on the machine. Choose the layer before you write the first line of the script.

Layer 1: the vendor command line

Most desktop clients ship a command line that takes a profile name and, too often, a password. Cisco's legacy client accepted a connect verb with user and password arguments; macOS exposes the built-in service through networksetup; NetworkManager answers to nmcli. These calls return when the command finishes, which is not the same moment as the tunnel becoming usable. Read the vendor's own documentation for the version you deploy, because these interfaces change and some are removed entirely.

nmcli connection up "Work VPN"
networksetup -connectpppoeservice "Work VPN"

The advantage is that the vendor knows its own profile store, so the call uses the same configuration the GUI would. The disadvantage is that each vendor invents its own verbs, and a script written for one client does not transfer to another. Treat this layer as the shortest path when the client is fixed and the version is pinned.

Layer 2: the operating system's own service

On Windows the dialer is part of the operating system, and it works against a phonebook entry rather than the vendor's profile store. That makes it more portable across clients and easier to script from a service account. It also means the connection can exist in one place and not the other, which confuses people who look in the wrong list.

rasdial "Work VPN" user pass
Get-VpnConnection -Name "Work VPN" | Select-Object ConnectionStatus

Use the PowerShell surface when you need to create or remove the entry rather than dial it. The WMI and cmdlet layers expose the same objects under different names, and removal is a separate operation from disconnecting. A script that only dials will leave stale entries behind after a migration. Check which store your automation writes to before you promise that cleanup happens.

Remove-VpnConnection -Name "Old VPN" -Force
Get-VpnConnection

On macOS the equivalent is the network service list, where an existing service is connected by name rather than created on the fly.

Credentials leak at every layer, so choose the channel

Anything you pass on the command line shows up in the process table for every local user, and it usually survives in shell history as well. OpenVPN's supported alternative is to read the secret from a file with --askpass, and the file itself needs permissions that match the trust you place in it. A named pipe avoids writing the secret to disk but lets any process that can open it write there. On Windows, store the secret in the credential manager, or in a file that only the service account can read, and never as a literal in a batch file.

openvpn --config /etc/openvpn/work.conf --askpass /etc/openvpn/work.secret --auth-nocache --daemon
ls -l /etc/openvpn/work.secret

The same rule applies to tokens and one-time codes: if the automation needs them, the automation needs a channel that is not a command-line argument. Rotate the secret when you change the mechanism, because the old value has been sitting in a log somewhere. A script that works and leaks is not a working script.

Wait for state, then supervise one owner

Every one of these calls returns before the tunnel is ready, so poll the state the client exposes or probe a destination inside it with a timeout. Copying files or opening a database connection immediately after the connect call is a race, and it fails intermittently in a way that looks like a network fault.

ip link show tun0
nmcli -t -f GENERAL.STATE connection show "Work VPN"
curl -sS --max-time 5 https://app.internal.example/health -o /dev/null

Disconnect has the same problem in reverse: the process can exit before the routes are gone. Use the client's hooks where they exist, such as OpenVPN's up and down scripts or a systemd unit's ExecStartPost, because those run at the point you actually care about. Hooks run with the privileges of whatever invoked them, so a hook that restarts networking needs an explicit rule rather than an assumption. Keep exactly one owner for the connection's lifetime.

When two supervisors both start and stop that profile, the result is a reconnect loop that reads like a provider outage. The runbook ends when the layer is documented, the secret has a channel that is not argv, the wait condition is a state check with a timeout, and one process owns the lifecycle.

Artihkkalat mat laktásit