openvpn
v2.7.6-alpineOpenVPN server image with sane defaults and signed verifiable supply chain. Multi-arch, lightweight.
Trust posture
VERIFIABLE TRUST ARTIFACTS
Provenance
- Build commit
8a8b07b- Build digest
-
sha256:bf67f68b646fc753259e3349ef226208f40ec77904032ee6f712358bb797112f - Index digest
-
sha256:4e9c7530324995a4250f3cecc0ce001d6ce37ee7748bfea369f42ee7e48731d6 - Manifest digest (amd64)
-
sha256:a526d298a262c6e60b8485f48d660bd7ba5da2910e26f610706c23ef76a7dbe9 - Manifest digest (arm64)
-
sha256:279d2b2b7aed558dc9554a0e006eca214cd92d38d2b6c1a3f13b16e56ffdf5f9 - SBOM attestation
-
43789020… - Trivy last scan
- 2026-08-29 00:47 UTC
- Base image
- Verify
gh attestation verify oci://ghcr.io/oorabona/openvpn:v2.7.6-alpine --owner oorabonaguide →
Security scan results
Trivy · last scan 2026-08-29
Top advisories (upstream, advisory)
- CVE-2026-75803 (NOTE) — openvpn — Issue summary: ChaCha20-Poly1305 and AES-OCB decryption with an empty ...
- CVE-2026-63075 (NOTE) — openvpn — openssl: QUIC ACK-only packet retention can cause memory exhaustion
- CVE-2026-63074 (NOTE) — openvpn — openssl: CMP indefinite cache growth of ExtraCerts
- CVE-2026-63073 (NOTE) — openvpn — openssl: untrusted sender DN used as format string in CMP response validation
- CVE-2026-54874 (NOTE) — openvpn — openssl: excessive memory use buffering DTLS records for a future epoch
→ Full report via gh api — see Verify Images.
Explore
Build lineagesha256:
Package summary n/a — runtime parsed
Recent changes n/a — runtime parsed
Build history n/a — runtime fetched
Dependency health all up to date
Dependency Health
all up to dateUp-to-date dependencies
Documentation
README
🔐 OpenVPN Docker Container built from sources with advanced security features
This is a simple Alpine based container with OpenVPN built from sources.
Platforms
amd64arm64
Features
- 🔐 Built from sources
- Dependant library
pkcs11-helperfrom the Alpine repository, signature-verified byapk - Embed
Google Authenticatorsupport
Verify this image
Every build ships a Sigstore-signed SBOM and a full Trivy scan — verify them yourself, no login required:
gh attestation verify oci://ghcr.io/oorabona/openvpn:latest --owner oorabona
Full walkthrough (SBOM payload, Trivy findings, multi-arch manifest inspection, upstream dependency tracking) → https://oorabona.github.io/docker-containers/verify-images/
Usage
Docker
docker run -d --name openvpn \
-p 1194:1194/udp \
-v openvpn-data:/etc/openvpn \
--cap-drop=ALL \
--cap-add=NET_ADMIN \
--cap-add=SETUID \
--cap-add=SETGID \
--security-opt no-new-privileges \
--device=/dev/net/tun \
--sysctl net.ipv6.conf.all.disable_ipv6=0 \
--sysctl net.ipv6.conf.all.forwarding=1 \
--sysctl net.ipv4.ip_forward=1 \
--sysctl net.ipv4.conf.all.forwarding=1 \
-e START_EXISTING=y \
-e AUTO_INSTALL=y \
-e AUTO_START=y \
oorabona/openvpn
First-run storage under
cap_drop: ALL. The example uses a named volume (openvpn-data) becauseAUTO_INSTALLgenerates the PKI andserver.confinto/etc/openvpnon first start, andcap_drop: ALLremoves theDAC_OVERRIDE/FOWNER/CHOWNcapabilities that let root bypass filesystem permissions. A named volume is created root-owned by the daemon, so that generation always succeeds. If you bind-mount a host directory instead, make sure it is writable by the container’s root (uid 0) — otherwise first-run generation fails.
Docker Compose
version: '3.7'
services:
openvpn:
image: oorabona/openvpn
container_name: openvpn
# First run bootstraps the PKI + server.conf into the empty named volume
# (AUTO_INSTALL=y) and starts the server (AUTO_START=y). START_EXISTING=y makes
# every subsequent start bring up the existing server non-interactively, so this
# is safe under `restart: unless-stopped` (see "Lifecycle" below). Set
# ENDPOINT=<host> for a fully non-interactive first install.
environment:
- START_EXISTING=y
- AUTO_INSTALL=y
- AUTO_START=y
- ENDPOINT=vpn.example.com # Set to the real public host/IP.
cap_add:
- NET_ADMIN
- SETUID
- SETGID
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
devices:
- /dev/net/tun
sysctls:
- net.ipv6.conf.all.disable_ipv6=0
- net.ipv6.conf.all.forwarding=1
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.forwarding=1
ports:
- 1194:1194/udp
volumes:
- openvpn_config:/etc/openvpn
restart: unless-stopped
volumes:
openvpn_config:
Lifecycle
On an empty volume, AUTO_INSTALL=y + AUTO_START=y generates the PKI +
server.conf and starts the server (set ENDPOINT=<host> for a fully
non-interactive first install). START_EXISTING=y then makes every
subsequent start bring up the already-installed server non-interactively — it
takes precedence over AUTO_INSTALL when a config already exists — so the
container is safe to run as a restart: unless-stopped service: a restart brings
the VPN back up instead of re-running setup or dropping into the management menu.
To manage clients (add/revoke), open the installer’s management menu with a
one-shot interactive Compose run against the deployed config volume while
clearing START_EXISTING and AUTO_INSTALL (those would start the server
instead of the menu): docker compose run --rm -e START_EXISTING= -e
AUTO_INSTALL= openvpn.
START_EXISTING applies to this Alpine image and expects the installer-generated
foreground config; a config carrying a daemon directive is rejected up front.
Restart-safety assumes the first install completed: START_EXISTING starts
whatever config exists, so if the initial bootstrap is interrupted after
server.conf is written but before the PKI finishes, reset the config volume and
bootstrap again rather than looping on a half-installed config. (Resolves
#912.)
Configuration
OpenVPN
The container is configured to use the server.conf file located in /etc/openvpn as default configuration file.
This file is generated from the script setup.sh located in /usr/local/bin and is based on the following environment variables:
- APPROVE_INSTALL
- IPV4_SUPPORT
- IPV6_SUPPORT
- PORT_CHOICE
- PROTOCOL_CHOICE
- DNS
- COMPRESSION_ENABLED
- CUSTOMIZE_ENC
- CLIENT
- PASS
- CONTINUE
- CLIENT_TO_CLIENT
- BLOCK_OUTSIDE_DNS
- OTP
- EASYRSA_CRL_DAYS
- SUBNET_IPv4
- SUBNET_IPv6
- SUBNET_MASKv4
- SUBNET_MASKv6
- ENDPOINT
For details about the meaning of each variable, please refer to the documentation.
Two more variables name the PKI. Both are optional; left unset, the installer generates a random name as before.
| Variable | Names | Description |
|---|---|---|
OPENVPN_SERVER_CN |
the CA | The common name of the certificate authority the install creates. |
OPENVPN_SERVER_NAME |
the server certificate | The name the server’s own certificate is issued to. Client profiles pin it with verify-x509-name, so it is the name a client checks the server by. It cannot equal CLIENT — the server certificate and the first client would collide. |
Both take effect only while the PKI is being built — that is, while
/etc/openvpn/easy-rsa is not yet a directory. A fresh volume qualifies, even
though /etc/openvpn itself already exists as the mount point. Once the PKI is
there the names are fixed: setting either variable has no effect on it, and
changing them means building a new PKI on an empty volume. The installer may
print a note when it ignores one, but do not rely on seeing it.
Three variables control the container’s lifecycle:
| Variable | Default | Description |
|---|---|---|
AUTO_INSTALL |
n |
y runs a non-interactive install (generates the PKI + server.conf) when no config exists. Left unset with a config already present, the entrypoint opens an interactive management menu instead of starting the server. |
AUTO_START |
n |
y starts the OpenVPN server after install (required for the server to actually run on this Alpine image). |
START_EXISTING |
n |
y starts an already-installed server non-interactively on restart, taking precedence over AUTO_INSTALL when a config exists — makes the container safe under restart: unless-stopped. Alpine (OS=other), installer-generated foreground configs only. |
Google Authenticator
The container is configured to use the google-authenticator library to generate the OTP code.
The generated QR code is stored in the container on a per user basis under the /etc/openvpn/otp directory.
The QR code can be retrieved using the following command:
docker exec -it openvpn cat /etc/openvpn/otp/username.png
More information can be found on the wiki.
Upgrading an OTP deployment created before this image
/etc/openvpn/otp holds one-time password secrets, and the installer checks that
an existing one is private before writing into it. Earlier images created that
directory with the default mode, usually 755, so on a volume carried over from
one of them adding another OTP client fails with:
Refusing to write one-time password secrets: /etc/openvpn/otp must give its owner rwx and neither group nor other any access (found mode 755).
That directory was created by root, so tightening it needs no capability beyond what the container already has. Existing secrets are untouched and no reinstall is needed:
docker exec openvpn chmod 700 /etc/openvpn/otp &&
docker exec openvpn stat -c '%U %a' /etc/openvpn/otp
Expect root 700. Anything else means the change did not take, and the cause is
worth identifying rather than guessing: a read-only or root-squashed mount, a
filesystem that carries no POSIX modes (some host bind mounts), or a directory
whose owner is not root. That last case cannot be repaired from inside the
container — the run command above drops every capability except NET_ADMIN,
SETUID and SETGID, so chown is unavailable. Fix ownership on the host, or
move /etc/openvpn to a named volume.
/etc/openvpn/clients, which holds client profiles and their keys, is checked
the same way and can need the same treatment.
Build Arguments
The following build arguments can be passed to customize the container build:
| Argument | Default | Description |
|---|---|---|
VERSION |
latest |
OpenVPN version to build |
UPSTREAM_VERSION |
(empty) | Fallback upstream version if VERSION is not specified |
OS_VERSION |
latest |
Alpine Linux version tag — 3.24 or newer, the releases carrying pkcs11-helper |
EASYRSA_VERSION |
3.2.6 |
EasyRSA version for certificate management |
NPROC |
1 |
Number of parallel processes for compilation |
PKCS11_HELPER_VERSION |
— | Refused. The build stops if it is set: pkcs11-helper now comes from Alpine, so its version follows OS_VERSION |
Build options
OpenVPN is built from sources using the following options:
--enable-iproute2option to use theipcommand instead ofifconfig--enable-pkcs11option to enable thepkcs11-helperlibrary and support of PKCS#11 tokens (e.g. Yubikey)--enable-plugin-auth-pamoption to enable thepamauthentication plugin (e.g. Google Authenticator uses this)--enable-async-pushoption to allow asynchronous push of configuration options to the client (and not wait for a remote authentification request to be completed)--enable-plugin-down-rootoption to allow thedown-rootplugin to be used (e.g. to drop privileges after the connection is established). Although this option is enabled, thedown-rootplugin is not used by default.--enable-selinuxoption to enable theselinuxsupport--disable-systemdoption to disable thesystemdsupport--disable-debugoption to make the binary smaller--disable-lzoand--disable-lz4options to disable thelzoandlz4compression support (prone to side-channel attacks)--enable-comp-stuboption to disable all compression altogether (still allow limited interoperability with compression-enabled peers)
Security
SELinux
The container is configured to run with the spc_t SELinux context.
This context is configured to allow the container to access the following resources:
/etc/openvpndirectory/etc/openvpn/otpdirectory/etc/openvpn/otp/*files
Capabilities
Run with cap_drop: ALL and only these capabilities added back:
NET_ADMIN— create the tun device and configure routes/firewall rulesSETUID/SETGID— let openvpn drop tonobody/nogroupafter setup
NET_RAW is not needed — openvpn’s UDP transport uses ordinary sockets,
not raw ones. These caps cover the default port (1194). If you configure
openvpn to bind an internal port below 1024, add NET_BIND_SERVICE as well
(the initial bind happens as root before the drop, and cap_drop: ALL removes
the privileged-port capability). Mapping a privileged host port to 1194
(-p 443:1194/udp) needs nothing extra.
Privileges
openvpn starts as root — creating the tun device, routes, and firewall
rules genuinely requires it — then drops to the unprivileged nobody
user/nogroup for the lifetime of the tunnel, provided the running
server.conf specifies user nobody / group nogroup (with persist-tun
/ persist-key so the already-open tun survives the drop). The image’s own
AUTO_INSTALL generates such a config; if you mount your own server.conf,
add those directives yourself — without them openvpn keeps running as root.
The SETUID/SETGID capabilities are what let that drop complete under
cap_drop: ALL. They aren’t optional for a user/group config: openvpn
treats a failed privilege drop as fatal, so without these caps the
container fails to start rather than silently running as root.
This drops the OpenVPN server worker to nobody; it does not make the whole
container rootless. The entrypoint wrapper that adds and removes the iptables
NAT rules around the tunnel stays alive as root for the server’s lifetime, and
OTP/PAM authentication (when enabled) uses a root helper. So the profile removes
NET_RAW and all other capabilities, runs under no-new-privileges, and drops
the server worker to nobody — a meaningful reduction, not a fully rootless
container.
The setuid/setgid capability requirement was verified directly under
cap_drop: ALL (setuid to nobody fails with only NET_ADMIN and succeeds
once SETUID/SETGID are added back). Exercising openvpn’s full end-to-end
drop needs a live tunnel with a real tun device, which the image’s current CI
e2e does not stand up — that coverage is tracked in
#910. Treat the
reduced-capability profile as verified at the capability level, not yet
exercised end-to-end by CI.
Security options
The default hardened profile (the run/Compose examples above) sets only
no-new-privileges.
On an SELinux-enforcing host the container additionally needs host-policy-
specific run options — relaxing seccomp/AppArmor and setting the container’s
SELinux label so it can manage the tun device and iptables under the spc_t
context described above. Those settings depend on your host’s policy, are not
part of the image’s default profile, and are not general hardening
recommendations — consult your platform’s SELinux + container documentation for
the exact values.
Dependencies
The following third-party dependencies are pinned and monitored for updates:
| Dependency | Version | Source | Monitoring |
|---|---|---|---|
| EasyRSA | 3.2.6 | GitHub Release (OpenVPN/easy-rsa) | Enabled |
pkcs11-helper is deliberately absent from that table: it comes from the Alpine
repository, so its version follows the base image and apk verifies its signature.
Nothing here pins or monitors it. Alpine carries it in main from 3.24 onward, so
OS_VERSION has that floor.