From 448ce05b3a347f935b0df3e8d5eda4ae36c4fd0c Mon Sep 17 00:00:00 2001 From: Slawomir Koszewski Date: Sun, 25 Oct 2026 23:03:03 +0100 Subject: [PATCH] fix: the PKI is kept in ou=pki of the router's directory instead of files under /etc/vpn-router/pki, and the router's site-to-site certificate names only the identities its connections present, and /ca/bundle gives the certificate bundle of a CA. --- ADR.md | 54 +- README.md | 55 +- debian-package/debian/changelog | 24 + debian-package/src/usr/lib/vpn-router/setup | 26 +- .../src/usr/lib/vpn-router/vpnrouter.py | 4 + .../src/usr/lib/vpn-router/vpnrouter_ldap.py | 59 +- .../src/usr/lib/vpn-router/vpnrouter_pki.py | 1040 ++++++++++------- .../src/usr/lib/vpn-router/vpnrouter_users.py | 19 +- debian-package/src/usr/lib/vpn-router/web | 6 + debian-package/src/usr/sbin/vpn-router-ca | 18 +- 10 files changed, 796 insertions(+), 509 deletions(-) diff --git a/ADR.md b/ADR.md index a6e250b..d5a39bb 100644 --- a/ADR.md +++ b/ADR.md @@ -152,8 +152,8 @@ Status: Accepted 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 PKI, whose state is one -file, so two changes must not run at the same time. The web unit is sandboxed with +passwords and person attributes, and both change the directory and the PKI, whose CRL numbers and +CRLs two changes must not write at the same time. The web unit is sandboxed with `ProtectSystem=strict`. Decision: `vpnrouter_users.py` holds the user operations, the checks and a session that takes a @@ -162,9 +162,9 @@ lock and the connection of the user manager account. The web service imports it 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, its `RuntimeDirectory`, which systemd creates before the service starts, also before setup has run, and keeps when it stops, as setup's state -files are there too; `/etc/vpn-router/pki` for the certificates and the PKI's state, and -`/etc/swanctl/x509crl`, `/etc/openvpn/server` and `/var/lib/vpn-router/pki-publish` for the -CRLs a revocation signs. The PKI module, `vpnrouter_pki.py`, works the same way for `/ca`, +files are there too, and `/etc/swanctl/x509crl`, `/etc/openvpn/server` and +`/var/lib/vpn-router/pki-publish` for the copies of the CRLs a revocation signs; the PKI itself is +in the directory (ADR-32). The PKI module, `vpnrouter_pki.py`, works the same way for `/ca`, `vpn-router-ca` and `vpn-router-setup`, and its lock is the users' lock, as adding and removing a user issue and revoke a certificate. @@ -757,8 +757,8 @@ of common name and the subject alternative names a certificate may have; `vpn-se `vpn-user` are issued by the router only, so no certificate for another use comes from the P2S CA. The module `vpnrouter_pki.py`, with `python3-cryptography` from the Ubuntu archive, builds the keys, certificates, CSRs and CRLs in the processes that need them, as the user module does -(ADR-6); the settings are the `pki` section of `vpn-router.json` and the state is -`pki/state.json`. Keeping the root key off the router is recommended, not required: while it is +(ADR-6); the settings are the `pki` section of `vpn-router.json`, and the CAs and the +certificates are kept in the router's directory (ADR-32). Keeping the root key off the router is recommended, not required: while it is on the router a new issuing CA is signed at once, and while it is not the CA gets its key and a CSR, signed offline and imported with `vpn-router-ca` or `/ca`. The root key itself crosses neither the API nor the Manager. For a certificate of something other than the router, the router @@ -808,14 +808,17 @@ secret to distribute. Azure's VPN gateway authenticates site-to-site connections keys only. Decision: A connection's `auth` is `psk`, the default, or `certificate`. With `certificate` the -router presents one site-to-site certificate of its `s2s` CA, whose names are `wan.local_fqdn`, -the public address and the internal address, so it fits every `local_id_mode`; the peer's +router presents one site-to-site certificate of its `s2s` CA, whose names are `wan.local_fqdn` and +the local identities the connections with `certificate` use, the public or the internal address of +their `local_id_mode`, and no address no connection presents, such as an internal address the peers +never see; the peer's certificate must name the connection's `id` and be issued by the router's `s2s` CA or, when the connection has `remote_ca`, by that CA, its PEM certificate kept in the connection and copied to `/etc/swanctl/x509ca/s2s-.pem`. A peer without a PKI gets its certificate from the router. Consequences: The connection Azure's gateway needs stays `psk`, so the platform writes nothing -new. The router's site-to-site certificate is issued again when an address it names changes. The +new. The router's site-to-site certificate is issued again when the set of names it needs changes, +such as when a connection's `local_id_mode` or its address changes. The router does not check a remote CA's revocations unless strongSwan can fetch the CRL its certificates name. @@ -861,3 +864,34 @@ Manager shows which one is in effect. Consequences: The first setup run with HTTPS starts the installation and serves HTTPS on the run that follows it. The key of Caddy's repository is trusted as downloaded over HTTPS, the way Caddy's installation instructions do it. Going back to the archive's Caddy is a manual downgrade. + +## ADR-32: The PKI is kept in the router's directory + +Status: Accepted + +Context: The CAs, their keys and CRLs, the inventory of the certificates issued and their +revocations, and the router's and the users' certificates and keys need a store. Files with a +JSON state file cannot be searched, and are copied apart from the users the certificates belong +to. The router runs slapd, which holds the users and is provisioned on every +configured router (ADR-10), and PostgreSQL, which holds the refresh tokens and the metrics, may +be another server (`web.database`) and is in a backup only with `backup.metrics`. RFC 4523 +defines `pkiCA` with `cACertificate` and `certificateRevocationList`, and `userCertificate`, which +OpenLDAP's core schema includes. + +Decision: The PKI is held in `ou=pki` of the router's directory. A CA is an entry of the package's +class `vpnRouterAuthority` with `cACertificate` and `certificateRevocationList`, its key, its CSR, +its CRL number, the source of the root CRL and the time it was retired; a certificate is an entry +of `vpnRouterCertificate` named by its serial number under its CA, with `userCertificate`, its +kind, template, names, expiry and revocation, and the key of a router or user certificate. A +user's certificate is also the `userCertificate` of the user's entry. An ACL lets no account read +`vpnRouterPrivateKey`; only root, the database's root DN, does, through the local socket. A +retired CA keeps its entry, renamed, so its certificates stay in the inventory. A root supplied +from outside, by cloud-init or by hand, is placed as files before the first setup with a platform, +which imports and removes them. strongSwan, OpenVPN, Caddy and the system's trust store read files, +so setup and every revocation write them from the directory. Setup provisions the directory before +the PKI; without the directory it leaves the PKI and its files as they are and configures the rest. + +Consequences: `slapcat` writes the users and the PKI into every backup in one consistent copy, and a +restore brings both back together. The PKI depends on slapd, as the users already do, and not on +PostgreSQL. The directory holds the CA keys, so it is as sensitive as the CA itself. The schema +grows by the package's PKI attributes and classes, which setup adds to slapd's configuration. diff --git a/README.md b/README.md index f0ee508..4d57ebb 100644 --- a/README.md +++ b/README.md @@ -202,8 +202,9 @@ certificate; all such connections are written to `/etc/swanctl/conf.d/site-to-si Traffic from a connection's `local_cidrs` to its `cidrs` is not masqueraded. With `auth` `certificate`, the router authenticates with its site-to-site certificate, which the -site-to-site issuing CA signs for `wan.local_fqdn`, the public address and the internal -address, so it fits every `local_id_mode`. The peer's certificate must name `id` and be issued +site-to-site issuing CA signs for `wan.local_fqdn` and for the public or the internal address +that a connection with `auth` `certificate` uses as its identity (`local_id_mode` `public_ip` or +`internal_ip`); it is issued again when that set of names changes. The peer's certificate must name `id` and be issued by the router's site-to-site CA, or by the CA of `remote_ca`. A peer with no PKI of its own gets its certificate from the router (template `site-to-site`, see Certificates), with the root and the site-to-site CA certificates as its trust anchors. @@ -371,18 +372,27 @@ services - that is what `systemctl restart vpn-router-setup` adds. ## Certificates -The router has a two-level certificate authority in `/etc/vpn-router/pki` (mode 0700), kept by -`vpn-router-setup` and managed with `vpn-router-ca`, `vpn-router-user` and `/ca` of the status -web service, which the Router Manager uses: +The router has a two-level certificate authority, kept by `vpn-router-setup` and managed with +`vpn-router-ca`, `vpn-router-user` and `/ca` of the status web service, which the Router Manager +uses. It is held in `ou=pki,dc=vpn-router,dc=local` of the router's directory, next to the users: ``` -root/cert.pem root/key.pem root/crl.pem the root CA; key.pem is absent while the root is offline -ca//cert.pem key.pem crl.pem csr.pem an issuing CA; csr.pem while it waits for the offline root -router/vpn-server_cert.pem _key.pem the router's VPN server certificate -router/site-to-site_cert.pem _key.pem the router's site-to-site certificate -users/_cert.pem _key.pem the users' certificates -retired/-