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

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.