feature: users have a first name, a last name and an e-mail address.
Release / release (push) Successful in 11s

This commit is contained in:
slawek committed 2026-10-24 01:47:50 +02:00
1 parent b9003bacb9
commit 08693f7e68
9 files changed
+872 -290

No files matched your search

+54 -30
View File
@@ -27,7 +27,7 @@ port is reachable from the external interface only when it is in `exposed_ports`
in `DOCKER-USER` are not touched, because the package owns only its own chain. Restarting Docker
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 small status web service, bound to the internal address
Status: Accepted
@@ -35,15 +35,15 @@ Context: The router has no way to see its state without a shell. A check from th
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 listens on the address of the internal interface and on
Decision: The `vpn-router-web` unit runs one Python script on the standard library's
`http.server`, with the package's own modules and the Python packages of the Ubuntu archive that
the package depends on, and no web framework. 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.
the features and the facts it resolves to `/run/vpn-router/web.json`.
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. `/` and `/status`
@@ -57,14 +57,14 @@ browser on a network without internet shows the unstyled page, and `curl` is una
Status: Accepted
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.
the router, which `/` and `/status` must not show. The service listens on plain HTTP on the
internal network, and its configuration file is readable by root only. The platform that deploys
the router generates one secret and gives it to the router's configuration and to the client.
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
algorithm, because a shared secret is what both sides have; RS256 or ES256 would need a key pair
and its distribution. 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 configuration
shown has no secret options (ADR-8).
@@ -125,24 +125,27 @@ checks certificates only; removing a user revokes the certificate for both. A us
certificate expires after the CA's default of a year and is renewed by removing and adding the
user again.
## ADR-6: The web service manages users by running vpn-router-user
## ADR-6: The web service and vpn-router-user share one module for users
Status: Accepted
Context: The web service has only the standard library (ADR-2), and managing users needs
`python3-ldap` and `simple-ca`. Its unit is sandboxed with `ProtectSystem=strict`.
Context: Users are managed from the command line, by an operator at a terminal, and through
`/users` of the web service, by the Router Manager. Both must apply the same rules to names,
passwords and person attributes, and both change the directory and the CA. `simple-ca` keeps
the CA's state in one file, so two changes must not run at the same time. The web unit is
sandboxed with `ProtectSystem=strict`.
Decision: `/users` runs `vpn-router-user` and returns its JSON answer, mapping its exit status
to the HTTP status. Passwords go to the program on standard input, so they never appear in a
command line. The unit allows writing the paths the program needs: `/run/vpn-router` for its
lock, `/etc/vpn-router/pki` for the certificates and the CA's state, and `/etc/swanctl/x509crl`
for the published CRL. A lock serializes the program's runs, because `simple-ca` keeps its state
in one file.
Decision: `vpnrouter_users.py` holds the user operations, the checks and a session that takes a
lock and the connection of the user manager account. The web service imports it and maps a
`Failure` to the HTTP status; `vpn-router-user` imports it and adds the argument parsing and
the output, text for people and JSON with `--json`. The web unit allows writing the paths the
operations need: `/run/vpn-router` for the lock, `/etc/vpn-router/pki` for the certificates and
the CA's state, and `/etc/swanctl/x509crl` for the published CRL.
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.
Consequences: The rules exist once. A request starts no process, and a password never leaves
the process that received it. The web service imports `python3-ldap` and the package's
configuration module. 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
@@ -154,13 +157,13 @@ whatever state setup is in, also before setup has run and while it fails. The in
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
`general.mode` directs, with the discovery code in `vpnrouter_net.py`, which uses only the
standard library and which `vpnrouter.py` uses too, and `web.port` and `web.enabled` from the
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 writes, because they need the
configuration code; while that file is missing the router shows no features. The unit is not
`general.mode` directs, with the discovery code in `vpnrouter_net.py`, which `vpnrouter.py` uses
too, and `web.port` and `web.enabled` from the 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 writes when it applies the configuration, so the service shows what setup
applied; 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
@@ -223,3 +226,24 @@ profile needs OpenVPN to be enabled.
Consequences: slapd always holds the directory and the CA always exists on a configured router,
even when no P2S system uses them. An operator who wants a CA of their own must place it before
choosing the platform.
## ADR-11: Groups are groupOfNames entries that never become empty
Status: Accepted
Context: Users are to be organised in groups. Groups record membership only; nothing on the
router reads them. LDAP clients that read groups, such as `openvpn-auth-ldap` and the LDAP
connectors of OIDC providers, expect `groupOfNames` with member DNs, and OpenLDAP's core schema
requires a `groupOfNames` to have at least one `member`.
Decision: A group is `cn=<name>,ou=groups,dc=vpn-router,dc=local`, a `groupOfNames` with the
DNs of its members and an optional `description`. A group is created with at least one member,
and taking out its last member is refused; such a group is removed instead. Removing a user
takes it out of its groups and is refused while the user is the last member of one. The group
operations are in `vpnrouter_users.py` with the user operations, so they share the session and
its lock; `vpn-router-group` is a tool of its own beside `vpn-router-user`, and the web service
serves them under `/groups`. Group names follow the rules of user names.
Consequences: A group always satisfies the schema without a placeholder member, and an empty
group cannot exist. Removing a user or a member can need another step first. A later use of
groups, such as allowing OpenVPN only to some, can read them as they are.