Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9003bacb9 | ||
|
|
0bf78b16b1 | ||
|
|
0ff74cc8c9 | ||
|
|
a7b29e0655 | ||
|
|
c377801be0 |
No files matched your search
@@ -29,65 +29,61 @@ restores the rules. The rules cover IPv4 only.
|
|||||||
|
|
||||||
## ADR-2: A status web service of the standard library, bound to the internal address
|
## ADR-2: A status web service of the standard library, bound to the internal address
|
||||||
|
|
||||||
Status: Accepted; amended by ADR-7
|
Status: Accepted
|
||||||
|
|
||||||
Context: The router has no way to see its state without a shell. A check from the protected
|
Context: The router has no way to see its state without a shell. A check from the protected
|
||||||
network, by a person or by a monitor, needs a page or an endpoint. The router is an
|
network, by a person or by a monitor, needs a page or an endpoint. The router is an
|
||||||
internet-facing host, so what the service shows and where it listens matter.
|
internet-facing host, so what the service shows and where it listens matter.
|
||||||
|
|
||||||
Decision: The `vpn-router-web` unit runs one Python script that uses only the standard library, in
|
Decision: The `vpn-router-web` unit runs one Python script that uses only the standard library, in
|
||||||
the way of a small test server. It binds to the address of the internal interface and to
|
the way of a small test server. It listens on the address of the internal interface and on
|
||||||
`web.port`, on by default at 8080. `/` is for people, in plain text for `curl` and `wget` and as
|
`web.port`, 7081 by default, and is on unless `web.enabled` is `false` (ADR-7). `/` is for
|
||||||
a Tailwind CSS page for a browser, and `/status` is the same status as JSON. It shows the
|
people, in plain text for `curl` and `wget` and as a Tailwind CSS page for a browser, and
|
||||||
state of the services, the package version and the state of the site-to-site, road-warrior and
|
`/status` is the same status as JSON. It shows the state of the services, the package version
|
||||||
WireGuard features, and nothing from the configuration. `vpn-router-setup` resolves the
|
and the state of the site-to-site, road-warrior, OpenVPN and WireGuard features, and nothing from
|
||||||
interface address, writes it with the port and the features to `/run/vpn-router/web.json`,
|
the configuration. `vpn-router-setup` opens the port on the internal interface in UFW and writes
|
||||||
opens the port on the internal interface in UFW and restarts the service, so the service does
|
the features and the facts it resolves to `/run/vpn-router/web.json`, so the service does not
|
||||||
not need the package's configuration code, which needs Jinja2.
|
need the package's configuration code, which needs Jinja2.
|
||||||
|
|
||||||
Consequences: Nothing outside the internal network can reach it, since it listens on the internal
|
Consequences: Nothing outside the internal network can reach it, since it listens on the internal
|
||||||
address only and the firewall opens the port on the internal interface only. It has no
|
address only and the firewall opens the port on the internal interface only. `/` and `/status`
|
||||||
authentication, which is acceptable because it is read only and shows no configuration. It runs
|
have no authentication, which is acceptable because they are read only and show no
|
||||||
as root, because `swanctl` and `wg` need it, with `NoNewPrivileges`, `ProtectSystem=strict`,
|
configuration. It runs as root, because `swanctl` and `wg` need it, with `NoNewPrivileges`,
|
||||||
`ProtectHome` and `PrivateTmp`. The Tailwind style comes from a CDN, so a browser on a network
|
`ProtectSystem=strict`, `ProtectHome` and `PrivateTmp`. The Tailwind style comes from a CDN, so a
|
||||||
without internet shows the unstyled page, and `curl` is unaffected. A change of the internal
|
browser on a network without internet shows the unstyled page, and `curl` is unaffected.
|
||||||
address is picked up at the next run of `vpn-router-setup`.
|
|
||||||
|
|
||||||
## ADR-3: /config is authenticated with a short-lived JWT signed with a shared secret
|
## ADR-3: /config is authenticated with a short-lived JWT signed with a shared secret
|
||||||
|
|
||||||
Status: Accepted; amended by ADR-7
|
Status: Accepted
|
||||||
|
|
||||||
Context: A client such as manage-ui needs the configuration and the resolved state of the
|
Context: A client such as the Router Manager needs the configuration and the resolved state of
|
||||||
router, which `/` and `/status` must not show. The service has only the standard library, listens
|
the router, which `/` and `/status` must not show. The service has only the standard library,
|
||||||
on plain HTTP on the internal network, and its configuration file is readable by root only.
|
listens on plain HTTP on the internal network, and its configuration file is readable by root
|
||||||
|
only.
|
||||||
|
|
||||||
Decision: `/config` needs a JWT in an `Authorization: Bearer` header. The service verifies HS256
|
Decision: `/config` needs a JWT in an `Authorization: Bearer` header. The service verifies HS256
|
||||||
tokens with `hmac` and `hashlib`, with the secret `web.jwt_secret`, and accepts no other
|
tokens with `hmac` and `hashlib`, with the secret `web.jwt_secret`, and accepts no other
|
||||||
algorithm, because verifying RS256 or ES256 needs a library outside the standard library. The
|
algorithm, because verifying RS256 or ES256 needs a library outside the standard library. The
|
||||||
token must carry `exp`, not more than ten minutes ahead, so that the client makes a fresh token for
|
token must carry `exp`, not more than ten minutes ahead, so that the client makes a fresh token for
|
||||||
each use and a leaked one expires. Without a secret the page is not served. The secrets in the
|
each use and a leaked one expires. Without a secret the page is not served. The configuration
|
||||||
configuration shown are masked by name (`psk`, and options with `secret`, `password`, `token` or a
|
shown has no secret options (ADR-8).
|
||||||
`_key` ending in the name).
|
|
||||||
|
|
||||||
Consequences: The client and the router share one secret, and whoever holds it can make tokens,
|
Consequences: The client and the router share one secret, and whoever holds it can make tokens,
|
||||||
so it is long and random; it is checked to have at least 32 characters. The secret is in the
|
so it is long and random; it is checked to have at least 32 characters. The secret is only in the
|
||||||
configuration file and in `/run/vpn-router/web.json`, both readable by root only. The token and
|
configuration file, readable by root only. The token and the answer cross the network
|
||||||
the answer cross the network unencrypted, so the page is for a trusted internal network; HTTPS
|
unencrypted, so the page is for a trusted internal network; HTTPS would need a certificate that
|
||||||
would need a certificate that the package does not have. The masking is by option name, so a new
|
the package does not have.
|
||||||
secret option needs a name that matches.
|
|
||||||
|
|
||||||
## ADR-4: Users are kept in a local OpenLDAP directory
|
## ADR-4: Users are kept in a local OpenLDAP directory
|
||||||
|
|
||||||
Status: Accepted
|
Status: Accepted
|
||||||
|
|
||||||
Context: OpenVPN needs user accounts with passwords, and the users are meant to cover every P2S
|
Context: OpenVPN needs user accounts with passwords, and the users are meant to cover every P2S
|
||||||
system of the router. The options weighed were an external OIDC provider (Entra ID, Keycloak)
|
system of the router, including IKEv2 EAP-TLS and WireGuard, which an OIDC provider cannot
|
||||||
with openvpn-auth-oauth2, an OIDC server embedded on the router, a file of password hashes, and
|
authenticate. Everything the router runs comes from the Ubuntu archive. slapd, its Python module
|
||||||
OpenLDAP on the router. OIDC for OpenVPN needs a browser on the client and a program from outside
|
`python3-ldap` and the OpenVPN plugin `openvpn-auth-ldap` are all in the archive; the plugin
|
||||||
the Ubuntu archive, and it cannot authenticate IKEv2 EAP-TLS or WireGuard. slapd, its Python
|
checks a password by binding as the user, so the hash scheme is slapd's alone. An OIDC provider
|
||||||
module `python3-ldap` and the OpenVPN plugin `openvpn-auth-ldap` are all in the archive; the
|
can read the directory through its LDAP connector.
|
||||||
plugin checks a password by binding as the user, so the hash scheme is slapd's alone. A directory
|
|
||||||
can also be read later by an OIDC provider through its LDAP connector.
|
|
||||||
|
|
||||||
Decision: The users are entries in slapd. The package keeps its own database,
|
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
|
`dc=vpn-router,dc=local` in `/var/lib/ldap/vpn-router` (the path the slapd AppArmor profile
|
||||||
@@ -101,19 +97,18 @@ slapd listens on the local socket only. Provisioning checks every step before ch
|
|||||||
runs at every start of `vpn-router-setup`.
|
runs at every start of `vpn-router-setup`.
|
||||||
|
|
||||||
Consequences: The package depends on `slapd`, `python3-ldap`, `openvpn` and
|
Consequences: The package depends on `slapd`, `python3-ldap`, `openvpn` and
|
||||||
`openvpn-auth-ldap`, and `vpn-router-setup` and `vpn-router-user` import `python3-ldap`, the first
|
`openvpn-auth-ldap`, and `vpn-router-setup` and `vpn-router-user` import `python3-ldap`. The
|
||||||
library outside the standard library besides `jinja2`. The service account's password is a
|
service account's password is a root-only file. The directory is user data: it is kept on purge,
|
||||||
root-only file. The directory is user data: it is kept when the features are switched off and
|
and it is backed up with `slapcat`, not by copying files.
|
||||||
on purge, and it is backed up with `slapcat`, not by copying files.
|
|
||||||
|
|
||||||
## ADR-5: OpenVPN clients need a certificate and the password of the user it names
|
## ADR-5: OpenVPN clients need a certificate and the password of the user it names
|
||||||
|
|
||||||
Status: Accepted
|
Status: Accepted
|
||||||
|
|
||||||
Context: The router CA already issues client certificates for IKEv2. OpenVPN can check a
|
Context: The router CA issues client certificates for IKEv2. OpenVPN can check a certificate, a
|
||||||
certificate, a password, or both. With both, a certificate alone and a password alone are not
|
password, or both. With both, a certificate alone and a password alone are not enough, but
|
||||||
enough, but `openvpn-auth-ldap` checks only the password of the user name given at login, which
|
`openvpn-auth-ldap` checks only the password of the user name given at login, which need not be
|
||||||
need not be the user of the certificate.
|
the user of the certificate.
|
||||||
|
|
||||||
Decision: OpenVPN checks the certificate against the CA and the CRL, the password with
|
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`,
|
`openvpn-auth-ldap`, and runs `openvpn-same-user` as `auth-user-pass-verify` with `via-env`,
|
||||||
@@ -144,19 +139,18 @@ lock, `/etc/vpn-router/pki` for the certificates and the CA's state, and `/etc/s
|
|||||||
for the published CRL. A lock serializes the program's runs, because `simple-ca` keeps its state
|
for the published CRL. A lock serializes the program's runs, because `simple-ca` keeps its state
|
||||||
in one file.
|
in one file.
|
||||||
|
|
||||||
Consequences: The web service still imports nothing outside the standard library, and the same
|
Consequences: The web service imports nothing outside the standard library, and the same program
|
||||||
program serves the command line and the API. Each request starts a process, which is fine for
|
serves the command line and the API. Each request starts a process, which is fine for user
|
||||||
user administration. The API is protected by the token of `/config`, so whoever has the secret
|
administration. The API is protected by the token of `/config`, so whoever has the secret can
|
||||||
can manage users.
|
manage users.
|
||||||
|
|
||||||
## ADR-7: The web service starts on its own and reads its inputs at each request
|
## ADR-7: The web service starts on its own and reads its inputs at each request
|
||||||
|
|
||||||
Status: Accepted
|
Status: Accepted
|
||||||
|
|
||||||
Context: The web service took the address to listen on, the port, the token secret and the
|
Context: The status page is the tool to see a problem of `vpn-router-setup`, so it must run
|
||||||
features from `/run/vpn-router/web.json`, written by `vpn-router-setup`, and ended with a failure
|
whatever state setup is in, also before setup has run and while it fails. The internal address,
|
||||||
while the file or the internal address was missing. It was ordered after `vpn-router-setup`, so a
|
`web.port` and `web.enabled` can change at any time.
|
||||||
problem in the setup kept the status page, the tool to see that problem, from running.
|
|
||||||
|
|
||||||
Decision: The service depends on nothing to start and does not end on its own. A discovery
|
Decision: The service depends on nothing to start and does not end on its own. A discovery
|
||||||
thread works out, every 30 seconds, the internal address from `vpn-router.json` as
|
thread works out, every 30 seconds, the internal address from `vpn-router.json` as
|
||||||
@@ -165,12 +159,67 @@ standard library and which `vpnrouter.py` uses too, and `web.port` and `web.enab
|
|||||||
same file. It runs the HTTP server in a thread of its own at that address and port, stops it and
|
same file. It runs the HTTP server in a thread of its own at that address and port, stops it and
|
||||||
starts a new one when they change, and runs none while the service is disabled, the address is
|
starts a new one when they change, and runs none while the service is disabled, the address is
|
||||||
not known or it cannot be bound. Each request reads the token secret from `vpn-router.json` and
|
not known or it cannot be bound. Each request reads the token secret from `vpn-router.json` and
|
||||||
the features and resolved facts from `web.json`, which setup still writes, because they need the
|
the features and resolved facts from `web.json`, which setup writes, because they need the
|
||||||
configuration code; while that file is missing the router shows no features. The unit is not
|
configuration code; while that file is missing the router shows no features. The unit is not
|
||||||
ordered after `vpn-router-setup`, and setup does not restart it.
|
ordered after `vpn-router-setup`, and setup does not restart it.
|
||||||
|
|
||||||
Consequences: The status page answers as soon as the internal address exists, whatever state
|
Consequences: The status page answers as soon as the internal address exists, whatever state
|
||||||
setup is in, and follows a changed address, port or `web.enabled` within 30 seconds without a
|
setup is in, and follows a changed address, port or `web.enabled` within 30 seconds without a
|
||||||
restart. `web.json` no longer holds the address, the port or the secret, so the secret is only in
|
restart. A change of the secret applies to the next request. The interfaces are read every 30
|
||||||
`vpn-router.json`. A change of the secret applies to the next request. The interfaces are read
|
seconds, which costs a few `ip` commands.
|
||||||
every 30 seconds, which costs a few `ip` commands.
|
|
||||||
|
## ADR-8: /config leaves secret options out
|
||||||
|
|
||||||
|
Status: Accepted
|
||||||
|
|
||||||
|
Context: A secret has no use outside the router, so a client never needs it, and showing even
|
||||||
|
that a secret is set tells a reader more than it needs.
|
||||||
|
|
||||||
|
Decision: The configuration that `vpn-router-setup` writes for the web service has no secret
|
||||||
|
options at all. They are recognised by name: `psk`, and options with `secret`, `password`,
|
||||||
|
`token` or a `_key` ending in the name.
|
||||||
|
|
||||||
|
Consequences: No secret reaches the web service's state file or any answer of the API. A client
|
||||||
|
cannot tell whether a secret is set. A new secret option needs a name that matches.
|
||||||
|
|
||||||
|
## ADR-9: The Router Manager is a separate package with a small Python server
|
||||||
|
|
||||||
|
Status: Accepted
|
||||||
|
|
||||||
|
Context: The router needs a web application to manage it, which may run on the router or on
|
||||||
|
another Linux system. The router's web service needs a token signed with `web.jwt_secret` for
|
||||||
|
`/config` and `/users` and sends no CORS headers, so a browser cannot call it with the secret
|
||||||
|
kept away from the page.
|
||||||
|
|
||||||
|
Decision: `vpn-router-manager` is a Debian package of its own, built in the repository
|
||||||
|
`linux-cloud-router-manager`. A React and MUI application in the style of manage-ui is built
|
||||||
|
with Vite when the package is built and shipped as static files. A Python server, using the
|
||||||
|
Ubuntu packages `python3-jwt` and `python3-requests`, serves them and passes every `/api/<path>`
|
||||||
|
call to the router's `/<path>`, signing a short-lived token with the router's secret; it holds
|
||||||
|
no logic of the router's own. It reads its settings from `/etc/vpn-router-manager/manager.json`
|
||||||
|
at each request; on the router the settings not given come from `vpn-router.json`, and it
|
||||||
|
listens on the internal address like the status web service, with the same discovery thread.
|
||||||
|
`vpn-router-setup` opens the manager's port on the internal interface while the manager is
|
||||||
|
installed, so the firewall stays in one place. The manager asks for no authentication.
|
||||||
|
|
||||||
|
Consequences: The router needs no Node.js; the package needs only `python3` and packages of the
|
||||||
|
Ubuntu archive. The secret never reaches the browser. The application checks the shape of every
|
||||||
|
answer of the router. Anyone who reaches the manager's port manages the router, so on another
|
||||||
|
system it listens on `127.0.0.1` unless told otherwise. The router's firewall depends on whether
|
||||||
|
the manager is installed, and a changed manager port needs `vpn-router-setup` to run again.
|
||||||
|
|
||||||
|
## ADR-10: Users and their certificates do not depend on a P2S system being enabled
|
||||||
|
|
||||||
|
Status: Accepted
|
||||||
|
|
||||||
|
Context: Users are prepared before a P2S system is switched on, and the Router Manager manages
|
||||||
|
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.
|
||||||
|
|
||||||
|
Consequences: slapd always holds the directory and the CA always exists on a configured router,
|
||||||
|
even when no P2S system uses them. An operator who wants a CA of their own must place it before
|
||||||
|
choosing the platform.
|
||||||
@@ -122,7 +122,7 @@ must have the type shown below; an unknown section or setting is an error.
|
|||||||
| `docker` | `mode` | string | `auto` | `auto` secures Docker when its service is installed, `on` requires it, `off` removes the rules |
|
| `docker` | `mode` | string | `auto` | `auto` secures Docker when its service is installed, `on` requires it, `off` removes the rules |
|
||||||
| `docker` | `exposed_ports` | list | | TCP ports and port ranges (`8000:8100`) reachable from the external interface; at most 15, a range counting as two |
|
| `docker` | `exposed_ports` | list | | TCP ports and port ranges (`8000:8100`) reachable from the external interface; at most 15, a range counting as two |
|
||||||
| `web` | `enabled` | boolean | `true` | The status web service on the internal address |
|
| `web` | `enabled` | boolean | `true` | The status web service on the internal address |
|
||||||
| `web` | `port` | integer | `8080` | TCP port of the status web service |
|
| `web` | `port` | integer | `7081` | TCP port of the status web service |
|
||||||
| `web` | `jwt_secret` | string | | Secret, at least 32 characters, that signs the tokens for `/config`; without it `/config` is not served |
|
| `web` | `jwt_secret` | string | | Secret, at least 32 characters, that signs the tokens for `/config`; without it `/config` is not served |
|
||||||
|
|
||||||
A minimal site-to-site configuration in `auto` mode:
|
A minimal site-to-site configuration in `auto` mode:
|
||||||
@@ -272,12 +272,12 @@ ca_cert.pem ca_key.pem ca_bundle.pem <label>_cert.pem <label>_key.pem crl.p
|
|||||||
`<label>` is the first component of the router FQDN, so `router.example.com` gives
|
`<label>` is the first component of the router FQDN, so `router.example.com` gives
|
||||||
`router_cert.pem`.
|
`router_cert.pem`.
|
||||||
|
|
||||||
If road-warrior or OpenVPN access is enabled and this directory is empty, a certificate
|
Once a platform is chosen and while this directory is empty, a certificate authority and a CRL
|
||||||
authority, a server certificate and a CRL valid for ten years are created there
|
valid for ten years are created there automatically, whether or not road-warrior or OpenVPN
|
||||||
automatically; a CA without a CRL gets one. User certificates are issued by
|
access is enabled. A CA of the router's own (with `ca_key.pem`) without a CRL gets one, and
|
||||||
`vpn-router-user` into `users/`. To use your own instead, place the
|
without a server certificate gets one once `wan.local_fqdn` is set. User certificates are issued
|
||||||
files in that layout before enabling the feature; nothing is generated or overwritten when
|
by `vpn-router-user` into `users/`. To use your own CA instead, place the files in that layout
|
||||||
material is already present.
|
before choosing the platform; nothing present is overwritten.
|
||||||
|
|
||||||
Either way, the package copies what each consumer needs into place: the CA into
|
Either way, the package copies what each consumer needs into place: the CA into
|
||||||
`/etc/swanctl/x509ca/`, the server certificate and key into `/etc/swanctl/x509/` and
|
`/etc/swanctl/x509ca/`, the server certificate and key into `/etc/swanctl/x509/` and
|
||||||
@@ -314,8 +314,9 @@ certificate names:
|
|||||||
|
|
||||||
### The user directory
|
### The user directory
|
||||||
|
|
||||||
While OpenVPN or road-warrior access is configured, `vpn-router-setup` keeps a directory of
|
Once a platform is chosen, `vpn-router-setup` keeps a directory of users in the local slapd,
|
||||||
users in the local slapd:
|
whether or not OpenVPN or road-warrior access is enabled, so users can be managed before either
|
||||||
|
is switched on:
|
||||||
|
|
||||||
- its own database, `dc=vpn-router,dc=local` in `/var/lib/ldap/vpn-router`, next to the one the
|
- its own database, `dc=vpn-router,dc=local` in `/var/lib/ldap/vpn-router`, next to the one the
|
||||||
slapd package creates; root manages it through the local socket, with no administrator
|
slapd package creates; root manages it through the local socket, with no administrator
|
||||||
@@ -328,7 +329,7 @@ users in the local slapd:
|
|||||||
(`pwdAccountLockedTime`);
|
(`pwdAccountLockedTime`);
|
||||||
- slapd listens on its local socket only (`SLAPD_SERVICES` in `/etc/default/slapd`).
|
- slapd listens on its local socket only (`SLAPD_SERVICES` in `/etc/default/slapd`).
|
||||||
|
|
||||||
The directory is kept when OpenVPN and road-warrior access are switched off, and on purge.
|
The directory is kept on purge.
|
||||||
|
|
||||||
### Managing users
|
### Managing users
|
||||||
|
|
||||||
@@ -406,19 +407,19 @@ modules shipped with the package; the configuration file accepts any module inst
|
|||||||
## Status web service
|
## Status web service
|
||||||
|
|
||||||
The `vpn-router-web` service shows the state of the router on its internal address and
|
The `vpn-router-web` service shows the state of the router on its internal address and
|
||||||
`web.port` (8080 by default). It uses only the Python standard library, serves HTTP without
|
`web.port` (7081 by default). It uses only the Python standard library, serves HTTP without
|
||||||
authentication, and shows no configuration: no key, identity, address or subnet.
|
authentication, and shows no configuration: no key, identity, address or subnet.
|
||||||
|
|
||||||
| Page | Content |
|
| Page | Content |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `/` | The status for people: plain text for `curl` and `wget`, a page styled with Tailwind CSS for a browser, in the browser's light or dark scheme, that reloads itself every minute (the style sheet is loaded from a CDN, so the browser needs internet access; the text is readable without it) |
|
| `/` | The status for people: plain text for `curl` and `wget`, a page styled with Tailwind CSS for a browser, in the browser's light or dark scheme, that reloads itself every minute (the style sheet is loaded from a CDN, so the browser needs internet access; the text is readable without it) |
|
||||||
| `/status` | The same status as JSON |
|
| `/status` | The same status as JSON |
|
||||||
| `/config` | The configuration with its secrets masked, and what the router resolved and runs, as JSON; needs a token |
|
| `/config` | The configuration without its secret options, and what the router resolved and runs, as JSON; needs a token |
|
||||||
| `/users` | The users of OpenVPN and road-warrior access; needs a token |
|
| `/users` | The users of OpenVPN and road-warrior access; needs a token |
|
||||||
|
|
||||||
The status holds the state of `vpn-router-setup`, of `strongswan` when site-to-site or road-warrior
|
The status holds the state of `vpn-router-setup` and `slapd`, of `strongswan` when site-to-site or
|
||||||
access is configured, of `wg-quick@wg0` when WireGuard is, of `openvpn-server@vpn-router` when
|
road-warrior access is configured, of `wg-quick@wg0` when WireGuard is, and of
|
||||||
OpenVPN is, and of `slapd` when either OpenVPN or road-warrior access is; the number of connected
|
`openvpn-server@vpn-router` when OpenVPN is; the number of connected
|
||||||
OpenVPN clients, from the server's status file, which OpenVPN rewrites every minute; the package version; whether
|
OpenVPN clients, from the server's status file, which OpenVPN rewrites every minute; the package version; whether
|
||||||
Docker is installed, and when it is, the state of `docker.service` and of `vpn-router-docker`
|
Docker is installed, and when it is, the state of `docker.service` and of `vpn-router-docker`
|
||||||
(the Docker rules), which do not change `status`; whether the
|
(the Docker rules), which do not change `status`; whether the
|
||||||
@@ -427,8 +428,8 @@ the WireGuard peers with the age of their last handshake and their traffic. `sta
|
|||||||
all these services are active, and `degraded` otherwise.
|
all these services are active, and `degraded` otherwise.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl http://10.0.0.4:8080/
|
curl http://10.0.0.4:7081/
|
||||||
curl http://10.0.0.4:8080/status
|
curl http://10.0.0.4:7081/status
|
||||||
```
|
```
|
||||||
|
|
||||||
### The /config page
|
### The /config page
|
||||||
@@ -437,7 +438,7 @@ curl http://10.0.0.4:8080/status
|
|||||||
|
|
||||||
| Key | Content |
|
| Key | Content |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `configuration` | `vpn-router.json` with the defaults applied and the secrets masked (`psk`, `jwt_secret`, and any option named like a password, token or key) |
|
| `configuration` | `vpn-router.json` with the defaults applied, without its secret options (`psk`, `jwt_secret`, and any option named like a password, token or key) |
|
||||||
| `network` | The external and internal interface, their addresses, the internal gateway, the local and remote IKE identities and subnets, and the IPv4 routes as `ip -j -4 route` prints them |
|
| `network` | The external and internal interface, their addresses, the internal gateway, the local and remote IKE identities and subnets, and the IPv4 routes as `ip -j -4 route` prints them |
|
||||||
| `ipsec` | `connections` and `sas`: one object per connection and per IKE SA, read from `swanctl --list-conns --pretty` and `swanctl --list-sas --pretty`, with the connection's name under `name` and swanctl's keys and string values |
|
| `ipsec` | `connections` and `sas`: one object per connection and per IKE SA, read from `swanctl --list-conns --pretty` and `swanctl --list-sas --pretty`, with the connection's name under `name` and swanctl's keys and string values |
|
||||||
| `wireguard` | The public key and port of `wg0`, and each peer's public key, endpoint, allowed IPs, handshake age and traffic; no private or preshared key |
|
| `wireguard` | The public key and port of `wg0`, and each peer's public key, endpoint, allowed IPs, handshake age and traffic; no private or preshared key |
|
||||||
@@ -465,7 +466,7 @@ print(f'{head}.{body}.{sign}')
|
|||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -H "Authorization: Bearer $TOKEN" http://10.0.0.4:8080/config
|
curl -H "Authorization: Bearer $TOKEN" http://10.0.0.4:7081/config
|
||||||
```
|
```
|
||||||
|
|
||||||
### The /users API
|
### The /users API
|
||||||
@@ -484,13 +485,13 @@ and answer bodies are JSON; a request body is at most 4096 bytes.
|
|||||||
| `GET /users/<name>/profile` | | `{"name", "profile"}`, the OpenVPN client profile |
|
| `GET /users/<name>/profile` | | `{"name", "profile"}`, the OpenVPN client profile |
|
||||||
|
|
||||||
A failure is `{"error": ...}` with 400 for an invalid request, 404 for a user that does not
|
A failure is `{"error": ...}` with 400 for an invalid request, 404 for a user that does not
|
||||||
exist, 409 for a user that exists and 500 otherwise. While neither OpenVPN nor road-warrior
|
exist, 409 for a user that exists and 500 otherwise. `/users` works whether or not OpenVPN or
|
||||||
access is configured, `/users` answers 404. Passwords and private keys cross the internal
|
road-warrior access is enabled; the profile needs OpenVPN. Passwords and private keys cross the internal
|
||||||
network in plain HTTP, like the token.
|
network in plain HTTP, like the token.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
curl -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
||||||
-d '{"name": "alice", "password": "s3cr3t-pass"}' http://10.0.0.4:8080/users
|
-d '{"name": "alice", "password": "s3cr3t-pass"}' http://10.0.0.4:7081/users
|
||||||
```
|
```
|
||||||
|
|
||||||
The service starts on its own, without waiting for `vpn-router-setup`, and keeps running. Every
|
The service starts on its own, without waiting for `vpn-router-setup`, and keeps running. Every
|
||||||
@@ -502,6 +503,15 @@ request reads the token secret from `vpn-router.json`, and the features and the
|
|||||||
before that file exists the page shows no features. `vpn-router-setup` opens `web.port` on the
|
before that file exists the page shows no features. `vpn-router-setup` opens `web.port` on the
|
||||||
internal interface in the firewall.
|
internal interface in the firewall.
|
||||||
|
|
||||||
|
## Router Manager
|
||||||
|
|
||||||
|
The `vpn-router-manager` package, built in the repository `linux-cloud-router-manager`, is a web
|
||||||
|
application that shows the state of the router and manages its users through the status web
|
||||||
|
service. It can be installed on the router or on another Linux system. While it is installed on
|
||||||
|
the router, `vpn-router-setup` opens its port (`port` in `/etc/vpn-router-manager/manager.json`,
|
||||||
|
7080 by default, HTTP; 7443 is kept for it with TLS) on the internal interface; restart
|
||||||
|
`vpn-router-setup` after changing it.
|
||||||
|
|
||||||
## Removal
|
## Removal
|
||||||
|
|
||||||
`apt-get remove` stops `vpn-router-docker`, which removes the Docker rules. It terminates the
|
`apt-get remove` stops `vpn-router-docker`, which removes the Docker rules. It terminates the
|
||||||
|
|||||||
Whitespace-only changes.
@@ -1,3 +1,57 @@
|
|||||||
|
vpn-router (0.5.6-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
update: the status web service listens on port 7081 by default.
|
||||||
|
|
||||||
|
* web.port defaults to 7081.
|
||||||
|
* vpn-router-setup opens the Router Manager's port, 7080 by default, on
|
||||||
|
the internal interface.
|
||||||
|
|
||||||
|
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Sat, 24 Oct 2026 18:00:00 +0200
|
||||||
|
|
||||||
|
vpn-router (0.5.5-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
feature: users and their certificates no longer need a P2S system enabled.
|
||||||
|
|
||||||
|
* vpn-router-setup provisions the user directory, the CA and its CRL once
|
||||||
|
a platform is chosen, whether or not OpenVPN or road-warrior access is
|
||||||
|
enabled. The server certificate is issued once wan.local_fqdn is set,
|
||||||
|
also for an existing CA that has none.
|
||||||
|
* /users and vpn-router-user manage users and issue and revoke their
|
||||||
|
certificates without a P2S system; only the OpenVPN client profile
|
||||||
|
needs OpenVPN.
|
||||||
|
|
||||||
|
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Sat, 24 Oct 2026 16:00:00 +0200
|
||||||
|
|
||||||
|
vpn-router (0.5.4-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
feature: the firewall lets the Router Manager in on the internal interface.
|
||||||
|
|
||||||
|
* While vpn-router-manager is installed, vpn-router-setup opens its port,
|
||||||
|
port in /etc/vpn-router-manager/manager.json or 8090, on the internal
|
||||||
|
interface. Purging the package removes the rule.
|
||||||
|
|
||||||
|
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Sat, 24 Oct 2026 13:00:00 +0200
|
||||||
|
|
||||||
|
vpn-router (0.5.3-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
fix: the status always shows the state of slapd.
|
||||||
|
|
||||||
|
* /status and the status page show the state of slapd whether or not
|
||||||
|
OpenVPN or road-warrior access is configured, since the package always
|
||||||
|
runs it.
|
||||||
|
|
||||||
|
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Sat, 24 Oct 2026 12:00:00 +0200
|
||||||
|
|
||||||
|
vpn-router (0.5.2-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
fix: /config no longer shows secret options.
|
||||||
|
|
||||||
|
* The configuration shown by /config leaves out the secret options (psk,
|
||||||
|
jwt_secret, and options named like a password, token or key) instead of
|
||||||
|
showing them masked.
|
||||||
|
|
||||||
|
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Sat, 24 Oct 2026 11:00:00 +0200
|
||||||
|
|
||||||
vpn-router (0.5.1-1) unstable; urgency=medium
|
vpn-router (0.5.1-1) unstable; urgency=medium
|
||||||
|
|
||||||
fix: the web service works in every state of the router.
|
fix: the web service works in every state of the router.
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ strip_ufw_blocks() {
|
|||||||
|
|
||||||
tmp="$(mktemp)"
|
tmp="$(mktemp)"
|
||||||
awk '
|
awk '
|
||||||
/^# (IPSEC|WIREGUARD|OPENVPN|P2S DNS|WEB|ROUTER FORWARD|ROUTER NAT|ROUTER MSS) RULES START$/ {
|
/^# (IPSEC|WIREGUARD|OPENVPN|P2S DNS|WEB|MANAGER|ROUTER FORWARD|ROUTER NAT|ROUTER MSS) RULES START$/ {
|
||||||
skip = 1; pending = 0; next
|
skip = 1; pending = 0; next
|
||||||
}
|
}
|
||||||
/^# (IPSEC|WIREGUARD|OPENVPN|P2S DNS|WEB|ROUTER FORWARD|ROUTER NAT|ROUTER MSS) RULES END$/ {
|
/^# (IPSEC|WIREGUARD|OPENVPN|P2S DNS|WEB|MANAGER|ROUTER FORWARD|ROUTER NAT|ROUTER MSS) RULES END$/ {
|
||||||
skip = 0; next
|
skip = 0; next
|
||||||
}
|
}
|
||||||
skip { next }
|
skip { next }
|
||||||
|
|||||||
@@ -31,9 +31,14 @@ UFW_DEFAULTS = pathlib.Path('/etc/default/ufw')
|
|||||||
WG_DIR = pathlib.Path('/etc/wireguard')
|
WG_DIR = pathlib.Path('/etc/wireguard')
|
||||||
WEB_STATE = pathlib.Path('/run/vpn-router/web.json')
|
WEB_STATE = pathlib.Path('/run/vpn-router/web.json')
|
||||||
|
|
||||||
|
#: The Router Manager, vpn-router-manager, when it is installed, and its settings.
|
||||||
|
MANAGER = pathlib.Path('/usr/lib/vpn-router-manager/manager')
|
||||||
|
MANAGER_CONFIG = pathlib.Path('/etc/vpn-router-manager/manager.json')
|
||||||
|
MANAGER_PORT = 7080
|
||||||
|
|
||||||
FILTER_ANCHOR = '# End required lines'
|
FILTER_ANCHOR = '# End required lines'
|
||||||
|
|
||||||
#: Configuration options whose values are masked wherever the configuration is shown.
|
#: Configuration options left out wherever the configuration is shown.
|
||||||
SECRET_OPTION = re.compile(r'(^psk$)|secret|password|token|_key$')
|
SECRET_OPTION = re.compile(r'(^psk$)|secret|password|token|_key$')
|
||||||
|
|
||||||
BLOCKS = (
|
BLOCKS = (
|
||||||
@@ -42,6 +47,7 @@ BLOCKS = (
|
|||||||
('# OPENVPN RULES START', '# OPENVPN RULES END'),
|
('# OPENVPN RULES START', '# OPENVPN RULES END'),
|
||||||
('# P2S DNS RULES START', '# P2S DNS RULES END'),
|
('# P2S DNS RULES START', '# P2S DNS RULES END'),
|
||||||
('# WEB RULES START', '# WEB RULES END'),
|
('# WEB RULES START', '# WEB RULES END'),
|
||||||
|
('# MANAGER RULES START', '# MANAGER RULES END'),
|
||||||
('# ROUTER FORWARD RULES START', '# ROUTER FORWARD RULES END'),
|
('# ROUTER FORWARD RULES START', '# ROUTER FORWARD RULES END'),
|
||||||
('# ROUTER NAT RULES START', '# ROUTER NAT RULES END'),
|
('# ROUTER NAT RULES START', '# ROUTER NAT RULES END'),
|
||||||
('# ROUTER MSS RULES START', '# ROUTER MSS RULES END'),
|
('# ROUTER MSS RULES START', '# ROUTER MSS RULES END'),
|
||||||
@@ -129,6 +135,15 @@ def is_configured(ctx):
|
|||||||
vpnrouter.openvpn_ready(ctx)))
|
vpnrouter.openvpn_ready(ctx)))
|
||||||
|
|
||||||
|
|
||||||
|
def manager_port():
|
||||||
|
"""The port of the Router Manager: port in its settings, or MANAGER_PORT when it is not set or not a port."""
|
||||||
|
try:
|
||||||
|
port = json.loads(MANAGER_CONFIG.read_text(encoding='utf-8')).get('port')
|
||||||
|
except (OSError, ValueError, AttributeError):
|
||||||
|
return MANAGER_PORT
|
||||||
|
return port if isinstance(port, int) and not isinstance(port, bool) and 1 <= port <= 65535 else MANAGER_PORT
|
||||||
|
|
||||||
|
|
||||||
def filter_block(ctx):
|
def filter_block(ctx):
|
||||||
lines = [
|
lines = [
|
||||||
'',
|
'',
|
||||||
@@ -185,6 +200,15 @@ def filter_block(ctx):
|
|||||||
'# WEB RULES END',
|
'# WEB RULES END',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if MANAGER.exists() and ctx['int_iface']:
|
||||||
|
port = manager_port()
|
||||||
|
lines += [
|
||||||
|
'',
|
||||||
|
'# MANAGER RULES START',
|
||||||
|
f'-A ufw-before-input -i {ctx["int_iface"]} -p tcp --dport {port} -j ACCEPT',
|
||||||
|
'# MANAGER RULES END',
|
||||||
|
]
|
||||||
|
|
||||||
if vpnrouter.routing_ready(ctx):
|
if vpnrouter.routing_ready(ctx):
|
||||||
lines += ['', '# ROUTER FORWARD RULES START']
|
lines += ['', '# ROUTER FORWARD RULES START']
|
||||||
for subnet in ctx['local_cidr_list']:
|
for subnet in ctx['local_cidr_list']:
|
||||||
@@ -323,13 +347,8 @@ def manage_wireguard(ctx, changed):
|
|||||||
run(['systemctl', 'restart', unit], check=False)
|
run(['systemctl', 'restart', unit], check=False)
|
||||||
|
|
||||||
|
|
||||||
def manage_directory(ctx):
|
def manage_directory():
|
||||||
"""Provision the user directory in slapd while a P2S system that has users is configured.
|
"""Provision the user directory in slapd, whether or not a P2S system is configured."""
|
||||||
|
|
||||||
The directory is left in place when they are switched off, so the users are kept.
|
|
||||||
"""
|
|
||||||
if not (vpnrouter.p2s_ready(ctx) or vpnrouter.openvpn_ready(ctx)):
|
|
||||||
return
|
|
||||||
if vpnrouter_ldap.ensure_services() or not unit_is_active('slapd'):
|
if vpnrouter_ldap.ensure_services() or not unit_is_active('slapd'):
|
||||||
run(['systemctl', 'restart', 'slapd'])
|
run(['systemctl', 'restart', 'slapd'])
|
||||||
try:
|
try:
|
||||||
@@ -381,14 +400,13 @@ def refresh_web(cfg, ctx):
|
|||||||
"""Hand the web service the features and facts it shows.
|
"""Hand the web service the features and facts it shows.
|
||||||
|
|
||||||
The service reads the file at each request: the features of the router,
|
The service reads the file at each request: the features of the router,
|
||||||
the configuration with its secrets masked and the facts resolved from the
|
the configuration without its secret options and the facts resolved from
|
||||||
system, which it cannot work out with the standard library alone. The file
|
the system, which it cannot work out with the standard library alone. The
|
||||||
is readable by root only. The service follows a changed address or port
|
file is readable by root only. The service follows a changed address or
|
||||||
itself.
|
port itself.
|
||||||
"""
|
"""
|
||||||
masked = {
|
public = {
|
||||||
section: {option: '********' if SECRET_OPTION.search(option) and value else value
|
section: {option: value for option, value in options.items() if not SECRET_OPTION.search(option)}
|
||||||
for option, value in options.items()}
|
|
||||||
for section, options in cfg.items()
|
for section, options in cfg.items()
|
||||||
}
|
}
|
||||||
WEB_STATE.parent.mkdir(parents=True, exist_ok=True)
|
WEB_STATE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -399,8 +417,7 @@ def refresh_web(cfg, ctx):
|
|||||||
'p2s': bool(vpnrouter.p2s_ready(ctx)),
|
'p2s': bool(vpnrouter.p2s_ready(ctx)),
|
||||||
'wireguard': bool(vpnrouter.wg_ready(ctx)),
|
'wireguard': bool(vpnrouter.wg_ready(ctx)),
|
||||||
'openvpn': bool(vpnrouter.openvpn_ready(ctx)),
|
'openvpn': bool(vpnrouter.openvpn_ready(ctx)),
|
||||||
'users': bool(vpnrouter.p2s_ready(ctx) or vpnrouter.openvpn_ready(ctx)),
|
'configuration': public,
|
||||||
'configuration': masked,
|
|
||||||
'network': {
|
'network': {
|
||||||
'external_interface': ctx['wan_iface'],
|
'external_interface': ctx['wan_iface'],
|
||||||
'internal_interface': ctx['int_iface'],
|
'internal_interface': ctx['int_iface'],
|
||||||
@@ -446,7 +463,7 @@ def main():
|
|||||||
elif rules_changed:
|
elif rules_changed:
|
||||||
run(['ufw', 'reload'], check=False)
|
run(['ufw', 'reload'], check=False)
|
||||||
|
|
||||||
manage_directory(ctx)
|
manage_directory()
|
||||||
manage_strongswan(ctx, changed)
|
manage_strongswan(ctx, changed)
|
||||||
manage_wireguard(ctx, changed)
|
manage_wireguard(ctx, changed)
|
||||||
manage_openvpn(ctx, changed)
|
manage_openvpn(ctx, changed)
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ SCHEMA = {
|
|||||||
'wireguard': {'enabled': False, 'address': '', 'listen_port': 51820},
|
'wireguard': {'enabled': False, 'address': '', 'listen_port': 51820},
|
||||||
'openvpn': {'enabled': False, 'address_pool': '', 'port': 1194, 'protocol': 'udp'},
|
'openvpn': {'enabled': False, 'address_pool': '', 'port': 1194, 'protocol': 'udp'},
|
||||||
'docker': {'mode': 'auto', 'exposed_ports': []},
|
'docker': {'mode': 'auto', 'exposed_ports': []},
|
||||||
'web': {'enabled': True, 'port': 8080, 'jwt_secret': ''},
|
'web': {'enabled': True, 'port': 7081, 'jwt_secret': ''},
|
||||||
}
|
}
|
||||||
|
|
||||||
#: The shortest secret that signs the tokens of the status web service.
|
#: The shortest secret that signs the tokens of the status web service.
|
||||||
@@ -565,24 +565,28 @@ def _simple_ca(*args):
|
|||||||
|
|
||||||
|
|
||||||
def provision_pki(ctx):
|
def provision_pki(ctx):
|
||||||
"""Create the CA, server certificate and CRL when the PKI directory is empty; a CA without a CRL gets one."""
|
"""Create the CA and CRL when the PKI directory is empty, and the server certificate once wan.local_fqdn is set.
|
||||||
|
|
||||||
|
A CA of the router's own (ca_key.pem present) without a CRL gets one, and without a
|
||||||
|
certificate for wan.local_fqdn gets one too; nothing present is overwritten.
|
||||||
|
"""
|
||||||
PKI_DIR.mkdir(parents=True, exist_ok=True)
|
PKI_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
os.chmod(PKI_DIR, 0o700)
|
os.chmod(PKI_DIR, 0o700)
|
||||||
|
|
||||||
if any(PKI_DIR.iterdir()):
|
|
||||||
if (PKI_DIR / 'ca_key.pem').is_file() and not (PKI_DIR / 'crl.pem').is_file():
|
|
||||||
_simple_ca('make-crl', '--ca-dir', str(PKI_DIR), '--days', str(CRL_DAYS))
|
|
||||||
return
|
|
||||||
if not ctx['p2s_ca_name']:
|
|
||||||
warn('p2s.ca_name is empty, not creating a local CA')
|
|
||||||
return
|
|
||||||
if not SIMPLE_CA.is_file():
|
if not SIMPLE_CA.is_file():
|
||||||
fail(f'{SIMPLE_CA} is missing')
|
fail(f'{SIMPLE_CA} is missing')
|
||||||
|
|
||||||
_simple_ca('make-ca', '--ca-dir', str(PKI_DIR), ctx['p2s_ca_name'])
|
if not any(PKI_DIR.iterdir()):
|
||||||
_simple_ca('make-cert', '--ca-dir', str(PKI_DIR),
|
if not ctx['p2s_ca_name']:
|
||||||
ctx['local_fqdn'], ctx['local_fqdn'])
|
warn('p2s.ca_name is empty, not creating a local CA')
|
||||||
_simple_ca('make-crl', '--ca-dir', str(PKI_DIR), '--days', str(CRL_DAYS))
|
return
|
||||||
|
_simple_ca('make-ca', '--ca-dir', str(PKI_DIR), ctx['p2s_ca_name'])
|
||||||
|
if not (PKI_DIR / 'ca_key.pem').is_file():
|
||||||
|
return
|
||||||
|
if not (PKI_DIR / 'crl.pem').is_file():
|
||||||
|
_simple_ca('make-crl', '--ca-dir', str(PKI_DIR), '--days', str(CRL_DAYS))
|
||||||
|
label = ctx['local_fqdn'].split('.', 1)[0] if ctx['local_fqdn'] else ''
|
||||||
|
if label and not (PKI_DIR / f'{label}_cert.pem').is_file():
|
||||||
|
_simple_ca('make-cert', '--ca-dir', str(PKI_DIR), ctx['local_fqdn'], ctx['local_fqdn'])
|
||||||
|
|
||||||
|
|
||||||
def distribute_pki(ctx):
|
def distribute_pki(ctx):
|
||||||
@@ -628,8 +632,7 @@ def configure(cfg=None, local_id_override=None):
|
|||||||
|
|
||||||
changed = set()
|
changed = set()
|
||||||
|
|
||||||
if p2s_ready(ctx) or openvpn_ready(ctx):
|
provision_pki(ctx)
|
||||||
provision_pki(ctx)
|
|
||||||
if distribute_pki(ctx):
|
if distribute_pki(ctx):
|
||||||
changed.add('swanctl')
|
changed.add('swanctl')
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Pages:
|
|||||||
/status the status as JSON
|
/status the status as JSON
|
||||||
/config the configuration without its secrets, and what the router resolved and
|
/config the configuration without its secrets, and what the router resolved and
|
||||||
runs, as JSON
|
runs, as JSON
|
||||||
/users the users of the P2S systems: list, add, set the password, disable, enable,
|
/users the users in the router's directory, whether or not a P2S system is enabled: list, add, set the password, disable, enable,
|
||||||
remove, and the OpenVPN client profile (see users())
|
remove, and the OpenVPN client profile (see users())
|
||||||
|
|
||||||
/config and /users need an HS256 JWT in an Authorization: Bearer header that is signed
|
/config and /users need an HS256 JWT in an Authorization: Bearer header that is signed
|
||||||
@@ -52,7 +52,7 @@ STATE_FILE = '/run/vpn-router/web.json'
|
|||||||
CONFIG_FILE = '/etc/vpn-router/vpn-router.json'
|
CONFIG_FILE = '/etc/vpn-router/vpn-router.json'
|
||||||
|
|
||||||
#: The port when web.port is not set.
|
#: The port when web.port is not set.
|
||||||
DEFAULT_PORT = 8080
|
DEFAULT_PORT = 7081
|
||||||
|
|
||||||
#: Seconds between two discoveries of where the service should listen.
|
#: Seconds between two discoveries of where the service should listen.
|
||||||
DISCOVERY_SECONDS = 30
|
DISCOVERY_SECONDS = 30
|
||||||
@@ -224,15 +224,13 @@ def openvpn_clients():
|
|||||||
|
|
||||||
def collect(state):
|
def collect(state):
|
||||||
"""The status document. state says which features the router has."""
|
"""The status document. state says which features the router has."""
|
||||||
units = {'vpn-router-setup': unit_state('vpn-router-setup.service')}
|
units = {'vpn-router-setup': unit_state('vpn-router-setup.service'), 'slapd': unit_state('slapd.service')}
|
||||||
if state.get('site_to_site') or state.get('p2s'):
|
if state.get('site_to_site') or state.get('p2s'):
|
||||||
units['strongswan'] = unit_state('strongswan.service')
|
units['strongswan'] = unit_state('strongswan.service')
|
||||||
if state.get('wireguard'):
|
if state.get('wireguard'):
|
||||||
units['wg-quick@wg0'] = unit_state('wg-quick@wg0.service')
|
units['wg-quick@wg0'] = unit_state('wg-quick@wg0.service')
|
||||||
if state.get('openvpn'):
|
if state.get('openvpn'):
|
||||||
units['openvpn-server@vpn-router'] = unit_state('openvpn-server@vpn-router.service')
|
units['openvpn-server@vpn-router'] = unit_state('openvpn-server@vpn-router.service')
|
||||||
if state.get('users'):
|
|
||||||
units['slapd'] = unit_state('slapd.service')
|
|
||||||
|
|
||||||
status = {
|
status = {
|
||||||
'status': 'ok' if all(value == 'active' for value in units.values()) else 'degraded',
|
'status': 'ok' if all(value == 'active' for value in units.values()) else 'degraded',
|
||||||
@@ -435,8 +433,6 @@ def users(state, method, path, body):
|
|||||||
DELETE /users/<name> remove the user and revoke the certificate
|
DELETE /users/<name> remove the user and revoke the certificate
|
||||||
GET /users/<name>/profile the OpenVPN client profile
|
GET /users/<name>/profile the OpenVPN client profile
|
||||||
"""
|
"""
|
||||||
if not state.get('users'):
|
|
||||||
return 404, {'error': 'no P2S system with users is configured'}
|
|
||||||
if body == 'invalid' or (body is not None and not isinstance(body, dict)):
|
if body == 'invalid' or (body is not None and not isinstance(body, dict)):
|
||||||
return 400, {'error': 'the body must be a JSON object'}
|
return 400, {'error': 'the body must be a JSON object'}
|
||||||
body = body or {}
|
body = body or {}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
"""Manage the users of the router's P2S systems.
|
"""Manage the users of the router's directory, whether or not a P2S system is enabled.
|
||||||
|
|
||||||
A user is an entry in the router's directory, whose password OpenVPN checks, and a client
|
A user is an entry in the router's directory, whose password OpenVPN checks, and a client
|
||||||
certificate of the router CA with the user name as its common name, which OpenVPN and
|
certificate of the router CA with the user name as its common name, which OpenVPN and
|
||||||
@@ -103,7 +103,7 @@ def list_users(connection):
|
|||||||
def add(connection, name):
|
def add(connection, name):
|
||||||
secret = password()
|
secret = password()
|
||||||
if not (vpnrouter.PKI_DIR / 'ca_key.pem').is_file():
|
if not (vpnrouter.PKI_DIR / 'ca_key.pem').is_file():
|
||||||
raise Failure('the router CA does not exist; enable OpenVPN or IKEv2 road-warrior access first')
|
raise Failure('the router CA does not exist; vpn-router-setup creates it once a platform is chosen')
|
||||||
try:
|
try:
|
||||||
connection.add_s(dn_of(name), ldap.modlist.addModlist({
|
connection.add_s(dn_of(name), ldap.modlist.addModlist({
|
||||||
'objectClass': [b'inetOrgPerson'], 'uid': [name.encode()], 'cn': [name.encode()], 'sn': [name.encode()],
|
'objectClass': [b'inetOrgPerson'], 'uid': [name.encode()], 'cn': [name.encode()], 'sn': [name.encode()],
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"port": 8080,
|
"port": 7081,
|
||||||
"jwt_secret": ""
|
"jwt_secret": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File renamed without changes.
Reference in new issue
Block a user