π Automate your VPN deployment in minutes! A powerful, production-ready Ansible playbook for deploying eduVPN v3 on a single Debian or Ubuntu server.
Warning
Disclaimer
Consortium GARR provides this code as-is to the community for sharing purposes but does not guarantee support, maintenance, or further development of the code. Use it at your own discretion.
This repository contains an Ansible playbook for automated deployment of eduVPN v3 on a single Debian/Ubuntu server, with the user portal (frontend) and VPN node (backend) co-located on the same host.
- β‘ Fully automated deployment with a single command
- π Configuration files ready with placeholder values to replace
- π WireGuard and OpenVPN support out-of-the-box
- π Automatic Let's Encrypt certificates
- π LDAP, OIDC/SAML or local database authentication
- π‘οΈ Automatically configured iptables firewall
- π’ Production-ready defaults
Official Reference: eduVPN Deployment Guide
- β¨ Key Features
- ποΈ Architecture
- π Requirements
- π Quick Start
- π οΈ Detailed Installation Guide
- π Variables Reference
- π§ Maintenance Operations
- π Troubleshooting
- π Useful Resources
graph TD
Internet-->|HTTPS :443| Apache
Internet-->|UDP :51820| WireGuard
subgraph SingleServer["Single Server vpn.example.org"]
subgraph WebServer["Web Server"]
Apache["Apache: mod_auth_openidc"]
Portal["vpn-user-portal: PHP-FPM"]
Apache --> Portal
end
subgraph VPNBackend["VPN Backend"]
Daemon["vpn-daemon: :41194 internal"]
WireGuard["WireGuard"]
end
Portal -->|localhost API| Daemon
Daemon --> WireGuard
Memcached[("Memcached: :11211 Session storage")]
Portal -.-> Memcached
end
classDef server fill:#f9f9f9,stroke:#333,stroke-width:2px;
classDef proxy fill:#e1f5fe,stroke:#03a9f4,stroke-width:1px;
classDef app fill:#e8f5e9,stroke:#4caf50,stroke-width:1px;
classDef vpn fill:#fff3e0,stroke:#ff9800,stroke-width:1px;
classDef db fill:#f3e5f5,stroke:#9c27b0,stroke-width:1px;
class Apache proxy;
class Portal app;
class Daemon,WireGuard vpn;
class Memcached db;
- vpn-user-portal: Web interface for users, profile and configuration management
- vpn-server-node: VPN backend with WireGuard and OpenVPN support
- vpn-daemon: Internal API for VPN connection management
- Apache + PHP-FPM: Web server with HTTPS support and mod_auth_openidc
- Memcached: User session storage
- iptables: Firewall with NAT/masquerade rules for VPN traffic
| Item | Requirements |
|---|---|
| Operating System | Debian or Ubuntu |
| System State | Clean installation with all updates applied |
| IP Address | Static public IPv4 (IPv6 optional) |
| Hostname | Properly configured FQDN (e.g., vpn.example.org) |
| DNS | A record (and optional AAAA) pointing to the server |
| Firewall | Open ports: TCP 22, 80, 443 and UDP 51820 |
| Access | SSH with sudo privileges |
| Item | Requirements |
|---|---|
| Python | 3.8+ |
| Ansible | β₯ 8.0 (installed via pip) |
| Connectivity | SSH access to target server |
| Port | Protocol | Service | Notes |
|---|---|---|---|
| 22 | TCP | SSH | Administration |
| 80 | TCP | HTTP | HTTPS redirect + ACME challenge |
| 443 | TCP | HTTPS | vpn-user-portal web portal |
| 51820 | UDP | WireGuard | VPN tunnel (can be modified) |
| 1194 | UDP/TCP | OpenVPN | VPN tunnel (optional) |
-
Edit configuration files:
cd ansible/ # Files already contain placeholder values to replace nano group_vars/all/vars.yml # Configure domain, IP, email, etc. nano inventories/single/hosts.ini # Configure target server nano group_vars/all/vault.yml # Generate and insert secrets
-
Generate your secrets:
# Node key openssl rand -hex 32 # WireGuard keys wg genkey | tee /tmp/wg.key | wg pubkey # OIDC secrets (if needed) pwgen -s 32 1
Insert generated values into
vault.yml -
Encrypt the vault:
ansible-vault encrypt group_vars/all/vault.yml
-
Run the deployment:
ansible-playbook playbook.yml --ask-vault-pass
Install Ansible dependencies on your local machine (not on the VPN server):
cd ansible/
pip install -r requirements.txt
ansible-galaxy collection install -r requirements.ymlDependencies installed:
ansible-coreβ₯ 8.0- Collection
ansible.posix(for sysctl, firewall management) - Collection
community.general(for Apache modules)
Edit the inventory file to specify the target server:
nano inventories/single/hosts.iniExample:
[single]
vpn.example.org ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/id_rsaEdit deployment variables:
nano group_vars/all/vars.ymlMain variables to modify:
| Variable | Description | Example |
|---|---|---|
eduvpn_fqdn |
VPN server FQDN | vpn.example.org |
server_ipv4 |
Public IPv4 address | 90.123.123.123 |
debian_code_name |
Debian/Ubuntu codename | bookworm or noble |
public_interface_name |
Public network interface | ens3, eth0 |
certbot_admin_email |
Email for Let's Encrypt | admin@example.org |
auth_module |
Authentication method | DbAuthModule, OidcAuthModule, LdapAuthModule |
Option 1: Local Database (DbAuthModule)
auth_module: DbAuthModule- Authentication based on local username/password
- Ideal for small deployments or testing
Option 2: OpenID Connect (OidcAuthModule)
auth_module: OidcAuthModule
oidc_provider_metadata_url: "https://sso.example.org/.well-known/openid-configuration"
oidc_client_id: "eduvpn-client"
oidc_redirect_uri: "https://{{ eduvpn_fqdn }}/vpn-user-portal/redirect_uri"- Integration with SSO providers (Keycloak, Auth0, Google, etc.)
- Requires client configuration on OIDC provider
Option 3: LDAP (LdapAuthModule)
auth_module: LdapAuthModule
ldap_uri: "ldap://ldap.example.org"
ldap_bind_dn_template: "uid={{UID}},ou=people,dc=example,dc=org"- Authentication against LDAP/Active Directory
Edit the profiles section to define VPN profiles:
profiles:
- id: default
name: "Default VPN"
hostname:
- "{{ eduvpn_fqdn }}"
default_gateway: false # true = all traffic goes through VPN
dnsServerList:
- "9.9.9.9" # Public or internal DNS
- "149.112.112.112"
routelist: # Only these networks go through VPN
- "192.168.0.0/16"
- "10.0.0.0/8"
preferred_proto: wg # wg = WireGuard, openvpn = OpenVPN
wg_range_ipv4:
"0": "10.43.43.0/24" # Internal IP range for VPN clientsThe group_vars/all/vault.yml file contains placeholders for all secrets.
IMPORTANT: Generate and insert your own values before deployment:
The vault_ca_crt and vault_ca_key certificates are used for internal mTLS communication between the portal and vpn-daemon.
Generate new certificates
# Generate CA key
openssl ecparam -name prime256v1 -genkey -noout -out ca.key
# Generate CA certificate
openssl req -new -x509 -key ca.key -out ca.crt -days 3650 \
-subj "/CN=vpn-daemon CA"
# Generate certificate for vpn-daemon
openssl ecparam -name prime256v1 -genkey -noout -out tls.key
openssl req -new -key tls.key -out tls.csr -subj "/CN=vpn-daemon"
openssl x509 -req -in tls.csr -CA ca.crt -CAkey ca.key Note: For single-server deployments,
vpn_daemon_use_tls: falseis the recommended setting (communication happens on localhost).
# Edit the vault with generated values
nano group_vars/all/vault.yml
# Encrypt the file with ansible-vault
ansible-vault encrypt group_vars/all/vault.ymlYou'll be prompted for a password that you must remember for future deployments.
Important: The
oauth.keyis automatically generated by the playbook via/usr/libexec/vpn-user-portal/generate-secretsand should NOT be manually inserted.
The playbook fully automates obtaining the Let's Encrypt certificate using certbot standalone.
Requirements:
- TCP port 80 must be reachable from the Internet during first deployment
- DNS must correctly point to the server
- The
certbot_admin_emailparameter must be configured invars.yml
Configuration in vars.yml:
certbot_create_if_missing: true
certbot_create_method: standalone
certbot_testmode: false # true for test certificates
certbot_admin_email: admin@example.orgThe playbook:
- Checks if certificate already exists
- Temporarily stops Apache
- Runs
certbotin standalone mode - Obtains Let's Encrypt certificate
- Restart Apache with new certificate
Automatic renewal: Let's Encrypt automatically configures renewal via systemd timer.
Run the complete deployment:
ansible-playbook playbook.yml --ask-vault-passExpected output:
- Installation of eduVPN packages from official repository
- Configuration of Apache, PHP-FPM, Memcached
- Deployment of portal and node configurations
- Generation and distribution of cryptographic keys
- Obtaining Let's Encrypt certificate
- Firewall configuration (iptables)
- Service activation (apache2, vpn-daemon, memcached)
ssh root@vpn.example.org
# Check service status
systemctl status apache2
systemctl status vpn-daemon
systemctl status memcached
# Verify certificate
certbot certificates
# Verify WireGuard interfaces
wg showIf using DbAuthModule, create an administrator user:
ssh root@vpn.example.org
sudo -u www-data vpn-user-portal-account --add admin
# Enter password when promptedConfigure the user as admin in /etc/vpn-user-portal/config.php:
'adminUserIdList' => ['admin'],Apply changes:
vpn-maint-apply-changesOpen browser and go to:
https://vpn.example.org/vpn-user-portal/
- You should see the eduVPN login page
- HTTPS certificate should be valid (Let's Encrypt)
- Login with the created user or via SSO (if OIDC configured)
To update only configurations (without reinstalling packages):
ansible-playbook playbook.yml --ask-vault-pass --tags update_configTo update only firewall rules:
ansible-playbook playbook.yml --ask-vault-pass --tags iptablesTo redistribute WireGuard keys:
ansible-playbook playbook.yml --ask-vault-pass --tags wg| Variable | Default | Description |
|---|---|---|
eduvpn_fqdn |
vpn.example.org |
VPN portal FQDN (must match DNS) |
debian_code_name |
bookworm |
Distribution codename (bookworm, noble) |
public_interface_name |
eth0 |
Public network interface for masquerading |
server_ipv4 |
β | Server's public IPv4 address |
server_ipv6 |
"" |
Server's public IPv6 address (empty to disable) |
wireguard_port |
51820 |
WireGuard listening port |
node_slave_port |
41194 |
vpn-daemon internal API port (localhost) |
vpn_daemon_use_tls |
false |
Enable mTLS between portal and vpn-daemon (not recommended for single-host) |
| Variable | Description |
|---|---|
auth_module |
Authentication module: DbAuthModule, OidcAuthModule, LdapAuthModule |
oidc_provider_metadata_url |
OpenID Connect provider metadata URL |
oidc_client_id |
Client ID registered on OIDC provider |
oidc_redirect_uri |
Redirect URI after OIDC authentication |
ldap_uri |
LDAP server URI (e.g., ldap://ldap.example.org) |
ldap_bind_dn_template |
LDAP bind DN template (e.g., uid={{UID}},ou=people,dc=example,dc=org) |
admin_user_id_list |
List of administrator users |
Profiles are defined in the profiles list, each with:
| Parameter | Description |
|---|---|
id |
Unique profile identifier |
name |
Display name of the profile |
hostname |
List of VPN server hostnames |
default_gateway |
true = all traffic goes through VPN, false = split tunneling |
dnsServerList |
List of DNS servers for clients |
dnsSearchDomainList |
List of DNS search domains |
routelist |
IPv4 networks to route through VPN |
routelist_ipv6 |
IPv6 networks to route through VPN |
preferred_proto |
Preferred protocol: wg (WireGuard) or openvpn |
wg_range_ipv4 |
Internal IP range for WireGuard clients |
wg_range_ipv6 |
Internal IPv6 range for WireGuard clients |
oUdpPortList |
UDP ports for OpenVPN |
oTcpPortList |
TCP ports for OpenVPN |
| Variable | Default | Description |
|---|---|---|
certbot_create_if_missing |
true |
Automatically obtain certificate |
certbot_create_method |
standalone |
Certbot method (standalone/webroot) |
certbot_testmode |
false |
Use Let's Encrypt staging server |
certbot_admin_email |
β | Email for Let's Encrypt notifications |
| Variable | Generation | Description |
|---|---|---|
vault_node_key |
openssl rand -hex 32 |
Shared key portalβnode (32 hex characters) |
vault_wireguard_private_key |
wg genkey |
WireGuard private key |
vault_wireguard_public_key |
wg pubkey |
WireGuard public key |
vault_oidc_clients_secret |
OIDC client secret | OIDC client secret |
vault_oidc_crypto_passphrase |
pwgen -s 32 1 |
OIDC encryption passphrase |
vault_ca_crt |
PEM certificate | Internal CA for vpn-daemon mTLS |
vault_ca_key |
PEM private key | Internal CA private key |
vault_tls_crt |
PEM certificate | vpn-daemon TLS certificate |
vault_tls_key |
PEM private key | vpn-daemon TLS private key |
| Tag | Effect | Command |
|---|---|---|
update_config |
Redeploy only PHP configuration files | ansible-playbook playbook.yml --tags update_config |
iptables |
Reapply only firewall rules | ansible-playbook playbook.yml --tags iptables |
wg |
Redistribute WireGuard keys | ansible-playbook playbook.yml --tags wg |
Add new user:
sudo -u www-data vpn-user-portal-account --add usernameModify existing user password:
sudo -u www-data vpn-user-portal-account --add username # overwritesConfigure administrator:
Edit /etc/vpn-user-portal/config.php:
'adminUserIdList' => ['admin', 'username'],Apply changes:
vpn-maint-apply-changesThe playbook installs an automatic update script in /etc/cron.weekly/vpn-maint-update-system.
Manual update:
# Update system packages
apt update && apt upgrade -y
# Update eduVPN packages
apt update && apt install --only-upgrade vpn-user-portal vpn-server-node vpn-maint-scripts
# Apply configuration changes
vpn-maint-apply-changesSee Install Updates for more details.
- Edit
group_vars/all/vars.yml(sectionprofiles) - Run playbook with
update_configtag:ansible-playbook playbook.yml --ask-vault-pass --tags update_config
Complete documentation: Profile Config
Check service status:
systemctl status apache2
systemctl status vpn-daemon
systemctl status memcachedUseful logs:
# Apache logs
tail -f /var/log/apache2/error.log
# vpn-daemon logs
journalctl -u vpn-daemon -f
# User portal logs
tail -f /var/log/vpn-user-portal.log
# Active WireGuard connections
wg showUsage statistics:
# Show connected users and traffic
vpn-user-portal-show-statsError: Failed to obtain certificate from Let's Encrypt
Common causes:
- TCP port 80 blocked by cloud/router firewall
- DNS not correctly pointing to server
- Hostname not properly configured
Solution:
# Verify DNS
dig +short vpn.example.org
# Verify port 80 reachability
nmap -p 80 vpn.example.org
# Verify hostname
hostnamectl
# Obtain certificate manually
certbot certonly --standalone -d vpn.example.org --email admin@example.org
# Re-run playbook
ansible-playbook playbook.yml --ask-vault-passSymptoms: Client downloads configuration but does not connect
Verify:
# 1. Verify WireGuard port is open
ss -ulnp | grep 51820
# 2. Verify WireGuard interface
wg show
# 3. Verify IP forwarding
sysctl net.ipv4.ip_forward # must be 1
sysctl net.ipv6.conf.all.forwarding # must be 1
# 4. Verify iptables rules
iptables -t nat -L -n -v | grep MASQUERADESolution:
# Reapply firewall rules
ansible-playbook playbook.yml --ask-vault-pass --tags iptables
# Restart vpn-daemon
systemctl restart vpn-daemonError: Unable to authenticate with OIDC provider
Verify:
# Test provider metadata reachability
curl https://sso.example.org/.well-known/openid-configuration
# Verify portal configuration
cat /etc/vpn-user-portal/config.php | grep -A 10 OidcAuthVerify on OIDC provider:
- Correct Client ID
- Redirect URI configured:
https://vpn.example.org/vpn-user-portal/redirect_uri - Correct client secret
Error: apache2.service failed
Verify:
# Test Apache configuration
apache2ctl configtest
# Verify certificates
ls -la /etc/letsencrypt/live/vpn.example.org/
# Apache logs
journalctl -u apache2 -n 50Common solution:
# Regenerate certificate if missing
certbot certonly --standalone -d vpn.example.org
# Restart Apache
systemctl restart apache2Symptoms: Web portal works but does not create VPN connections
Verify:
# Service status
systemctl status vpn-daemon
# Detailed logs
journalctl -u vpn-daemon -n 100
# Verify keys
ls -la /etc/vpn-server-node/keys/
# Test internal connection
curl http://localhost:41194/api/statsSolution:
# Redistribute node keys
ansible-playbook playbook.yml --ask-vault-pass --tags update_config
# Restart vpn-daemon
systemctl restart vpn-daemon
# Apply changes
vpn-maint-apply-changesansible/
βββ ansible.cfg # Ansible configuration
βββ playbook.yml # Main playbook (portal + node)
βββ requirements.txt # Python dependencies
βββ requirements.yml # Ansible collections
βββ Makefile # Helper make (encrypt/decrypt vault)
βββ encrypt.sh / decrypt.sh # Vault management scripts
β
βββ inventories/
β βββ single/
β βββ hosts.ini # Target server
β
βββ group_vars/
β βββ all/
β βββ vars.yml # Public variables
β βββ vault.yml # Secrets (encrypted)
β
βββ files/
β βββ credentials.conf # Credentials template
β βββ vpn-daemon # Service script
β
βββ templates/ # Jinja2 templates
βββ apache2/
β βββ apache_site-eduvpn.conf.j2 # Main VirtualHost
β βββ apache_site-vpn-user-portal.conf.j2 # Portal API
βββ eduvpn/
β βββ portal-config.php.j2 # Portal configuration
β βββ node-config.php.j2 # Node configuration
β βββ vpn-daemon.env.j2 # vpn-daemon environment
βββ iptables/
β βββ iptables.j2 # IPv4 firewall rules
β βββ ip6tables.j2 # IPv6 firewall rules
βββ keys/
β βββ ca.crt.j2 / ca.key.j2 # Internal CA
β βββ oauth.key.j2 # OAuth key
β βββ tls.crt.j2 / tls.key.j2 # TLS daemon certificates
βββ memcached/
βββ memcached.conf.j2 # Memcached config
apache2,php-fpm,memcachediptables-persistent,certbot- Tools:
curl,wget,pwgen,ipcalc-ng,wg libapache2-mod-auth-openidc(for OIDC)
- Downloads deploy scripts from Codeberg eduVPN/deploy
- Adds eduVPN GPG key
- Configures apt repository
https://repo.eduvpn.org/v3/deb
vpn-user-portal(web frontend)vpn-server-node(VPN backend)vpn-maint-scripts(maintenance scripts)
- Deploy
/etc/vpn-user-portal/config.phpfrom template - Deploy
/etc/vpn-server-node/config.phpfrom template - Configure VPN profiles, auth module, admin users
node.0.keyβ Shared key portalβnodeoauth.keyβ Automatically generated byvpn-user-portal- WireGuard keys β Private key on node, public key on portal
- CA/TLS certs β For vpn-daemon mTLS (if enabled)
- Temporarily stops Apache
- Runs
certbot standalonefor ACME challenge - Obtains validated certificate
- Restarts Apache with configured HTTPS
- Deploy VirtualHost with TLS
- Enable modules:
ssl,headers,rewrite,proxy_fcgi,proxy_http - Configure php-fpm
- Disable default site
- Enable IPv4/IPv6 IP forwarding via sysctl
- Deploy iptables rules (NAT/MASQUERADE)
- Configure
iptables-persistent
- Configure vpn-daemon environment
- Enable services:
apache2,vpn-daemon,memcached - Run
vpn-maint-apply-changesto apply configurations
- Deploy Debian/Ubuntu
- Profile Configuration
- Authentication - LDAP
- Authentication - SAML
- Authentication - RADIUS
- Permissions
- Firewall
- Portal Branding
- Install Updates
This project is licensed under the GNU General Public License v3.0. See the LICENSE file for details.
Contributions, further developments, error reports (and possibly fixes) are welcome.
For more info, please read the CONTRIBUTING file.