From 45d73abe7bf6f6bd51e1f8e2240b4d9b37e98014 Mon Sep 17 00:00:00 2001 From: Slawomir Koszewski Date: Sun, 25 Oct 2026 12:25:57 +0100 Subject: [PATCH] feature: Caddy serves the status page and the Router Manager over HTTPS with a certificate from Let's Encrypt. --- ADR.md | 35 ++++++++++++- README.md | 42 ++++++++++++++-- debian-package/debian/changelog | 23 +++++++++ debian-package/debian/control | 3 +- debian-package/debian/install | 1 + debian-package/debian/postrm | 5 +- debian-package/debian/prerm | 8 +-- .../system/caddy.service.d/vpn-router.conf | 10 ++++ debian-package/src/usr/lib/vpn-router/setup | 49 +++++++++++++++++++ .../src/usr/lib/vpn-router/vpnrouter.py | 34 ++++++++++++- .../src/usr/lib/vpn-router/vpnrouter_net.py | 13 +++-- .../doc/vpn-router/vpn-router.json.example | 6 +++ .../share/vpn-router/templates/Caddyfile.j2 | 17 +++++++ 13 files changed, 226 insertions(+), 20 deletions(-) create mode 100644 debian-package/src/usr/lib/systemd/system/caddy.service.d/vpn-router.conf create mode 100644 debian-package/src/usr/share/vpn-router/templates/Caddyfile.j2 diff --git a/ADR.md b/ADR.md index cad46d8..62e3220 100644 --- a/ADR.md +++ b/ADR.md @@ -49,8 +49,9 @@ port on both interfaces in UFW, so the choice of `web.listen` is made by the ser writes the features and the facts it resolves to `/run/vpn-router/web.json`. Consequences: By default the service is reachable from the internet on the external address; -`web.listen` set to `internal` keeps it to the internal network. It serves plain HTTP, so the -passwords of `/auth` and the tokens cross the internet unencrypted. A change of `web.listen` +`web.listen` set to `internal` keeps it to the internal network. It serves plain HTTP itself; +HTTPS is Caddy's (ADR-21), so on the plain HTTP port the passwords of `/auth` and the tokens +cross the internet unencrypted. A change of `web.listen` needs no run of setup, since the firewall does not depend on it; the package's upgrade runs setup, which writes the rules. `/` and `/status` have no authentication, which is acceptable because they are read only and show no configuration. It runs as root, because `swanctl` and `wg` need it, with `NoNewPrivileges`, @@ -486,3 +487,33 @@ Consequences: slapd's `otp` overlay accepts each code once, so a change right af needs the next code. Changing the password revokes the user's refresh tokens, so the user's Manager session ends with its access token. An enrolled user who has lost the authenticator needs an administrator. + +## ADR-21: Caddy from the Ubuntu archive serves HTTPS with a certificate from Let's Encrypt + +Status: Accepted + +Context: The status web service and the Router Manager are reachable from the internet (ADR-2), +and `/auth` and the Manager's sign-in carry passwords. Both are small servers on the standard +library's `http.server`, which would need certificates, their renewal and a TLS setup of their +own. Browsers trust a public CA, not the router's. + +Decision: Caddy, the `caddy` package of the Ubuntu archive, is the HTTPS proxy. While +`https.enabled` is `true` (the default) and `wan.local_fqdn` is set, `vpn-router-setup` writes +`/etc/caddy/vpn-router.Caddyfile`: a site for `wan.local_fqdn` on `https.web_port` (7444) for the +web service and, while the Router Manager is installed, one on `https.manager_port` (7443) for +the manager, each passing calls to the service on `127.0.0.1`. Caddy obtains and renews the +certificate from Let's Encrypt with the HTTP challenge on port 80, which the firewall opens on +the external interface; `auto_https disable_redirects` keeps port 80 to the challenge. The +package's drop-in for `caddy.service` points it at that file and keeps it from starting before +the file exists, so `/etc/caddy/Caddyfile`, a configuration file of the `caddy` package, is left +unchanged. Both services always listen on `127.0.0.1` as well as on the interfaces their +`listen` settings name, and trust `X-Forwarded-For` from it, which Caddy sets from the peer and +never takes from the client. + +Consequences: HTTPS needs `wan.local_fqdn` in public DNS pointing at the router and port 80 +reachable from the internet; until then Caddy retries and the HTTPS ports do not answer. The +certificate and the ACME account are in `/var/lib/caddy`. Plain HTTP stays as the `listen` +settings set it, so `internal` keeps passwords off the internet. Caddy's admin endpoint listens +on `localhost:2019`, which `caddy reload` uses. Port 80 and the HTTPS ports are not available +to Docker containers while HTTPS is on. The Manager's HTTPS port is a setting of the router, not +of the manager. diff --git a/README.md b/README.md index d391340..816c5e6 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,10 @@ must have the type shown below; an unknown section or setting is an error. | `web` | `port` | integer | `7081` | TCP port of the status web service | | `web` | `database` | string | | A PostgreSQL connection string for the refresh tokens; without it, the `vpn-router` database on this host that the web service creates | | `web` | `trusted_proxies` | list | the address the call came in on | Addresses whose `X-Forwarded-For` header names the client for the `/auth` rate limit | +| `https` | `enabled` | boolean | `true` | Caddy serves the status web service and the Router Manager over HTTPS for `wan.local_fqdn` | +| `https` | `email` | string | | The e-mail address of the Let's Encrypt account | +| `https` | `web_port` | integer | `7444` | HTTPS port of the status web service | +| `https` | `manager_port` | integer | `7443` | HTTPS port of the Router Manager | A minimal site-to-site configuration in `auto` mode: @@ -494,8 +498,8 @@ modules shipped with the package; the configuration file accepts any module inst ## Status web service The `vpn-router-web` service shows the state of the router on the addresses of the interfaces -`web.listen` names, the internal, the external or both (the default), and `web.port` (7081 by -default). Its pages `/` and `/status` need no authentication and show no +`web.listen` names, the internal, the external or both (the default), and on `127.0.0.1`, where +Caddy calls it (see HTTPS), with `web.port` (7081 by default). Its pages `/` and `/status` need no authentication and show no configuration: no key, identity, address or subnet. The service first makes its signing key and the refresh tokens' database, retrying every 15 @@ -691,20 +695,47 @@ and the facts for `/config` from `/run/vpn-router/web.json` (readable by root on `vpn-router-setup` writes; before that file exists the page shows no features. `vpn-router-setup` opens `web.port` on the internal and the external interface in the firewall. +## HTTPS + +Caddy, from Ubuntu's `caddy` package, serves the status web service on `https.web_port` (7444 by +default) and, while the Router Manager is installed, the manager on `https.manager_port` (7443), +both for `wan.local_fqdn`, while `https.enabled` is `true` and `wan.local_fqdn` is set. It obtains +and renews the certificate from Let's Encrypt with the HTTP challenge, so `wan.local_fqdn` must +resolve to the router's public address and port 80 must be reachable from the internet; the +certificate and the ACME account are kept in `/var/lib/caddy`. Port 80 answers the challenge only; +it does not redirect to HTTPS. + +`vpn-router-setup` writes Caddy's configuration to `/etc/caddy/vpn-router.Caddyfile` (root, group +`caddy`, the group Caddy runs as, mode 0640), which the +package's drop-in `/usr/lib/systemd/system/caddy.service.d/vpn-router.conf` makes `caddy.service` +read; `caddy.service` does not start before the file exists, and `/etc/caddy/Caddyfile` is not +used. Setup starts Caddy, reloads it when the file changes, and stops it and removes the file when +HTTPS is off. In the firewall it opens port 80 on the external interface and the HTTPS ports on the +internal and the external interface. Caddy calls both services on `127.0.0.1`, where they listen +whatever `web.listen` and the manager's `listen` say, and passes the client's address in +`X-Forwarded-For`, which the manager trusts from `127.0.0.1` and the web service by default, as +the address the call came in on; a `web.trusted_proxies` that is set must list `127.0.0.1`. The plain HTTP ports stay as +`web.listen` and the manager's `listen` set them. `docker.exposed_ports` may not take port 80 or +the HTTPS ports while `https.enabled` is `true`. + +```bash +curl https://router.example.com:7444/status +``` + ## Router Manager The `vpn-router-manager` package, built in the repository `linux-cloud-router-manager`, is a web application that shows the state of the router and manages its users through the status web service. It can be installed on the router or on another Linux system. While it is installed on the router, `vpn-router-setup` opens its port (`port` in `/etc/vpn-router-manager/manager.json`, -7080 by default, HTTP; 7443 is kept for it with TLS) on the internal and the external +7080 by default, HTTP; HTTPS is Caddy's, on `https.manager_port`) on the internal and the external interface; restart `vpn-router-setup` after changing it. The manager's `listen` chooses the interfaces it listens on. ## Removal `apt-get remove` stops `vpn-router-docker`, which removes the Docker rules. It terminates the -tunnels and takes `wg-quick@wg0` and `openvpn-server@vpn-router` down, but leaves the +tunnels and takes `wg-quick@wg0`, `openvpn-server@vpn-router` and `caddy` down, but leaves the configuration in place. `strongswan` is left enabled, since it is a shared service that may be in use by something else. @@ -712,7 +743,8 @@ may be in use by something else. `/etc/ufw/before.rules` to its original content, and hands `vpn-router.json` back to `ucf` before deleting it so a later reinstall starts clean. It also removes the web service's signing key, and the refresh tokens' database, its role and their password file when the package -created them. Key material and users are never deleted: `/etc/vpn-router/pki`, `/etc/vpn-router/ldap`, the user directory in slapd, the +created them, and Caddy's configuration `/etc/caddy/vpn-router.Caddyfile`; the certificate in +`/var/lib/caddy` belongs to the `caddy` package. Key material and users are never deleted: `/etc/vpn-router/pki`, `/etc/vpn-router/ldap`, the user directory in slapd, the WireGuard key pair and everything under `/etc/swanctl` are left alone. ## Building diff --git a/debian-package/debian/changelog b/debian-package/debian/changelog index 585d584..d21f313 100644 --- a/debian-package/debian/changelog +++ b/debian-package/debian/changelog @@ -1,3 +1,26 @@ +vpn-router (0.13.0-1) unstable; urgency=medium + + feature: Caddy serves the status page and the Router Manager over HTTPS with a certificate from Let's Encrypt. + + * The package depends on caddy. While https.enabled is true, the + default, and wan.local_fqdn is set, vpn-router-setup writes + /etc/caddy/vpn-router.Caddyfile and runs Caddy, which serves the + status page on https.web_port (7444) and the Router Manager, when it + is installed, on https.manager_port (7443) for wan.local_fqdn, and + obtains the certificate from Let's Encrypt with the HTTP challenge. + * The package's drop-in for caddy.service makes Caddy read that file + and keeps it from starting before the file exists. The file belongs + to root and the group caddy, which Caddy runs as, with mode 0640. + * The firewall opens port 80 on the external interface and the HTTPS + ports on both interfaces while HTTPS is on. + * The web service also listens on 127.0.0.1, where Caddy calls it. + * docker.exposed_ports may not take port 80 or the HTTPS ports while + HTTPS is on. + * Removing the package stops and disables caddy; purging it removes + /etc/caddy/vpn-router.Caddyfile. + + -- Slawomir Koszewski Sun, 25 Oct 2026 15:00:00 +0200 + vpn-router (0.12.3-1) unstable; urgency=medium fix: the web service listens on the internal address, the external address or both. diff --git a/debian-package/debian/control b/debian-package/debian/control index c46126a..e586dae 100644 --- a/debian-package/debian/control +++ b/debian-package/debian/control @@ -28,7 +28,8 @@ Depends: ${misc:Depends}, openvpn, openvpn-auth-ldap, iproute2, - systemd-resolved + systemd-resolved, + caddy Description: Linux VPN router with IPSec, road-warrior, OpenVPN and WireGuard support Configures a Linux host or virtual machine as a VPN router: site-to-site IKEv2 IPSec (strongSwan swanctl), optional road-warrior access using EAP-TLS, diff --git a/debian-package/debian/install b/debian-package/debian/install index d2d3108..8b2b1c8 100644 --- a/debian-package/debian/install +++ b/debian-package/debian/install @@ -19,4 +19,5 @@ src/usr/lib/vpn-router/wait-for-libpq usr/lib/vpn-router/ src/usr/lib/vpn-router/generate-config usr/lib/vpn-router/ src/usr/lib/vpn-router/vpnrouter_platforms/*.py usr/lib/vpn-router/vpnrouter_platforms/ src/usr/share/vpn-router/templates/* usr/share/vpn-router/templates/ +src/usr/lib/systemd/system/caddy.service.d/vpn-router.conf usr/lib/systemd/system/caddy.service.d/ src/usr/share/doc/vpn-router/vpn-router.json.example usr/share/doc/vpn-router/ diff --git a/debian-package/debian/postrm b/debian-package/debian/postrm index b7421a4..37a1d16 100755 --- a/debian-package/debian/postrm +++ b/debian-package/debian/postrm @@ -11,10 +11,10 @@ strip_ufw_blocks() { tmp="$(mktemp)" awk ' - /^# (IPSEC|WIREGUARD|OPENVPN|P2S DNS|WEB|BOOTSTRAP|MANAGER|ROUTER FORWARD|ROUTER NAT|ROUTER MSS) RULES START$/ { + /^# (IPSEC|WIREGUARD|OPENVPN|P2S DNS|WEB|BOOTSTRAP|MANAGER|HTTPS|ROUTER FORWARD|ROUTER NAT|ROUTER MSS) RULES START$/ { skip = 1; pending = 0; next } - /^# (IPSEC|WIREGUARD|OPENVPN|P2S DNS|WEB|BOOTSTRAP|MANAGER|ROUTER FORWARD|ROUTER NAT|ROUTER MSS) RULES END$/ { + /^# (IPSEC|WIREGUARD|OPENVPN|P2S DNS|WEB|BOOTSTRAP|MANAGER|HTTPS|ROUTER FORWARD|ROUTER NAT|ROUTER MSS) RULES END$/ { skip = 0; next } skip { next } @@ -49,6 +49,7 @@ case "$1" in /etc/wireguard/wg0.conf \ /etc/openvpn/server/vpn-router.conf \ /etc/openvpn/server/vpn-router-ldap.conf \ + /etc/caddy/vpn-router.Caddyfile \ /etc/vpn-router/vpn-router.json \ /etc/vpn-router/web.key \ /var/lib/vpn-router/setup.done diff --git a/debian-package/debian/prerm b/debian-package/debian/prerm index 7ef9b48..7e229da 100755 --- a/debian-package/debian/prerm +++ b/debian-package/debian/prerm @@ -11,10 +11,10 @@ case "$1" in swanctl --terminate --ike road-warrior >/dev/null 2>&1 || true fi - # This package enabled wg-quick@wg0 and openvpn-server@vpn-router, so - # it takes them down again. Use the debhelper wrappers rather than - # systemctl, so the script behaves on a machine without systemd. - for unit in wg-quick@wg0.service openvpn-server@vpn-router.service; do + # This package enabled wg-quick@wg0, openvpn-server@vpn-router and + # caddy, so it takes them down again. Use the debhelper wrappers rather + # than systemctl, so the script behaves on a machine without systemd. + for unit in wg-quick@wg0.service openvpn-server@vpn-router.service caddy.service; do if command -v deb-systemd-invoke >/dev/null 2>&1; then deb-systemd-invoke stop "$unit" >/dev/null 2>&1 || true fi diff --git a/debian-package/src/usr/lib/systemd/system/caddy.service.d/vpn-router.conf b/debian-package/src/usr/lib/systemd/system/caddy.service.d/vpn-router.conf new file mode 100644 index 0000000..462fca7 --- /dev/null +++ b/debian-package/src/usr/lib/systemd/system/caddy.service.d/vpn-router.conf @@ -0,0 +1,10 @@ +# Caddy is the HTTPS proxy of vpn-router: it reads the configuration vpn-router-setup writes, and +# does not start until that exists. +[Unit] +ConditionPathExists=/etc/caddy/vpn-router.Caddyfile + +[Service] +ExecStart= +ExecStart=/usr/bin/caddy run --environ --adapter caddyfile --config /etc/caddy/vpn-router.Caddyfile +ExecReload= +ExecReload=/usr/bin/caddy reload --adapter caddyfile --config /etc/caddy/vpn-router.Caddyfile --force diff --git a/debian-package/src/usr/lib/vpn-router/setup b/debian-package/src/usr/lib/vpn-router/setup index da7dbee..c42400d 100755 --- a/debian-package/src/usr/lib/vpn-router/setup +++ b/debian-package/src/usr/lib/vpn-router/setup @@ -16,6 +16,7 @@ firewall and NAT rules are written against them, so choosing the wrong one would be a silent and security-relevant mistake. """ +import grp import json import os import pathlib @@ -58,6 +59,12 @@ MANAGER_CONFIG = pathlib.Path('/etc/vpn-router-manager/manager.json') MANAGER_ADMIN = pathlib.Path('/etc/vpn-router-manager/admin.json') MANAGER_PORT = 7080 +#: The configuration of Caddy, the HTTPS proxy, which the package's drop-in for caddy.service names; +#: Caddy runs as the user and group CADDY_GROUP, which the caddy package creates. +CADDYFILE = pathlib.Path('/etc/caddy/vpn-router.Caddyfile') +CADDY_UNIT = 'caddy.service' +CADDY_GROUP = 'caddy' + FILTER_ANCHOR = '# End required lines' #: Configuration options left out wherever the configuration is shown. @@ -71,6 +78,7 @@ BLOCKS = ( ('# WEB RULES START', '# WEB RULES END'), ('# BOOTSTRAP RULES START', '# BOOTSTRAP RULES END'), ('# MANAGER RULES START', '# MANAGER RULES END'), + ('# HTTPS RULES START', '# HTTPS RULES END'), ('# ROUTER FORWARD RULES START', '# ROUTER FORWARD RULES END'), ('# ROUTER NAT RULES START', '# ROUTER NAT RULES END'), ('# ROUTER MSS RULES START', '# ROUTER MSS RULES END'), @@ -242,6 +250,19 @@ def filter_block(ctx): '# MANAGER RULES END', ] + # Let's Encrypt reaches the HTTP challenge on the external interface; the HTTPS ports are open on both. + if vpnrouter.https_ready(ctx) and interfaces: + ports = [port for wanted, port in ((ctx['web_enabled'], ctx['https_web_port']), + (MANAGER.exists(), ctx['https_manager_port'])) if wanted] + lines += [ + '', + '# HTTPS RULES START', + *([f'-A ufw-before-input -i {ctx["wan_iface"]} -p tcp --dport {vpnrouter.ACME_HTTP_PORT} -j ACCEPT'] + if ctx['wan_iface'] else []), + *(f'-A ufw-before-input -i {name} -p tcp --dport {port} -j ACCEPT' for name in interfaces for port in ports), + '# HTTPS RULES END', + ] + if vpnrouter.routing_ready(ctx): lines += ['', '# ROUTER FORWARD RULES START'] for subnet in ctx['local_cidr_list']: @@ -433,6 +454,32 @@ def manage_openvpn(ctx, changed): run(['systemctl', 'restart', unit], check=False) +def manage_caddy(ctx): + """Run Caddy, the HTTPS proxy of the web service and the Router Manager, while HTTPS is ready, and stop it when not. + + CADDYFILE serves the web service on https.web_port and, while the manager is installed, the + manager on https.manager_port, both for wan.local_fqdn with a certificate from Let's Encrypt, + and both calling the service on its loopback address. + """ + if not vpnrouter.https_ready(ctx): + vpnrouter.remove(CADDYFILE) + if unit_is_active(CADDY_UNIT): + run(['systemctl', 'disable', '--now', CADDY_UNIT], check=False) + return + + try: + group = grp.getgrnam(CADDY_GROUP).gr_gid + except KeyError: + fail(f'the group {CADDY_GROUP} does not exist; is the caddy package installed?') + caddy_ctx = dict(ctx, manager_port=manager_port() if MANAGER.exists() else 0) + changed = vpnrouter.render(vpnrouter._jinja_env(), caddy_ctx, 'Caddyfile.j2', CADDYFILE, 0o640) + os.chown(CADDYFILE, 0, group) + if not unit_is_active(CADDY_UNIT): + run(['systemctl', 'enable', '--now', CADDY_UNIT], check=False) + elif changed: + run(['systemctl', 'reload', CADDY_UNIT], check=False) + + def run_platform_setup(cfg, ctx): """Let the platform module apply its own state, if it defines any.""" module = vpnrouter.platform_module(cfg) @@ -576,6 +623,8 @@ def main(): problem = manage_admin() progress('running', 'OpenVPN') manage_openvpn(ctx, changed) + progress('running', 'HTTPS proxy') + manage_caddy(ctx) if 'resolved' in changed: progress('running', 'resolver') diff --git a/debian-package/src/usr/lib/vpn-router/vpnrouter.py b/debian-package/src/usr/lib/vpn-router/vpnrouter.py index 0c09732..7f17291 100644 --- a/debian-package/src/usr/lib/vpn-router/vpnrouter.py +++ b/debian-package/src/usr/lib/vpn-router/vpnrouter.py @@ -21,7 +21,7 @@ if str(LIB_DIR) not in sys.path: import vpnrouter_ldap import vpnrouter_platforms -from vpnrouter_net import DEFAULT_LISTEN, LISTEN_CHOICES, ConfigError, detect_interfaces, interface_addresses, ip_output +from vpnrouter_net import DEFAULT_LISTEN, LISTEN_CHOICES, LOOPBACK, ConfigError, detect_interfaces, interface_addresses, ip_output CONFIG_FILE = pathlib.Path('/etc/vpn-router/vpn-router.json') TEMPLATE_DIR = pathlib.Path('/usr/share/vpn-router/templates') @@ -55,8 +55,12 @@ SCHEMA = { 'openvpn': {'enabled': False, 'address_pool': '', 'port': 1194, 'protocol': 'udp'}, 'docker': {'mode': 'auto', 'exposed_ports': []}, 'web': {'enabled': True, 'listen': DEFAULT_LISTEN, 'port': 7081, 'database': '', 'trusted_proxies': []}, + 'https': {'enabled': True, 'email': '', 'web_port': 7444, 'manager_port': 7443}, } +#: The port of Let's Encrypt's HTTP challenge, which Caddy answers while HTTPS is on. +ACME_HTTP_PORT = 80 + DOCKER_MODES = ('auto', 'on', 'off') OPENVPN_PROTOCOLS = ('udp', 'tcp') @@ -264,6 +268,17 @@ def validate(cfg): raise ConfigError( f'web.listen: {web_listen!r} is not one of {", ".join(LISTEN_CHOICES)}') + https_ports = [] + for option in ('web_port', 'manager_port'): + port = _get(cfg, 'https', option) + if isinstance(port, bool) or not isinstance(port, int) or not 1 <= port <= 65535: + raise ConfigError(f'https.{option}: {port!r} is not a valid port') + if port == ACME_HTTP_PORT: + raise ConfigError(f'https.{option}: {port} is the port of the HTTP challenge') + https_ports.append(port) + if https_ports[0] == https_ports[1]: + raise ConfigError('https.web_port and https.manager_port must differ') + for item in _get_list(cfg, 'web', 'trusted_proxies'): _check_address(item, 'web.trusted_proxies') @@ -280,6 +295,13 @@ def validate(cfg): raise ConfigError( f'docker.exposed_ports: {item!r} is not a port or a port range') weight += 2 if ':' in item else 1 + first, _, last = item.partition(':') + if _get(cfg, 'https', 'enabled'): + for port in (ACME_HTTP_PORT, *https_ports): + if int(first) <= port <= int(last or first): + raise ConfigError( + f'docker.exposed_ports: {item!r} takes port {port}, which Caddy serves while ' + 'https.enabled is true') if weight > DOCKER_PORT_LIMIT: raise ConfigError( f'docker.exposed_ports: at most {DOCKER_PORT_LIMIT} ports are allowed, ' @@ -448,6 +470,11 @@ def build_context(cfg, local_id_override=None): 'ldap_users': vpnrouter_ldap.USERS, 'web_enabled': _get(cfg, 'web', 'enabled'), 'web_port': _get(cfg, 'web', 'port'), + 'https_enabled': _get(cfg, 'https', 'enabled'), + 'https_email': _get(cfg, 'https', 'email'), + 'https_web_port': _get(cfg, 'https', 'web_port'), + 'https_manager_port': _get(cfg, 'https', 'manager_port'), + 'loopback': LOOPBACK, } @@ -477,6 +504,11 @@ def openvpn_ready(ctx): return bool(ctx['openvpn_enabled'] and ctx['openvpn_pool'] and ctx['local_fqdn']) +def https_ready(ctx): + """Caddy serves HTTPS once it is enabled and wan.local_fqdn names the certificate.""" + return bool(ctx['https_enabled'] and ctx['local_fqdn']) + + def _jinja_env(): return jinja2.Environment( loader=jinja2.FileSystemLoader(str(TEMPLATE_DIR)), diff --git a/debian-package/src/usr/lib/vpn-router/vpnrouter_net.py b/debian-package/src/usr/lib/vpn-router/vpnrouter_net.py index eab60ac..a280f20 100644 --- a/debian-package/src/usr/lib/vpn-router/vpnrouter_net.py +++ b/debian-package/src/usr/lib/vpn-router/vpnrouter_net.py @@ -13,6 +13,9 @@ LISTEN_CHOICES = ('internal', 'external', 'both') #: The interfaces a service listens on when it is not set. DEFAULT_LISTEN = 'both' +#: The address every service also listens on, where the HTTPS proxy calls it. +LOOPBACK = '127.0.0.1' + class ConfigError(Exception): """A setting is present but malformed.""" @@ -140,7 +143,7 @@ def listen_addresses(configuration, choice): """The addresses a service listens on for choice of LISTEN_CHOICES, from vpn-router.json read as configuration. The internal address comes first; an address not known yet is left out. A choice that is not - one of LISTEN_CHOICES is DEFAULT_LISTEN. + one of LISTEN_CHOICES is DEFAULT_LISTEN. LOOPBACK is not among them; web_listen adds it. """ if choice not in LISTEN_CHOICES: choice = DEFAULT_LISTEN @@ -157,9 +160,9 @@ def listen_addresses(configuration, choice): def web_listen(configuration, default_port): """Where the web service listens, from vpn-router.json read as configuration, as ([(address, port), ...], ''). - The addresses are those of the interfaces web.listen names (listen_addresses), the port - web.port, else default_port. While web.enabled is false or no address is known, the answer - is ([], the reason). + The addresses are those of the interfaces web.listen names (listen_addresses), then + LOOPBACK, the port web.port, else default_port. While web.enabled is false or no address of + an interface is known, the answer is ([], the reason). """ web = _section(configuration, 'web') if web.get('enabled', True) is False: @@ -170,4 +173,4 @@ def web_listen(configuration, default_port): addresses = listen_addresses(configuration, web.get('listen', DEFAULT_LISTEN)) if not addresses: return [], 'the addresses to listen on are not known yet' - return [(address, port) for address in addresses], '' + return [(address, port) for address in [*addresses, LOOPBACK]], '' diff --git a/debian-package/src/usr/share/doc/vpn-router/vpn-router.json.example b/debian-package/src/usr/share/doc/vpn-router/vpn-router.json.example index 85e9fc8..77dbec4 100644 --- a/debian-package/src/usr/share/doc/vpn-router/vpn-router.json.example +++ b/debian-package/src/usr/share/doc/vpn-router/vpn-router.json.example @@ -49,5 +49,11 @@ "port": 7081, "database": "", "trusted_proxies": [] + }, + "https": { + "enabled": true, + "email": "", + "web_port": 7444, + "manager_port": 7443 } } diff --git a/debian-package/src/usr/share/vpn-router/templates/Caddyfile.j2 b/debian-package/src/usr/share/vpn-router/templates/Caddyfile.j2 new file mode 100644 index 0000000..bc0315a --- /dev/null +++ b/debian-package/src/usr/share/vpn-router/templates/Caddyfile.j2 @@ -0,0 +1,17 @@ +# Written by vpn-router-setup from /etc/vpn-router/vpn-router.json; edits are overwritten. +{ + auto_https disable_redirects +{%- if https_email %} + email {{ https_email }} +{%- endif %} +} +{% if web_enabled %} +{{ local_fqdn }}:{{ https_web_port }} { + reverse_proxy {{ loopback }}:{{ web_port }} +} +{% endif %} +{%- if manager_port %} +{{ local_fqdn }}:{{ https_manager_port }} { + reverse_proxy {{ loopback }}:{{ manager_port }} +} +{% endif %}