Windows Server for beginners. Part 28: Setting up a remote access VPN
All parts of the series “Windows Server for beginners”How IPsec is built, what happens in an IKEv2 exchange and how a VPN differs from a proxy is covered in parts 19 and 20 of the networking series. There the tunnel joined two networks. Here the tunnel joins an employee on a sofa at home to the office, and this case has its own questions: how the gateway knows it is really the employee connecting and not whoever picked up their laptop, which address to give out, where to let them in, and how all of this ties into the directory, NPS from part 26 and the certificates from part 25.
The role of Windows in this setup is to check users. The tunnel itself is usually terminated not by Windows but by whatever already stands at the network edge: a firewall, a router or a Linux machine with strongSwan. Windows Server has its own VPN role, Routing and Remote Access, but it is not used in this part. The gateway here is strongSwan: it is free, runs on any Linux, and its configuration shows the mechanics without extra layers. I have written before about installing it and configuring it in the modern swanctl format: moving from ipsec.conf to swanctl and swanctl with EAP-MSCHAPv2. There the users and passwords lived in strongSwan itself; today the domain checks them. Even earlier there was a post on an IKEv2 VPN on Ubuntu 20.04 — do not set up a gateway by it: it configures strongSwan in the deprecated ipsec.conf format and tells Windows clients to download a hand-built plugin. How to set up the gateway properly is covered below, in this part.
The layout
laptop at home internet network edge inside
┌───────────┐ ┌────────────────┐ ┌──────────┐
│ Windows │ ── IKEv2: UDP 500, 4500 ────► │ vpn-gw │ ── RADIUS ──► │ nps1 │ ── AD ──► dc1, dc2
│ client │ │ strongSwan │ UDP 1812 │ NPS │
└───────────┘ └────────────────┘ └──────────┘
address 192.168.50.x from the vpn-gw pool, 192.0.2.10 outside checks the password,
DNS 192.168.1.10 and .11 192.168.1.1 inside decides by policy
The name vpn.example.com is in the public zone and points to the external address of vpn-gw, 192.0.2.10. The gateway accepts IKEv2 on UDP 500 and 4500 (4500 is IKE and ESP wrapped in UDP to get through NAT, NAT-T from part 19), checks the user through nps1 over RADIUS, and gives the client an address from the 192.168.50.0/24 pool and the addresses of the DNS servers — the domain controllers. The internal network has to know that 192.168.50.0/24 is reachable through vpn-gw; if vpn-gw is itself the router of the internal network, as here, nothing needs to be known.
Three roles, three protocols between them:
| Between | Protocol | What's inside |
|---|---|---|
client — vpn-gw |
IKEv2 over UDP | the IPsec handshake, with EAP inside it |
vpn-gw — nps1 |
RADIUS, UDP 1812 | the same EAP messages, repacked into RADIUS attributes |
nps1 — controllers |
LDAP and Netlogon | who alice is, which group she is in, whether the answer to the MS-CHAPv2 challenge is right |
Over LDAP NPS reads the account and its groups, but it does not verify the MS-CHAPv2 response itself: it passes it to a controller over Netlogon — the secure RPC channel between a domain member and a controller over which NTLM is verified too (part 3).
The gateway knows no passwords and cannot check them: it only relays EAP between the client and NPS and gets back "let in" or "keep out". Exactly like the access point in part 27 — except that EAP travels not in EAPOL frames but inside IKE_AUTH messages.
How it works
In part 19 IKE_AUTH was a single exchange: both sides presented AUTH and the SA was ready. With EAP the exchange stretches out:
client (alice) vpn-gw (strongSwan) nps1 (NPS)
│ │ │
├─ IKE_SA_INIT ─────────────────────►│ │
│◄─ IKE_SA_INIT ─────────────────────┤ │
├─ IKE_AUTH: IDi, CP, SA, TS ───────►│ no AUTH = client wants EAP │
│◄─ IKE_AUTH: IDr, CERT, AUTH, │ │
│ EAP-Request/Identity ────────────┤ │
├─ EAP-Response/Identity: alice ────►│ │
│ ├─ Access-Request (EAP-Message) ─►│ is alice in AD?
│ │◄─ Access-Challenge (EAP) ───────┤ MS-CHAPv2 Challenge
│◄─ EAP-Request: MS-CHAPv2 ──────────┤ │
├─ EAP-Response: MS-CHAPv2 ─────────►├─ Access-Request (EAP-Message) ─►│ verify the response
│ │◄─ Access-Challenge (EAP) ───────┤ MS-CHAPv2 Success
│◄─ EAP-Request: MS-CHAPv2 ──────────┤ │
├─ EAP-Response: MS-CHAPv2 ─────────►├─ Access-Request (EAP-Message) ─►│
│ │◄─ Access-Accept + MS-MPPE keys ─┤ VPN policy, group
│◄─ EAP-Success ─────────────────────┤ │
├─ IKE_AUTH: AUTH (from EAP keys) ──►│ keys match = password right │
│◄─ IKE_AUTH: AUTH, CP (address, │ │
│ DNS), SA, TS ────────────────────┤ │
│══ ESP ════════════════════════════►│ │
Step by step.
The client does not present AUTH. In the first IKE_AUTH it sends its identity but no proof: per RFC 7296 that is exactly what "I want to authenticate with EAP" means. The gateway answers with its own certificate and AUTH — that is, it authenticates itself first and only then asks for the user. The client checks the certificate: a chain to a trusted root, and the name vpn.example.com in the certificate matching what it connected to. This is the only protection against a fake gateway, and below you'll see why it matters.
The gateway's certificate travels in the response only if the client asked for it: in the first IKE_AUTH the client puts a CERTREQ payload listing the CAs it trusts, and strongSwan by default (send_cert = ifasked) sends certificates only in reply to it. Windows lists all of its trusted roots in CERTREQ, so the message gets big. The gateway's response gets big too: the gateway and intermediate CA certificates with 4096-bit RSA keys come to about 3.5 KB, more than the MTU. IKEv2 cuts such messages into fragments itself (IKE fragmentation, RFC 7383: each fragment is encrypted and reassembled by the receiver before parsing); Windows supports it since version 1803, strongSwan by default. Without it large UDP datagrams are split into IP fragments, and many home routers and ISPs drop those.
EAP inside IKE. Next, inside the encrypted IKE_AUTH, come EAP messages — the same protocol as in 802.1X: an identity request, the answer CORP\alice (the name as the user typed it), then the method. The gateway puts every message from the client into the EAP-Message attribute of a RADIUS Access-Request and sends it to NPS, and whatever NPS returns in an Access-Challenge it unpacks back into EAP and hands to the client. The gateway does not understand the content and doesn't need to.
The method is EAP-MSCHAPv2. NPS offers the client MS-CHAPv2: it sends a random challenge, the client computes a response from the challenge and the NT hash of its password, NPS passes the challenge and response to a domain controller over Netlogon and, once confirmed, sends its own response, by which the client makes sure the server knows the password too; the last exchange acknowledges success. Each connection takes three RADIUS request–response pairs: two Access-Challenge and an Access-Accept at the end. In Wi-Fi the same method was wrapped in PEAP — a TLS tunnel with the NPS certificate — because nobody protects EAPOL frames on the air. Here TLS isn't needed: the exchange already runs inside the IKE SA, encrypted with keys agreed in IKE_SA_INIT, and the party they were agreed with has proven itself with the vpn-gw certificate. That's why for VPN NPS gets EAP-MSCHAPv2 without PEAP; in the NPS method list it is called "Microsoft: Secured password (EAP-MSCHAP v2)", and it is what the Windows client offers by default. This is not the MS-CHAPv2 without EAP from the table in part 26: that one was carried in PPP (Point-to-Point Protocol — the protocol of dial-up and PPTP connections) with no protection of the exchange, while here every byte is covered by IPsec.
Hence the importance of the client checking the gateway certificate: if the client accepts any certificate, anyone who intercepts the connection gets its MS-CHAPv2 response to their own challenge, and that response can be brute-forced offline. Windows checks the certificate by default and won't connect without a trusted root.
The EAP keys become AUTH. In the Access-Accept NPS returns not just permission but key material — the MS-MPPE-Send-Key and MS-MPPE-Recv-Key attributes (RFC 2548), encrypted with the RADIUS shared secret. From them strongSwan assembles the MSK (master session key), the client derives the same key from its password and the MS-CHAPv2 exchange, and both sides use it for the final AUTH payloads. If the password was wrong, the keys won't match and AUTH fails; a fake RADIUS server cannot verify the response through a controller and produce the right keys. This is how IKE binds "the user passed EAP" to "this tunnel belongs to this user".
Configuration Payload. Already in the first IKE_AUTH the client asks for an address and parameters (CP(CFG_REQUEST): Windows asks for INTERNAL_IP4_ADDRESS, INTERNAL_IP4_DNS, INTERNAL_IP4_NBNS — the NetBIOS name server — the same for IPv6 and a couple of service attributes), and in the last response the gateway returns CP(CFG_REPLY) with an address from the pool and the DNS servers — whatever is configured for them. Traffic selectors define what goes into the tunnel: 0.0.0.0/0 — everything (full tunnel), specific networks — split.
Besides passwords, EAP can do certificates: the EAP-TLS method (RFC 5216, mutual TLS authentication) with a user certificate from part 25 goes through the same RADIUS chain, in NPS it is "Smart Card or other certificate", and for strongSwan nothing changes. Below is the password variant as the most common one; what to change for EAP-TLS is noted along the way.
NPS
The gateway is registered as a RADIUS client, as in part 26 (vpn-gw, 192.168.1.1, New-NpsRadiusClient). The client address is the one the requests come from, that is, the internal address of the gateway: inside the requests strongSwan writes NAS-IP-Address — the external address it accepts IKE on — and NPS identifies the client not by that but by the UDP source address.
Next come two policies. There are no PowerShell cmdlets for NPS policies; they are created in the console or with netsh nps. The console offers the "Configure VPN or Dial-Up" wizard, which creates both, and this is how they look on a server where the wizard has already been run (an excerpt of the output):
PS C:\> netsh nps show crp
Connection request policy configuration:
---------------------------------------------------------
Name = Virtual Private Network (VPN) Connections
State = Enabled
Processing order = 2
Policy source = 0
Condition attributes:
Name Id Value
---------------------------------------------------------
Condition0 0x3d "^5$"
Profile attributes:
Name Id Value
---------------------------------------------------------
Auth-Provider-Type 0x1025 "0x1"
Override-RAP-Auth 0x1fb0 "FALSE"
The connection request policy decides where requests with NAS-Port-Type = Virtual are authenticated. 0x3d is the NAS-Port-Type attribute, ^5$ is its value 5, Virtual (strongSwan puts it in every request; access points put 19, Wireless-802.11). 0x1025 is Auth-Provider-Type, 1 means authenticate here rather than forward to another RADIUS server. The default policy "Use Windows authentication for all users" does the same for all requests, so a separate one is needed only if other requests are forwarded to a proxy. As a command it is a single add crp, and a condition in it is mandatory:
PS C:\> netsh nps add crp name="VPN" state="enable" processingorder="2" policysource="0" conditionid="0x3d" conditiondata="^5$" profileid="0x1025" profiledata="0x1"
The network policy — whom to let in and with which method. The wizard makes it for the chosen group (on the server above, Domain Users), but it is better to create a separate VPN Users group and create the policy with one netsh nps add np command carrying all conditions and settings — as in part 26:
PS C:\> $sid = (Get-ADGroup 'VPN Users').SID.Value
PS C:\> netsh nps add np name="VPN Users" state="enable" processingorder="2" policysource="0" conditionid="0x3d" conditiondata="^5$" conditionid="0x1023" conditiondata="$sid" profileid="0x100f" profiledata="TRUE" profileid="0x1005" profiledata="TRUE" profileid="0x1009" profiledata="0x5" profileid="0x100a" profiledata="1A000000000000000000000000000000" profileid="0x7" profiledata="0x1" profileid="0x6" profiledata="0x2"
What's in it:
| Identifier | What | Value |
|---|---|---|
0x3d condition |
NAS-Port-Type | ^5$ — Virtual |
0x1023 condition |
Windows-Groups | the SID of CORP\VPN Users — the SID, not the name |
0x100f |
NP-Allow-Dial-in | allow (Grant) |
0x1005 |
Ignore-User-Dialin-Properties | ignore the "Network Access Permission" flag in the account properties: the policy decides |
0x1009 |
NP-Authentication-Type | 0x5 — EAP and only EAP: neither MS-CHAPv2 without EAP nor PAP |
0x100a |
NP-Allowed-EAP-Type | the first byte is the EAP type: 1A = 26 — EAP-MSCHAPv2; for EAP-TLS add a second value 0D000000000000000000000000000000 (13) |
0x7, 0x6 |
Framed-Protocol = PPP, Service-Type = Framed | reply attributes the wizard sets too; strongSwan doesn't read them |
The wizard also adds Microsoft attributes for RRAS — MS-Link-Utilization-Threshold, MS-Link-Drop-Time-Limit, MS-MPPE-Encryption-Policy, MS-MPPE-Encryption-Types; strongSwan ignores those as well. Check the result with netsh nps show np (it has no name parameter and prints all policies).
Processing order matters: the VPN policy must come before policies without a port type condition, otherwise the first matching one picks the request up. The credentials are the same as everywhere: the domain password; accounts in Protected Users won't get through MS-CHAPv2, because NTLM, on which MS-CHAPv2 is built, is blocked for them — as in part 26. What else NPS can put in the reply: Framed-IP-Address gives the user a fixed address (strongSwan applies it if the connection's pools include the radius pool), Session-Timeout limits the session, and Class or Filter-Id with a group name strongSwan can treat as group membership (the plugin's class_group and filter_id options) and use to pick a configuration with groups.
The gateway: strongSwan
vpn-gw is Ubuntu with two interfaces: eth0 facing out with the address 192.0.2.10 and eth1 facing in with 192.168.1.1. The packages:
apt install charon-systemd strongswan-swanctl libcharon-extra-plugins
charon-systemd is the IKE daemon itself and the strongswan.service unit, which runs swanctl --load-all after start; libcharon-extra-plugins contains the eap-radius plugin, without which strongSwan can only do local EAP methods. To check that the plugin is loaded after start: swanctl --stats lists the loaded plugins in its loaded plugins line, and eap-radius must be among them.
The gateway certificate
Clients check the certificate by the same rules as a browser: a chain to a trusted root, the name. Two ways:
- Your own certification authority, for example from part 25. The template is a duplicate of "Web Server" or "RAS and IAS Server" with the name supplied in the request and the Server Authentication EKU; the root certificate is on every domain machine, while personal devices need it installed by hand. The IP security IKE intermediate EKU (
1.3.6.1.5.5.8.2.2) isn't needed by Windows but doesn't hurt, and older macOS versions require it. - A public CA (Let's Encrypt via ACME — the protocol by which the gateway itself proves to the CA that it controls the name and gets the certificate, RFC 8555): the root is everywhere already, nothing has to be installed on personal devices, but the certificate lives 90 days and after renewal strongSwan has to reload it (
swanctl --load-creds).
Either way the name vpn.example.com must be in the certificate — in the Subject or the Subject Alternative Name, more reliably in the SAN as a dNSName; otherwise Windows answers with error 13801. The files go into the swanctl directories:
/etc/swanctl/x509/vpn.example.com.pem gateway certificate
/etc/swanctl/private/vpn.example.com.pem its private key, mode 600
/etc/swanctl/x509ca/issuing-ca.pem intermediate CA, to send the chain to clients
Configuration
/etc/swanctl/conf.d/vpn.conf:
connections {
ikev2-eap-radius {
version = 2
proposals = aes256-sha256-modp2048,aes256-sha256-modp1024
mobike = yes
unique = never
rekey_time = 0
dpd_delay = 30
pools = pool-v4, pool-v6
local {
auth = pubkey
certs = vpn.example.com.pem
id = vpn.example.com
}
remote {
auth = eap-radius
eap_id = %any
}
children {
ikev2-eap-radius-child {
local_ts = 0.0.0.0/0, ::/0
esp_proposals = aes256-sha256-modp2048,aes256-sha256,aes256-sha1
rekey_time = 0
dpd_action = clear
}
}
}
}
pools {
pool-v4 {
addrs = 192.168.50.0/24
dns = 192.168.1.10, 192.168.1.11
}
pool-v6 {
addrs = 2001:db8:50::/64
dns = 2001:db8:1::10, 2001:db8:1::11
}
}
What matters here:
local { auth = pubkey }— the gateway authenticates with a certificate;idmust match the name in the certificate and what the client enters as the server address.remote { auth = eap-radius; eap_id = %any }— check clients through RADIUS and accept any identity (NPS decides). The IKE identity of a Windows client is its own IP address (behind a home router, an internal one like10.0.0.15), and the user name only arrives in EAP;eap_id = %anykeeps these two names apart.proposals— AES-256 and SHA-256 with 2048- and 1024-bit Diffie–Hellman groups. By default Windows offers onlymodp1024for IKEv2, and without the second option in the list the client won't connect untilmodp2048is enabled on it (below, in the client section, and in a separate post). Once all clients are configured,modp1024is removed from the list.esp_proposals— the algorithms of the tunnel itself. Without its own configuration Windows offers only SHA-1 integrity for ESP, so withoutaes256-sha1such a client brings up the IKE SA but not the tunnel.aes256-sha256-modp2048is for clients with PFS enabled (-PfsGroupbelow): when the tunnel keys are renewed they send a Diffie–Hellman exchange, and the gateway has to accept it; for the first tunnel, created in IKE_AUTH, the group isn't used.mobike = yes— moving the client between networks without reconnecting (MOBIKE, RFC 4555): the laptop fell asleep on Wi-Fi and woke up tethered to a phone — the tunnel is alive. This is also the default.unique = never— one user can connect from several devices at once; with the defaultnoa new connection carrying an INITIAL_CONTACT notification would push out the old one.rekey_time = 0at both levels — don't renew keys on a timer from the gateway side: the Windows client does it itself on its own schedule, and Windows handles gateway-initiated renewal poorly: for example, if the gateway proposes a Diffie–Hellman exchange for the tunnel and PFS isn't configured on the client, the client fails with error 13816.dpd_delay = 30— Dead Peer Detection: if no packet has come from the client for 30 seconds, the gateway sends an empty INFORMATIONAL message; no answer after all retransmissions (five by default, about 165 seconds) — the client is considered gone. Thedpd_timeoutoption has no effect on IKEv2.dpd_action = clear— close the tunnel and do nothing more; this is the default, andswanctl --list-connsshows it asdpd action is none.local_ts = 0.0.0.0/0, ::/0— full tunnel: the gateway is ready to accept all of the client's traffic. Which traffic the client actually sends is decided by the client with its routes (below); to have the gateway pass only the office networks, list them here, for example192.168.0.0/16.pools— addresses and attributes for the Configuration Payload;dns— the domain controllers, so that internal names resolve. An address sticks to a user: strongSwan remembers what it gave to whom and hands out the same one on reconnect. An IPv6 pool of size/64strongSwan trims to/97(2³¹ addresses) and says so when loading:virtual IP pool too large, limiting to 2001:db8:50::/97. Optionally,split_include = 192.168.0.0/16goes here too — a hint for clients that understand the split tunnel attribute (Apple, strongSwan); Windows ignores it.
The RADIUS plugin is configured separately, /etc/strongswan.d/charon/eap-radius.conf:
eap-radius {
load = yes
accounting = yes
servers {
nps1 {
address = 192.168.1.20
secret = <shared secret, the same as for the vpn-gw RADIUS client in NPS>
auth_port = 1812
acct_port = 1813
}
}
}
accounting = yes (off by default) — send NPS an Accounting-Request at session start and end: then the NPS log shows not only logons but also duration and volumes. Several servers can be listed, and strongSwan switches to the next one if the first doesn't answer; a second NPS is worth setting up right after the first, because without RADIUS the VPN doesn't work at all. The file with the secret — mode 600. The rest of what strongSwan puts in every request: NAS-Port-Type = 5 (Virtual), Service-Type = 2 (Framed), NAS-Port — the IKE SA number, NAS-Port-Id — the connection name, Called-Station-Id and Calling-Station-Id — the gateway and client addresses with ports, NAS-Identifier = strongSwan.
Apply:
systemctl restart strongswan # eap-radius.conf is read at start
swanctl --list-conns
Firewall and routing
The gateway has to accept IKE and ESP, forward the clients' decrypted traffic and, for a full tunnel, let them out to the internet under its own address:
sysctl -w net.ipv4.ip_forward=1
iptables -A INPUT -i eth0 -p udp -m multiport --dports 500,4500 -j ACCEPT
iptables -A INPUT -i eth0 -p esp -j ACCEPT
iptables -A FORWARD -s 192.168.50.0/24 -m policy --dir in --pol ipsec --proto esp -j ACCEPT
iptables -A FORWARD -d 192.168.50.0/24 -m policy --dir out --pol ipsec --proto esp -j ACCEPT
iptables -t nat -A POSTROUTING -o eth0 -m policy --dir out --pol ipsec -j ACCEPT
iptables -t nat -A POSTROUTING -s 192.168.50.0/24 -o eth0 -j MASQUERADE
iptables -t mangle -A FORWARD -p tcp -m policy --dir in --pol ipsec -m tcp --tcp-flags SYN,RST SYN -m tcpmss --mss 1361:1536 -j TCPMSS --set-mss 1360
-p esp is ESP without UDP, for clients that aren't behind NAT (with NAT-T everything goes to 4500). -m policy --pol ipsec makes the rule match only packets that came out of a tunnel or will go into one; that is how VPN client traffic is told apart from internet traffic with the same addresses. The ACCEPT rule in nat before MASQUERADE keeps the address of packets going into the gateway's other tunnels (if any) from being rewritten. The last line is MSS clamping: ESP and UDP eat part of the MTU, and without lowering the MSS large TCP segments don't make it through the tunnel (part 4 of the networking series — the technique itself, part 19 — what the tunnel costs); --mss 1361:1536 rewrites only values above 1360. If the internal router isn't vpn-gw, it needs a route 192.168.50.0/24 → 192.168.1.1.
For EAP-TLS instead of passwords nothing changes on the gateway: EAP of any type passes through eap-radius transparently; what changes is the NPS policy (type 13 allowed) and the client (a user certificate instead of a password).
The client
The connection on Windows is created by a command, and everything configured on the gateway is in it:
PS C:\> Add-VpnConnection -Name 'Corp VPN' -ServerAddress vpn.example.com -TunnelType Ikev2 -AuthenticationMethod Eap -EncryptionLevel Required -RememberCredential -DnsSuffix corp.example.com
PS C:\> Set-VpnConnectionIPsecConfiguration -ConnectionName 'Corp VPN' -EncryptionMethod AES256 -IntegrityCheckMethod SHA256 -DHGroup Group14 -PfsGroup PFS2048 -CipherTransformConstants AES256 -AuthenticationTransformConstants SHA256128 -Force
Without -AllUserConnection the connection is created for the current user, with it — for all users of the machine (administrator rights required, and the same switch then goes into the other commands below). -AuthenticationMethod Eap without an XML profile means EAP-MSCHAPv2, "Secured password": the client asks for the domain user name and password and offers NPS exactly the method the policy allows. This is what the profile created by this command looks like:
PS C:\> Get-VpnConnection -Name 'Corp VPN' | Select-Object Name, TunnelType, AuthenticationMethod, EncryptionLevel, SplitTunneling, ConnectionStatus
Name TunnelType AuthenticationMethod EncryptionLevel SplitTunneling ConnectionStatus
---- ---------- -------------------- --------------- -------------- ----------------
Corp VPN Ikev2 {Eap} Required False Disconnected
PS C:\> $x = (Get-VpnConnection -Name 'Corp VPN').EapConfigXmlStream
PS C:\> $x.EapHostConfig.EapMethod.Type.'#text'
26
PS C:\> $x.EapHostConfig.Config.Eap.EapType.UseWinLogonCredentials
false
EAP type 26 and UseWinLogonCredentials = false: the password is asked for at connect time rather than taken from the Windows sign-in. For EAP-TLS a different EAP XML profile is passed via -EapConfigXmlStream, taken from a machine configured by hand through the same EapConfigXmlStream property.
The second command moves the client to modp2048 (Group14), AES-256 and SHA-256 for both IKE and ESP and turns on PFS — the same policy as in the gateway's proposals and esp_proposals. Without it Windows offers modp1024 for IKE and SHA-1 for ESP. There is also a machine-wide way for all connections at once — the NegotiateDH2048_AES256 registry value in HKLM:\SYSTEM\CurrentControlSet\Services\RasMan\Parameters (1 — offer AES-256 and modp2048 along with the old ones, 2 — only them); it changes IKE only, ESP stays on SHA-1:
PS C:\> Get-ItemPropertyValue HKLM:\SYSTEM\CurrentControlSet\Services\RasMan\Parameters -Name NegotiateDH2048_AES256
1
Trusting the gateway certificate. If the certificate is from your own CA, the root must be in Cert:\LocalMachine\Root. Domain machines with an enterprise CA have it there since part 25; on a personal laptop — Import-Certificate -FilePath corp-root-ca.cer -CertStoreLocation Cert:\LocalMachine\Root from an administrator console. Without it — error 13801 below.
Full or split tunnel. By default Windows sets the default route through the VPN: all of the client's internet traffic goes through the office (that's why vpn-gw had NAT). A split tunnel is the -SplitTunneling flag plus routes by hand:
PS C:\> Set-VpnConnection -Name 'Corp VPN' -SplitTunneling $true
PS C:\> Add-VpnConnectionRoute -ConnectionName 'Corp VPN' -DestinationPrefix 192.168.0.0/16
Then only the office networks go through the tunnel and the rest goes directly, with all the caveats about leaks from part 20. IPv6 has its own detail: Windows does get an address from the pool-v6 pool, but it doesn't set an IPv6 route through the VPN even with a full tunnel. The cmdlet won't add the ::/0 default route, but 2000::/3 — all global IPv6 addresses — is enough: Add-VpnConnectionRoute -DestinationPrefix 2000::/3.
To connect from the command line — rasdial 'Corp VPN' alice * (the asterisk means prompt for the password), to disconnect — rasdial 'Corp VPN' /DISCONNECT; without arguments rasdial lists active connections:
PS C:\> rasdial
No connections
Command completed successfully.
The same profile can be pushed to domain machines by Group Policy or through Intune (Microsoft's cloud device management service) as XML for the VPNv2 CSP — the configuration node through which MDM configures VPN; that is also where automatic connection is turned on (Always On VPN is not a separate protocol but a way to deliver and auto-start the same IKEv2 profile). macOS and iOS connect with the built-in IKEv2 client with "Username" authentication — that is the same EAP-MSCHAPv2; Android — with the strongSwan app and the "IKEv2 EAP (Username/Password)" profile. Linux — with strongSwan as a client or NetworkManager with a plugin.
What breaks
The Windows client reports errors by number; the same numbers appear in RasClient events in the Application log:
| Code | Windows text | What it usually means |
|---|---|---|
| 809 | the remote server is not responding | the gateway didn't answer on UDP 500/4500: a firewall, no port forwarding |
| 13801 | IKE authentication credentials are unacceptable | the gateway certificate failed the check: no Server Authentication EKU, expired, the client doesn't trust the root, the name doesn't match ServerAddress |
| 13868 | Policy match error | the algorithms didn't match: the client has modp1024, the gateway only modp2048, or different ciphers |
| 691 | the user name and password combination … is not recognized, or the selected authentication protocol is not permitted | NPS rejected: password, group, policy — look at 6273 on NPS |
| 812 | the authentication method used by the server … may not match the authentication method configured in your connection profile | the NPS policy requires a method the client profile doesn't have |
| 13806 | IKE failed to find valid machine certificate | the client lacks the required certificate (EAP-TLS, computer authentication) |
The codes were designed for an RRAS gateway, and behind a third-party gateway the client doesn't always see the cause this precisely; the exact reason for a rejection is in the NPS log.
On the gateway side journalctl -u strongswan tells the rest. NPS doesn't answer — retransmit 1 of RADIUS Access-Request and finally RADIUS Access-Request timed out after … attempts: this is how a stopped NPS, a wrong secret and a gateway registered in NPS with the wrong address all look — NPS silently drops such requests and writes event 13 (unknown client), 14 or 18 (the Authenticator or Message-Authenticator doesn't match, i.e. the secret) to the System log. An NPS rejection — RADIUS authentication of 'CORP\alice' failed and EAP method … failed for peer …. The client didn't accept the gateway's authentication — instead of the next EAP message comes parsed INFORMATIONAL request 2 [ N(AUTH_FAILED) ], and the half-open IKE SA is deleted on timeout. On NPS — 6273 events with a reason code: 16 — wrong credentials, 48 — no network policy matched (most often the port type or policy order), 66 — the method isn't allowed, 65 — access denied by the permission in the account or in the matching policy.
Reachability from outside is checked before anything else: nmap -sU -p 500,4500 vpn.example.com shows open or open|filtered, and a client that got 13801 or 13868 has already proved that IKE answers.
Looking at it by hand
What strongSwan has loaded — the connection, the pools and the certificate:
# swanctl --list-conns
ikev2-eap-radius: IKEv2, no reauthentication, no rekeying, dpd delay 30s
local: %any
remote: %any
local public key authentication:
id: vpn.example.com
certs: CN=vpn.example.com
remote EAP_RADIUS authentication:
eap_id: %any
ikev2-eap-radius-child: TUNNEL, no rekeying, dpd action is none
local: 0.0.0.0/0 ::/0
remote: dynamic
# swanctl --list-pools
pool-v4 192.168.50.0 0 / 0 / 254
pool-v6 2001:db8:50:: 0 / 0 / 2147483646
# swanctl --list-certs --subject "CN=vpn.example.com" | grep -vE 'serial|keyid|subjkey|authkey'
List of X.509 End Entity Certificates
subject: "CN=vpn.example.com"
issuer: "CN=Example Corp Issuing CA"
validity: not before Feb 02 18:39:58 2026, ok
not after Feb 02 18:39:58 2031, ok (expires in 1556 days)
altNames: vpn.example.com
flags: serverAuth ikeIntermediate
CRL URIs: http://pki.example.com/issuing-ca.crl
pubkey: RSA 4096 bits, has private key
no rekeying is rekey_time = 0, remote: dynamic means the client's selector becomes its virtual address. The three numbers of a pool are the leases connected now (online), those of clients that disconnected but keep their address (offline), and the size; the size of the IPv6 pool is that 2³¹ − 2. The certificate has both EKUs and has private key — the key was found in /etc/swanctl/private.
Active connections are shown by swanctl --list-sas: for a remote access connection the remote line with the client's identity and address gets EAP: '<user name>' and the virtual address in square brackets appended, and the tunnel mode behind NAT is TUNNEL-in-UDP.
The log of one Windows client connection, without the vendor ID lines, certificate requests for unknown CAs, fragments and packet sending:
# journalctl -u strongswan -o cat --since '2026-10-26 20:22:40' --until '2026-10-26 20:22:55' | grep -vE 'vendor ID|unknown ca with|endpoint changed|received fragment|EF\(|\[NET\]'
11[ENC] parsed IKE_SA_INIT request 0 [ SA KE No N(FRAG_SUP) N(NATD_S_IP) N(NATD_D_IP) V V V V ]
11[IKE] 203.0.113.77 is initiating an IKE_SA
11[IKE] IKE_SA (unnamed)[201] state change: CREATED => CONNECTING
11[CFG] selected proposal: IKE:AES_CBC_256/HMAC_SHA2_256_128/PRF_HMAC_SHA2_256/MODP_2048
11[IKE] remote host is behind NAT
11[IKE] sending cert request for "CN=Example Corp Root CA"
11[IKE] sending cert request for "CN=Example Corp Issuing CA"
11[ENC] generating IKE_SA_INIT response 0 [ SA KE No N(NATD_S_IP) N(NATD_D_IP) CERTREQ N(FRAG_SUP) N(CHDLESS_SUP) N(MULT_AUTH) ]
05[ENC] parsed IKE_AUTH request 1 [ IDi CERTREQ N(MOBIKE_SUP) CPRQ(ADDR DNS NBNS SRV ADDR6 DNS6 SRV6) SA TSi TSr ]
05[IKE] received cert request for "CN=Example Corp Root CA"
05[IKE] received 62 cert requests for an unknown ca
05[CFG] looking for peer configs matching 192.0.2.10[%any]...203.0.113.77[10.0.0.15]
05[CFG] selected peer config 'ikev2-eap-radius'
05[IKE] initiating EAP_IDENTITY method (id 0x00)
05[IKE] processing INTERNAL_IP4_ADDRESS attribute
05[IKE] processing INTERNAL_IP4_DNS attribute
05[IKE] processing INTERNAL_IP4_NBNS attribute
05[IKE] processing INTERNAL_IP4_SERVER attribute
05[IKE] processing INTERNAL_IP6_ADDRESS attribute
05[IKE] processing INTERNAL_IP6_DNS attribute
05[IKE] processing INTERNAL_IP6_SERVER attribute
05[IKE] peer supports MOBIKE
05[IKE] authentication of 'vpn.example.com' (myself) with RSA signature successful
05[IKE] sending end entity cert "CN=vpn.example.com"
05[IKE] sending issuer cert "CN=Example Corp Issuing CA"
05[ENC] generating IKE_AUTH response 1 [ IDr CERT CERT AUTH EAP/REQ/ID ]
05[ENC] splitting IKE message (3488 bytes) into 3 fragments
06[ENC] parsed IKE_AUTH request 2 [ EAP/RES/ID ]
06[IKE] received EAP identity 'CORP\alice'
06[CFG] sending RADIUS Access-Request to server 'nps1'
06[CFG] received RADIUS Access-Challenge from server 'nps1'
06[IKE] initiating EAP_MSCHAPV2 method (id 0x01)
06[ENC] generating IKE_AUTH response 2 [ EAP/REQ/MSCHAPV2 ]
16[ENC] parsed IKE_AUTH request 3 [ EAP/RES/MSCHAPV2 ]
16[CFG] sending RADIUS Access-Request to server 'nps1'
16[CFG] received RADIUS Access-Challenge from server 'nps1'
16[ENC] generating IKE_AUTH response 3 [ EAP/REQ/MSCHAPV2 ]
09[ENC] parsed IKE_AUTH request 4 [ EAP/RES/MSCHAPV2 ]
09[CFG] sending RADIUS Access-Request to server 'nps1'
09[CFG] received RADIUS Access-Accept from server 'nps1'
09[IKE] RADIUS authentication of 'CORP\alice' successful
09[IKE] EAP method EAP_MSCHAPV2 succeeded, MSK established
09[ENC] generating IKE_AUTH response 4 [ EAP/SUCC ]
15[ENC] parsed IKE_AUTH request 5 [ AUTH ]
15[IKE] authentication of '10.0.0.15' with EAP successful
15[IKE] authentication of 'vpn.example.com' (myself) with EAP
15[IKE] peer requested virtual IP %any
15[CFG] reassigning offline lease to 'CORP\alice'
15[IKE] assigning virtual IP 192.168.50.1 to peer 'CORP\alice'
15[IKE] peer requested virtual IP 2001:db8:50::2
15[CFG] reassigning offline lease to 'CORP\alice'
15[IKE] assigning virtual IP 2001:db8:50::1 to peer 'CORP\alice'
15[IKE] building INTERNAL_IP4_DNS attribute
15[IKE] building INTERNAL_IP4_DNS attribute
15[IKE] building INTERNAL_IP6_DNS attribute
15[IKE] building INTERNAL_IP6_DNS attribute
15[IKE] IKE_SA ikev2-eap-radius[201] established between 192.0.2.10[vpn.example.com]...203.0.113.77[10.0.0.15]
15[IKE] IKE_SA ikev2-eap-radius[201] state change: CONNECTING => ESTABLISHED
15[CFG] selected proposal: ESP:AES_CBC_256/HMAC_SHA1_96/NO_EXT_SEQ
15[IKE] CHILD_SA ikev2-eap-radius-child{219} established with SPIs c5f5e571_i 8151acd4_o and TS 0.0.0.0/0 ::/0 === 192.168.50.1/32 2001:db8:50::1/128
15[ENC] generating IKE_AUTH response 5 [ AUTH CPRP(ADDR ADDR6 DNS DNS DNS6 DNS6) SA TSi TSr N(MOBIKE_SUP) N(ADD_4_ADDR) N(ADD_4_ADDR) N(ADD_4_ADDR) N(ADD_6_ADDR) N(ADD_6_ADDR) N(ADD_6_ADDR) ]
The whole diagram from the start of the article is visible here. The client's first IKE_AUTH is IDi CERTREQ … CPRQ(…) SA TSi TSr without AUTH: a request for EAP, a request for an address and 63 certificate requests — one per trusted root of Windows, of which the gateway recognised one. The client's identity is 10.0.0.15, its address behind the home router. The gateway answers IDr CERT CERT AUTH EAP/REQ/ID — its certificate, the intermediate CA, the signature and an identity request — and cuts the response into three fragments. Then three RADIUS pairs: identity → MS-CHAPv2 challenge, response → the server's success message, acknowledgement → Access-Accept. MSK established is those keys from the Access-Accept, from which the last AUTH is then computed. The pool gives the user the same address as last time (reassigning offline lease), and the response goes out with CPRP(ADDR ADDR6 DNS DNS DNS6 DNS6): only addresses and DNS; strongSwan doesn't fill in NBNS and the rest. IKE agreed on MODP_2048 — this client has NegotiateDH2048_AES256 enabled — and ESP on HMAC_SHA1_96: the connection has no IPsec policy of its own, and SHA-1 is the only integrity Windows offers for ESP. Without aes256-sha1 in esp_proposals this connection wouldn't have come up.
The same conversation through the eyes of NPS — the accounting file (its format is covered in part 26), VPN policy records only:
PS C:\> Invoke-Command -ComputerName nps1 -ScriptBlock {
$type = @{ '1'='Access-Request'; '2'='Access-Accept'; '3'='Access-Reject'; '11'='Access-Challenge' }
Get-Content (Get-ChildItem C:\Windows\System32\LogFiles\IN*.log | Sort-Object Name | Select-Object -Last 1).FullName |
Where-Object { $_ -like '*Virtual Private Network*' } | Select-Object -Last 6 | ForEach-Object {
$e = ([xml]$_).Event
[PSCustomObject]@{ Time = $e.Timestamp.'#text'.Split(' ')[1]; Packet = $type[$e.'Packet-Type'.'#text']; Port = $e.'NAS-Port'.'#text'; Calling = $e.'Calling-Station-Id'.'#text'; Eap = $e.'EAP-Friendly-Name'.'#text' } }
} | Format-Table Time, Packet, Port, Calling, Eap -AutoSize
Time Packet Port Calling Eap
---- ------ ---- ------- ---
20:22:48.502 Access-Request 201 203.0.113.77[43113]
20:22:48.502 Access-Challenge
20:22:48.546 Access-Request 201 203.0.113.77[43113]
20:22:48.546 Access-Challenge
20:22:48.576 Access-Request 201 203.0.113.77[43113] Microsoft: Secured password (EAP-MSCHAP v2)
20:22:48.576 Access-Accept Microsoft: Secured password (EAP-MSCHAP v2)
Three pairs in 74 milliseconds. NAS-Port 201 is the same number as in ikev2-eap-radius[201] in the gateway log: it links the NPS record to the connection on the gateway. The first request in full — all elements of the line:
Timestamp=10/26/2026 20:22:48.502 | Computer-Name=NPS1 | Event-Source=IAS | User-Name=CORP\alice | NAS-Port-Type=5 | Service-Type=2 | NAS-Port=201 | NAS-Port-Id=ikev2-eap-radius | NAS-IP-Address=192.0.2.10 | Called-Station-Id=192.0.2.10[4500] | Calling-Station-Id=203.0.113.77[43113] | NAS-Identifier=strongSwan | Client-IP-Address=192.168.1.1 | Client-Vendor=0 | Client-Friendly-Name=vpn-gw | Proxy-Policy-Name=Virtual Private Network (VPN) Connections | Provider-Type=1 | SAM-Account-Name=CORP\alice | Fully-Qualifed-User-Name=CORP\alice | Class=311 1 192.168.1.20 10/26/2026 10:43:14 104 | Authentication-Type=5 | NP-Policy-Name=Virtual Private Network (VPN) Connections | Packet-Type=1 | Reason-Code=0
NAS-IP-Address is the external address of the gateway, Client-IP-Address the internal one the packet came from and by which NPS found the vpn-gw client. Calling-Station-Id is the user's external address with a port, instead of the MAC in Wi-Fi. In the Access-Challenge NPS puts Session-Timeout = 60 — how many seconds to wait for the next EAP step — and in the Access-Accept Framed-Protocol, Service-Type, the Microsoft attributes from the policy and MS-CHAP-Domain = 01434F5250 (an identifier byte and CORP); the MS-MPPE keys are not written to the accounting file. There are no Accounting-Request records: accounting is not enabled on this gateway. Event 6272 with the same fields goes to the Security log — with Client Friendly Name = vpn-gw and the user's external address in Calling Station Identifier; the Security log is overwritten quickly, so for a history of VPN connections the accounting file is more reliable.
Sources
IKEv2 and IPsec:
- RFC 7296 — IKEv2: IKE_SA_INIT and IKE_AUTH, EAP in IKE_AUTH and the MSK in the final AUTH (section 2.16), CERTREQ, Configuration Payload, liveness checks;
- RFC 7383 — IKEv2 message fragmentation;
- RFC 4555 — MOBIKE;
- RFC 3948 — ESP in UDP to get through NAT;
- RFC 4303 — ESP.
EAP and RADIUS:
- RFC 3748 and RFC 3579 — EAP and carrying it in RADIUS;
- RFC 2759 — MS-CHAPv2;
- RFC 5216 — EAP-TLS;
- RFC 2548 — Microsoft attributes:
MS-MPPE-Send-Key,MS-MPPE-Recv-Key,MS-CHAP-Domain; - RFC 2865 and RFC 2866 — RADIUS (
NAS-Port-TypeVirtual = 5,Calling-Station-Id,Class) and accounting; - RFC 8555 — ACME.
strongSwan:
- swanctl.conf — connection options:
unique,mobike,rekey_time,dpd_delay(anddpd_timeout, which has no effect in IKEv2),send_cert,eap_id,dpd_action, pool attributes; - swanctl —
--list-conns,--list-pools,--list-certs,--list-sas,--stats,--load-creds; - eap-radius Plugin — RADIUS servers,
accounting,class_group,filter_id,nas_identifier, the attributes the plugin passes to the client; - Virtual IP Addresses — pools, address affinity to an identity, the
radiuspool forFramed-IP-Address; - Retransmission — retransmissions and the time until a peer is considered dead;
- Windows Clients — default Windows proposals (
modp1024, SHA-1 in ESP),NegotiateDH2048_AES256, PFS and error 13816,rekey_time = 0, fragmentation since 1803, the IPv6 route2000::/3; - Windows Certificate Requirements — EKU, the name in the Subject or SAN, ikeIntermediate for older macOS, error 13801;
- Forwarding and Split-Tunneling — forwarding, NAT and split tunneling on the gateway;
- iptables-extensions — the
policyandtcpmssmodules, theTCPMSStarget.
Network Policy Server:
- Connection Request Policies and Network Policies — the two policy queues, conditions, constraints, ignoring Dial-in;
- RADIUS Clients — registering the gateway as a RADIUS client;
- Netsh Commands for Network Policy Server —
netsh nps add crp,add np,show np; - Interpret NPS Database Format Log Files — accounting file fields;
- NPS Reason Codes — rejection reason codes;
- Audit Network Policy Server — events 6272 and 6273;
- Protected Users Security Group — NTLM is blocked for group members.
The Windows client:
- Add-VpnConnection, Set-VpnConnection, Get-VpnConnection — the connection profile, authentication method, split tunneling;
- Set-VpnConnectionIPsecConfiguration — IKE and ESP algorithms, Diffie–Hellman groups and PFS;
- Add-VpnConnectionRoute — routes through the VPN;
- VPNv2 CSP and About Always On VPN — a profile through MDM and automatic connection;
- Troubleshoot Always On VPN — errors 809, 812, 13801, 13806 and their causes, RasClient events;
- Routing and Remote Access Error Codes and System Error Codes (12000-15999) — the texts of codes 691, 809, 812 and 13801, 13806, 13868.
The block on certificates and network access is done: one directory, one NPS, the same accounts for Wi-Fi, wired ports and VPN, and the gateway can be anything — as long as it speaks RADIUS. The next two parts are about machines that in all previous parts were only DNS clients and ssh targets: Linux servers that become domain members on a par with Windows.
Next — part 29: joining Linux to the domain. What "domain member" means for a machine without Netlogon; realmd and sssd, krb5.conf, PAM and NSS; mapping SIDs to POSIX IDs; short names and the default domain; restricting logon to groups and sudo for domain groups; registration in DNS; and the canonical name in /etc/hosts, which often makes Kerberos fail on a freshly joined machine.
Only registered users can leave a comment.
Sign in or register to leave a comment.