VyOS version: 1.5 (circinus) Symptom: Windows native IKEv2 clients suddenly fail to connect with error 13801 (“IKE authentication credentials are unacceptable”), while the VyOS side looks completely healthy — valid cert, valid time sync, correct config, swanctl --list-certs shows no errors.
Posting this because it cost me several days to fully diagnose, and I couldn’t find existing threads connecting all the pieces. Sharing the full path in case it saves someone else the time.
Setup
-
Two independent VyOS 1.5 firewalls acting as IKEv2 remote-access VPN servers (
ra-rwconnection type), each on a different ISP -
Server authentication: x509 certificate issued via VyOS’s built-in ACME/Let’s Encrypt integration (
set pki certificate <name> acme ...) -
Client authentication: EAP-MSCHAPv2 (username/password), standard Windows 10/11 native IKEv2 client
-
Both firewalls had been running stable for months on Let’s Encrypt certs
First red herring: power outage / NTP
The initial trigger for investigating was a power outage that knocked out both firewalls overnight. On restart, VPN connections failed intermittently with the same 13801 error. In that specific case, the actual cause was NTP — the servers came up with drifted clocks before NTP had synced, and IKEv2 certificate validation is extremely time-sensitive. Once NTP settled, that particular episode resolved itself. Worth checking first (show ntp, show date), but in our case it masked a second, unrelated, and more persistent problem — see below.
The real problem: Let’s Encrypt’s Gen-Y intermediate rotation
A few days later, VPN connections started failing again with the identical 13801 error, despite NTP being clean (sub-millisecond offsets). This time the certificate itself was the issue.
Let’s Encrypt has introduced a new certificate hierarchy — “Generation Y” — with new roots ISRG Root YR / ISRG Root YE, alongside the long-established Generation X (ISRG Root X1). New leaf certs are now being issued under intermediates YR1/YR2, chaining to Root YR, which is not yet in most operating systems’ trust stores, including Windows.
Let’s Encrypt does cross-sign back to the trusted ISRG Root X1, but that requires serving the cross-signed intermediate, not just the direct Root YR-signed one. Our VyOS-managed ACME certs were only presenting the direct (untrusted-root) chain.
Confirming it
sudo swanctl --list-certs
Compare the leaf’s authkeyId against the loaded CA’s subjkeyId — if you have a valid chain, they must match exactly.
In our case, after a renew certbot force, the leaf correctly renewed to YR2/YR1, but the CA object VyOS had stored (AUTOCHAIN_<name>) sometimes still contained the previous intermediate. show pki certificate <name> reported CA Present: Yes, which is misleading — it only checks that a CA object reference exists, not that it’s the cryptographically correct one for the current leaf.
Two separate bugs stacked on top of this
-
VyOS config/disk sync bug: after
renew certbot force, the CA object in the config tree could be correct while the on-disk file strongSwan actually reads (/etc/swanctl/x509ca/AUTOCHAIN_<name>.pem) remained stale, surviving even a reboot. Compare:show configuration commands | grep "AUTOCHAIN_<name> certificate" | awk -F"'" '{print $2}' | base64 -d | openssl x509 -noout -subject sudo cat /etc/swanctl/x509ca/AUTOCHAIN_<name>.pem | openssl x509 -noout -subjectIf these disagree, forcing a trivial real config change (e.g. add then remove a
descriptionfield on the CA object) and committing triggered VyOS to regenerate the file correctly. -
strongSwan only loads the first certificate from a CA file/value — confirmed via strongswan/strongswan#3072. This means even once you have the correct chain, you cannot cram intermediate + cross-signed root into a single
pki cavalue — they need to be loaded as separate CA objects. Several people on the Let’s Encrypt community forum hit exactly this (thread 1, thread 2) and resolved it by splitting the cross-signed root into its own file/CA slot.
What actually fixed it for us
After fighting the config-sync bug and the multi-cert-per-CA-file limitation for a while, we decided the ACME/ISRG-YR transition churn wasn’t worth continuing to chase, and switched both firewalls to a commercial DV certificate from a standard reseller (SSL.com), replacing the Let’s Encrypt/ACME setup entirely.
This sidesteps the problem completely: an established commercial root has been in every OS trust store for years, there’s no Gen-Y transition to worry about, and — since the reseller only supplied a two-certificate chain (single intermediate + self-signed root, no root needed to be served) — it also avoids the strongSwan multi-cert-per-file bug entirely.
Note: as of March 2026, CA/Browser Forum rules cap public cert validity at ~200 days regardless of CA, so even a “2-year” commercial purchase is delivered as a subscription that reissues every ~197 days rather than one long-lived cert. Still far more predictable than chasing ACME automation gaps.
Quick outline of the manual cert deployment (if useful to anyone)
# Generate key + CSR
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr -subj "/C=xx/O=YourOrg/CN=vpn.example.com"
# Submit CSR to your CA/reseller of choice, get back:
# server.crt (your signed leaf cert)
# server.ca-bundle (intermediate [+ root, self-signed])
# Split the bundle and confirm which entry is the intermediate (subject != issuer)
csplit -z -f ca_ -b "%02d.pem" server.ca-bundle '/-----BEGIN CERTIFICATE-----/' '{*}'
for f in ca_*; do openssl x509 -in "$f" -noout -subject -issuer; done
# Inside a single `configure` session (VyOS: bash vars don't survive into config mode!)
configure
CERT_B64=$(awk '/BEGIN CERTIFICATE/{flag=1;next}/END CERTIFICATE/{flag=0}flag' server.crt | tr -d '\n')
KEY_B64=$(awk '/BEGIN.*PRIVATE KEY/{flag=1;next}/END.*PRIVATE KEY/{flag=0}flag' server.key | tr -d '\n')
CA_B64=$(awk '/BEGIN CERTIFICATE/{flag=1;next}/END CERTIFICATE/{flag=0}flag' ca_00.pem | tr -d '\n') # the intermediate only
set pki certificate my_commercial certificate "$CERT_B64"
set pki certificate my_commercial private key "$KEY_B64"
set pki ca my_commercial_ca certificate "$CA_B64"
set vpn ipsec remote-access connection rw authentication x509 certificate 'my_commercial'
set vpn ipsec remote-access connection rw authentication x509 ca-certificate 'my_commercial_ca'
commit
save
Then sudo swanctl --load-creds and verify with sudo swanctl --list-certs that the leaf’s authkeyId matches the CA’s subjkeyId.
Summary / TL;DR
-
13801 on Windows IKEv2 clients almost always means a broken certificate chain (or, less commonly, clock skew)
-
If you’re on VyOS ACME/Let’s Encrypt certs and this started recently: suspect the Gen-Y (
YR1/YR2→Root YR) transition -
Verify with
swanctl --list-certs: leafauthkeyIdmust equal loaded CAsubjkeyId -
renew certbot forcemay fix the leaf but not reliably sync the CA chain file — verify config vs./etc/swanctl/x509ca/*.pemdirectly, don’t trustCA Present: Yesalone -
strongSwan only reads the first cert per CA file/value — multi-cert chains need separate CA objects (also see strongswan/strongswan#3072)
-
If you want to stop fighting this entirely: a commercial DV cert on an established root sidesteps all of the above
Happy to answer questions if anyone else is hitting this — took a while to pin down all the moving pieces.
References
-
strongSwan issue confirming the root cause — strongSwan only loads the first certificate from a CA file/config value, breaking multi-cert chains like the YR2 → cross-signed Root YR case: strongswan only loads one certificate per file, which breaks with Let's Encrypt's YR transition · Issue #3072 · strongswan/strongswan · GitHub
-
Let’s Encrypt Community forum — same symptom, same fix (splitting the cross-signed root into its own file resolved it): IKEv2 (strongSwan) fails with Let's Encrypt YR2 chain (works with other servers / chain mismatch suspected) - Help - Let's Encrypt Community Support
-
Let’s Encrypt Community forum — related report, IKEv2 breaking after cert renewal: IKEv2 Vpn connection fails after certificate update - Help - Let's Encrypt Community Support
-
Let’s Encrypt’s own chain-of-trust page — confirms the default YR2 chain terminates at
Root YR, and that Gen Y roots are not yet expected to work with major trust stores without the cross-signed path back toISRG Root X1: Chains of Trust - Let's Encrypt