Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
71abfcc
fix: stop compose and .env.example overriding profiles
catinspace-au Nov 1, 2026
c37351f
fix: hold one lock across WireGuard issue and revoke
catinspace-au Nov 1, 2026
d21f7c5
fix: stop OpenVPN full tunnel leaking IPv6
catinspace-au Nov 1, 2026
f524b39
fix: write OpenVPN status files in the format metrics reads
catinspace-au Nov 1, 2026
27a34a9
fix: replace WireGuard allocations.json atomically
catinspace-au Nov 1, 2026
217aff1
fix: keep client tls-crypt-v2 keys apart from client private keys
catinspace-au Nov 1, 2026
759998c
fix: fail and roll back a WireGuard issue the live interface refuses
catinspace-au Nov 1, 2026
5b6e7a2
fix: validate fetched external PKI as one set before writing any of it
catinspace-au Nov 1, 2026
6c0eb49
fix: never mint a local tls-crypt-v2 key when a shared one is configured
catinspace-au Nov 1, 2026
aaff425
test: wait for the management stand-in before reading its commands
catinspace-au Nov 1, 2026
26e8d29
fix: reload OpenVPN after a CRL refresh only when it revokes somethin…
catinspace-au Nov 1, 2026
695a0c9
fix: refuse OpenVPN client signing and revocation under external PKI
catinspace-au Nov 1, 2026
aef9108
fix: run logrotate on a schedule from the server process
catinspace-au Nov 1, 2026
7735b78
fix: name WireGuard configs so wg-quick and Android accept them
catinspace-au Nov 1, 2026
227a26f
fix: keep wstunnel's connection out of a WireGuard full tunnel over H…
catinspace-au Nov 1, 2026
6cc7245
fix: drain on SIGTERM before stopping the VPN processes
catinspace-au Nov 1, 2026
db98cb9
fix: annotate the registry lock as a Generator
catinspace-au Nov 1, 2026
fb477fd
fix: type the fetched PKI set and clear the docstring and vulture fin…
catinspace-au Nov 1, 2026
2341094
fix: route the wstunnel server outside a WireGuard full tunnel
catinspace-au Nov 1, 2026
c1ba325
fix: require openssl's verify OK before accepting a CRL
catinspace-au Nov 1, 2026
c0f5b33
fix: let the log rotation schedule be stopped
catinspace-au Nov 1, 2026
71ef941
fix: add the blank line the Returns section needs
catinspace-au Nov 1, 2026
9515c83
fix: require a release tag in the generated compose fragment
catinspace-au Nov 1, 2026
da70b7a
test: check the client zip carries the full-tunnel host route
catinspace-au Nov 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 47 additions & 28 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,25 @@
# All configuration uses the CULVERT_* environment variable prefix.
# See README.md for full documentation.
#
# Every CULVERT_* value set here overrides CULVERT_PROFILE, so the settings below
# stay commented at their defaults. Uncomment only what you mean to change.
#
# License: Apache-2.0
# Copyright: (c) 2026 HYPERI PTY LIMITED

#===============================================================================
# Site Profile
#===============================================================================
# Loads profiles/<name>.yaml (home, corporate, travel, mobile, edge-fleet,
# default) as the base for everything below, or a path to your own YAML.
#CULVERT_PROFILE="corporate"

#===============================================================================
# Protocol Selection
#===============================================================================
# VPN protocol to run. Options: openvpn, wireguard, both
# Default: openvpn
CULVERT_PROTOCOL="openvpn"
#CULVERT_PROTOCOL="openvpn"

#===============================================================================
# WireGuard Configuration (only when CULVERT_PROTOCOL includes wireguard)
Expand Down Expand Up @@ -68,16 +78,16 @@ CULVERT_SERVER_CN="vpn.example.com"
# - Optional: per-listener OAuth2 enable/disable

# UDP Listener (primary - best performance; the default)
CULVERT_UDP_ENABLED="true"
CULVERT_UDP_PORT="1194"
CULVERT_UDP_NETWORK="10.8.0.0"
CULVERT_UDP_NETMASK="255.255.255.0"
#CULVERT_UDP_ENABLED="true"
#CULVERT_UDP_PORT="1194"
#CULVERT_UDP_NETWORK="10.8.0.0"
#CULVERT_UDP_NETMASK="255.255.255.0"

# HTTPS Listener: OpenVPN wrapped in TLS on port 443 -- opt-in
CULVERT_HTTPS_ENABLED="false"
CULVERT_HTTPS_PORT="443"
CULVERT_HTTPS_NETWORK="10.8.2.0"
CULVERT_HTTPS_NETMASK="255.255.255.0"
#CULVERT_HTTPS_ENABLED="false"
#CULVERT_HTTPS_PORT="443"
#CULVERT_HTTPS_NETWORK="10.8.2.0"
#CULVERT_HTTPS_NETMASK="255.255.255.0"
# Internal port stunnel forwards decrypted traffic to (localhost only)
#CULVERT_HTTPS_INTERNAL_PORT=1195
# TLS certificate + key for the HTTPS-tunnelled listeners. REQUIRED when
Expand All @@ -89,26 +99,26 @@ CULVERT_HTTPS_NETMASK="255.255.255.0"
#CULVERT_STUNNEL_KEY="/etc/vpn/tls/privkey.pem"

# TCP Listener (proxy fallback) -- opt-in
CULVERT_TCP_ENABLED="false"
CULVERT_TCP_PORT="1194"
CULVERT_TCP_NETWORK="10.8.1.0"
CULVERT_TCP_NETMASK="255.255.255.0"
#CULVERT_TCP_ENABLED="false"
#CULVERT_TCP_PORT="1194"
#CULVERT_TCP_NETWORK="10.8.1.0"
#CULVERT_TCP_NETMASK="255.255.255.0"

#===============================================================================
# DNS and Routing
#===============================================================================
# DNS servers pushed to clients
CULVERT_DNS1="1.1.1.1"
CULVERT_DNS2="1.0.0.1"
#CULVERT_DNS1="1.1.1.1"
#CULVERT_DNS2="1.0.0.1"

# Routing mode
# true = all traffic through VPN (full tunnel)
# false = only VPN network (split tunnel)
CULVERT_FULL_TUNNEL="false"
#CULVERT_FULL_TUNNEL="false"

# Additional routes to push (comma-separated CIDR)
# Example: 10.1.0.0/16,192.168.1.0/24
CULVERT_PUSH_ROUTES=""
#CULVERT_PUSH_ROUTES=""

#===============================================================================
# Routing Control (opt-in FORWARD filtering)
Expand Down Expand Up @@ -148,7 +158,7 @@ CULVERT_PUSH_ROUTES=""
#===============================================================================
# Performance Tuning
#===============================================================================
# Tuning is DATA, carried by a profile YAML -- see CULVERT_PROFILE above and
# Tuning is DATA, carried by a profile YAML -- see Site Profile above and
# profiles/*.yaml. The shipped tuning profiles are:
# default - robust general-purpose baseline (mssfix 1420, OS-auto buffers)
# mobile - 5G / tether / hotel wifi (WireGuard MTU 1280, conservative
Expand Down Expand Up @@ -176,7 +186,7 @@ CULVERT_PUSH_ROUTES=""
# Server limits
# max-clients: "auto" calculates from CPU/memory (formula: min(cores*64, GB*650, 1000))
# Set explicit number to override auto-calculation
CULVERT_MAX_CLIENTS="auto"
#CULVERT_MAX_CLIENTS="auto"
#CULVERT_RENEG_SEC=3600

#===============================================================================
Expand Down Expand Up @@ -206,10 +216,17 @@ CULVERT_MAX_CLIENTS="auto"
# Forced to 1 when CULVERT_ALLOW_SHARED_CLIENTS=false.
#CULVERT_SHARED_CLIENT_SLOTS=2

# Seconds connected clients keep their tunnels after SIGTERM drops readiness,
# so load balancers stop sending new ones first. Ends early with none connected.
# The children then get 15 s, so keep the total inside the stop timeout
# (docker run --stop-timeout, compose stop_grace_period, K8s grace period).
#CULVERT_DRAIN_SECONDS=10

# Logging configuration
# CULVERT_LOG_MODE: "file" (default), "stdout" (K8s/containers), "both"
# Kubernetes/EKS/Rancher: Use "stdout" for container logging pipelines
CULVERT_LOG_MODE="file"
# Files under /var/log/vpn rotate daily or past 100M, and 7 are kept.
#CULVERT_LOG_MODE="file"
#CULVERT_VERB=3
#CULVERT_MUTE=20

Expand All @@ -226,7 +243,7 @@ CULVERT_LOG_MODE="file"
# Access: https://<server>:<port>/ to list files (with the token).

# Enable/disable client download server
CULVERT_CLIENT_DOWNLOAD_ENABLED="false"
#CULVERT_CLIENT_DOWNLOAD_ENABLED="false"

# Bearer token - REQUIRED when enabled (generate: openssl rand -hex 32)
#CULVERT_CLIENT_DOWNLOAD_TOKEN=
Expand Down Expand Up @@ -268,7 +285,7 @@ CULVERT_LOGS_PATH="./logs"
# external secrets backend via CULVERT_SECRETS_PROVIDER.
# Best for: production Kubernetes with enterprise PKI.

CULVERT_PKI_MODE="local"
#CULVERT_PKI_MODE="local"

#-------------------------------------------------------------------------------
# LOCAL PKI Settings (CULVERT_PKI_MODE=local)
Expand All @@ -282,11 +299,11 @@ CULVERT_PKI_MODE="local"
#CULVERT_CA_CN="Culvert CA"

# Key type: ec (ECDSA - recommended) or rsa
CULVERT_KEY_TYPE="ec"
#CULVERT_KEY_TYPE="ec"

# For EC: secp384r1 (CNSA 2.0), secp256r1
# For RSA: 4096 (CNSA 2.0), 2048
CULVERT_KEY_SIZE="secp384r1"
#CULVERT_KEY_SIZE="secp384r1"

# OpenVPN data-channel cipher order (NCP). AES-256-GCM first is the CNSA 2.0 /
# FIPS-compliant default. Reorder to prefer ChaCha20-Poly1305 for AES-NI-less
Expand Down Expand Up @@ -512,7 +529,7 @@ CULVERT_KEY_SIZE="secp384r1"
# For example: HTTPS requires OAuth2, UDP is cert-only for automation.

# Global OAuth2 toggle (default for all listeners if per-listener not set)
CULVERT_OAUTH2_ENABLED="false"
#CULVERT_OAUTH2_ENABLED="false"

# Per-listener OAuth2 enable/disable (defaults to CULVERT_OAUTH2_ENABLED if not set)
# Set to false to use certificate-only auth on specific listeners
Expand Down Expand Up @@ -549,16 +566,18 @@ CULVERT_OAUTH2_TLS_KEY="/etc/vpn/oauth2-tls/privkey.pem"

# Group-based access control
# Only members of these groups can connect (comma-separated)
# Use your IdP's group identifiers (GUIDs for Entra ID, names for others)
CULVERT_OAUTH2_VALIDATE_GROUPS=""
# Use your IdP's group identifiers (GUIDs for Entra ID, names for others).
# Left unset, any authenticated user connects (the corporate profile gates on
# "vpn-users"). An empty value set here clears a profile's gate.
#CULVERT_OAUTH2_VALIDATE_GROUPS="vpn-users"

# Session secret (generate with: openssl rand -hex 16)
CULVERT_OAUTH2_HTTP_SECRET="GENERATE_ME_openssl_rand_hex_16"

# OIDC scopes. Default matches the code (openid,profile,email). Add
# offline_access for IdPs that issue refresh tokens through it (e.g.
# Entra ID); some IdPs reject unknown scopes, so it is not the default.
CULVERT_OAUTH2_SCOPES="openid,profile,email"
#CULVERT_OAUTH2_SCOPES="openid,profile,email"

#===============================================================================
# Container Image (REQUIRED by docker-compose.yaml)
Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ That mints a real credential, so treat the output accordingly:

One identity covers Zaphod's devices concurrently, laptop and phone both
connected. OpenVPN shares the certificate across sessions; WireGuard cannot
share a key, so alice gets one peer per device slot (`zaphod-wg-split.conf`,
`zaphod-wg2-split.conf`) - `CULVERT_SHARED_CLIENT_SLOTS` sets how many. Set
share a key, so Zaphod gets one peer per device slot (`zaphod/wg-split.conf`,
`zaphod/wg2-split.conf`) - `CULVERT_SHARED_CLIENT_SLOTS` sets how many. Set
`CULVERT_ALLOW_SHARED_CLIENTS=false` where a credential must identify a single
device.

Expand Down Expand Up @@ -212,6 +212,12 @@ culvert supports three external PKI backends via
Local PKI mode (self-generated Easy-RSA CA) is the default for quick
starts and dev environments.

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. WireGuard peers work as in local mode.

## Cryptography and CNSA 2.0

The OpenVPN path ships the CNSA 2.0 *classical* suite by default, aligned
Expand Down
1 change: 1 addition & 0 deletions config/server-https.conf.template
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ reneg-sec ${OPENVPN_RENEG_SEC}
# Logging
#===============================================================================
status /var/log/vpn/status-https.log 10
status-version 3
log-append /var/log/vpn/openvpn-https.log
verb ${OPENVPN_VERB}
mute ${OPENVPN_MUTE}
Expand Down
1 change: 1 addition & 0 deletions config/server-tcp.conf.template
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ reneg-sec ${OPENVPN_RENEG_SEC}
# Logging
#===============================================================================
status /var/log/vpn/status-tcp.log 10
status-version 3
log-append /var/log/vpn/openvpn-tcp.log
verb ${OPENVPN_VERB}
mute ${OPENVPN_MUTE}
Expand Down
1 change: 1 addition & 0 deletions config/server.conf.template
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ reneg-sec ${OPENVPN_RENEG_SEC}
# Logging
#===============================================================================
status /var/log/vpn/status.log 10
status-version 3
log-append /var/log/vpn/openvpn.log
verb ${OPENVPN_VERB}
mute ${OPENVPN_MUTE}
Expand Down
2 changes: 1 addition & 1 deletion deploy/compose/culvert.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
# Generated by scalo deployment module
services:
culvert:
image: ghcr.io/hyperi-io/culvert:${CULVERT_VERSION:-latest}
image: ghcr.io/hyperi-io/culvert:${CULVERT_VERSION:?set CULVERT_VERSION to the culvert release to run}
expose:
- "9090"
ports:
Expand Down
Loading
Loading