Guide

Native IKEv2 and IPsec

Pre-Alpha. This page describes behaviour that may change.

Ze implements native IKEv2 in Go for route-based IPsec VPN tunnels. It does not require strongSwan, libreswan, or another external IKE daemon. The IKE engine, cryptographic primitives, wire codec, and XFRM dataplane integration are all in-tree. The Ze binary negotiates IKE SAs, installs XFRM policies and states, and programs routes through XFRM interfaces from the same YANG config tree as every other subsystem.

Architecture

The IPsec stack is split across several packages:

Package Role
internal/component/ike/wire IKEv2 wire format codec for RFC 7296 payloads
internal/component/ike/crypto DH groups, PRFs, integrity, encryption, and key derivation
internal/component/ike/transport UDP transport with NAT-T keepalives and port 4500 encapsulation
internal/core/eap EAP-MSCHAPv2, EAP-TLS and EAP MD5-Challenge authentication
internal/component/ike/engine IKE_SA_INIT, IKE_AUTH, CREATE_CHILD_SA, and INFORMATIONAL state machines for initiator and responder roles, rekeying, and DPD
internal/component/ike/ipsec YANG schema, configuration, and validation
internal/component/ike/dataplane XFRM policy and state programming through netlink
internal/component/pki X.509 certificate and private-key store

Configuration

IPsec is configured under vpn { ipsec { } }. Named ike-group and esp-group blocks hold the IKE and ESP proposals and lifetimes. Each site-to-site peer references a group pair, sets its authentication, and binds to an XFRM interface for route-based forwarding.

pki {
    certificate my-cert {
        certificate-file /etc/ze/certs/router.pem;
        private-key-file /etc/ze/certs/router-key.pem;
    }
    ca-certificate my-ca {
        certificate-file /etc/ze/certs/ca.pem;
    }
}

vpn {
    ipsec {
        interface eth0;

        ike-group site-b-ike {
            key-exchange ikev2;
            lifetime     28800;
            proposal 1 {
                encryption aes256gcm;
                hash       sha256;
                dh-group   14;
            }
            dead-peer-detection { interval 30; timeout 120; }
        }

        esp-group site-b-esp {
            lifetime 3600;
            pfs      enable;
            proposal 1 { encryption aes256gcm; }
        }

        site-to-site {
            peer site-b {
                ike-group       site-b-ike;
                esp-group       site-b-esp;
                connection-type initiate;
                local-address   198.51.100.2;
                remote-address  198.51.100.1;
                authentication {
                    mode           x509;
                    certificate    my-cert;
                    ca-certificate my-ca;
                }
                vti { bind vti0; }
            }
        }
    }
}

Traffic selectors are not listed per tunnel. Route-based IPsec encrypts traffic that the routing table forwards through the bound XFRM interface. The generated configuration reference documents the XFRM interface leaves.

Which edits restart a tunnel

Committing a change to ANY leaf inside a peer block restarts that peer, and so does a change to the ike-group or esp-group that peer names. The tunnel drops, Ze re-negotiates it, and the new configuration is what the new IKE SA and its Child SA carry. There is no list of leaves that count and leaves that do not: the whole peer configuration and both resolved groups are compared, so a leaf that Ze gains in a later release restarts the tunnel from the day it exists.

Rotating a cipher is the case worth naming, because it edits no peer block at all. A peer holds the NAME of its groups and none of the crypto, so ike-group IKE-1 { proposal 1 { encryption ... restarts every peer that names IKE-1 and no other.

A commit that leaves a peer's block and its two groups unchanged does not touch that peer, whatever else in the configuration moved. Adding a second peer, editing a firewall rule or changing a log level leaves a running tunnel carrying traffic.

Three consequences are worth planning around:

A commit is REFUSED when interface cannot supply an address and some peer has no local-address of its own. The transaction rolls back and every running tunnel keeps carrying traffic, because applying that configuration would restart each of those peers into a state that cannot bind. The error names the interface and how many peers depend on it. Give those peers a local-address, or bring the interface up, then commit again. At DAEMON START the same configuration is applied and the condition is logged instead: there is no running tunnel to protect, and the peers that carry their own local-address still come up.

IKE features

Feature Detail
IKEv2, RFC 7296 Full IKE_SA_INIT, IKE_AUTH, CREATE_CHILD_SA, and INFORMATIONAL exchange support
Role Initiator or responder per peer through connection-type initiate or respond
Proposals AES-CBC, AES-GCM 128/256, and ChaCha20-Poly1305; MODP 2048/3072/4096/8192 and ECP 256/384/521 DH groups; SHA-256/384/512 PRFs
Authentication Pre-shared key, X.509 certificates, EAP-MSCHAPv2, and EAP-TLS; Ze acts as the EAP authenticator in responder mode
NAT-T, RFC 3948 Automatic NAT detection, UDP encapsulation on port 4500, and keepalives
DPD Dead Peer Detection through INFORMATIONAL exchanges with configurable interval and timeout
Rekeying On-wire CREATE_CHILD_SA rekeying for IKE and Child SAs, make-before-break installation, and collision handling
MOBIKE, RFC 4555 Not implemented; an endpoint address change requires the SA to be re-established
Denial-of-service COOKIE challenge before a half-open slot is committed, and the INVALID_KE_PAYLOAD group retry
Dataplane read-back show vpn ipsec dataplane reads the kernel SAD and SPD, and a drift check compares them against the engine
Virtual IP pool Not wired. The remote-access container parses and no session reads it

IKEv2 responder role

Ze can act as an IKEv2 responder or initiator. A peer with connection-type respond waits for an unsolicited inbound IKE_SA_INIT from its configured remote-address, answers IKE_AUTH, and installs the first Child SA. connection-type initiate, the default, starts the exchange. The UDP transport always listens, so responder mode does not need a separate listen switch.

As responder, Ze authenticates with a pre-shared key, X.509 certificate, or EAP. For EAP it acts as the EAP-MSCHAPv2 or EAP-TLS server for a road-warrior client. It presents its own certificate or PSK first, runs the EAP method, and derives session keys from the EAP MSK.

A new inbound IKE_SA_INIT can proceed while an older established SA for the same peer is still being maintained. Once the replacement authenticates, its INITIAL_CONTACT notification tells the remote endpoint to discard the stale SA. This lets a responder recover immediately after an operator clear or peer restart instead of waiting for Dead Peer Detection.

vpn {
    ipsec {
        interface eth0;
        ike-group IKE-PSK {
            key-exchange ikev2;
            proposal 1 { encryption aes256gcm; hash sha256; dh-group 14; }
        }
        esp-group ESP-PSK {
            pfs enable;
            proposal 1 { encryption aes256gcm; }
        }
        site-to-site {
            peer swan {
                ike-group       IKE-PSK;
                esp-group       ESP-PSK;
                connection-type respond;
                local-address   172.28.0.2;
                remote-address  172.28.0.3;
                authentication {
                    mode              pre-shared-secret;
                    local-id          "172.28.0.2";
                    remote-id         "172.28.0.3";
                    pre-shared-secret "$9$encoded";
                }
            }
        }
    }
}

For a road-warrior EAP server, set authentication { mode eap-mschapv2 }, eap-tls or eap-md5, then reference a device certificate and ca-certificate from the PKI store. Configure the client as a site-to-site peer. Read the EAP MD5-Challenge section below before you select eap-md5.

The remote-access container is not wired yet. It parses, and no session reads it, so a pool assigns no address and an eap-user list authenticates nobody. plan/immediate/spec-ipsec-remote-access.md owns the work.

Every EAP mode and X.509 needs a ca-certificate. The daemon refuses a remote certificate it cannot chain to that anchor. A certificate with no trust anchor authenticates nobody: any self-signed certificate that carries a valid signature would pass. RFC 7296 Section 2.16 requires EAP to run with a public-key authentication of the responder, and the anchor is what makes that signature attributable.

ca-certificate holds one anchor, not a path. A two-level authority therefore needs the peer to send its intermediate certificates. RFC 7296 Section 3.6 puts the peer certificate first, and Ze reads every later CERT payload as a link toward the anchor. Ze sends its own chain the same way: the certificate entry first, then the intermediate that PKI entry carries. A refusal names the anchor and the number of intermediate certificates the peer supplied.

Ze refuses a config that names no ca-certificate at commit and at reload. It does NOT refuse it under ze config validate, which reads the schema and does not run a plugin's config verifier.

Pre-shared secret encoding

pre-shared-secret-encoding reads the secret as ascii, the default, or as hex. RFC 7296 Section 2.15 requires the management interface to accept both. Use hex for a secret that holds non-printable octets.

The encoding is stated and is never guessed from the value. A secret such as abcdef0123456789 is valid ASCII and valid hex. A daemon that read the value to decide would silently reinterpret a deployed secret. A hex value with an odd length or a non-hexadecimal character is refused at commit rather than read as ASCII.

The at-rest $9$ obfuscation is unwrapped first, so a hex secret can also be stored obfuscated.

Remote identity

remote-id is the identity Ze expects the remote endpoint to assert. When it is set, Ze enforces it twice on every authentication.

Check What it compares Refusal
Policy The IDi or IDr payload the peer sent against remote-id Every authentication mode
Certificate The same asserted identity against the remote certificate Certificate modes only

The certificate check is the half that matters when one authority issues to several clients. The chain proves the authority issued the certificate. It does not prove the certificate speaks for this peer, because the peer chooses the identity its signature covers. Without the second check any client of that authority authenticates as any peer.

A subject alternative name binds. The subject common name binds only when the certificate carries no alternative name extension at all. The two fields do not carry the same attestation: X.509 name constraints reach the alternative names and never the subject distinguished name. An authority that permits only dNSName .branch.example.com leaves the common name free, so a common name read after a present alternative name would defeat that constraint.

The value you configure picks the field. An address remote-id binds against the address alternative names alone, never against a domain name that spells the same address. Certificate authorities issue an address alternative name under a tighter policy than a name, and this stops the peer from choosing the weaker field.

Ze compares six identity types: ID_IPV4_ADDR, ID_IPV6_ADDR, ID_FQDN, ID_RFC822_ADDR, ID_KEY_ID, and ID_DER_ASN1_DN. A domain name and a mail address compare with the ASCII letters folded. An address compares as an address, so the encoding does not matter. A distinguished name compares in RFC 4514 string form, and it binds against the certificate subject octet for octet. A peer that asserts ID_DER_ASN1_GN is refused, because Ze compares no general name.

A local-id written as a distinguished name is still refused at commit. Ze derives the type it SENDS from the shape of the value, and that derivation has no ID_DER_ASN1_DN form.

One peer, or a set of peers

A remote-id can name one peer or a group of them. RFC 4301 Section 4.4.3.1 requires both forms of Peer Authorization Database entry. Write a group with the hierarchy separator in front. Such an entry admits the identities BELOW it and never the entry text itself.

You write It admits
vpn.example.com that name alone
.example.com vpn.example.com, gw.branch.example.com
gateway@example.com that address alone
@example.com gateway@example.com
CN=hq,O=Example that distinguished name alone
,ST=MA,C=US every distinguished name whose top RDNs are ST=MA,C=US
10.0.0.1 that address alone
10.0.0.0/24 every address in the block

RFC 4514 renders the most significant RDN last, so a distinguished-name group is written with the top of the tree at the end and a leading comma in front. An address group takes the same CIDR prefix a traffic selector takes. ID_KEY_ID is always compared exactly: an opaque octet string has no hierarchy, so a leading . or @ in one is content.

Substring matching is not offered. .example.com refuses notexample.com, and it refuses the bare example.com too. Write the apex exactly when you mean it.

A group never widens what one certificate proves. The certificate check above still binds the identity the peer ASSERTED, so a peer admitted by .example.com must hold a certificate that carries its own name. A local-id that names a group is refused at commit, because Ze asserts exactly one identity and would send the pattern text verbatim.

RFC 7296 Section 4 requires that you can configure Ze to accept a PKIX peer whose identity is ID_KEY_ID. An opaque key id matches no certificate field, so Ze cannot derive that binding and denies it by default. Set remote-id-type key-id to state that the chain to ca-certificate plus the exact key id IS the binding you intend.

remote-id-type also pins ONE identity type for the peer. Without it a text remote-id accepts ID_FQDN, ID_RFC822_ADDR and ID_KEY_ID alike. All three compare as text. Set remote-id-type rfc822-address when you know the value is a mail address. Ze then refuses a peer that asserts ID_FQDN with the same text.

The leaf governs what Ze ACCEPTS. The type Ze sends still follows local-id.

An unset remote-id runs neither check. Every certificate the configured authority issued then authenticates as this peer. Ze logs a warning that names the peer and the identity it accepted. Set remote-id whenever the authority issues to more than one client.

Certificate chain length

certificate-count bounds the X.509 chain in both directions. It is the most certificates Ze sends for the peer, and the most it accepts from it. RFC 7296 Section 3.6 sets that figure at four, and the default is four, so a peer you never configure gets the conformant behaviour.

A peer that sends more than the bound is REFUSED. Ze does not truncate the chain. A silent trim hides from you that the limit was reached. It also makes the surviving certificates depend on the order the peer chose.

Name the intermediates on the PKI certificate entry, in order from the issuer of your device certificate toward the trust anchor. Ze refuses at commit a peer whose certificate-count is smaller than the chain its PKI entry holds.

Hash and URL certificates

hash-and-url replaces the certificate on the wire with a 20-octet SHA-1 hash and a URL that resolves to it (RFC 7296 Section 3.6). It keeps IKE messages short. Set certificate-url to the http URL where you publish this device's certificate.

The leaf defaults to false, and that default is a security property. Resolving a URL a peer sends makes an outbound request on behalf of a peer that is NOT yet authenticated. Ze must fetch the certificate before it CAN authenticate the peer. With the leaf false Ze advertises nothing, a conforming peer sends no such payload, and Ze drops one from a peer that sends it anyway.

When you enable it, the lookup is bounded:

Control Bound
Scheme http only. Every other scheme is refused before any name resolution
Response size 64 KiB
Total timeout 5 seconds
Redirects None. A redirect is refused
Destination Loopback, private, link-local, multicast and metadata addresses are denied
Hash The SHA-1 is verified BEFORE any parser reads the fetched bytes
Cache Keyed by the hash, never by the URL

Use certificate-url-allow to permit a destination the deny list refuses. Run ze doctor to check a configured certificate-url.

The EAP peer validates the authenticator's certificate chain against that trust anchor. EAP-TLS has no server hostname, so the check validates the chain without DNS-name matching.

EAP method negotiation

The far end chooses the EAP method, and authentication { mode ... } sets the one Ze runs. When the far end asks for a different authentication method, Ze answers with an EAP Nak that names the configured method (RFC 3748 Section 5.3.1) rather than ending the exchange. A concentrator that also runs that method then offers it, and the tunnel establishes with no configuration change. A concentrator that runs no other method ends the exchange, and Ze logs ike: EAP failed. Change mode to a method both ends run.

The far end can also send an EAP Notification, which is how a concentrator reports something such as an expiring password. Ze logs it as ike: EAP notification from the authenticator with the message, and the authentication continues. The message is unauthenticated: it is chosen by whoever sent the packet, so read it as a claim.

When Ze is the EAP server and the client refuses the offered method, the ike: EAP authentication failed line names the types the client asked for and the type Ze offered.

When Ze is the EAP-TLS client on TLS 1.3, it requires the RFC 9190 Section 2.5 protected success result indication before it accepts the EAP-Success. An authenticator that does not send it fails the peering, and the message names the authenticator, the negotiated version and two remedies: an authenticator that follows Section 2.5, or a peering on TLS 1.2, which RFC 5216 governs and where no indication exists. TLS 1.2 peerings are unaffected.

EAP MD5-Challenge

authentication { mode eap-md5 } runs EAP Type 4, the CHAP exchange of RFC 1994 carried inside EAP (RFC 3748 Section 5.4). Both ends read the pre-shared-secret leaf as the CHAP secret. Ze runs it as the authenticator and as the peer.

It is discouraged, and the daemon says so. RFC 7296 Section 2.16: "EAP methods that do not establish a shared key SHOULD NOT be used, as they are subject to a number of man-in-the-middle attacks". The daemon writes one warning for each peer that carries the mode. It writes it when the configuration is applied, and not for each handshake.

What it costs you, against the two other EAP modes:

Property eap-md5 eap-mschapv2 and eap-tls
Key derivation None. RFC 3748 Section 5.4 records "Key derivation: No" An MSK of 64 octets
IKEv2 AUTH payloads Keyed by SK_pi and SK_pr, which every party completing IKE_SA_INIT holds Keyed by the EAP MSK
Who is authenticated by EAP The client only Both ends, for eap-tls
Secret at rest The CHAP secret, needed in the clear on both ends The same for eap-mschapv2; a private key for eap-tls

The responder still authenticates itself with a public-key signature, because RFC 7296 Section 2.16 requires one whichever EAP method runs. So eap-md5 needs the same certificate and ca-certificate every other EAP mode needs. Select it when the client runs no other method, and prefer eap-tls or eap-mschapv2 everywhere else.

EAP-TLS with TLS 1.3

RFC 9190 Section 2.5 requires an encrypted TLS record that carries application data 0x00. The EAP-TLS 1.3 server sends it once it has processed the client Finished and sent its last handshake message. That record is the protected success result indication. Ze sends it in the flight that round already carries, so the exchange keeps its round count.

The record is what makes a TLS 1.3 EAP-TLS client work. strongSwan derives no MSK without it, so the AUTH payload of RFC 7296 Section 2.16 cannot be computed and the SA never establishes. A TLS 1.2 exchange concludes with the bare EAP-Success it concluded with before, because the version test reads the negotiated version.

Ze issues a TLS session ticket on every EAP-TLS 1.3 authentication, because RFC 9190 Section 2.1.2 makes that a MUST with no condition on it. The ticket key belongs to the peering rather than to the session, so a later authentication with the same peer can redeem it. Whether Ze USES a ticket is the session-resumption leaf, which defaults to true and covers both roles; with it false every authentication runs a full handshake and the ticket still goes out. A resumed authentication re-checks the cached chain against the current ca-certificate and crl, so a certificate that was revoked or expired since the first handshake is refused.

Ze caps the TLS version at 1.3 on both EAP-TLS roles. RFC 9190 Section 1 requires that cap, because EAP-TLS couples the TLS state machine to the EAP one and a version nobody integrated into the EAP half breaks the pair. Ze offers no setting that raises the cap. TLS 1.2 stays reachable under it, and an EAP-TLS peer that speaks only TLS 1.2 still authenticates with the RFC 5216 key derivation.

Ze puts the four-octet TLS Message Length only on the first fragment of a message it had to fragment. RFC 9190 Section 2.1.9 forbids the L bit on a message that fits in one EAP-TLS packet, so in a packet capture the TLS data of such a message starts one octet after the EAP Type, not five. Ze accepts an unfragmented message from the far end with the L bit or without it, which the same sentence requires.

Ze does not send local-id as the EAP-TLS identity

RFC 9190 Section 2.1.8 says that "a client supporting TLS 1.3 MUST NOT send its username (or any other permanent identifiers) in cleartext in the Identity Response". So for mode eap-tls Ze does not put local-id in the EAP-Response/Identity. It sends an anonymous Network Access Identifier derived from it:

local-id EAP identity Ze sends Why
alice@example.com @example.com the username is omitted and the realm is kept, which is the form Section 2.1.8 recommends
@example.com @example.com already anonymous
ze-test-client anonymous
alice@localhost anonymous localhost is not a realm the RFC 7542 Section 2.2 grammar accepts

The realm survives because it is what routes an EAP exchange to a home domain, and because RFC 9190 Section 2.1.3 asks for the same realm again when a session resumes. Give local-id a realm when the exchange has to route: an identity with none tells the far end nothing.

local-id is unchanged everywhere else. It is still IDi in IKE_AUTH, and the EAP-MSCHAPv2 and EAP MD5-Challenge methods still send it as their identity, because RFC 9190 governs EAP-TLS alone and those methods carry their username inside the method exchange.

EAP-TLS 1.3 needs a certificate revocation list

RFC 9190 Section 5.4 says that "when EAP-TLS is used with TLS 1.3, the revocation status of all the certificates in the certificate chains MUST be checked (except the trust anchor)". Ze checks it on both roles: the authenticator over the client's chain, and the peer over the authenticator's chain.

The revocation lists come from the pki ca the peer validates against, in a crl leaf-list. Each value is an X509 CRL PEM document or base64-encoded DER, and one CA can hold several, which is what a segmented CRL or a rollover needs. A list this CA did not sign is refused at commit, because it cannot answer for the certificates this CA issued.

pki {
    ca my-ca {
        certificate "-----BEGIN CERTIFICATE-----...";
        crl "-----BEGIN X509 CRL-----...";
    }
}

A CA with NO list and a list that revokes NOTHING are different answers, and the difference decides whether a session establishes:

What the CA holds TLS 1.3 TLS 1.2
A current list, and the chain is not on it The session establishes The session establishes
A current list naming a certificate on the chain Ze refuses, naming the certificate, its serial number and the CA that withdrew it The same
Ze refuses: nothing can answer the question Section 5.4 makes mandatory The session establishes
A list whose nextUpdate has passed Ze refuses: an expired list says nothing about the present The same

The "no list at all" row is the only one where the two versions differ. TLS 1.2 is governed by RFC 5216 Section 5.4 instead, which asks only that an implementation "MUST support the use of Certificate Revocation Lists (CRLs)". Nothing there obliges a session the operator configured no list for, so a TLS 1.2 peer with no list still authenticates while the same peer on TLS 1.3 does not.

Every other row holds on both versions, because a list that IS configured is checked whatever was negotiated. An expired list is not a usable one, so it refuses on TLS 1.2 too: the operator asked for the check, and the source can no longer perform it.

Publish a fresh list before the nextUpdate of the one in the config passes. An expired list refuses the same peers a missing one does.

EAP-TLS 1.3 staples the OCSP response you give it

RFC 9190 Section 5.4 says that "EAP-TLS servers supporting TLS 1.3 MUST implement Certificate Status Requests (OCSP stapling)". Ze answers one with the response configured on the certificate it presents, in an ocsp-response leaf beside that certificate:

pki {
    certificate vpn-server {
        certificate "-----BEGIN CERTIFICATE-----...";
        ocsp-response "MIIBxAoBAKCB...";
        private { key "..."; }
    }
}

Fetch the value from the responder your CA publishes, with openssl ocsp -respout. Give it as base64-encoded DER or as an OCSP RESPONSE PEM document. A response about another certificate is refused at commit. A certificate with no response answers a Certificate Status Request with no status, which RFC 6066 Section 8 permits, and the peer then falls back to the crl of the CA that issued it.

An OCSP response carries a nextUpdate, and a peer that checks the status refuses the certificate once that time passes. Replace the value before it does, the same discipline a crl needs.

An EAP-TLS peer can require the stapled status

RFC 9190 Section 5.4 puts a MUST on a peer that uses Certificate Status Requests to check a chain: "it MUST treat a CertificateEntry (but not the trust anchor) without a valid CertificateStatus extension as invalid and abort the handshake with an appropriate alert". The certificate-status-request leaf makes Ze such a peer, and it is false by default:

ipsec {
    peer headquarters {
        authentication {
            mode eap-tls;
            certificate-status-request true;
        }
    }
}
What the authenticator staples certificate-status-request true certificate-status-request false
A current response saying the certificate is good The session establishes The session establishes
Nothing Ze refuses, naming the missing CertificateStatus The session establishes
A response reporting the certificate revoked or unknown Ze refuses, naming what the responder said The session establishes
A response whose nextUpdate has passed, or one about another certificate Ze refuses, naming which The session establishes

With the leaf false the chain is still checked against the crl of its CA, which Section 5.4 requires on TLS 1.3 either way.

One deployment limit comes with the leaf. Ze reads the stapled status of the LEAF certificate and of no other, because Go's TLS stack parses the status extension of the first CertificateEntry alone. Section 5.4 makes an entry whose status is not valid invalid, and an entry Ze cannot read the status of is one it cannot call valid, so a chain carrying an intermediate CA below the trust anchor is refused while the leaf is on. Give the authenticator a certificate its trust anchor signed directly, or leave the leaf off and check by crl.

An EAP-TLS peer re-checks the server certificate once the tunnel is up

RFC 9190 Section 5.4 asks an EAP-TLS peer to "support checking for certificate revocation after authentication completes and network connectivity is available", and to "use a secure transport" for it. RFC 5216 Section 5.4 asks for the same with no TLS version attached.

Ze runs that check as soon as the Child SA is installed, which is the moment the tunnel it just authenticated gives it a network. It asks the OCSP responder named by the authenticator's own certificate, over https, and it needs no configuration: the responder URL comes from the certificate's authority information access extension.

What the responder says What Ze does
The certificate is revoked Closes the SA and logs the revocation. The reconnect that follows runs a fresh authentication, which the same certificate fails inside the handshake
The certificate is good Logs it and leaves the SA up
Nothing, because it cannot be reached Logs a warning and leaves the SA up. A responder outage is not evidence of a revocation
Nothing, because the certificate names an http responder or none at all The same warning. Section 5.4 requires a secure transport, so an http responder is refused before any connection opens

The check runs once per authentication rather than on a timer, and it never delays the tunnel: the Child SA is up before it starts.

EAP-TLS with TLS 1.2 needs RFC 7627

RFC 5216 Section 2.3 derives the EAP-TLS MSK from a TLS key material export, and Go refuses that export on a TLS 1.2 session that did not negotiate the RFC 7627 extended master secret. Ze cannot authenticate such a peer. It says so rather than failing silently: the log line names the peer, the negotiated version, RFC 7627 and the answers below.

strongSwan 5.9.14 is such a peer, by default rather than by limitation. charon ships version_max = 1.2 in /etc/strongswan.d/charon.conf, with the comment "default to TLS 1.2 until 1.3 is stable for use in EAP", and it implements no extended master secret. Set charon.tls.version_max = 1.3 and the same 5.9.14 build reaches an established SA, which is what test/interop-ipsec/scenarios/eap-tls13 runs.

Answer What to change
Move the peer to TLS 1.3 RFC 9190 covers EAP-TLS over TLS 1.3, and the export is unconditional there. This is the preferred answer, and on strongSwan it is one line: charon.tls.version_max = 1.3
Add RFC 7627 to the peer's TLS 1.2 stack The export then works, and ze needs no change
Configure another EAP method EAP-MSCHAPv2 derives its key without a TLS export

Ze offers no setting that lifts the refusal, and there is nothing to add to the environment. Go 1.27 removed the tlsunsafeekm GODEBUG setting that once lifted it. A removed setting carrying its old value is a fatal error that the Go runtime raises before the daemon starts, so setting it stops ze rather than reaching the peer.

Denial-of-service protection

RFC 7296 Section 2.6 names two attacks on an IKE responder: state and CPU exhaustion from initiation requests with forged source addresses. The answer is a COOKIE. Ze answers an inbound IKE_SA_INIT with a COOKIE notification and commits no state, and the initiator must repeat its request with that cookie as the first payload before Ze creates an SA or takes the peer's half-open handshake slot.

cookie-threshold sets how many half-open IKE SAs Ze tolerates before it challenges. It defaults to 0, which challenges every inbound initiation. That default costs a genuine peer one extra round trip and closes a real availability hole: a responder peer has one half-open slot, held for 30 seconds, so a single spoofed datagram bearing that peer's source address would otherwise deny it service, and one datagram every 30 seconds would deny it indefinitely. Raise the value to tolerate that many half-open handshakes before challenging.

vpn {
    ipsec {
        cookie-threshold 0;
    }
}

The cookie is an HMAC-SHA256 over the initiator's nonce, its source address and its SPI, under a secret Ze generates and rotates every 60 seconds. A cookie minted under the previous secret stays valid for one further interval, so a rotation never breaks an in-flight handshake. A cookie that does not match is ignored rather than rejected, as the RFC requires: the message is processed as though it carried no cookie, which for a pressured responder means issuing a fresh challenge.

Diffie-Hellman group correction

An initiator has to guess which Diffie-Hellman group the responder will pick. When the guess is wrong the responder answers INVALID_KE_PAYLOAD naming the group it accepts, and Ze retries the IKE_SA_INIT under that group. Without this, Ze could never establish with a peer that prefers a different group.

The retry re-offers the whole configured proposal set rather than narrowing it to the suite carrying the responder's group. RFC 7296 Section 1.2 requires that, because the rejection is unauthenticated and a narrowed re-offer would let an attacker who forges one notify choose the cipher. Ze also refuses a group it never proposed, which stops the same attacker choosing the group. Both COOKIE and INVALID_KE_PAYLOAD retries share one budget of three per connection attempt, so two peers can never oscillate between them.

Rekeying

Both Child SAs and IKE SAs are rekeyed with an on-wire CREATE_CHILD_SA exchange. This replaced an earlier local-only key roll that could silently desynchronise a live tunnel. A Child SA rekey carries N(REKEY_SA) with fresh nonces and traffic selectors. An IKE SA rekey performs a fresh Diffie-Hellman exchange and resets the message-ID counters.

Rekeying is make-before-break: the replacement SA is installed before the old one is deleted, so forwarding does not pause. Simultaneous rekeys are resolved by the RFC 7296 nonce rule. Section 2.8.1 states it as a recommendation: if redundant SAs are created, the endpoint that created the SA with the lowest of the four nonces SHOULD close that SA. Ze resolves the collision one step earlier and abandons its own pending exchange when its nonce is the lower one, so the redundant SA is never created and both ends converge on one SA. Ze rekeys at the ESP or IKE soft lifetime and retransmits a lost CREATE_CHILD_SA before tearing the tunnel down. Rekeys are counted by ze_ipsec_rekey_total{peer} and streamed as child-rekey events by monitor vpn ipsec.

Child SA and dataplane

Child SAs define traffic selectors and the ESP proposal. Ze programs XFRM policies and states through netlink. Route-based IPsec uses XFRM interfaces.

Feature Detail
ESP proposals AES-GCM-16 128/256 and AES-CBC with HMAC-SHA-256/384/512
Traffic selectors IPv4 and IPv6 CIDR prefixes, an optional IP protocol, and an optional single port. See Traffic selectors and narrowing below
Encapsulation Tunnel mode by default; mode transport negotiates transport mode per RFC 7296 Section 1.3.1
Connection connection-type initiate starts the exchange; connection-type respond waits for the peer
Replay protection Anti-replay window, default 64, the size RFC 4303 Section 3.4.3 says SHOULD be the default
Lifetime Time-based rekeying, from the ESP group's lifetime in seconds. Ze carries no byte lifetime, so RFC 4301 Section 4.4.2.1's simultaneous use of both is not met
Policy order policy-priority ranks this peer's SPD entries, lower first. See SPD ordering below

SPD ordering

RFC 4301 Section 4.4.1 makes the Security Policy Database an ordered database, because entries with range selectors overlap: "Thus, a user or administrator MUST be able to order the entries to express a desired access control policy."

policy-priority under a peer is that order. A LOWER value is searched FIRST, in Ze and in the Linux kernel. Two peers that both negotiate 0.0.0.0/0 describe the same traffic, so give the one that must win the lower value.

The default is 2000, which every peer took before the leaf existed, so an unordered configuration installs what it installed before. The range starts at 101 because 100 holds the IKE control-plane bypass. A peer entry at or above that rank matches the IKE datagrams first, which hands the exchange that builds and rekeys the SA to the SA itself, and the tunnel could then never renegotiate. Ze refuses such a value at commit.

Security Policy Database entries

RFC 4301 Section 4.4.1 gives the Security Policy Database three dispositions: "PROTECT, BYPASS, or DISCARD". A site-to-site peer produces the PROTECT entries, because a protect entry names an ESP transform and a peer to negotiate keys with. The other two name neither, so no negotiation can produce them and they are written directly:

set vpn ipsec policy drop-guest action discard
set vpn ipsec policy drop-guest local prefix 10.0.0.0/24
set vpn ipsec policy drop-guest remote prefix 192.168.99.0/24
set vpn ipsec policy drop-guest order 800

Section 7.4 makes the discard disposition an obligation: "All implementations MUST support DISCARDing of fragments using the normal SPD packet classification mechanisms." The list above is the management interface Section 4.4.1 requires for it.

Leaf Values Default Meaning
action bypass, discard none, required What the entry does with matching traffic
order 101 to 2147483647 1000 Rank, lowest searched first
direction out, in, both both Which half of the database the entry joins
protocol 0 to 255 0 (any) Next-layer protocol selector
local prefix CIDR none, required Ze's own side of the boundary
local port 1 to 65535, or any any Local port selector
remote prefix CIDR none, required The far side of the boundary
remote port 1 to 65535, or any any Remote port selector

both installs a mirrored pair, one entry in each direction: the outbound entry keeps local and remote as written, and the inbound entry swaps them, because the local side of a flow is the destination of an inbound packet.

The default order of 1000 sits between the IKE control-plane bypass at 100 and a peer's default rank of 2000, so an entry written with no order outranks the tunnels and carves its hole out of them. Raise it above 2000 to let a tunnel win instead. An order at or below 100 is refused at commit, for the reason policy-priority states above.

Four rules are refused at commit rather than approximated at install time, because a widened selector fails in both directions: a widened bypass passes protected traffic in the clear, and a widened discard black-holes traffic the operator meant to carry.

On Linux a discard entry is an XFRM policy with action block and a bypass is allow with no template. On VPP they are IPSEC_API_SPD_ACTION_DISCARD and IPSEC_API_SPD_ACTION_BYPASS. show vpn ipsec dataplane reports the disposition of every installed policy, including one another daemon installed.

Ze installs these entries when the configuration is applied and removes them when the entry leaves the configuration or the engine stops. They need no peer, no key and no negotiation, so they are in force whether or not any tunnel is up.

Both ESP wire forms

RFC 7296 Section 2.23 requires a device that supports NAT traversal to receive UDP-encapsulated ESP and bare ESP at any time. One Linux XFRM state serves one form. A state that carries the encapsulation template refuses bare ESP, and a state without one refuses encapsulated ESP. Ze serves the second form beside the kernel. A raw IPPROTO_ESP reader takes the datagram XFRM refused. It re-presents that datagram through port 4500, which carries UDP_ENCAP, so the kernel hands XFRM the form its template wants. One established Child SA therefore keeps carrying traffic when its peer changes ESP form mid-session, with no rekey and no delete.

The reader carries a per-socket inbound policy exemption. Linux applies the inbound IPsec policy check before it queues a packet to a raw socket. Ze's own Child SA policy refuses exactly the datagrams this reader exists to recover. The exemption reaches that one socket. XFRM still decrypts, still owns the replay window, and still applies the inbound policy to the re-presented datagram.

The second-form reader serves IPv4. An SA that asks for both forms over IPv6 is refused rather than installed with one form. The form Ze SENDS is a separate decision and follows the NAT verdict alone: Ze encapsulates when it detected a NAT.

Traffic selectors and narrowing

A peer's traffic-selector list states which traffic its Child SAs carry. It is also the policy RFC 7296 Section 2.9 narrows a peer's proposal against.

vpn { ipsec { site-to-site { peer branch-1 {
    traffic-selector 1 {
        local  { prefix 10.1.0.0/16; }
        remote { prefix 10.2.0.0/16; }
    }
    traffic-selector 2 {
        protocol 6
        local  { prefix 10.3.0.0/24; port 179; }
        remote { prefix 10.4.0.0/24; }
    }
} } } }

When the list is absent the peer accepts whatever the remote endpoint proposes. That is the behaviour of every configuration written before the list existed, so adding the list is what restricts a peer, never omitting it.

As responder, Ze narrows the initiator's proposed TSi and TSr to a subset the list allows, and leads the answer with the initiator's first choices. A proposal it cannot narrow to a non-empty subset draws a TS_UNACCEPTABLE notification. As initiator, Ze proposes the list in order, because RFC 7296 reads the order as the preference order.

The selectors Ze puts on the wire are the selectors it programs. A proposal it cannot program exactly is narrowed FURTHER, never rounded outward: an address range that is not a prefix becomes the largest prefix inside it, and a port range that is neither all ports nor one port becomes its first port. A rekey is never narrowed below the scope in use.

Editing a peer's traffic-selector list and committing restarts that peer. The tunnel drops and comes back carrying the new selectors, and the kernel policy follows. This is what RFC 7296 Section 2.9.2 asks for: a rekey that would need a narrower scope means the policy changed, and the SA should have been deleted when the change took effect. Narrowing a live tunnel by waiting for the rekey timer is not an option the protocol offers.

A REMOTE peer whose selectors narrow while a tunnel is up proposes exactly that at its next rekey. Ze refuses such a rekey with TS_UNACCEPTABLE instead of replacing the SA with a narrower one, and the log line names both selector sets. A repeating refusal means the other end narrowed its policy and left its tunnel up. Narrow the selectors on the Ze end too, or clear the peer.

The same floor applies to a rekey Ze STARTS. Ze installs the selectors the peer answered with, never the retired SA's, so both ends program the same traffic. An answer narrower than the scope in use is refused: Ze keeps the SA that is carrying traffic, sends no error notification (RFC 7296 Section 2.21.3), and logs both selector sets. It retries at each lifetime tick, so a repeating narrows the scope in use line means the peer's policy shrank and the tunnel will hard-expire unless one end is corrected.

Port value Meaning RFC 7296 Section 3.13.1 encoding
any (the default) every port start 0, end 65535
1..65535 one port start N, end N

A port other than any needs protocol to name a protocol that defines ports, such as 6 for TCP or 17 for UDP. Section 3.13.1 requires the port fields to be 0 and 65535 whenever the protocol is 0, so the combination is refused at commit rather than widened silently. Opaque ports are refused for the same reason: no dataplane backend can express an exact match on port 0, so accepting one would install an any-port policy.

Transport mode

mode transport asks the peer for transport mode with the USE_TRANSPORT_MODE notification of RFC 7296 Section 1.3.1. Tunnel mode is the default, and it is the RFC's own default.

vpn { ipsec { site-to-site { peer host-1 {
    mode transport
    transport-required true
    traffic-selector 1 {
        local  { prefix 10.0.0.3/32; }
        remote { prefix 10.0.0.4/32; }
    }
} } } }

Transport mode constrains the selectors. RFC 7296 Section 2.23.1 requires exactly one IP address in TSi and in TSr, so every prefix must be a single host, and a vti binding is refused because an XFRM interface carries tunnel encapsulation. Several selectors are still allowed when they share that one address, for example to negotiate several ports.

A peer that declines the request establishes the Child SA in tunnel mode. Set transport-required true when that downgrade is unacceptable: Ze then deletes the SA instead, which is what Section 1.3.1 asks of an initiator. It defaults to false, so a peer without transport mode keeps a working tunnel.

Transport mode behind a NAT

Transport mode works across an address-translating NAT, on both roles, and it needs no extra configuration. Write the addresses each end's own stack holds in local-address, local-id and the local half of every traffic-selector, and write the address you DIAL in remote-address, remote-id and the remote half. A respond peer writes the address it SEES the far end at, which is the translated one, because Ze accepts an unsolicited IKE_SA_INIT only from the configured remote-address.

Ze then substitutes the selector addresses for you. RFC 7296 Section 2.23.1 requires it: the peer answers in the addresses ITS stack sees, and those exist on neither node. Ze replaces them with the addresses it observed, so the policy it programs matches the packets its own kernel handles. This is the one place where the selectors on the wire and the selectors in show vpn ipsec sa differ, and the command reports what was programmed.

show vpn ipsec sa names which side is translated. nat-detected says a NAT is on the path, behind-nat says this node's own address was translated, and peer-behind-nat says the far end's was. Both can be true at once. A transport tunnel that does not come up behind a NAT is diagnosed with those three fields first: all false means the peer sent no NAT_DETECTION notification, which no conforming IKEv2 implementation omits.

The command shows both selector pairs, so you can see the substitution rather than infer it. original-tsi and original-tsr are the addresses the peer put on the wire, and ts-local and ts-remote inside child-sa are the addresses the kernel programs. On a tunnel-mode peer, and on any peer that never negotiated transport mode, the two original fields are null.

Tunnel mode across the same NAT is unaffected. Section 2.23.1 governs transport mode alone, so a tunnel-mode Child SA negotiates the selectors you configured, untouched.

The VPP dataplane backend does not implement transport mode. It refuses a transport-mode install with a clear error rather than programming a tunnel-mode entry and reporting success. The SA path and the policy path each refuse it on their own.

The VPP dataplane backend cannot be driven by IKE, and no configuration selects it. It installs security associations, and it refuses every security policy the IKE engine produces for them. VPP has no node-wide policy database: a policy lives in a security policy database that acts only on the interfaces it is bound to, and nothing tells the backend which interface to use. The backend is reached only through a private test override, it is compiled in only with the ze_vpp build tag, and no test has sent ESP through it. Use the XFRM backend, which is the default and the production path.

PKI certificate store

The pki { } block stores X.509 certificates, private keys, CA certificates, and the certificate revocation lists each CA published. Certificates are loaded from PEM files and validated at commit time. The PKI store also serves TLS certificates for the web UI and gRPC API.

Health monitoring reports certificate expiry as a warning at 30 days and an error after expiry. Prometheus exposes ze_pki_certificate_expiry_seconds and ze_pki_certificate_valid.

XFRM interfaces

XFRM interfaces provide route-based IPsec. Traffic routed through the XFRM interface is encrypted, and incoming traffic is decrypted before it appears on the interface. The generated configuration reference documents the interface surface.

Reading the kernel dataplane

Every other IPsec command reports what the IKE engine believes it installed. The show vpn ipsec dataplane commands read the kernel instead. Three failures become visible: a state the kernel refused, a state it expired, and a policy that outlived its owner.

Command What it reads
show vpn ipsec dataplane sa [spi <spi>] The Security Association Database: SPI, addresses, if-id, mode, algorithms, replay window, byte and packet counters, and timestamps. spi <spi> selects every matching SPI, which can occur under different destinations or interfaces. SPI 0 is refused under RFC 4303 Section 2.1
show vpn ipsec dataplane policy The Security Policy Database: selector prefixes and ports, direction, priority, upper-layer protocol, if-id, tunnel endpoints, and the peer that installed each policy. A policy Ze did not install reports its owner as unknown
show vpn ipsec dataplane drift Each expected Child SA identity absent from the kernel. The command exits non-zero when it finds drift, so a script can test it

RFC 4301 Section 4.4 keeps the SPD and the SAD separate, and so does this command tree.

A read that cannot happen is an error, never an empty table. No backend loaded, a backend that cannot enumerate, and a process without CAP_NET_ADMIN each get their own message. An empty table answers "is my tunnel programmed?" with "no", and that is the wrong answer when the truth is that nobody asked the kernel.

Drift compares expected SA identities: SPI, destination, protocol, and XFRM interface ID. Extra kernel SAs are permitted during rekey. RFC 7296 Section 2.8 permits the old and new Child SAs to coexist until cleanup.

show vpn ipsec sa and show vpn ipsec peer name <name> carry the kernel counters in each child SA object: bytes-in, packets-in, bytes-out, and packets-out. They come from the kernel because the IKE engine never sees ESP payload. A failed or changing observation sets counters-known: false and all counters to null. A stable SAD read sets counters-known: true; a missing SA then has null counters, while an SA that carried no traffic has numeric zero counters.

The health registry uses the same observation. A missing expected Child SA reports degraded and names the peer. An unreadable or changing observation also reports degraded, with an unknown-dataplane diagnosis.

IPsec is an enrolled kernel capability. When the configuration installs a Security Association and the kernel holds no XFRM dataplane, ze doctor reports doctor-ipsec-xfrm-unavailable as an ERROR, ze refuses to start with the same message and exit 1, and ze config validate fails. There is no override: a daemon that negotiates a tunnel and encrypts nothing is the hazard this removes.

The probe opens the XFRM netlink socket, which needs no CAP_NET_ADMIN, and it reports absence only for the errno that means the kernel carries no XFRM protocol. Every other failure, a permission denial included, is doctor-ipsec-xfrm-unknown at warning severity and ze still starts: no kernel rebuild fixes a privilege fault.

An empty vpn { ipsec { } } block installs no Security Association, so it is not IPsec in use. It gates nothing, warns about no kernel module, and binds no IKE listener.

doctor-ipsec-udp-encap is an error, and it covers a NAT-T socket that would not bind as well as one the kernel will not decapsulate through. Both leave a tunnel that establishes and carries no traffic.

CLI

Command Description
show vpn ipsec status Tunnel and peer status summary
show vpn ipsec sa Active IKE and Child SAs, including algorithms, kernel byte and packet counters, rekey timers, SPIs, NAT detection, and initiator role
show vpn ipsec peer name <name> Detail for one configured peer
show vpn ipsec dataplane sa [spi <spi>] The SAs the kernel holds. See Reading the kernel dataplane
show vpn ipsec dataplane policy The policies the kernel holds
show vpn ipsec dataplane drift Child SAs the engine expects that the kernel does not hold. Exits non-zero on drift
clear vpn ipsec sa [peer <name>] Tear down and re-establish SAs, optionally for one peer
monitor vpn ipsec Stream sa-up, sa-down, child-up, child-down, and child-rekey lifecycle events
show pki certificates List loaded certificates with expiry information

show vpn ipsec sa also reports peer-window-size, the number of outstanding requests the peer promised in its SET_WINDOW_SIZE notification. Zero means the peer sent none, which RFC 7296 Section 2.3 reads as a window of one. Ze holds one request outstanding and accepts exactly one request id.

clear vpn ipsec sa sends a best-effort encrypted IKE Delete before removing local state. Initiator peers then re-establish immediately. If the UDP Delete is lost, the normal DPD path still removes the stale remote SA.

Health and metrics

The IPsec component registers with the health registry. It reports healthy when all configured tunnels are established, degraded when some are down, and down when critical tunnels fail.

Prometheus exposes ze_ipsec_sa_count, ze_ipsec_tunnel_up{peer}, ze_ipsec_tunnel_degraded{peer}, and ze_ipsec_rekey_total{peer}.

ze_ipsec_error_notify_sent_total{type,protected} counts error notifications Ze sent, by notify type and by whether the carrying message was encrypted. ze_ipsec_error_notify_suppressed_total{reason} counts the ones a guard stopped, by the name of that guard.

Three more counters report the COOKIE challenge described under Denial-of-service protection. ze_ipsec_cookie_challenges_total{peer} counts the challenges Ze issues, ze_ipsec_cookie_verify_failures_total{peer} counts inbound cookies that did not verify, and ze_ipsec_sa_init_retries_total{peer,cause} counts the IKE_SA_INIT retries Ze sends, labeled cookie or invalid-ke-payload. A rising verify-failure count is either an attacker probing the half-open slot or a secret rotation catching an in-flight challenge. A rising retry count on the cookie cause is the signature of the forged-notify flood RFC 7296 Section 2.6 describes.

Two more gauges report kernel state. ze_ipsec_dataplane_sa_count{if_id} counts SAs under each interface ID used by an installed Child SA. ze_ipsec_dataplane_drift{peer} reads 1 when the kernel lacks an expected SA identity. Kernel expiry or an external flush can produce drift while ze_ipsec_tunnel_up still reports a successful install.

A failed or changing dataplane observation removes both gauge families' previous series. Absent series mean the observation is unknown, not that the kernel holds no SAs or that no drift exists. Peer removal also removes the peer's drift series and unused interface-ID series.

ze_ipsec_tunnel_up reads 1 only when the IKE SA is established and the Child SA is installed in the dataplane. A tunnel whose ESP install the kernel refused reads ze_ipsec_tunnel_up 0 and ze_ipsec_tunnel_degraded 1. Such a tunnel has a live control plane and carries no encrypted traffic. Alert on the degraded gauge, because the two gauges together separate a lost session from a session with no ESP.

The daemon logs child-sa: dataplane refused the ESP state, tunnel is degraded and carries no encrypted traffic when this happens.

Interop testing

The IKE implementation includes interop tests against strongSwan, from the Alpine 3.21 test image. The infrastructure in test/interop-ipsec/ drives strongSwan containers as remote IKE peers. Twenty-four scenarios run today:

Area Scenarios
Authentication PSK, EAP-MSCHAPv2, EAP-TLS, and EAP-TLS over TLS 1.3
Ze as responder PSK, EAP-MSCHAPv2, EAP-TLS over TLS 1.3, IKE SA rekey started by strongSwan, and a fresh IKE_SA_INIT accepted beside an established SA
Rekey and teardown Child SA rekey with make-before-break, clear vpn ipsec sa and re-establishment, and a Delete sent while the one request window is held
Negotiation The INVALID_KE_PAYLOAD retry and the COOKIE challenge
Dataplane A live Child SA whose peer changes ESP form mid-session, and BGP routes exchanged with FRR over the tunnel
NAT traversal Transport mode and tunnel mode with ESP in UDP 4500, each measuring what Ze's stack does with the inner TCP and UDP checksum after decapsulation (RFC 3948 Section 3.1.2)
Traffic selectors Child SA rekey narrowing, an answer that narrows the initiator's proposal, ESN offered both ways, and a peer reload that narrows

There is no certificate-only (mode x509) scenario. The certificate paths are proven by unit tests and by the EAP-TLS scenarios, which authenticate both ends with certificates.

Ze holds every gated MUST-level requirement extracted from RFC 7296 in rfc/short/rfc7296.md. That is 222 of the summary's 227 rows, each proven in both directions by RFC requirement: tagged tests. The remaining five rows are SHOULD-level and ungated.

See also