Build a Secure IKEv2 VPN Server on Linux

Build a private IKEv2 VPN server you can use to protect your traffic and appear from within another country.

Build a Secure IKEv2 VPN Server on Linux
Photo by Daniel Jerez / 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).


Step 11: Reboot and Verify

reboot now

After reboot, verify everything is working:

# Check if strongSwan is running
ipsec status

# Check if IP forwarding is enabled
sysctl net.ipv4.ip_forward net.ipv6.conf.all.forwarding

# Check firewall rules
ufw status verbose

Client Configuration

iOS/macOS

  1. Create a configuration profile using the Apple Configuration Profile template
  2. Set RemoteAddress and RemoteIdentifier to your VPN domain (vpn.example.com)
  3. Install the profile on your device
  4. Go to Settings → VPN → Add VPN Configuration
  5. Enter your username and password from /etc/ipsec.secrets

Android

  1. Settings → Network & Internet → VPN → Add VPN
  2. Type: IKEv2/IPSec MSCHAPv2
  3. Server address: vpn.example.com
  4. Username and password from /etc/ipsec.secrets

Windows

  1. Settings → Network & Internet → VPN → Add VPN
  2. VPN provider: Windows (built-in)
  3. Connection name: Your choice
  4. Server name: vpn.example.com
  5. VPN type: IKEv2
  6. Type of sign-in info: User name and password

Linux

# Install NetworkManager strongSwan plugin
apt install network-manager-strongswan

# Use NetworkManager GUI to configure
# Or edit /etc/NetworkManager/system-connections/vpn.nmconnection

Apple Configuration Profile Template

For iOS, iPadOS, and macOS devices, you can create a configuration profile that simplifies setup. Here's the structure:

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE plist PUBLIC '-//Apple//DTD PLIST 1.0//EN' 'http://www.apple.com/DTDs/PropertyList-1.0.dtd'>
<plist version='1.0'>
<dict>
  <key>PayloadContent</key>
  <array>
    <dict>
      <key>IKEv2</key>
      <dict>
        <key>AuthenticationMethod</key>
        <string>None</string>
        <key>ChildSecurityAssociationParameters</key>
        <dict>
          <key>EncryptionAlgorithm</key>
          <string>AES-256-GCM</string>
          <key>IntegrityAlgorithm</key>
          <string>SHA2-384</string>
          <key>DiffieHellmanGroup</key>
          <integer>20</integer>
          <key>LifeTimeInMinutes</key>
          <integer>1440</integer>
        </dict>
        <key>DeadPeerDetectionRate</key>
        <string>Medium</string>
        <key>DisableMOBIKE</key>
        <integer>0</integer>
        <key>DisableRedirect</key>
        <integer>0</integer>
        <key>EnableCertificateRevocationCheck</key>
        <integer>0</integer>
        <key>EnablePFS</key>
        <true/>
        <key>ExtendedAuthEnabled</key>
        <true/>
        <key>IKESecurityAssociationParameters</key>
        <dict>
          <key>EncryptionAlgorithm</key>
          <string>AES-256-GCM</string>
          <key>IntegrityAlgorithm</key>
          <string>SHA2-384</string>
          <key>DiffieHellmanGroup</key>
          <integer>20</integer>
          <key>LifeTimeInMinutes</key>
          <integer>1440</integer>
        </dict>
        <key>OnDemandEnabled</key>
        <integer>1</integer>
        <key>OnDemandRules</key>
        <array>
          <dict>
            <key>Action</key>
            <string>Connect</string>
          </dict>
        </array>
        <key>RemoteAddress</key>
        <string>vpn.example.com</string>
        <key>RemoteIdentifier</key>
        <string>vpn.example.com</string>
        <key>UseConfigurationAttributeInternalIPSubnet</key>
        <integer>0</integer>
      </dict>
      <key>IPv4</key>
      <dict>
        <key>OverridePrimary</key>
        <integer>1</integer>
      </dict>
      <key>PayloadDescription</key>
      <string>Configures VPN settings</string>
      <key>PayloadDisplayName</key>
      <string>vpn.example.com</string>
      <key>PayloadIdentifier</key>
      <string>com.apple.vpn.managed.YOUR-UUID-HERE</string>
      <key>PayloadType</key>
      <string>com.apple.vpn.managed</string>
      <key>PayloadUUID</key>
      <string>YOUR-UUID-HERE</string>
      <key>PayloadVersion</key>
      <integer>1</integer>
      <key>Proxies</key>
      <dict>
        <key>HTTPEnable</key>
        <integer>0</integer>
        <key>HTTPSEnable</key>
        <integer>0</integer>
      </dict>
      <key>UserDefinedName</key>
      <string>vpn.example.com</string>
      <key>VPNType</key>
      <string>IKEv2</string>
    </dict>
  </array>
  <key>PayloadDisplayName</key>
  <string>IKEv2 VPN configuration (vpn.example.com)</string>
  <key>PayloadIdentifier</key>
  <string>YOUR-UUID-HERE</string>
  <key>PayloadRemovalDisallowed</key>
  <false/>
  <key>PayloadType</key>
  <string>Configuration</string>
  <key>PayloadUUID</key>
  <string>YOUR-UUID-HERE</string>
  <key>PayloadVersion</key>
  <integer>1</integer>
</dict>
</plist>

To use this:

  1. Replace all instances of vpn.example.com with your actual domain
  2. Generate UUIDs with uuidgen and replace all YOUR-UUID-HERE placeholders
  3. Save as vpn.mobileconfig
  4. Email it or AirDrop it to yourself.
  5. Open on your iOS/macOS device to install

Configuration highlights:

  • OnDemandEnabled: Automatically connects when needed
  • EnablePFS: Perfect Forward Secrecy (each session has unique keys)
  • ExtendedAuthEnabled: Enables username/password authentication
  • OverridePrimary: Routes all traffic through VPN (not split-tunnel)

Troubleshooting

Check strongSwan Logs

journalctl -u strongswan-starter -f

Common Issues

"Certificate verification failed"

  • Ensure your domain correctly points to your server's IP
  • Verify certificate with: certbot certificates
  • Check AppArmor permissions

"Connection timeout"

  • Verify UDP ports 500 and 4500 are open: ufw status
  • Check if your hosting provider blocks UDP
  • Ensure your domain's DNS has propagated: dig +short vpn.example.com and dig +short vpn.example.com AAAA

"Authentication failed"

  • Double-check username/password in /etc/ipsec.secrets
  • Reload secrets: ipsec secrets
  • Verify client is using EAP-MSCHAPv2 authentication

"Connected but no internet"

  • Verify IP forwarding: sysctl net.ipv4.ip_forward
  • Check NAT rules: iptables -t nat -L -n -v
  • Ensure correct network interface in NAT rules (not eth0? Find yours with ip link)

Test connectivity from server:

# Ping a VPN client
ping 10.1.60.100

# Check active connections
ipsec statusall

Security Considerations

Strengths of This Setup

  1. Modern Cryptography: AES-256-GCM with P-384 ECC is future-proof
  2. Trusted Certificates: Let's Encrypt means no certificate warnings
  3. Perfect Forward Secrecy: Each session has unique keys
  4. No Logs: This setup doesn't log user activity (though you can enable logging if needed)

Potential Improvements

  1. Certificate Authentication: Replace passwords with client certificates (more secure but harder to manage)
  2. 2FA: Integrate with RADIUS for two-factor authentication
  3. Fail2ban: Automatically block brute-force attempts
  4. Monitoring: Set up alerts for unusual activity

Maintenance

Certificate Renewal: Automatic via certbot's systemd timer. Verify with:

systemctl status certbot.timer
certbot renew --dry-run

Adding/Removing Users: Edit /etc/ipsec.secrets and run:

ipsec secrets

Updating strongSwan:

apt update && apt upgrade strongswan
ipsec restart

Performance Tuning

For Better Mobile Performance

In /etc/ipsec.conf, adjust DPD for faster detection:

dpddelay=300s  # Check every 5 minutes instead of 15

Conclusion

You now have a production-ready IKEv2 VPN server with:

  • ✅ Military-grade encryption (AES-256-GCM)
  • ✅ Automatic certificate renewal
  • ✅ Native support on all major platforms
  • ✅ Dual-stack IPv4/IPv6
  • ✅ Privacy-respecting DNS

This setup is suitable for personal use, small teams, or as a starting point for larger deployments. The combination of IKEv2's efficiency and strongSwan's flexibility makes this an excellent choice for modern VPN needs.

You can set this up in another country to get country-specific resources such as when you're learning a new language and want to practice it online while browsing.

This does not anonymize you like a TOR browser on the ONION router. This is to protect you from people trying to see and profit off your traffic locally.

Stay secure! 🔒


Additional Resources


This guide was created based on a production deployment. Always test in a non-production environment first and adjust settings for your specific needs.