Skip to content
hyperi-ioPublic

About

OpenVPN and WireGuard in one container, with OpenVPN reachable over HTTPS and OIDC sign-in.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

133 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Culvert icon Culvert

OpenVPN and WireGuard in one container, with OpenVPN reachable over HTTPS and OIDC sign-in.

What it does

  • The VPN server that installs as a container: docker run it on one host, or install the Helm chart. In a DFE deployment it is the edge-fleet VPN, the door an appliance dials before it posts to dfe-receiver.
  • OpenVPN on UDP or TCP and WireGuard, on their own or both at once.
  • Either protocol wrapped in TLS on a web port: stunnel for OpenVPN on TCP 443, wstunnel for WireGuard on TCP 4443. A network that lets out HTTPS alone passes the tunnel.
  • OIDC sign-in on OpenVPN with Microsoft Entra ID, Google Workspace, Okta, Keycloak, Auth0 or any other OIDC provider, through openvpn-auth-oauth2. WireGuard uses keys.
  • PKI from file mounts, OpenBao or Vault, or AWS Secrets Manager, with a self-generated Easy-RSA CA when you bring none.
  • TLS 1.3 only, AES-256-GCM, SHA-384 and P-384 on the OpenVPN path, the CNSA 2.0 classical suite (docs/cryptography.md).
  • Prometheus /metrics and OTLP export, /livez and /readyz, a drain on SIGTERM and cgroup-aware resource detection. Images for linux/amd64 and linux/arm64.

Culvert is a classic hub VPN server you run yourself, one image with no management plane and no per-user fees. Coming from something else?

If you use What you gain with Culvert
AWS Client VPN Flat cost of a node instead of per-subnet-association plus per-connection charges. Adds WireGuard, any-IdP OIDC, and the HTTPS-tunnelled listeners. You lose the managed control plane.
kylemanna/docker-openvpn Appears unmaintained since 2020 and its image ships OpenVPN 2.4. Culvert is a maintained alternative: OpenVPN 2.7 with DCO, plus everything above.
angristan/openvpn-install Same result without a host-mutating bash script: the server is an image, config is env vars, upgrades are a docker pull.
OpenVPN Access Server No per-connection licensing. Culvert adds WireGuard and provider-neutral OIDC.
wg-easy Keep the WireGuard simplicity, add OpenVPN for the clients that need it, external PKI, and either protocol over HTTPS. wg-easy's OAuth signs users into its web UI, and Culvert's OIDC gates the VPN session itself.

Quick start

The default is the simplest working server: OpenVPN over UDP, local PKI, nothing else.

docker run -d \
  --cap-add=NET_ADMIN \
  --device=/dev/net/tun \
  -p 1194:1194/udp \
  -p 127.0.0.1:9090:9090/tcp \
  -e CULVERT_SERVER_CN=vpn.yourdomain.example \
  -v culvert-pki:/etc/vpn/pki \
  -v culvert-clients:/etc/vpn/clients \
  ghcr.io/hyperi-io/culvert:latest

Then mint a client and hand it the file it writes into the culvert-clients volume, over a channel you trust, because the .ovpn embeds the private key:

docker exec -it <container> generate-client --name zaphod
docker exec -it <container> generate-client --status   # CURRENT, STALE or UNKNOWN per client
docker exec -it <container> revoke-client zaphod        # access ends here, not when the file goes

The culvert-pki volume holds the CA that signs every client. Back it up and keep it private. One identity covers Zaphod's devices at once: OpenVPN shares the certificate across sessions, and WireGuard gets one peer per device slot (CULVERT_SHARED_CLIENT_SLOTS). The client side, provider by provider, is docs/vpn-client-setup.md.

Everything else is an opt-in with its own port:

Capability Enable with Port
WireGuard (alongside or instead) CULVERT_PROTOCOL=both (or wireguard) 51820/udp
TCP fallback listener CULVERT_TCP_ENABLED=true 1194/tcp
OpenVPN over HTTPS CULVERT_HTTPS_ENABLED=true + stunnel certs 443/tcp
WireGuard over HTTPS CULVERT_WG_HTTPS_TUNNEL_ENABLED=true 4443/tcp
OIDC SSO CULVERT_OAUTH2_ENABLED=true + IdP config 9000-9002/tcp
Prometheus metrics CULVERT_METRICS_ENABLED=true on 9090/tcp

Configuration

Everything is a CULVERT_* environment variable, read through scalo's cascade (CLI, env, .env, settings.{env}.yaml, settings.yaml, defaults.yaml, built-in). .env.example documents the common variables with example values. CULVERT_ORG_NAME=Acme names the self-generated CA Acme VPN CA.

A profile is a YAML of site defaults under profiles/, loaded with CULVERT_PROFILE=<name> (the shipped ones live at /etc/vpn/profiles/ in the image, so the bare name works), and an explicit env var always wins over it. Four shipped profiles have a walkthrough, and example is a placeholder template to copy:

Profile Shape Walkthrough
home OpenVPN UDP, split tunnel, local PKI: reach your home LAN or a lab VM docs/use-case-home.md
corporate Per-user certs, OIDC SSO, group gating, TCP fallback, clean offboarding docs/use-case-corporate.md
travel OpenVPN and WireGuard over HTTPS, for networks that only pass web traffic docs/use-case-travel.md
edge-fleet An appliance fleet streaming to a central receiver, reverse admin down the tunnels, CGNAT addressing docs/use-case-edge-fleet.md

Kubernetes has no profile and its own walkthrough: docs/use-case-kubernetes.md, the chart at oci://ghcr.io/hyperi-io/charts/culvert, one replica, assembled from deploy/deployment-contract.json on the scalo-service library chart.

External PKI keeps the CA key at your CA, so Culvert neither signs nor revokes OpenVPN client certificates. Issue one there, place it at pki/issued/<name>.crt and pki/private/<name>.key, and run generate-client --config-only --name <name>. Revoke at the CA too: the refresh from CULVERT_SECRETS_CRL_PATH ends the session.

Operating it

One listener (CULVERT_METRICS_ADDR, default 0.0.0.0:9090) carries /livez, /readyz and, with CULVERT_METRICS_ENABLED=true, /metrics. OTLP push to :4317 or :4318 comes on with CULVERT_OTEL_*. Point a startupProbe at /livez: Kubernetes suspends liveness until it passes, so one path covers a generous boot and a tight liveness period.

Two metrics earn an alert. vpn_crl_seconds_until_expiry goes negative once the CRL expires, and OpenVPN then refuses every client, valid ones included, with certificate expired. The auto-refresh keeps it topped up, so it moves only when the refresh has been failing, which is also logged at error. vpn_wireguard_endpoint_flaps_total{peer} counts a peer's endpoint returning to an address it left within 10 minutes: a roaming phone moves once and stays, and a key loaded on two devices makes them take turns.

Documentation

The suite's published docs are at https://dfe.docs.hyperi.io, and this repository's pages at https://dfe.docs.hyperi.io/culvert/.

Help and contributing

How to work in this repo, the commands that prove a change and what tends to bite are in CONTRIBUTING.md. Security issues go to the security policy, and community expectations are the code of conduct.

Open bugs: this repo | whole DFE suite

License

Apache-2.0. Third-party components bundled in the container image are listed in NOTICE with their own licences.

About

OpenVPN and WireGuard in one container, with OpenVPN reachable over HTTPS and OIDC sign-in.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages