OVCP runs an OpenVPN server end to end — its own certificate authority
(no easy-rsa), server config, client cert issuance, the openvpn
process itself, one static Go binary. No shell scripts hacked around
openssl. No compose stack. No separate database. Self-hosted: ovcp
owns the CA, the config, the worker process — bare metal, systemd,
container, no difference.
Beyond PKI: internal DNS, policy-routed egress, per-client traffic stats, operational notifications. Same binary. All optional. Zero extra installs. CLI and web UI both do the full job — neither is a stripped-down stand-in for the other.
Features · Requirements · Install · Quick start · CLI · Documentation · License
Always on. No external tool required.
- Native PKI — issue, revoke, renew. No
easy-rsa, no hand-rolledopensslwrappers. CA private key encrypted at rest. Passphrase never touches disk. - One binary — web UI embedded. No separate frontend, no reverse proxy.
- Full CLI — every UI action has a CLI twin, not a crippled subset.
-jsonflips any command to machine output, same fields either way. Completion for bash/zsh/fish. - Client export — ready-to-import
.ovpnon stdout, optionally key-encrypted, split-tunnel or full-tunnel, custom OpenVPN directives per client. Same output, CLI or UI. - Live stats & audit log — per-client traffic history, a top(1)-style
stats -follow, an audit trail of every admin action. - Log management — rotation for
openvpn.log/ovcp.log/dnsmasq.log, retention configurable. Copytruncate against each worker's already-open handle. No restart. - Access control — role-based (admin/operator/readonly), optional TOTP 2FA with QR enrollment, rate-limited logins, CSRF-protected HTTPS-only sessions.
- Encrypted backup/restore — one command, one encrypted archive. Everything needed to rebuild the server. Nothing tied to the original host.
- Packaging — systemd unit,
.deb/.rpm/.apk/Arch/Void (amd64, ARM, RISC-V; glibc or musl), multi-arch container image, SELinux policy. Shipped. Not bolted on after the fact.
Gated on a tool being on PATH, or explicit opt-in. Core doesn't care
whether they're there or not.
- Internal DNS (needs
dnsmasq) — resolve the server and every connected client by hostname over the tunnel, plus custom hosts-file entries. Restrict it to local names only, no upstream forwarding. - Upstreams (needs
nftables) — pick the egress connected clients exit through instead of the box's own default route, switchable live in the common case: a local interface (a second NIC, a WireGuard peer — anything already up on the host,ovcponly steers and NATs) or a SOCKS5 backend (Tor, proxychains, self-hosted, relayed through a small in-process proxy). Per-client overrides on top of a server-wide default, kill-switched: a dead egress fails traffic closed, never leaks it out the host's own route. - Telegram bot (needs a bot token) — notifications for revokes, user changes, CA rotation, cert expiry, plus a narrow Start/Stop/Restart/ Reconnect menu gated to one linked chat. Off unless configured. Nothing else depends on it.
Linux with /dev/net/tun, openvpn on PATH.
OVCP runs as root — owns the PKI, starts openvpn itself as a supervised
child process. dnsmasq
and nftables are optional, gating DNS and
Upstreams above — ovcp starts/programs each itself when found, never
installs either.
That's what it takes to run ovcp. Building from source has its own
toolchain requirements — see docs/BUILD.md.
Ships three ways. Pick one.
- Distro package —
.deb,.rpm,.apk, or Arch/Void — binary, man page, shell completions, service file, one step.openvpnpulls in as a hard dependency;dnsmasq/nftablesare suggested, not forced. - Container image — one image, OpenVPN included, nothing else to install.
- Static binary — drop
ovcponPATH. Nothing else required at runtime beyondopenvpnitself.
No tagged release yet. Nothing prebuilt. Build from source — see
docs/BUILD.md for your architecture and libc.
Examples assume ovcp is on PATH — a distro package puts it there; a
from-source build needs sudo make install first, or run bin/ovcp
straight from the checkout (see docs/BUILD.md).
Two commands: init once, serve forever after.
export OVCP_DATA=$PWD/data # or run as root against /var/lib/ovcp — see below
ovcp init -server-cn vpn.example.com
# → CA, server cert, tls-crypt key, server.conf, admin user
# → prompts for a CA passphrase and the admin password
ovcp serve
# → starts openvpn (ovcp owns the process) and the admin UI
# → https://127.0.0.1:8443 (self-signed cert on first run)init is idempotent. Re-run it whenever; it only fills gaps. serve
refusing with not initialized, missing: ... means run init first.
Issue a client, get a ready-to-import profile:
ovcp certificate issue -cn alice -export -remote vpn.example.com > alice.ovpnProduction, installed package:
sudo ovcp init -server-cn vpn.example.com
sudo systemctl enable --now ovcpContainer, same two steps:
podman run --rm -v ovcp:/var/lib/ovcp ovcp init -server-cn vpn.example.com
podman run -d --cap-add=NET_ADMIN --device /dev/net/tun \
-p 1194:1194/udp -p 127.0.0.1:8443:8443 \
-v ovcp:/var/lib/ovcp ovcpIdentical RealAddress on every client: man ovcp, DEPLOYMENT →
container.
ovcp <command> covers certs, users, backups, live status, stats — ovcp -h for the full command table, man ovcp for every flag, the security/
privilege model, environment variables, deployment recipes. Every command
scriptable: ovcp -json <command> (flag goes before the command, like
-data/-debug) swaps human text for JSON on stdout — same fields, both
directions, errors included ({"error": "..."}, same shape the REST API
uses). stats -follow and completion are the only exceptions — no
single result to encode.
ovcp -json certificate list | jq -r '.[] | select(.status == "expired") | .cn'Four documents, four altitudes, nothing duplicated between them:
- This README — what OVCP is, why it exists, shortest path to a running server.
man ovcp— the complete runtime reference: every command and flag, the security and privilege model, every environment variable, every file ovcp reads or writes, deployment recipes for systemd, containers, SELinux. Same page renders into the web UI's Docs tab at build time — no terminal needed, one source either way. From a checkout, before installing anything:make man.docs/BUILD.md— building the binary, cross-compiling for another arch/libc, the container image, distro/Void packages.docs/ARCHITECTURE.md— internal design: source layout, the process and IPC model, the rationale behind decisions that affect more than one package.