Back to Dashboard

openvpn

v2.7.8-alpine

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

Trust posture

📋 SBOM ATTESTED🛡 0 open alerts🏗 AMD64 + ARM64 🔗 1/1 deps tracked daily 🔍 REPRODUCE THESE CHECKS LOCALLY
PULLS 15.7K 9656 to 15704 pulls across 30 recorded days 2026-09-18: 9656 pulls2026-09-19: 10100 pulls2026-09-20: 11235 pulls2026-09-21: 11897 pulls2026-09-22: 11947 pulls2026-09-23: 11975 pulls2026-09-24: 11988 pulls2026-09-25: 12740 pulls2026-09-26: 13166 pulls2026-09-27: 13180 pulls2026-09-28: 13405 pulls2026-09-29: 13605 pulls2026-09-30: 13856 pulls2026-10-01: 14129 pulls2026-10-02: 14414 pulls2026-10-03: 14617 pulls2026-10-04: 14651 pulls2026-10-05: 14669 pulls2026-10-06: 14674 pulls2026-10-07: 15070 pulls2026-10-08: 15169 pulls2026-10-09: 15644 pulls2026-10-10: 15665 pulls2026-10-11: 15665 pulls2026-10-12: 15666 pulls2026-10-13: 15673 pulls2026-10-14: 15673 pulls2026-10-15: 15683 pulls2026-10-16: 15700 pulls2026-10-17: 15704 pulls
STARS 1

VERIFIABLE TRUST ARTIFACTS

Provenance

Build commit
43b72a4
Build digest
sha256:ef8b48705fed563c801650834ea6c327c91c5cd0f02e5e472e001f21f961d8dc
Index digest
sha256:f1561c28bf7a1f619ad8f1bf849c0c815539bc7559de9e5da617fc63b5a27735
Manifest digest (amd64)
sha256:d153188ee72edfb5b0df6fd557dad08cd884f5bfb72e27405dfb5fa77a3a9a24
Manifest digest (arm64)
sha256:b1409edaf36364a3cb230830cf65d0c24fd3d7848ad3e4d3c7b178cd02b2b54d
SBOM attestation
54047896…
Trivy evidence
Code Scanning fetched 2026-10-17 11:24 UTC
Base image
ghcr.io/oorabona/library/alpine:latest
Verify
gh attestation verify oci://ghcr.io/oorabona/openvpn:v2.7.8-alpine --owner oorabona guide →

Security evidence

Trivy · Code Scanning for image v2.7.8-alpine fetched 2026-10-17

0 open Code Scanning alerts · fetched 2026-10-17 · advisory mode (does not block builds)

Code Scanning: 0 open alerts · fetched 2026-10-17Recorded scan: 0 findings · scanned 2026-10-08
0Critical
0High
0Medium
0Low
0Info

No open Code Scanning alerts as of 2026-10-17.

Top advisories (upstream, advisory)

    → See verification steps.

    Explore

    Build lineageef8b48705fed
    Build Lineage
    Build Digest sha256:ef8b48705fed563c801650834ea6c327c91c5cd0f02e5e472e001f21f961d8dc
    Base Image ghcr.io/oorabona/library/alpine:latest
    OS_VERSION latest
    EASYRSA_VERSION 3.2.7
    EASYRSA_KEY_FPR 6F4056821152F03B6B24F2FCF8489F839D7367F3
    OPENVPN_KEY_FPR F554A3687412CFFEBDEFE0A312F5F7B42F2B01E7
    OVPN_SHA256 a1cfb43755aeb0e56d06cdb60be6e6bbf831468414ccbea8cb860729bd8ed451
    VERSION v2.7.8-alpine
    UPSTREAM_VERSION v2.7.8
    ARTIFACT_URL https://github.com/OpenVPN/openvpn/releases/download/v2.7.8/openvpn-2.7.8.tar.gz
    ARTIFACT_SIGNATURE_URL https://github.com/OpenVPN/openvpn/releases/download/v2.7.8/openvpn-2.7.8.tar.gz.asc
    REMOTE_CR ghcr.io/oorabona
    NPROC 1
    Package summary 57 packages
    Recent changes
    Build history 3 builds
    Dependency health all up to date

    Dependency Health

    all up to date
    1/1 dependencies monitored
    Up-to-date dependencies
    EASYRSA_VERSION 3.2.7

    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 pinned in config.yaml 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 build_args.EASYRSA_VERSION in config.yaml 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