diff --git a/ADR.md b/ADR.md index dc0d0a0..a6e250b 100644 --- a/ADR.md +++ b/ADR.md @@ -121,25 +121,30 @@ and it is backed up with `slapcat`, not by copying files. Status: Accepted -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. +Context: The P2S issuing CA (ADR-27) issues the users' client certificates, for IKEv2 and +OpenVPN. 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. OpenVPN trusts every chain to a +CA of its `ca` file and has no option that accepts one intermediate CA only, while the root also +signs the site-to-site and the general-purpose CAs. -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`, -which accepts the login only when the user name equals the certificate's common name. OpenVPN -calls both and needs both to pass. A user's certificate has the user name as its common name, is -issued when the user is added and revoked when the user is removed. The CRL is created with the -CA, so that `crl-verify` has a file from the start, and is valid for ten years, because the router -renews it only when it revokes a certificate. The server uses the router's own certificate, whose -common name is `wan.local_fqdn`; the client profile checks that name (`verify-x509-name`), since -the CA's certificates carry both the server and the client purpose. +Decision: OpenVPN checks the certificate against the root and the P2S CA and their CRLs, which +it needs for every CA of the chain, the password with `openvpn-auth-ldap`, and runs +`openvpn-same-user` as `auth-user-pass-verify` with `via-env`, which accepts the login only when +the user name equals the certificate's common name. `openvpn-issuer`, as `tls-verify`, accepts +the certificate at depth 1 only when its SHA-256 fingerprint, which OpenVPN gives the script, +is the P2S CA's. OpenVPN calls all of them and needs all to pass. A user's certificate has the +user name as its common name, is issued with the template `vpn-user`, client authentication +only, when the user is added, and revoked when the user is removed. The server uses the router's +VPN server certificate, whose common name is `wan.local_fqdn`; the client profile checks that +name (`verify-x509-name`). Consequences: Disabling a user locks the password, which stops OpenVPN but not IKEv2, which -checks certificates only; removing a user revokes the certificate for both. A user's -certificate expires after the CA's default of a year and is renewed by removing and adding the -user again. +checks certificates only; removing a user revokes the certificate for both. A certificate of +another issuing CA with a user's name does not reach the password check. A user's certificate +expires after the days of `vpn-user` and is renewed by removing and adding the user again. An +expired root CRL stops every OpenVPN client, so an offline root needs its CRL renewed in time +(ADR-28). ## ADR-6: The web service and vpn-router-user share one module for users @@ -147,9 +152,9 @@ 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 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`. +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 +`ProtectSystem=strict`. 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 @@ -157,11 +162,14 @@ 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 CA's state, and -`/etc/swanctl/x509crl` for the published CRL. +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`, +`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. -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 +Consequences: The rules exist once. A request starts no process but `swanctl --load-creds` after +a revocation, 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 tokens of ADR-3, so only members of `admins` and the holders of an admin token manage users. @@ -245,12 +253,13 @@ Context: Users are prepared before a P2S system is switched on, and the Router M 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. -`/users` and `vpn-router-user` rely only on the directory and the CA. Only the OpenVPN client -profile needs OpenVPN to be enabled. +slapd and the PKI (ADR-27) with its CRLs; the router's certificates follow once `wan.local_fqdn` +is set. `/users` and `vpn-router-user` rely only on the directory and the P2S CA. Only the +OpenVPN client 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 +Consequences: slapd always holds the directory and the CAs exist on a configured router, even +when no P2S system uses them; with an offline root the P2S CA, and so the users' certificates, +wait for its certificate. An operator who wants a root CA of their own must place it before choosing the platform. ## ADR-11: Groups are groupOfNames entries that never become empty @@ -721,9 +730,134 @@ state in one directory and then archives or sends it. it stops their services, keeps what it replaces and lets `vpn-router-setup` rebuild the rest. The API offers no restore, so a stolen admin session cannot replace the router's keys. -Consequences: A backup holds every secret of the router, including the CA key, in plain form +Consequences: A backup holds every secret of the router, including the keys of the issuing CAs +and the root key while it is on the router, in plain form unless a PBS key file encrypts it, so where it is kept must be as safe as the router. A restore onto another VM brings back the old interface names and addresses, which may need changing. The timer's schedules are systemd's shorthands, so all backups of one schedule start at the same moment. A change of the users between the end of the copy and the next backup is in no backup until then. + +## ADR-27: A two-level CA with dedicated issuing CAs, whose root may be offline + +Status: Accepted + +Context: One self-signed CA signed every certificate of the router with the same server and +client purposes, so a certificate for any use was also a VPN client certificate, and the key +that signed everything had to stay on the router. The router needs CAs for the VPN clients, for +site-to-site certificates and for the administrator's own uses, and a root whose key can be kept +elsewhere. The `simple-ca` script handled one level and ran as a separate process with its own +state file. + +Decision: The root signs only issuing CAs, path length 1; the issuing CAs, path length 0, sign +the certificates. `p2s` signs the router's VPN server certificate and the users' certificates, +`s2s` the site-to-site certificates, and the CAs of `pki.issuing_cas` what their templates +allow. A template fixes the key, the lifetime, the key usages, the extended key usages, the kind +of common name and the subject alternative names a certificate may have; `vpn-server` and +`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 +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 +signs the requester's CSR, or makes the key and returns it once in a PKCS#12 file, keeping only +the certificate, so a key it gave away cannot leak from it later. The PKCS#12 file uses 3DES and +SHA-1 MACs, the encryption the key stores of Windows and macOS import. strongSwan reads only the +first certificate of a PEM file, so each CA has a file of its own in `/etc/swanctl/x509ca`; +road-warrior and site-to-site name their CA in `cacerts`, which restricts the trust chain to it. + +Consequences: The P2S CA's certificates are accepted by road-warrior and OpenVPN only, the site-to-site CA's by +site-to-site only. With an offline root, new issuing CAs and the root CRL need the offline step, +and a router whose P2S CA waits has no VPN server certificate and so no P2S access. The router +keeps the users' keys, as the OpenVPN profile embeds them. + +## ADR-28: CA certificates and CRLs are published at configurable endpoints, and CRLs are short-lived + +Status: Accepted + +Context: Clients outside the router, such as Windows and servers using the general-purpose CAs, +find the issuer's certificate and the CRL through the AIA and CRL distribution point extensions, +and cache a CRL until its next update. The router's CRLs were valid for ten years and published +nowhere. Caddy serves files itself, and the web service already answers on the status port. + +Decision: `pki.publish` lists the endpoints, in the order the URLs appear in the certificates: +plain HTTP on the status port, served by the web service; plain HTTP on port 80, HTTPS on the +status HTTPS port and HTTPS on port 443, served by Caddy's `file_server`. The files are DER, as +RFC 5280 asks of these URLs, copied to `/var/lib/vpn-router/pki-publish`, outside the 0700 PKI +directory, so that Caddy's user reads them. Ports 80 and 443 serve only the CA files and are opened +on both interfaces when chosen. The CRLs of the issuing CAs are valid for `pki.crl_days`, a week +by default, signed again at every revocation and by a daily timer once half their validity has +passed; the root CRL has its own `pki.root_crl_days`, half a year by default, so an offline root +needs its CRL signed twice a year, not weekly. + +Consequences: A client that caches a CRL sees a revocation within a week. The URLs are fixed in a +certificate when it is signed, so a change of the endpoints or of `wan.local_fqdn` applies to +certificates signed afterwards. HTTPS URLs are offered for clients that cannot use plain HTTP, +although a client checking the revocation of the HTTPS server's own certificate cannot use them; +Caddy's certificate is Let's Encrypt's, so the router's endpoints do not depend on themselves. + +## ADR-29: Site-to-site connections authenticate with a pre-shared key or certificates + +Status: Accepted + +Context: Site-to-site connections used pre-shared keys only. Peers that are routers of this +package, or have a PKI of their own, can authenticate with certificates, which leaves no shared +secret to distribute. Azure's VPN gateway authenticates site-to-site connections with pre-shared +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 +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 +router does not check a remote CA's revocations unless strongSwan can fetch the CRL its +certificates name. + +## ADR-30: A general-purpose issuing CA can back an ACME server of Caddy + +Status: Accepted + +Context: Servers that use a general-purpose CA need their certificates renewed, which ACME clients +automate. Caddy, which the router runs for HTTPS, has an ACME server built on a CA of its `pki` +app. Caddy 2.6.2 of the Ubuntu archive needs the key file of that CA's root, never renews an +intermediate given to it, keeps no CRL and takes only the CA in the `acme_server` directive, +issuing certificates valid for 12 hours. Its ACME handler answers every path under `/acme/`. + +Decision: An issuing CA of `pki.issuing_cas` with `acme` makes Caddy serve an ACME server for it +at `/acme//` of the HTTPS status port, each in a `handle` block of its own. The issuing CA's +certificate and key are given to Caddy as both the root and the intermediate of a `pki` CA, copied +to `/etc/caddy/vpn-router-pki//` for Caddy's group, so that the router's root key never +reaches Caddy and may be offline. + +Consequences: The certificates Caddy issues chain to the router's root through the issuing CA, are +not in the router's inventory, follow no template and are not revoked; their short lifetime takes +the place of revocation. An issuing CA waiting for the offline root has no ACME server. + +## ADR-31: vpn-router-setup installs Caddy from the Ubuntu archive or from Caddy's repository + +Status: Accepted + +Context: Caddy 2.6.2 of the Ubuntu archive lacks later features, such as a pki root without its key +and the settings of the ACME server. Caddy's own repository has current versions of the package of +the same name. A dependency on `caddy` installs the archive's package with `vpn-router`. apt cannot +run inside `vpn-router-setup` when the package's installation runs setup, as dpkg holds its lock. + +Decision: `vpn-router` only suggests `caddy`. While HTTPS is on, setup has +`vpn-router-caddy.service` install it from `https.caddy_source`: `ubuntu`, or `upstream`, for +which the unit writes the deb822 source and the key it downloads over HTTPS from Caddy's +repository. The unit waits for dpkg's lock and runs setup again when Caddy is in place. +`vpn-router-caddy use-upstream` switches an installed router. Upstream's versions sort above the +archive's, so once the repository is configured apt keeps upstream's Caddy without a pin, and +`ubuntu` has no effect; nothing downgrades Caddy on its own, as a downgrade may break the +configuration and is the administrator's decision. Only the CLI switches the source; the Router +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. diff --git a/README.md b/README.md index 440e85c..be54721 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,11 @@ A Debian package that configures a Linux host or virtual machine as a VPN router: -- site-to-site IKEv2 IPSec with pre-shared key authentication (strongSwan swanctl) -- road-warrior (P2S) access using IKEv2 EAP-TLS, with a bundled certificate authority +- site-to-site IKEv2 IPSec with pre-shared key or certificate authentication (strongSwan swanctl) +- road-warrior (P2S) access using IKEv2 EAP-TLS +- a two-level certificate authority: a root, which may be kept offline, and issuing CAs for the VPN + clients, for site-to-site and for general use, with certificate templates, published certificates + and CRLs, and an optional ACME server - OpenVPN (P2S) access with a certificate and a password per user, the users kept in a local LDAP directory and managed with `vpn-router-user` or the web service - optional WireGuard endpoint @@ -110,7 +113,6 @@ must have the type shown below; an unknown section or setting is an error. | `site_to_site` | | object | | The site-to-site connections, each a name and an object of the settings below | | `p2s` | `enabled` | boolean | `false` | Road-warrior access | | `p2s` | `address_pool` | string | | CIDR assigned to road-warrior clients | -| `p2s` | `ca_name` | string | `VPN Router CA` | Common name for a locally created CA | | `wireguard` | `enabled` | boolean | `false` | WireGuard endpoint | | `wireguard` | `address` | string | | Address and prefix for `wg0`, for example `192.168.200.1/24` | | `wireguard` | `listen_port` | integer | `51820` | UDP port | @@ -130,6 +132,18 @@ must have the type shown below; an unknown section or setting is an error. | `https` | `email` | string | | The e-mail address of the Let's Encrypt account | | `https` | `web_port` | integer | `7444` | HTTPS port of the status web service | | `https` | `manager_port` | integer | `7443` | HTTPS port of the Router Manager | +| `https` | `caddy_source` | string | `ubuntu` | Where Caddy is installed from: `ubuntu`, the Ubuntu archive, or `upstream`, Caddy's own repository (see HTTPS) | +| `pki` | `root_name` | string | `VPN Router Root CA` | Common name of the root CA, when the router creates it | +| `pki` | `root_days` | integer | `7300` | Days the root CA's certificate is valid | +| `pki` | `ca_days` | integer | `3650` | Days an issuing CA's certificate is valid | +| `pki` | `key` | string | `rsa-4096` | Key of the root and the issuing CAs: `rsa-2048`, `rsa-3072`, `rsa-4096`, `ec-p256` or `ec-p384` | +| `pki` | `crl_days` | integer | `7` | Days the CRLs of the issuing CAs are valid | +| `pki` | `root_crl_days` | integer | `180` | Days the root CRL is valid, when the router signs it | +| `pki` | `publish` | list | `http` | Where the CA certificates and CRLs are published: `http`, `http-80`, `https`, `https-default` (see Certificates) | +| `pki` | `p2s_name` | string | `VPN Router P2S CA` | Common name of the P2S issuing CA | +| `pki` | `s2s_name` | string | `VPN Router Site-to-Site CA` | Common name of the site-to-site issuing CA | +| `pki` | `issuing_cas` | object | | The general-purpose issuing CAs, each a name and `{name, templates, acme}` (see Certificates) | +| `pki` | `templates` | object | | Certificate templates, each a name and the options it changes or defines (see Certificates) | | `metrics` | `enabled` | boolean | `true` | The metrics service: collection, history and `/metrics` | | `metrics` | `listen` | string | `internal` | The interfaces `/metrics` is served on: `internal`, `external` or `both` | | `metrics` | `port` | integer | `9464` | TCP port of `/metrics` | @@ -176,13 +190,22 @@ of the status web service, which the Router Manager uses. | `cidrs` | list | | Remote subnets reachable through the tunnel | | `local_cidrs` | list | | Local subnets advertised into the tunnel | | `local_id_mode` | string | `fqdn` | The router's IKE identity: `fqdn` is `wan.local_fqdn`, `public_ip` the address it resolves to, `internal_ip` the internal address | -| `psk` | string | | Pre-shared key | +| `auth` | string | `psk` | How both sides authenticate: `psk`, a pre-shared key, or `certificate` | +| `psk` | string | | Pre-shared key, with `auth` `psk` | | `psk_file` | string | | Path to a file holding the key; wins over `psk` | +| `remote_ca` | string | | With `auth` `certificate`, the PEM certificate of the CA that issued the peer's certificate; without it the router's site-to-site CA | -A connection is set up once all of `addrs`, `id`, `cidrs`, `local_cidrs`, the key and its -local identity are known; all such connections are written to -`/etc/swanctl/conf.d/site-to-site.conf`. Traffic from a connection's `local_cidrs` to its -`cidrs` is not masqueraded. +A connection is set up once all of `addrs`, `id`, `cidrs`, `local_cidrs` and its local +identity are known, with `psk` the key, and with `certificate` the router's site-to-site +certificate; all such connections are written to `/etc/swanctl/conf.d/site-to-site.conf`. +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 +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. A minimal site-to-site configuration in `auto` mode: @@ -341,57 +364,215 @@ anything: ``` This does still write to the system: it renders into `/etc/swanctl/`, -`/etc/systemd/resolved.conf.d/` and `/etc/wireguard/`, and if road-warrior access is -enabled with an empty PKI directory it creates the certificate authority and adds it -to the host trust store. What it leaves alone is routes, firewall rules and services - -that is what `systemctl restart vpn-router-setup` adds. +`/etc/systemd/resolved.conf.d/` and `/etc/wireguard/`, creates what the PKI is missing and +adds the root CA to the host trust store. What it leaves alone is routes, firewall rules and +services - that is what `systemctl restart vpn-router-setup` adds. ## Certificates -`/etc/vpn-router/pki` is the PKI directory, laid out the way `simple-ca` expects: +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: ``` -ca_cert.pem ca_key.pem ca_bundle.pem