Compare commits

...
2 Commits
Author SHA1 Message Date
slawek c2c33cfca0 fix: /https/settings shows the https section and switches https.acme_staging, for the Router Manager.
Release / release (push) Successful in 56s
2026-10-30 02:46:07 +01:00
slawek f36e2a3b04 feature: https.acme_staging takes Caddy's certificates from Let's Encrypt's staging environment.
Release / release (push) Successful in 14s
2026-10-30 02:35:21 +01:00
7 changed files with 104 additions and 6 deletions

No files matched your search

+8 -2
View File
@@ -534,11 +534,17 @@ package's drop-in for `caddy.service` points it at that file and keeps it from s
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.
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. `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
certificate and the ACME account are in `/var/lib/caddy`. Plain HTTP stays as the `listen`
certificate and the ACME account are in `/var/lib/caddy`. A router on the staging CA has a
certificate no client trusts. 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
+28 -2
View File
@@ -141,6 +141,7 @@ must have the type shown below; an unknown section or setting is an error.
| `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 |
| `https` | `acme_staging` | boolean | `false` | Caddy takes its certificates from Let's Encrypt's staging environment alone, whose certificates no browser trusts and whose rate limits are higher, for routers rebuilt often (see HTTPS) |
| `https` | `caddy_source` | string | `ubuntu` | Where Caddy is installed from: `ubuntu`, the Ubuntu archive, or `upstream`, Caddy's own repository (see HTTPS) |
| `oidc` | `enabled` | boolean | `false` | The OpenID Provider at the status web service's HTTPS site, which needs `web.enabled`, `https.enabled` and `wan.local_fqdn` (see OpenID Provider) |
| `sites` | `enabled` | boolean | `false` | Caddy serves the web sites (see Web sites) |
@@ -1017,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
@@ -1404,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`),
@@ -1471,7 +1484,20 @@ while metrics are enabled the metrics service on `metrics.https_port` (9465), al
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.
it does not redirect to HTTPS. Caddy's default CAs are Let's Encrypt and ZeroSSL, which it takes
when Let's Encrypt refuses, such as for its rate limit of 5 certificates a week for the same name.
While `https.acme_staging` is `true`, Caddy's only CA is Let's Encrypt's staging environment, for
routers that are rebuilt often. Its rate limits are far higher, but no browser or client trusts
its certificates, so they are for tests only. The setting applies to every certificate Caddy
obtains from an ACME CA, those of the web sites with `tls` `acme` included.
```sh
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
+20
View File
@@ -1,3 +1,23 @@
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 <slawek@koszewscy.waw.pl> 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.
* While https.acme_staging is true, Caddy's only CA is Let's Encrypt's
staging environment, for the router's services and the web sites
with tls acme; its certificates are not trusted by browsers.
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Fri, 30 Oct 2026 02:34:01 +0100
vpn-router (0.37.3-1) unstable; urgency=medium
fix: an answer whose client went away ends quietly, and a shell whose client left before it opened is stopped.
@@ -66,7 +66,8 @@ SCHEMA = {
'status_content': 'basic', 'shell': False, 'shell_idle_minutes': 15, 'shell_auth_timeout': 15,
'log_files': []},
'ssh': {'ldap_login': False},
'https': {'enabled': True, 'email': '', 'web_port': 7444, 'manager_port': 7443, 'caddy_source': 'ubuntu'},
'https': {'enabled': True, 'email': '', 'web_port': 7444, 'manager_port': 7443, 'caddy_source': 'ubuntu',
'acme_staging': False},
'oidc': {'enabled': False},
'sites': {'enabled': False, 'upload_max_mb': 512, 'webdav': False, 'webdav_port': 7445},
'dns': {'enabled': False, 'nameserver': '', 'addresses': [], 'listen': 'external', 'ttl': 300},
@@ -164,6 +165,9 @@ MANAGER_PORT = 7080
#: Where vpn-router-setup installs Caddy from, https.caddy_source: the Ubuntu archive or Caddy's own repository.
CADDY_SOURCES = ('ubuntu', 'upstream')
#: The ACME directory of Let's Encrypt's staging environment, Caddy's only CA while https.acme_staging is true.
ACME_STAGING_CA = 'https://acme-staging-v02.api.letsencrypt.org/directory'
#: Caddy's own repository, which https.caddy_source upstream configures: the source, its signing key and their URLs.
CADDY_APT_SOURCE = pathlib.Path('/etc/apt/sources.list.d/caddy-stable.sources')
CADDY_KEYRING = pathlib.Path('/etc/apt/keyrings/caddy-stable.asc')
@@ -1034,6 +1038,7 @@ def build_context(cfg, public_ip=None):
'web_port': _get(cfg, 'web', 'port'),
'https_enabled': _get(cfg, 'https', 'enabled'),
'https_email': _get(cfg, 'https', 'email'),
'https_acme_ca': ACME_STAGING_CA if _get(cfg, 'https', 'acme_staging') else '',
'https_web_port': _get(cfg, 'https', 'web_port'),
'https_manager_port': _get(cfg, 'https', 'manager_port'),
'pki_publish': _get_list(cfg, 'pki', 'publish'),
@@ -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/'):
@@ -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})
@@ -4,6 +4,9 @@
{%- if https_email %}
email {{ https_email }}
{%- endif %}
{%- if https_acme_ca %}
acme_ca {{ https_acme_ca }}
{%- endif %}
{%- if pki_cas %}
skip_install_trust
pki {