update: the status web service listens on port 7081 by default.
Release / release (push) Successful in 11s
Release / release (push) Successful in 11s
This commit is contained in:
1 parent
0bf78b16b1
commit
b9003bacb9
7 files changed
+91
-89
No files matched your search
@@ -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
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
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
|
||||
`web.port`, on by default at 8080. `/` is for people, in plain text for `curl` and `wget` and as
|
||||
a Tailwind CSS page for a browser, and `/status` is the same status as JSON. It shows the
|
||||
state of the services, the package version and the state of the site-to-site, road-warrior and
|
||||
WireGuard features, and nothing from the configuration. `vpn-router-setup` resolves the
|
||||
interface address, writes it with the port and the features to `/run/vpn-router/web.json`,
|
||||
opens the port on the internal interface in UFW and restarts the service, so the service does
|
||||
not need the package's configuration code, which needs Jinja2.
|
||||
the way of a small test server. It listens on the address of the internal interface and on
|
||||
`web.port`, 7081 by default, and is on unless `web.enabled` is `false` (ADR-7). `/` is for
|
||||
people, in plain text for `curl` and `wget` and as a Tailwind CSS page for a browser, and
|
||||
`/status` is the same status as JSON. It shows the state of the services, the package version
|
||||
and the state of the site-to-site, road-warrior, OpenVPN and WireGuard features, and nothing from
|
||||
the configuration. `vpn-router-setup` opens the port on the internal interface in UFW and writes
|
||||
the features and the facts it resolves to `/run/vpn-router/web.json`, so the service does not
|
||||
need the package's configuration code, which needs Jinja2.
|
||||
|
||||
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
|
||||
authentication, which is acceptable because it is read only and shows no configuration. It runs
|
||||
as root, because `swanctl` and `wg` need it, with `NoNewPrivileges`, `ProtectSystem=strict`,
|
||||
`ProtectHome` and `PrivateTmp`. The Tailwind style comes from a CDN, so a browser on a network
|
||||
without internet shows the unstyled page, and `curl` is unaffected. A change of the internal
|
||||
address is picked up at the next run of `vpn-router-setup`.
|
||||
address only and the firewall opens the port on the internal interface only. `/` 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`,
|
||||
`ProtectSystem=strict`, `ProtectHome` and `PrivateTmp`. The Tailwind style comes from a CDN, so a
|
||||
browser on a network without internet shows the unstyled page, and `curl` is unaffected.
|
||||
|
||||
## 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
|
||||
router, which `/` and `/status` must not show. The service has only the standard library, listens
|
||||
on plain HTTP on the internal network, and its configuration file is readable by root only.
|
||||
Context: A client such as the Router Manager needs the configuration and the resolved state of
|
||||
the router, which `/` and `/status` must not show. The service has only the standard library,
|
||||
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
|
||||
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
|
||||
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
|
||||
configuration shown are masked by name (`psk`, and options with `secret`, `password`, `token` or a
|
||||
`_key` ending in the name).
|
||||
each use and a leaked one expires. Without a secret the page is not served. The configuration
|
||||
shown has no secret options (ADR-8).
|
||||
|
||||
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
|
||||
configuration file and in `/run/vpn-router/web.json`, both readable by root only. The token and
|
||||
the answer cross the network unencrypted, so the page is for a trusted internal network; HTTPS
|
||||
would need a certificate that the package does not have. The masking is by option name, so a new
|
||||
secret option needs a name that matches.
|
||||
so it is long and random; it is checked to have at least 32 characters. The secret is only in the
|
||||
configuration file, readable by root only. The token and the answer cross the network
|
||||
unencrypted, so the page is for a trusted internal network; HTTPS would need a certificate that
|
||||
the package does not have.
|
||||
|
||||
## ADR-4: Users are kept in a local OpenLDAP directory
|
||||
|
||||
Status: Accepted
|
||||
|
||||
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)
|
||||
with openvpn-auth-oauth2, an OIDC server embedded on the router, a file of password hashes, and
|
||||
OpenLDAP on the router. OIDC for OpenVPN needs a browser on the client and a program from outside
|
||||
the Ubuntu archive, and it cannot authenticate IKEv2 EAP-TLS or WireGuard. slapd, its Python
|
||||
module `python3-ldap` and the OpenVPN plugin `openvpn-auth-ldap` are all in the archive; the
|
||||
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.
|
||||
system of the router, including IKEv2 EAP-TLS and WireGuard, which an OIDC provider cannot
|
||||
authenticate. Everything the router runs comes from the Ubuntu archive. slapd, its Python module
|
||||
`python3-ldap` and the OpenVPN plugin `openvpn-auth-ldap` are all in the archive; the plugin
|
||||
checks a password by binding as the user, so the hash scheme is slapd's alone. An OIDC provider
|
||||
can read the directory through its LDAP connector.
|
||||
|
||||
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
|
||||
@@ -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`.
|
||||
|
||||
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
|
||||
library outside the standard library besides `jinja2`. The service account's password is a
|
||||
root-only file. The directory is user data: it is kept when the features are switched off and
|
||||
on purge, and it is backed up with `slapcat`, not by copying files.
|
||||
`openvpn-auth-ldap`, and `vpn-router-setup` and `vpn-router-user` import `python3-ldap`. The
|
||||
service account's password is a root-only file. The directory is user data: it is kept 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
|
||||
|
||||
Status: Accepted
|
||||
|
||||
Context: The router CA already issues client certificates for IKEv2. OpenVPN can check a
|
||||
certificate, a password, or both. With both, a certificate alone and a password alone are not
|
||||
enough, but `openvpn-auth-ldap` checks only the password of the user name given at login, which
|
||||
need not be the user of the certificate.
|
||||
Context: The router CA issues client certificates for IKEv2. OpenVPN can check a certificate, a
|
||||
password, or both. With both, a certificate alone and a password alone are not enough, but
|
||||
`openvpn-auth-ldap` checks only the password of the user name given at login, which need not be
|
||||
the user of the certificate.
|
||||
|
||||
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`,
|
||||
@@ -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
|
||||
in one file.
|
||||
|
||||
Consequences: The web service still imports nothing outside the standard library, and the same
|
||||
program serves the command line and the API. Each request starts a process, which is fine for
|
||||
user administration. The API is protected by the token of `/config`, so whoever has the secret
|
||||
can manage users.
|
||||
Consequences: The web service imports nothing outside the standard library, and the same program
|
||||
serves the command line and the API. Each request starts a process, which is fine for user
|
||||
administration. The API is protected by the token of `/config`, so whoever has the secret can
|
||||
manage users.
|
||||
|
||||
## ADR-7: The web service starts on its own and reads its inputs at each request
|
||||
|
||||
Status: Accepted
|
||||
|
||||
Context: The web service took the address to listen on, the port, the token secret and the
|
||||
features from `/run/vpn-router/web.json`, written by `vpn-router-setup`, and ended with a failure
|
||||
while the file or the internal address was missing. It was ordered after `vpn-router-setup`, so a
|
||||
problem in the setup kept the status page, the tool to see that problem, from running.
|
||||
Context: The status page is the tool to see a problem of `vpn-router-setup`, so it must run
|
||||
whatever state setup is in, also before setup has run and while it fails. The internal address,
|
||||
`web.port` and `web.enabled` can change at any time.
|
||||
|
||||
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
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
`vpn-router.json`. A change of the secret applies to the next request. The interfaces are read
|
||||
every 30 seconds, which costs a few `ip` commands.
|
||||
restart. A change of the secret applies to the next request. The interfaces are read every 30
|
||||
seconds, which costs a few `ip` commands.
|
||||
|
||||
## ADR-8: /config leaves secret options out
|
||||
|
||||
Status: Accepted
|
||||
|
||||
Context: `/config` showed the secret options of the configuration with their values replaced by
|
||||
`********`. That still tells a reader which secrets are set, and a secret has no use outside the
|
||||
router, so a client never needs it.
|
||||
Context: A secret has no use outside the router, so a client never needs it, and showing even
|
||||
that a secret is set tells a reader more than it needs.
|
||||
|
||||
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`,
|
||||
`password`, `token` or a `_key` ending in the name.
|
||||
options at all. They are recognised by name: `psk`, and options with `secret`, `password`,
|
||||
`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
|
||||
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
|
||||
|
||||
@@ -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
|
||||
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
|
||||
MUI application in the style of manage-ui is built with Vite when the package is built and
|
||||
shipped as static files. A Python server, using the Ubuntu packages `python3-jwt` and `python3-requests`, serves them and
|
||||
passes every `/api/<path>` call to the router's `/<path>`, signing a short-lived token with the
|
||||
router's secret; it holds no logic of the router's own. It reads its settings from
|
||||
`/etc/vpn-router-manager/manager.json` at each request; on the router the settings not given
|
||||
come from `vpn-router.json`, and it listens on the internal address like the status web
|
||||
service, with the same discovery thread. `vpn-router-setup` opens the manager's port on the
|
||||
internal interface while the manager is installed, so the firewall stays in one place. The
|
||||
manager asks for no authentication yet.
|
||||
Decision: `vpn-router-manager` is a Debian package of its own, built in the repository
|
||||
`linux-cloud-router-manager`. A React and MUI application in the style of manage-ui is built
|
||||
with Vite when the package is built and shipped as static files. A Python server, using the
|
||||
Ubuntu packages `python3-jwt` and `python3-requests`, serves them and passes every `/api/<path>`
|
||||
call to the router's `/<path>`, signing a short-lived token with the router's secret; it holds
|
||||
no logic of the router's own. It reads its settings from `/etc/vpn-router-manager/manager.json`
|
||||
at each request; on the router the settings not given come from `vpn-router.json`, and it
|
||||
listens on the internal address like the status web service, with the same discovery thread.
|
||||
`vpn-router-setup` opens the manager's port on the internal interface while the manager is
|
||||
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
|
||||
reaches the browser. The application checks the shape of every answer of the router. Anyone who
|
||||
reaches the manager's port manages the router, so on another system it listens on `127.0.0.1`
|
||||
unless told otherwise. The router's firewall depends on whether the manager is installed, and a
|
||||
changed manager port needs `vpn-router-setup` to run again.
|
||||
Consequences: The router needs no Node.js; the package needs only `python3` and packages of the
|
||||
Ubuntu archive. The secret never reaches the browser. The application checks the shape of every
|
||||
answer of the router. Anyone who reaches the manager's port manages the router, so on another
|
||||
system it listens on `127.0.0.1` unless told otherwise. The router's firewall depends on whether
|
||||
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
|
||||
|
||||
Status: Accepted
|
||||
|
||||
Context: The user directory and the CA were provisioned, and `/users` was served, only while
|
||||
OpenVPN or road-warrior access was enabled. Users could not be prepared before a P2S system was
|
||||
switched on, and the Router Manager could not manage them on a router without one.
|
||||
Context: Users are prepared before a P2S system is switched on, and the Router Manager manages
|
||||
them on any configured router, with or without one.
|
||||
|
||||
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.
|
||||
|
||||
@@ -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` | `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` | `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 |
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
| 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.
|
||||
|
||||
```bash
|
||||
curl http://10.0.0.4:8080/
|
||||
curl http://10.0.0.4:8080/status
|
||||
curl http://10.0.0.4:7081/
|
||||
curl http://10.0.0.4:7081/status
|
||||
```
|
||||
|
||||
### The /config page
|
||||
@@ -466,7 +466,7 @@ print(f'{head}.{body}.{sign}')
|
||||
```
|
||||
|
||||
```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
|
||||
@@ -491,7 +491,7 @@ network in plain HTTP, like the token.
|
||||
|
||||
```bash
|
||||
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
|
||||
@@ -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
|
||||
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`,
|
||||
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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
feature: users and their certificates no longer need a P2S system enabled.
|
||||
|
||||
@@ -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.
|
||||
MANAGER = pathlib.Path('/usr/lib/vpn-router-manager/manager')
|
||||
MANAGER_CONFIG = pathlib.Path('/etc/vpn-router-manager/manager.json')
|
||||
MANAGER_PORT = 8090
|
||||
MANAGER_PORT = 7080
|
||||
|
||||
FILTER_ANCHOR = '# End required lines'
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ SCHEMA = {
|
||||
'wireguard': {'enabled': False, 'address': '', 'listen_port': 51820},
|
||||
'openvpn': {'enabled': False, 'address_pool': '', 'port': 1194, 'protocol': 'udp'},
|
||||
'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.
|
||||
|
||||
@@ -52,7 +52,7 @@ STATE_FILE = '/run/vpn-router/web.json'
|
||||
CONFIG_FILE = '/etc/vpn-router/vpn-router.json'
|
||||
|
||||
#: The port when web.port is not set.
|
||||
DEFAULT_PORT = 8080
|
||||
DEFAULT_PORT = 7081
|
||||
|
||||
#: Seconds between two discoveries of where the service should listen.
|
||||
DISCOVERY_SECONDS = 30
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
},
|
||||
"web": {
|
||||
"enabled": true,
|
||||
"port": 8080,
|
||||
"port": 7081,
|
||||
"jwt_secret": ""
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user