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.
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:
- strongSwan acts as our IKEv2 server
- Let's Encrypt provides trusted SSL certificates
- UFW (Uncomplicated Firewall) handles firewall rules and NAT
- AppArmor is configured to allow strongSwan to read certificates
- 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-hooktemporarily opens port 80 for HTTP challenges - The
post-hookcloses port 80 immediately after verification - The
renew-hookreloads 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.
Step 4: Link Certificates to strongSwan
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 certificateprivkey.pem: Your server's private keyfullchain.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
limitinstead ofallowrate-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 initiatetype=tunnel: Full tunnel mode (all traffic goes through VPN)keyexchange=ikev2: Use IKEv2 protocolfragmentation=yes: Handle large packets gracefullyforceencaps=yes: Always use UDP encapsulation (helps with NAT)
Cryptography:
ike=: Algorithms for IKE handshakeaes256gcm16: AES-256 in GCM mode (authenticated encryption)prfsha384: PRF using SHA-384ecp384: Elliptic curve P-384 (very strong)
esp=: Algorithms for data encryptionaes256gcm16-ecp384: Strong authenticated encryption with PFS
Dead Peer Detection:
dpdaction=clear: Drop connection if peer diesdpddelay=900s: Check every 15 minutesrekey=no: Don't automatically rekey (saves battery on mobile)
Server side (left):
left=%any: Accept connections on any interfaceleftid=@vpn.example.com: Server's identityleftcert=cert.pem: Server's certificateleftsubnet=::/0,0.0.0.0/0: Route all IPv4 and IPv6 traffic through VPN
Client side (right):
right=%any: Accept any client IPrightauth=eap-mschapv2: Clients authenticate with username/passwordrightdns=: 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,and1.0.0.2are 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 bootipsec restart: Start the VPN server nowipsec 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
- Create a configuration profile using the Apple Configuration Profile template
- Set
RemoteAddressandRemoteIdentifierto your VPN domain (vpn.example.com) - Install the profile on your device
- Go to Settings → VPN → Add VPN Configuration
- Enter your username and password from
/etc/ipsec.secrets
Android
- Settings → Network & Internet → VPN → Add VPN
- Type: IKEv2/IPSec MSCHAPv2
- Server address:
vpn.example.com - Username and password from
/etc/ipsec.secrets
Windows
- Settings → Network & Internet → VPN → Add VPN
- VPN provider: Windows (built-in)
- Connection name: Your choice
- Server name:
vpn.example.com - VPN type: IKEv2
- 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:
- Replace all instances of
vpn.example.comwith your actual domain - Generate UUIDs with
uuidgenand replace allYOUR-UUID-HEREplaceholders - Save as
vpn.mobileconfig - Email it or AirDrop it to yourself.
- Open on your iOS/macOS device to install
Configuration highlights:
OnDemandEnabled: Automatically connects when neededEnablePFS: Perfect Forward Secrecy (each session has unique keys)ExtendedAuthEnabled: Enables username/password authenticationOverridePrimary: 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.comanddig +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 withip 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
- Modern Cryptography: AES-256-GCM with P-384 ECC is future-proof
- Trusted Certificates: Let's Encrypt means no certificate warnings
- Perfect Forward Secrecy: Each session has unique keys
- No Logs: This setup doesn't log user activity (though you can enable logging if needed)
Potential Improvements
- Certificate Authentication: Replace passwords with client certificates (more secure but harder to manage)
- 2FA: Integrate with RADIUS for two-factor authentication
- Fail2ban: Automatically block brute-force attempts
- 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.