Self-hosted OpenVPN Community server stack with a certificate-management Web UI, UDP and TCP listeners, DCO-compatible profiles, encrypted client keys, configurable inbound endpoints, and bulk profile downloads.
The stack uses the maintained lisenet/openvpn image and runs with rootful Podman Compose. The Web UI never receives the Podman or Docker socket.
- OpenVPN 2.7.5 server over UDP and TCP on the same host port.
- Separate UDP and TCP address pools.
- Linux Data Channel Offload (DCO) support.
- Authenticated, CSRF-protected Web UI bound to loopback by default.
- EasyRSA user creation, key-password rotation, certificate revocation, and CRL updates.
- Configurable inbound names, domains/IPs, UDP ports, and TCP ports.
- Per-user UDP, TCP, or combined profile matrices.
- Optional AUTOPASS variants containing the
askpassdirective. - Per-user ZIP downloads and a global ZIP containing every active user's valid profiles.
- Revoked users are excluded from all downloads.
For user demo_user, inbound edge-a, and one port per protocol:
demo_user_edge-a.ovpn
demo_user_edge-a_tcp.ovpn
When AUTOPASS is enabled, the normal profiles remain and additional variants are generated:
demo_user_autopass_edge-a.ovpn
demo_user_autopass_edge-a_tcp.ovpn
UDP intentionally has no protocol suffix. If an inbound has multiple ports for one protocol, the port is added to prevent filename collisions:
demo_user_edge-a_443.ovpn
demo_user_edge-a_1196.ovpn
demo_user_edge-a_443_tcp.ovpn
demo_user_edge-a_1196_tcp.ovpn
AUTOPASS does not embed the private-key password. It adds askpass to a profile whose inline private key is already encrypted. The user must still provide that password to the OpenVPN client.
Linux host
├── ovpn kernel module (optional DCO acceleration)
└── rootful Podman
├── openvpn-control-udp host UDP 1196 → container 1194
├── openvpn-control-tcp host TCP 1196 → container 1194
└── openvpn-control-webui 127.0.0.1:8096 → container 8080
Persistent state is stored in:
data/ EasyRSA PKI, server configuration, CRL, and client material
webui-data/ SQLite database, PKI lock, and generated admin credentials
webui.env password verifier and Flask session secret
These paths are excluded from Git.
- Linux host with
/dev/net/tun. - Podman 4.x or newer with a Compose provider.
- Passwordless
sudofor the supplied operational scripts, or equivalent root access. - Python 3 on the host for initial Web UI credential generation.
- An unused host port for both UDP and TCP.
The pinned lisenet/openvpn image currently targets Linux AMD64.
cp .env.example .env
$EDITOR .envAt minimum, set:
OPENVPN_HOST=vpn.example.comOPENVPN_HOST is embedded in generated client profiles. Configure DNS and router/cloud port forwarding separately.
./init-webui.shThe generated administrator password is stored with mode 0600 at:
webui-data/admin-credentials.txt
./init.shThis operation refuses to overwrite an existing data/pki or data/openvpn.conf.
sudo podman compose up -d --buildCheck it:
sudo podman ps
sudo podman logs openvpn-control-udp
sudo podman logs openvpn-control-tcp
sudo podman logs openvpn-control-webuiThe UI listens only on loopback by default. From another computer:
ssh -L 8096:127.0.0.1:8096 vpn-hostThen open http://127.0.0.1:8096 and read the credentials on the server:
sudo cat webui-data/admin-credentials.txtIf .env contains OPENVPN_PORT=1196, forward both protocols:
WAN 1196/UDP → VPN host 1196/UDP
WAN 1196/TCP → VPN host 1196/TCP
An inbound port configured in the Web UI represents an external endpoint. The Web UI generates profiles; it does not change routers, cloud firewalls, NAT gateways, or DNS.
For example, an external TCP port 443 may forward to the VPN host's TCP port 1196, while the matching Web UI inbound uses TCP port 443.
OpenVPN works without DCO. For Ubuntu 24.04 hosts using a kernel older than Linux 6.16, enable-dco.sh installs the OpenVPN project's pinned ovpn-dkms backport and configures module loading:
./enable-dco.shThe script is Ubuntu-version-specific. Review it before use on another distribution.
Pure profile and PKI-state tests require only Python:
PYTHONPATH=webui python3 -m unittest discover -s webui/tests -vAfter deployment, create a test client and exercise the UDP tunnel:
./create-test-client.sh client-smoke
./smoke-test.sh client-smokeThe smoke test requires DCO and checks the VPN gateway plus SMOKE_TARGET from .env.
- Keep the Web UI bound to loopback and access it through SSH or a trusted authenticated reverse proxy with TLS.
- The Web UI has write access to the certificate authority and runs as root inside a capability-dropped container. A Web UI compromise can compromise the VPN PKI.
- The bootstrap script currently creates an unencrypted CA key so the Web UI can issue and revoke certificates without storing a CA passphrase. Protect the host and
data/backups accordingly. - Client private-key passwords are used only through mode-
0600temporary files and are not stored in SQLite. - Do not commit
.env,webui.env,data/,webui-data/, generated profiles, keys, certificates, or database files. - Back up
data/andwebui-data/together. Test certificate revocation and restore procedures before production use.
MIT