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.
Release / release (push) Successful in 29s
Release / release (push) Successful in 29s
This commit is contained in:
1 parent
e11c88f0de
commit
448ce05b3a
10 files changed
+713
-426
No files matched your search
@@ -152,8 +152,8 @@ Status: Accepted
|
|||||||
|
|
||||||
Context: Users are managed from the command line, by an operator at a terminal, and through
|
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,
|
`/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
|
passwords and person attributes, and both change the directory and the PKI, whose CRL numbers and
|
||||||
file, so two changes must not run at the same time. The web unit is sandboxed with
|
CRLs two changes must not write at the same time. The web unit is sandboxed with
|
||||||
`ProtectSystem=strict`.
|
`ProtectSystem=strict`.
|
||||||
|
|
||||||
Decision: `vpnrouter_users.py` holds the user operations, the checks and a session that takes a
|
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
|
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
|
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
|
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
|
files are there too, and `/etc/swanctl/x509crl`, `/etc/openvpn/server` and
|
||||||
`/etc/swanctl/x509crl`, `/etc/openvpn/server` and `/var/lib/vpn-router/pki-publish` for the
|
`/var/lib/vpn-router/pki-publish` for the copies of the CRLs a revocation signs; the PKI itself is
|
||||||
CRLs a revocation signs. The PKI module, `vpnrouter_pki.py`, works the same way for `/ca`,
|
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
|
`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.
|
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
|
`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
|
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
|
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
|
(ADR-6); the settings are the `pki` section of `vpn-router.json`, and the CAs and the
|
||||||
`pki/state.json`. Keeping the root key off the router is recommended, not required: while it is
|
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
|
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
|
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
|
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.
|
keys only.
|
||||||
|
|
||||||
Decision: A connection's `auth` is `psk`, the default, or `certificate`. With `certificate` the
|
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`,
|
router presents one site-to-site certificate of its `s2s` CA, whose names are `wan.local_fqdn` and
|
||||||
the public address and the internal address, so it fits every `local_id_mode`; the peer's
|
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
|
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
|
connection has `remote_ca`, by that CA, its PEM certificate kept in the connection and copied to
|
||||||
`/etc/swanctl/x509ca/s2s-<name>.pem`. A peer without a PKI gets its certificate from the router.
|
`/etc/swanctl/x509ca/s2s-<name>.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
|
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
|
router does not check a remote CA's revocations unless strongSwan can fetch the CRL its
|
||||||
certificates name.
|
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
|
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
|
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.
|
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.
|
||||||
@@ -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.
|
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
|
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
|
site-to-site issuing CA signs for `wan.local_fqdn` and for the public or the internal address
|
||||||
address, so it fits every `local_id_mode`. The peer's certificate must name `id` and be issued
|
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
|
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
|
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.
|
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
|
## Certificates
|
||||||
|
|
||||||
The router has a two-level certificate authority in `/etc/vpn-router/pki` (mode 0700), kept by
|
The router has a two-level certificate authority, kept by `vpn-router-setup` and managed with
|
||||||
`vpn-router-setup` and managed with `vpn-router-ca`, `vpn-router-user` and `/ca` of the status
|
`vpn-router-ca`, `vpn-router-user` and `/ca` of the status web service, which the Router Manager
|
||||||
web service, which the Router Manager uses:
|
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
|
cn=root the root CA; without vpnRouterPrivateKey while the root is offline
|
||||||
ca/<name>/cert.pem key.pem crl.pem csr.pem an issuing CA; csr.pem while it waits for the offline root
|
cn=<name> an issuing CA; with vpnRouterCertificateRequest while it waits for the offline root
|
||||||
router/vpn-server_cert.pem _key.pem the router's VPN server certificate
|
cn=<name>-<time> an issuing CA taken out of use, with vpnRouterRetired
|
||||||
router/site-to-site_cert.pem _key.pem the router's site-to-site certificate
|
serialNumber=<serial>,cn=<CA> a certificate the CA issued
|
||||||
users/<user>_cert.pem _key.pem the users' certificates
|
```
|
||||||
retired/<name>-<time>/ the issuing CAs taken out of use
|
|
||||||
state.json the certificates issued, revocations and CRL numbers
|
A CA entry (class `vpnRouterAuthority`) has its certificate (`cACertificate;binary`), key, CRL
|
||||||
|
(`certificateRevocationList;binary`) and CRL number. A certificate entry (class
|
||||||
|
`vpnRouterCertificate`) has the certificate (`userCertificate;binary`), its kind (`ca`, `router`,
|
||||||
|
`user` or `external`), template, names, expiry and revocation, and for the router's and the users'
|
||||||
|
certificates the key. A user's certificate is also the `userCertificate` of the user's entry. No
|
||||||
|
account of the directory reads a key (`vpnRouterPrivateKey`); only root does, through the local
|
||||||
|
socket. `slapcat` writes the whole PKI into every backup with the users.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b ou=pki,dc=vpn-router,dc=local -s one cn
|
||||||
```
|
```
|
||||||
|
|
||||||
### The CAs
|
### The CAs
|
||||||
@@ -403,8 +413,8 @@ The root signs the issuing CAs only (path length 1), and the issuing CAs sign th
|
|||||||
Once a platform is chosen, `vpn-router-setup` creates what is missing: the root when it has
|
Once a platform is chosen, `vpn-router-setup` creates what is missing: the root when it has
|
||||||
neither certificate nor key, each issuing CA, the router's certificates once `wan.local_fqdn` is
|
neither certificate nor key, each issuing CA, the router's certificates once `wan.local_fqdn` is
|
||||||
set, and the CRLs. The names, lifetimes and key type of `pki` apply when something is created. An
|
set, and the CRLs. The names, lifetimes and key type of `pki` apply when something is created. An
|
||||||
issuing CA removed from `pki.issuing_cas` is retired: its directory moves to `retired/` and its
|
issuing CA removed from `pki.issuing_cas` is retired: its entry is renamed `cn=<name>-<time>`,
|
||||||
certificate is revoked on the root CRL. The router renews its own certificates when less than a
|
loses its key, and its certificate is revoked on the root CRL. The router renews its own certificates when less than a
|
||||||
third of their lifetime is left, when the names they need change, or when their CA changes; a
|
third of their lifetime is left, when the names they need change, or when their CA changes; a
|
||||||
user's certificate is renewed by removing and adding the user.
|
user's certificate is renewed by removing and adding the user.
|
||||||
|
|
||||||
@@ -541,9 +551,9 @@ vpn-router-ca root import-crl root.crl
|
|||||||
```
|
```
|
||||||
|
|
||||||
An existing root CA of your own is used by placing its certificate, and its key if the router may
|
An existing root CA of your own is used by placing its certificate, and its key if the router may
|
||||||
use it, before choosing the platform: `vpn-router-ca root import-cert root-cert.pem`, or the files
|
use it, as `/etc/vpn-router/pki/root/cert.pem` and `key.pem` before `vpn-router-setup` first runs
|
||||||
`root/cert.pem` and `root/key.pem`. Without the key, the issuing CAs wait for their certificates
|
with a platform, as cloud-init can. Setup imports them into the directory and removes the files.
|
||||||
as above.
|
Without the key, the issuing CAs wait for their certificates as above.
|
||||||
|
|
||||||
### ACME server
|
### ACME server
|
||||||
|
|
||||||
@@ -623,7 +633,7 @@ it is added:
|
|||||||
|
|
||||||
- a VPN user, an `inetOrgPerson` with a first name, a last name and an e-mail address
|
- a VPN user, an `inetOrgPerson` with a first name, a last name and an e-mail address
|
||||||
(`givenName`, `sn` and `mail`; `cn` is the first and the last name), and a client certificate
|
(`givenName`, `sn` and `mail`; `cn` is the first and the last name), and a client certificate
|
||||||
with the user name as its common name, in `/etc/vpn-router/pki/users`;
|
with the user name as its common name, the entry's `userCertificate`, whose key the PKI keeps;
|
||||||
- a Manager-only account, an `account`, `simpleSecurityObject` and `vpnRouterAccount` with the
|
- a Manager-only account, an `account`, `simpleSecurityObject` and `vpnRouterAccount` with the
|
||||||
name, the password and an optional e-mail address, for notifications: no names and no
|
name, the password and an optional e-mail address, for notifications: no names and no
|
||||||
certificate, so no VPN access. It can sign in to `/auth` and, as a member of `admins`, manage
|
certificate, so no VPN access. It can sign in to `/auth` and, as a member of `admins`, manage
|
||||||
@@ -1061,6 +1071,7 @@ A change is made, checked and applied as for `/site-to-site`.
|
|||||||
| `POST /ca/certificates` | `{"ca", "template", "csr"}` | 201 and `{"serial", "certificate", "chain"}`, PEM |
|
| `POST /ca/certificates` | `{"ca", "template", "csr"}` | 201 and `{"serial", "certificate", "chain"}`, PEM |
|
||||||
| `POST /ca/certificates` | `{"ca", "template", "subject", "sans", "password"}` | 201 and `{"serial", "pfx"}`, a PKCS#12 file in base64 |
|
| `POST /ca/certificates` | `{"ca", "template", "subject", "sans", "password"}` | 201 and `{"serial", "pfx"}`, a PKCS#12 file in base64 |
|
||||||
| `POST /ca/certificates/<serial>/revoke` | `{"reason"}`, optional | the certificate, with `reissue` for a router certificate |
|
| `POST /ca/certificates/<serial>/revoke` | `{"reason"}`, optional | the certificate, with `reissue` for a router certificate |
|
||||||
|
| `GET /ca/bundle/<name>` | | `{"name", "pem"}`: the certificates a client trusts the CA with, the root alone for `root`, the issuing CA followed by the root for an issuing CA |
|
||||||
| `GET /ca/issuing/<name>/csr` | | `{"name", "csr", "extensions"}` of an issuing CA waiting for the offline root |
|
| `GET /ca/issuing/<name>/csr` | | `{"name", "csr", "extensions"}` of an issuing CA waiting for the offline root |
|
||||||
| `PUT /ca/issuing/<name>/certificate` | `{"certificate"}`, PEM, signed by the root | `{"name", "subject", "expires"}` |
|
| `PUT /ca/issuing/<name>/certificate` | `{"certificate"}`, PEM, signed by the root | `{"name", "subject", "expires"}` |
|
||||||
| `PUT /ca/root/crl` | `{"crl"}`, PEM, signed by the root | `{"next_update"}` |
|
| `PUT /ca/root/crl` | `{"crl"}`, PEM, signed by the root | `{"next_update"}` |
|
||||||
@@ -1266,12 +1277,12 @@ A backup holds the state that `vpn-router-setup` cannot make again:
|
|||||||
|
|
||||||
| Item | Content |
|
| Item | Content |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `/etc/vpn-router` | `vpn-router.json`, the secrets, `rclone.conf`, and the PKI: the CAs, the root key while it is on the router, and the certificates of the router and the users |
|
| `/etc/vpn-router` | `vpn-router.json`, the secrets and `rclone.conf` |
|
||||||
| `/etc/wireguard` | The WireGuard keys and the peers |
|
| `/etc/wireguard` | The WireGuard keys and the peers |
|
||||||
| `/etc/ufw`, `/etc/default/ufw` | The firewall rules |
|
| `/etc/ufw`, `/etc/default/ufw` | The firewall rules |
|
||||||
| `/etc/vpn-router-manager` | The Router Manager's settings, when it is installed |
|
| `/etc/vpn-router-manager` | The Router Manager's settings, when it is installed |
|
||||||
| `files/<path>` | Each `psk_file` outside `/etc/vpn-router` |
|
| `files/<path>` | Each `psk_file` outside `/etc/vpn-router` |
|
||||||
| `ldap/vpn-router.ldif` | The user directory, written by `slapcat`: users, groups, service accounts, password hashes and TOTP secrets |
|
| `ldap/vpn-router.ldif` | The user directory, written by `slapcat`: users, groups, service accounts, password hashes and TOTP secrets, and the PKI: the CAs and their keys, the root key while it is on the router, and the certificates issued, with the keys of the router's and the users' certificates |
|
||||||
| `postgresql/vpn-router.dump` | With `backup.metrics`, the database of the metrics and the refresh tokens, written by `pg_dump -Fc` |
|
| `postgresql/vpn-router.dump` | With `backup.metrics`, the database of the metrics and the refresh tokens, written by `pg_dump -Fc` |
|
||||||
| `manifest.json` | The host, the time, the version of the package, the schedule and the items |
|
| `manifest.json` | The host, the time, the version of the package, the schedule and the items |
|
||||||
|
|
||||||
@@ -1373,8 +1384,8 @@ file when the package created them, and Caddy's configuration `/etc/caddy/vpn-ro
|
|||||||
`/var/lib/caddy` belongs to the `caddy` package. It removes the backup timer's schedule, the
|
`/var/lib/caddy` belongs to the `caddy` package. It removes the backup timer's schedule, the
|
||||||
record of the last backup, the published CA files, the copies of the ACME CAs in
|
record of the last backup, the published CA files, the copies of the ACME CAs in
|
||||||
`/etc/caddy/vpn-router-pki` and OpenVPN's CA and CRL files. Caddy stays installed, and with it the
|
`/etc/caddy/vpn-router-pki` and OpenVPN's CA and CRL files. Caddy stays installed, and with it the
|
||||||
source of Caddy's repository. Key material, users and backups are never deleted: `/etc/vpn-router/pki`,
|
source of Caddy's repository. Key material, users and backups are never deleted:
|
||||||
`/etc/vpn-router/ldap`, `/etc/vpn-router/rclone.conf`, the user directory in slapd, the WireGuard
|
`/etc/vpn-router/ldap`, `/etc/vpn-router/rclone.conf`, the user directory in slapd with the PKI, the WireGuard
|
||||||
key pair, everything under `/etc/swanctl`, the archives in `backup.path` and
|
key pair, everything under `/etc/swanctl`, the archives in `backup.path` and
|
||||||
`/var/lib/vpn-router/backup/before-restore` are left alone.
|
`/var/lib/vpn-router/backup/before-restore` are left alone.
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,27 @@
|
|||||||
|
vpn-router (0.21.3-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
* The CAs with their certificates, keys, CSRs and CRLs, and every
|
||||||
|
certificate issued with its revocation, are entries of the
|
||||||
|
package's schema in ou=pki; a user's certificate is also the
|
||||||
|
userCertificate of the user's entry, and only root reads a key.
|
||||||
|
* slapcat puts the PKI into every backup with the users.
|
||||||
|
* A root CA supplied as /etc/vpn-router/pki/root/cert.pem and key.pem
|
||||||
|
before the first setup is imported into the directory, and the files
|
||||||
|
are removed; vpn-router-ca root import-cert is gone.
|
||||||
|
* Setup provisions the directory before the configuration; without it,
|
||||||
|
the PKI and its files are left as they are.
|
||||||
|
* The router's site-to-site certificate names wan.local_fqdn and the
|
||||||
|
public or internal address a connection with auth certificate uses
|
||||||
|
as its identity, not every address of the router, and is issued
|
||||||
|
again when those names change.
|
||||||
|
* GET /ca/bundle/<name> of the status web service answers the PEM
|
||||||
|
certificates a client trusts a CA with: the root alone, or an issuing
|
||||||
|
CA followed by the root.
|
||||||
|
|
||||||
|
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Sun, 01 Nov 2026 10:00:00 +0100
|
||||||
|
|
||||||
vpn-router (0.21.2-1) unstable; urgency=medium
|
vpn-router (0.21.2-1) unstable; urgency=medium
|
||||||
|
|
||||||
fix: pki.root_key_warning silences the warning of vpn-router-ca status and /ca about the root key on the router.
|
fix: pki.root_key_warning silences the warning of vpn-router-ca status and /ca about the root key on the router.
|
||||||
|
|||||||
@@ -577,7 +577,12 @@ def manage_caddy(cfg, ctx):
|
|||||||
group = grp.getgrnam(CADDY_GROUP).gr_gid
|
group = grp.getgrnam(CADDY_GROUP).gr_gid
|
||||||
except KeyError:
|
except KeyError:
|
||||||
fail(f'the group {CADDY_GROUP} does not exist; is the caddy package installed?')
|
fail(f'the group {CADDY_GROUP} does not exist; is the caddy package installed?')
|
||||||
acme = [name for name in ctx['pki_acme'] if vpnrouter_pki.ready(name)]
|
try:
|
||||||
|
credentials = {name: vpnrouter_pki.authority_files(name) for name in ctx['pki_acme']}
|
||||||
|
except ldap.LDAPError as exc:
|
||||||
|
warn(f'the ACME servers are left out: the PKI cannot be read: {exc}')
|
||||||
|
credentials = {}
|
||||||
|
acme = [name for name, files in credentials.items() if files is not None]
|
||||||
copied = False
|
copied = False
|
||||||
for folder in (CADDY_PKI_DIR.iterdir() if CADDY_PKI_DIR.is_dir() else []):
|
for folder in (CADDY_PKI_DIR.iterdir() if CADDY_PKI_DIR.is_dir() else []):
|
||||||
if folder.name not in acme:
|
if folder.name not in acme:
|
||||||
@@ -587,8 +592,8 @@ def manage_caddy(cfg, ctx):
|
|||||||
for path in (CADDY_PKI_DIR, folder):
|
for path in (CADDY_PKI_DIR, folder):
|
||||||
path.mkdir(mode=0o750, exist_ok=True)
|
path.mkdir(mode=0o750, exist_ok=True)
|
||||||
os.chown(path, 0, group)
|
os.chown(path, 0, group)
|
||||||
for file in ('cert.pem', 'key.pem'):
|
for file, data in zip(('cert.pem', 'key.pem'), credentials[name]):
|
||||||
copied |= vpnrouter.install_file(vpnrouter_pki.CA_DIR / name / file, folder / file, 0o640)
|
copied |= vpnrouter_pki.write(folder / file, data, 0o640)
|
||||||
os.chown(folder / file, 0, group)
|
os.chown(folder / file, 0, group)
|
||||||
authorities = vpnrouter.issuing_cas(cfg)
|
authorities = vpnrouter.issuing_cas(cfg)
|
||||||
caddy_ctx = dict(ctx, manager_port=manager_port() if MANAGER.exists() else 0,
|
caddy_ctx = dict(ctx, manager_port=manager_port() if MANAGER.exists() else 0,
|
||||||
@@ -739,14 +744,19 @@ def main():
|
|||||||
start_bootstrap()
|
start_bootstrap()
|
||||||
progress('running', 'configuration')
|
progress('running', 'configuration')
|
||||||
cfg = vpnrouter.load_config()
|
cfg = vpnrouter.load_config()
|
||||||
ctx, changed = vpnrouter.configure(cfg)
|
if vpnrouter.deferred(cfg):
|
||||||
|
|
||||||
if ctx is None:
|
|
||||||
print('vpn-router: platform is none, configuration deferred; '
|
print('vpn-router: platform is none, configuration deferred; '
|
||||||
f'set general.platform in {vpnrouter.CONFIG_FILE}')
|
f'set general.platform in {vpnrouter.CONFIG_FILE}')
|
||||||
progress('deferred', 'configuration', f'general.platform is none; set it in {vpnrouter.CONFIG_FILE}')
|
progress('deferred', 'configuration', f'general.platform is none; set it in {vpnrouter.CONFIG_FILE}')
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
# The directory holds the PKI, which the configuration needs; its problem ends setup only after every other step.
|
||||||
|
failed_step = 'user directory'
|
||||||
|
progress('running', failed_step)
|
||||||
|
problem = manage_directory()
|
||||||
|
progress('running', 'configuration')
|
||||||
|
ctx, changed = vpnrouter.configure(cfg)
|
||||||
|
|
||||||
progress('running', 'routes')
|
progress('running', 'routes')
|
||||||
if vpnrouter.routing_ready(ctx):
|
if vpnrouter.routing_ready(ctx):
|
||||||
apply_routes(ctx)
|
apply_routes(ctx)
|
||||||
@@ -778,10 +788,6 @@ def main():
|
|||||||
progress('running', 'WireGuard')
|
progress('running', 'WireGuard')
|
||||||
manage_wireguard(ctx, changed)
|
manage_wireguard(ctx, changed)
|
||||||
|
|
||||||
# The tunnels are up before the directory and the admin, whose problem ends setup only after every other step.
|
|
||||||
failed_step = 'user directory'
|
|
||||||
progress('running', failed_step)
|
|
||||||
problem = manage_directory()
|
|
||||||
if not problem:
|
if not problem:
|
||||||
failed_step = 'Router Manager admin'
|
failed_step = 'Router Manager admin'
|
||||||
progress('running', failed_step)
|
progress('running', failed_step)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import tempfile
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
import jinja2
|
import jinja2
|
||||||
|
import ldap
|
||||||
|
|
||||||
LIB_DIR = pathlib.Path(__file__).resolve().parent
|
LIB_DIR = pathlib.Path(__file__).resolve().parent
|
||||||
if str(LIB_DIR) not in sys.path:
|
if str(LIB_DIR) not in sys.path:
|
||||||
@@ -1011,6 +1012,7 @@ def configure(cfg=None, public_ip=None):
|
|||||||
|
|
||||||
changed = set()
|
changed = set()
|
||||||
|
|
||||||
|
# Without the directory, which holds the PKI, the copies of the last run stay and what needs none is configured.
|
||||||
with vpnrouter_pki.locked():
|
with vpnrouter_pki.locked():
|
||||||
try:
|
try:
|
||||||
notices = vpnrouter_pki.provision(cfg, ctx)
|
notices = vpnrouter_pki.provision(cfg, ctx)
|
||||||
@@ -1018,6 +1020,8 @@ def configure(cfg=None, public_ip=None):
|
|||||||
changed.add('swanctl')
|
changed.add('swanctl')
|
||||||
except vpnrouter_pki.Failure as failure:
|
except vpnrouter_pki.Failure as failure:
|
||||||
fail(str(failure))
|
fail(str(failure))
|
||||||
|
except ldap.LDAPError as exc:
|
||||||
|
notices = [f'the PKI is left as it is: the directory cannot be reached: {exc}']
|
||||||
for notice in notices:
|
for notice in notices:
|
||||||
warn(notice)
|
warn(notice)
|
||||||
# What needs the router's own certificates.
|
# What needs the router's own certificates.
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ The password policy locks an account for LOCKOUT_SECONDS after LOCKOUT_FAILURES
|
|||||||
a successful bind clears the count. slapd's otp overlay is configured on the database, so a
|
a successful bind clears the count. slapd's otp overlay is configured on the database, so a
|
||||||
bind of a user with a TOTP token takes the password followed by a current code.
|
bind of a user with a TOTP token takes the password followed by a current code.
|
||||||
cn=totp,ou=policies holds the TOTP parameters the token of every user points to.
|
cn=totp,ou=policies holds the TOTP parameters the token of every user points to.
|
||||||
|
|
||||||
|
ou=pki holds the router's PKI (vpnrouter_pki): an entry per CA, and under it an entry per
|
||||||
|
certificate the CA issued. Private keys are readable by root alone, the database's root DN.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -29,6 +32,7 @@ USERS = f'ou=users,{BASE}'
|
|||||||
GROUPS = f'ou=groups,{BASE}'
|
GROUPS = f'ou=groups,{BASE}'
|
||||||
SERVICES = f'ou=services,{BASE}'
|
SERVICES = f'ou=services,{BASE}'
|
||||||
POLICIES = f'ou=policies,{BASE}'
|
POLICIES = f'ou=policies,{BASE}'
|
||||||
|
PKI = f'ou=pki,{BASE}'
|
||||||
DEFAULT_POLICY = f'cn=default,{POLICIES}'
|
DEFAULT_POLICY = f'cn=default,{POLICIES}'
|
||||||
TOTP_PARAMS = f'cn=totp,{POLICIES}'
|
TOTP_PARAMS = f'cn=totp,{POLICIES}'
|
||||||
MANAGER = f'cn=user-manager,{SERVICES}'
|
MANAGER = f'cn=user-manager,{SERVICES}'
|
||||||
@@ -54,16 +58,43 @@ OVERLAYS = ('ppolicy', 'otp')
|
|||||||
#: under .1 and object classes under .2. vpnRouterAccount, auxiliary, adds properties of the
|
#: under .1 and object classes under .2. vpnRouterAccount, auxiliary, adds properties of the
|
||||||
#: package to a user of either kind: mail, for a Manager-only account, vpnRouterScope,
|
#: package to a user of either kind: mail, for a Manager-only account, vpnRouterScope,
|
||||||
#: authorization scopes, of which service marks a service account, and displayName and
|
#: authorization scopes, of which service marks a service account, and displayName and
|
||||||
#: description, the labels of a service account.
|
#: description, the labels of a service account. vpnRouterAuthority is a CA of the PKI: its
|
||||||
|
#: certificate and CRL (cACertificate and certificateRevocationList of RFC 4523), its key, the
|
||||||
|
#: CSR it waits with for the offline root, the number of its last CRL, where the CRL came from,
|
||||||
|
#: and when it was retired. vpnRouterCertificate is a certificate a CA issued, named by its
|
||||||
|
#: serial number: the certificate, the key the router keeps for its own and the users'
|
||||||
|
#: certificates, its kind, template, names and expiry, and its revocation.
|
||||||
SCHEMA_OID = '2.25.98477352595735834532404256531620013001'
|
SCHEMA_OID = '2.25.98477352595735834532404256531620013001'
|
||||||
SCHEMA_NAME = 'vpn-router'
|
SCHEMA_NAME = 'vpn-router'
|
||||||
|
_STRING = 'EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} SINGLE-VALUE'
|
||||||
|
_TIME = 'EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE'
|
||||||
|
_OCTETS = 'EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 SINGLE-VALUE'
|
||||||
SCHEMA_ATTRIBUTE_TYPES = (
|
SCHEMA_ATTRIBUTE_TYPES = (
|
||||||
f"( {SCHEMA_OID}.1.1 NAME 'vpnRouterScope' DESC 'VPN router: an authorization scope of the account' "
|
f"( {SCHEMA_OID}.1.1 NAME 'vpnRouterScope' DESC 'VPN router: an authorization scope of the account' "
|
||||||
"EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )",
|
"EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )",
|
||||||
|
f"( {SCHEMA_OID}.1.2 NAME 'vpnRouterPrivateKey' DESC 'VPN router: a private key, PEM' {_OCTETS} )",
|
||||||
|
f"( {SCHEMA_OID}.1.3 NAME 'vpnRouterCertificateRequest' DESC 'VPN router: the CSR of a CA, PEM' {_OCTETS} )",
|
||||||
|
f"( {SCHEMA_OID}.1.4 NAME 'vpnRouterCRLNumber' DESC 'VPN router: the number of the last CRL of a CA' "
|
||||||
|
"EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )",
|
||||||
|
f"( {SCHEMA_OID}.1.5 NAME 'vpnRouterCRLSource' DESC 'VPN router: signed or uploaded, where the CRL of a CA came from' {_STRING} )",
|
||||||
|
f"( {SCHEMA_OID}.1.6 NAME 'vpnRouterRetired' DESC 'VPN router: when a CA was retired' {_TIME} )",
|
||||||
|
f"( {SCHEMA_OID}.1.7 NAME 'vpnRouterKind' DESC 'VPN router: ca, router, user or external, the kind of a certificate' {_STRING} )",
|
||||||
|
f"( {SCHEMA_OID}.1.8 NAME 'vpnRouterTemplate' DESC 'VPN router: the template of a certificate' {_STRING} )",
|
||||||
|
f"( {SCHEMA_OID}.1.9 NAME 'vpnRouterSubjectAltName' DESC 'VPN router: a subject alternative name of a certificate' "
|
||||||
|
"EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )",
|
||||||
|
f"( {SCHEMA_OID}.1.10 NAME 'vpnRouterNotAfter' DESC 'VPN router: when a certificate expires' {_TIME} )",
|
||||||
|
f"( {SCHEMA_OID}.1.11 NAME 'vpnRouterRevoked' DESC 'VPN router: when a certificate was revoked' {_TIME} )",
|
||||||
|
f"( {SCHEMA_OID}.1.12 NAME 'vpnRouterRevocationReason' DESC 'VPN router: why a certificate was revoked' {_STRING} )",
|
||||||
)
|
)
|
||||||
SCHEMA_OBJECT_CLASSES = (
|
SCHEMA_OBJECT_CLASSES = (
|
||||||
f"( {SCHEMA_OID}.2.1 NAME 'vpnRouterAccount' DESC 'VPN router: properties of a user or a Manager-only account' "
|
f"( {SCHEMA_OID}.2.1 NAME 'vpnRouterAccount' DESC 'VPN router: properties of a user or a Manager-only account' "
|
||||||
"SUP top AUXILIARY MAY ( mail $ vpnRouterScope $ displayName $ description ) )",
|
"SUP top AUXILIARY MAY ( mail $ vpnRouterScope $ displayName $ description ) )",
|
||||||
|
f"( {SCHEMA_OID}.2.2 NAME 'vpnRouterAuthority' DESC 'VPN router: a CA of the PKI' SUP top STRUCTURAL MUST cn "
|
||||||
|
"MAY ( cACertificate $ certificateRevocationList $ vpnRouterPrivateKey $ vpnRouterCertificateRequest $ "
|
||||||
|
"vpnRouterCRLNumber $ vpnRouterCRLSource $ vpnRouterRetired ) )",
|
||||||
|
f"( {SCHEMA_OID}.2.3 NAME 'vpnRouterCertificate' DESC 'VPN router: a certificate a CA of the PKI issued' SUP top "
|
||||||
|
"STRUCTURAL MUST serialNumber MAY ( cn $ userCertificate $ vpnRouterPrivateKey $ vpnRouterKind $ vpnRouterTemplate $ "
|
||||||
|
"vpnRouterSubjectAltName $ vpnRouterNotAfter $ vpnRouterRevoked $ vpnRouterRevocationReason ) )",
|
||||||
)
|
)
|
||||||
|
|
||||||
#: The TOTP parameters: digits, the OID of HMAC-SHA1, seconds per time step, and the time steps accepted before and after now.
|
#: The TOTP parameters: digits, the OID of HMAC-SHA1, seconds per time step, and the time steps accepted before and after now.
|
||||||
@@ -74,6 +105,7 @@ TOTP_WINDOW = 1
|
|||||||
|
|
||||||
#: The ACLs of the database, in order.
|
#: The ACLs of the database, in order.
|
||||||
ACCESS = (
|
ACCESS = (
|
||||||
|
'to attrs=vpnRouterPrivateKey by * none',
|
||||||
f'to attrs=userPassword by dn.exact="{MANAGER}" =wx by self =wx by anonymous auth by * none',
|
f'to attrs=userPassword by dn.exact="{MANAGER}" =wx by self =wx by anonymous auth by * none',
|
||||||
f'to attrs=oathSecret by dn.exact="{MANAGER}" =w by * none',
|
f'to attrs=oathSecret by dn.exact="{MANAGER}" =w by * none',
|
||||||
f'to dn.subtree="{USERS}" by dn.exact="{MANAGER}" manage by * read',
|
f'to dn.subtree="{USERS}" by dn.exact="{MANAGER}" manage by * read',
|
||||||
@@ -126,20 +158,27 @@ def _ensure_modules(connection):
|
|||||||
|
|
||||||
|
|
||||||
def _ensure_schema(connection):
|
def _ensure_schema(connection):
|
||||||
"""Add the package's schema to cn=config when missing, else set its definitions that differ."""
|
"""Add the package's schema to cn=config when missing, else add the definitions it lacks; a definition that differs
|
||||||
|
from the one of the same OID replaces the whole list of its kind."""
|
||||||
found = connection.search_s('cn=schema,cn=config', ldap.SCOPE_ONELEVEL, f'(cn=*{SCHEMA_NAME})',
|
found = connection.search_s('cn=schema,cn=config', ldap.SCOPE_ONELEVEL, f'(cn=*{SCHEMA_NAME})',
|
||||||
['olcAttributeTypes', 'olcObjectClasses'])
|
['olcAttributeTypes', 'olcObjectClasses'])
|
||||||
attributes = {'olcAttributeTypes': [value.encode() for value in SCHEMA_ATTRIBUTE_TYPES],
|
attributes = {'olcAttributeTypes': list(SCHEMA_ATTRIBUTE_TYPES), 'olcObjectClasses': list(SCHEMA_OBJECT_CLASSES)}
|
||||||
'olcObjectClasses': [value.encode() for value in SCHEMA_OBJECT_CLASSES]}
|
|
||||||
if not found:
|
if not found:
|
||||||
connection.add_s(f'cn={SCHEMA_NAME},cn=schema,cn=config', ldap.modlist.addModlist(
|
connection.add_s(f'cn={SCHEMA_NAME},cn=schema,cn=config', ldap.modlist.addModlist(
|
||||||
dict(attributes, objectClass=[b'olcSchemaConfig'], cn=[SCHEMA_NAME.encode()])))
|
dict({name: [value.encode() for value in values] for name, values in attributes.items()},
|
||||||
|
objectClass=[b'olcSchemaConfig'], cn=[SCHEMA_NAME.encode()])))
|
||||||
return
|
return
|
||||||
dn, current = found[0]
|
dn, current = found[0]
|
||||||
changes = [(ldap.MOD_REPLACE, name, values) for name, values in attributes.items()
|
for name, wanted in attributes.items():
|
||||||
if unordered(current.get(name, [])) != [value.decode() for value in values]]
|
present = unordered(current.get(name, []))
|
||||||
if changes:
|
oids = {value.split()[1] for value in present}
|
||||||
connection.modify_s(dn, changes)
|
missing = [value for value in wanted if value not in present]
|
||||||
|
if not missing:
|
||||||
|
continue
|
||||||
|
if any(value.split()[1] in oids for value in missing):
|
||||||
|
connection.modify_s(dn, [(ldap.MOD_REPLACE, name, [value.encode() for value in wanted])])
|
||||||
|
else:
|
||||||
|
connection.modify_s(dn, [(ldap.MOD_ADD, name, [value.encode() for value in missing])])
|
||||||
|
|
||||||
|
|
||||||
def _ensure_password_hash(connection):
|
def _ensure_password_hash(connection):
|
||||||
@@ -230,7 +269,7 @@ def provision():
|
|||||||
database = _ensure_database(connection)
|
database = _ensure_database(connection)
|
||||||
_ensure_overlays(connection, database)
|
_ensure_overlays(connection, database)
|
||||||
_ensure_entry(connection, BASE, {'objectClass': [b'dcObject', b'organization'], 'dc': [b'vpn-router'], 'o': [b'vpn-router']})
|
_ensure_entry(connection, BASE, {'objectClass': [b'dcObject', b'organization'], 'dc': [b'vpn-router'], 'o': [b'vpn-router']})
|
||||||
for unit in (USERS, GROUPS, SERVICES, POLICIES):
|
for unit in (USERS, GROUPS, SERVICES, POLICIES, PKI):
|
||||||
_ensure_entry(connection, unit, {'objectClass': [b'organizationalUnit'], 'ou': [unit.split(',')[0][3:].encode()]})
|
_ensure_entry(connection, unit, {'objectClass': [b'organizationalUnit'], 'ou': [unit.split(',')[0][3:].encode()]})
|
||||||
_ensure_entry(connection, DEFAULT_POLICY, {
|
_ensure_entry(connection, DEFAULT_POLICY, {
|
||||||
'objectClass': [b'organizationalRole', b'pwdPolicy'],
|
'objectClass': [b'organizationalRole', b'pwdPolicy'],
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -3,7 +3,8 @@
|
|||||||
A user is an entry in ou=users of the router's directory, of one of two kinds, fixed when it is
|
A user is an entry in ou=users of the router's directory, of one of two kinds, fixed when it is
|
||||||
added. A VPN user is an inetOrgPerson with a first name, a last name and an e-mail address, whose
|
added. A VPN user is an inetOrgPerson with a first name, a last name and an e-mail address, whose
|
||||||
password OpenVPN checks, and a client certificate of the P2S issuing CA (vpnrouter_pki) with the
|
password OpenVPN checks, and a client certificate of the P2S issuing CA (vpnrouter_pki) with the
|
||||||
user name as its common name, which OpenVPN and IKEv2 accept. A Manager-only account is an account,
|
user name as its common name, which OpenVPN and IKEv2 accept; the certificate is the entry's
|
||||||
|
userCertificate, and the PKI keeps it with its key. A Manager-only account is an account,
|
||||||
simpleSecurityObject and vpnRouterAccount (the package's schema) with a name, a password and
|
simpleSecurityObject and vpnRouterAccount (the package's schema) with a name, a password and
|
||||||
optionally an e-mail address, no names, no certificate and so no VPN access; it is added when
|
optionally an e-mail address, no names, no certificate and so no VPN access; it is added when
|
||||||
neither the first nor the last name is given, and can sign in to the web service. A service
|
neither the first nor the last name is given, and can sign in to the web service. A service
|
||||||
@@ -68,7 +69,7 @@ PERSON = (('first_name', 'givenName'), ('last_name', 'sn'), ('email', 'mail'))
|
|||||||
LABELS = (('display_name', 'displayName'), ('description', 'description'))
|
LABELS = (('display_name', 'displayName'), ('description', 'description'))
|
||||||
#: The attributes read for a user.
|
#: The attributes read for a user.
|
||||||
ATTRIBUTES = ['uid', 'objectClass', 'pwdAccountLockedTime', 'givenName', 'sn', 'mail', 'oathTOTPToken', 'vpnRouterScope',
|
ATTRIBUTES = ['uid', 'objectClass', 'pwdAccountLockedTime', 'givenName', 'sn', 'mail', 'oathTOTPToken', 'vpnRouterScope',
|
||||||
'displayName', 'description']
|
'displayName', 'description', 'userCertificate;binary']
|
||||||
#: The vpnRouterScope of a service account.
|
#: The vpnRouterScope of a service account.
|
||||||
SERVICE_SCOPE = 'service'
|
SERVICE_SCOPE = 'service'
|
||||||
#: The object classes of a VPN user and of a Manager-only account.
|
#: The object classes of a VPN user and of a Manager-only account.
|
||||||
@@ -173,9 +174,6 @@ def dn_of(name):
|
|||||||
return f'uid={name},{vpnrouter_ldap.USERS}'
|
return f'uid={name},{vpnrouter_ldap.USERS}'
|
||||||
|
|
||||||
|
|
||||||
def certificate(name):
|
|
||||||
return vpnrouter_pki.user_files(name)
|
|
||||||
|
|
||||||
|
|
||||||
def entry(connection, name):
|
def entry(connection, name):
|
||||||
"""The attributes of the user's entry; Failure with status 3 when there is none."""
|
"""The attributes of the user's entry; Failure with status 3 when there is none."""
|
||||||
@@ -234,7 +232,7 @@ def summary(attributes):
|
|||||||
'internal': service(attributes) and name in internal_accounts(),
|
'internal': service(attributes) and name in internal_accounts(),
|
||||||
'enabled': lock != LOCKED,
|
'enabled': lock != LOCKED,
|
||||||
'locked': lock not in (None, LOCKED) and locked_since(lock) > time.time() - vpnrouter_ldap.LOCKOUT_SECONDS,
|
'locked': lock not in (None, LOCKED) and locked_since(lock) > time.time() - vpnrouter_ldap.LOCKOUT_SECONDS,
|
||||||
'certificate': certificate(name)[0].is_file(),
|
'certificate': 'userCertificate;binary' in attributes,
|
||||||
'otp': 'oathTOTPToken' in attributes,
|
'otp': 'oathTOTPToken' in attributes,
|
||||||
}
|
}
|
||||||
for key, attribute in PERSON + LABELS:
|
for key, attribute in PERSON + LABELS:
|
||||||
@@ -468,9 +466,10 @@ def profile(connection, name):
|
|||||||
cfg = vpnrouter.load_config()
|
cfg = vpnrouter.load_config()
|
||||||
if not cfg['openvpn']['enabled']:
|
if not cfg['openvpn']['enabled']:
|
||||||
raise Failure('OpenVPN is not enabled', 2)
|
raise Failure('OpenVPN is not enabled', 2)
|
||||||
cert, key = certificate(name)
|
credentials = vpnrouter_pki.user_credentials(name)
|
||||||
if not cert.is_file() or not key.is_file():
|
if credentials is None:
|
||||||
raise Failure(f'user {name} has no certificate')
|
raise Failure(f'user {name} has no certificate')
|
||||||
|
cert, key = (value.decode() for value in credentials)
|
||||||
fqdn = cfg['wan']['local_fqdn'].strip()
|
fqdn = cfg['wan']['local_fqdn'].strip()
|
||||||
text = '\n'.join([
|
text = '\n'.join([
|
||||||
'client',
|
'client',
|
||||||
@@ -486,8 +485,8 @@ def profile(connection, name):
|
|||||||
'auth-nocache',
|
'auth-nocache',
|
||||||
'verb 3',
|
'verb 3',
|
||||||
'<ca>', extract_pem(vpnrouter_pki.OPENVPN_CA.read_text(encoding='utf-8')), '</ca>',
|
'<ca>', extract_pem(vpnrouter_pki.OPENVPN_CA.read_text(encoding='utf-8')), '</ca>',
|
||||||
'<cert>', extract_pem(cert.read_text(encoding='utf-8')), '</cert>',
|
'<cert>', extract_pem(cert), '</cert>',
|
||||||
'<key>', key.read_text(encoding='utf-8').strip(), '</key>',
|
'<key>', key.strip(), '</key>',
|
||||||
'',
|
'',
|
||||||
])
|
])
|
||||||
return {'name': name, 'profile': text}
|
return {'name': name, 'profile': text}
|
||||||
|
|||||||
@@ -788,6 +788,8 @@ def ca(method, path, body):
|
|||||||
"certificate", "chain"}; {"ca", "template", "subject", "sans",
|
"certificate", "chain"}; {"ca", "template", "subject", "sans",
|
||||||
"password"} answers {"serial", "pfx"}, a PKCS#12 file in base64
|
"password"} answers {"serial", "pfx"}, a PKCS#12 file in base64
|
||||||
POST /ca/certificates/<serial>/revoke revoke a certificate: {"reason"?}
|
POST /ca/certificates/<serial>/revoke revoke a certificate: {"reason"?}
|
||||||
|
GET /ca/bundle/<name> the PEM certificates a client trusts the CA with: {"name", "pem"}; for
|
||||||
|
root the root alone, for an issuing CA the issuing CA and the root
|
||||||
GET /ca/issuing/<name>/csr the CSR of an issuing CA that waits for the offline root, with the
|
GET /ca/issuing/<name>/csr the CSR of an issuing CA that waits for the offline root, with the
|
||||||
openssl extensions to sign it with: {"name", "csr", "extensions"}
|
openssl extensions to sign it with: {"name", "csr", "extensions"}
|
||||||
PUT /ca/issuing/<name>/certificate take its certificate signed by the root: {"certificate"}
|
PUT /ca/issuing/<name>/certificate take its certificate signed by the root: {"certificate"}
|
||||||
@@ -803,6 +805,8 @@ def ca(method, path, body):
|
|||||||
route = (method, *parts[1:])
|
route = (method, *parts[1:])
|
||||||
try:
|
try:
|
||||||
cfg = vpnrouter.load_config()
|
cfg = vpnrouter.load_config()
|
||||||
|
if len(route) == 3 and route[:2] == ('GET', 'bundle'):
|
||||||
|
return 200, vpnrouter_pki.bundle(route[2])
|
||||||
if route == ('GET',):
|
if route == ('GET',):
|
||||||
return 200, {'settings': cfg['pki'], 'templates': vpnrouter.pki_templates(cfg),
|
return 200, {'settings': cfg['pki'], 'templates': vpnrouter.pki_templates(cfg),
|
||||||
'status': vpnrouter_pki.status(cfg), 'certificates': vpnrouter_pki.certificates()}
|
'status': vpnrouter_pki.status(cfg), 'certificates': vpnrouter_pki.certificates()}
|
||||||
@@ -836,6 +840,8 @@ def ca(method, path, body):
|
|||||||
return 400, {'error': str(exc)}
|
return 400, {'error': str(exc)}
|
||||||
except vpnrouter_pki.Failure as failure:
|
except vpnrouter_pki.Failure as failure:
|
||||||
return PKI_STATUS.get(failure.status, 500), {'error': str(failure)}
|
return PKI_STATUS.get(failure.status, 500), {'error': str(failure)}
|
||||||
|
except ldap.LDAPError as exc:
|
||||||
|
return 500, {'error': f'the directory, which holds the PKI, failed: {exc}'}
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
return 500, {'error': f'the PKI cannot be changed: {exc}'}
|
return 500, {'error': f'the PKI cannot be changed: {exc}'}
|
||||||
except SystemExit as exc:
|
except SystemExit as exc:
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
"""Manage the router's PKI from the command line, with vpnrouter_pki.
|
"""Manage the router's PKI, which the router's directory holds, from the command line, with vpnrouter_pki.
|
||||||
|
|
||||||
status shows the root, the issuing CAs and the warnings; list the certificates the router issued. issue signs a CSR,
|
status shows the root, the issuing CAs and the warnings; list the certificates the router issued. issue signs a CSR,
|
||||||
or makes a key and writes a PFX file whose password is read from the first line of standard input. ca csr prints the
|
or makes a key and writes a PFX file whose password is read from the first line of standard input. ca csr prints the
|
||||||
CSR of an issuing CA that waits for the offline root, with the openssl extensions to sign it with, and ca import takes
|
CSR of an issuing CA that waits for the offline root, with the openssl extensions to sign it with, and ca import takes
|
||||||
the signed certificate. root export-key writes the root key encrypted with the passphrase read from standard input,
|
the signed certificate. root export-key writes the root key encrypted with the passphrase read from standard input,
|
||||||
root remove-key removes it from the router, root import-key puts it back, root import-cert places an existing root's
|
root remove-key removes it from the router, root import-key puts it back, and root import-crl takes a root CRL signed
|
||||||
certificate before the root exists, and root import-crl takes a root CRL signed offline. maintain signs the stale CRLs
|
offline. maintain signs the stale CRLs again; vpn-router-pki.service runs it daily. A change that needs
|
||||||
again; vpn-router-pki.service runs it daily. A change that needs vpn-router-setup restarts it without waiting.
|
vpn-router-setup restarts it without waiting.
|
||||||
|
|
||||||
The answer is text for people on standard output, and a failure is a message on standard error. With --json the answer
|
The answer is text for people on standard output, and a failure is a message on standard error. With --json the answer
|
||||||
is JSON on standard output, and a failure is {"error": ...} there too. The exit status says what failed: 2 the request
|
is JSON on standard output, and a failure is {"error": ...} there too. The exit status says what failed: 2 the request
|
||||||
@@ -22,7 +22,6 @@ commands and their options.
|
|||||||
vpn-router-ca [--json] maintain
|
vpn-router-ca [--json] maintain
|
||||||
vpn-router-ca [--json] ca csr NAME
|
vpn-router-ca [--json] ca csr NAME
|
||||||
vpn-router-ca [--json] ca import NAME FILE
|
vpn-router-ca [--json] ca import NAME FILE
|
||||||
vpn-router-ca [--json] root import-cert FILE
|
|
||||||
vpn-router-ca [--json] root export-key --out FILE (passphrase on stdin)
|
vpn-router-ca [--json] root export-key --out FILE (passphrase on stdin)
|
||||||
vpn-router-ca [--json] root remove-key [--yes]
|
vpn-router-ca [--json] root remove-key [--yes]
|
||||||
vpn-router-ca [--json] root import-key FILE (passphrase on stdin, an empty line for none)
|
vpn-router-ca [--json] root import-key FILE (passphrase on stdin, an empty line for none)
|
||||||
@@ -39,6 +38,8 @@ import sys
|
|||||||
|
|
||||||
sys.path.insert(0, '/usr/lib/vpn-router')
|
sys.path.insert(0, '/usr/lib/vpn-router')
|
||||||
|
|
||||||
|
import ldap
|
||||||
|
|
||||||
import vpnrouter
|
import vpnrouter
|
||||||
import vpnrouter_pki
|
import vpnrouter_pki
|
||||||
from vpnrouter_pki import Failure
|
from vpnrouter_pki import Failure
|
||||||
@@ -125,10 +126,6 @@ def run(options):
|
|||||||
document = vpnrouter_pki.import_ca(cfg, options.name, pathlib.Path(options.file).read_text(encoding='ascii'))
|
document = vpnrouter_pki.import_ca(cfg, options.name, pathlib.Path(options.file).read_text(encoding='ascii'))
|
||||||
restart_setup()
|
restart_setup()
|
||||||
return document, f'Issuing CA {options.name} has its certificate, which expires {document["expires"]}.'
|
return document, f'Issuing CA {options.name} has its certificate, which expires {document["expires"]}.'
|
||||||
if options.action == 'import-cert':
|
|
||||||
document = vpnrouter_pki.import_root_cert(pathlib.Path(options.file).read_bytes())
|
|
||||||
restart_setup()
|
|
||||||
return document, f'Root CA {document["subject"]} placed, without its key.'
|
|
||||||
if options.action == 'export-key':
|
if options.action == 'export-key':
|
||||||
data = vpnrouter_pki.export_root_key(secret())
|
data = vpnrouter_pki.export_root_key(secret())
|
||||||
out = pathlib.Path(options.out)
|
out = pathlib.Path(options.out)
|
||||||
@@ -177,7 +174,6 @@ def main(argv):
|
|||||||
importer.add_argument('file')
|
importer.add_argument('file')
|
||||||
root = commands.add_parser('root', help='the root CA')
|
root = commands.add_parser('root', help='the root CA')
|
||||||
root_actions = root.add_subparsers(dest='action', required=True, parser_class=Parser)
|
root_actions = root.add_subparsers(dest='action', required=True, parser_class=Parser)
|
||||||
root_actions.add_parser('import-cert', help="place an existing root's certificate").add_argument('file')
|
|
||||||
exporter = root_actions.add_parser('export-key', help='write the root key encrypted with the passphrase from stdin')
|
exporter = root_actions.add_parser('export-key', help='write the root key encrypted with the passphrase from stdin')
|
||||||
exporter.add_argument('--out', required=True, help='the file to write')
|
exporter.add_argument('--out', required=True, help='the file to write')
|
||||||
remover = root_actions.add_parser('remove-key', help='remove the root key from the router')
|
remover = root_actions.add_parser('remove-key', help='remove the root key from the router')
|
||||||
@@ -193,6 +189,8 @@ def main(argv):
|
|||||||
code, message = failure.status, str(failure)
|
code, message = failure.status, str(failure)
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
code, message = 1, str(exc)
|
code, message = 1, str(exc)
|
||||||
|
except ldap.LDAPError as exc:
|
||||||
|
code, message = 1, f'the directory, which holds the PKI, failed: {exc}'
|
||||||
except SystemExit as exc:
|
except SystemExit as exc:
|
||||||
code, message = 1, str(exc.code)
|
code, message = 1, str(exc.code)
|
||||||
else:
|
else:
|
||||||
|
|||||||
Reference in new issue
Block a user