The job runs at login, the log shows the client starting, and the tunnel never comes up. Or it comes up and disappears a few seconds later. In both cases the property list was probably fine. launchd made two promises when it accepted the job: it will supervise a process, and that process will keep running. The VPN needs both, and a correct XML file provides neither.

A valid property list is not a working service

plutil -lint checks structure. It cannot tell you whether the binary exists, whether the arguments are supported by this client version, or whether the job starts in a context where the credentials are available. Those are semantic questions, and they account for most of the failures that look like launchd problems.

plutil -lint /Library/LaunchDaemons/com.example.vpn.plist
launchctl print system/com.example.vpn

Read the second command's output rather than assuming the job loaded. It reports the last exit status and whether the service is running, which distinguishes "never started" from "started and died" before you touch the profile.

Agents inherit a user session, daemons do not

Apple's launchd documentation separates per-user agents from system daemons. An agent runs in the logged-in user's context, with that user's keychain and environment. A daemon runs without that session, and moving a job into a system directory does not recreate the user's interactive credentials.

That distinction decides the design. If the connection requires a person to log in or approve an MFA prompt, the supported arrangement is a user-session job that runs after login. If it must work with nobody logged in, the client needs an approved unattended identity with its own renewal and revocation process. Choosing the wrong row produces a job that starts cleanly and fails at the first authentication step.

Do not paper over the difference with a password in the argument array or by disabling certificate checks. Both turn an authentication requirement into a permanent secret in a readable file, and the second one means the tunnel authenticates anything that answers.

Supervision only works if the process stays in front

A client that daemonises itself returns control to launchd immediately, and the supervisor then watches a process that has already exited. The tunnel may be up, but launchd thinks the job failed, and its restart policy will do something you did not intend.

Use the client's documented foreground or service mode. If the product has a supported startup feature of its own, that feature already knows how to stay in the foreground and how to hand off to the network stack; a custom job is a separate design, not an improvement on it.

Give the job a unique label, the intended executable, and a deliberate argument array. An array is not a shell command line, so pipes and redirections do not work there, and quoting behaves differently. A command that succeeds in an administrator's terminal is a preliminary check at best.

Credentials have to work without a prompt

Unattended operation moves the credential problem rather than removing it. Someone has to own the identity, decide how long it lives, and document how it is replaced. A certificate with no expiry monitoring becomes an outage with a calendar date on it.

Keep secrets out of arguments and out of the job's standard output. If the client supports reading a credential from a protected store, use that path, and check that the job's execution context can reach the store. A daemon running as root usually cannot read a login keychain item, which is the kind of detail that only shows up in the system context.

Define what ready means, then test it there

A successful launch proves the process started. It does not prove the network is up, the gateway accepted the identity, or the private application is reachable. If something else depends on the tunnel, it needs a readiness signal that reflects the connection, not the job's start time.

Retries should be bounded and should distinguish a missing network from a rejected credential. Relaunching on every failure cannot repair an expired certificate and produces a log full of identical attempts that hides the real one.

Accept the job in the context it will actually run in: no network at boot, a normal login, a wake from sleep, and a controlled stop. Then record the job label, the client version, and the profile revision together, so the next person inherits an attributable service instead of a file that once worked.

Nsɛm a ɛfa ho