diff --git a/ADR.md b/ADR.md index 8983ecc..6e939ed 100644 --- a/ADR.md +++ b/ADR.md @@ -537,7 +537,9 @@ unchanged. Both services always listen on `127.0.0.1` as well as on the interfac never takes from the client. Let's Encrypt issues at most 5 certificates a week for the same names, which routers rebuilt for tests reach; `https.acme_staging` sets Caddy's global `acme_ca` to Let's Encrypt's staging directory, which replaces Caddy's default CAs for every site, so no -test certificate comes from a production CA. +test certificate comes from a production CA. `PUT /https/settings` changes `acme_staging` alone: +the other settings of the section switch HTTPS off or move its ports, which would end the +connection the Router Manager makes the change on. 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 diff --git a/README.md b/README.md index 6b673f9..e4befc2 100644 --- a/README.md +++ b/README.md @@ -1018,7 +1018,7 @@ curl http://10.0.0.4:7081/status ### Tokens and signing in -`/config`, `/metrics/history`, `/metrics/settings`, `/users`, `/service-accounts`, `/groups`, `/site-to-site`, `/backup`, `/remote-access`, `/ca`, `/logs`, `/sites`, `/dns`, `/shell` and `/account` need a JWT in an `Authorization: Bearer` header, signed with +`/config`, `/metrics/history`, `/metrics/settings`, `/https/settings`, `/users`, `/service-accounts`, `/groups`, `/site-to-site`, `/backup`, `/remote-access`, `/ca`, `/logs`, `/sites`, `/dns`, `/shell` and `/account` need a JWT in an `Authorization: Bearer` header, signed with HS256 by the key the web service makes on its first run, `/etc/vpn-router/web.key` (root only). The key never leaves the router; while it does not exist these paths and `/auth` are not served (404). A missing, expired or wrongly signed token, or one of another algorithm, gets 401 with @@ -1405,6 +1405,18 @@ A change is made, checked and applied as for `/site-to-site`. A change is made, checked and applied as for `/site-to-site`. +### The /https/settings API + +`/https/settings` shows the settings of HTTPS and changes the CA of Caddy's certificates, with the +same token as `/users`. + +| Request | Body | Answer | +| --- | --- | --- | +| `GET /https/settings` | | `{"settings"}`: the `https` section | +| `PUT /https/settings` | `{"acme_staging"}`; any other setting of the `https` section is 400, as it is changed on the router with `vpn-config-edit` | `{"settings"}` | + +A change is made, checked and applied as for `/site-to-site`. + ### The /remote-access API `/remote-access` shows and changes the remote access systems, IKEv2 (`p2s`), @@ -1484,6 +1496,9 @@ obtains from an ACME CA, those of the web sites with `tls` `acme` included. vpn-config-edit set --restart https.acme_staging true ``` +The Router Manager switches it on the Overview, and `PUT /https/settings` changes it (see The +/https/settings API). + `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` diff --git a/debian-package/debian/changelog b/debian-package/debian/changelog index 96100e5..911be35 100644 --- a/debian-package/debian/changelog +++ b/debian-package/debian/changelog @@ -1,3 +1,13 @@ +vpn-router (0.38.1-1) unstable; urgency=medium + + fix: /https/settings shows the https section and switches https.acme_staging, for the Router Manager. + + * GET /https/settings shows the https section, and PUT /https/settings + changes acme_staging; the other settings of the section stay with + vpn-config-edit. + + -- Slawomir Koszewski Fri, 30 Oct 2026 02:43:22 +0100 + vpn-router (0.38.0-1) unstable; urgency=medium feature: https.acme_staging takes Caddy's certificates from Let's Encrypt's staging environment. diff --git a/debian-package/src/usr/lib/vpn-router/vpnrouter_web/handler.py b/debian-package/src/usr/lib/vpn-router/vpnrouter_web/handler.py index 57c82c9..7f24a59 100644 --- a/debian-package/src/usr/lib/vpn-router/vpnrouter_web/handler.py +++ b/debian-package/src/usr/lib/vpn-router/vpnrouter_web/handler.py @@ -22,6 +22,7 @@ from .connectivity import remote_access, site_to_site from .dns import dns from .identities import account, groups, service_accounts, users from .logs import logs +from .https import https_settings from .metrics import metrics_history, metrics_settings from .pki import PUBLISHED, ca from .shell import EDITOR_BODY_LIMIT, shell, shell_editor, shell_passkey, shell_ticket @@ -66,7 +67,8 @@ class Handler(BaseHTTPRequestHandler): directory = next((name for name in ('users', 'service-accounts', 'groups', 'account', 'site-to-site', 'backup', 'ca', 'shell', 'remote-access', 'logs', 'sites', 'dns') if path == f'/{name}' or path.startswith(f'/{name}/')), None) - protected = path in ('/status/full', '/config', '/metrics/history', '/metrics/settings', '/postgresql') or \ + protected = path in ('/status/full', '/config', '/metrics/history', '/metrics/settings', '/https/settings', + '/postgresql') or \ directory is not None signing = path in ('/auth', '/auth/passkey', '/auth/passkey/complete', '/auth/refresh', '/auth/logout') oidc = (path == '/.well-known/openid-configuration' or path.startswith('/oidc/')) and \ @@ -113,6 +115,9 @@ class Handler(BaseHTTPRequestHandler): elif path == '/metrics/settings': code, document = metrics_settings(self.command, payload) body, kind = json.dumps(document, indent=2).encode() + b'\n', 'application/json' + elif path == '/https/settings': + code, document = https_settings(self.command, payload) + body, kind = json.dumps(document, indent=2).encode() + b'\n', 'application/json' elif directory == 'shell' and (path, self.command) == ('/shell', 'GET'): return shell(self, claims.get('sub')) elif directory == 'shell' and path.startswith('/shell/editor/'): diff --git a/debian-package/src/usr/lib/vpn-router/vpnrouter_web/https.py b/debian-package/src/usr/lib/vpn-router/vpnrouter_web/https.py new file mode 100644 index 0000000..7c922dd --- /dev/null +++ b/debian-package/src/usr/lib/vpn-router/vpnrouter_web/https.py @@ -0,0 +1,33 @@ +"""/https/settings: the settings of HTTPS, of which the API changes the CA of Caddy's certificates.""" + +import vpnrouter + +from .common import section_change + +#: The settings of the https section PUT /https/settings changes. +CHANGEABLE = ('acme_staging',) + + +def https_settings(method, body): + """Answer a /https/settings request: the HTTP status and the JSON document. + + GET /https/settings {"settings"}: the https section + PUT /https/settings change acme_staging; the other settings of the section are changed on the router with + vpn-config-edit, and a body naming one is 400 + + A change is applied by restarting vpn-router-setup without waiting for it. + """ + if body == 'invalid' or (body is not None and not isinstance(body, dict)): + return 400, {'error': 'the body must be a JSON object'} + if method in ('GET', 'HEAD'): + try: + return 200, {'settings': vpnrouter.load_config()['https']} + except SystemExit as exc: + return 500, {'error': str(exc.code)} + if method != 'PUT': + return 404, {'error': f'no route {method} /https/settings'} + others = sorted(set(body or {}) - set(CHANGEABLE)) + if others: + return 400, {'error': f'https.{", https.".join(others)}: changed on the router with vpn-config-edit only'} + changed = section_change('https', body or {}) + return changed if isinstance(changed, tuple) else (200, {'settings': changed})