IPsec

FlexVPN Site-to-Site Configuration on IOS XE

FlexVPN hub: real show crypto ikev2 sa with both spokes READY and the IKEv2-assigned pool addresses
In: IPsec, VPN, FlexVPN, CCIE, Labs

FlexVPN is what happens when Cisco stops bolting features onto crypto maps and rebuilds the whole thing on top of IKEv2. There is no crypto ACL, no static tunnel per peer, and no routing protocol running over the overlay just to tell the hub where a spoke's LAN lives. You define one authorization policy, one Virtual-Template, and the hub clones a fresh Virtual-Access interface for every spoke that authenticates, hands it an IP address, and installs its routes. If you want the conceptual background first, start with the IPsec VPN pillar and FlexVPN explained. This article is the other half: the build.

Everything below came out of a live CML lab. Three cat8000v routers on IOS XE 17.18.02 (HUB, SPOKE1, SPOKE2) with an IOL-XE router acting as the ISP between them. Every command and every piece of output on this page is real, including the failure we walked into and the log lines it produced.

The Lab

HUB
Internet:203.0.113.1
LAN:10.20.10.0/24
Also:192.168.99.0/24 (real Debian VM)
SPOKE1
Internet:198.51.100.2
LAN:10.30.10.0/24
Identity:spoke1.pinglabz.lab
SPOKE2
Internet:198.51.100.6
LAN:10.30.20.0/24
Identity:spoke2.pinglabz.lab

The spokes have a default route to the ISP and nothing else. They do not know the hub's LAN exists. By the end of this build they will, and no routing protocol will have been involved.

Block 1: AAA and the Authorization Policy

This is the block people skim, and it is the single most important block in the entire configuration. FlexVPN's headline feature is that the hub pushes an IP address and a set of routes down to the spoke as part of the IKEv2 exchange. The mechanism it uses to do that is AAA network authorization. So the first thing you turn on is AAA.

aaa new-model
aaa authorization network PLZ-FLEX-LIST local

aaa authorization network creates a method list called PLZ-FLEX-LIST that resolves against the local database (in production this would point at RADIUS instead). When the IKEv2 profile later says "go ask AAA what this peer is allowed to have", PLZ-FLEX-LIST is what it asks.

What AAA hands back is the authorization policy:

crypto ikev2 authorization policy PLZ-AUTHOR-POL
 pool PLZ-FLEX-POOL
 route set interface
 route set access-list PLZ-FLEX-ROUTES

Three lines, three jobs:

  • pool PLZ-FLEX-POOL tells the hub to allocate the connecting spoke an address out of a local pool. That address becomes the spoke's tunnel IP.
  • route set interface advertises the interface the Virtual-Access is unnumbered to (on our hub, Loopback0, 10.255.0.1). That is how the spoke learns the hub's tunnel endpoint address.
  • route set access-list PLZ-FLEX-ROUTES advertises whatever the ACL permits. This is the hub telling the spoke "here are the networks I can reach for you."

And the ACL itself:

ip access-list standard PLZ-FLEX-ROUTES
 permit 10.20.10.0 0.0.0.255
 permit 192.168.99.0 0.0.0.255
 permit 10.30.0.0 0.0.255.255

The first two lines are the hub's own networks (the LAN and the segment where our real Debian VM lives). The third, 10.30.0.0/16, is the summary that covers both spoke LANs. That is what makes spoke-to-spoke traffic possible at all: SPOKE1 installs a route for 10.30.0.0/16 pointing at the tunnel, so when it wants to reach SPOKE2's 10.30.20.0/24 it sends the packet to the hub.

This block is the heart of FlexVPN. Get it right and the rest is plumbing. Skip it and you have built an ordinary SVTI with extra steps.

Block 2: The Address Pool

ip local pool PLZ-FLEX-POOL 10.0.9.10 10.0.9.100

Ninety one addresses, one per spoke. Every spoke that authenticates gets one, and it gets it back into the pool when the session tears down. This is a plain ip local pool, the exact same construct used by remote-access VPN and PPP, which is a nice reminder that FlexVPN treats a site-to-site spoke as just another client.

Block 3: The IKEv2 Keyring (and an honest note about wildcard PSKs)

crypto ikev2 keyring PLZ-FLEX-KEYRING
 peer SPOKES
  address 0.0.0.0 0.0.0.0
  pre-shared-key PingLabz-IKEv2-PSK-01

One keyring entry, matching 0.0.0.0 0.0.0.0, which is every address on the internet. This is deliberate. The entire point of a FlexVPN hub is that you do not have to touch it when you add spoke number 47. If the keyring had to name each peer by IP, you would be back to editing the hub for every new site, and the dynamic Virtual-Template would be pointless.

Be honest with yourself about what that costs. A wildcard pre-shared key means every spoke shares one secret, and any device that learns that secret and can present an acceptable identity can bring up a tunnel to your hub. It is fine for a lab and it is fine for a proof of concept. In production, the answer is certificates: each spoke gets its own identity certificate from your CA, the hub validates it, and revoking one spoke does not mean rekeying the fleet. We use a wildcard PSK here because it keeps the config readable and the failure modes visible, not because it is what you should ship.

Block 4: The IKEv2 Profile

crypto ikev2 profile PLZ-FLEX-PROF
 match identity remote fqdn domain pinglabz.lab
 identity local fqdn hub.pinglabz.lab
 authentication local pre-share
 authentication remote pre-share
 keyring local PLZ-FLEX-KEYRING
 aaa authorization group psk list PLZ-FLEX-LIST PLZ-AUTHOR-POL
 virtual-template 1

The profile is where the hub decides who it will talk to and what happens when it does. Four things are worth stopping on.

Identity matching by FQDN domain. match identity remote fqdn domain pinglabz.lab says: accept any peer whose IKE identity ends in pinglabz.lab. Not an IP address, a domain. That is the second half of what makes the hub spoke-agnostic. SPOKE1 announces itself as spoke1.pinglabz.lab, SPOKE2 as spoke2.pinglabz.lab, and a spoke you build next month as spoke47.pinglabz.lab, and the hub matches all of them against one line. (If you are hazy on how IKEv2 identities and the SA exchange actually work, IKEv2 explained covers it.)

The AAA line. aaa authorization group psk list PLZ-FLEX-LIST PLZ-AUTHOR-POL is the wire that connects Block 1 to the tunnel. It says: for PSK-authenticated peers, run group authorization against method list PLZ-FLEX-LIST, using PLZ-AUTHOR-POL as the username to look up. Without this line, IKEv2 will still authenticate, the tunnel will still come up, and absolutely nothing will be pushed to the spoke.

virtual-template 1. This tells IKEv2 which template to clone when a peer matches this profile. This is the dynamic part.

Block 5: The IPsec Profile

crypto ipsec profile PLZ-FLEX-IPSEC
 set transform-set PLZ-TS-GCM
 set ikev2-profile PLZ-FLEX-PROF

Short and mechanical. The IPsec profile binds the data-plane transform (we are using esp-gcm 256) to the IKEv2 profile that governs the control plane. The Virtual-Template applies this profile, and every Virtual-Access cloned from that template inherits it.

Block 6: The Virtual-Template (read this part twice)

interface Virtual-Template1 type tunnel
 ip unnumbered Loopback0
 ip nhrp network-id 1
 ip nhrp redirect
 tunnel source GigabitEthernet2
 tunnel mode ipsec ipv4
 tunnel protection ipsec profile PLZ-FLEX-IPSEC

The Virtual-Template is a blueprint, not an interface that carries traffic. When SPOKE1 authenticates, IOS XE stamps out Virtual-Access1 from it. When SPOKE2 authenticates, it stamps out Virtual-Access2. Same template, two live interfaces, each with its own tunnel destination filled in from the IKEv2 negotiation.

ip unnumbered Loopback0 means every Virtual-Access borrows the hub's loopback address (10.255.0.1), which is why later on you will see both of them showing the same IP. ip nhrp network-id and ip nhrp redirect are the groundwork for spoke-to-spoke shortcut switching, which is a topic of its own.

The gotcha that will eat your afternoon

The Virtual-Template must carry tunnel source and tunnel mode ipsec ipv4. If it does not, here is what happens: IKEv2 negotiates successfully, the peer authenticates, the Virtual-Access is cloned, it comes up, and then it immediately dies. Then it does it again. Forever. This is exactly what our lab did:

%LINK-3-UPDOWN: Interface Virtual-Access1, changed state to down
%DMVPN-5-NHRP_NETID_UNCONFIGURED:  Virtual-Access1: NETID : 1 Unconfigured (Tunnel: 0.0.0.0 NBMA:  UNKNOWN)

Look closely at that second line. Tunnel: 0.0.0.0 and NBMA: UNKNOWN. The cloned interface has no idea what its own source address is or what encapsulation it should be using, so NHRP cannot register a network ID against it and the interface is torn back down.

What makes this genuinely nasty is that the crypto is fine. IKEv2 authentication succeeds. You can watch the authorization request go out in the debug:

IKEv2:Using mlist PLZ-FLEX-LIST and username PLZ-AUTHOR-POL for group author request

So every instinct you have says crypto bug. You go and stare at your keyring, your proposal, your transform set, your identities. All of them are correct. The problem is two missing lines on an interface that is not even in your running config in the form you are looking at, because the thing that is flapping is a clone.

Add tunnel source GigabitEthernet2 and tunnel mode ipsec ipv4 to the Virtual-Template. The flapping stops instantly. Also note the message is tagged %DMVPN, not %FLEXVPN, because FlexVPN reuses the NHRP machinery that DMVPN is built on. Same plumbing, different front end.

Block 7: The Spoke

crypto ikev2 authorization policy PLZ-AUTHOR-POL
 route set interface
 route set access-list PLZ-FLEX-ROUTES

crypto ikev2 profile PLZ-FLEX-PROF
 match identity remote fqdn domain pinglabz.lab
 identity local fqdn spoke1.pinglabz.lab
 authentication local pre-share
 authentication remote pre-share
 keyring local PLZ-FLEX-KEYRING
 aaa authorization group psk list PLZ-FLEX-LIST PLZ-AUTHOR-POL
 virtual-template 1

interface Tunnel0
 ip address negotiated
 ip nhrp network-id 1
 ip nhrp shortcut virtual-template 1
 tunnel source GigabitEthernet2
 tunnel mode ipsec ipv4
 tunnel destination 203.0.113.1
 tunnel protection ipsec profile PLZ-FLEX-IPSEC

The spoke is a mirror image with three differences that matter.

ip address negotiated. The spoke does not configure a tunnel IP. It asks for one, and the hub's PLZ-FLEX-POOL gives it one. This is the client relationship made concrete.

A static tunnel destination. The hub is dynamic and accepts anyone; the spoke knows exactly where it is going, so it points a normal Tunnel interface at 203.0.113.1. No Virtual-Template needed for the hub-facing tunnel (the spoke's virtual-template 1 reference exists for the spoke-to-spoke case).

Its own identity and its own route-set ACL. identity local fqdn spoke1.pinglabz.lab must land inside the hub's fqdn domain pinglabz.lab match, and SPOKE1's PLZ-FLEX-ROUTES ACL permits 10.30.10.0 0.0.0.255, which is how the hub learns SPOKE1's LAN. SPOKE2 is identical apart from spoke2.pinglabz.lab and 10.30.20.0 0.0.0.255. Route advertisement runs both directions over the same IKEv2 exchange.

Verification: Does It Actually Work?

Two spokes, both READY, both AES-GCM-256 with DH group 19 (note Hash: None, which is correct for an AEAD cipher because GCM handles integrity internally):

HUB#show crypto ikev2 sa
Tunnel-id Local                 Remote                fvrf/ivrf            Status
1         203.0.113.1/500       198.51.100.2/500      none/none            READY
      Encr: AES-GCM, keysize: 256, PRF: SHA256, Hash: None, DH Grp:19, Auth sign: PSK, Auth verify: PSK
Tunnel-id Local                 Remote                fvrf/ivrf            Status
2         203.0.113.1/500       198.51.100.6/500      none/none            READY
      Encr: AES-GCM, keysize: 256, PRF: SHA256, Hash: None, DH Grp:19, Auth sign: PSK, Auth verify: PSK

Two Virtual-Access interfaces exist that you never typed into the config, both up, both wearing the hub's Loopback0 address:

HUB#show ip interface brief | include Virtual-Access
Virtual-Access1        10.255.0.1      YES unset  up                    up
Virtual-Access2        10.255.0.1      YES unset  up                    up

HUB#show crypto session
Interface: Virtual-Access1
Session status: UP-ACTIVE
Peer: 198.51.100.2 port 500
Interface: Virtual-Access2
Session status: UP-ACTIVE
Peer: 198.51.100.6 port 500

Now the payoff. Look at the hub's routing table:

HUB#show ip route | include Virtual-Access
S        10.0.9.24/32 is directly connected, Virtual-Access1
S        10.0.9.25/32 is directly connected, Virtual-Access2
S        10.30.10.0/24 is directly connected, Virtual-Access1
S        10.30.20.0/24 is directly connected, Virtual-Access2

SPOKE1's LAN and SPOKE2's LAN, each pointing at the correct Virtual-Access, plus the two /32s for the pool addresses the spokes were handed. There is no OSPF here. No EIGRP, no BGP, no static route you typed. IKEv2 installed all four entries out of the authorization policy.

The pool confirms the allocation:

HUB#show ip local pool PLZ-FLEX-POOL
 Pool                     Begin           End             Free  In use
 PLZ-FLEX-POOL            10.0.9.10       10.0.9.100        89       2
Inuse addresses:
   10.0.9.24          IKEv2 Addr IDB
   10.0.9.25          IKEv2 Addr IDB

IKEv2 Addr IDB is the pool telling you who took the address and why. Two spokes, two leases, 89 free.

And the same trick ran in reverse. On the spoke:

SPOKE1#show ip route static
S*    0.0.0.0/0 [1/0] via 198.51.100.1
S        10.20.10.0/24 is directly connected, Tunnel0
S        10.30.0.0/16 is directly connected, Tunnel0
S        10.255.0.1/32 is directly connected, Tunnel0
S     192.168.99.0/24 is directly connected, Tunnel0

SPOKE1 started with one static default route to its ISP. It now also has the hub's LAN (10.20.10.0/24), the hub's real-VM segment (192.168.99.0/24), the hub's tunnel endpoint (10.255.0.1/32 from route set interface), and the 10.30.0.0/16 summary that lets it reach the other spoke. All four arrived over IKEv2.

End to End: Spoke to Spoke

The proof that the route injection is not just cosmetic: SPOKE1 pinging SPOKE2's LAN, sourced from its own LAN interface.

SPOKE1#traceroute 10.30.20.1 source Loopback10 probe 1 timeout 1
Type escape sequence to abort.
Tracing the route to 10.30.20.1
  1 10.255.0.1 34 msec
  2 10.0.9.25 81 msec

SPOKE1#ping 10.30.20.1 source Loopback10 repeat 20
!!!!!!!!!!!!!!!!!!!!
Success rate is 100 percent (20/20), round-trip min/avg/max = 4/7/15 ms

Twenty for twenty. Two hops: the hub's tunnel address (10.255.0.1), then SPOKE2's pool address (10.0.9.25). That is spoke-to-spoke traffic hairpinning through the hub, decrypted on arrival and re-encrypted on the way back out. It works, and for a lot of designs that is entirely acceptable. Turning those two hops into one is the job of NHRP shortcut switching, which gets its own article.

Troubleshooting: What Actually Broke

Virtual-Access flapping up and down
Symptom:%DMVPN-5-NHRP_NETID_UNCONFIGURED with Tunnel: 0.0.0.0 NBMA: UNKNOWN, in a loop. IKEv2 auth succeeds.
Fix:Add tunnel source and tunnel mode ipsec ipv4 to the Virtual-Template.
Peer never matches the profile
Symptom:No IKEv2 SA at all. The hub silently ignores the spoke.
Fix:The spoke's identity local fqdn must end in the domain the hub matches. spoke1.pinglabz.lab matches fqdn domain pinglabz.lab. A typo in either half is a silent no-match.
Tunnel is up but no routes arrive
Symptom:IKEv2 SA READY, Virtual-Access up, routing table unchanged.
Fix:Authorization is not being attempted. Confirm with the debug line IKEv2:Using mlist PLZ-FLEX-LIST and username PLZ-AUTHOR-POL for group author request. If it is missing, check aaa new-model, the aaa authorization network list, and the aaa authorization group psk list line in the IKEv2 profile.

One more habit worth building: when a FlexVPN Virtual-Access misbehaves, do not read the Virtual-Template to see what the interface looks like. Read the clone itself with show derived-config interface Virtual-Access1. That shows you the merged result of the template plus everything IKEv2 filled in, which is the config the router is actually running.

Key Takeaways

  • The IKEv2 authorization policy is FlexVPN. aaa new-model, aaa authorization network, and crypto ikev2 authorization policy with pool, route set interface, and route set access-list are what push addresses and routes to the spokes. Everything else is supporting cast.
  • Routes arrive over IKEv2, not over a routing protocol. Our hub learned 10.30.10.0/24 and 10.30.20.0/24, and the spokes learned 10.20.10.0/24 and 192.168.99.0/24, with zero OSPF, EIGRP, or BGP anywhere in the config.
  • A Virtual-Template without tunnel source and tunnel mode ipsec ipv4 will flap forever and lie to you about it, because IKEv2 authentication succeeds while the cloned Virtual-Access dies in a loop. The NBMA: UNKNOWN in the NHRP log is the tell.
  • A wildcard PSK (address 0.0.0.0 0.0.0.0) is what makes the hub spoke-agnostic, and it is a real security tradeoff. Certificates are the production answer.
  • The hub scales without edits. One Virtual-Template, one FQDN domain match, one pool. Spoke number 47 needs zero changes on the hub.
  • Spoke-to-spoke works, at two hops. 20/20 pings through the hub. Collapsing that to one hop is NHRP's job, not IKEv2's.

Next in this cluster: FlexVPN spoke-to-spoke and NHRP shortcut switching, where the two-hop path is supposed to become one. If you want the conceptual model behind everything above, read FlexVPN explained and IKEv2 explained, and for the wider picture of where FlexVPN sits next to crypto maps, SVTIs, and DMVPN, the IPsec VPN pillar is the map.

Written by
More from Ping Labz
Great! You’ve successfully signed up.
Welcome back! You've successfully signed in.
You've successfully subscribed to Ping Labz.
Your link has expired.
Success! Check your email for magic link to sign-in.
Success! Your billing info has been updated.
Your billing was not updated.