Skip to content

About

Self-hosted OpenVPN control plane with UDP/TCP servers, DCO, EasyRSA Web UI, AUTOPASS profiles, and bulk downloads

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Repository files navigation

OpenVPN Control

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.

Features

  • 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 askpass directive.
  • Per-user ZIP downloads and a global ZIP containing every active user's valid profiles.
  • Revoked users are excluded from all downloads.

Profile naming

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.

Architecture

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.

Requirements

  • Linux host with /dev/net/tun.
  • Podman 4.x or newer with a Compose provider.
  • Passwordless sudo for 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.

Quick start

1. Configure the deployment

cp .env.example .env
$EDITOR .env

At minimum, set:

OPENVPN_HOST=vpn.example.com

OPENVPN_HOST is embedded in generated client profiles. Configure DNS and router/cloud port forwarding separately.

2. Generate Web UI credentials

./init-webui.sh

The generated administrator password is stored with mode 0600 at:

webui-data/admin-credentials.txt

3. Initialize the PKI and server configuration

./init.sh

This operation refuses to overwrite an existing data/pki or data/openvpn.conf.

4. Start the stack

sudo podman compose up -d --build

Check it:

sudo podman ps
sudo podman logs openvpn-control-udp
sudo podman logs openvpn-control-tcp
sudo podman logs openvpn-control-webui

5. Open the Web UI

The UI listens only on loopback by default. From another computer:

ssh -L 8096:127.0.0.1:8096 vpn-host

Then open http://127.0.0.1:8096 and read the credentials on the server:

sudo cat webui-data/admin-credentials.txt

Router port forwarding

If .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.

DCO on Ubuntu 24.04

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.sh

The script is Ubuntu-version-specific. Review it before use on another distribution.

Tests

Pure profile and PKI-state tests require only Python:

PYTHONPATH=webui python3 -m unittest discover -s webui/tests -v

After deployment, create a test client and exercise the UDP tunnel:

./create-test-client.sh client-smoke
./smoke-test.sh client-smoke

The smoke test requires DCO and checks the VPN gateway plus SMOKE_TARGET from .env.

Security notes

  • 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-0600 temporary 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/ and webui-data/ together. Test certificate revocation and restore procedures before production use.

License

MIT

About

Self-hosted OpenVPN control plane with UDP/TCP servers, DCO, EasyRSA Web UI, AUTOPASS profiles, and bulk downloads

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages