Back to Dashboard

openvpn

v2.7.6-alpine

OpenVPN server image with sane defaults and signed verifiable supply chain. Multi-arch, lightweight.

Trust posture

📋 SBOM ATTESTED🛡 TRIVY: 3 HIGH (advisory) · SCANNED 2026-08-29🏗 AMD64 + ARM64 🔗 1/1 deps tracked daily 🔍 REPRODUCE THESE CHECKS LOCALLY
PULLS 8.8K 6213 to 8814 pulls across 30 recorded days 2026-08-02: 6213 pulls2026-08-03: 6217 pulls2026-08-04: 6244 pulls2026-08-05: 6246 pulls2026-08-06: 6252 pulls2026-08-07: 6417 pulls2026-08-08: 6440 pulls2026-08-09: 6682 pulls2026-08-10: 6712 pulls2026-08-11: 6740 pulls2026-08-12: 6768 pulls2026-08-13: 6882 pulls2026-08-14: 6991 pulls2026-08-15: 7057 pulls2026-08-16: 7079 pulls2026-08-17: 7170 pulls2026-08-18: 7199 pulls2026-08-19: 7379 pulls2026-08-20: 7386 pulls2026-08-21: 7584 pulls2026-08-22: 7802 pulls2026-08-23: 7816 pulls2026-08-24: 7839 pulls2026-08-25: 7849 pulls2026-08-26: 7989 pulls2026-08-27: 8049 pulls2026-08-28: 8055 pulls2026-08-30: 8768 pulls2026-08-31: 8814 pulls2026-09-01: 8814 pulls
STARS 1

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 oorabona guide →

Security scan results

Trivy · last scan 2026-08-29

Last scan: (advisory mode — Trivy runs continue-on-error in CI; we surface findings, we do not block builds on them).

0Critical
3High
9Medium
18Low
0Info

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:
Build Lineage
Build Digest sha256:bf67f68b646fc753259e3349ef226208f40ec77904032ee6f712358bb797112f
Base Image
OS_VERSION latest
EASYRSA_VERSION 3.2.6
EASYRSA_KEY_FPR 6F4056821152F03B6B24F2FCF8489F839D7367F3
OPENVPN_KEY_FPR F554A3687412CFFEBDEFE0A312F5F7B42F2B01E7
OVPN_SHA256 a1cfb43755aeb0e56d06cdb60be6e6bbf831468414ccbea8cb860729bd8ed451
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 date
1/1 dependencies monitored
Up-to-date dependencies
EASYRSA_VERSION 3.2.6

Documentation

README

🔐 OpenVPN Docker Container built from sources with advanced security features

Docker Image Version (latest semver) Docker Pulls Docker Stars GHCR

This is a simple Alpine based container with OpenVPN built from sources.

Platforms

  • amd64 Docker Image Size AMD64
  • arm64 Docker Image Size ARM64

Features

  • 🔐 Built from sources
  • Dependant library pkcs11-helper from the Alpine repository, signature-verified by apk
  • Embed Google Authenticator support

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) because AUTO_INSTALL generates the PKI and server.conf into /etc/openvpn on first start, and cap_drop: ALL removes the DAC_OVERRIDE/FOWNER/CHOWN capabilities 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-iproute2 option to use the ip command instead of ifconfig
  • --enable-pkcs11 option to enable the pkcs11-helper library and support of PKCS#11 tokens (e.g. Yubikey)
  • --enable-plugin-auth-pam option to enable the pam authentication plugin (e.g. Google Authenticator uses this)
  • --enable-async-push option to allow asynchronous push of configuration options to the client (and not wait for a remote authentification request to be completed)
  • --enable-plugin-down-root option to allow the down-root plugin to be used (e.g. to drop privileges after the connection is established). Although this option is enabled, the down-root plugin is not used by default.
  • --enable-selinux option to enable the selinux support
  • --disable-systemd option to disable the systemd support
  • --disable-debug option to make the binary smaller
  • --disable-lzo and --disable-lz4 options to disable the lzo and lz4 compression support (prone to side-channel attacks)
  • --enable-comp-stub option 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/openvpn directory
  • /etc/openvpn/otp directory
  • /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 rules
  • SETUID / SETGID — let openvpn drop to nobody/nogroup after 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.

References

License

MIT

Other projects