When strongSwan met Diffusion Restreinte on Stormshield

Sometimes infrastructure problems end with a large architectural change.
Sometimes they end with changing one constant from 32 to 16.

Let me give you a bit of background.

I was assigned to an existing project that was still in its early stages, involving a highly secure, almost air-gapped system. The environment consists of a few Windows 11 VDIs for specific tasks behind a Guacamole remote desktop gateway. Access to Guacamole is through a VPN terminating on a pair of Stormshield EVA firewalls in HA, and the VPN itself is only available from a specific physical location. The whole architecture must conform to the French Diffusion Restreinte specifications. I was in charge of configuring part of the environment, including the firewalls and VPNs.

While most users will connect from Windows laptops using the Stormshield DR-compatible VPN client, I wanted a small number of Linux systems to be able to connect through the same DR VPN infrastructure as well and reach the services they needed.

That desire turned into hours spent looking at network traces and debug logs, accompanied by an insane amount of tea and biscuits.

The eventual strongSwan changes turned out to be almost embarrassingly small. The interesting part was discovering why they were necessary.

A little context first

Diffusion Restreinte, usually shortened to DR, is a French government protection marking for sensitive information. ANSSI, France’s national cybersecurity agency, publishes the technical requirements for systems that need to handle it, including a specific profile for IPsec.

Stormshield implements that profile in its DR mode. This is not simply a matter of selecting a stricter cipher suite on an otherwise standard IKEv2 configuration. The ANSSI profile also constrains or changes some parts of normal IKEv2 behaviour, which means that a client can be perfectly compliant with standard RFC 7296 and still fail to negotiate with a firewall running in DR mode.

Stormshield’s own documentation makes the interoperability model quite clear: third-party devices and VPN clients are supported in principle, provided they also comply with the ANSSI IPsec DR requirements. So my problem was not that I was trying to make an unsupported Linux client speak some proprietary VPN protocol. strongSwan already speaks IKEv2 perfectly well. The problem was that stock strongSwan and Stormshield DR mode clashed on a few IPsec details during the negotiation.

They spoke the same language. They just disagreed rather strongly about the grammar.

Starting with the obvious

I was not starting completely blind. There was already a working Windows client, and most of the relevant VPN parameters were known from the Stormshield configuration, so the first strongSwan setup was based on those rather than on guesswork. The firewall was using IKEv2 with certificate authentication, AES-256-GCM, HMAC-SHA-256 as the PRF, Brainpool P-256 and ESN, all choices coming from the DR profile rather than from some particularly enthusiastic local hardening exercise.

The certificates had their own constraints too. In my case the client certificate used NIST P-256 (prime256v1 / secp256r1) with ECDSA and SHA-256, while the IKE and Child SA key exchange used Brainpool P-256, or DH group 28. These are two separate uses of elliptic curves, and it is quite easy to mentally merge them into one thing when staring at configuration files for too long.

With the obvious parameters in place, I tried the Linux client. And of course it failed immediately. That, at least, gave me somewhere to start.

Thirty-two is apparently too many

The first useful clue came from the Stormshield logs, which were complaining about the nonce length during IKE_SA_INIT. strongSwan was sending a 32-byte nonce, which is perfectly normal for standard IKEv2, but the DR profile requires it to be exactly 16 bytes.

My first instinct, before knowing this, had actually been in the opposite direction. This was a security-oriented profile, so perhaps it wanted a larger nonce. I tried larger values. But DR had other opinions.

So I went back to the only thing I knew worked: the Windows client. I started a capture and compared the negotiation. The answer was sitting there in plain sight. The Stormshield client was sending a 16-byte nonce.

In strongSwan, the relevant part turned out to be almost insultingly simple:

#define NONCE_SIZE 32

Changing that constant to 16 was enough to get past the first failure and move the negotiation forward. One line changed. Several hours already spent.

Then CERTREQ broke

Getting past the nonce problem only moved the negotiation on to the next disagreement. This time the failure was on the strongSwan side:

invalid X509 hash length (32) in certreq

That sent me into the ANSSI-modified version of RFC 7296. In standard IKEv2, the X.509 CERTREQ payload identifies trusted certification authorities using 20-byte SHA-1 hashes. The ANSSI profile changes that behaviour and uses 32-byte SHA-256 hashes of the CA SubjectPublicKeyInfo instead. Stormshield was therefore doing exactly what the DR profile expected, while stock strongSwan was doing exactly what standard RFC 7296 expected. Same protocol again, different grammar.

The relevant strongSwan parser only accepted SHA-1-sized chunks, so I changed the validation to accept either 20-byte or 32-byte values. That was enough to stop the parser rejecting the payload and let the negotiation continue, but it is important not to overstate what this patch does. It does not add full ANSSI CERTREQ support to strongSwan. The parser becomes tolerant of the 32-byte representation, while other parts of the code still expect the traditional 20-byte CA identifiers, which means the requested CA can still appear as unknown in the logs.

For this client that was not a blocker. I already knew which certificate had to be presented, so I configured strongSwan to always send it and disabled the client’s own normal CERTREQ. This is a practical interoperability fix: my goal was to make it work, not to build a complete implementation of the ANSSI behaviour. At this stage I had enough packet captures to justify even more biscuits.

Childless, but not child-free

The next issue was childless IKE, but this one did not exactly take me by surprise. I had already encountered it while configuring the Windows VPN client, where the logs showed that the client established the IKE SA first and created the Child SA only afterwards. So I already knew this behaviour was part of the Stormshield DR negotiation, and I was more or less waiting for it to become my problem on Linux too. What I did not know yet was how to express that in strongSwan.

The behaviour is defined by RFC 6023 as childless IKE, and it is required by the DR profile. The name is slightly misleading the first time you encounter it, because the connection does not remain childless. It simply means that no Child SA is created during IKE_AUTH. It is negotiated separately once the IKE SA has been established.

Fortunately, strongSwan already supports this, so there was no source patch involved. It was only a matter of finding the right setting:

childless = force

With that in place, the IKE SA authenticated successfully and the negotiation moved on again.

One final refusal: ESN

At this point the IKE SA was finally coming up, but the Child SA was still being rejected with NO_PROPOSAL_CHOSEN. Compared with the previous issues, this one was much less mysterious. The DR profile requires Extended Sequence Numbers for ESP, and I had simply not included ESN in the proposal.

strongSwan already supports it, so there was nothing to patch this time. I only had to add esn to the ESP proposal and retry the negotiation. That was it.

After spending hours on nonce lengths, CERTREQ parsing and childless IKE, the last protocol problem was a missing configuration option. The protocol archaeology was over, and Linux was finally talking to Stormshield DR. Unfortunately, that was only half the job.

And then Linux happened

Having an established IPsec SA and having a usable Linux VPN client are not quite the same thing. I still had to deal with routing, the virtual IP assigned by the firewall and split DNS, and I wanted the Linux client to behave in a reasonably predictable way rather than relying on whatever routes happened to appear automatically.

I ended up using an XFRM interface, explicit routes and systemd-resolved for the internal DNS domains. The Stormshield side assigns the virtual IP and narrows the traffic selectors as expected, while the Linux side takes care of putting the right routes on the XFRM interface and handing the internal DNS servers to the resolver. The exact configuration is in the detailed implementation notes linked at the end, because this is the point where a blog post can very quickly turn into a slightly unfriendly installation manual.

There was also one completely self-inflicted detour. During the first part of the investigation I had been rebuilding only libcharon and copying the modified library into place, which was enough to test the protocol changes. Once I had something working, I rebuilt the proper Ubuntu packages and, in a moment of excessive enthusiasm, installed every generated package with dpkg -i *.deb.

That pulled in plugins I did not actually need, including kernel-libipsec. Routing promptly became strange, ESN stopped behaving as expected, and traffic to a machine on the local LAN started taking routes it had absolutely no business taking. For a while it looked suspiciously like one of my DR changes had broken something else, but the culprit was much more mundane: I had simply installed too much.

Disabling kernel-libipsec and going back to the normal kernel-netlink backend restored sanity. I also left a warning in the implementation notes not to repeat that particular experiment.

Testing something beyond “ESTABLISHED”

At that point I had a tunnel, routes, DNS and something that looked convincingly alive in swanctl –list-sas. But I did not want the end of the exercise to be a satisfying-looking ESTABLISHED line. A VPN is only useful if the application behind it actually works, so I tested the whole path rather than stopping at the IPsec status.

The first checks were deliberately simple. I verified the virtual IP assigned by the firewall, the negotiated traffic selectors and the routes on the XFRM interface. Then I queried the internal DNS servers directly, checked that split DNS resolution was working through systemd-resolved, and finally accessed the internal Guacamole service over HTTPS. ICMP was not part of the test because ping is not allowed in that environment, so the application path was a much more useful validation anyway.

I also wanted to know whether the connection would survive normal lifecycle events rather than only the first negotiation. I tested Child SA rekey, IKE SA rekey and full IKE reauthentication, and all of them completed successfully. During the investigation I also terminated and re-established the tunnel repeatedly, partly as a test and partly because debugging IKE gives you plenty of opportunities to destroy your own progress and start again.

That was enough for me to call the Linux side operational rather than merely negotiated.

One interesting disagreement with the manual

There was one detail I did not want to quietly smooth over. Stormshield’s 2023 DR technical note states that DR negotiation and ESP traffic are carried over UDP/4500, but that is not what I observed on the EVA 4.8.18 system I was testing.

In normal operation, the client negotiated IKE over UDP/500 and the protected traffic went out as native ESP. I then forced encapsulation locally, both to check that the alternative path worked and to make sure the firewall was happy with it. With native ESP blocked and encapsulation enabled on the strongSwan side, the traffic moved over UDP/4500 as expected.

So both modes worked in practice, but the default behaviour I observed did not match the wording in the manual. I do not have enough evidence to explain why, and after enough protocol archaeology I have learned not to invent an explanation merely because an empty space is available for one. The implementation notes therefore record exactly what I saw on this version and in this environment, without pretending that it necessarily applies to every Stormshield DR deployment.

Revocation is unfinished too

There was still one part I was not entirely happy with: certificate revocation. The PKI publishes CRLs, but from the Linux test environment the relevant HTTP and LDAP distribution points were not reachable. An OCSP responder was not part of the original Windows CA deployment either. strongSwan therefore reported that the certificate status could not be checked and continued because revocation checking on the client was not configured as strict.

I could have solved that particular symptom by opening additional network access just so the client could fetch the CRL, but that felt like solving the wrong problem. Revocation is part of the PKI design and should be treated as such, not fixed by adding a convenient firewall rule until the warning goes away.

So I left it explicit rather than pretending the job was complete. The VPN works, certificate authentication works, and the Stormshield side performs its own certificate verification according to the DR configuration, but revocation checking on the Linux client still deserves a proper solution. That is outside the scope of this little interoperability exercise.

The end result

At that point I had a Linux strongSwan client talking successfully to the Stormshield DR environment, including the routing, DNS and lifecycle behaviour I actually needed. What I found interesting was how small the final changes were compared with the amount of investigation required to get there. The work crossed ANSSI documentation, the modified RFC 7296, Stormshield logs, strongSwan source code, Wireshark captures and a known-good Windows client. In the end, the source changes themselves were tiny: one constant for the nonce size and one parser check for the ANSSI CERTREQ format.

There is a certain irony in spending hours doing protocol archaeology only to discover that the actual patch is almost embarrassingly small. But that is often how infrastructure work goes: the difficult part is not changing the code, it is finding the two lines that actually need changing.

I collected the exact patches, build steps, strongSwan configuration, XFRM setup, split DNS configuration, validation commands and the various caveats in a set of detailed implementation notes here:

https://codeberg.org/tarastella/strongswan-stormshield-dr

They are deliberately written as a record of what worked for me with Stormshield EVA 4.8.18 and strongSwan 5.9.13 in October 2026, not as an ANSSI implementation guide or a reference DR client. If someone else falls into the same hole later, hopefully they can start from the two loose floorboards rather than excavating the whole building again, and perhaps with a little less tea and biscuits.

2026-10-16