update: the status web service listens on port 7081 by default.
Release / release (push) Successful in 11s

This commit is contained in:
slawek committed 2026-10-24 01:14:21 +02:00
1 parent 0bf78b16b1
commit b9003bacb9
7 files changed
+91 -89

No files matched your search

+69 -78
View File
@@ -29,65 +29,61 @@ restores the rules. The rules cover IPv4 only.
## ADR-2: A status web service of the standard library, bound to the internal address ## ADR-2: A status web service of the standard library, bound to the internal address
Status: Accepted; amended by ADR-7 Status: Accepted
Context: The router has no way to see its state without a shell. A check from the protected Context: The router has no way to see its state without a shell. A check from the protected
network, by a person or by a monitor, needs a page or an endpoint. The router is an network, by a person or by a monitor, needs a page or an endpoint. The router is an
internet-facing host, so what the service shows and where it listens matter. internet-facing host, so what the service shows and where it listens matter.
Decision: The `vpn-router-web` unit runs one Python script that uses only the standard library, in Decision: The `vpn-router-web` unit runs one Python script that uses only the standard library, in
the way of a small test server. It binds to the address of the internal interface and to the way of a small test server. It listens on the address of the internal interface and on
`web.port`, on by default at 8080. `/` is for people, in plain text for `curl` and `wget` and as `web.port`, 7081 by default, and is on unless `web.enabled` is `false` (ADR-7). `/` is for
a Tailwind CSS page for a browser, and `/status` is the same status as JSON. It shows the people, in plain text for `curl` and `wget` and as a Tailwind CSS page for a browser, and
state of the services, the package version and the state of the site-to-site, road-warrior and `/status` is the same status as JSON. It shows the state of the services, the package version
WireGuard features, and nothing from the configuration. `vpn-router-setup` resolves the and the state of the site-to-site, road-warrior, OpenVPN and WireGuard features, and nothing from
interface address, writes it with the port and the features to `/run/vpn-router/web.json`, the configuration. `vpn-router-setup` opens the port on the internal interface in UFW and writes
opens the port on the internal interface in UFW and restarts the service, so the service does the features and the facts it resolves to `/run/vpn-router/web.json`, so the service does not
not need the package's configuration code, which needs Jinja2. need the package's configuration code, which needs Jinja2.
Consequences: Nothing outside the internal network can reach it, since it listens on the internal Consequences: Nothing outside the internal network can reach it, since it listens on the internal
address only and the firewall opens the port on the internal interface only. It has no address only and the firewall opens the port on the internal interface only. `/` and `/status`
authentication, which is acceptable because it is read only and shows no configuration. It runs have no authentication, which is acceptable because they are read only and show no
as root, because `swanctl` and `wg` need it, with `NoNewPrivileges`, `ProtectSystem=strict`, configuration. It runs as root, because `swanctl` and `wg` need it, with `NoNewPrivileges`,
`ProtectHome` and `PrivateTmp`. The Tailwind style comes from a CDN, so a browser on a network `ProtectSystem=strict`, `ProtectHome` and `PrivateTmp`. The Tailwind style comes from a CDN, so a
without internet shows the unstyled page, and `curl` is unaffected. A change of the internal browser on a network without internet shows the unstyled page, and `curl` is unaffected.
address is picked up at the next run of `vpn-router-setup`.
## ADR-3: /config is authenticated with a short-lived JWT signed with a shared secret ## ADR-3: /config is authenticated with a short-lived JWT signed with a shared secret
Status: Accepted; amended by ADR-7 and ADR-8 Status: Accepted
Context: A client such as manage-ui needs the configuration and the resolved state of the Context: A client such as the Router Manager needs the configuration and the resolved state of
router, which `/` and `/status` must not show. The service has only the standard library, listens the router, which `/` and `/status` must not show. The service has only the standard library,
on plain HTTP on the internal network, and its configuration file is readable by root only. listens on plain HTTP on the internal network, and its configuration file is readable by root
only.
Decision: `/config` needs a JWT in an `Authorization: Bearer` header. The service verifies HS256 Decision: `/config` needs a JWT in an `Authorization: Bearer` header. The service verifies HS256
tokens with `hmac` and `hashlib`, with the secret `web.jwt_secret`, and accepts no other tokens with `hmac` and `hashlib`, with the secret `web.jwt_secret`, and accepts no other
algorithm, because verifying RS256 or ES256 needs a library outside the standard library. The algorithm, because verifying RS256 or ES256 needs a library outside the standard library. The
token must carry `exp`, not more than ten minutes ahead, so that the client makes a fresh token for token must carry `exp`, not more than ten minutes ahead, so that the client makes a fresh token for
each use and a leaked one expires. Without a secret the page is not served. The secrets in the each use and a leaked one expires. Without a secret the page is not served. The configuration
configuration shown are masked by name (`psk`, and options with `secret`, `password`, `token` or a shown has no secret options (ADR-8).
`_key` ending in the name).
Consequences: The client and the router share one secret, and whoever holds it can make tokens, Consequences: The client and the router share one secret, and whoever holds it can make tokens,
so it is long and random; it is checked to have at least 32 characters. The secret is in the so it is long and random; it is checked to have at least 32 characters. The secret is only in the
configuration file and in `/run/vpn-router/web.json`, both readable by root only. The token and configuration file, readable by root only. The token and the answer cross the network
the answer cross the network unencrypted, so the page is for a trusted internal network; HTTPS unencrypted, so the page is for a trusted internal network; HTTPS would need a certificate that
would need a certificate that the package does not have. The masking is by option name, so a new the package does not have.
secret option needs a name that matches.
## ADR-4: Users are kept in a local OpenLDAP directory ## ADR-4: Users are kept in a local OpenLDAP directory
Status: Accepted Status: Accepted
Context: OpenVPN needs user accounts with passwords, and the users are meant to cover every P2S Context: OpenVPN needs user accounts with passwords, and the users are meant to cover every P2S
system of the router. The options weighed were an external OIDC provider (Entra ID, Keycloak) system of the router, including IKEv2 EAP-TLS and WireGuard, which an OIDC provider cannot
with openvpn-auth-oauth2, an OIDC server embedded on the router, a file of password hashes, and authenticate. Everything the router runs comes from the Ubuntu archive. slapd, its Python module
OpenLDAP on the router. OIDC for OpenVPN needs a browser on the client and a program from outside `python3-ldap` and the OpenVPN plugin `openvpn-auth-ldap` are all in the archive; the plugin
the Ubuntu archive, and it cannot authenticate IKEv2 EAP-TLS or WireGuard. slapd, its Python checks a password by binding as the user, so the hash scheme is slapd's alone. An OIDC provider
module `python3-ldap` and the OpenVPN plugin `openvpn-auth-ldap` are all in the archive; the can read the directory through its LDAP connector.
plugin checks a password by binding as the user, so the hash scheme is slapd's alone. A directory
can also be read later by an OIDC provider through its LDAP connector.
Decision: The users are entries in slapd. The package keeps its own database, Decision: The users are entries in slapd. The package keeps its own database,
`dc=vpn-router,dc=local` in `/var/lib/ldap/vpn-router` (the path the slapd AppArmor profile `dc=vpn-router,dc=local` in `/var/lib/ldap/vpn-router` (the path the slapd AppArmor profile
@@ -101,19 +97,18 @@ slapd listens on the local socket only. Provisioning checks every step before ch
runs at every start of `vpn-router-setup`. runs at every start of `vpn-router-setup`.
Consequences: The package depends on `slapd`, `python3-ldap`, `openvpn` and Consequences: The package depends on `slapd`, `python3-ldap`, `openvpn` and
`openvpn-auth-ldap`, and `vpn-router-setup` and `vpn-router-user` import `python3-ldap`, the first `openvpn-auth-ldap`, and `vpn-router-setup` and `vpn-router-user` import `python3-ldap`. The
library outside the standard library besides `jinja2`. The service account's password is a service account's password is a root-only file. The directory is user data: it is kept on purge,
root-only file. The directory is user data: it is kept when the features are switched off and and it is backed up with `slapcat`, not by copying files.
on purge, and it is backed up with `slapcat`, not by copying files.
## ADR-5: OpenVPN clients need a certificate and the password of the user it names ## ADR-5: OpenVPN clients need a certificate and the password of the user it names
Status: Accepted Status: Accepted
Context: The router CA already issues client certificates for IKEv2. OpenVPN can check a Context: The router CA issues client certificates for IKEv2. OpenVPN can check a certificate, a
certificate, a password, or both. With both, a certificate alone and a password alone are not password, or both. With both, a certificate alone and a password alone are not enough, but
enough, but `openvpn-auth-ldap` checks only the password of the user name given at login, which `openvpn-auth-ldap` checks only the password of the user name given at login, which need not be
need not be the user of the certificate. the user of the certificate.
Decision: OpenVPN checks the certificate against the CA and the CRL, the password with Decision: OpenVPN checks the certificate against the CA and the CRL, the password with
`openvpn-auth-ldap`, and runs `openvpn-same-user` as `auth-user-pass-verify` with `via-env`, `openvpn-auth-ldap`, and runs `openvpn-same-user` as `auth-user-pass-verify` with `via-env`,
@@ -144,19 +139,18 @@ lock, `/etc/vpn-router/pki` for the certificates and the CA's state, and `/etc/s
for the published CRL. A lock serializes the program's runs, because `simple-ca` keeps its state for the published CRL. A lock serializes the program's runs, because `simple-ca` keeps its state
in one file. in one file.
Consequences: The web service still imports nothing outside the standard library, and the same Consequences: The web service imports nothing outside the standard library, and the same program
program serves the command line and the API. Each request starts a process, which is fine for serves the command line and the API. Each request starts a process, which is fine for user
user administration. The API is protected by the token of `/config`, so whoever has the secret administration. The API is protected by the token of `/config`, so whoever has the secret can
can manage users. manage users.
## ADR-7: The web service starts on its own and reads its inputs at each request ## ADR-7: The web service starts on its own and reads its inputs at each request
Status: Accepted Status: Accepted
Context: The web service took the address to listen on, the port, the token secret and the Context: The status page is the tool to see a problem of `vpn-router-setup`, so it must run
features from `/run/vpn-router/web.json`, written by `vpn-router-setup`, and ended with a failure whatever state setup is in, also before setup has run and while it fails. The internal address,
while the file or the internal address was missing. It was ordered after `vpn-router-setup`, so a `web.port` and `web.enabled` can change at any time.
problem in the setup kept the status page, the tool to see that problem, from running.
Decision: The service depends on nothing to start and does not end on its own. A discovery Decision: The service depends on nothing to start and does not end on its own. A discovery
thread works out, every 30 seconds, the internal address from `vpn-router.json` as thread works out, every 30 seconds, the internal address from `vpn-router.json` as
@@ -165,30 +159,28 @@ standard library and which `vpnrouter.py` uses too, and `web.port` and `web.enab
same file. It runs the HTTP server in a thread of its own at that address and port, stops it and same file. It runs the HTTP server in a thread of its own at that address and port, stops it and
starts a new one when they change, and runs none while the service is disabled, the address is starts a new one when they change, and runs none while the service is disabled, the address is
not known or it cannot be bound. Each request reads the token secret from `vpn-router.json` and not known or it cannot be bound. Each request reads the token secret from `vpn-router.json` and
the features and resolved facts from `web.json`, which setup still writes, because they need the the features and resolved facts from `web.json`, which setup writes, because they need the
configuration code; while that file is missing the router shows no features. The unit is not configuration code; while that file is missing the router shows no features. The unit is not
ordered after `vpn-router-setup`, and setup does not restart it. ordered after `vpn-router-setup`, and setup does not restart it.
Consequences: The status page answers as soon as the internal address exists, whatever state Consequences: The status page answers as soon as the internal address exists, whatever state
setup is in, and follows a changed address, port or `web.enabled` within 30 seconds without a setup is in, and follows a changed address, port or `web.enabled` within 30 seconds without a
restart. `web.json` no longer holds the address, the port or the secret, so the secret is only in restart. A change of the secret applies to the next request. The interfaces are read every 30
`vpn-router.json`. A change of the secret applies to the next request. The interfaces are read seconds, which costs a few `ip` commands.
every 30 seconds, which costs a few `ip` commands.
## ADR-8: /config leaves secret options out ## ADR-8: /config leaves secret options out
Status: Accepted Status: Accepted
Context: `/config` showed the secret options of the configuration with their values replaced by Context: A secret has no use outside the router, so a client never needs it, and showing even
`********`. That still tells a reader which secrets are set, and a secret has no use outside the that a secret is set tells a reader more than it needs.
router, so a client never needs it.
Decision: The configuration that `vpn-router-setup` writes for the web service has no secret Decision: The configuration that `vpn-router-setup` writes for the web service has no secret
options at all. They are recognised by name as before: `psk`, and options with `secret`, options at all. They are recognised by name: `psk`, and options with `secret`, `password`,
`password`, `token` or a `_key` ending in the name. `token` or a `_key` ending in the name.
Consequences: No secret reaches the web service's state file or any answer of the API. A client Consequences: No secret reaches the web service's state file or any answer of the API. A client
cannot tell whether a secret is set. A new secret option still needs a name that matches. cannot tell whether a secret is set. A new secret option needs a name that matches.
## ADR-9: The Router Manager is a separate package with a small Python server ## ADR-9: The Router Manager is a separate package with a small Python server
@@ -199,30 +191,29 @@ another Linux system. The router's web service needs a token signed with `web.jw
`/config` and `/users` and sends no CORS headers, so a browser cannot call it with the secret `/config` and `/users` and sends no CORS headers, so a browser cannot call it with the secret
kept away from the page. kept away from the page.
Decision: `vpn-router-manager` is a Debian package of its own, built in the repository `linux-cloud-router-manager`. A React and Decision: `vpn-router-manager` is a Debian package of its own, built in the repository
MUI application in the style of manage-ui is built with Vite when the package is built and `linux-cloud-router-manager`. A React and MUI application in the style of manage-ui is built
shipped as static files. A Python server, using the Ubuntu packages `python3-jwt` and `python3-requests`, serves them and with Vite when the package is built and shipped as static files. A Python server, using the
passes every `/api/<path>` call to the router's `/<path>`, signing a short-lived token with the Ubuntu packages `python3-jwt` and `python3-requests`, serves them and passes every `/api/<path>`
router's secret; it holds no logic of the router's own. It reads its settings from call to the router's `/<path>`, signing a short-lived token with the router's secret; it holds
`/etc/vpn-router-manager/manager.json` at each request; on the router the settings not given no logic of the router's own. It reads its settings from `/etc/vpn-router-manager/manager.json`
come from `vpn-router.json`, and it listens on the internal address like the status web at each request; on the router the settings not given come from `vpn-router.json`, and it
service, with the same discovery thread. `vpn-router-setup` opens the manager's port on the listens on the internal address like the status web service, with the same discovery thread.
internal interface while the manager is installed, so the firewall stays in one place. The `vpn-router-setup` opens the manager's port on the internal interface while the manager is
manager asks for no authentication yet. installed, so the firewall stays in one place. The manager asks for no authentication.
Consequences: The router needs no Node.js; the package needs only `python3` and packages of the Ubuntu archive. The secret never Consequences: The router needs no Node.js; the package needs only `python3` and packages of the
reaches the browser. The application checks the shape of every answer of the router. Anyone who Ubuntu archive. The secret never reaches the browser. The application checks the shape of every
reaches the manager's port manages the router, so on another system it listens on `127.0.0.1` answer of the router. Anyone who reaches the manager's port manages the router, so on another
unless told otherwise. The router's firewall depends on whether the manager is installed, and a system it listens on `127.0.0.1` unless told otherwise. The router's firewall depends on whether
changed manager port needs `vpn-router-setup` to run again. the manager is installed, and a changed manager port needs `vpn-router-setup` to run again.
## ADR-10: Users and their certificates do not depend on a P2S system being enabled ## ADR-10: Users and their certificates do not depend on a P2S system being enabled
Status: Accepted Status: Accepted
Context: The user directory and the CA were provisioned, and `/users` was served, only while Context: Users are prepared before a P2S system is switched on, and the Router Manager manages
OpenVPN or road-warrior access was enabled. Users could not be prepared before a P2S system was them on any configured router, with or without one.
switched on, and the Router Manager could not manage them on a router without one.
Decision: Once a platform is chosen, `vpn-router-setup` always provisions the user directory in Decision: Once a platform is chosen, `vpn-router-setup` always provisions the user directory in
slapd and the CA with its CRL; the server certificate follows once `wan.local_fqdn` is set. slapd and the CA with its CRL; the server certificate follows once `wan.local_fqdn` is set.
+8 -7
View File
@@ -122,7 +122,7 @@ must have the type shown below; an unknown section or setting is an error.
| `docker` | `mode` | string | `auto` | `auto` secures Docker when its service is installed, `on` requires it, `off` removes the rules | | `docker` | `mode` | string | `auto` | `auto` secures Docker when its service is installed, `on` requires it, `off` removes the rules |
| `docker` | `exposed_ports` | list | | TCP ports and port ranges (`8000:8100`) reachable from the external interface; at most 15, a range counting as two | | `docker` | `exposed_ports` | list | | TCP ports and port ranges (`8000:8100`) reachable from the external interface; at most 15, a range counting as two |
| `web` | `enabled` | boolean | `true` | The status web service on the internal address | | `web` | `enabled` | boolean | `true` | The status web service on the internal address |
| `web` | `port` | integer | `8080` | TCP port of the status web service | | `web` | `port` | integer | `7081` | TCP port of the status web service |
| `web` | `jwt_secret` | string | | Secret, at least 32 characters, that signs the tokens for `/config`; without it `/config` is not served | | `web` | `jwt_secret` | string | | Secret, at least 32 characters, that signs the tokens for `/config`; without it `/config` is not served |
A minimal site-to-site configuration in `auto` mode: A minimal site-to-site configuration in `auto` mode:
@@ -407,7 +407,7 @@ modules shipped with the package; the configuration file accepts any module inst
## Status web service ## Status web service
The `vpn-router-web` service shows the state of the router on its internal address and The `vpn-router-web` service shows the state of the router on its internal address and
`web.port` (8080 by default). It uses only the Python standard library, serves HTTP without `web.port` (7081 by default). It uses only the Python standard library, serves HTTP without
authentication, and shows no configuration: no key, identity, address or subnet. authentication, and shows no configuration: no key, identity, address or subnet.
| Page | Content | | Page | Content |
@@ -428,8 +428,8 @@ the WireGuard peers with the age of their last handshake and their traffic. `sta
all these services are active, and `degraded` otherwise. all these services are active, and `degraded` otherwise.
```bash ```bash
curl http://10.0.0.4:8080/ curl http://10.0.0.4:7081/
curl http://10.0.0.4:8080/status curl http://10.0.0.4:7081/status
``` ```
### The /config page ### The /config page
@@ -466,7 +466,7 @@ print(f'{head}.{body}.{sign}')
``` ```
```bash ```bash
curl -H "Authorization: Bearer $TOKEN" http://10.0.0.4:8080/config curl -H "Authorization: Bearer $TOKEN" http://10.0.0.4:7081/config
``` ```
### The /users API ### The /users API
@@ -491,7 +491,7 @@ network in plain HTTP, like the token.
```bash ```bash
curl -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ curl -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"name": "alice", "password": "s3cr3t-pass"}' http://10.0.0.4:8080/users -d '{"name": "alice", "password": "s3cr3t-pass"}' http://10.0.0.4:7081/users
``` ```
The service starts on its own, without waiting for `vpn-router-setup`, and keeps running. Every The service starts on its own, without waiting for `vpn-router-setup`, and keeps running. Every
@@ -509,7 +509,8 @@ The `vpn-router-manager` package, built in the repository `linux-cloud-router-ma
application that shows the state of the router and manages its users through the status 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 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`, the router, `vpn-router-setup` opens its port (`port` in `/etc/vpn-router-manager/manager.json`,
8090 by default) on the internal interface; restart `vpn-router-setup` after changing it. 7080 by default, HTTP; 7443 is kept for it with TLS) on the internal interface; restart
`vpn-router-setup` after changing it.
## Removal ## Removal
+10
View File
@@ -1,3 +1,13 @@
vpn-router (0.5.6-1) unstable; urgency=medium
update: the status web service listens on port 7081 by default.
* web.port defaults to 7081.
* vpn-router-setup opens the Router Manager's port, 7080 by default, on
the internal interface.
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Sat, 24 Oct 2026 18:00:00 +0200
vpn-router (0.5.5-1) unstable; urgency=medium vpn-router (0.5.5-1) unstable; urgency=medium
feature: users and their certificates no longer need a P2S system enabled. feature: users and their certificates no longer need a P2S system enabled.
+1 -1
View File
@@ -34,7 +34,7 @@ WEB_STATE = pathlib.Path('/run/vpn-router/web.json')
#: The Router Manager, vpn-router-manager, when it is installed, and its settings. #: The Router Manager, vpn-router-manager, when it is installed, and its settings.
MANAGER = pathlib.Path('/usr/lib/vpn-router-manager/manager') MANAGER = pathlib.Path('/usr/lib/vpn-router-manager/manager')
MANAGER_CONFIG = pathlib.Path('/etc/vpn-router-manager/manager.json') MANAGER_CONFIG = pathlib.Path('/etc/vpn-router-manager/manager.json')
MANAGER_PORT = 8090 MANAGER_PORT = 7080
FILTER_ANCHOR = '# End required lines' FILTER_ANCHOR = '# End required lines'
@@ -54,7 +54,7 @@ SCHEMA = {
'wireguard': {'enabled': False, 'address': '', 'listen_port': 51820}, 'wireguard': {'enabled': False, 'address': '', 'listen_port': 51820},
'openvpn': {'enabled': False, 'address_pool': '', 'port': 1194, 'protocol': 'udp'}, 'openvpn': {'enabled': False, 'address_pool': '', 'port': 1194, 'protocol': 'udp'},
'docker': {'mode': 'auto', 'exposed_ports': []}, 'docker': {'mode': 'auto', 'exposed_ports': []},
'web': {'enabled': True, 'port': 8080, 'jwt_secret': ''}, 'web': {'enabled': True, 'port': 7081, 'jwt_secret': ''},
} }
#: The shortest secret that signs the tokens of the status web service. #: The shortest secret that signs the tokens of the status web service.
+1 -1
View File
@@ -52,7 +52,7 @@ STATE_FILE = '/run/vpn-router/web.json'
CONFIG_FILE = '/etc/vpn-router/vpn-router.json' CONFIG_FILE = '/etc/vpn-router/vpn-router.json'
#: The port when web.port is not set. #: The port when web.port is not set.
DEFAULT_PORT = 8080 DEFAULT_PORT = 7081
#: Seconds between two discoveries of where the service should listen. #: Seconds between two discoveries of where the service should listen.
DISCOVERY_SECONDS = 30 DISCOVERY_SECONDS = 30
@@ -45,7 +45,7 @@
}, },
"web": { "web": {
"enabled": true, "enabled": true,
"port": 8080, "port": 7081,
"jwt_secret": "" "jwt_secret": ""
} }
} }