feature: the web service is an OpenID Provider, which signs the directory's users in to other systems, the clients being service accounts with redirect URIs.
Release / release (push) Successful in 13s

This commit is contained in:
slawek committed 2026-10-26 11:15:12 +01:00
1 parent d1d565b1d9
commit 8cac21845a
17 files changed
+1189 -58

No files matched your search

+44 -5
View File
@@ -98,8 +98,8 @@ Context: OpenVPN needs user accounts with passwords, and the users are meant to
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.
checks a password by binding as the user, so the hash scheme is slapd's alone. The router's own
OpenID Provider (ADR-37) signs the directory's users in to other systems.
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
@@ -979,7 +979,7 @@ Consequences: Peers follow their users: disabling a user drops the user's device
directory's backup holds them. A peer cannot exist without a VPN user. A change restarts
`wg-quick@wg0` through setup, which briefly interrupts the other peers.
## ADR-36: Passkeys sign in to the Router Manager only, verified by the router with python3-fido2
## ADR-36: Passkeys sign in to the Router Manager and the OpenID Provider, verified by the router with python3-fido2
Context: Typing the password and a TOTP code at every sign-in to the Router Manager is tedious.
WebAuthn needs a browser, HTTPS and a domain name; OpenVPN, IKEv2, WireGuard, SSH and sudo have no
@@ -998,9 +998,48 @@ passkey sign-in gives the same tokens as `/auth`; as it binds no password, the r
the user is enabled, not locked and not a service account. The begun steps are kept in the web
service's memory for 2 minutes. The browser converts the options and answers with
`PublicKeyCredential.parseCreationOptionsFromJSON`, `parseRequestOptionsFromJSON` and `toJSON`.
The OpenID Provider's sign-in page (ADR-37) takes a passkey the same way.
Consequences: Passkeys are a convenience for the Router Manager; the password and code stay for
everything else, the shell included. A changed `wan.local_fqdn` orphans every passkey. The
Consequences: Passkeys are a convenience for the Router Manager and the OpenID Provider; the
password and code stay for everything else, the shell included. A changed `wan.local_fqdn` orphans every passkey. The
Router Manager on another name, or in development on `localhost`, cannot use them. Failed passkey
sign-ins do not count towards the directory's lockout, but the sign-in calls are rate limited.
Browsers without the JSON methods of WebAuthn Level 3 cannot add or use passkeys.
## ADR-37: The web service is an OpenID Provider built on python3-oauthlib; its clients are service accounts
Status: Accepted
Context: Other systems, such as dashboards, should sign the router's users in with the directory's
passwords, TOTP codes and passkeys, without a second user store. The Ubuntu archive has no
packaged identity provider. `python3-oauthlib` and `python3-jwt` are in main for noble and
resolute; oauthlib implements the OpenID Connect flows and leaves the checks, the storage and the
ID token's signature to the application. `python3-authlib` and `python3-jwcrypto` are in universe
only. The web service already checks passwords and passkeys, rate limits sign-ins and keeps
tokens in PostgreSQL, and Caddy serves it over HTTPS on `wan.local_fqdn`. ADR-3's HS256 key must
not leave the router, but a relying party verifies an ID token itself.
Decision: The web service is the OpenID Provider, in `vpnrouter_oidc`, while `oidc.enabled` is
true; its issuer is its HTTPS site, `https://<wan.local_fqdn>:<https.web_port>`, without the port
on 443, and it answers the provider's paths only on `127.0.0.1`, where Caddy passes them. Only
the authorization code flow with PKCE S256 and the refresh token grant are allowed. A client is a
service account with redirect URIs, kept with the account in the new attributes
`vpnRouterRedirectUri`, `vpnRouterPostLogoutRedirectUri` and `vpnRouterAllowedGroup` of
`vpnRouterAccount`: its name is the client ID and its password the secret, checked with a bind.
Allowed groups limit who may sign in; without them every user may. A registered client is
trusted, so there is no consent page. The sign-in page is the web service's own, a Jinja2
template with the password, the code and a passkey, so the provider works without the Router
Manager. ID tokens are RS256, signed with an RSA key the web service makes on its first run
(`/etc/vpn-router/oidc.key`), published as a JWK named by its RFC 7638 thumbprint; `sub` is the
entry's `entryUUID`. Codes, access and refresh tokens and sessions are kept as their SHA-256 in
the database of the refresh tokens, defined in `/usr/share/vpn-router/sql/oidc.sql` with
procedures and functions the module calls; a process loads the file at its first connection. The
operations that revoke the refresh tokens of `/auth` also revoke them, and every use of a code or
token checks the user and the client's allowed groups again.
Consequences: The package depends on `python3-oauthlib` and `python3-jwt`. The provider needs
HTTPS, a public name and the web service. Clients are managed like service accounts, by the CLI,
`/service-accounts` and the Router Manager; a group a client allows cannot be removed, and renaming
it renames it in the clients. The signing key does not rotate; replacing it invalidates every ID
token issued. Wrong client secrets lock the client like any account. The implicit and hybrid
flows, dynamic client registration and public clients without a secret are not offered.