diff --git a/.art/openvpn-19.0-landing.png b/.art/openvpn-19.0-landing.png new file mode 100644 index 0000000..c07ea99 Binary files /dev/null and b/.art/openvpn-19.0-landing.png differ diff --git a/.art/openvpn-19.0-qr-code.png b/.art/openvpn-19.0-qr-code.png new file mode 100644 index 0000000..753f575 Binary files /dev/null and b/.art/openvpn-19.0-qr-code.png differ diff --git a/.art/openvpn-19.0-quick-reference.png b/.art/openvpn-19.0-quick-reference.png new file mode 100644 index 0000000..af38cef Binary files /dev/null and b/.art/openvpn-19.0-quick-reference.png differ diff --git a/Makefile b/Makefile index 6da5990..41858b5 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,5 @@ WEBMIN_FW_TCP_INCOMING = 22 80 443 12321 WEBMIN_FW_UDP_INCOMING = 1194 -WEBMIN_FW_NAT_EXTRA = -A POSTROUTING -o eth0 -j MASQUERADE COMMON_OVERLAYS = tkl-webcp timezone COMMON_CONF = tkl-webcp diff --git a/changelog b/changelog index 1a37b2a..0539e3a 100644 --- a/changelog +++ b/changelog @@ -2,7 +2,19 @@ turnkey-openvpn-19.0 (1) turnkey; urgency=low * Install OpenVPN from Debian repos - v2.6.14. - * Fix missing Cipher/Auth in server.conf - closes #2064. + * Fix (and updated deprecated) missing Cipher/Auth in server.conf - closes + #2064. + + * Update/modernize other default OpenVPN config, including increased key + size. A future release will likely move to eliptic curve algorithm; e.g. + ecp384r1. + + * Move masquerade firewall config to systemd service rather than Webmin. + Allows separation of default/shared TurnKey firewall rules (disabled by + default as per all other apps) and OpenVPN specific conf. + + * Include IPv6 notes/docs in build code repo (openvpn/docs/ipv6.md). A + future TurnKey release will ship with them pre-enabled/pre-applied. * Upgraded base distribution to Debian 13.x/Trixie. diff --git a/conf.d/main b/conf.d/main index 1f6fc05..7a9ce31 100755 --- a/conf.d/main +++ b/conf.d/main @@ -3,9 +3,8 @@ EASY_RSA=/etc/openvpn/easy-rsa HTDOCS=/var/www/openvpn/htdocs OPENSSL_CONF=$EASY_RSA/openssl-easyrsa.cnf -SRC=/usr/local/src -# enable ip forwarding +# enable ipv4 forwarding echo 'net.ipv4.ip_forward=1' >> /etc/sysctl.d/40-openvpn.conf mkdir -p $EASY_RSA @@ -36,5 +35,5 @@ systemctl enable openvpn@server # enable openvpn-tun service (creates /dev/net/tun device in containers - closes #1011) systemctl enable openvpn-tun.service -echo 'cipher AES-256-GCM' >> /etc/openvpn/server.conf -echo 'auth SHA512' >> /etc/openvpn/server.conf +# enable masquerade firewall rule service for OpenVPN LAN reachback +systemctl enable openvpn-masquerade.service diff --git a/docs/ipv6.md b/docs/ipv6.md new file mode 100644 index 0000000..e54c60e --- /dev/null +++ b/docs/ipv6.md @@ -0,0 +1,96 @@ +OpenVPN IPv6 Configuration - OpenVPN v2.6+ +========================================== + +Inclusion of some updated IPv6 default config are intended for a future TurnKey +release. In the meantime here are some (untested) notes. Please confirm their +validity before rolling out for production. Also please provide feedback if you +try them out. + +OpenVPN config +============== + +Config file: `/etc/openvpn/server.conf` + +Protocol +-------- + +To set IPv6 as the preferred (UDP) listening address for clients to connect to, +append '6' to the existing `proto udp` config line. I.e. so it looks like this: + +``` +proto udp6 +``` + +The above will fall back to IPv4 when IPv6 is not available. The current +default (`proto udp`) will do the opposite. I.e. default to IPv4 & fallback to +IPv6 if IPv4 is not available. + +IPv6 Tunnel Interface +--------------------- + +To assign clients IPv6 addresses **within** the tunnel, _add_ these lines: + +``` +tun-ipv6 +push "tun-ipv6" +``` + +Push IPv6 Routes to Clients +--------------------------- + +Examples: + +Redirect all client IPv6 traffic through the VPN: + +``` +push "redirect-gateway ipv6" +``` + +Route public IPv6 traffic through the tunnel: +``` +push "route-ipv6 2000::/3" +``` + +Route the VPN's own IPv6 subnet: +``` +push "route-ipv6 fd42:42:42::/112" +``` + +Redirect _all_ IPv6 traffic through the VPN: +``` +push "redirect-gateway ipv6" +``` + +DNS +--- + +Push an IPv6-capable DNS server to clients. E.g. Cloudflare DNS: + +``` +push "dhcp-option DNS 2606:4700:4700::1111" +``` + +Host server system config +========================= + +IPv6 Forwarding +--------------- + +Enable IPv6 network forwarding: + +``` +echo 'net.ipv6.ip_forward=1' >> /etc/sysctl.d/40-openvpn.conf +``` + +And reboot. To check that it has been applied: + +``` +sysctl net.ipv6.conf.all.forwarding +``` + +Firewall - ip6tables +-------------------- + +The default TurnKey firewall rules should already be appropriate for IPv6. +Currently TurnKey still uses iptables config (legacy wrapper around nftables) +via Webmin. You can inspect the rules via Webmin, or `/etc/iptables/rules.v6`. diff --git a/overlay/etc/systemd/system/openvpn-masquerade.service b/overlay/etc/systemd/system/openvpn-masquerade.service new file mode 100644 index 0000000..2a92ce3 --- /dev/null +++ b/overlay/etc/systemd/system/openvpn-masquerade.service @@ -0,0 +1,13 @@ +[Unit] +Description=OpenVPN LAN masquerade firewall conf for VPN reachback +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +ExecStart=/sbin/iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE +ExecStop=/sbin/iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target diff --git a/overlay/usr/lib/inithooks/bin/openvpn-server-init.sh b/overlay/usr/lib/inithooks/bin/openvpn-server-init.sh index 7860d9e..103b76b 100755 --- a/overlay/usr/lib/inithooks/bin/openvpn-server-init.sh +++ b/overlay/usr/lib/inithooks/bin/openvpn-server-init.sh @@ -50,7 +50,7 @@ KEY_NAME="${KEY_NAME:-openvpn}" KEY_COUNTRY="${KEY_COUNTRY:-US}" KEY_PROVINCE="${KEY_PROVINCE:-CA}" KEY_CITY="${KEY_CITY:-San Francisco}" -KEY_SIZE="${KEY_SIZE:-2048}" +KEY_SIZE="${KEY_SIZE:-3072}" KEY_EXPIRE="${KEY_EXPIRE:-3650}" CA_EXPIRE="${CA_EXPIRE:-3650}" @@ -64,7 +64,7 @@ export EASYRSA_PKI="$EASYRSA/keys" export EASYRSA_CERT_EXPIRE="$KEY_EXPIRE" export EASYRSA_KEY_SIZE=$KEY_SIZE export EASYRSA_DN=cn_only -export EASYRSA_REQ_COUNTRY="$KEY_COUNTRY" +export EASYRSA_CA_EXPIRE="$CA_EXPIRE" export EASYRSA_REQ_ORG="$KEY_ORG" export EASYRSA_REQ_OU="$KEY_OU" export EASYRSA_REQ_NAME="$KEY_NAME" @@ -102,8 +102,7 @@ mkdir -p "$SERVER_CCD" # generate ca and server keys/certs export EASYRSA_BATCH=1 -$EASYRSA/easyrsa init-pki soft-reset -$EASYRSA/easyrsa gen-dh +$EASYRSA/easyrsa init-pki $EASYRSA/easyrsa --req-cn='server' build-ca nopass $EASYRSA/easyrsa gen-req server nopass $EASYRSA/easyrsa sign-req server server @@ -142,7 +141,7 @@ chroot $EASYRSA_PKI/crl.jail crl-verify /etc/openvpn/crl.pem ca $EASYRSA_PKI/ca.crt -dh $EASYRSA_PKI/dh.pem +dh none tls-auth $EASYRSA_PKI/ta.key 0 key $EASYRSA_PKI/private/server.key cert $EASYRSA_PKI/issued/server.crt @@ -157,6 +156,25 @@ verb 4 # important: must not be used on your network server $(expand_cidr "$virtual_subnet") -cipher AES-256-GCM +data-ciphers AES-256-GCM:AES-128-GCM:CHACHA20-POLY1305 auth SHA512 + +# tell clients when the server restarts or exits - clients reconnect quicker +# (don't need to wait for keepalive timeout) +explicit-exit-notify 1 + +# recommended modern topology puts all clients on a shared subnet - like a +# normal LAN. OpenVPN v2.7+ default - clients must be v2.0.9+. +topology subnet +# legacy topology hands every client its own /30 (4 addresses each). To revert +# to default v2.6 and earlier behavior comment above line and uncomment below. +#topology net30 + +# uncomment and set DNS server IP - e.g. below uses Cloudflare (1.1.1.1). +# Note by default OpenVPN pushes the server's configured nameserver. +#push "dhcp-option DNS 1.1.1.1" + +# uncomment if all clients support TLSv1.3 +#tls-version-min 1.3 + EOF diff --git a/overlay/usr/lib/inithooks/bin/openvpn.py b/overlay/usr/lib/inithooks/bin/openvpn.py index 131a685..d34bac1 100755 --- a/overlay/usr/lib/inithooks/bin/openvpn.py +++ b/overlay/usr/lib/inithooks/bin/openvpn.py @@ -1,5 +1,5 @@ #!/usr/bin/python3 -"""Initialize OpenVPN easy-rsa, server keys and configuration +"""Initialize OpenVPN easy-rsa, server keys and configuration. Options: @@ -21,29 +21,38 @@ Note: options not specified but required by profile will be asked interactively """ +# ruff: noqa: C901, CPY001, D103, PLR0912, PLR0915, PTH110, PTH118, PTH120 import getopt -import os import subprocess import sys -from os.path import exists +from os.path import dirname, exists, join from random import randint as r +from typing import NoReturn from libinithooks import info, inithooks_cache, is_interactive, warn from libinithooks.dialog_wrapper import Dialog +TUN_CONTAINER_MSG = """\ +Failed to create `/dev/net/tun` device on boot. -def fatal(e): +If this server is an unprivileged container, you will need to create the tun \ +device on the host system.""" + + +def fatal(e: str) -> NoReturn: print("Error:", e, file=sys.stderr) sys.exit(1) -def usage(e=None): + +def usage(e: str | getopt.GetoptError | None = None) -> None: if e: print("Error:", e, file=sys.stderr) print(f"Syntax: {sys.argv[0]} [options]", file=sys.stderr) print(__doc__, file=sys.stderr) sys.exit(1) -def expand_cidr(cidr): + +def expand_cidr(cidr: str) -> str: network, bitcount = cidr.split("/") # turn / into a 32-long bit array bits = ("1" * int(bitcount)).ljust(32, "0") @@ -57,11 +66,21 @@ def expand_cidr(cidr): return "{} {}.{}.{}.{}".format(network, *bytes_list) -def main(): + +def main() -> None: try: - opts, args = getopt.gnu_getopt(sys.argv[1:], "h", - ["help", "profile=", "key-email=", "public-address=", "virtual-subnet=", - "private-subnet="]) + opts, _args = getopt.gnu_getopt( + sys.argv[1:], + "h", + [ + "help", + "profile=", + "key-email=", + "public-address=", + "virtual-subnet=", + "private-subnet=", + ], + ) except getopt.GetoptError as e: usage(e) @@ -89,27 +108,23 @@ def main(): tun_exists = exists("/dev/net/tun") if not tun_exists: if is_interactive: - dialog.msgbox("Tun device not created", """ -Failed to create `/dev/net/tun` device on boot, this is expected when running inside a non-privileged container. - -If you are running on an unprivileged container, you will need to create this device on the host.""") + dialog.msgbox("Tun device not created", TUN_CONTAINER_MSG) else: - warn("Failed to create `/dev/net/tun` device on boot, this is expected when " - "running inside a non-privileged container. If you are " - "running on an unprivileged container, you will need to " - "create this device on the host.") + warn(TUN_CONTAINER_MSG) else: info("/dev/net/tun created successfully") if not profile: profile = dialog.menu( "OpenVPN Profile", - "Choose a profile for this server.\n\n* Gateway: clients will be configured to route all\n their traffic through the VPN.", + "Choose a profile for this server.\n\n" + "* Gateway: clients will route all traffic through the VPN.", [ ("server", "Accept VPN connections from clients"), ("gateway", "Accept VPN connections from clients*"), ("client", "Initiate VPN connections to a server"), - ]) + ], + ) if profile not in ("server", "gateway", "client"): fatal(f"invalid profile: {profile}") @@ -121,7 +136,8 @@ def main(): key_email = dialog.get_email( "OpenVPN Email", "Enter email address for the OpenVPN server key.", - "admin@example.com") + "admin@example.com", + ) inithooks_cache.write("APP_EMAIL", key_email) @@ -129,42 +145,67 @@ def main(): public_address = dialog.get_input( "OpenVPN Public Address", "Enter FQDN or IP address of server reachable by clients", - "vpn.example.com") + "vpn.example.com", + ) - auto_virtual_subnet = f"10.{r(2, 254)}.{r(2, 254)}.0/24" + # disable 'pseudo-random generator not suitable for crypto rule [S311]' + # pseudo-random generator only used for subnet generation + auto_virtual_subnet = f"10.{r(2, 254)}.{r(2, 254)}.0/24" # noqa: S311 if not virtual_subnet: virtual_subnet = dialog.get_input( "OpenVPN Virtual Subnet", - "Enter CIDR subnet address pool to allocate to clients. This server will be configured with x.x.x.1. The CIDR must not be in-use on your network.", - auto_virtual_subnet) + "Enter CIDR subnet address pool to allocate to clients. This" + " server will be configured with x.x.x.1. The CIDR must not be" + " in-use on your network.", + auto_virtual_subnet, + ) if virtual_subnet.upper() == "AUTO": virtual_subnet = auto_virtual_subnet - if profile == "server": - if not private_subnet: - retcode, private_subnet = dialog.inputbox( - "OpenVPN Private Subnet", - "Enter CIDR subnet behind server for clients to reach.", - "10.0.1.0/24", "Apply", "Skip") - - if private_subnet.upper() == "SKIP": + if profile == "server" and not private_subnet: + retcode, private_subnet = dialog.inputbox( + "OpenVPN Private Subnet", + "Enter CIDR subnet behind server for clients to reach.", + "10.0.1.0/24", + "Apply", + "Skip", + ) + + # retcode is one of 'ok' ("Apply") or 'cancel' ("Skip") + if retcode == "cancel": private_subnet = "" - cmd = os.path.join(os.path.dirname(__file__), "openvpn-server-init.sh") - subprocess.run([cmd, key_email, public_address, virtual_subnet]) + cmd = join(dirname(__file__), "openvpn-server-init.sh") + # subprocess command is safe in this case + subprocess.run( # noqa: S603 + [cmd, key_email, public_address, virtual_subnet], check=False, + ) if profile == "gateway": with open("/etc/openvpn/server.conf", "a") as fob: - fob.write("# configure clients to route all their traffic through the vpn\n") + fob.write( + "# configure clients to route all their traffic through the" + " vpn\n", + ) fob.write('push "redirect-gateway def1 bypass-dhcp"\n\n') if private_subnet: with open("/etc/openvpn/server.conf", "a") as fob: - fob.write("# push routes to clients to allow them to reach private subnets\n") - fob.writelines(f'push "route {expand_cidr(_private_subnet)}"\n' for _private_subnet in private_subnet.split(",")) - subprocess.run(["systemctl", "start", "openvpn@server"]) + fob.write( + "# push routes to clients to allow them to reach private" + " subnets\n", + ) + fob.writelines( + ( + f'push "route {expand_cidr(_private_subnet)}"\n' + for _private_subnet in private_subnet.split(",") + ), + ) + subprocess.run( + ["/usr/bin/systemctl", "restart", "openvpn@server"], check=False, + ) + if __name__ == "__main__": main() - diff --git a/overlay/usr/lib/inithooks/firstboot.d/40openvpn b/overlay/usr/lib/inithooks/firstboot.d/40openvpn index 1251b9c..ab6da58 100755 --- a/overlay/usr/lib/inithooks/firstboot.d/40openvpn +++ b/overlay/usr/lib/inithooks/firstboot.d/40openvpn @@ -1,12 +1,21 @@ #!/bin/bash -e # initialize openvpn easy-rsa, server keys and configuration -. /etc/default/inithooks +# shellcheck source=/dev/null +source /etc/default/inithooks -[ -e $INITHOOKS_CONF ] && . $INITHOOKS_CONF +if [[ -e $INITHOOKS_CONF ]]; then + source "$INITHOOKS_CONF" +fi # skip initialization if headless (will be configured by turnkey-init) -[ "$APP_DOMAIN" == "DEFAULT" ] && exit 0 - -$INITHOOKS_PATH/bin/openvpn.py --profile="$APP_PROFILE" --public-address="$APP_DOMAIN" --virtual-subnet="$APP_VIRTUAL_SUBNET" --private-subnet="$APP_PRIVATE_SUBNET" --key-email="$APP_EMAIL" +if [[ "$APP_DOMAIN" == "DEFAULT" ]]; then + exit 0 +fi +"$INITHOOKS_PATH/bin/openvpn.py" \ + --profile="$APP_PROFILE" \ + --public-address="$APP_DOMAIN" \ + --virtual-subnet="$APP_VIRTUAL_SUBNET" \ + --private-subnet="$APP_PRIVATE_SUBNET" \ + --key-email="$APP_EMAIL" diff --git a/overlay/var/www/openvpn/htdocs/index.html b/overlay/var/www/openvpn/htdocs/index.html index 95f61b7..6501ab8 100644 --- a/overlay/var/www/openvpn/htdocs/index.html +++ b/overlay/var/www/openvpn/htdocs/index.html @@ -49,11 +49,14 @@

TurnKey OpenVPN

Resources and references