Build a Secure IKEv2 VPN Server on Linux

Build a private IKEv2 VPN server you can use to protect your traffic from inspection by a local provider.

Build a Secure IKEv2 VPN Server on Linux
Photo by Kyle Glenn / Unsplash

Introduction

This guide walks you through building a production-ready IKEv2 VPN server using strongSwan on Ubuntu/Debian. IKEv2 (Internet Key Exchange version 2) is a modern VPN protocol that offers excellent security, native support on most devices, and automatic reconnection when switching networks—perfect for mobile devices.

By the end of this guide, you'll have a VPN server with:

  • Modern cryptographic algorithms (AES-256-GCM, ECC P-384)
  • Let's Encrypt SSL certificates with automatic renewal
  • Dual-stack IPv4 and IPv6 support
  • Cloudflare DNS for privacy & malware prevention
  • MSCHAPv2 authentication (compatible with all major platforms)

Prerequisites:

  • A Linux server (Ubuntu 24.04 or Debian 11+ recommended) with root access
  • A registered domain name pointing to your server
  • Basic understanding of Linux command line and networking

Architecture Overview

Before diving in, let's understand what we're building:

  1. strongSwan acts as our IKEv2 server
  2. Let's Encrypt provides trusted SSL certificates
  3. UFW (Uncomplicated Firewall) handles firewall rules and NAT
  4. AppArmor is configured to allow strongSwan to read certificates
  5. Clients authenticate using username/password (EAP-MSCHAPv2)

Step 1: Install Required Packages

apt install -y certbot strongswan libstrongswan-standard-plugins \
    strongswan-libcharon libcharon-extra-plugins \
    libcharon-extauth-plugins

What this does:

  • certbot: Let's Encrypt client for obtaining SSL certificates
  • strongswan: The core IKEv2/IPsec VPN server
  • libstrongswan-standard-plugins: Essential cryptographic algorithms
  • libcharon-extra-plugins: Additional features including EAP authentication
  • libcharon-extauth-plugins: External authentication methods like MSCHAPv2

Step 2: Configure Certbot

Create Let's Encrypt configuration file:

nano /etc/letsencrypt/cli.ini

Add the following configuration:

# Because we are using logrotate for greater flexibility, disable the
# internal certbot logrotation.
max-log-backups = 0

# Adjust interactive output regarding automated renewal
preconfigured-renewal = True
standalone = true
agree-tos = true
non-interactive = true
preferred-challenges = http

# Use ECDSA keys (smaller, faster than RSA)
key-type = ecdsa
elliptic-curve = secp384r1

# Your email for renewal notifications
email = admin@example.com

# Automatically open/close port 80 for certificate challenges
pre-hook = /sbin/ufw allow from any to any port 80 proto tcp
post-hook = /sbin/ufw delete allow from any to any port 80 proto tcp

# Reload strongSwan when certificates renew
renew-hook = /usr/sbin/ipsec reload && /usr/sbin/ipsec secrets

What this does:

  • Configures certbot to run non-interactively (important for automatic renewals)
  • Uses ECDSA with P-384 curve for certificates (better performance than RSA-4096)
  • The pre-hook temporarily opens port 80 for HTTP challenges
  • The post-hook closes port 80 immediately after verification
  • The renew-hook reloads strongSwan when certificates are renewed (every 60-90 days)

Step 3: Obtain SSL Certificate

certbot certonly --key-type ecdsa -d vpn.example.com

Replace vpn.example.com with your actual domain. This obtains a certificate using the HTTP-01 challenge method.

Why ECDSA? ECDSA keys with P-384 provide equivalent security to RSA-7680 but are much smaller and faster. Perfect for mobile VPN clients.


strongSwan expects certificates in specific directories:

ln -f -s "/etc/letsencrypt/live/vpn.example.com/cert.pem" \
    /etc/ipsec.d/certs/cert.pem
ln -f -s "/etc/letsencrypt/live/vpn.example.com/privkey.pem" \
    /etc/ipsec.d/private/privkey.pem
ln -f -s "/etc/letsencrypt/live/vpn.example.com/fullchain.pem" \
    /etc/ipsec.d/cacerts/chain.pem

What this does:

  • Creates symbolic links from Let's Encrypt certificates to strongSwan directories
  • cert.pem: Your server's certificate
  • privkey.pem: Your server's private key
  • fullchain.pem: Your certificate + intermediate CA certificates (needed for trust)

Using symlinks means certificate renewals automatically take effect without manual copying.


Step 5: Configure AppArmor

AppArmor is a Linux security module that restricts what programs can access. We need to allow strongSwan to read Let's Encrypt certificates:

nano /etc/apparmor.d/local/usr.lib.ipsec.charon

Add this line:

/etc/letsencrypt/archive/vpn.example.com/* r,

Then reload AppArmor:

aa-status --enabled && invoke-rc.d apparmor reload

What this does:

  • Grants strongSwan read access to Let's Encrypt certificate archives
  • Without this, strongSwan would be denied access by AppArmor's mandatory access control

Step 6: Enable IP Forwarding

For a VPN to work, your server needs to forward packets between the VPN tunnel and the internet:

nano /etc/sysctl.conf

Add or uncomment these lines:

net.ipv4.ip_forward = 1
net.ipv4.ip_no_pmtu_disc = 1
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv6.conf.all.forwarding = 1

Apply the changes:

sysctl -p

What each setting does:

  • net.ipv4.ip_forward = 1: Enables IPv4 packet forwarding (essential for VPN)
  • net.ipv4.ip_no_pmtu_disc = 1: Disables Path MTU Discovery (helps prevent fragmentation issues)
  • net.ipv4.conf.all.rp_filter = 1: Enables reverse path filtering (prevents IP spoofing)
  • net.ipv4.conf.all.accept_redirects = 0: Ignores ICMP redirects (security)
  • net.ipv4.conf.all.send_redirects = 0: Doesn't send ICMP redirects (security)
  • net.ipv6.conf.all.forwarding = 1: Enables IPv6 packet forwarding

Step 7: Configure UFW Firewall

Allow Packet Forwarding

nano /etc/default/ufw

Change the forward policy:

- DEFAULT_FORWARD_POLICY="DROP"
+ DEFAULT_FORWARD_POLICY="ACCEPT"

Configure NAT for IPv4

nano /etc/ufw/before.rules

Add these lines at the top of the file (before the *filter section):

# NAT for IPSec
*nat
:POSTROUTING ACCEPT [0:0]

-A POSTROUTING -s 10.1.60.0/24 -o eth0 -j MASQUERADE

COMMIT

Important: Replace eth0 with your actual network interface name. Find it with ip link show.

Configure NAT for IPv6

nano /etc/ufw/before6.rules

Add these lines at the top of the file:

# NAT for IPSec
*nat
:POSTROUTING ACCEPT [0:0]

-A POSTROUTING -s fd:10:1:60::/64 -o eth0 -j MASQUERADE

COMMIT

What NAT/MASQUERADE does:

  • Translates VPN client private IPs (10.1.60.0/24 and fd:10:1:60::/64) to your server's public IP
  • Without this, replies from the internet wouldn't know how to reach VPN clients
  • Think of it like home router NAT, but for VPN traffic

Apply Firewall Rules

# Allow IKEv2 traffic
ufw allow from any to any port 500 proto udp
ufw allow from any to any port 4500 proto udp

# Allow VPN client traffic to reach the internet
ufw allow from 10.1.60.0/24 to any
ufw allow from fd:10:1:60::/64 to any

# Limit SSH access to your home IP (optional but recommended)
ufw limit from YOUR_HOME_IP to any port 22 proto tcp
ufw limit from YOUR_IPv6_HOME_NETWORK to any port 22 proto tcp

# Enable firewall
ufw enable

What each rule does:

  • Port 500/UDP: IKE negotiation (initial handshake)
  • Port 4500/UDP: NAT-T (NAT traversal for clients behind NAT)
  • 10.1.60.0/24 and fd:10:1:60::/64: Allow VPN clients to access any destination
  • SSH limits: Using limit instead of allow rate-limits connections (prevents brute force)

Security tip: Replace YOUR_HOME_IP with your actual IP address to restrict SSH access. You can find it with curl ifconfig.me.


Step 8: Configure strongSwan

Create IPsec Configuration

rm /etc/ipsec.conf
nano /etc/ipsec.conf

Add this configuration:

config setup
  strictcrlpolicy=yes
  uniqueids=never

conn roadwarrior
  auto=add
  compress=no
  type=tunnel
  keyexchange=ikev2
  fragmentation=yes
  forceencaps=yes

  # Cryptographic algorithms (modern, secure)
  ike=aes256gcm16-prfsha384-ecp384,aes256gcm16-prfsha256-ecp256!
  esp=aes256gcm16-ecp384!

  # Dead Peer Detection
  dpdaction=clear
  dpddelay=900s
  rekey=no

  # Server configuration
  left=%any
  leftid=@vpn.example.com
  leftcert=cert.pem
  leftsendcert=always
  leftsubnet=::/0,0.0.0.0/0

  # Client configuration
  right=%any
  rightid=%any
  rightauth=eap-mschapv2
  eap_identity=%any
  rightdns=2606:4700:4700::1112,2606:4700:4700::1002,1.1.1.2,1.0.0.2
  rightsourceip=fd:10:1:60::/64,10.1.60.0/24
  rightsendcert=never

Configuration breakdown:

Global settings:

  • strictcrlpolicy=yes: Enforce certificate revocation checks (security)
  • uniqueids=never: Allow same user on multiple devices simultaneously

Connection "roadwarrior":

  • auto=add: Load connection at startup but don't initiate
  • type=tunnel: Full tunnel mode (all traffic goes through VPN)
  • keyexchange=ikev2: Use IKEv2 protocol
  • fragmentation=yes: Handle large packets gracefully
  • forceencaps=yes: Always use UDP encapsulation (helps with NAT)

Cryptography:

  • ike=: Algorithms for IKE handshake
    • aes256gcm16: AES-256 in GCM mode (authenticated encryption)
    • prfsha384: PRF using SHA-384
    • ecp384: Elliptic curve P-384 (very strong)
  • esp=: Algorithms for data encryption
    • aes256gcm16-ecp384: Strong authenticated encryption with PFS

Dead Peer Detection:

  • dpdaction=clear: Drop connection if peer dies
  • dpddelay=900s: Check every 15 minutes
  • rekey=no: Don't automatically rekey (saves battery on mobile)

Server side (left):

  • left=%any: Accept connections on any interface
  • leftid=@vpn.example.com: Server's identity
  • leftcert=cert.pem: Server's certificate
  • leftsubnet=::/0,0.0.0.0/0: Route all IPv4 and IPv6 traffic through VPN

Client side (right):

  • right=%any: Accept any client IP
  • rightauth=eap-mschapv2: Clients authenticate with username/password
  • rightdns=: Cloudflare DNS servers (privacy-focused, supports malware blocking)
  • rightsourceip=: IP pools to assign to clients (both IPv4 and IPv6)

Why these DNS servers?

  • 2606:4700:4700::1112, 2606:4700:4700::100, 1.1.1.2, and 1.0.0.2 are Cloudflare's malware-blocking DNS

Step 9: Configure User Credentials

nano /etc/ipsec.secrets

Add your credentials:

# This file holds shared secrets or RSA private keys for authentication.

# Server's private key
vpn.example.com : ECDSA "privkey.pem"

# User credentials (username : EAP "password")
user1 : EAP "StrongRandomPassword123"
user2 : EAP "AnotherSecurePassword456"
alice-iphone : EAP "7xK9mPq2NzE5rTy8"
bob-laptop : EAP "vL3nQ8wX6jH2sM9c"

Security best practices:

  • Use unique, strong passwords for each user.
  • Consider using device-specific accounts (easier to revoke if device is lost)
  • Generate passwords with: openssl rand -base64 12

Step 10: Start and Enable strongSwan

ipsec enable
ipsec restart
ipsec status

What this does:

  • ipsec enable: Configure strongSwan to start on boot
  • ipsec restart: Start the VPN server now
  • ipsec status: Verify the "roadwarrior" connection is loaded

You should see output like:

Security Associations (0 up, 0 connecting):
  roadwarrior:  10.1.60.0/24,fd:10:1:60::/64...%any  IKEv2

This means the server is ready to accept connections (no clients connected yet).