IKEv2 (strongSwan) fails with Let's Encrypt YR2 chain (works with other servers / chain mismatch suspected)

My domain is:

tyo1.1pb.xyz


I ran this command:

certbot renewcertbot certificatesipsec listcertsipsec listcacertsopenssl crl2pkcs7 -nocrl -certfile /etc/letsencrypt/live/tyo1.1pb.xyz/fullchain.pem | openssl pkcs7 -print_certs -text -noout

It produced this output:

Certificate issued by Let's Encrypt YR2 intermediate:

Subject: CN=tyo1.1pb.xyzIssuer: C=US, O=Let's Encrypt, CN=YR2

CA chain installed on strongSwan:

C=US, O=Let's Encrypt, CN=YR2C=US, O=ISRG, CN=Root YR

IKEv2 authentication via Windows client fails during connection.

However, the same VPN server works correctly when accessed from iOS devices.

Additionally, another server using the older Let's Encrypt R12 / X1 chain works without issues on both Windows and iOS.

Even after forcing removal of "Root YR" from chain.pem, the issue persists.

Certbot issuance succeeds but continues to return the YR2-based chain even when attempting to prefer ISRG Root X1.


My web server is (include version):

strongSwan (IKEv2 VPN server)
Version: (please insert actual version if needed)


The operating system my web server runs on is (include version):

Linux (please specify distro/version if needed)


My hosting provider, if applicable, is:

Self-managed VPS


I can login to a root shell on my machine (yes or no, or I don't know):

Yes


I'm using a control panel to manage my site:

No


The version of my client is:

certbot 1.21.0

Hi, sorry for the confusion in my previous post.

I mistakenly described the operating system as a generic Linux system.
The correct OS is:

Ubuntu 22.04.2 LTS

Everything else in the original report remains unchanged.

That is very outdated.
Please visit https://certbot.org and replace it with the current version.

Strongswan only uses the first certificate in a chain (Frequently Asked Questions (FAQ) :: strongSwan Documentation).

This means that either the issuing certificate must be trusted directly or the missing certificates are downloaded somehow. A potential issue is that the YR cross sign (certificate) had to be replaced recently and Windows might have stored the old revoked cross sign. Apparently you can clear the SSL cache in Windows settings which might fix your issues.

That sounds like a terrible bug that would prevent that system from working at all, isn't it? Having more than one cert in a chain is a pretty normal thing, even if Let's Encrypt hasn't generally been serving such until recently. Surely they can't really mean that?

It appears that this has been an issue since at least 2021 strongSwan and X3 to R3 transition

Edit: I misunderstood the FAQ and though that only the end entity certificate would be served. Since YR* is served, then trusting YR on the clients could provide a fix.

As recent as Dec 2025 a maintainer of StrongSwan said they only used the first cert in the chain. See: PKI tool for certificate splitting · Issue #2953 · strongswan/strongswan · GitHub

Further, a post from yesterday in that thread asked what should be done now that Let's Encrypt is issuing from its Y generation with multiple intermediates.

That github thread is the best place to handle this. It is a definite weakness in StrongSwan's support for public CA cert chains.

As it stands, it seems to me the only solution is what @MaxHearnden suggested by adding the appropriate Y root to each client. However, the main attraction to using a public CA is to avoid customizing clients. Most of the docs and examples for this VPN server show using a private CA.

Update: Another option is to try a different CA like Google Trust Services or ZeroSSL. It is possible the first intermediate in their chains will be more widely trusted than Let's Encrypt's new Y generation. GTS' first intermediate for an RSA cert chains to GTS Root R1

Using a different CA for your certificate will help in this case. But sooner or later this CA will have to move to a newer root certificate as well, their chain will be longer during the rollover as well. It doesn't really fix your problem in the long run.

If I understand correctly, it's a bug in strongswan, they should address this bug.

Yes, I agree. But, they were aware of this in 2021 and as recently as Dec 2025 had no interest in doing that. Of course circumstances change.

The github thread I posted just before your comment had the suggestions below. Any details of those options are best asked of StrongSwan.

From: PKI tool for certificate splitting · Issue #2953 · strongswan/strongswan · GitHub

A: So I currently don't see a reason to invest extra work into this.

Q: I'm not entirely sure whether intermediate issuer certs should go in x509 or x509ca.

A: Both work as any certificate with CA basic constraint will be treated as CA certificate by the vici plugin, but the latter is the intended folder (any certificate loaded from there without basic constraint will be rejected).

hmm... I wonder if the connection can be proxied.
If so, then the connection can be to the proxy [which can be set to provide a proper chain].

Based on your advice, I created a small script to split Let's Encrypt intermediate certificates into separate files for strongSwan.

My environment:

  • Ubuntu 22.04
  • strongSwan IKEv2
  • Let's Encrypt certificates
  • Windows clients failed when using the newer YR chain

Script:

#!/usr/bin/env python3

"""
Split Let's Encrypt chain.pem into separate CA certificate files
for strongSwan.

Behavior:
- When executed from Certbot deploy hooks:
    Uses the RENEWED_LINEAGE environment variable automatically
- When executed manually:
    Falls back to detecting the first Let's Encrypt live directory
- Removes previously generated issuer-*.pem files
- Recreates issuer-1.pem, issuer-2.pem, etc.

Typical location:
    /etc/letsencrypt/renewal-hooks/deploy/update-ipsec-certs.py
"""

from pathlib import Path
import re
import os
import sys

# Directory where strongSwan loads CA certificates
IPSEC_CA_DIR = Path("/etc/ipsec.d/cacerts")

# Prefer Certbot deploy-hook environment variable
renewed_lineage = os.environ.get("RENEWED_LINEAGE")

if renewed_lineage:
    LE_DIR = Path(renewed_lineage)
else:
    # Fallback for manual execution
    LE_BASE = Path("/etc/letsencrypt/live")

    try:
        LE_DIR = next(
            d for d in LE_BASE.iterdir()
            if d.is_dir() and (d / "chain.pem").exists()
        )
    except StopIteration:
        print("ERROR: Could not locate Let's Encrypt live directory")
        sys.exit(1)

CHAIN_FILE = LE_DIR / "chain.pem"

if not CHAIN_FILE.exists():
    print(f"ERROR: Missing {CHAIN_FILE}")
    sys.exit(1)

# Read bundled certificate chain file
content = CHAIN_FILE.read_text()

# Extract PEM blocks individually
certs = re.findall(
    r"-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----",
    content,
    re.S
)

if not certs:
    print("ERROR: No certificates found in chain.pem")
    sys.exit(1)

# Remove old generated issuer files
for old_file in IPSEC_CA_DIR.glob("issuer-*.pem"):
    old_file.unlink()

# Write certificates separately so strongSwan loads all issuers
for i, cert in enumerate(certs, start=1):
    outfile = IPSEC_CA_DIR / f"issuer-{i}.pem"
    outfile.write_text(cert + "\n")
    print(f"Created: {outfile}")

print()
print(f"Loaded {len(certs)} certificate(s)")
print(f"Source: {CHAIN_FILE}")
print("Done")

Example procedure:

First, place the script under Certbot deploy hooks:

vi /etc/letsencrypt/renewal-hooks/deploy/update-ipsec-certs.py
chmod +x /etc/letsencrypt/renewal-hooks/deploy/update-ipsec-certs.py

If you previously linked chain.pem directly into /etc/ipsec.d/cacerts/, remove it first:

ls -l /etc/ipsec.d/cacerts/chain.pem
rm /etc/ipsec.d/cacerts/chain.pem

Then test renewal:

certbot renew --force-renewal

Deploy hook output:

Created: /etc/ipsec.d/cacerts/issuer-1.pem
Created: /etc/ipsec.d/cacerts/issuer-2.pem

Loaded 2 certificate(s)
Source: /etc/letsencrypt/live/tyo1.1pb.xyz/chain.pem
Done

Before splitting the certificates, ipsec listcacerts only showed:

CN=YR2

After splitting, strongSwan loads both certificates correctly:

CN=YR2
CN=Root YR

After this change, Windows clients that previously failed were able to connect successfully.

So the issue appears to be that strongSwan was not loading the full chain correctly when using chain.pem directly.

Hopefully this helps others facing the same issue.

Excellent. Do you know if the names of the split cert files matter? Yours are in alphabetical order I just wonder if that is a requirement so they get sent out in that order.

Good question. I tested this because I was not sure either.

I renamed the files so that the ordering was intentionally reversed:

  • issuer-1.pemz-issuer.pem
  • issuer-2.pema-issuer.pem

After restarting strongSwan, ipsec listcacerts showed that the load order had changed. Previously the certificates were listed as:

root@tyo1:~# ipsec listcacerts

List of X.509 CA Certificates

  subject:  "C=US, O=Let's Encrypt, CN=YR2"
  issuer:   "C=US, O=ISRG, CN=Root YR"
  validity:  not before Sep 03 09:00:00 2025, ok
             not after  Sep 03 08:59:59 2028, ok (expires in 824 days)
  serial:    4e:bd:24:94:7e:24:d3:94:80:2d:84:a5:2f:d5:b3:19
  flags:     CA CRLSign serverAuth 
  CRL URIs:  http://yr.c.lencr.org/
  pathlen:   0
  certificatePolicies:
             2.23.140.1.2.1
  authkeyId: de:e7:5b:60:d0:22:6d:40:28:7d:3f:0d:01:fe:a4:b5:52:b4:51:94
  subjkeyId: 40:15:2d:26:79:ed:32:20:9e:df:9a:72:1d:d6:32:1f:81:0c:81:0c
  pubkey:    RSA 2048 bits
  keyid:     93:28:4e:7c:5b:ca:60:0b:02:11:63:65:38:09:04:66:6a:c8:9e:0c
  subjkey:   74:89:42:7e:67:9e:4b:01:f7:6e:0b:a6:e5:76:13:c1:06:62:d7:f3

  subject:  "C=US, O=ISRG, CN=Root YR"
  issuer:   "C=US, O=Internet Security Research Group, CN=ISRG Root X1"
  validity:  not before May 13 09:00:00 2026, ok
             not after  Sep 03 08:59:59 2032, ok (expires in 2285 days)
  serial:    f2:4b:6d:17:f9:d9:ad:7c:b1:c9:fe:a7:87:82:69:9f
  flags:     CA CRLSign serverAuth 
  CRL URIs:  http://x1.c.lencr.org/
  certificatePolicies:
             2.23.140.1.2.1
  authkeyId: 79:b4:59:e6:7b:b6:e5:e4:01:73:80:08:88:c8:1a:58:f6:e9:9b:6e
  subjkeyId: de:e7:5b:60:d0:22:6d:40:28:7d:3f:0d:01:fe:a4:b5:52:b4:51:94
  pubkey:    RSA 4096 bits
  keyid:     c9:b9:a1:13:c3:db:0d:6d:09:d2:3e:a8:ea:b8:1d:ce:c1:e6:52:20
  subjkey:   3f:28:d0:db:e5:3f:20:d9:fa:65:12:04:de:aa:af:68:d8:3a:45:a9

After renaming, they appeared in the opposite order:

root@tyo1:~# ipsec listcacerts

List of X.509 CA Certificates

  subject:  "C=US, O=ISRG, CN=Root YR"
  issuer:   "C=US, O=Internet Security Research Group, CN=ISRG Root X1"
  validity:  not before May 13 09:00:00 2026, ok
             not after  Sep 03 08:59:59 2032, ok (expires in 2285 days)
  serial:    f2:4b:6d:17:f9:d9:ad:7c:b1:c9:fe:a7:87:82:69:9f
  flags:     CA CRLSign serverAuth 
  CRL URIs:  http://x1.c.lencr.org/
  certificatePolicies:
             2.23.140.1.2.1
  authkeyId: 79:b4:59:e6:7b:b6:e5:e4:01:73:80:08:88:c8:1a:58:f6:e9:9b:6e
  subjkeyId: de:e7:5b:60:d0:22:6d:40:28:7d:3f:0d:01:fe:a4:b5:52:b4:51:94
  pubkey:    RSA 4096 bits
  keyid:     c9:b9:a1:13:c3:db:0d:6d:09:d2:3e:a8:ea:b8:1d:ce:c1:e6:52:20
  subjkey:   3f:28:d0:db:e5:3f:20:d9:fa:65:12:04:de:aa:af:68:d8:3a:45:a9

  subject:  "C=US, O=Let's Encrypt, CN=YR2"
  issuer:   "C=US, O=ISRG, CN=Root YR"
  validity:  not before Sep 03 09:00:00 2025, ok
             not after  Sep 03 08:59:59 2028, ok (expires in 824 days)
  serial:    4e:bd:24:94:7e:24:d3:94:80:2d:84:a5:2f:d5:b3:19
  flags:     CA CRLSign serverAuth 
  CRL URIs:  http://yr.c.lencr.org/
  pathlen:   0
  certificatePolicies:
             2.23.140.1.2.1
  authkeyId: de:e7:5b:60:d0:22:6d:40:28:7d:3f:0d:01:fe:a4:b5:52:b4:51:94
  subjkeyId: 40:15:2d:26:79:ed:32:20:9e:df:9a:72:1d:d6:32:1f:81:0c:81:0c
  pubkey:    RSA 2048 bits
  keyid:     93:28:4e:7c:5b:ca:60:0b:02:11:63:65:38:09:04:66:6a:c8:9e:0c
  subjkey:   74:89:42:7e:67:9e:4b:01:f7:6e:0b:a6:e5:76:13:c1:06:62:d7:f3

Despite that, Windows clients still connected successfully and the certificate chain validation continued to work.

So based on my testing, filename ordering does not appear to matter. My assumption is that strongSwan builds the chain from the Subject/Issuer relationships of the CA certificates loaded from /etc/ipsec.d/cacerts/, rather than relying on filename order.

Of course, this is based on my environment and testing, but at least for my setup, alphabetical ordering was not required.

Thanks much.

Seems it would be easier for them to use the fullchain.pem just as it is rather than requiring it to be split only for them to combine it again :slight_smile: