2489 lines
167 KiB
Markdown
2489 lines
167 KiB
Markdown
# vpn-router
|
|
|
|
A Debian package that configures a Linux host or virtual machine as a VPN router:
|
|
|
|
- site-to-site IKEv2 IPSec with pre-shared key or certificate authentication (strongSwan swanctl)
|
|
- IKEv2 access using EAP-TLS
|
|
- a two-level certificate authority: a root, which may be kept offline, and issuing CAs for the VPN
|
|
clients, for site-to-site and for general use, with certificate templates, published certificates
|
|
and CRLs, and an optional ACME server
|
|
- OpenVPN access with a certificate and a password per user, the users kept in a local
|
|
LDAP directory and managed with `vpn-router-user` or the web service
|
|
- optional WireGuard endpoint
|
|
- routing and NAT for a protected subnet, TCP MSS clamping, and UFW firewall rules
|
|
- firewall rules for the ports Docker publishes, when Docker is installed
|
|
- metrics of the router kept in PostgreSQL, served for an OpenTelemetry Collector and pushed
|
|
over OTLP
|
|
- static web sites served by Caddy, uploaded through the web service or WebDAV, and an optional
|
|
primary name server (PowerDNS) of the router's domains, with a secondary and records for the sites
|
|
|
|
Nothing in the package is specific to a cloud provider. It is built on the external and
|
|
internal NIC names, and a `mode` setting says how much of the rest you are supplying and
|
|
how much is read from the system. Detection happens only in the mode that asks for it, so
|
|
a router never quietly guesses which interface to NAT out of unless you told it to.
|
|
Platform-specific additions live in separate modules and are selected explicitly.
|
|
|
|
Installing the package configures nothing by default. The shipped platform is `none`,
|
|
which means the files are laid down and the machine is left alone until you say
|
|
otherwise.
|
|
|
|
## Installing
|
|
|
|
The package can be installed by hand or as a step in automated provisioning. Both are
|
|
supported equally, and both end up with the same two things: a configuration file and a
|
|
service that applies it.
|
|
|
|
```
|
|
/etc/vpn-router/vpn-router.json the configuration
|
|
systemctl restart vpn-router-setup apply it
|
|
```
|
|
|
|
If the host runs Docker, the recommended order is Docker first and the package second. The
|
|
package secures the ports Docker publishes when it finds the Docker service; the steps for a
|
|
host that already runs Docker, and for installing Docker later, are under [Docker](#docker).
|
|
|
|
### By hand
|
|
|
|
```sh
|
|
apt-get install vpn-router # answer "none" to the platform question
|
|
$EDITOR /etc/vpn-router/vpn-router.json
|
|
systemctl restart vpn-router-setup
|
|
```
|
|
|
|
Answering `none` to the platform question defers everything: no further questions are
|
|
asked and nothing on the machine is configured. The only change an install makes is the
|
|
package's own `/etc/sysctl.d/` drop-in, which enables IP forwarding and relaxes
|
|
`rp_filter` as a router needs. Set `platform` in the file when you are ready.
|
|
|
|
### Automated
|
|
|
|
Preseed the answers and the router comes up configured, with no follow-up command. Note
|
|
that `platform` must be something other than `none`, and `mode` says how much you are
|
|
supplying:
|
|
|
|
```shell
|
|
debconf-set-selections <<'EOF'
|
|
vpn-router vpn-router/platform select generic
|
|
vpn-router vpn-router/mode select manual
|
|
vpn-router vpn-router/external_interface string eth0
|
|
vpn-router vpn-router/internal_interface string eth1
|
|
vpn-router vpn-router/int_addr string 10.1.1.4
|
|
vpn-router vpn-router/int_gateway_ip string 10.1.1.1
|
|
vpn-router vpn-router/local_fqdn string router.example.com
|
|
vpn-router vpn-router/local_cidrs string 10.0.0.0/24
|
|
vpn-router vpn-router/remote_addrs string peer.example.net
|
|
vpn-router vpn-router/remote_id string peer.example.net
|
|
vpn-router vpn-router/remote_cidrs string 192.168.0.0/24
|
|
vpn-router vpn-router/psk password s3cr3t
|
|
EOF
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y vpn-router
|
|
```
|
|
|
|
With mode `interfaces` the `int_addr` and `int_gateway_ip` lines are dropped and read
|
|
from the internal NIC instead; with mode `auto` the interface names go too.
|
|
|
|
Alternatively write `/etc/vpn-router/vpn-router.json` directly before or after installing.
|
|
A configuration-management tool should do that and then restart `vpn-router-setup`.
|
|
|
|
See [examples/](examples/) for a cloud-init template and a shell installer, and
|
|
[examples/azure/](examples/azure/) / [examples/gcp/](examples/gcp/) for full Terraform examples
|
|
that deploy a router on Azure or GCP.
|
|
|
|
## Configuration
|
|
|
|
`/etc/vpn-router/vpn-router.json` is JSON, mode 0600, and is created on first install. It
|
|
is managed with `ucf`, so it is yours to edit: a later `dpkg-reconfigure` applies the answers
|
|
given and asks before touching anything you altered by hand, and an upgrade asks no questions
|
|
and leaves the file as it is. Settings the file leaves out take their defaults when it is read, so a
|
|
file written by cloud-init or by hand needs only the settings that differ. A complete example is
|
|
installed at `/usr/share/doc/vpn-router/vpn-router.json.example`.
|
|
|
|
The file is an object of sections, each an object of settings. A section or setting that
|
|
is left out takes its default, and an empty string or empty list means unset. Every value
|
|
must have the type shown below; an unknown section or setting is an error.
|
|
|
|
| Section | Setting | Type | Default | Meaning |
|
|
|---|---|---|---|---|
|
|
| `general` | `platform` | string | `none` | `none`, `generic`, `azure` or `gcp`. `none` defers configuration entirely |
|
|
| `general` | `mode` | string | `manual` | How much you supply: `manual`, `interfaces` or `auto` |
|
|
| `interfaces` | `external` | string | | Name of the NIC facing the untrusted network |
|
|
| `interfaces` | `internal` | string | | Name of the NIC facing the protected network |
|
|
| `wan` | `local_fqdn` | string | | This router's name or IP address: its IKE identity in `fqdn` mode, the name resolved in `public_ip` mode, the name of HTTPS, IKEv2 and OpenVPN, and the primary name server of its domains |
|
|
| `wan` | `public_address` | string | | The router's public IP address, which its own interfaces do not show behind a cloud's NAT: the address of the records of the sites with `dns_address` `public` |
|
|
| `local` | `cidrs` | list | | Local subnets routed via the internal side, masqueraded to the external one, and offered to IKEv2 and OpenVPN clients |
|
|
| `local` | `int_addr` | string | | This host's address on the internal network |
|
|
| `local` | `int_gateway_ip` | string | | Next hop on the internal side |
|
|
| `site_to_site` | | object | | The site-to-site connections, each a name and an object of the settings below |
|
|
| `p2s` | `enabled` | boolean | `false` | IKEv2 access |
|
|
| `p2s` | `address_pool` | string | | CIDR assigned to IKEv2 clients |
|
|
| `p2s` | `dns_domains` | list | | The domains IKEv2 clients resolve with the router's resolver (INTERNAL_DNS_DOMAIN, RFC 8598, and the match domains of the Apple VPN profile), at most 16 |
|
|
| `p2s` | `dns_listen` | string | `internal` | The addresses of the resolver the IKEv2, OpenVPN and WireGuard clients are given: `internal`, `external` or `both` |
|
|
| `wireguard` | `enabled` | boolean | `false` | WireGuard endpoint |
|
|
| `wireguard` | `address` | string | | The router's own address on `wg0` with the prefix of the peers' network, for example `192.168.200.1/24`; the network and the broadcast address of a network larger than /31 are refused |
|
|
| `wireguard` | `listen_port` | integer | `51820` | UDP port |
|
|
| `openvpn` | `enabled` | boolean | `false` | OpenVPN access for users |
|
|
| `openvpn` | `address_pool` | string | | IPv4 CIDR assigned to OpenVPN clients |
|
|
| `openvpn` | `port` | integer | `1194` | Port of the OpenVPN server on the external interface |
|
|
| `openvpn` | `protocol` | string | `udp` | `udp` or `tcp` |
|
|
| `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 |
|
|
| `web` | `enabled` | boolean | `true` | The status web service |
|
|
| `web` | `listen` | string | `both` | The interfaces the status web service listens on: `internal`, `external` or `both` |
|
|
| `web` | `port` | integer | `7081` | TCP port of the status web service |
|
|
| `web` | `database` | string | | A PostgreSQL connection string for the refresh tokens; without it, the `vpn-router` database on this host that the web service creates |
|
|
| `web` | `trusted_proxies` | list | the address the call came in on | Addresses whose `X-Forwarded-For` header names the client for the `/auth` rate limit and the Client IP of the status |
|
|
| `web` | `status_content` | string | `basic` | What `/` and `/status` show: `basic`, `standard` or `full` (see Status web service) |
|
|
| `web` | `shell` | boolean | `false` | `/shell` opens a login shell for a member of `admins` with console login (see The shell) |
|
|
| `web` | `shell_idle_minutes` | integer | `15` | Minutes without a keystroke after which a shell is closed, 1 to 1440 |
|
|
| `web` | `log_files` | list | | Absolute paths of the log files `/logs` shows, at most 32 (see The /logs API) |
|
|
| `web` | `shell_auth_timeout` | integer | `15` | Minutes after the last use of a shell in which a new shell opens without signing in again, 0 (always sign in) to 1440 |
|
|
| `ssh` | `ldap_login` | boolean | `false` | sshd accepts the directory password of the console accounts in the directory (see Console login and SSH) |
|
|
| `https` | `enabled` | boolean | `true` | Caddy serves the status web service and the Router Manager over HTTPS for `wan.local_fqdn` |
|
|
| `https` | `email` | string | | The e-mail address of the Let's Encrypt account |
|
|
| `https` | `web_port` | integer | `7444` | HTTPS port of the status web service |
|
|
| `https` | `manager_port` | integer | `7443` | HTTPS port of the Router Manager |
|
|
| `https` | `acme_staging` | boolean | `false` | Caddy takes its certificates from Let's Encrypt's staging environment alone, whose certificates no browser trusts and whose rate limits are higher, for routers rebuilt often (see HTTPS) |
|
|
| `https` | `caddy_source` | string | `ubuntu` | Where Caddy is installed from: `ubuntu`, the Ubuntu archive, or `upstream`, Caddy's own repository (see HTTPS) |
|
|
| `oidc` | `enabled` | boolean | `false` | The OpenID Provider at the status web service's HTTPS site, which needs `web.enabled`, `https.enabled` and `wan.local_fqdn` (see OpenID Provider) |
|
|
| `sites` | `enabled` | boolean | `false` | Caddy serves the web sites (see Web sites) |
|
|
| `sites` | `upload_max_mb` | integer | `512` | The largest upload of a site's file or zip archive, in MiB, 1 to 10240 |
|
|
| `sites` | `webdav` | boolean | `false` | WebDAV of the sites' folders, which needs `https.enabled` and `wan.local_fqdn` (see WebDAV) |
|
|
| `sites` | `webdav_port` | integer | `7445` | HTTPS port of WebDAV |
|
|
| `dns` | `enabled` | boolean | `false` | The primary name server of the router's domains, `wan.local_fqdn`, which must then be a DNS name (see Name server) |
|
|
| `dns` | `secondary` | string | | The secondary name server of every domain, a host name other than `wan.local_fqdn`; it may transfer the domains |
|
|
| `dns` | `listen` | string | `external` | The interfaces the name server answers on: `internal`, `external` or `both` |
|
|
| `dns` | `ttl` | integer | `300` | TTL of the records, 60 to 86400 seconds |
|
|
| `pki` | `root_name` | string | `VPN Router Root CA` | Common name of the root CA, when the router creates it |
|
|
| `pki` | `root_days` | integer | `7300` | Days the root CA's certificate is valid |
|
|
| `pki` | `ca_days` | integer | `3650` | Days an issuing CA's certificate is valid |
|
|
| `pki` | `key` | string | `rsa-4096` | Key of the root and the issuing CAs: `rsa-2048`, `rsa-3072`, `rsa-4096`, `ec-p256` or `ec-p384` |
|
|
| `pki` | `crl_days` | integer | `7` | Days the CRLs of the issuing CAs are valid |
|
|
| `pki` | `root_crl_days` | integer | `180` | Days the root CRL is valid, when the router signs it |
|
|
| `pki` | `publish` | list | `http` | Where the CA certificates and CRLs are published: `http`, `http-80`, `https`, `https-default` (see Certificates) |
|
|
| `pki` | `p2s_name` | string | `VPN Router P2S CA` | Common name of the P2S issuing CA |
|
|
| `pki` | `s2s_name` | string | `VPN Router Site-to-Site CA` | Common name of the site-to-site issuing CA |
|
|
| `pki` | `issuing_cas` | object | | The general-purpose issuing CAs, each a name and `{name, templates, acme}` (see Certificates) |
|
|
| `pki` | `templates` | object | | Certificate templates, each a name and the options it changes or defines (see Certificates) |
|
|
| `pki` | `extended_key_usages` | object | | Custom extended key usages templates may give, each a name and its OID in dotted form (see Certificates) |
|
|
| `pki` | `root_key_warning` | boolean | `true` | `vpn-router-ca status` and `/ca` warn while the root key is on the router; `false` silences that warning |
|
|
| `metrics` | `enabled` | boolean | `true` | The metrics service: collection, history and `/metrics` |
|
|
| `metrics` | `listen` | string | `internal` | The interfaces `/metrics` is served on: `internal`, `external` or `both` |
|
|
| `metrics` | `port` | integer | `9464` | TCP port of `/metrics` |
|
|
| `metrics` | `https_port` | integer | `9465` | HTTPS port of `/metrics`, served by Caddy while `https.enabled` is true |
|
|
| `metrics` | `service_account` | string | `metrics` | The service account `/metrics` takes, which `vpn-router-setup` adds and keeps |
|
|
| `metrics` | `otlp_endpoint` | string | | An OTLP/HTTP URL the metrics are pushed to every minute, for example `https://collector.example.com:4318/v1/metrics` |
|
|
| `metrics` | `otlp_username` | string | | The user name of HTTP Basic authentication at the OTLP endpoint |
|
|
| `metrics` | `otlp_password` | string | | Its password |
|
|
| `metrics` | `keep_details` | string | `24 hours` | How long the per-minute metrics are kept |
|
|
| `metrics` | `keep_hourly` | string | `1 month` | How long the hourly summaries are kept |
|
|
| `metrics` | `keep_daily` | string | `3 months` | How long the daily summaries are kept |
|
|
| `metrics` | `keep_monthly` | string | `1 year` | How long the monthly summaries are kept |
|
|
| `metrics` | `keep_yearly` | string | `10 years` | How long the yearly summaries are kept |
|
|
| `backup` | `enabled` | boolean | `false` | Scheduled backups (see Backups) |
|
|
| `backup` | `schedule` | string | `daily` | `hourly`, `daily` or `weekly` |
|
|
| `backup` | `target` | string | `local` | `local`, an archive in `backup.path`, or `pbs`, a Proxmox Backup Server |
|
|
| `backup` | `path` | string | `/var/backups/vpn-router` | The folder of local archives, an absolute path |
|
|
| `backup` | `keep_hourly` | integer | `24` | Hourly local archives kept, with the hourly schedule |
|
|
| `backup` | `keep_daily` | integer | `7` | Daily local archives kept, with the hourly and the daily schedule |
|
|
| `backup` | `keep_weekly` | integer | `4` | Weekly local archives kept |
|
|
| `backup` | `keep_monthly` | integer | `12` | Monthly local archives kept |
|
|
| `backup` | `keep_yearly` | integer | `0` | Yearly local archives kept |
|
|
| `backup` | `metrics` | boolean | `false` | The backup holds the database of the metrics and the refresh tokens |
|
|
| `backup` | `rclone_remote` | string | | `remote:path` of `/etc/vpn-router/rclone.conf` the local archives are copied to |
|
|
| `backup` | `rclone_mode` | string | `sync` | `sync` mirrors the archives, `copy` deletes nothing in the cloud |
|
|
| `backup` | `pbs_repository` | string | | `user@realm!token@host[:port]:datastore` |
|
|
| `backup` | `pbs_password` | string | | The secret of the API token |
|
|
| `backup` | `pbs_fingerprint` | string | | The SHA-256 fingerprint of the server's certificate, when the system's CAs do not verify it |
|
|
| `backup` | `pbs_namespace` | string | | The namespace of the datastore, such as `routers/azure` |
|
|
| `backup` | `pbs_keyfile` | string | | An encryption key of `proxmox-backup-client key create`, an absolute path |
|
|
|
|
### Site-to-site connections
|
|
|
|
`site_to_site` maps a connection name to its settings. A name starts with a lowercase letter
|
|
and has at most 31 lowercase letters, digits and hyphens; it names the strongSwan connection,
|
|
its child and its secret (`ike-psk-<name>`). The install answers fill the connection
|
|
`remote-site`; more are added in the file, with `vpn-config-edit`, or through `/site-to-site`
|
|
of the status web service, which the Router Manager uses.
|
|
|
|
| Setting | Type | Default | Meaning |
|
|
|---|---|---|---|
|
|
| `addrs` | list | | Remote gateway addresses or FQDNs |
|
|
| `id` | string | | Remote IKE identity, an FQDN or an IPv4 or IPv6 address |
|
|
| `cidrs` | list | | Remote subnets reachable through the tunnel |
|
|
| `local_cidrs` | list | | Local subnets advertised into the tunnel |
|
|
| `local_id_mode` | string | `fqdn` | The router's IKE identity: `fqdn` is `wan.local_fqdn`, `public_ip` the address it resolves to, `internal_ip` the internal address |
|
|
| `auth` | string | `psk` | How both sides authenticate: `psk`, a pre-shared key, or `certificate` |
|
|
| `psk` | string | | Pre-shared key, with `auth` `psk` |
|
|
| `psk_file` | string | | Path to a file holding the key; wins over `psk` |
|
|
| `remote_ca` | string | | With `auth` `certificate`, the PEM certificate of the CA that issued the peer's certificate; without it the router's site-to-site CA |
|
|
|
|
A connection is set up once all of `addrs`, `id`, `cidrs`, `local_cidrs` and its local
|
|
identity are known, with `psk` the key, and with `certificate` the router's site-to-site
|
|
certificate; all such connections are written to `/etc/swanctl/conf.d/site-to-site.conf`.
|
|
Traffic from a connection's `local_cidrs` to its `cidrs` is not masqueraded.
|
|
|
|
With `auth` `certificate`, the router authenticates with its site-to-site certificate, which the
|
|
site-to-site issuing CA signs for `wan.local_fqdn` and for the public or the internal address
|
|
that a connection with `auth` `certificate` uses as its identity (`local_id_mode` `public_ip` or
|
|
`internal_ip`); it is issued again when that set of names changes. The peer's certificate must name `id` and be issued
|
|
by the router's site-to-site CA, or by the CA of `remote_ca`. A peer with no PKI of its own gets
|
|
its certificate from the router (template `site-to-site`, see Certificates), with the root and
|
|
the site-to-site CA certificates as its trust anchors.
|
|
|
|
A minimal site-to-site configuration in `auto` mode:
|
|
|
|
```json
|
|
{
|
|
"general": { "platform": "generic", "mode": "auto" },
|
|
"wan": { "local_fqdn": "router.example.com" },
|
|
"local": { "cidrs": ["10.0.0.0/24"] },
|
|
"site_to_site": {
|
|
"remote-site": {
|
|
"addrs": ["peer.example.net"],
|
|
"id": "peer.example.net",
|
|
"cidrs": ["192.168.0.0/24"],
|
|
"local_cidrs": ["10.0.0.0/24"],
|
|
"psk": "s3cr3t"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
The router's IKE identity (`wan.local_fqdn` in `fqdn` mode) and the remote one (`id`) are
|
|
an FQDN or an IPv4 or IPv6 address. They are written to the strongSwan configuration as given;
|
|
strongSwan reads an address as an IP identity and a name as an FQDN, so a leading `@` is not needed
|
|
and is refused. IKEv2 and OpenVPN access need a name in `wan.local_fqdn`.
|
|
|
|
WireGuard peers are VPN users' devices: a VPN user of the router's directory may have up to 16
|
|
peers, each with its public key and its IPs, IPv4 addresses or networks, kept in the user's
|
|
entry (`vpnRouterWireGuardPeer`) and set with `PUT /users/<name>` (`wireguard_peers`) or on the
|
|
WireGuard tab of the Router Manager's Remote access management. A key is used by one peer only,
|
|
the IPs of two peers may not overlap, and no peer's IPs cover the router's own address of
|
|
`wireguard.address`. The package owns the `[Interface]` section of `/etc/wireguard/wg0.conf` and,
|
|
between the lines `# BEGIN` and `# END WireGuard peers of the users of the router's directory`, a
|
|
`[Peer]` section for each peer of a user who is not disabled; setup rewrites them when the
|
|
settings or the peers change, and a change of a user's peers, disabling, enabling or removing a
|
|
user with peers restarts setup. Every other `[Peer]` section, added by hand, is carried across
|
|
untouched. While the directory cannot be reached, the users' peers in the file stay as they are.
|
|
|
|
Every feature is independent. Leaving a group empty simply means that feature is not
|
|
configured, and its generated files are removed.
|
|
|
|
### What `mode` decides
|
|
|
|
`mode` states how much of the network configuration you are supplying, and therefore how
|
|
much the package reads from the system:
|
|
|
|
| `mode` | You provide | The package works out |
|
|
|---|---|---|
|
|
| `manual` | both interface names, `int_addr`, `int_gateway_ip` | nothing |
|
|
| `interfaces` | both interface names | `int_addr` and `int_gateway_ip`, from those interfaces |
|
|
| `auto` | nothing | the interface names too: the external one from the routing table, the internal one as the only other NIC with an address |
|
|
|
|
`manual` is the only mode in which no detection code runs at all. `auto` is the
|
|
convenient one and can pick the wrong interface, which is a trade you make deliberately
|
|
by choosing it rather than something the package does behind your back.
|
|
|
|
Detection ignores virtual interfaces, those the kernel lists under `/sys/devices/virtual`:
|
|
loopback, bridges such as `docker0`, veth pairs and tunnels. Only the NICs of the machine
|
|
count, so Docker does not change the result. If the host has more than two NICs with an
|
|
address, `auto` stops and says so; use mode `interfaces` and name them.
|
|
|
|
The addresses strongSwan binds on always come from the external NIC, in every mode, so
|
|
they are never configured twice and cannot drift out of step with the host. If a named
|
|
interface does not exist the service stops and says so. In `interfaces` and `auto` mode
|
|
the internal gateway is taken from the routing table, falling back to the first host
|
|
address of the connected subnet with a warning when the routing table has nothing to say.
|
|
|
|
## Docker
|
|
|
|
Docker publishes container ports in its own firewall rules, which bypass UFW and can expose
|
|
ports on the external interface. Docker evaluates the `DOCKER-USER` chain before its own
|
|
rules, so the package filters there. With `docker.mode` set to `auto`, the package secures
|
|
Docker when `docker.service` is installed, and does nothing otherwise.
|
|
|
|
### On a host that already runs Docker
|
|
|
|
Installing Docker first is the recommended order. After installing the package:
|
|
|
|
1. Set `general.platform` in `/etc/vpn-router/vpn-router.json`. While it is `none`, nothing is
|
|
configured, the Docker rules included.
|
|
2. List the TCP ports the running containers must expose to the internet, for example
|
|
`"docker": { "exposed_ports": ["443"] }`. The default is none: once the rules are applied,
|
|
every published port that is not listed is unreachable from the external interface, and
|
|
remains reachable through the IPsec tunnel.
|
|
3. Apply the configuration with `systemctl restart vpn-router-setup`. Docker is running, so
|
|
this also restarts `vpn-router-docker`, which installs the rules.
|
|
4. Check the result:
|
|
|
|
```sh
|
|
iptables -S DOCKER-USER # "-j VPN-ROUTER-DOCKER" is the first rule
|
|
iptables -S VPN-ROUTER-DOCKER # the rules of the external interface
|
|
```
|
|
|
|
### When Docker is installed after the package
|
|
|
|
The Docker rules are not applied when Docker starts for the first time, so run
|
|
`systemctl restart vpn-router-setup` after installing Docker, then check as above. A reboot or a
|
|
restart of Docker applies them as well.
|
|
|
|
The rules sit in their own chain, `VPN-ROUTER-DOCKER`, entered by one jump at the top of
|
|
`DOCKER-USER`; other rules in `DOCKER-USER` are left alone. For traffic that arrives on the
|
|
external interface, in this order:
|
|
|
|
1. IPsec protected traffic is accepted.
|
|
2. TCP to the ports in `docker.exposed_ports` is accepted; none by default.
|
|
3. Packets of established and related connections are accepted.
|
|
4. Everything else is dropped.
|
|
|
|
Traffic from every other interface, including the internal one and the Docker bridges, is
|
|
not affected. The rules cover IPv4 only.
|
|
|
|
`vpn-router-setup` starts after `docker.service`, so Docker is up when it looks for it; the
|
|
ordering has no effect on a host without Docker. The `vpn-router-docker` service applies the
|
|
rules after `docker.service` starts and removes
|
|
them when Docker stops, so restarting Docker restores them. `systemctl restart
|
|
vpn-router-setup` applies a changed configuration while Docker runs. With `docker.mode` set to
|
|
`on`, the service fails when Docker is not installed or its daemon is not running; with
|
|
`off` it removes the rules.
|
|
|
|
## Editing the configuration
|
|
|
|
`vpn-config-edit`, run as root, shows and changes `/etc/vpn-router/vpn-router.json`:
|
|
|
|
```sh
|
|
vpn-config-edit show
|
|
vpn-config-edit set web.status_content standard
|
|
vpn-config-edit set web.port 7082
|
|
vpn-config-edit set web.trusted_proxies 127.0.0.1,10.0.0.4
|
|
vpn-config-edit set site_to_site.branch.addrs branch.example.net
|
|
vpn-config-edit remove site_to_site.branch
|
|
vpn-config-edit set --restart oidc.enabled true
|
|
```
|
|
|
|
`show` prints the file as indented JSON and does not change it. `set` takes a setting as
|
|
`section.option`, or as `site_to_site.NAME.option` for a site-to-site connection, which it
|
|
creates with the defaults when it does not exist, and converts the value to the setting's type:
|
|
a whole number, `true` or `false`, a list separated by commas (an empty value is an empty list),
|
|
or a string; an unknown setting is refused. `remove site_to_site.NAME` removes a connection. It
|
|
checks the whole configuration as `vpn-router-setup` does, and only then writes the file,
|
|
indented, with mode 0600, and reminds that `vpn-router-setup` applies it; with `--restart`, `set`
|
|
and `remove` restart `vpn-router-setup` and wait until it has applied the change, and exit with
|
|
status 1 when it fails. The status web service
|
|
writes the file the same way, and a lock keeps the two from overwriting each other's change. A
|
|
refused request exits with status 2, any other failure with 1.
|
|
|
|
## Bash completion
|
|
|
|
Bash completes the commands, options and arguments of `vpn-router-user`, `vpn-router-group`,
|
|
`vpn-router-role`, `vpn-router-token`, `vpn-config-edit`, `vpn-router-backup`, `vpn-router-ca`, `vpn-router-caddy`
|
|
and `vpn-router-site` with the Tab key. The package installs a file per command in
|
|
`/usr/share/bash-completion/completions`, which the `bash-completion` package, recommended by
|
|
`vpn-router`, loads on the first Tab after the command. For root, who can read them, the
|
|
completions also offer the users and groups of the directory, the settings and site-to-site
|
|
connections of `vpn-router.json` with `true` and `false` for a boolean setting, the issuing CAs and
|
|
certificate templates, and the web sites; for other users only the fixed words.
|
|
|
|
```sh
|
|
sudo -i
|
|
vpn-config-edit set oidc.<Tab>
|
|
vpn-router-user passwd <Tab>
|
|
```
|
|
|
|
## Applying changes
|
|
|
|
```sh
|
|
systemctl restart vpn-router-setup
|
|
```
|
|
|
|
The service also runs at boot. With platform `none` it does nothing and says so.
|
|
Otherwise it regenerates the configuration files, resolves the interfaces and their
|
|
addresses as `mode` directs, applies routes, generates WireGuard keys and injects
|
|
firewall rules. It provisions the user directory in `slapd` while OpenVPN or IKEv2
|
|
access is configured. It then starts `strongswan`, `wg-quick@wg0` and
|
|
`openvpn-server@vpn-router` if the corresponding feature is configured, reloading or
|
|
restarting them only when their inputs changed, and
|
|
restarts the remote access clients' resolver only when its configuration changed. Turning a
|
|
feature off in the configuration stops its service and removes its files. Every step is
|
|
idempotent, so restarting with no changes does nothing.
|
|
|
|
To regenerate the configuration files and PKI without starting, stopping or reloading
|
|
anything:
|
|
|
|
```sh
|
|
/usr/lib/vpn-router/configure
|
|
```
|
|
|
|
This does still write to the system: it renders into `/etc/swanctl/`,
|
|
`/etc/systemd/resolved.conf.d/` and `/etc/wireguard/`, creates what the PKI is missing and
|
|
adds the root CA to the host trust store. What it leaves alone is routes, firewall rules and
|
|
services - that is what `systemctl restart vpn-router-setup` adds.
|
|
|
|
## Certificates
|
|
|
|
The router has a two-level certificate authority, kept by `vpn-router-setup` and managed with
|
|
`vpn-router-ca`, `vpn-router-user` and `/ca` of the status web service, which the Router Manager
|
|
uses. It is held in `ou=pki,dc=vpn-router,dc=local` of the router's directory, next to the users:
|
|
|
|
```
|
|
cn=root the root CA; without vpnRouterPrivateKey while the root is offline
|
|
cn=<name> an issuing CA; with vpnRouterCertificateRequest while it waits for the offline root
|
|
cn=<name>-<time> an issuing CA taken out of use, with vpnRouterRetired
|
|
serialNumber=<serial>,cn=<CA> a certificate the CA issued
|
|
```
|
|
|
|
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 root signs the issuing CAs only (path length 1), and the issuing CAs sign the certificates
|
|
(path length 0):
|
|
|
|
- `p2s`, named `pki.p2s_name`, signs the router's VPN server certificate, which IKEv2 and OpenVPN
|
|
present, and the users' certificates. IKEv2 and OpenVPN accept client certificates of
|
|
this CA only.
|
|
- `s2s`, named `pki.s2s_name`, signs the router's site-to-site certificate and the certificates
|
|
of its peers.
|
|
- The general-purpose CAs of `pki.issuing_cas`, each a name and `{"name": common name,
|
|
"templates": [...], "acme": false}`. The name starts with a lowercase letter, has at most 31
|
|
lowercase letters, digits and hyphens, and is not `root`, `p2s` or `s2s`; `templates` are the
|
|
templates the CA issues.
|
|
|
|
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
|
|
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 entry is renamed `cn=<name>-<time>`,
|
|
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
|
|
user's certificate is renewed by removing and adding the user.
|
|
|
|
The package copies what each consumer needs: the root, `p2s` and `s2s` into
|
|
`/etc/swanctl/x509ca/` as `root.pem`, `p2s.pem` and `s2s.pem` (strongSwan reads one certificate
|
|
per file), the remote CA of each site-to-site connection that has one as `s2s-<name>.pem`, the
|
|
router's certificates into `/etc/swanctl/x509/` and their keys into `/etc/swanctl/private/`, the
|
|
CRLs into `/etc/swanctl/x509crl/`, the root and `p2s` with their CRLs into
|
|
`/etc/openvpn/server/vpn-router-ca.pem` and `vpn-router-crl.pem`, and the root into the system
|
|
trust store.
|
|
|
|
`vpn-router-ca status` shows the root, the issuing CAs and what needs attention:
|
|
|
|
```sh
|
|
vpn-router-ca status
|
|
vpn-router-ca list --ca p2s
|
|
```
|
|
|
|
### Templates
|
|
|
|
A template says what a certificate holds:
|
|
|
|
| Option | Type | Default | Meaning |
|
|
|---|---|---|---|
|
|
| `key` | string | `rsa-3072` | `rsa-2048`, `rsa-3072`, `rsa-4096`, `ec-p256` or `ec-p384`, for a key the router makes |
|
|
| `days` | integer | `365` | Days the certificate is valid, at most until its CA's certificate expires |
|
|
| `key_usage` | list | `digital_signature` | Of `digital_signature`, `content_commitment`, `key_encipherment`, `data_encipherment`, `key_agreement` |
|
|
| `extended_key_usage` | list | | Of the built-in `server_auth`, `client_auth`, `code_signing`, `email_protection`, `time_stamping`, `ocsp_signing`, `ipsec_ike`, `ike_intermediate`, and the custom usages of `pki.extended_key_usages` |
|
|
| `policies` | list | | Certificate policy OIDs in dotted form, written into a Certificate Policies extension |
|
|
| `subject` | string | `name` | The common name: `dns`, a DNS name, which is also a subject alternative name; `name`, any name; `email`, an e-mail address |
|
|
| `sans` | list | | The subject alternative names allowed: of `dns`, `ip`, `email` |
|
|
|
|
The built-in templates:
|
|
|
|
| Template | Key usage | Extended key usage | Subject | Names | Issued by |
|
|
|---|---|---|---|---|---|
|
|
| `vpn-server` | digital signature, key encipherment | server auth, IKE intermediate | `dns` | `dns`, `ip` | `p2s`, for the router only |
|
|
| `vpn-user` | digital signature | client auth | `name` | `dns`, `email` | `p2s`, for the users only |
|
|
| `site-to-site` | digital signature, key encipherment | server auth, client auth | `dns` | `dns`, `ip` | `s2s` |
|
|
| `server` | digital signature, key encipherment | server auth | `dns` | `dns`, `ip` | general-purpose CAs |
|
|
| `client` | digital signature | client auth | `name` | `dns`, `email` | general-purpose CAs |
|
|
|
|
`pki.templates` changes options of the built-in templates, which cannot be removed, and adds
|
|
templates of its own, named like the issuing CAs:
|
|
|
|
```json
|
|
"pki": {
|
|
"issuing_cas": { "servers": { "name": "Example Server CA", "templates": ["server", "web", "smartcard"] } },
|
|
"extended_key_usages": { "smartcard_logon": "1.3.6.1.4.1.311.20.2.2" },
|
|
"templates": {
|
|
"server": { "days": 397 },
|
|
"web": { "key": "ec-p256", "days": 90, "key_usage": ["digital_signature"],
|
|
"extended_key_usage": ["server_auth"], "subject": "dns", "sans": ["dns"] },
|
|
"smartcard": { "key_usage": ["digital_signature", "key_encipherment"],
|
|
"extended_key_usage": ["client_auth", "smartcard_logon"], "subject": "name",
|
|
"sans": ["email"], "policies": ["1.3.6.1.4.1.99999.1.1"] }
|
|
}
|
|
}
|
|
```
|
|
|
|
A custom extended key usage is first added to `pki.extended_key_usages`, a name of at most 31
|
|
lowercase letters, digits, hyphens and underscores that is not a built-in one, and its OID; a
|
|
template then gives it by its name. A usage a template gives cannot be removed. `policies` puts
|
|
the OIDs of the issuance policies a certificate is issued under into its Certificate Policies
|
|
extension, without qualifiers.
|
|
|
|
### Issuing and revoking
|
|
|
|
```sh
|
|
vpn-router-ca issue --ca servers --template server --csr web.csr > web.pem
|
|
vpn-router-ca issue --ca servers --template server --subject web.example.com --san 10.0.0.5 --pfx web.pfx
|
|
vpn-router-ca revoke 5F0A12C3D4 --reason key_compromise
|
|
```
|
|
|
|
With a CSR, the certificate is for the CSR's key, common name and subject alternative names,
|
|
checked against the template, and the certificate and the chain (issuing CA, root) are printed.
|
|
Without one, the router makes a key as the template says and writes a PKCS#12 file with the key,
|
|
the certificate and the chain, protected by the password read from the first line of standard
|
|
input; the router keeps the certificate and not the key. `vpn-server` and `vpn-user` are issued by
|
|
the router only.
|
|
|
|
`revoke` takes the serial number `list` shows and a reason: `unspecified`, `key_compromise`,
|
|
`affiliation_changed`, `superseded`, `cessation_of_operation` or `privilege_withdrawn`. A user's
|
|
certificate is revoked by removing the user, and an issuing CA by removing it from
|
|
`pki.issuing_cas`; a revoked router certificate is issued again by `vpn-router-setup`.
|
|
|
|
### Published certificates and CRLs
|
|
|
|
Every certificate names its issuer's certificate (AIA) and CRL (CRL distribution point) at each
|
|
endpoint of `pki.publish`, in that order. They are served, without authentication, as DER at
|
|
`/pki/<name>.crt` and `/pki/<name>.crl`, the root as `root`, from copies in
|
|
`/var/lib/vpn-router/pki-publish`:
|
|
|
|
| Endpoint | URL | Served by |
|
|
|---|---|---|
|
|
| `http` | `http://<wan.local_fqdn>:<web.port>/pki/` | the status web service; needs `web.enabled` |
|
|
| `http-80` | `http://<wan.local_fqdn>/pki/` | Caddy, on port 80, open on both interfaces; needs `https.enabled` |
|
|
| `https` | `https://<wan.local_fqdn>:<https.web_port>/pki/` | Caddy; needs `https.enabled` |
|
|
| `https-default` | `https://<wan.local_fqdn>/pki/` | Caddy, on port 443, open on both interfaces; needs `https.enabled` |
|
|
|
|
The URLs are written into a certificate when it is signed, so a change of `pki.publish` or of the
|
|
names applies to the certificates signed afterwards. An empty `pki.publish` leaves the extensions
|
|
out.
|
|
|
|
The CRLs of the issuing CAs are valid for `pki.crl_days`, and the root CRL, when the router signs
|
|
it, for `pki.root_crl_days`. A CRL is signed again at every revocation and, once half of its
|
|
validity has passed, by `vpn-router-pki.timer`, which runs `vpn-router-ca maintain` daily; it also
|
|
has `vpn-router-setup` renew the router's certificates when they are due. OpenVPN checks the CRL
|
|
of every CA of a client's chain, the root's included, and refuses every client while one has
|
|
expired.
|
|
|
|
### Offline root
|
|
|
|
Keeping the root key off the router is recommended, not required; while it is on the router,
|
|
`vpn-router-ca status` says so, unless `pki.root_key_warning` is `false`. Export it, encrypted with a
|
|
passphrase read from the first line of standard input, keep the file in a safe place, and remove
|
|
the key from the router:
|
|
|
|
```sh
|
|
vpn-router-ca root export-key --out /root/vpn-router-root-key.pem
|
|
vpn-router-ca root remove-key
|
|
```
|
|
|
|
While the root key is not on the router, a new issuing CA gets its key and a CSR and waits;
|
|
`vpn-router-ca status` says so. Print the CSR and the extensions to sign it with, sign it with the
|
|
root on the offline machine, and import the certificate:
|
|
|
|
```sh
|
|
vpn-router-ca ca csr servers # the CSR, then the [ issuing_ca ] extensions
|
|
# on the offline machine, with the CSR in servers.csr and the extensions in issuing_ca.cnf:
|
|
openssl x509 -req -in servers.csr -CA root-cert.pem -CAkey vpn-router-root-key.pem -CAcreateserial \
|
|
-days 3650 -sha256 -extfile issuing_ca.cnf -extensions issuing_ca -out servers.pem
|
|
vpn-router-ca ca import servers servers.pem
|
|
```
|
|
|
|
The router cannot sign the root CRL without the key. Sign it where the key is, or put the key back
|
|
for a moment, and take the CRL before the current one expires:
|
|
|
|
```sh
|
|
vpn-router-ca root import-key /root/vpn-router-root-key.pem # the passphrase on standard input
|
|
vpn-router-ca maintain
|
|
vpn-router-ca root remove-key
|
|
# or, with a CRL signed elsewhere:
|
|
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
|
|
use it, as `/etc/vpn-router/pki/root/cert.pem` and `key.pem` before `vpn-router-setup` first runs
|
|
with a platform, as cloud-init can. Setup imports them into the directory and removes the files.
|
|
Without the key, the issuing CAs wait for their certificates as above.
|
|
|
|
### ACME server
|
|
|
|
A general-purpose CA with `"acme": true` backs an ACME server of Caddy, while `https.enabled`, at
|
|
`https://<wan.local_fqdn>:<https.web_port>/acme/<name>/directory`, so servers get and renew
|
|
certificates with any ACME client:
|
|
|
|
```sh
|
|
certbot certonly --standalone -d web.example.com \
|
|
--server https://router.example.com:7444/acme/servers/directory
|
|
```
|
|
|
|
Caddy proves the client's control of the name as Let's Encrypt does, so it must reach the client.
|
|
The certificates are valid for 12 hours, carry Caddy's extensions, are not listed by
|
|
`vpn-router-ca list`, cannot be revoked, and no template applies to them. Caddy reads the CA's
|
|
certificate and key from `/etc/caddy/vpn-router-pki/<name>/`, readable by its group.
|
|
|
|
## OpenVPN and users
|
|
|
|
With `openvpn.enabled`, an `address_pool` and `wan.local_fqdn` set, `vpn-router-setup` runs
|
|
`openvpn-server@vpn-router` with `/etc/openvpn/server/vpn-router.conf`. The server uses the
|
|
router's VPN server certificate, the root and the P2S CA and their CRLs (see Certificates),
|
|
pushes routes to `local.cidrs` and the router's addresses of `p2s.dns_listen` as DNS servers, and
|
|
opens `openvpn.port` on the external interface.
|
|
|
|
A client needs both a certificate of the P2S CA and the password of the user the
|
|
certificate names:
|
|
|
|
- the certificate is checked against the root and the P2S CA and their CRLs (`crl-verify`, read
|
|
again when the file changes), and `/usr/lib/vpn-router/openvpn-issuer` (`tls-verify`) refuses
|
|
a certificate another issuing CA signed;
|
|
- the password is checked by `openvpn-auth-ldap`, which binds to the user directory as the
|
|
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.
|
|
|
|
### The user directory
|
|
|
|
Once a platform is chosen, `vpn-router-setup` keeps a directory of users in the local slapd,
|
|
whether or not OpenVPN or IKEv2 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
|
|
slapd package creates; root manages it through the local socket, with no administrator
|
|
password;
|
|
- `ou=users` holds one entry per user, named by `uid`: an `inetOrgPerson` for a VPN user, an
|
|
`account` for a Manager-only account (see Managing users);
|
|
- the package's own schema, `cn=vpn-router,cn=schema,cn=config`, under the OID
|
|
`2.25.98477352595735834532404256531620013001` of the UUID arc: the auxiliary class
|
|
`vpnRouterAccount`, which Manager-only and service accounts have, and the attribute
|
|
`vpnRouterScope`, authorization scopes, of which `service` marks a service account;
|
|
`displayName` and `description` are the display name and the description of a service
|
|
account, and `vpnRouterRedirectUri`, `vpnRouterPostLogoutRedirectUri` and
|
|
`vpnRouterAllowedGroup` the settings of a service account that is a client of the OpenID
|
|
Provider;
|
|
- `ou=groups` holds one `groupOfNames` per group, named by `cn`, with its members' DNs in
|
|
`member` and an optional `description`;
|
|
- `cn=user-manager,ou=services` is the account that changes users and groups; it may write
|
|
`ou=users` and `ou=groups` and set passwords, never read them, and its password is in
|
|
`/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`);
|
|
- 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). 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.
|
|
|
|
### Managing users
|
|
|
|
`vpn-router-user` manages the users. A user is a directory entry of one of three kinds, fixed when
|
|
it is added:
|
|
|
|
- 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
|
|
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
|
|
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
|
|
the router. `add` without the first and the last name adds one; its names cannot be set later;
|
|
- a service account, a Manager-only account whose `vpnRouterScope` is `service`, which another
|
|
system signs in with to use a router service, such as an OpenTelemetry Collector reading
|
|
`/metrics`; it cannot sign in to `/auth`. Besides its name, it has a display name and a
|
|
description, a service account with redirect URIs or grants is a client of the OpenID Provider,
|
|
and every one is a resource of it, with its scopes and roles (see OpenID Provider). `add
|
|
--service` adds one, and the web service manages them at `/service-accounts`,
|
|
not at `/users`. Internal service accounts are those the router creates and manages for its own
|
|
services, display name and description included; at present that is the metrics service's, the
|
|
account `metrics.service_account` names while metrics are enabled (see Metrics), which
|
|
`vpn-router-setup` names Metrics. The API only lists
|
|
them: `/service-accounts` refuses every other request on one, and `/groups` refuses to put one
|
|
in or take one out of a group, with 403.
|
|
|
|
Passwords and TOTP secrets are read from the first line of standard input. The answer is text: a
|
|
table of the users, the client profile, a TOTP secret or a short message, and a failure is a
|
|
message on standard error.
|
|
`--json`, before the command, gives the answer as JSON on standard output, and a failure as
|
|
`{"error": ...}` there too; the web service uses it. `-h` or `--help` shows the commands, and
|
|
after a command its options.
|
|
|
|
```sh
|
|
echo 's3cr3t-pass' | vpn-router-user add alice --first-name Alice --last-name Smith --email alice@example.com
|
|
echo 'm4nager-pass' | vpn-router-user add ops
|
|
echo 's3rvice-pass' | vpn-router-user add backup --service
|
|
vpn-router-user update alice --email a.smith@example.com
|
|
vpn-router-user list
|
|
vpn-router-user profile alice > alice.ovpn
|
|
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
|
|
vpn-router-user otp-remove alice
|
|
```
|
|
|
|
| Command | Effect |
|
|
| --- | --- |
|
|
| `list` | The users with their kind, names and e-mail addresses, whether each is enabled or locked, has a certificate and is enrolled for TOTP, the number of passkeys and the console login |
|
|
| `add <name> --first-name F --last-name L --email E` | Adds a VPN user and issues the certificate |
|
|
| `add <name> [--email E]` | Adds a Manager-only account, with the e-mail address when given; one name without the other two options is refused |
|
|
| `add <name> --service [--display-name N] [--description D] [--email E] [--redirect-uri U ...] [--post-logout-redirect-uri U ...] [--allowed-group G ...] [--grant G ...] [--public-client yes\|no] [--client-scope R:S ...] [--resource-scope S ...]` | Adds a service account, with its display name, description, e-mail address and OpenID Provider settings when given; the names are refused |
|
|
| `update <name> [--first-name F] [--last-name L] [--email E]` | Changes the names or the e-mail address |
|
|
| `update <name> [--display-name N] [--description D] [--email E] [--redirect-uri U ...] [--post-logout-redirect-uri U ...] [--allowed-group G ...] [--grant G ...] [--public-client yes\|no] [--client-scope R:S ...] [--resource-scope S ...]` | Changes the display name, the description, the e-mail address or the OpenID Provider settings of a service account; `''` removes one; a repeated option gives all values of its kind |
|
|
| `passwd <name>` | Sets the password |
|
|
| `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 |
|
|
| `otp-enrol <name> <code>` | Stores the secret read from standard input when the code is a current code of it, replacing a secret the user has |
|
|
| `otp-remove <name>` | Removes the user's TOTP secret and ends the sessions of the user's own POSIX account |
|
|
| `console <name> (--posix \| --account LOCAL \| --off)` | Switches the console login of a member of `admins` enrolled for TOTP on, as the user's own POSIX account or a local account, or off (see Console login and SSH) |
|
|
| `passkeys-remove <name>` | Removes all the user's passkeys (see Passkeys) |
|
|
|
|
A user name is 1 to 32 lowercase letters, digits and hyphens, starting with a letter, and a
|
|
service account may instead be named by a GUID in lowercase, such as
|
|
`3f2b8c1e-9a4d-4e6f-8b2a-1c5d7e9f0a3b`; 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. Once enrolled, a user logs in to OpenVPN
|
|
and `/auth` with the password followed by a current code. In the answers `manager_only`, `service`
|
|
and `internal` tell the kind, and a Manager-only account has its names as `null`; `update` changes only its e-mail
|
|
address, and `--email ''` removes it. Changing a user's password, disabling, unlocking or removing the user also revokes its
|
|
refresh tokens of `/auth` and its codes, tokens and sessions of the OpenID Provider. 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.
|
|
|
|
When the package is installed with a Router Manager administrator (the debconf questions
|
|
`vpn-router/admin_username` and `admin_password`), it leaves the answers in
|
|
`/etc/vpn-router/manager-admin.json` (root only). `vpn-router-setup` then creates that user as a
|
|
Manager-only account, adds it to the group `admins`, which it creates when missing, and removes
|
|
the file.
|
|
|
|
The certificate is also accepted by IKEv2 (EAP-TLS) access, which checks
|
|
certificates only, so `disable` does not affect it; `remove` revokes the certificate for both.
|
|
|
|
### Apple VPN profile
|
|
|
|
While `p2s.enabled` is `true` and `wan.local_fqdn` is set, `/users/<name>/apple-profile` and a
|
|
user's own `/account/apple-profile` give an unsigned Apple configuration profile (`.mobileconfig`)
|
|
that sets up IKEv2 access in the built-in VPN client of macOS, iOS and iPadOS. It holds:
|
|
|
|
- the P2S CA and the root CA, the root installed as a trusted root;
|
|
- the user's certificate and key as a PKCS#12 encrypted with the password of the request, with
|
|
3DES and SHA-1, the only encryption Apple's clients read; the device asks for the password when
|
|
the profile is installed, and the router does not keep it;
|
|
- an IKEv2 VPN to `wan.local_fqdn`, with the user name as the local identity, the certificate over
|
|
EAP-TLS, and AES-256, SHA2-256 and group 14 for IKE and ESP, as the router's IKEv2
|
|
connection (`road-warrior`) takes them;
|
|
- with `p2s.dns_domains`, the router's resolver for those domains and their subdomains
|
|
(`SupplementalMatchDomains`). macOS and iOS use the DNS server of a VPN that does not carry all
|
|
traffic only for its match domains.
|
|
|
|
The password is 8 to 128 characters. A user asking for the own profile also gives the current
|
|
password and, when enrolled, a code.
|
|
|
|
### Console login and SSH
|
|
|
|
A member of `admins` enrolled for TOTP may have console login: the Router Manager's Shell page
|
|
(see The shell), and, for the user's own POSIX account, SSH and sudo. Only root on the router
|
|
switches it, with `vpn-router-user console` or a token of `vpn-router-token`, as for the group
|
|
`admins`. Switching it on is refused without the enrolment, and a user whose enrolment is removed
|
|
is refused the shell, SSH and sudo until enrolling again; the user's open shells end within a
|
|
minute, and removing the enrolment ends the sessions of the user's own POSIX account:
|
|
|
|
```sh
|
|
vpn-router-user console alice --posix
|
|
vpn-router-user console bob --account azureuser
|
|
vpn-router-user console alice --off
|
|
vpn-config-edit set ssh.ldap_login true
|
|
```
|
|
|
|
- **Own POSIX account** (`--posix`): the user's entry becomes a `posixAccount` of the directory
|
|
with the user's name, a uid from 20000 up, the local group `vpn-router-console` as its group,
|
|
`/home/<name>` and `/bin/bash`. `nslcd` makes it known to the system over slapd's local socket
|
|
and checks its password by binding as the user, so the user types the password followed by a
|
|
current code, failed logins count towards the lockout, and a disabled user cannot sign in. The home is created at the first login and kept when the user is removed. Members of
|
|
`vpn-router-console` may run any command with sudo, with the same password. A local account of
|
|
the same name is refused.
|
|
- **Local account** (`--account`): the console runs as an existing local account with a uid from
|
|
1000 to 19999, such as the VM's admin user; its SSH keys and sudo rights stay as they are.
|
|
- **Off** (`--off`): the system no longer knows the user's own POSIX account, and its sessions
|
|
end; its uid is kept for the next time. Leaving `admins`, or removing or renaming the group,
|
|
switches the console login of the members off.
|
|
|
|
While `ssh.ldap_login` is `true`, setup lets sshd accept the password of the members of
|
|
`vpn-router-console` (`/etc/ssh/sshd_config.d/vpn-router.conf`); other accounts keep sshd's
|
|
settings. A password change is made in the Router Manager or with `vpn-router-user passwd`, not
|
|
with `passwd`.
|
|
|
|
### Passkeys
|
|
|
|
A user who is not a service account may sign in to the Router Manager and to the OpenID Provider
|
|
with a passkey instead of the password and code. Passkeys are for those sign-ins only: OpenVPN,
|
|
the shell, SSH and sudo keep the password and code. A passkey belongs to `wan.local_fqdn`, so the
|
|
browser offers it on the HTTPS addresses on that name, and passkeys need the name set; a passkey made
|
|
before the name changes is no longer offered. The authenticator checks the user (a PIN or a
|
|
fingerprint), and the passkey is kept by it or by the user's password manager.
|
|
|
|
A user adds a passkey on the Router Manager's Account page with the current password and code,
|
|
and removes it there. Each passkey is a value of `vpnRouterPasskey` on the user's entry, with its
|
|
public key, label and the time it was added; a user has at most 10. An administrator removes all
|
|
passkeys of a user on the Users page, through `/users`, or with:
|
|
|
|
```sh
|
|
vpn-router-user passkeys-remove alice
|
|
```
|
|
|
|
A disabled or locked user is refused a passkey sign-in too.
|
|
|
|
### Managing groups
|
|
|
|
`vpn-router-group` manages the groups. A group has a name, with the rules of a user name, an
|
|
optional description of at most 256 printable characters, and at least one member, each a user
|
|
of the directory. The group `admins` grants the management of the router, and the allowed groups
|
|
of a client of the OpenID Provider admit their members to it, and a role of a resource assigned to
|
|
a group is a role of its members; other groups record membership only. Renaming a group renames it
|
|
in the clients that allow it and in its roles, a group a client allows cannot be removed, and
|
|
removing a group takes it out of its roles. The output, `--json`, `-h` and `--help` work as for `vpn-router-user`.
|
|
|
|
```sh
|
|
vpn-router-group add ops --member alice --member bob --description 'Operations team'
|
|
vpn-router-group list
|
|
vpn-router-group update ops --name netops
|
|
vpn-router-group update netops --description ''
|
|
vpn-router-group member-add netops carol
|
|
vpn-router-group member-remove netops bob
|
|
vpn-router-group remove netops
|
|
```
|
|
|
|
| Command | Effect |
|
|
| --- | --- |
|
|
| `list` | The groups with their members and descriptions |
|
|
| `add <name> --member U [--member U ...] [--description D]` | Adds the group with its members |
|
|
| `update <name> [--name N] [--description D]` | Renames the group or sets its description; `''` removes it |
|
|
| `remove <name>` | Removes the group; its members are kept; refused while a client of the OpenID Provider allows it |
|
|
| `member-add <name> <user>` | Adds the user to the group |
|
|
| `member-remove <name> <user>` | Takes the user out of the group; refused for the last member |
|
|
|
|
The exit status is 0 on success, 2 for an invalid request, 3 when the group or the user does
|
|
not exist, 4 when the group exists and 1 for any other failure.
|
|
|
|
## Platform modules
|
|
|
|
Platform-specific values do not belong in the core, so they are Python modules in
|
|
`/usr/lib/vpn-router/vpnrouter_platforms/`, imported at run time and selected by
|
|
`general.platform`. The platform is never probed: the caller already knows where it is
|
|
deploying.
|
|
|
|
`none` is not a module. It is the shipped default and means "configure nothing at all",
|
|
which is how a deferred install is expressed. `generic` is the module to choose when the
|
|
router should be configured but the platform adds nothing.
|
|
|
|
The shipped modules currently add nothing. IKEv2, OpenVPN and WireGuard clients are given the
|
|
router's addresses of `p2s.dns_listen` as their DNS servers, by default the internal address,
|
|
the WireGuard clients in the configuration the Router Manager makes for a device (`DNS =`),
|
|
where `vpn-router-resolver.service`, dnsmasq with `/etc/vpn-router/dnsmasq.conf`, answers and
|
|
the firewall opens port 53 for the client pools and the WireGuard network, so no provider resolver has to be advertised or
|
|
routed through the tunnel on any platform. dnsmasq forwards to `systemd-resolved` on
|
|
127.0.0.53, which keeps only its loopback stubs, and forwards no name without a dot, so the
|
|
router's own host name, `_gateway` and `localhost` are not answered to the clients. The clients reach
|
|
the internal address when it is inside `local.cidrs`, which they route through the tunnel. Port 53
|
|
of the other addresses stays free for the name server (see Name server).
|
|
The modules exist as the documented place for platform specifics if that changes.
|
|
|
|
A module may define any of the following, and takes the default for anything it omits:
|
|
|
|
| Name | Type | Effect |
|
|
|---|---|---|
|
|
| `EXTRA_P2S_DNS` | `str` | added to the `dns` list of the IKEv2 address pool |
|
|
| `EXTRA_LOCAL_TS` | `str` | added to `local_ts` on both connections |
|
|
| `apply(ctx)` | function | called once the system is configured, to apply platform-specific state; must be idempotent |
|
|
|
|
`ctx` is the template context, so a module reads what it needs from it -
|
|
`ctx['wan_iface']`, `ctx['int_iface']`, `ctx['local_cidrs']` and the rest.
|
|
|
|
Adding a platform is adding one file. Modules are discovered by listing the package, so
|
|
nothing in the core needs editing:
|
|
|
|
```python
|
|
# /usr/lib/vpn-router/vpnrouter_platforms/example.py
|
|
"""Example provider."""
|
|
|
|
EXTRA_P2S_DNS = '192.0.2.53'
|
|
EXTRA_LOCAL_TS = '192.0.2.53/32'
|
|
```
|
|
|
|
Set `general.platform` to `example` and restart the service. The debconf question offers only the
|
|
modules shipped with the package; the configuration file accepts any module installed.
|
|
|
|
## Status web service
|
|
|
|
The `vpn-router-web` service shows the state of the router on the addresses of the interfaces
|
|
`web.listen` names, the internal, the external or both (the default), and on `127.0.0.1`, where
|
|
Caddy calls it (see HTTPS), with `web.port` (7081 by default). Its pages `/` and `/status` need no authentication and show no
|
|
configuration: no key, identity, subnet, or address but the client's. They show as much as
|
|
`web.status_content` says, read at each request:
|
|
|
|
| `status_content` | `/` and `/status` show |
|
|
|---|---|
|
|
| `basic` (default) | The overall status (`ok` or `degraded`), the time and the Client IP |
|
|
| `standard` | Also the `vpn-router` version and the number of active services and of services in any other state, without their names |
|
|
| `full` | Also each service and its state, Docker, how many site-to-site connections are set up and how many of them are established (`site_to_site`: `connections` and `established`, `null` when swanctl cannot be asked), IKEv2, OpenVPN and WireGuard |
|
|
|
|
The Client IP is the address the request came from: the last entry of `X-Forwarded-For` for a
|
|
request from one of `web.trusted_proxies`, by default the address the request came in on, so
|
|
behind Caddy or a Router Manager on the router it is the browser's. `/status` names the content in
|
|
`content` and the Client IP in `client_ip`; `/` keeps the time and the link to `/status`.
|
|
|
|
The service first makes its signing key, the OpenID Provider's key and the refresh tokens'
|
|
database, retrying every 15 seconds, for example while PostgreSQL is not running yet, and logs
|
|
each failure; then it listens.
|
|
|
|
The service is `/usr/lib/vpn-router/web`, which starts it, and the package
|
|
`/usr/lib/vpn-router/vpnrouter_web/`, which answers the requests:
|
|
|
|
- `handler`: reads each request, checks its token, and passes it to the module of its path.
|
|
- `common`: what the modules share.
|
|
- One module per family of paths: `status`, `auth`, `identities`, `connectivity`, `backup`,
|
|
`pki`, `logs`, `sites`, `dns`, `metrics` and `shell`.
|
|
|
|
### Bootstrap diagnostics
|
|
|
|
Until a run of `vpn-router-setup` has succeeded, every run starts `vpn-router-bootstrap`, which
|
|
serves diagnostics as plain text on port 17081 of every address, and the firewall opens 17081
|
|
on the internal interface. The first successful run stops
|
|
the bootstrap service and closes its port, and writes `/var/lib/vpn-router/setup.done`, after
|
|
which it never runs again; a failed run leaves it running, so the reason stays readable until
|
|
a later run succeeds. The unit is never enabled.
|
|
|
|
The page, read again every 10 seconds, shows the state, the step and the failure of the last
|
|
setup run (`/run/vpn-router/setup.json`, which setup writes at every step), the state of the
|
|
router's services, the failed units, the addresses and routes, `cloud-init status`, and the end of
|
|
the logs of `vpn-router-setup`, `vpn-router-web` and `vpn-router-manager`:
|
|
|
|
```bash
|
|
curl http://10.0.0.4:17081/
|
|
```
|
|
|
|
| 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) |
|
|
| `/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 IKEv2 access; needs a token |
|
|
| `/service-accounts` | The service accounts; needs a token |
|
|
| `/groups` | The groups of users; needs a token |
|
|
| `/site-to-site` | The site-to-site connections: list, add, change and remove; needs a token |
|
|
| `/account` | The account of the signed-in user: its details, its password and its TOTP enrolment; needs a user's token |
|
|
| `/logs` | The last lines of the journal of the router's units and of the files of `web.log_files`; needs a token |
|
|
| `/sites` | The web sites and their files; needs a token |
|
|
| `/dns` | The name server's settings and records; needs a token |
|
|
| `/.well-known/openid-configuration`, `/oidc/...` | The OpenID Provider, on `127.0.0.1` only, where Caddy passes the requests (see OpenID Provider) |
|
|
|
|
The status holds the state of `vpn-router-setup`, `slapd` and each PostgreSQL cluster unit
|
|
(`postgresql@<version>-<cluster>`, or `postgresql` as `no cluster` without one), of
|
|
`vpn-router-manager`, of `strongswan` when site-to-site or
|
|
IKEv2 access is configured, of `wg-quick@wg0` when WireGuard is, and of
|
|
`openvpn-server@vpn-router` when OpenVPN is; the number of connected
|
|
OpenVPN clients, from the server's status file, which OpenVPN rewrites every 5 seconds; the package version; whether
|
|
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`; as `site_to_site`, the number of site-to-site
|
|
connections that are set up (`connections`) and of those established (`established`, `null` when
|
|
swanctl cannot be asked), without their names; the number of connected IKEv2 clients; and
|
|
the WireGuard peers with the age of their last handshake and their traffic. `status` is `ok` when
|
|
all these services are active, and `degraded` otherwise.
|
|
|
|
```bash
|
|
curl http://10.0.0.4:7081/
|
|
curl http://10.0.0.4:7081/status
|
|
```
|
|
|
|
### Tokens and signing in
|
|
|
|
`/config`, `/metrics/history`, `/metrics/settings`, `/https/settings`, `/users`, `/service-accounts`, `/groups`, `/site-to-site`, `/backup`, `/remote-access`, `/ca`, `/logs`, `/sites`, `/dns`, `/shell` and `/account` need a JWT in an `Authorization: Bearer` header, signed with
|
|
HS256 by the key the web service makes on its first run, `/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; every user may
|
|
call `/account`, and for the other paths 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 names no user, so `/account` refuses it with 403;
|
|
it may call everything else, 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 and `"name"` |
|
|
| `POST /auth/passkey` | `{}` | `{"id", "options"}`: a pending sign-in, valid 2 minutes, and the options of `navigator.credentials.get` in their JSON form |
|
|
| `POST /auth/passkey/complete` | `{"id", "credential"}`, `credential` the browser's answer as its `toJSON()` gives it | the tokens and `"name"`, the user whose passkey signed |
|
|
| `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). A passkey that does not sign, is not the user's, or of a disabled, locked or service
|
|
account gets 401 too (see Passkeys). `/auth`, `/auth/passkey/complete` 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 call came in 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 the web service creates on its first run 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 /status/full page
|
|
|
|
`/status/full`, with the same token as `/config`, shows the whole status document, as `/status`
|
|
does with `web.status_content` `full`, whatever that setting says, so the Router Manager shows
|
|
every service and tunnel while the public status page shows less.
|
|
|
|
### The shell
|
|
|
|
While `web.shell` is `true`, `/shell` gives a member of `admins` whose console login is on and
|
|
who is enrolled for TOTP (see Console login and SSH) a login shell on the router, as the user's
|
|
console account, for the Router Manager's Shell page. It is off by default and switched on and
|
|
off on the router only:
|
|
|
|
```sh
|
|
vpn-router-user console alice --posix
|
|
vpn-config-edit set web.shell true
|
|
```
|
|
|
|
| Request | Body | Answer |
|
|
| --- | --- | --- |
|
|
| `POST /shell/passkey` | `{}` | `{"id", "options"}`, `options` those of `navigator.credentials.get` for the user's own passkeys |
|
|
| `POST /shell` | `{"password", "code"}`, or `{"passkey": {"id", "credential"}}`; `{}` within `web.shell_auth_timeout` | `{"ticket", "expires_in": 30}` |
|
|
| `GET /shell?ticket=<ticket>` | a WebSocket handshake | the terminal |
|
|
|
|
`POST /shell` needs an access token of `/auth`, not one of `vpn-router-token`, and signs its user
|
|
in again, with the password and the TOTP code, or with a passkey: `POST /shell/passkey` begins a
|
|
proof for one of the user's own passkeys with user verification, which the browser answers within
|
|
two minutes, and `POST /shell` takes its `id` and the browser's answer as `credential`. A passkey
|
|
with user verification is two factors in one, so it takes the place of the password and the code.
|
|
A wrong password or code gets 403 and counts as a failed login, and a passkey that is not accepted
|
|
gets 403. `POST /shell` gets 403 too while `web.shell` is `false`, the user's console login is off
|
|
or the user is not enrolled for TOTP: also with a passkey, the PAM login session of the shell,
|
|
`pam_authz_search` of nslcd for a directory account, and sudo need the enrolment. Without a
|
|
password or a passkey it gives a ticket while the user's last sign-in of
|
|
`POST /shell` or keystroke in a shell is less than `web.shell_auth_timeout` minutes ago, and
|
|
`{"ticket": null}` otherwise. The ticket opens one
|
|
shell for the same user within 30 seconds. Over the WebSocket, binary messages of the client
|
|
are keystrokes, a text message `{"cols", "rows"}` sets the window size, and binary messages of
|
|
the router are the terminal's output. The shell is a PAM login session of the console account,
|
|
run by `systemd-run` in a transient unit `vpn-router-shell-<id>`; sudo works as for SSH, with the
|
|
password and the TOTP code. It is closed after `web.shell_idle_minutes` without a keystroke, when
|
|
the shell exits, when the connection closes, or within a minute of the user's console login being
|
|
switched off or the TOTP enrolment removed; the router then sends a close frame with the reason. The
|
|
journal of `vpn-router-web` records who opened each shell, from which address, in which unit,
|
|
when it ended and why, and each refused ticket or shell with the reason.
|
|
|
|
#### The editor of the shell
|
|
|
|
In the shell, `vpn-router-editor FILE...`, or its short name `vre`, opens files in the editor of
|
|
the Router Manager's Shell page and returns at once:
|
|
|
|
```sh
|
|
vre /etc/vpn-router/vpn-router.json ~/notes.txt
|
|
```
|
|
|
|
- **How the files reach the page:** each shell has its session in `VPN_ROUTER_SHELL`. `vre`
|
|
names each file, as an absolute path, with that session on the socket
|
|
`/run/vpn-router/editor.sock`. The web service accepts it only from a process that runs as the
|
|
shell's console account, and sends the page a text message `{"editor": "open", "session", "id", "path"}`
|
|
over the shell's WebSocket.
|
|
- **Reading and saving:** the page uses the routes below, which need the user's access token. The
|
|
file is read and written as the console account by `/usr/lib/vpn-router/editor-file`, run with
|
|
`systemd-run --uid --pipe`, so the account's permissions apply. A save writes the file in place,
|
|
keeping its owner and mode, and creates a file that does not exist.
|
|
- **Limits:** only regular files of UTF-8 text up to 1 MiB, and at most 20 open files per shell.
|
|
- **Lifetime:** the files are open while the shell lives. `vre` outside the Router Manager's shell
|
|
exits with status 2.
|
|
|
|
| Request | Body | Answer |
|
|
| --- | --- | --- |
|
|
| `GET /shell/editor/<session>/<id>` | | `{"path", "content", "version"}`, `version` the SHA-256 of the content |
|
|
| `PUT /shell/editor/<session>/<id>` | `{"content", "version"}` | `{"path", "version"}`; 409 when the file no longer has `version` |
|
|
| `DELETE /shell/editor/<session>/<id>` | | `{"path", "closed": true}` |
|
|
|
|
### The /config page
|
|
|
|
`/config` shows what `/` and `/status` leave out, for a client such as Cloud Manager:
|
|
|
|
| Key | Content |
|
|
| --- | --- |
|
|
| `configuration` | `vpn-router.json` with the defaults applied, without its secret options at any level (`psk`, `database`, and any option named like a secret, password, token or key) |
|
|
| `network` | The external and internal interface, their addresses, the internal gateway, `local.cidrs`, `site_to_site`: each connection with `name`, `local_identity`, `remote_identity`, `local_cidrs` and `remote_cidrs`, 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 |
|
|
| `postgresql` | `clusters`: each PostgreSQL cluster of `pg_lsclusters` with its version, name, port, status and data directory, and for one that is online its `settings`: `server_version`, `listen_addresses`, `port`, `max_connections`, `shared_buffers`, `effective_cache_size`, `work_mem`, `password_encryption`, `ssl` and `timezone`; `null` when `pg_lsclusters` fails |
|
|
| `caddy` | The installed Caddy: `version`, `source`, `upstream` once Caddy's repository is configured and `ubuntu` otherwise, and `service`, the state of `caddy.service`; `null` when Caddy is not installed |
|
|
| `oidc` | The OpenID Provider: `enabled`, and `issuer`, its issuer URL, `null` while it is disabled |
|
|
|
|
### The /postgresql page
|
|
|
|
`/postgresql`, with the same token as `/config`, shows the databases of the PostgreSQL clusters on the
|
|
router:
|
|
|
|
- **Content:** `clusters` lists each cluster with its version, name, port and status, its `databases` (name, owner, encoding and size in bytes, `pg_database_size`) and their total `size`.
|
|
- **A cluster that is not online:** `databases` and `size` are `null`.
|
|
- **Collection:** the web service reads it with `psql` as the `postgres` user at each request.
|
|
|
|
### The /logs API
|
|
|
|
`/logs`, with the same token as `/config`, shows the last lines of the journal and of log files,
|
|
read at each request:
|
|
|
|
| Request | Answer |
|
|
| --- | --- |
|
|
| `GET /logs` | `{"units", "files", "max_lines"}`: the units whose journal it shows, the files of `web.log_files` and the most lines one request answers, 1000 |
|
|
| `GET /logs/journal?unit=<unit>&lines=<n>` | `{"lines": [...]}`: the last lines of the unit's journal, or of the whole journal without `unit`, with ISO times |
|
|
| `GET /logs/file?path=<path>&lines=<n>` | `{"lines": [...]}`: the last lines of a file `web.log_files` lists |
|
|
|
|
The units are the package's own (`vpn-router-setup`, `-web`, `-metrics`, `-pki`, `-backup`,
|
|
`-caddy`, `-resolver`, `-webdav`), `vpn-router-manager`, `caddy`, `slapd`, `nslcd`, `postgresql`,
|
|
`strongswan`, `wg-quick@wg0`, `openvpn-server@vpn-router`, `ssh` and `pdns`. `lines` is 1 to 1000, 200 without it. A
|
|
file is read from at most its last MiB, so very long lines give fewer lines. A unit or a file not
|
|
in the lists is 404, as is a listed file that does not exist.
|
|
|
|
```sh
|
|
vpn-config-edit set web.log_files /var/log/syslog,/var/log/auth.log
|
|
```
|
|
|
|
### The /sites API
|
|
|
|
`/sites`, with the same token as `/config`, manages the web sites, with the module
|
|
`vpn-router-site` uses too, `/usr/lib/vpn-router/vpnrouter_sites.py`:
|
|
|
|
| Request | Answer |
|
|
| --- | --- |
|
|
| `GET /sites` | `{"settings", "sites"}`: the `sites` section and the sites, each with its settings but `private_key`, with `private_key_set`, `modified` and `directory` |
|
|
| `PUT /sites` | Changes any of the settings of the `sites` section |
|
|
| `POST /sites` | Adds a site: `{"name", ...}` with any of its settings; an existing name is 409 |
|
|
| `GET /sites/<name>` | The site |
|
|
| `PUT /sites/<name>` | Changes any of its settings; an empty or absent `private_key` keeps the key |
|
|
| `DELETE /sites/<name>?content=true` | Removes the site, with `content=true` also its folder |
|
|
| `GET /sites/<name>/files/<path>` | A folder: `{"path", "entries": [{"name", "folder", "size", "modified"}]}`; a file: the file, as an attachment |
|
|
| `PUT /sites/<name>/files/<path>` | Writes the body to the file, creating its folders; with `?unzip=true` the body is a zip archive extracted into the folder of the path: `{"path", "files"}` |
|
|
| `POST /sites/<name>/files/<path>` | `{"folder": true}` creates the folder; `{"rename": "<path>"}` moves the file or folder |
|
|
| `DELETE /sites/<name>/files/<path>` | Removes the file or folder |
|
|
|
|
The paths of `/files/` are relative to the site's folder and may not leave it; a `PUT` needs a
|
|
`Content-Length`. The files of a site may also be used by a member of its `editors` group. Invalid
|
|
settings are 400, a missing site 404.
|
|
|
|
```sh
|
|
curl -X PUT -H "Authorization: Bearer $TOKEN" --data-binary @site.zip \
|
|
'https://router.example.com:7444/sites/www/files/?unzip=true'
|
|
```
|
|
|
|
### The /dns API
|
|
|
|
`/dns`, with the same token as `/config`:
|
|
|
|
| Request | Answer |
|
|
| --- | --- |
|
|
| `GET /dns` | `{"settings", "nameserver", "public_address", "installed", "running", "domains", "problem"}`: the `dns` section, `wan.local_fqdn`, the primary name server, `wan.public_address`, whether PowerDNS is installed and runs, the domains, each `{"name", "ttl", "manual", "records"}` with its manual records and the records it serves, and why they cannot be read, `null` when they can |
|
|
| `PUT /dns` | Changes any of the settings of the `dns` section, and with `public_address` `wan.public_address`; answers `{"settings", "public_address"}` |
|
|
| `POST /dns/domains` | Adds a domain: `{"name", "ttl"?}`; an existing name is 409 |
|
|
| `PUT /dns/domains/<domain>` | Changes `ttl` of the domain |
|
|
| `DELETE /dns/domains/<domain>` | Removes the domain with its manual records |
|
|
| `POST /dns/domains/<domain>/records` | Adds a manual record: `{"name", "type", "content", "ttl"?}`; `name` is relative to the domain, `@` for the domain itself, or absolute with a final dot, and `ttl` is the domain's without it |
|
|
| `DELETE /dns/domains/<domain>/records?name=&type=&content=` | Removes the manual records of the name and type, only the one of `content` when it is given |
|
|
|
|
A TXT content without quotes is quoted; MX is `<preference> <host>`, SRV `<priority> <weight>
|
|
<port> <host>` and CAA `<flags> <tag> "<value>"`. A CNAME must be its name's only record.
|
|
|
|
### The /users API
|
|
|
|
`/users` manages users with the same token as `/config`, with the module `vpn-router-user` uses
|
|
too, `/usr/lib/vpn-router/vpnrouter_users.py`, so both apply the same rules. Request
|
|
and answer bodies are JSON; a request body is at most 65536 bytes.
|
|
|
|
| Request | Body | Answer |
|
|
| --- | --- | --- |
|
|
| `GET /users` | | `{"users": [{"name", "manager_only", "service", "internal", "enabled", "locked", "certificate", "otp", "console", "wireguard_peers", "passkeys", "first_name", "last_name", "email"}, ...], "wireguard": {"enabled", "address"}}`; `console` is `null` or `{"enabled", "account", "local", "uid"}`, `wireguard_peers` a list of `{"public_key", "ips"}`, `passkeys` a list of `{"id", "label", "created"}`, and `wireguard` the endpoint's settings, whose network the peers' addresses come from |
|
|
| `POST /users` | `{"name", "password", "first_name", "last_name", "email"}` for a VPN user; `{"name", "password", "email"?}` for a Manager-only account | 201 and the user |
|
|
| `PUT /users/<name>` | any of `{"first_name", "last_name", "email", "wireguard_peers"}`; `wireguard_peers`, `[{"public_key", "ips"}]`, replaces a VPN user's WireGuard peers (see Configuration); for a Manager-only account only `{"email"}`, `""` removing it | 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>/apple-profile` | `{"password"}` | `{"name", "profile"}`, the Apple configuration profile of IKEv2 access (see Apple VPN profile) |
|
|
| `POST /users/<name>/otp` | | `{"name", "secret", "uri"}`, a new TOTP secret; nothing is stored |
|
|
| `PUT /users/<name>/otp` | `{"secret", "code"}` | the user, once the code matches the secret |
|
|
| `DELETE /users/<name>/otp` | | the user |
|
|
| `DELETE /users/<name>/passkeys` | | the user, without passkeys |
|
|
| `PUT /users/<name>/console` | `{"enabled", "account"?}`; only with a token of `vpn-router-token` | the user |
|
|
|
|
A failure is `{"error": ...}` with 400 for an invalid request, 404 for a user that does not
|
|
exist or is a service account, 409 for a user that exists and 500 otherwise. `/users` does not
|
|
list service accounts; `/service-accounts` serves them. `/users` works whether or not OpenVPN or
|
|
IKEv2 access is enabled; the profile needs OpenVPN. Passwords, private keys and TOTP
|
|
secrets cross the internal network in plain HTTP, like the token.
|
|
|
|
```bash
|
|
curl -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
|
-d '{"name": "alice", "password": "s3cr3t-pass", "first_name": "Alice", "last_name": "Smith", "email": "alice@example.com"}' \
|
|
http://10.0.0.4:7081/users
|
|
```
|
|
|
|
### The /service-accounts API
|
|
|
|
`/service-accounts` manages the service accounts with the same token and the same module as
|
|
`/users`.
|
|
|
|
| Request | Body | Answer |
|
|
| --- | --- | --- |
|
|
| `GET /service-accounts` | | `{"service_accounts": [{"name", "display_name", "description", "internal", "enabled", "locked", "email", "redirect_uris", "post_logout_redirect_uris", "allowed_groups", "grants", "public_client", "client_scopes", "resource_scopes", "roles", ...}, ...], "oidc": {"enabled", "issuer"}}`, each as `/users` shows a user, and the issuer of the OpenID Provider, `null` while it is disabled |
|
|
| `POST /service-accounts` | `{"name", "password", "display_name"?, "description"?, "email"?, "redirect_uris"?, "post_logout_redirect_uris"?, "allowed_groups"?, "grants"?, "public_client"?, "client_scopes"?, "resource_scopes"?, "roles"?}` | 201 and the account |
|
|
| `PUT /service-accounts/<name>` | any of `{"display_name", "description", "email", "redirect_uris", "post_logout_redirect_uris", "allowed_groups", "grants", "public_client", "client_scopes", "resource_scopes", "roles"}`; `""` removes one of the first three, `[]` the values of the lists, `false` the public client | the account |
|
|
| `PUT /service-accounts/<name>/password` | `{"password": ...}` | the account |
|
|
| `POST /service-accounts/<name>/disable` | | the account |
|
|
| `POST /service-accounts/<name>/enable` | | the account |
|
|
| `POST /service-accounts/<name>/unlock` | | the account |
|
|
| `DELETE /service-accounts/<name>` | | `{"name", "removed": true}` |
|
|
|
|
A display name has at most 64 printable characters and a description at most 256. The settings of
|
|
the OpenID Provider (see OpenID Provider) are:
|
|
|
|
- **URIs and allowed groups:** lists of at most 16 values: URIs of `https`, or of `http` for
|
|
`localhost`, `127.0.0.1` or `::1`, without a fragment and of at most 1024 characters, and names
|
|
of existing groups;
|
|
- **`grants`:** `client_credentials` and `device_code`; `public_client`, `true` or `false`;
|
|
- **`client_scopes`:** at most 16 scopes of resources, `<resource>:<scope>`;
|
|
- **`resource_scopes`:** at most 16 scope names of lowercase letters, digits, dots, underscores and
|
|
hyphens;
|
|
- **`roles`:** `[{"name", "description", "groups", "accounts"}]`, which replaces the account's roles:
|
|
role names of letters, digits, dots, underscores and hyphens, and existing groups and accounts.
|
|
|
|
The internal service accounts, `internal` true, are only listed: every other request on one, and
|
|
a role assigned to one, gets 403, as the router manages them, their display name and description
|
|
included. A password change, disabling, unlocking and removing revoke the tokens the account was
|
|
issued. A failure is otherwise as for `/users`, with 404 also for a name that is not a service
|
|
account.
|
|
|
|
### The /account API
|
|
|
|
`/account` serves the user the access token names, whether or not the user is a member of
|
|
`admins`, with the same module as `/users`:
|
|
|
|
| Request | Body | Answer |
|
|
| --- | --- | --- |
|
|
| `GET /account` | | the user as `/users` shows it, and `"admin"`, whether the user is a member of `admins` |
|
|
| `PUT /account/password` | `{"current_password", "current_code", "password"}` | the user |
|
|
| `POST /account/otp` | | `{"name", "secret", "uri"}`, a new TOTP secret; nothing is stored |
|
|
| `PUT /account/otp` | `{"current_password", "current_code", "secret", "code"}` | the user, once the code matches the secret |
|
|
| `POST /account/passkeys` | `{"current_password", "current_code"}` | `{"id", "options"}`: a pending registration, valid 2 minutes, and the options of `navigator.credentials.create` in their JSON form |
|
|
| `PUT /account/passkeys` | `{"id", "credential", "label"?}`, `credential` the browser's answer as its `toJSON()` gives it; `label` at most 64 characters, `Passkey` without one | the user |
|
|
| `DELETE /account/passkeys/<id>` | | the user |
|
|
| `POST /account/profile` | `{"current_password", "current_code"}` | `{"name", "profile"}`, the user's OpenVPN client profile |
|
|
| `POST /account/apple-profile` | `{"current_password", "current_code", "password"}` | `{"name", "profile"}`, the user's Apple configuration profile of IKEv2 access (see Apple VPN profile) |
|
|
|
|
`current_code` is needed only for a user enrolled for TOTP; removing a passkey needs neither. A wrong current password or code gets
|
|
403 and counts as a failed login; slapd accepts each code once, so a code used to sign in is not
|
|
accepted again, and a change right after signing in waits for the next code. A user enrols or
|
|
replaces the enrolment; removing it is left to `/users` and `vpn-router-user`. Changing the
|
|
password revokes the user's refresh tokens. Other failures are those of `/users`.
|
|
|
|
### The /groups API
|
|
|
|
`/groups` manages groups with the same token and the same module as `/users`.
|
|
|
|
| Request | Body | Answer |
|
|
| --- | --- | --- |
|
|
| `GET /groups` | | `{"groups": [{"name", "description", "members": [...]}, ...]}` |
|
|
| `POST /groups` | `{"name", "members": [...]}`, and `"description"` | 201 and the group |
|
|
| `PUT /groups/<name>` | any of `{"name", "description"}`; `""` removes the description | the group |
|
|
| `DELETE /groups/<name>` | | `{"name", "removed": true}` |
|
|
| `POST /groups/<name>/members` | `{"user": ...}` | the group |
|
|
| `DELETE /groups/<name>/members/<user>` | | the group |
|
|
|
|
A failure is `{"error": ...}` with 400 for an invalid request, among them taking out the last
|
|
member, 404 for a group or user that does not exist, 409 for a group that exists and 500
|
|
otherwise. `DELETE /users/<name>` is refused with 400 while the user is the last member of a
|
|
group.
|
|
|
|
### The /site-to-site API
|
|
|
|
`/site-to-site` manages the site-to-site connections of `vpn-router.json` with the same token as
|
|
`/users`.
|
|
|
|
| Request | Body | Answer |
|
|
| --- | --- | --- |
|
|
| `GET /site-to-site` | | `{"connections": [{"name", "addrs", "id", "cidrs", "local_cidrs", "local_id_mode", "auth", "psk_file", "remote_ca", "psk_set", "established"}, ...]}` |
|
|
| `POST /site-to-site` | `{"name", "addrs", "id", "cidrs", "local_cidrs", "local_id_mode", "auth", "psk", "remote_ca"}`; `psk` is required with `auth` `psk`, the default | 201 and the connection |
|
|
| `PUT /site-to-site/<name>` | any of the settings of `POST` but `name`; an empty or absent `psk` keeps the key, a `psk` given replaces `psk_file` | the connection |
|
|
| `DELETE /site-to-site/<name>` | | `{"name", "removed": true}` |
|
|
|
|
A connection is shown without its key: `psk_set` says whether `psk` holds one, and `established`
|
|
whether its tunnel is up, `null` when swanctl cannot be asked. Each change is made under the lock
|
|
`vpn-config-edit` takes, checked as `vpn-router-setup` checks the configuration, written to
|
|
`vpn-router.json`, and applied by restarting `vpn-router-setup` without waiting for it. A failure
|
|
is `{"error": ...}` with 400 for an invalid request or configuration, with the reason, 404 for a
|
|
connection that does not exist, 409 for a name that exists and 500 otherwise. Keys cross the
|
|
internal network in plain HTTP, like the token.
|
|
|
|
### The /backup API
|
|
|
|
`/backup` shows and changes the backup settings and starts a backup, with the same token as
|
|
`/users`.
|
|
|
|
| Request | Body | Answer |
|
|
| --- | --- | --- |
|
|
| `GET /backup` | | `{"settings", "state", "archives", "next", "client", "rclone"}` |
|
|
| `PUT /backup` | any of the settings of the `backup` section; an empty or absent `pbs_password` keeps the secret | `{"settings"}` |
|
|
| `POST /backup/run` | | 202 and `{"started": true}`; 409 while a backup runs |
|
|
|
|
The answer of `GET /backup` holds:
|
|
|
|
- **`settings`:** the `backup` section without `pbs_password`, with `pbs_password_set`.
|
|
- **`state`:** the last run, `null` before the first. Its `result` is `running`, `ok`, `warning` (the backup was made but the cloud copy failed), `failed` or `interrupted` (the run ended without recording its result). It also has `started`, `finished`, `target`, `archive` or `snapshot`, `size` in bytes, `removed` (the archives retention deleted), `error` and `cloud` (`{"remote", "mode", "result", "error"}`).
|
|
- **`archives`:** the local archives of this host, newest first, as `{"name", "time", "size"}`.
|
|
- **`next`:** the next run as systemd shows it, `null` while backups are off.
|
|
- **`client`:** whether `proxmox-backup-client` is installed.
|
|
- **`rclone`:** `{"installed", "config", "remotes"}`, the remotes being the sections of `rclone.conf`.
|
|
|
|
A change is made, checked and applied as for `/site-to-site`.
|
|
|
|
### The /metrics/settings API
|
|
|
|
`/metrics/settings` shows and changes the settings of the metrics service, with the same token as
|
|
`/users`.
|
|
|
|
| Request | Body | Answer |
|
|
| --- | --- | --- |
|
|
| `GET /metrics/settings` | | `{"settings"}`: the `metrics` section without `otlp_password`, with `otlp_password_set` |
|
|
| `PUT /metrics/settings` | any of the settings of the `metrics` section; an empty or absent `otlp_password` keeps the secret, and an empty `otlp_username` removes it | `{"settings"}` |
|
|
|
|
A change is made, checked and applied as for `/site-to-site`.
|
|
|
|
### The /https/settings API
|
|
|
|
`/https/settings` shows the settings of HTTPS and changes the CA of Caddy's certificates, with the
|
|
same token as `/users`.
|
|
|
|
| Request | Body | Answer |
|
|
| --- | --- | --- |
|
|
| `GET /https/settings` | | `{"settings"}`: the `https` section |
|
|
| `PUT /https/settings` | `{"acme_staging"}`; any other setting of the `https` section is 400, as it is changed on the router with `vpn-config-edit` | `{"settings"}` |
|
|
|
|
A change is made, checked and applied as for `/site-to-site`.
|
|
|
|
### The /remote-access API
|
|
|
|
`/remote-access` shows and changes the remote access systems, IKEv2 (`p2s`),
|
|
OpenVPN (`openvpn`) and WireGuard (`wireguard`), with the same token as `/users`.
|
|
|
|
| Request | Body | Answer |
|
|
| --- | --- | --- |
|
|
| `GET /remote-access` | | `{"p2s", "openvpn", "wireguard", "running", "local_fqdn", "wireguard_server"}`; `wireguard_server` is `{"public_key", "endpoint", "allowed_ips", "dns"}`: the router's WireGuard public key (`null` before setup made it), `<wan.local_fqdn>:<wireguard.listen_port>` (`null` without a name), the networks a peer reaches through the tunnel, the router's local CIDRs and the WireGuard network, and the addresses of the remote access clients' resolver (`[]` while it does not run) |
|
|
| `PUT /remote-access/<system>` | any of the settings of the section `p2s`, `openvpn` or `wireguard` | the section |
|
|
|
|
`running` tells for each system whether setup runs it. IKEv2 and OpenVPN run once enabled with an
|
|
address pool, `wan.local_fqdn` set and the router's VPN server certificate issued by the P2S CA;
|
|
WireGuard once enabled with an address. A system enabled without its address pool or address is
|
|
refused. A change is made, checked and applied as for `/site-to-site`. The router's firewall
|
|
follows the settings; a cloud firewall in front of the router, such as an Azure network security
|
|
group, must allow the OpenVPN and WireGuard ports itself.
|
|
|
|
### The /ca API
|
|
|
|
`/ca` manages the PKI (see Certificates) with the same token as `/users`, with the module
|
|
`vpn-router-ca` uses too, `/usr/lib/vpn-router/vpnrouter_pki.py`.
|
|
|
|
| Request | Body | Answer |
|
|
| --- | --- | --- |
|
|
| `GET /ca` | | `{"settings", "templates", "extended_key_usages", "status", "certificates"}` |
|
|
| `PUT /ca` | any of the settings of the `pki` section | `{"settings", "templates"}` |
|
|
| `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/<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 |
|
|
| `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"}` |
|
|
|
|
The answer of `GET /ca` holds:
|
|
|
|
- **`settings`:** the `pki` section.
|
|
- **`templates`:** every template, the built-in ones included, with all its options.
|
|
- **`extended_key_usages`:** every extended key usage templates may give, name -> `{"oid", "builtin"}`.
|
|
- **`status`:** `root` (`subject`, `expires`, `key`, whether the root key is on the router, `crl_next_update` and `crl_source`, `signed` or `uploaded`), `authorities`, one per issuing CA (`name`, `common_name`, `state`: `active`, `pending` or `missing`, `builtin`, `templates`, `acme`, `subject`, `expires`, `key`, `crl_next_update`, `acme_url`), `retired` and `warnings`.
|
|
- **`certificates`:** the certificates issued, newest first, each `{"serial", "ca", "kind", "template", "subject", "sans", "created", "expires", "revoked", "reason", "state"}`; `kind` is `ca`, `router`, `user` or `external`, and `state` `valid`, `expired` or `revoked`.
|
|
|
|
A change of the settings is made, checked and applied as for `/site-to-site`; an imported issuing
|
|
CA certificate and a revoked router certificate restart `vpn-router-setup` too. A failure is
|
|
`{"error": ...}` with 400 for an invalid request, 404 for something that does not exist, 409 when
|
|
the state does not allow it, as for a revoked certificate or an issuing CA that has its
|
|
certificate, and 500 otherwise. The root key never crosses the API; `vpn-router-ca` exports,
|
|
removes and imports it.
|
|
|
|
The service starts on its own, without waiting for `vpn-router-setup`, and keeps running. Every
|
|
30 seconds it reads `web.listen`, `web.port` and `web.enabled` from `vpn-router.json` and works
|
|
out the addresses of the interfaces `web.listen` names as `general.mode` directs; it listens on
|
|
each, moves when an address or the port changes, and stops listening while `web.enabled` is
|
|
`false` or no address is known. A change of `web.listen` takes effect within 30 seconds, or when
|
|
the service restarts. Each 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 and the external interface in the firewall.
|
|
|
|
## HTTPS
|
|
|
|
Caddy serves the status web service on `https.web_port` (7444 by
|
|
default), the Router Manager on `https.manager_port` (7443), and
|
|
while metrics are enabled the metrics service on `metrics.https_port` (9465), all for `wan.local_fqdn`, while `https.enabled` is `true` and `wan.local_fqdn` is set. It obtains
|
|
and renews the certificate from Let's Encrypt with the HTTP challenge, so `wan.local_fqdn` must
|
|
resolve to the router's public address and port 80 must be reachable from the internet; the
|
|
certificate and the ACME account are kept in `/var/lib/caddy`. Port 80 answers the challenge only;
|
|
it does not redirect to HTTPS. Caddy's default CAs are Let's Encrypt and ZeroSSL, which it takes
|
|
when Let's Encrypt refuses, such as for its rate limit of 5 certificates a week for the same name.
|
|
|
|
While `https.acme_staging` is `true`, Caddy's only CA is Let's Encrypt's staging environment, for
|
|
routers that are rebuilt often. Its rate limits are far higher, but no browser or client trusts
|
|
its certificates, so they are for tests only. The setting applies to every certificate Caddy
|
|
obtains from an ACME CA, those of the web sites with `tls` `acme` included.
|
|
|
|
```sh
|
|
vpn-config-edit set --restart https.acme_staging true
|
|
```
|
|
|
|
The Router Manager switches it on the Overview, and `PUT /https/settings` changes it (see The
|
|
/https/settings API).
|
|
|
|
`vpn-router-setup` writes Caddy's configuration to `/etc/caddy/vpn-router.Caddyfile` (root, group
|
|
`caddy`, the group Caddy runs as, mode 0640), which the
|
|
package's drop-in `/usr/lib/systemd/system/caddy.service.d/vpn-router.conf` makes `caddy.service`
|
|
read; `caddy.service` does not start before the file exists, and `/etc/caddy/Caddyfile` is not
|
|
used. Setup starts Caddy, reloads it when the file changes, and stops it and removes the file when
|
|
HTTPS is off and no web site is served (see Web sites). In the firewall it opens port 80 on the external interface and the HTTPS ports on the
|
|
internal and the external interface, and ports 80 and 443 on both when `pki.publish` has `http-80`
|
|
and `https-default`, on which Caddy serves only the CA files (see Certificates). Caddy calls both services on `127.0.0.1`, where they listen
|
|
whatever `web.listen` and the manager's `listen` say, and passes the client's address in
|
|
`X-Forwarded-For`, which the manager trusts from `127.0.0.1` and the web service by default, as
|
|
the address the call came in on; a `web.trusted_proxies` that is set must list `127.0.0.1`. The plain HTTP ports stay as
|
|
`web.listen` and the manager's `listen` set them. `docker.exposed_ports` may not take port 80 or
|
|
the HTTPS ports while `https.enabled` is `true`.
|
|
|
|
```bash
|
|
curl https://router.example.com:7444/status
|
|
```
|
|
|
|
### Installing Caddy
|
|
|
|
The package does not depend on Caddy; `vpn-router-setup` has it installed, while HTTPS is on,
|
|
from the source `https.caddy_source` names:
|
|
|
|
- `ubuntu`, the default: the `caddy` package of the Ubuntu archive.
|
|
- `upstream`: Caddy's own repository, `https://dl.cloudsmith.io/public/caddy/stable/deb/debian`,
|
|
whose source the package writes to `/etc/apt/sources.list.d/caddy-stable.sources` with its key,
|
|
downloaded over HTTPS, in `/etc/apt/keyrings/caddy-stable.asc`.
|
|
|
|
apt runs in `vpn-router-caddy.service`, which waits for dpkg's lock, so setup run by a package
|
|
installation does not wait for itself; the unit runs `vpn-router-caddy apply` and then setup
|
|
again, which serves with the new Caddy. `vpn-router-caddy status` shows the configured source, the
|
|
source in effect and the versions.
|
|
|
|
A router installed with `ubuntu` is switched to upstream's Caddy with:
|
|
|
|
```sh
|
|
vpn-router-caddy use-upstream
|
|
```
|
|
|
|
It sets `https.caddy_source` to `upstream`, configures the repository and replaces Caddy, printing
|
|
each step. Upstream versions sort above Ubuntu's, so once the repository is configured apt keeps
|
|
upstream's Caddy, and `ubuntu` has no effect; setup says so. Going back is a downgrade nothing
|
|
does on its own: remove the source and the key, run `apt-get update`, and install Ubuntu's version
|
|
with `apt-get install --allow-downgrades caddy=<version>`, the version `apt-cache policy caddy`
|
|
lists for the archive.
|
|
|
|
## OpenID Provider
|
|
|
|
While `oidc.enabled` is `true`, the status web service is an OpenID Provider (OpenID Connect
|
|
Core 1.0) for the users of the directory. Its issuer is the web service's HTTPS site,
|
|
`https://<wan.local_fqdn>:<https.web_port>`, without the port when it is 443; the web service
|
|
answers it only on `127.0.0.1`, where Caddy passes the requests, so the provider is never reached
|
|
over plain HTTP. The setting needs `web.enabled`, `https.enabled` and `wan.local_fqdn`.
|
|
|
|
```sh
|
|
vpn-config-edit set --restart oidc.enabled true
|
|
curl https://router.example.com:7444/.well-known/openid-configuration
|
|
```
|
|
|
|
| Endpoint | Purpose |
|
|
| --- | --- |
|
|
| `GET /.well-known/openid-configuration` | The discovery document |
|
|
| `GET /oidc/jwks` | The public key ID tokens and access tokens are signed with |
|
|
| `GET`, `POST /oidc/authorize` | The authorization code flow; the sign-in page while the browser has no session |
|
|
| `POST /oidc/token` | Exchanges a code, a refresh token, a device code or a client's credentials for tokens |
|
|
| `POST /oidc/device_authorization` | Begins the device authorization grant: a device code and a user code |
|
|
| `GET`, `POST /oidc/device` | The page where a user enters a device's user code, signs in and allows the device |
|
|
| `GET`, `POST /oidc/userinfo` | The claims of the user of an access token |
|
|
| `GET /oidc/logout` | Ends the session (RP-initiated logout) |
|
|
|
|
**Clients**
|
|
|
|
A client is a service account with a redirect URI or a grant. Its name is the `client_id` and its
|
|
password the `client_secret`, sent with HTTP Basic authentication (`client_secret_basic`) or in
|
|
the body of the token request (`client_secret_post`). The password is checked with a bind, so 5
|
|
wrong secrets lock the client for 15 minutes. A public client sends only its `client_id` (`none`).
|
|
Its settings are:
|
|
|
|
- **redirect URIs:** where the provider sends the browser back with a code; they give the client
|
|
the authorization code grant. A URI uses `https`, or `http` for `localhost`, `127.0.0.1` or
|
|
`::1`, and is matched exactly, but the port of an `http` one, which a native program chooses
|
|
when it starts (RFC 8252);
|
|
- **post-logout redirect URIs:** where `/oidc/logout` may send the browser afterwards;
|
|
- **allowed groups:** only their members may sign in to the client; without any, every user who
|
|
is not a service account may;
|
|
- **grants:** `client_credentials` and `device_code`, besides the authorization code;
|
|
- **public client:** the client has no secret, as a native program or a CLI, which cannot keep
|
|
one; it cannot have `client_credentials`;
|
|
- **client scopes:** the scopes of resources the client may ask for, `<resource>:<scope>`.
|
|
|
|
```sh
|
|
echo 'cl1ent-s3cret' | vpn-router-user add 3f2b8c1e-9a4d-4e6f-8b2a-1c5d7e9f0a3b --service \
|
|
--display-name Grafana --redirect-uri https://grafana.example.com/login/generic_oauth \
|
|
--post-logout-redirect-uri https://grafana.example.com/login --allowed-group ops
|
|
vpn-router-user update 3f2b8c1e-9a4d-4e6f-8b2a-1c5d7e9f0a3b --allowed-group ''
|
|
```
|
|
|
|
**Grants**
|
|
|
|
| Grant | Who signs in | Tokens |
|
|
| --- | --- | --- |
|
|
| `authorization_code` | A user, in the browser, with PKCE (`S256`) | Access, refresh and ID token |
|
|
| `urn:ietf:params:oauth:grant-type:device_code` | A user, on another device | Access, refresh and ID token |
|
|
| `client_credentials` | The service account itself, with its secret | Access token |
|
|
| `refresh_token` | | A new access and refresh token |
|
|
|
|
The implicit and hybrid flows, the password grant and dynamic registration are refused. A
|
|
registered client is trusted, so a user is not asked for consent, except for a device.
|
|
|
|
**Signing in**
|
|
|
|
- **The page:** a user signs in on the provider's page with the name, the password and, when
|
|
enrolled, the TOTP code, or with a passkey (see Passkeys).
|
|
- **Lockout and rate limit:** failed sign-ins count towards the lockout. `POST /oidc/login`,
|
|
`/oidc/device` and `/oidc/device_authorization` take at most 10 calls a minute from one client
|
|
address, as `/auth` does.
|
|
- **Who may not sign in:** a disabled, locked or removed user, a service account, and a user
|
|
outside the client's allowed groups. The codes and tokens already issued to such a user stop
|
|
working.
|
|
- **The session:** the sign-in starts a cookie of the issuer's `/oidc/` path, valid for 8 hours,
|
|
so later requests of any client go on without the page. `prompt=login` and `max_age` ask for a
|
|
new sign-in, and `prompt=none` answers `login_required` without a session.
|
|
|
|
**Device authorization (RFC 8628)**
|
|
|
|
A client with `device_code`, such as a CLI on a host without a browser, signs a user in like this:
|
|
|
|
1. It posts `client_id` (and its secret, unless it is public), `scope` and optionally `resource` to
|
|
`/oidc/device_authorization`. The answer holds `device_code`, `user_code` (8 letters, shown as
|
|
`XXXX-XXXX`), `verification_uri`, `verification_uri_complete`, `expires_in` (600 seconds) and
|
|
`interval` (5 seconds).
|
|
2. It shows the user the code and the URI, then polls `/oidc/token` with
|
|
`grant_type=urn:ietf:params:oauth:grant-type:device_code` and `device_code`. Until the user
|
|
decides, the answer is `authorization_pending`; a poll sooner than the interval answers
|
|
`slow_down` and adds 5 seconds to it.
|
|
3. The user opens the URI on any device, enters the code, signs in, and allows or denies the
|
|
client, the resource and the scopes shown. The next poll answers the tokens, or
|
|
`access_denied`; an expired code answers `expired_token`.
|
|
|
|
```sh
|
|
curl -d client_id=backup-cli -d 'scope=openid profile' \
|
|
https://router.example.com:7444/oidc/device_authorization
|
|
```
|
|
|
|
**Client credentials**
|
|
|
|
A confidential client with `client_credentials` gets an access token as its own service account,
|
|
without a user. The scopes are those of a resource, and the profile scopes give the service
|
|
account's claims; `openid` is refused, and no refresh token is issued.
|
|
|
|
```sh
|
|
curl -u backup-job:'j0b-s3cret' -d grant_type=client_credentials -d resource=s3 -d scope=s3:read \
|
|
https://router.example.com:7444/oidc/token
|
|
```
|
|
|
|
**Resources, roles and scopes**
|
|
|
|
Every service account is also a resource, an application that checks the router's tokens, as an
|
|
API registration of Entra ID:
|
|
|
|
- **Scopes:** the resource scopes the account defines, such as `read`, which clients ask for as
|
|
`<resource>:<scope>` (`s3:read`) when their client scopes allow them.
|
|
- **Roles:** named roles, such as `Reader`, each assigned to groups, users and service accounts;
|
|
`vpn-router-role` manages them.
|
|
|
|
```sh
|
|
echo 's3-s3cret' | vpn-router-user add s3 --service --resource-scope read --resource-scope write
|
|
vpn-router-role add s3 Reader --description 'Reads every bucket'
|
|
vpn-router-role member-add s3 Reader --group backup
|
|
vpn-router-user update backup-job --grant client_credentials --client-scope s3:read
|
|
```
|
|
|
|
A request is for a resource when it names it with `resource` (RFC 8707), its name or
|
|
`<issuer>/<name>`, or asks for its scopes; more than one resource, or one that is not a
|
|
resource, is `invalid_target`. A scope the client may not ask for is `invalid_scope`. Without a
|
|
resource, the tokens are for the client itself. The resource is kept with the code, the device code
|
|
and the refresh token, and a token request naming another one is `invalid_target`.
|
|
|
|
| Command | What it does |
|
|
| --- | --- |
|
|
| `vpn-router-role list [<resource>]` | The roles with the groups and accounts they are assigned to |
|
|
| `vpn-router-role add <resource> <role> [--description D]` | Add a role |
|
|
| `vpn-router-role remove <resource> <role>` | Remove a role |
|
|
| `vpn-router-role member-add <resource> <role> --group G\|--account A` | Assign the role |
|
|
| `vpn-router-role member-remove <resource> <role> --group G\|--account A` | Take the role away |
|
|
|
|
A scope a client may ask for cannot be removed from its resource, nor the resource while a client
|
|
may ask for its scopes. Removing a service account removes its roles and takes it out of the
|
|
others; removing or renaming a group does the same in the roles.
|
|
|
|
**Tokens**
|
|
|
|
| Scope | Claims |
|
|
| --- | --- |
|
|
| `openid` (required but by `client_credentials`, which refuses it) | `sub`, the `entryUUID` of the user's entry |
|
|
| `profile` | `preferred_username` (the user name), `name`, `given_name`, `family_name` |
|
|
| `email` | `email` |
|
|
| `groups` | `groups`, the names of the user's groups |
|
|
| `<resource>:<scope>` | None; the scope is in the access token's `scope` |
|
|
|
|
- **ID tokens:** RS256 JWTs for the client (`aud`), valid for 5 minutes, with the claims of the
|
|
scopes, `auth_time`, `amr` (`pwd`, `pwd` and `otp`, or `hwk` and `user` for a passkey), and
|
|
`roles`, the user's roles of the client itself. `/oidc/userinfo` answers the same claims.
|
|
- **Access tokens:** RS256 JWTs of RFC 9068 (`typ` `at+jwt`), valid for 5 minutes, which a
|
|
resource verifies with the JWKS. Their claims:
|
|
- `iss`, `sub`, `iat`, `exp` and `jti`;
|
|
- `aud`, the resource, or the client without one, and `client_id`, the client;
|
|
- `scope`, the scopes granted;
|
|
- `roles`, the roles of the resource the user or the service account holds, directly or by a
|
|
group;
|
|
- the claims of the scopes, and of a user `auth_time` and `amr`.
|
|
- **Roles are read at every issue,** refresh included, so a role taken away is gone from the next
|
|
token.
|
|
- **The key:** tokens are signed with an RSA key of 3072 bits that the web service makes on its
|
|
first run, `/etc/vpn-router/oidc.key` (root only), and named in the JWKS by its RFC 7638
|
|
thumbprint.
|
|
- **Lifetimes:** a code is valid for 1 minute and used once, a device code for 10 minutes, and a
|
|
refresh token for 8 hours and used once: each refresh answers a new one.
|
|
- **Storage and revocation:** the codes, device codes, tokens and sessions are kept, as their
|
|
SHA-256, in the database of the refresh tokens of `/auth`. Changing the password of a user or a
|
|
service account, disabling, unlocking or removing it ends its sessions and revokes its codes and
|
|
tokens.
|
|
|
|
## Web sites
|
|
|
|
While `sites.enabled` is `true`, Caddy serves static web sites from folders on the router, each
|
|
`/var/lib/vpn-router/sites/<name>`. The sites are rows of the database `vpn-router-sites`, which
|
|
the web service creates on its first run with its own role and the password in
|
|
`/etc/vpn-router/sites-database.secret`; they are managed with `vpn-router-site`, `/sites` and the
|
|
Router Manager, each change applied by restarting `vpn-router-setup`. A site has:
|
|
|
|
| Setting | Meaning |
|
|
| --- | --- |
|
|
| `name` | 1 to 31 lowercase letters, digits and hyphens, starting with a letter; also the name of its folder |
|
|
| `enabled` | Whether Caddy serves it |
|
|
| `hosts` | The host names (or IP addresses) Caddy answers on the port, the names TLS clients send as SNI; none answers any name |
|
|
| `port` | The TCP port, 443 by default |
|
|
| `listen` | `internal`, `external` (the default) or `both`: the interfaces whose firewall opens the port |
|
|
| `tls` | `none` (plain HTTP), `acme` (Let's Encrypt, the default), `ca` (certificates of an issuing CA of `pki.issuing_cas`, named by `ca`) or `file` (`certificate` and `private_key`, PEM) |
|
|
| `index_file` | The file served for a folder, `index.html` by default |
|
|
| `spa` | Whether a path without a file is served the site's `index_file`, for single-page applications |
|
|
| `editors` | The group whose members may change the site's files besides the admins |
|
|
| `dns` | Who serves the records of its host names: `user` (the default), a DNS provider outside the router, or `router`, the router's name server (see Name server) |
|
|
| `dns_address` | With `dns` `router`, the router's address in the records: `public` (the default, `wan.public_address`), `internal` or `external` |
|
|
|
|
A site may not take a port of the router's own services (SSH, `web.port`, the HTTPS ports, the
|
|
metrics ports, the Router Manager's, `sites.webdav_port`, 7446, 53, the bootstrap ports, a TCP
|
|
`openvpn.port` and `docker.exposed_ports`). It may use `wan.local_fqdn` as a host name, on a port
|
|
of its own, such as 443, which the router's services leave free; its record is then kept where the
|
|
domain of `wan.local_fqdn` is, so `dns` `router` takes neither it, nor wildcards or addresses, and
|
|
`public` needs `wan.public_address`. Enabled sites on one port need host
|
|
names of their own, the same `listen` and the same kind of TLS (plain HTTP or HTTPS). A site of
|
|
`acme` needs host names, without wildcards or addresses, that resolve to the router, and port 80,
|
|
which setup opens on the external interface. The certificate of `file` must be the key's and name
|
|
the hosts; it is written to `/etc/caddy/vpn-router-sites/<name>` (root, group `caddy`, 0640). An
|
|
issuing CA of `ca` is given to Caddy as the ACME servers' CAs are, in `/etc/caddy/vpn-router-pki`;
|
|
its certificates are trusted by clients that trust the router's root. Caddy runs while HTTPS is
|
|
ready or a site is served; while the sites cannot be read, setup leaves the Caddyfile as it is.
|
|
|
|
```sh
|
|
vpn-config-edit set --restart sites.enabled true
|
|
vpn-router-site add www --host www.example.com --host example.com --spa
|
|
vpn-router-site add intranet --host intranet.example.lan --port 8443 --listen internal --tls ca --ca web \
|
|
--editors web-team
|
|
vpn-router-site update www --no-enabled
|
|
vpn-router-site remove intranet --content
|
|
```
|
|
|
|
`vpn-router-site remove` keeps the folder without `--content`. Renaming a group renames it in the
|
|
sites; a group a site has cannot be removed.
|
|
|
|
### Site files
|
|
|
|
The files of a site are read and written through the web service (see The /sites API) by the
|
|
admins and the members of the site's `editors` group, and through WebDAV. An upload is at most
|
|
`sites.upload_max_mb` MiB (512 by default, at most 10240); a zip archive is extracted into a
|
|
folder, and one with an absolute path, a `..`, a link, more than 10000 members or more content than
|
|
the limit is refused before anything is written. Files are written with mode 0644 and folders with
|
|
0755, so Caddy reads them.
|
|
|
|
### WebDAV
|
|
|
|
While `sites.webdav` is `true` and HTTPS is ready, `vpn-router-setup` runs
|
|
`vpn-router-webdav.service`, `rclone serve webdav` on `127.0.0.1:7446`, which Caddy serves at
|
|
`https://<wan.local_fqdn>:<sites.webdav_port>` (7445 by default) and the firewall opens on both
|
|
interfaces. A user signs in with the name and password of the router's directory, followed by the
|
|
TOTP code for an enrolled user; an admin sees a folder for every site, any other user the folders of
|
|
the sites whose `editors` group the user is a member of, and no folder means no access. rclone writes
|
|
an upload to its cache in `/var/cache/vpn-router-webdav` and to the site's folder a few seconds
|
|
later. rclone is installed from the Ubuntu archive by `vpn-router-install@webdav.service` when it is
|
|
missing.
|
|
|
|
```sh
|
|
vpn-config-edit set --restart sites.webdav true
|
|
```
|
|
|
|
### Name server
|
|
|
|
While `dns.enabled` is `true`, `vpn-router-setup` runs PowerDNS (`pdns.service`) as the
|
|
primary name server of the router's domains, on port 53 of the addresses of `dns.listen`
|
|
(`internal`, `external`, the default, or `both`), which the firewall opens for UDP and TCP. The
|
|
domains and records are in the database `vpn-router-dns` (password in `/etc/vpn-router/dns-database.secret`),
|
|
read by PowerDNS's gpgsql backend; setup writes the settings
|
|
`/etc/powerdns/pdns.d/vpn-router.conf` (root, group `pdns`, 0640) and installs `pdns-server` and
|
|
`pdns-backend-pgsql` from the Ubuntu archive with `vpn-router-install@dns.service` when they are
|
|
missing, without the recommended bind backend.
|
|
|
|
| Setting | Meaning |
|
|
| --- | --- |
|
|
| `wan.local_fqdn` | The primary name server of every domain: the SOA's primary and the first NS |
|
|
| `dns.secondary` | The secondary name server of every domain, the second NS; the addresses it resolves to are notified of every change and may transfer the domains (AXFR) |
|
|
| `dns.ttl` | The TTL of the records and the SOA's negative TTL of a domain without its own, 60 to 86400 seconds, 300 by default |
|
|
|
|
A domain the router hosts, for example `example.com`, with `wan.local_fqdn` `router.other.net`,
|
|
`dns.secondary` `ns2.provider.net` and a site of `www.example.com` and `example.com` with `dns`
|
|
`router` and `dns_address` `public` (`wan.public_address` `203.0.113.10`):
|
|
|
|
```text
|
|
example.com. SOA router.other.net. hostmaster.example.com. <serial> 10800 3600 604800 300
|
|
example.com. NS router.other.net.
|
|
example.com. NS ns2.provider.net.
|
|
example.com. A 203.0.113.10
|
|
www.example.com. A 203.0.113.10
|
|
```
|
|
|
|
The domains are added with `/dns` or the Router Manager, each with:
|
|
|
|
- **TTL**: the TTL of its records, `dns.ttl` when not set;
|
|
- **Manual records** (A, AAAA, CNAME, TXT, MX, SRV, CAA), served too, but not those of a name and
|
|
type the domain's own records have.
|
|
|
|
The host names of an enabled site with `dns` `router` get an A or AAAA record of its `dns_address`
|
|
in the closest domain they are in; a site with `dns` `user` gets none, and a host name in no domain
|
|
of the router gets none either. `wan.local_fqdn` gets no record in the domains: its A record is kept
|
|
by the DNS of its own domain, wherever that is. A domain inside another domain of the router is
|
|
delegated from it with NS records. Setup writes the domains at each run, and after each change
|
|
through `/dns`; the SOA's serial grows only when the records change. While IKEv2, OpenVPN or
|
|
WireGuard is enabled, dnsmasq answers their clients on port 53 of the addresses of
|
|
`p2s.dns_listen`, by default the internal address, so `dns.listen` must not share an interface with
|
|
it. The parent zone of each domain must delegate it to `wan.local_fqdn` and `dns.secondary`, with
|
|
glue records for those of them inside the domain. While `dns.secondary` is not set, the domains have
|
|
`wan.local_fqdn` as their only name server, and setup warns.
|
|
|
|
```sh
|
|
vpn-config-edit set dns.secondary ns2.provider.net
|
|
vpn-config-edit set wan.public_address 203.0.113.10
|
|
vpn-config-edit set --restart dns.enabled true
|
|
```
|
|
|
|
## Metrics
|
|
|
|
While `metrics.enabled` is `true`, `vpn-router-setup` runs `vpn-router-metrics`, which every minute
|
|
takes a sample of the router with `psutil`:
|
|
|
|
- the CPU utilization;
|
|
- the memory used and in total;
|
|
- the free and the total space of `/`;
|
|
- the bytes received and sent per second on the external and the internal interface;
|
|
- the 1, 5 and 15 minute load averages;
|
|
- the VPN clients of each enabled VPN system: IKEv2 and OpenVPN clients, and
|
|
WireGuard peers with a handshake in the last 3 minutes;
|
|
- for each site-to-site connection that is set up: whether it is up (1 or 0), the bytes and the
|
|
packets per second received and sent through its child SAs since the previous sample, and the
|
|
seconds its IKE SA has been up. The child SAs' counters start again at a rekey; a counter lower
|
|
than in the previous sample counts from zero.
|
|
|
|
### History
|
|
|
|
The samples are kept in the router's PostgreSQL database, the one of the refresh tokens
|
|
(`web.database`), in four detail tables, one row per minute, and per interface, kind of client or
|
|
site-to-site connection, and each is summarized hourly, daily, monthly and yearly. A summary row
|
|
has the start of its period, in UTC, and the average and the maximum of each value; the average
|
|
of a connection's `up` is the part of the period it was up:
|
|
|
|
| Level | Tables | Kept |
|
|
|---|---|---|
|
|
| Details | `metrics_system`, `metrics_network`, `metrics_vpn`, `metrics_site_to_site` | `metrics.keep_details` |
|
|
| Hourly | `metrics_system_hourly`, `metrics_network_hourly`, `metrics_vpn_hourly`, `metrics_site_to_site_hourly` | `metrics.keep_hourly` |
|
|
| Daily | `metrics_system_daily`, `metrics_network_daily`, `metrics_vpn_daily`, `metrics_site_to_site_daily` | `metrics.keep_daily` |
|
|
| Monthly | `metrics_system_monthly`, `metrics_network_monthly`, `metrics_vpn_monthly`, `metrics_site_to_site_monthly` | `metrics.keep_monthly` |
|
|
| Yearly | `metrics_system_yearly`, `metrics_network_yearly`, `metrics_vpn_yearly`, `metrics_site_to_site_yearly` | `metrics.keep_yearly` |
|
|
|
|
`metrics_site_to_site` has the columns `connection`, `up`, `rx`, `tx`, `packets_rx`, `packets_tx`
|
|
and `uptime`.
|
|
|
|
How it works:
|
|
|
|
- **Summaries:** a period is summarized once it is complete, from the level below. A stopped service catches up on the periods it missed, as long as their rows are still kept.
|
|
- **Garbage collection:** runs on start and once a day. It deletes the rows older than their retention, a count and a unit such as `24 hours`, `3 months` or `10 years`.
|
|
- **History API:** `GET /metrics/history?range=hour|day|month|year` of the status web service, protected like `/config`, answers the views of the Router Manager's Metrics tab. Each point has the system values, `network` by interface, `clients` by kind and `site_to_site` by connection, each value as its `avg` and `max`:
|
|
|
|
| Range | Bucket | Source |
|
|
|---|---|---|
|
|
| `hour` | 1 minute | details |
|
|
| `day` | 10 minutes | details |
|
|
| `month` | 6 hours | hourly |
|
|
| `year` | 1 month | monthly |
|
|
|
|
- **Site-to-site availability:** `site_to_site_up` of the same answer divides the view into slots of its own, `slot`, and gives for each slot and connection the percentage of the samples in which the connection was up, 0 to 100, as `points` of `{"time", "connections": {name: percent}}`:
|
|
|
|
| Range | Slot | Source |
|
|
|---|---|---|
|
|
| `hour` | 5 minutes | details |
|
|
| `day` | 1 hour | details |
|
|
| `month` | 1 day | hourly |
|
|
| `year` | 1 month | monthly |
|
|
|
|
A sample is taken every minute, so an interruption shorter than a minute may be missed; a slot without samples has no value, and the year view, like the others of its level, has the complete months only.
|
|
|
|
### Scraping
|
|
|
|
`GET /metrics` of the metrics service serves the last sample in the Prometheus text format, written by `prometheus_client`.
|
|
|
|
- **Metric names:** the OpenTelemetry semantic convention names in their Prometheus form:
|
|
- `system_cpu_utilization`;
|
|
- `system_memory_usage_bytes` and `system_memory_limit_bytes`;
|
|
- `system_filesystem_usage_bytes` and `system_filesystem_utilization`;
|
|
- `system_network_io_bytes_total`;
|
|
- `system_cpu_load_average_1m`, `_5m` and `_15m`;
|
|
- `vpn_router_clients`, by `kind`: `p2s`, `openvpn` and `wireguard`;
|
|
- by `connection`, for each site-to-site connection: `vpn_router_site_to_site_up`, `vpn_router_site_to_site_uptime_seconds`, and, by `direction` too, `vpn_router_site_to_site_io_bytes_per_second` and `vpn_router_site_to_site_packets_per_second`. The rates are gauges, since the child SAs' counters start again at each rekey.
|
|
- **Listening:** on the interfaces `metrics.listen` names, `internal` by default, at `metrics.port`, and on `127.0.0.1`. While HTTPS is on, Caddy serves it at `metrics.https_port` for `wan.local_fqdn`. `vpn-router-setup` opens both ports on the internal and the external interface.
|
|
- **Authentication:** HTTP Basic as the service account `metrics.service_account` names, `metrics` by default. The password is checked with a bind to the user directory, so 5 failed attempts lock the account as for any user.
|
|
- **The account:** `vpn-router-setup` owns it while metrics are enabled.
|
|
- It adds the account with a random password, which it writes to `/etc/vpn-router/metrics.secret` (root only). Give that password to the collector.
|
|
- When the file is missing or no longer holds the account's password, setup sets a new random password and ends any lockout. Delete the file and restart `vpn-router-setup` to change the password.
|
|
- A disabled account is left as it is.
|
|
- A user of that name who is not a service account is a setup failure.
|
|
|
|
An OpenTelemetry Collector scrapes it with its `prometheus` receiver:
|
|
|
|
```yaml
|
|
receivers:
|
|
prometheus:
|
|
config:
|
|
scrape_configs:
|
|
- job_name: vpn-router
|
|
scrape_interval: 60s
|
|
scheme: https
|
|
basic_auth:
|
|
username: metrics
|
|
password_file: /etc/otelcol/vpn-router-metrics.secret # a copy of /etc/vpn-router/metrics.secret
|
|
static_configs:
|
|
- targets: ['router.example.com:9465']
|
|
```
|
|
|
|
### Pushing
|
|
|
|
When `metrics.otlp_endpoint` is set, every sample is also pushed there as an OTLP/HTTP JSON request.
|
|
|
|
- **Content:** the semantic convention names (`system.cpu.utilization`, `system.network.io` and so on), `vpn_router.clients`, the gauges `vpn_router.site_to_site.up`, `.uptime`, `.io` and `.packets` with the attribute `connection`, and the resource attributes `service.name` `vpn-router` and `host.name`.
|
|
- **Authentication:** HTTP Basic with `metrics.otlp_username` and `metrics.otlp_password` when they are set.
|
|
- **Failures:** a failed push is logged and not repeated; the next sample is pushed a minute later.
|
|
|
|
## Backups
|
|
|
|
While `backup.enabled` is `true`, `vpn-router-setup` runs `vpn-router-backup.timer` on `backup.schedule`. The timer starts `vpn-router-backup.service`, which runs `vpn-router-backup run`. The schedule is written to `/etc/systemd/system/vpn-router-backup.timer.d/schedule.conf`:
|
|
|
|
- `hourly` runs at the start of each hour;
|
|
- `daily` runs at midnight;
|
|
- `weekly` runs at midnight between Sunday and Monday.
|
|
|
|
A run missed while the router was off is made at the next boot.
|
|
|
|
### What a backup holds
|
|
|
|
A backup holds the state that `vpn-router-setup` cannot make again:
|
|
|
|
| Item | Content |
|
|
|---|---|
|
|
| `/etc/vpn-router` | `vpn-router.json`, the secrets, the Router Manager's settings and `rclone.conf` |
|
|
| `/etc/wireguard` | The WireGuard keys and the peers |
|
|
| `/etc/ufw`, `/etc/default/ufw` | The firewall rules |
|
|
| `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, 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` |
|
|
| `/var/lib/vpn-router/sites` | The folders of the web sites |
|
|
| `postgresql/vpn-router-sites.dump`, `postgresql/vpn-router-dns.dump` | The databases of the web sites and of the name server, once they exist |
|
|
| `manifest.json` | The host, the time, the version of the package, the schedule and the items |
|
|
|
|
The files and the directory are copied while the locks of the configuration and of the users are held, so no change of the API or the command-line tools falls between them; `pg_dump` reads a snapshot of its own. The copy is staged in `/var/lib/vpn-router/backup/staging`, which is removed after each run. The backup holds every secret of the router, so keep it as safe as the router itself.
|
|
|
|
### Local archives
|
|
|
|
With `backup.target` `local`, a backup is a tar archive compressed with zstd, `vpn-router-<host>-<UTC time>.tar.zst`, in `backup.path` (mode 0600, the folder 0700). The folder may be a mount, such as an NFS share or an Azure Files share, which exports the archives.
|
|
|
|
After each backup, the archives of this host that the retention settings no longer keep are deleted. The rules are those of Proxmox Backup Server's prune:
|
|
|
|
- The settings apply in order: `keep_hourly`, `keep_daily`, `keep_weekly` (ISO weeks, Monday to Sunday), `keep_monthly`, `keep_yearly`.
|
|
- Each keeps the newest archive of each of its last N periods that have archives, in local time.
|
|
- A period that has an archive a former setting kept is passed over.
|
|
- A setting finer than the schedule does not apply: `keep_hourly` only with `hourly`, `keep_daily` with `hourly` and `daily`. At least one setting that applies must be above 0.
|
|
|
|
### Cloud copy with rclone
|
|
|
|
With `backup.rclone_remote` set, each local backup is followed by an rclone run that copies the archives of this host to the remote. rclone comes from the `rclone` package, which the package suggests:
|
|
|
|
```sh
|
|
sudo apt-get install rclone
|
|
sudo rclone config --config /etc/vpn-router/rclone.conf
|
|
sudo chmod 0600 /etc/vpn-router/rclone.conf
|
|
sudo vpn-config-edit set backup.rclone_remote azure:backups/router
|
|
sudo systemctl restart vpn-router-setup
|
|
```
|
|
|
|
The two modes:
|
|
|
|
- `sync` (the default) makes the remote mirror the archives of this host, so an archive retention deletes is deleted remotely too.
|
|
- `copy` uploads new archives and deletes nothing, so retention in the cloud is left to its own rules, such as a lifecycle policy.
|
|
|
|
Other files in the remote path are left alone in both modes. A failed cloud copy leaves the local backup in place, and the run's result is `warning`.
|
|
|
|
### Proxmox Backup Server
|
|
|
|
With `backup.target` `pbs`, the staged copy is sent with `proxmox-backup-client` as the archive `router.pxar` of the snapshot `host/<host>/<time>`, in `backup.pbs_namespace` when it is set. The server keeps the snapshots by its own prune jobs; the router prunes nothing.
|
|
|
|
The static client comes from Proxmox's client repository, which the package suggests but does not set up. Following Proxmox's installation guide, as `/etc/apt/sources.list.d/pbs-client.sources`:
|
|
|
|
```text
|
|
Types: deb
|
|
URIs: http://download.proxmox.com/debian/pbs-client
|
|
Suites: trixie
|
|
Components: main
|
|
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg
|
|
```
|
|
|
|
Get the key `https://enterprise.proxmox.com/debian/proxmox-archive-keyring-trixie.gpg` into `/usr/share/keyrings/proxmox-archive-keyring.gpg`, check it against the SHA-256 the [installation guide](https://pbs.proxmox.com/docs/installation.html) gives, and install `proxmox-backup-client-static`. Until it is installed, a backup to PBS fails with a message that names the package; nothing else depends on it.
|
|
|
|
On the server, make an API token for the router and give the token itself the `DatastoreBackup` role on `/datastore/<store>`, or on the namespace path. `backup.pbs_repository` names the token, such as `backup@pbs!router@pbs.example.com:store`, and `backup.pbs_password` holds its secret. Set `backup.pbs_fingerprint` when the server's certificate is self-signed. With `backup.pbs_keyfile`, the snapshot is encrypted with a key of `proxmox-backup-client key create --kdf none <file>`. Keep a copy of that key away from the router, since a restore needs it.
|
|
|
|
### Running, listing and restoring
|
|
|
|
```sh
|
|
sudo vpn-router-backup run # a backup now, whether or not backups are enabled
|
|
sudo vpn-router-backup list # the local archives, or the snapshots on the server
|
|
sudo vpn-router-backup restore /var/backups/vpn-router/vpn-router-router-20261025T030000Z.tar.zst
|
|
sudo vpn-router-backup restore --pbs host/router/2026-10-25T03:00:00Z
|
|
```
|
|
|
|
Each run is recorded in `/var/lib/vpn-router/backup.json`, which `/backup` and the Router Manager show. Only one run or restore takes place at a time.
|
|
|
|
`restore` is only on the command line. It shows what the backup holds and asks for `yes`, which `--yes` gives. It then:
|
|
|
|
1. stops the web service, the metrics service, WebDAV, PowerDNS and slapd;
|
|
2. puts back the files;
|
|
3. loads the directory with `slapadd` into an empty `/var/lib/ldap/vpn-router`;
|
|
4. sets the database roles' passwords to the restored password files, and restores each database the backup holds with `pg_restore --clean`;
|
|
5. restarts `vpn-router-setup`, which regenerates everything else, followed by WireGuard, OpenVPN, strongSwan and the Router Manager, and reloads ufw.
|
|
|
|
What it replaces is kept in `/var/lib/vpn-router/backup/before-restore/<UTC time>`. To restore onto a new VM, install the package first and let `vpn-router-setup` run once. The restored configuration names the interfaces of the old VM, so change them with `vpn-config-edit` if they differ.
|
|
|
|
## Router Manager
|
|
|
|
The Router Manager is the package's web application: it shows the state of the router and manages
|
|
the users of its OpenVPN and IKEv2 access. Users sign in with their user name, password
|
|
and, when enrolled for one-time passwords, the code, or with a passkey they added on their
|
|
account page; only members of the router's group `admins`
|
|
can use the tabs, as the router decides. Every user has an account page at `/user`, which is the
|
|
only page of a user who is not a member of `admins`. Administrators switch between Router (`/`:
|
|
the Overview, Metrics, Logs, Backup and System tabs), Identities (`/user-manager`: the Users, Groups and Service
|
|
Accounts tabs), Connectivity (`/site-to-site-manager`: the Status, Network and Connections tabs), Remote
|
|
Access (`/remote-access-manager`: the IKEv2, OpenVPN and WireGuard tabs), Certificates
|
|
(`/ca-manager`), Web Sites (`/sites-manager`: the Sites, Files and DNS tabs) and
|
|
Docker (`/docker-manager`) with the page menu at the
|
|
right end of the header, a button naming the page shown. The button with the signed-in user's
|
|
name, left of Sign out, opens the account page, and the terminal icon left of it the Shell
|
|
(`/shell`); pressed on its page, each returns to the page it was opened from.
|
|
|
|
- **Help** (the question mark in the header between the theme switcher and the page menu, for every
|
|
signed-in user): a panel on the right, under
|
|
which the page moves left, with the help topic of the page and tab shown. The topics are the
|
|
Markdown files of `docs/` (`index.md` and one per page or tab), served by the manager at `/docs`
|
|
and rendered in the browser with marked and DOMPurify, in the style of github-markdown-css in
|
|
the light or dark scheme. A link to another topic opens it in the panel; Back returns to the
|
|
previous one and the house icon to `index.md`. Topics link to SVG and PNG pictures by paths
|
|
relative to their folder. The browser keeps whether the panel is open.
|
|
|
|
- **Settings** (the gear in the header, hidden on the sign-in and the account page): how often the Overview and the Metrics tab read the router
|
|
on their own, kept in the browser's localStorage, so each browser has its own. Each tab reads
|
|
manually, or every 5, 10 or 30 seconds, 1, 5 or 15 minutes; the Overview tab every 10 seconds and
|
|
the Metrics tab every minute by default. Manual reads when the tab opens and on Refresh. A change applies at once, also in the browser's other tabs; Defaults sets both back.
|
|
|
|
- **Account** (`/user`): the Account and the Passkeys panel side by side, with One-time passwords
|
|
and VPN profiles below. The user's name, kind, names, e-mail address, whether they are an
|
|
administrator and whether they are enrolled for one-time passwords; Change password in the
|
|
Account panel, which opens a dialog and signs the user out after the change; enrolling an authenticator app or replacing it; and the user's passkeys,
|
|
each with its label and when it was added, with Add a passkey and a remove icon. Each change but
|
|
the removal of a passkey asks for the current password and, for an enrolled user, a code not
|
|
already used to sign in. Removing the enrolment is left to an administrator. A VPN user downloads
|
|
the own OpenVPN profile (`.ovpn`) and the Apple VPN profile of the router's IKEv2 access
|
|
(`.mobileconfig`), each with the current password and the code when enrolled, the Apple profile
|
|
also with a password that protects the key in it.
|
|
|
|
- **Passkeys:** Sign in with a passkey on the sign-in page uses a passkey the browser or the
|
|
user's password manager offers for the router's `wan.local_fqdn`, without the user name, the
|
|
password or the code. Passkeys work on the Manager's HTTPS address on that name only, not on
|
|
another name or in development on `localhost`, and in browsers with the JSON methods of WebAuthn
|
|
Level 3. They sign in to the Manager, and open a shell on the Shell page in place of the password
|
|
and code.
|
|
|
|
- **Certificates** (`/ca-manager`, in the page menu of administrators): the router's two-level CA, from its `/ca`, in tabs:
|
|
- **Root:** the root CA's subject, expiry, whether its key is on the router, marked with a warning sign, or offline, and its CRL, with Download root CA, the root's certificate alone, and Upload CRL for a root CRL signed offline; the general information: where certificates and CRLs are published, the CA key type, the lifetimes, and the CRL validity; the certificates of the issuing CAs, each active one with a download icon for its CA bundle, and the retired ones.
|
|
- **Templates:** the certificate templates with their key, lifetime, subject, usages, alternative names, certificate policies and the CAs that issue them; Add template, Edit on each, Remove on one of the router's own and Reset on a changed built-in one, which brings back its defaults. A template's extended key usages are chosen from those of the next tab, and its certificate policies are OIDs typed in.
|
|
- **Extended key usages:** the usages templates may give, with their OIDs and the templates that give them; the built-in ones are shown and not changed, and custom ones are added with a name and an OID, and changed or removed.
|
|
- **P2S Issuing CA** and **S2S Issuing CA:** the CA's common name, state (active, or pending while it waits for the offline root), expiry, CRL and templates, and the certificates it issued. An active CA gives its CA bundle, the issuing CA followed by the root, with Download CA bundle; a pending CA gives its CSR with the openssl extensions to sign it with, and takes the certificate signed by the root.
|
|
- **Issuing CAs:** the general-purpose CAs with their state, expiry, CRL, templates and ACME directory URL; Add issuing CA, and Edit and Remove on each: its name, fixed once it exists, its common name, the templates it issues and its ACME server; an active CA's download icon gives its CA bundle. A removed CA is retired by the router, which revokes its certificate on the root CRL. Below, their certificates, filtered by CA. - **Certificates:** each with its kind, template, names, expiry and state; a certificate of another system or of the router is revoked after a confirmation with a reason.
|
|
- **Issue** (above the tabs): from a CSR, saving the certificate with its chain, or with a key the router makes, saving a PFX file protected by a password; the router keeps no copy of the key.
|
|
- **Settings** (above the tabs): the names, lifetimes, key type, CRL validity and publishing endpoints.
|
|
- **Warnings** (above the tabs): what the router says needs attention, such as a CRL about to expire.
|
|
|
|
The root key is exported, removed and imported on the router with `vpn-router-ca`, never through the Manager.
|
|
|
|
- **Overview**: the router's overall status, ok or degraded, on the title bar of the Status panel;
|
|
at the top of that panel, a table of `vpn-router`
|
|
with the installed version and, when the repository has a newer one, that version; Check update
|
|
is always in its top right corner, and Upgrade is added beside it when an update is found. Then
|
|
IKEv2, OpenVPN and WireGuard, read from the router's authenticated `/status/full`, and from its `/config` the status web service and its port, whether HTTPS is on and for which name, with a chip while Caddy takes Let's Encrypt's staging CA and a settings button that switches it (`https.acme_staging`, through the router's `/https/settings`), and Caddy, its HTTPS proxy: its version, whether it comes from Ubuntu or upstream (switched on the router with `vpn-router-caddy use-upstream`) with the configured `https.caddy_source` when that differs, and the state of `caddy.service`. The Services panel shows the state of each of the router's
|
|
systemd services and of Caddy, with a chip on its title bar telling whether the router's services are active. The Addresses panel links, opening in a new tab, to the status page over HTTPS and over plain HTTP on the addresses `web.listen` names, and to the Router Manager over HTTPS, and shows the issuer and the discovery URL of the router's OpenID Provider, each with an icon that copies it, or off while the provider is disabled. Further panels
|
|
show each PostgreSQL cluster of the
|
|
router with its main settings and its databases with their sizes, and one Configuration panel
|
|
with a group of rows under a heading for each section the other pages do not show, the status web service (`web`) and HTTPS (`https`) among them, read
|
|
every 10 seconds by default (see Settings). Nothing is read, and Refresh is off, while a check
|
|
for updates, an upgrade or a reboot runs; the tab reads again when it ends.
|
|
- **Panel controls:** each panel folds by its title bar, and every panel but Status hides by its [X]; Panels shows them again.
|
|
- **Edit layout:** moves panels by dragging and switches each between one column and both.
|
|
- **Layout storage:** kept in the browser.
|
|
- **Metrics**: charts of the router's CPU, memory, free disk space in bytes and in percent, the
|
|
bytes per second of each interface, the VPN clients and the load averages, and, with a line per
|
|
site-to-site connection, Site-to-site up, Site-to-site traffic and Site-to-site packets. They
|
|
cover the last hour, day, month or year, each bucket as its average or its maximum. Site-to-site
|
|
up is the percentage of time each connection was up, 0 to 100, in slots of 5 minutes for an
|
|
hour, 1 hour for a day, 1 day for a month and 1 month for a year, as the router computes it;
|
|
Average and Maximum do not change it. The charts are read every minute by default (see
|
|
Settings) from the router's `/metrics/history`. The charts fold, hide, move and change width as the Router
|
|
tab's panels do, with a layout of their own. A click on a line's legend item hides or shows the
|
|
line, for example the 1 and 5 minute load averages to see the 15 minute one; the browser keeps
|
|
the hidden lines of each chart. Settings edits the router's `metrics` section through its
|
|
`/metrics/settings`: whether the metrics service runs, the interfaces and the ports of
|
|
`/metrics`, its service account, the OTLP endpoint with its user name and password, and how long
|
|
each level of the history is kept. An empty password keeps the current one.
|
|
- **Logs**: the last lines of the router's logs, from its `/logs`. The Journal sub-tab shows the
|
|
systemd journal of a unit the router names, or of all units, and the Files sub-tab a log file of
|
|
the router's `web.log_files`, set on the router with `vpn-config-edit`. 100, 200, 500 or 1000
|
|
lines, up to the router's limit of one read, are read when the unit, the file or the number
|
|
changes and on Refresh. While Follow is on, the log is read every 2 seconds and kept scrolled to
|
|
its last line, as `tail -f` shows it.
|
|
- **Users**: the users with their names and e-mail addresses, without the service accounts, all
|
|
of them, the VPN users or the Manager-only accounts, searched by user name, names and e-mail
|
|
address, with adding (user name, first
|
|
name, last name, e-mail address and password, or, with Manager access only, a Manager-only
|
|
account with the user name, the password and an optional e-mail address: no certificate and no
|
|
VPN access), editing the names and the e-mail address of VPN users and the e-mail address of
|
|
Manager-only accounts,
|
|
setting the password, disabling, enabling, unlocking after failed logins, removing and
|
|
downloading the OpenVPN client profile, offered only while the router runs OpenVPN, and the Apple VPN profile
|
|
(`.mobileconfig`, IKEv2), offered only while the router runs IKEv2 access, whose dialog takes the
|
|
password that protects the user's key in it, and whether each is enrolled for TOTP one-time passwords. Enrolling shows a new secret as a QR
|
|
code and as text for an authenticator app, and stores it once a code of the app is entered;
|
|
the enrolment can be removed. The Passkeys column shows how many passkeys each user has, and
|
|
the remove icon removes all of them after a confirmation. The Console column shows the console
|
|
login of each member of `admins`: off, the user's own LDAP POSIX account with its uid, or the
|
|
local account it runs as, marked as refused while the user is not enrolled for TOTP; root
|
|
switches it on the router with `vpn-router-user console`, the Manager does not.
|
|
- **Groups**: the groups with their descriptions and members, searched by name, description and
|
|
member, with adding a group, editing its
|
|
name, description and members in one dialog, and removing it. A group has at least one member;
|
|
the members offered are the users and the service accounts but the internal ones.
|
|
- **Service Accounts**: the accounts other systems sign in with to use the router's services, such
|
|
as an OpenTelemetry Collector reading `/metrics`, from the router's `/service-accounts`; they
|
|
cannot sign in to the Manager. Each is shown with its display name, description, kind, e-mail
|
|
address, OpenID Provider settings and state, searched by name, display name, description
|
|
and e-mail address, with adding one (name, a new GUID unless a user name replaces it, display
|
|
name, description, an optional e-mail address, the OpenID Provider settings and a password),
|
|
editing the display name, the description, the e-mail address and the OpenID Provider settings,
|
|
setting the password, disabling, enabling, unlocking after failed logins and removing after a
|
|
confirmation. An account with redirect URIs or grants is a client of the router's OpenID
|
|
Provider, its name the client ID and its password the secret; its settings are the redirect URIs
|
|
and the post-logout redirect URIs, one per line, the allowed groups, chosen from the router's
|
|
groups, whose members alone may sign in to it, the client credentials and device authorization
|
|
grants, whether it is a public client without a secret, and the scopes of resources it may ask
|
|
for. Every account is also a resource of the provider, with its scopes and its roles, each
|
|
assigned to groups, users and service accounts, which the tokens for it carry. While the
|
|
provider is enabled, the title of the list has the number of its clients.
|
|
Internal accounts, which the router creates and manages for its own services with all their
|
|
properties, display name and description included (at present the metrics service's), are
|
|
marked Internal and only listed: the router refuses every change of them through its API, so
|
|
they have no actions here.
|
|
- **Connectivity**, Connections tab: the router's IPsec site-to-site connections with their remote peer, remote
|
|
identity, remote and local CIDRs, local identity mode, authentication (whether the pre-shared key
|
|
is set, read from a file or missing, or, with certificates, whether the peer's CA is the router's
|
|
site-to-site CA or a remote CA) and whether the tunnel is established; with adding a connection,
|
|
editing it and removing it after a confirmation. With a pre-shared key, the key is typed as a
|
|
password, required while the connection has none and kept when left empty, and is never shown;
|
|
with certificates, the PEM certificate of a remote CA is optional. The router applies a change in the
|
|
background by running its setup again; the list is read again after each change. The
|
|
connection the router was installed with is `remote-site`; Cloud Manager names the one it
|
|
deploys with `router_site_to_site_name`, `remote-site` by default.
|
|
- **Connectivity**, Status tab: each connection the router set up, with the local and the
|
|
remote identity and the subnets it resolved, and how long the connection has been up, and their
|
|
IPsec security associations, child SAs, connections and connection children, from the router's
|
|
`/config`. The other IPsec connections, IKEv2 access, are on the IKEv2 tab of Remote Access.
|
|
- **Connectivity**, Network tab: the external and the internal interface with their addresses, the
|
|
internal gateway and the local CIDRs, and the IPv4 routes, from the router's `/config`; the panels
|
|
fold, hide, move and change width, and the browser keeps their layout.
|
|
- **Remote Access** (`/remote-access-manager`): a tab each for IKEv2,
|
|
OpenVPN and WireGuard access, from the router's `/remote-access` and `/status/full`: the
|
|
settings, whether the router runs the system, why an enabled one does not run yet, and the
|
|
connected clients; for IKEv2 the IPsec security associations, child SAs, connections and
|
|
connection children of IKEv2 access, all but the site-to-site ones, from the router's `/config`;
|
|
for WireGuard each connection with its user, found by the peer's key (a short
|
|
key for a peer added to `wg0.conf` by hand), its IPs, endpoint, handshake and traffic, from the
|
|
router's `/config` and `/users`. Edit enables or
|
|
disables the system and sets its address pool, for WireGuard the address of `wg0`, for IKEv2 the
|
|
DNS domains its clients resolve with the router's DNS, one per line, and the port, with OpenVPN's
|
|
protocol; the router applies a change in the background. The WireGuard tab also
|
|
lists the peers of the VPN users, each a device with its user, public key and IPs: a peer is
|
|
removed with its button after a confirmation, and added with Add peer, whose dialog takes the
|
|
user, the device's public key and the IPs, which start as the next free address of the `wg0`
|
|
network. The router keeps the peers with their users and checks that keys are unique and
|
|
IPs do not overlap. Its Server section shows the router's public key, with a copy button, the
|
|
endpoint and the allowed IPs a device needs; Show config opens a device's configuration, ready to
|
|
copy, with placeholders for the device's private key and IPs and, while it runs, the router's
|
|
resolver as the device's DNS server. The eye of a peer opens its device's
|
|
configuration the same way, with the peer's IPs as its address. In both dialogs a switch leaves
|
|
out the `[Interface]` and `PrivateKey` lines, which WireGuard for Windows fills in for a new empty
|
|
tunnel, so the rest pastes below them; the browser keeps its position with its other settings.
|
|
Each peer's configuration downloads as
|
|
`<user>-<n>.conf` with them filled in and a placeholder for the device's private key, which the
|
|
router never has. IKEv2 and OpenVPN also need `wan.local_fqdn` and the
|
|
router's VPN server certificate. A cloud firewall in front of the router, such as an Azure
|
|
network security group, must allow the OpenVPN and WireGuard ports itself.
|
|
- **Web Sites** (`/sites-manager`): the static web sites the router's Caddy serves, from its
|
|
`/sites` and `/dns`, in tabs:
|
|
- **Sites:** the Settings panel shows whether the sites are served, the largest upload and
|
|
WebDAV with its URL and a copy icon, edited with Settings. The list shows each site with its
|
|
URLs, linked, its port, the interfaces whose firewall opens it, its TLS (none, Let's Encrypt,
|
|
an issuing CA of the router or a certificate and key) and its editors group, searched by name,
|
|
host name and group; Add and Edit take the name, fixed once added, the host names, one per
|
|
line, the port, the interfaces, the TLS with the issuing CA or the PEM certificate chain and
|
|
key, pasted or read from files, the key kept when left empty, the index file, the single-page
|
|
fallback and the editors group. Enable, Disable and Remove, which removes the site's folder too
|
|
when asked; the folder icon opens the site on the Files tab.
|
|
- **Files:** the files of the chosen site, a folder at a time, with the path linked up to the
|
|
site's folder. Upload sends chosen files, or files dropped on the list, into the folder shown,
|
|
with a progress bar; while Extract zip archives is on, a `.zip` file is extracted there
|
|
instead. New folder, Rename, Remove and Download. A file larger than the router's largest
|
|
upload is not sent.
|
|
- **DNS:** the router's primary name server: its settings with whether PowerDNS is installed
|
|
and runs, the router's FQDN as the primary name server, edited with Settings (enabled,
|
|
secondary name server, public IP address, interfaces, TTL); the domains, added with Add
|
|
domain (domain, TTL), edited and removed with their icons; and for the selected domain the
|
|
records it serves, and its manual records, added with Add record (name, type, content and
|
|
TTL) and removed with their icon.
|
|
- **Docker** (`/docker-manager`): whether Docker is installed, the state of `docker.service` and of
|
|
the router's Docker rules, the ports exposed on the external interface and the rules of the
|
|
`VPN-ROUTER-DOCKER` chain, from the router's `/status/full` and `/config`.
|
|
- **Shell** (`/shell`, the terminal icon in the header): a login shell on the router in xterm.js, the terminal of VS Code, as the
|
|
signed-in user's console account, the user's own LDAP POSIX account or a local account. Opening
|
|
it asks for the password and the code again, or Open with a passkey takes one of the user's
|
|
passkeys, unlocked with the device's PIN or biometrics, unless the user used a shell within the
|
|
router's `web.shell_auth_timeout` minutes. A user without console login sees how root switches it on,
|
|
and one not enrolled for one-time passwords that the shell needs the enrolment. The router offers the shell only while its `web.shell` is `true`, set
|
|
on the router with `vpn-config-edit`, and closes it after `web.shell_idle_minutes` without a
|
|
keystroke; the page then shows the reason and keeps the terminal readable.
|
|
- **Editor:** `vre FILE...` (`vpn-router-editor`) in the shell opens the files in a Monaco editor on the page, one tab per file, and returns at once. The router names each file in a text message of the shell's WebSocket, and the page reads and saves it through `/api/shell/editor/<session>/<id>`, as the console account. Saves have a body limit of 2 MiB plus 64 KiB, for a file of up to 1 MiB escaped in JSON; other calls keep 64 KiB.
|
|
- **Editor icon:** while a file is open, an icon left of the shell icon switches the page between the terminal and the editor, and the shell keeps running. A button left of Save shows or hides the minimap, remembered by the browser. Save or Ctrl+S writes the file; a file changed on the router since it was read offers Reload or Overwrite. Closing a tab with unsaved changes asks first, and the icon goes when the last tab closes or the shell ends.
|
|
- **Loading:** Monaco is bundled with the page, without its language services, and loaded only when the first file opens.
|
|
- **Backup**: the router's backups, from its `/backup`:
|
|
- **Settings:** whether scheduled backups run, the schedule (hourly, daily or weekly), the target and whether the metrics database is included. For a local folder it also shows the folder, the archives kept and the cloud copy with rclone; for a Proxmox Backup Server, the repository, whether the token secret is set, the fingerprint, the namespace and the encryption key file.
|
|
- **Last run:** its result, times, archive or snapshot, size, cloud copy and error, and the next run.
|
|
- **Local archives:** with their times and sizes, searched by name and time, and shown 10, 25, 50 or 100 at a time.
|
|
- **Edit:** changes the settings in one dialog. Only the retention settings that apply to the schedule are offered. The rclone remote is chosen from the remotes of the router's `/etc/vpn-router/rclone.conf`, with a path and the mode, sync or copy. The token secret is typed as a password and kept when left empty. The router applies a change in the background.
|
|
- **Run now:** starts a backup, and the tab reads every 3 seconds until it ends.
|
|
- **Warnings:** shown when the PBS client or rclone the settings need is not installed on the router.
|
|
|
|
Restoring is done on the router with `vpn-router-backup restore`. The Overview tab has a Backup panel with the result of the last run, the next run and Run now.
|
|
- **System**: the router's operating system. The System updates panel lists the packages `apt-get upgrade` would upgrade, each with its installed and its new version, from `/api/system-updates`; Check update is always in its top right corner, and Upgrade is added beside it when a package can be upgraded. Upgrade runs `apt-get upgrade -y` after a confirmation and shows its output in a dialog, as Upgrade on the Overview tab does, with Reload page and Reboot when they apply.
|
|
|
|
### How it works
|
|
|
|
`vpn-router-manager.service` runs `/usr/lib/vpn-router/manager`, a Python server that
|
|
calls the router with `python3-requests` and keeps sessions in PostgreSQL with `python3-psycopg`.
|
|
It serves the application from `/usr/share/vpn-router/manager/web`.
|
|
|
|
- `POST /api/session` with `{"name", "password", "code"}` signs in through the router's `/auth`,
|
|
passing the browser's address in `X-Forwarded-For`; on success the manager keeps the user's
|
|
access and refresh tokens in the table `sessions` and sets the cookie
|
|
`vpn_router_manager_session` (`HttpOnly`, `SameSite=Strict`), which holds a random id of which
|
|
only the SHA-256 is stored. `GET /api/session` answers `{"name"}` of the signed-in user, and
|
|
`DELETE /api/session` signs out and revokes the refresh token.
|
|
- Every other call to `/api/<path>` needs a session and is passed to the router's status web
|
|
service at `/<path>` with the session's access token, renewed through `/auth/refresh` when it
|
|
expires within 30 seconds. The method, the body and the router's answer pass unchanged. A
|
|
session ends when the router refuses to renew it; sessions are kept across restarts.
|
|
- `GET`, `HEAD` and `PUT` of a web site's files, `/api/sites/<name>/files/<path>`, pass streamed:
|
|
the body of a `PUT` goes to the router as it arrives, with its `Content-Type` and
|
|
`Content-Length`, which it needs, and without the limit of 65536 bytes, and the router's answer
|
|
comes back with its `Content-Type`, `Content-Length` and `Content-Disposition`, so a download is
|
|
not held in memory either. The router's `sites.upload_max_mb` limits an upload.
|
|
- `GET /docs/<file>`, with a session, gives a file of the help from
|
|
`/usr/share/vpn-router/manager/docs`, which the package installs from the repository's `docs/`:
|
|
Markdown (`text/markdown`), SVG or PNG; any other file, or a path outside the folder, is 404.
|
|
The answers carry `X-Content-Type-Options: nosniff` and a `Content-Security-Policy` that keeps
|
|
an SVG opened on its own from running scripts.
|
|
- A WebSocket handshake on `/api/shell`, with a session and an `Origin` of the page's own host,
|
|
is passed to the router's `/shell` with the session's access token; after both handshakes the
|
|
manager relays the WebSocket's bytes both ways until either side closes. `POST /api/shell`, which
|
|
gives the ticket the handshake needs, and `POST /api/shell/passkey`, which begins the passkey
|
|
proof it may take, pass as every other call does.
|
|
- `/api/info` gives the version of the package and the URL of the router's status page, without a
|
|
session. On the router, while `router_url` and `terraform_dir` are not set and the status web
|
|
service listens on the address the call came in on, the URL has the name or address the browser
|
|
used and `web.port`, for example `http://router.example.com:7081`, and for a page opened over
|
|
the router's HTTPS proxy `https` and `https.web_port`, for example
|
|
`https://router.example.com:7444`; otherwise it is `router_url`.
|
|
- `/api/updates`, with a session, gives the installed and the candidate version of `vpn-router`
|
|
from `apt-cache policy`, that is from the repository apt is configured with,
|
|
as of apt's last update of its package lists (`/var/lib/apt/periodic/update-success-stamp`,
|
|
answered as `checked`), whether an update is available, and as `checking` whether a check for
|
|
updates runs. The Overview tab shows it at the top of its Status panel. Where apt cannot be asked, such as
|
|
on a workstation, it answers `"available": false`.
|
|
- `POST /api/updates`, by a member of `admins`, checks for updates: it runs `apt-get update` in the
|
|
transient unit `vpn-router-manager-check` outside the manager's unit, waiting up to 5 minutes for
|
|
apt's lock, and touches `update-success-stamp` when it succeeds. The page's Check update button
|
|
starts it and reads the versions again until it ends.
|
|
It answers 202 when started, 409 while a check, an upgrade or a reboot runs, 403 for a user who
|
|
is not an admin, and 503 where apt is missing.
|
|
- `/api/system-updates`, with a session, gives the packages `apt-get upgrade` would upgrade, from
|
|
its simulation (`apt-get -s upgrade`), each with its installed and its candidate version, in the
|
|
shape of `/api/updates`. The System tab shows it; Check update there is `POST /api/updates`.
|
|
- `POST /api/upgrade`, by a member of `admins` (the router's `/account` decides), upgrades
|
|
`vpn-router`; the page's Upgrade button starts it.
|
|
- **Where it runs:** the helper `/usr/lib/vpn-router/upgrade`, in the transient unit `vpn-router-manager-upgrade` (`systemd-run`) outside the manager's unit. The manager's own restart and the router web service's restart during the upgrade do not end it.
|
|
- **What it does:** first `dpkg --configure -a`, then `apt-get install --only-upgrade`. It asks nothing, keeps changed configuration files, and waits up to 5 minutes for the dpkg lock.
|
|
- **Record:** the state goes to `/var/lib/vpn-router/upgrade.json` and the output to `upgrade.log`.
|
|
- **Answers:** 202 when started, 409 while an upgrade or a reboot runs, 403 for a user who is not an admin, 503 where apt is missing.
|
|
- `GET /api/upgrade`, with a session, gives that state and the last 200 lines of the output.
|
|
- **States:** `idle`, `running`, `succeeded`, `failed`, or `interrupted` when the upgrade ended without recording a result.
|
|
- **Reboot information:** whether `/var/run/reboot-required` exists, the packages that ask for it, and whether a reboot is about to happen.
|
|
- `POST /api/system-upgrade`, by a member of `admins`, upgrades the system's packages, and `GET /api/system-upgrade` gives its state, as `/api/upgrade` does for `vpn-router`.
|
|
- **What it does:** the helper with `--system`, in the transient unit `vpn-router-manager-system-upgrade`: first `dpkg --configure -a`, then `apt-get upgrade -y`, with the same options.
|
|
- **Record:** `/var/lib/vpn-router/system-upgrade.json` and `system-upgrade.log`.
|
|
- **Answers:** as those of `/api/upgrade`; only one of the two upgrades runs at a time, and either answers 409 while the other runs.
|
|
- `POST /api/reboot`, by a member of `admins`, reboots the router 5 seconds later (the transient timer `vpn-router-manager-reboot`), and answers 409 while an upgrade runs. The page offers it when a reboot is required.
|
|
|
|
| Answer | When |
|
|
|--------|------|
|
|
| 401 | Not signed in, or the session has ended |
|
|
| 403 | The router refuses the user, who is not a member of `admins` |
|
|
| 502 | The router cannot be reached |
|
|
| 503 | No router or no session database is configured, or the database fails |
|
|
| 413 | The request body is larger than 65536 bytes, but for the upload of a site's file |
|
|
| 411 | The upload of a site's file has no `Content-Length` |
|
|
|
|
The router's rate limit of sign-ins counts the browser's address when the manager's address is
|
|
in the router's `web.trusted_proxies`; by default that is the router's address the call came in
|
|
on, which is the address a manager on the router calls from, so it needs nothing more.
|
|
|
|
On the router, the router's Caddy serves the manager over HTTPS on `https.manager_port` of
|
|
`vpn-router.json` (7443 by default) for `wan.local_fqdn`, with a certificate from Let's Encrypt,
|
|
and calls it on `127.0.0.1`, where the manager also listens. For a call from `127.0.0.1` with
|
|
`X-Forwarded-For`, the manager takes the browser's address from the last entry of that header.
|
|
|
|
The service needs nothing else to start and keeps running. Every 30 seconds it works out where to
|
|
listen and starts, moves or stops a server on each address to match, so a change of `listen`,
|
|
`bind` or `port` takes effect within 30 seconds, or when the service restarts. Each request
|
|
reads the settings again.
|
|
|
|
Before it listens, it creates its sessions database on the router when it is missing and connects
|
|
to the sessions database, retrying every 15 seconds and logging each failure, for example while
|
|
PostgreSQL is not running yet.
|
|
|
|
### Settings
|
|
|
|
`/etc/vpn-router/manager.json` (all optional; see
|
|
`/usr/share/doc/vpn-router/manager.json.example`):
|
|
|
|
| Setting | Meaning | Default |
|
|
|---------|---------|---------|
|
|
| `router_url` | The router's status web service | `http://<first address of the web service>:<web.port>`, the internal address when it listens on both |
|
|
| `database` | A PostgreSQL connection string for the sessions | The `vpn-router-manager` database on the router |
|
|
| `listen` | The interfaces to listen on when `bind` is not set: `internal`, `external` or `both`, and always `127.0.0.1` | `both` |
|
|
| `bind` | The one address to listen on; the router's HTTPS proxy reaches the manager only when it is `127.0.0.1` | none, `listen` applies |
|
|
| `port` | The port to listen on, HTTP | `7080` |
|
|
| `terraform_dir` | A Terraform root directory whose outputs give `router_url` when it is not set; `dev.sh` sets it | none |
|
|
| `iac_tool` | The tool that reads the outputs of `terraform_dir`: `terraform` or `tofu` | `terraform` |
|
|
|
|
From the outputs of `<iac_tool> -chdir=<terraform_dir> output -json`, read every 30 seconds at
|
|
most, `router_url` is the output `router_web_url`, the status web service's URL on the router's
|
|
private address with its port. It needs that tool, the
|
|
one the root directory was initialized with, and access to the state.
|
|
|
|
The addresses
|
|
of the interfaces `listen` names are worked out as `general.mode` directs, as the status web
|
|
service does, and `vpn-router-setup` opens `port` on the internal and the external interface;
|
|
restart `vpn-router-setup` after changing `port`. When `database` is not set, the manager creates the role and the database
|
|
`vpn-router-manager` on the router's PostgreSQL on its first run, with a password in
|
|
`/etc/vpn-router/manager-database.secret` (root only), and reaches it over TCP on `localhost`.
|
|
Make `manager.json` readable by root only when its `database` holds a password.
|
|
|
|
#### First administrator
|
|
|
|
The package asks two debconf questions on a fresh installation, `vpn-router/admin_username`
|
|
and `vpn-router/admin_password`. With both answered, it writes them to
|
|
`/etc/vpn-router/manager-admin.json` (root only) and clears them from debconf;
|
|
`vpn-router-setup` creates that user as a Manager-only account in the group `admins`, creating
|
|
the group when missing, and removes the file. Without a user name no account and no group is
|
|
created. They can be preseeded:
|
|
|
|
```sh
|
|
echo 'vpn-router vpn-router/admin_username string admin' | debconf-set-selections
|
|
echo 'vpn-router vpn-router/admin_password password s3cr3t-pass' | debconf-set-selections
|
|
```
|
|
|
|
### Running the application on a workstation
|
|
|
|
`dev.sh` runs the manager and the Vite development server, which reloads on changes, on the
|
|
workstation; the application is at http://localhost:5180. Its first argument is the
|
|
`config.json` of the platform that deployed the router. Its `module_root`, relative to the
|
|
directory of that file, is the Terraform root module whose outputs give the router's address.
|
|
The second argument, optional, is the connection string of a PostgreSQL database for the
|
|
sessions; without it the sessions are kept in the database `PGDATABASE` names, or the one named
|
|
after the login, and libpq takes the server, port, user, password and SSL mode from the `PG*`
|
|
variables of the environment, such as those of the platform's `pg.env.sh`. `dev.sh` writes them
|
|
as `terraform_dir` and `database`, with the platform's `iac_tool` and `bind` `127.0.0.1`, to `dev.json` in the repository root, which is not in Git, and
|
|
the manager reads that and listens on `127.0.0.1:7080`:
|
|
|
|
```sh
|
|
./dev.sh /path/to/azure-platform/config.json
|
|
./dev.sh /path/to/azure-platform/config.json 'host=localhost dbname=manager user=manager password=secret'
|
|
```
|
|
|
|
It creates `.venv` with `psycopg[binary]` and `requests` when they are missing, and installs the npm dependencies
|
|
when they are missing or out of date. The manager also takes `--config <file>` in place of
|
|
`/etc/vpn-router/manager.json`.
|
|
|
|
The Vite servers pass `/api` to `VITE_MANAGER` (`http://127.0.0.1:7080` by default), so the built
|
|
application runs against any manager, at http://localhost:4180:
|
|
|
|
```sh
|
|
cd web
|
|
npm install
|
|
npm run build
|
|
VITE_MANAGER=http://10.0.0.4:7080 npm run preview
|
|
```
|
|
|
|
## Removal
|
|
|
|
`apt-get remove` stops `vpn-router-docker`, which removes the Docker rules. It terminates the
|
|
tunnels and takes `wg-quick@wg0`, `openvpn-server@vpn-router`, `caddy`, `vpn-router-metrics`,
|
|
`vpn-router-webdav`, `vpn-router-backup.timer` and `vpn-router-pki.timer` down, but leaves the
|
|
configuration in place. `strongswan` is left enabled, since it is a shared service that
|
|
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. It also removes the web service's
|
|
signing key and the OpenID Provider's, the password file of the metrics service account, the Router Manager's
|
|
settings and the record of its last upgrades, the database of the refresh
|
|
tokens and the metrics and those of the web sites, the name server and the Router Manager's sessions, their roles and their password
|
|
files when the package created them, Caddy's configuration `/etc/caddy/vpn-router.Caddyfile` and PowerDNS's
|
|
`/etc/powerdns/pdns.d/vpn-router.conf`; the certificate in
|
|
`/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 CAs in
|
|
`/etc/caddy/vpn-router-pki`, the sites' certificates in `/etc/caddy/vpn-router-sites` and OpenVPN's CA and CRL files.
|
|
Caddy, rclone and PowerDNS stay installed, and with Caddy the
|
|
source of its repository. Key material, users, content and backups are never deleted:
|
|
`/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 folders of the web sites in `/var/lib/vpn-router/sites`, the archives in
|
|
`backup.path` and `/var/lib/vpn-router/backup/before-restore` are left alone.
|
|
|
|
## Building
|
|
|
|
```sh
|
|
./build.sh
|
|
```
|
|
|
|
The build runs in a container from the repository root: a Node.js stage builds the Router
|
|
Manager's application in `web/` with Vite, and an Ubuntu stage builds the package from
|
|
`debian-package/`, with the application and the help in `docs/`, and writes it to `out/`. The
|
|
container runtime is Apple's `container` on macOS and Docker on Linux, or `CONTAINER_RUNTIME`
|
|
when set, for example Docker on a remote engine:
|
|
|
|
```sh
|
|
CONTAINER_RUNTIME=docker DOCKER_CONTEXT=build-host ./build.sh
|
|
```
|
|
|
|
`./publish.sh` uploads the package to the component `main` of the distributions `noble` and
|
|
`resolute` of the configured repository, skipping a distribution that has the version already;
|
|
`DISTRIBUTIONS` names others.
|