DEV Community

ZeroTrust Architect
ZeroTrust Architect

Posted on • Originally published at cacheguard.com

Self-Hosted IPsec VPN: PKI Setup, StrongSwan Config, and Client Provisioning End-to-End

A self-hosted VPN server gives you full control over authentication, logging, and certificate issuance. Here is what the complete technical stack looks like — from PKI setup through to client connection.

Self-Hosted VPN Server

The PKI: generating your certificate hierarchy

A minimal PKI for IPsec VPN needs three certificate types:

Root CA (self-signed, 10yr validity)
  ├── Server certificate (signed by Root CA, for VPN server)
  └── Client certificates (signed by Root CA, one per device/user)
Enter fullscreen mode Exit fullscreen mode

Using OpenSSL:

# Generate root CA key and self-signed certificate
openssl genrsa -out root-ca.key 4096
openssl req -x509 -new -key root-ca.key -sha256 -days 3650 \
  -out root-ca.crt -subj "/CN=VPN Root CA/O=MyOrg"

# Generate server key and CSR
openssl genrsa -out server.key 2048
openssl req -new -key server.key \
  -out server.csr -subj "/CN=vpn.example.com/O=MyOrg"

# Sign server certificate with root CA
openssl x509 -req -in server.csr -CA root-ca.crt -CAkey root-ca.key \
  -CAcreateserial -out server.crt -days 825 -sha256 \
  -extfile <(echo "subjectAltName=DNS:vpn.example.com")

# Generate client certificate (repeat per device)
openssl genrsa -out client-device1.key 2048
openssl req -new -key client-device1.key \
  -out client-device1.csr -subj "/CN=device1/O=MyOrg"
openssl x509 -req -in client-device1.csr -CA root-ca.crt -CAkey root-ca.key \
  -CAcreateserial -out client-device1.crt -days 825 -sha256
Enter fullscreen mode Exit fullscreen mode

Keep root-ca.key offline — it is only needed to sign new certificates.

StrongSwan server configuration

# /etc/ipsec.conf
config setup
    charondebug="ike 1, knl 1, cfg 0"

conn ikev2-vpn
    auto=add
    compress=no
    type=tunnel
    keyexchange=ikev2
    fragmentation=yes
    forceencaps=yes          # Always use UDP/4500 (better NAT compatibility)

    # Server side
    left=%any
    leftid=@vpn.example.com
    leftcert=server.crt
    leftsendcert=always
    leftsubnet=0.0.0.0/0     # Route all client traffic through VPN

    # Client side
    right=%any
    rightid=%any
    rightauth=pubkey
    rightsourceip=10.8.0.0/24  # Virtual IP pool
    rightdns=10.8.0.1

    ike=aes256-sha256-ecp256,aes256-sha1-modp2048!
    esp=aes256-sha256,aes256-sha1!
    dpdaction=clear
    dpddelay=300s
    rekey=no
Enter fullscreen mode Exit fullscreen mode
# /etc/ipsec.secrets — certificate-based auth needs no shared secrets
: RSA server.key
Enter fullscreen mode Exit fullscreen mode

Platform-specific client configuration

iOS / macOS (native IKEv2): Package root CA + client cert + client key into a .mobileconfig profile. iOS imports it via Settings → Profile Downloaded.

Windows (native IKEv2):

# Import root CA to machine trust store
Import-Certificate -FilePath root-ca.crt -CertStoreLocation Cert:\LocalMachine\Root

# Import client cert to personal store
Import-PfxCertificate -FilePath client-device1.pfx -CertStoreLocation Cert:\CurrentUser\My

# Add VPN connection
Add-VpnConnection -Name "OrgVPN" -ServerAddress "vpn.example.com" `
  -TunnelType IKEv2 -AuthenticationMethod MachineCertificate `
  -EncryptionLevel Required -RememberCredential $false
Enter fullscreen mode Exit fullscreen mode

Android (StrongSwan app): Import .p12 bundle (client cert + key + root CA), configure IKEv2 Certificate auth.

Linux (NetworkManager):

nmcli connection add type vpn vpn-type libreswan \
  con-name "OrgVPN" vpn.data \
  "right=vpn.example.com,rightid=@vpn.example.com,\
   leftcert=client-device1.crt,leftkey=client-device1.key,\
   rightca=root-ca.crt,ikev2=insist"
Enter fullscreen mode Exit fullscreen mode

Certificate revocation

When a device is lost or compromised, revoke its client certificate:

openssl ca -revoke client-device1.crt -config openssl.cnf
openssl ca -gencrl -out crl.pem -config openssl.cnf
Enter fullscreen mode Exit fullscreen mode

Distribute the updated CRL to the server. StrongSwan checks CRL on connection if configured:

# strongswan.conf
charon {
    crl_strict = yes
}
Enter fullscreen mode Exit fullscreen mode

CacheGuard automates this entire workflow — root CA generation, certificate signing, and client profile creation — through its web interface, and handles DynDNS updates when the server's public IP is dynamic.

https://www.cacheguard.com/self-hosted-vpn-server/


Originally published on the CacheGuard Blog. CacheGuard is free and open source — GitHub.

Top comments (0)