There is no Import-VpnProfile cmdlet, and the one that looks closest, Add-VpnConnection, never reads a .ovpn or .conf file. It assembles a native Windows profile from parameters you pass in, which is why the first checkpoint here is the file format and not the command line. If the gateway speaks SSTP, IKEv2, L2TP/IPsec or PPTP, Windows can own the profile and the rest of the list applies. If you were handed an OpenVPN or WireGuard file, stop at step one: those clients own their importers.

Step 1: Confirm the profile Windows can actually own

Run Get-Command -Module VpnClient on the machine before anything else. Windows 10, Windows 11 and Server 2016 or later ship the module; if it is missing, no copied script will make this host accept a native profile. The cmdlets that matter are Add-VpnConnection, Get-VpnConnection and Remove-VpnConnection, and the parameter list in the Add-VpnConnection reference is worth reading once in full.

Native means the Windows VPN stack terminates the tunnel. That covers IKEv2, often with a machine certificate; SSTP, which is TLS with user or certificate authentication inside; and L2TP/IPsec with a pre-shared key. An .ovpn file is an OpenVPN script, not a Windows connection: it carries directives such as remote, cipher or tls-auth that the Windows stack has no concept of. A WireGuard .conf is the same story with PrivateKey and AllowedIPs; converting one into the other is not a parameter choice but a different tunnel implementation.

There is a third branch. If an administrator hands you a VPNv2 CSP XML document or an Intune profile, that is the MDM path, applied by the provisioning stack rather than by Add-VpnConnection. Do not translate those node names into PowerShell parameters; ask which mechanism applies the XML instead.

Step 2: Pair the tunnel type with the authentication method

-TunnelType and -AuthenticationMethod are separate parameters, and the gateway only cares about the combination. Get one right and the other wrong and you get a profile that connects to nothing, or stalls with the tunnel already up. Set both explicitly instead of trusting a default that was chosen for a different service.

  • IKEv2 with MachineCertificate: the client presents a computer certificate; set -EncryptionLevel Required as well.
  • SSTP with Eap: TLS tunnel first, then PEAP/MSCHAPv2 or a certificate inside EAP. This needs the XML in step 3.
  • SSTP, L2TP or PPTP with MSChapv2, Chap or Pap: plain username and password, no EAP blob.
  • L2TP with MSChapv2 and -Lt2pPsk: the PSK covers the IPsec half, the user covers PPP; both must match the server.

A splat keeps the values in one place:

$p = @{
  Name                 = "Corp IKEv2"
  ServerAddress        = "vpn.example.com"
  TunnelType           = "Ikev2"
  AuthenticationMethod = "MachineCertificate"
  EncryptionLevel      = "Required"
  SplitTunneling       = $false
  DnsSuffix            = "corp.example.com"
}
Add-VpnConnection @p -PassThru

-PassThru returns the object Windows just wrote, so you can inspect it without a second call. If the name already exists the cmdlet refuses to run; -Force overwrites it, which is the wrong reflex on a production laptop because it silently discards whatever someone configured by hand.

Step 3: Hand over the credential material correctly

Passwords, pre-shared keys and EAP blobs are where these scripts usually break. -EapConfigXmlStream wants an XmlDocument, not a path, so read and cast the file first. Passing a string produces a conversion error that reads like a schema problem and sends people rewriting XML that was never wrong.

$eap = [xml](Get-Content -Raw .\peap-mschapv2.xml)
Add-VpnConnection -Name "Corp SSTP" -ServerAddress "sstp.example.com" `
  -TunnelType Sstp -AuthenticationMethod Eap `
  -EapConfigXmlStream $eap -EncryptionLevel Required

That XML is not something to invent from a blog snippet. It names the EAP method type and the inner authentication, and the gateway compares both against its policy. A mismatched inner method fails after the TLS handshake completes, which looks exactly like a bad password and is not one. Take the XML from whoever runs the gateway, or export it from a profile that already works.

For IKEv2 the certificate half is a store lookup. -MachineCertificateIssuerFilter takes a certificate object, and Windows uses it to find a usable client certificate in LocalMachine\My. The certificate needs a private key and a chain the server trusts; when either is missing, authentication fails with no useful detail, so check the store first:

Get-ChildItem Cert:\LocalMachine\My |
  Where-Object { $_.HasPrivateKey -and $_.NotAfter -gt (Get-Date) } |
  Select-Object Subject, Issuer, Thumbprint, NotAfter

Keep secrets out of the script file. A PSK passed on the command line or baked into a .ps1 ends up in transcripts, saved sessions and shared repositories, and -RememberCredential caches user credentials for later, turning a one-off test into a lasting grant.

Step 4: Decide the scope before the first run

The same cmdlet writes to two different phonebooks. Without -AllUserConnection the profile belongs to you and lives under %APPDATA%\Microsoft\Network\Connections\Pbk\rasphone.pbk. With it, from an elevated prompt, the profile goes to %ProgramData%\Microsoft\Network\Connections\Pbk\rasphone.pbk and is visible to every account, including the sign-in screen.

Scope is not cosmetic. An all-user profile is what a service account, a scheduled task or a pre-logon scenario can use; a per-user profile is invisible to all three. The objects are read differently too: Get-VpnConnection lists the current user entries, Get-VpnConnection -AllUserConnection lists the machine-wide ones. Pick one now, and treat a later change as delete and recreate rather than an edit:

# elevated prompt
Remove-VpnConnection -Name "Corp IKEv2" -Force
Add-VpnConnection -Name "Corp IKEv2" -ServerAddress "vpn.example.com" `
  -TunnelType Ikev2 -AuthenticationMethod MachineCertificate `
  -EncryptionLevel Required -AllUserConnection

Step 5: Test what a tunnel actually has to deliver

A connected icon proves the tunnel came up and nothing else. Connect from the command line first so failures arrive as text, not a dialog, then read back what Windows recorded:

rasdial "Corp IKEv2"
Get-VpnConnection -Name "Corp IKEv2" |
  Select-Object Name, ServerAddress, TunnelType, ConnectionStatus, SplitTunneling
Get-NetRoute -DestinationPrefix "0.0.0.0/0"

The two failures users report while the status still says Connected are DNS and routing. Resolve an internal name and check which server answered; a profile without -DnsSuffix can tunnel perfectly and still fail every intranet lookup. Then look at the default route: with split tunneling off, the lowest-metric 0.0.0.0/0 route should point into the VPN interface, and with it on only the prefixes you expect should. If both look right and traffic still dies, the problem is past the profile, in policy or the gateway filter.

When the connection fails outright, the code narrows the step to revisit:

  • 691: the gateway rejected the credentials. Go back to step 3, not the tunnel.
  • 809: the IKEv2 handshake never completed. Check UDP 500 and 4500, and certificate trust.
  • 789: the L2TP/IPsec pre-shared key is missing or wrong.
  • 868: the server name did not resolve. Fix DNS before editing the profile.

Keep the working command next to the settings it encodes, so the next person can rebuild instead of reverse-engineering the profile from the UI. If you do have to rebuild, delete the entry and recreate it from the same parameters; patching a half-configured profile leaves stale routes and authentication state behind. And if the file was never native, go back to the client that owns it. A .ovpn dressed up as an IKEv2 profile is not an import, it is a support ticket.

Ngaahi tokoni fekau'aki