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.