feature: users sign in to the web service, and only members of admins manage the router.
Release / release (push) Failing after 16m57s
Release / release (push) Failing after 16m57s
This commit is contained in:
1 parent
791fc7fddd
commit
caf0e334f0
17 files changed
+792
-177
No files matched your search
@@ -125,7 +125,8 @@ must have the type shown below; an unknown section or setting is an error.
|
||||
| `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` | `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` | `database` | string | | A PostgreSQL connection string for the refresh tokens; without it, the `vpn-router` database on this host that `vpn-router-setup` creates |
|
||||
| `web` | `trusted_proxies` | list | the listening address | Addresses whose `X-Forwarded-For` header names the client for the `/auth` rate limit |
|
||||
|
||||
A minimal site-to-site configuration in `auto` mode:
|
||||
|
||||
@@ -310,7 +311,7 @@ certificate names:
|
||||
- the certificate is checked against the CA and the CRL (`crl-verify`, read at every
|
||||
connection);
|
||||
- the password is checked by `openvpn-auth-ldap`, which binds to the user directory as the
|
||||
user;
|
||||
user; a user enrolled for TOTP types the password followed by the current 6-digit code;
|
||||
- `/usr/lib/vpn-router/openvpn-same-user` refuses a login whose user name is not the common
|
||||
name of the certificate.
|
||||
|
||||
@@ -331,11 +332,14 @@ is switched on:
|
||||
`/etc/vpn-router/ldap/user-manager.secret` (root only);
|
||||
- passwords are stored as ARGON2 hashes, and a disabled user is locked with the password policy
|
||||
(`pwdAccountLockedTime`);
|
||||
- slapd's `otp` module is loaded for the OATH-LDAP schema; a user enrolled for TOTP is also an
|
||||
- the password policy locks an account for 15 minutes after 5 failed binds within 15 minutes,
|
||||
whether they come from OpenVPN or from `/auth`; a successful bind clears the count, and
|
||||
`vpn-router-user unlock` ends a lockout;
|
||||
- slapd's `otp` overlay is configured on the database; a user enrolled for TOTP is also an
|
||||
`oathTOTPUser` and `oathTOTPToken` whose `oathTOTPToken` names the entry itself, with the
|
||||
secret in `oathSecret`, which the user manager account may set and no one can read, and
|
||||
`oathTOTPParams` naming `cn=totp,ou=policies` (6 digits, HMAC-SHA1, 30 second steps, one step
|
||||
of tolerance). The module's overlay is not configured, so binds take the password alone;
|
||||
of tolerance). A bind of an enrolled user takes the password followed by a current code;
|
||||
- slapd listens on its local socket only (`SLAPD_SERVICES` in `/etc/default/slapd`).
|
||||
|
||||
The directory is kept on purge.
|
||||
@@ -361,6 +365,7 @@ vpn-router-user --json list
|
||||
echo 'n3w-pass' | vpn-router-user passwd alice
|
||||
vpn-router-user disable alice
|
||||
vpn-router-user enable alice
|
||||
vpn-router-user unlock alice
|
||||
vpn-router-user remove alice
|
||||
vpn-router-user otp-new alice
|
||||
echo 'JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP' | vpn-router-user otp-enrol alice 123456
|
||||
@@ -369,11 +374,12 @@ vpn-router-user otp-remove alice
|
||||
|
||||
| Command | Effect |
|
||||
| --- | --- |
|
||||
| `list` | The users with their names and e-mail addresses, whether each is enabled, has a certificate and is enrolled for TOTP |
|
||||
| `list` | The users with their names and e-mail addresses, whether each is enabled or locked, has a certificate and is enrolled for TOTP |
|
||||
| `add <name> --first-name F --last-name L --email E` | Adds the user and issues the certificate; all three are required |
|
||||
| `update <name> [--first-name F] [--last-name L] [--email E]` | Changes the names or the e-mail address |
|
||||
| `passwd <name>` | Sets the password |
|
||||
| `disable <name>`, `enable <name>` | Locks the user out of OpenVPN, and lets the user in again |
|
||||
| `disable <name>`, `enable <name>` | Locks the user out of OpenVPN and `/auth`, and lets the user in again |
|
||||
| `unlock <name>` | Ends a lockout after failed logins and clears the failure count; a disabled user stays disabled |
|
||||
| `remove <name>` | Takes the user out of its groups, revokes the certificate, renews the CRL and removes the user; refused while the user is the last member of a group |
|
||||
| `profile <name>` | The OpenVPN client profile, with the CA, the certificate and the key inline |
|
||||
| `otp-new <name>` | A new TOTP secret and its `otpauth://` URI, issued by `wan.local_fqdn`, for an authenticator app; nothing is stored |
|
||||
@@ -383,8 +389,8 @@ vpn-router-user otp-remove alice
|
||||
A user name is 1 to 32 lowercase letters, digits and hyphens, starting with a letter; a
|
||||
password has 8 to 128 characters; a first or last name has 1 to 64 printable characters; an
|
||||
e-mail address has one `@`, no spaces and a dot in its domain; a TOTP secret is 32 base32
|
||||
characters, as `otp-new` makes it, and a code 6 digits. Enrolment does not change how users log
|
||||
in. In the answers a user added
|
||||
characters, as `otp-new` makes it, and a code 6 digits. Once enrolled, a user logs in to OpenVPN
|
||||
and `/auth` with the password followed by a current code. In the answers a user added
|
||||
without the names or the e-mail address has them as `null`. The exit status is 0 on success, 2 for an invalid request,
|
||||
3 when the user does not exist, 4 when the user exists and 1 for any other failure.
|
||||
|
||||
@@ -471,6 +477,7 @@ configuration: no key, identity, address or subnet.
|
||||
| --- | --- |
|
||||
| `/` | 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 |
|
||||
| `/auth` | Signing in with a user's password and TOTP code, refreshing and signing out |
|
||||
| `/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 |
|
||||
| `/groups` | The groups of users; needs a token |
|
||||
@@ -490,43 +497,70 @@ curl http://10.0.0.4:7081/
|
||||
curl http://10.0.0.4:7081/status
|
||||
```
|
||||
|
||||
### Tokens and signing in
|
||||
|
||||
`/config`, `/users` and `/groups` need a JWT in an `Authorization: Bearer` header, signed with
|
||||
HS256 by the key `vpn-router-setup` makes on the router, `/etc/vpn-router/web.key` (root only).
|
||||
The key never leaves the router; while it does not exist these paths and `/auth` are not served
|
||||
(404). A missing, expired or wrongly signed token, or one of another algorithm, gets 401 with
|
||||
`WWW-Authenticate: Bearer`. There are two kinds of tokens:
|
||||
|
||||
- an access token of `/auth` names a user in `sub` and is valid for 5 minutes; the user must
|
||||
be a member of the group `admins`, otherwise the request gets 403;
|
||||
- a token of `vpn-router-token`, run as root on the router, has the claim `admin` and is valid
|
||||
for the lifetime given, without a maximum. It may call everything, and only such a token may
|
||||
change the group `admins`: create it, rename a group to it, change or remove it, add or take
|
||||
out its members, or remove a user who is a member of it.
|
||||
|
||||
The first member of `admins` is added on the router:
|
||||
|
||||
```sh
|
||||
vpn-router-group add admins --member alice --description 'Router administrators'
|
||||
vpn-router-token --hours 1
|
||||
vpn-router-token --json --days 30
|
||||
```
|
||||
|
||||
`POST /auth` signs a user in. Request and answer bodies are JSON:
|
||||
|
||||
| Request | Body | Answer |
|
||||
| --- | --- | --- |
|
||||
| `POST /auth` | `{"name", "password", "code"}`; `code` only for a user enrolled for TOTP | the tokens |
|
||||
| `POST /auth/refresh` | `{"refresh_token"}` | new tokens; the refresh token given is used up |
|
||||
| `POST /auth/logout` | `{"refresh_token"}` | `{"revoked": true}`, or `false` for a token not known |
|
||||
|
||||
The tokens are `{"access_token", "token_type": "Bearer", "expires_in": 300, "refresh_token",
|
||||
"refresh_expires_in": 28800}`. A wrong name, password or code, and a locked or disabled user,
|
||||
get 401 with the same message; a refresh token is refused once its user is disabled, locked or
|
||||
removed. Changing a user's password, disabling, unlocking or removing the user revokes the
|
||||
user's refresh tokens. Five failed logins lock the account for 15 minutes (see the user
|
||||
directory). `/auth` and `/auth/refresh` take at most 10 calls a minute from one client address,
|
||||
and answer 429 with `Retry-After` beyond that; for a call from one of `web.trusted_proxies`, by
|
||||
default the address the service listens on, the client address is the last entry of
|
||||
`X-Forwarded-For`.
|
||||
|
||||
Refresh tokens are kept, as their SHA-256, in PostgreSQL: by default in the database and role
|
||||
`vpn-router` on the router, which `vpn-router-setup` creates with a password in
|
||||
`/etc/vpn-router/database.secret` (root only), reached over TCP on `localhost`; `web.database`
|
||||
names another database. Tokens and passwords travel over plain HTTP on the internal network.
|
||||
|
||||
```bash
|
||||
curl -H 'Content-Type: application/json' -d '{"name": "alice", "password": "s3cr3t-pass", "code": "123456"}' \
|
||||
http://10.0.0.4:7081/auth
|
||||
curl -H "Authorization: Bearer $TOKEN" http://10.0.0.4:7081/config
|
||||
```
|
||||
|
||||
### The /config page
|
||||
|
||||
`/config` shows what `/` and `/status` leave out, for a client such as manage-ui:
|
||||
|
||||
| Key | Content |
|
||||
| --- | --- |
|
||||
| `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) |
|
||||
| `configuration` | `vpn-router.json` with the defaults applied, without its secret options (`psk`, `database`, and any option named like a secret, 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 |
|
||||
| `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 |
|
||||
| `docker` | `docker.exposed_ports` and the rules of the `VPN-ROUTER-DOCKER` chain |
|
||||
|
||||
The page needs a JWT in an `Authorization: Bearer` header, signed with HS256 by `web.jwt_secret`.
|
||||
The token must have an `exp` claim, not more than ten minutes from now (a minute of clock
|
||||
difference is allowed); `nbf` is checked when present. Other algorithms are refused, with 401 and
|
||||
`WWW-Authenticate: Bearer`. While `web.jwt_secret` is not set the page is not served (404). The
|
||||
token and the page travel over plain HTTP on the internal network, and the token can be used
|
||||
until it expires, so keep the lifetime short. A client makes a token from the secret with only
|
||||
HMAC-SHA256; with Python:
|
||||
|
||||
```python
|
||||
import base64, hashlib, hmac, json, time
|
||||
|
||||
def b64(data):
|
||||
return base64.urlsafe_b64encode(data).rstrip(b'=').decode()
|
||||
|
||||
secret = 'the web.jwt_secret value'
|
||||
head = b64(json.dumps({'alg': 'HS256', 'typ': 'JWT'}).encode())
|
||||
body = b64(json.dumps({'exp': int(time.time()) + 300}).encode())
|
||||
sign = b64(hmac.new(secret.encode(), f'{head}.{body}'.encode(), hashlib.sha256).digest())
|
||||
print(f'{head}.{body}.{sign}')
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer $TOKEN" http://10.0.0.4:7081/config
|
||||
```
|
||||
|
||||
### The /users API
|
||||
|
||||
`/users` manages users with the same token as `/config`, with the module `vpn-router-user` uses
|
||||
@@ -535,12 +569,13 @@ and answer bodies are JSON; a request body is at most 4096 bytes.
|
||||
|
||||
| Request | Body | Answer |
|
||||
| --- | --- | --- |
|
||||
| `GET /users` | | `{"users": [{"name", "enabled", "certificate", "otp", "first_name", "last_name", "email"}, ...]}` |
|
||||
| `GET /users` | | `{"users": [{"name", "enabled", "locked", "certificate", "otp", "first_name", "last_name", "email"}, ...]}` |
|
||||
| `POST /users` | `{"name", "password", "first_name", "last_name", "email"}`, all required | 201 and the user |
|
||||
| `PUT /users/<name>` | any of `{"first_name", "last_name", "email"}` | the user |
|
||||
| `PUT /users/<name>/password` | `{"password": ...}` | the user |
|
||||
| `POST /users/<name>/disable` | | the user |
|
||||
| `POST /users/<name>/enable` | | the user |
|
||||
| `POST /users/<name>/unlock` | | the user |
|
||||
| `DELETE /users/<name>` | | `{"name", "removed": true}` |
|
||||
| `GET /users/<name>/profile` | | `{"name", "profile"}`, the OpenVPN client profile |
|
||||
| `POST /users/<name>/otp` | | `{"name", "secret", "uri"}`, a new TOTP secret; nothing is stored |
|
||||
@@ -580,7 +615,7 @@ The service starts on its own, without waiting for `vpn-router-setup`, and keeps
|
||||
30 seconds it works out the internal address from `vpn-router.json` as `general.mode` directs,
|
||||
and reads `web.port` and `web.enabled`; it listens there, moves when the address or the port
|
||||
changes, and stops listening while `web.enabled` is `false` or the address is not known. Each
|
||||
request reads the token secret from `vpn-router.json`, and the features and the facts for
|
||||
request reads the signing key and `vpn-router.json`, and the features and the facts for
|
||||
`/config` from `/run/vpn-router/web.json` (readable by root only), which `vpn-router-setup` writes;
|
||||
before that file exists the page shows no features. `vpn-router-setup` opens `web.port` on the
|
||||
internal interface in the firewall.
|
||||
@@ -603,8 +638,9 @@ may be in use by something else.
|
||||
|
||||
`apt-get purge` additionally removes the generated files and the firewall rules, restoring
|
||||
`/etc/ufw/before.rules` to its original content, and hands `vpn-router.json` back to `ucf`
|
||||
before deleting it so a later reinstall starts clean. Key material and users are never
|
||||
deleted: `/etc/vpn-router/pki`, `/etc/vpn-router/ldap`, the user directory in slapd, the
|
||||
before deleting it so a later reinstall starts clean. It also removes the web service's
|
||||
signing key, and the refresh tokens' database, its role and their password file when the package
|
||||
created them. Key material and users are never deleted: `/etc/vpn-router/pki`, `/etc/vpn-router/ldap`, the user directory in slapd, the
|
||||
WireGuard key pair and everything under `/etc/swanctl` are left alone.
|
||||
|
||||
## Building
|
||||
|
||||
Reference in new issue
Block a user