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.
|
||||
|
||||
Reference in new issue
Block a user