vpn_user row carries a NOT NULL network_id, and the config file or connect
command handed to a device is rendered from its network’s endpoint host. Create
users before a network exists and the portal can show credentials it has no way
to tell anyone how to use.
The console screen lists networks (adminVpn.listNetworks) and writes them
through a single adminVpn.upsertNetwork mutation — the same procedure creates
and edits, with id present only when editing. Saving a network also
invalidates the user list, because users are read with their network joined in
order to render that connect command.
Validity does not disable the Create network / Save changes button.
Clicking an invalid form reveals the per-field messages instead.
What a network is
Both list fields — routes and DNS — are entered as one text field, separated by
commas or whitespace. Empty entries are dropped, so
10.0.0.0/8, 192.168.10.0/24 and 10.0.0.0/8 192.168.10.0/24 are equivalent.
Each entry is validated individually and the first failing entry is named in
the error message.
Tunnel CIDR and the engine’s vpn.network value
The Tunnel CIDR you type is copied straight into the engine’svpn.network
knob. That is why it must include the server’s own address:
Enter a CIDR block (10.88.0.0/24). Then the shared CIDR validator checks the
block itself. The value is required; a network with no address space cannot be
saved.
Endpoint host, port and WebTransport path
These three fields are what a device dials, and together they are what the portal renders into a device config or connect command.- Endpoint host is validated as a hostname and is required. Leave it blank and a device can be issued a working credential and still have nothing to connect to — which is why the console refuses to save without it.
- Port may be left blank; the save defaults it to
443. A non-empty value must be a valid port (1–65535). - WebTransport path must start with
/, otherwise the field reportsPath must start with /. Blank is accepted and the save defaults it to/vpn.
Pushed routes, DNS and MTU in the assign frame
When a client connects, the engine answers with an assign frame. The network’s routes, DNS servers and MTU travel in that frame, so all three are policy set once per network rather than per device.- Pushed routes are the CIDRs the client should send through the tunnel. Leave the field blank to route nothing but the tunnel itself.
- DNS servers are plain IP addresses, for example
10.88.0.1, 1.1.1.1. Each entry is validated as an IP. - MTU must be between 576 and 1500. The default is
1350.
Client isolation
Isolate clients from each other is a single checkbox on the network. With it enabled, devices still reach the routed networks you pushed, but they cannot reach each other’s tunnel addresses. The networks table flags isolation on the row so you can see at a glance which address spaces are segmented.Requiring a second factor (TOTP)
Require a second factor (TOTP) makes every device on the network present an authenticator code in addition to its token. The flag is stored on the network (require_mfa), so it applies to all users whose network_id points at it.
Enrolment is per user. The policy is evaluated at connect time against the
user’s enrolment state, which means turning the toggle on is a policy change,
not a migration: users who have already enrolled are held to their authenticator
immediately, and users who have not are handled by the grace window described
below.
Grace periods and the mfa_not_enrolled refusal
When the requirement is on, the engine refuses a connection from a user who has not enrolled a TOTP secret withmfa_not_enrolled. That refusal is
correct, and from the perspective of the person holding the laptop it is
indistinguishable from an outage. Plan for it.
The grace period field softens the rollout:
- Blank or
0enforces immediately. - A positive number of days (up to 365) sets an exemption window. During the window, users who have not enrolled keep connecting with their token alone. Users who have enrolled are held to their authenticator right away.
- Once the window passes, unenrolled users are refused with
mfa_not_enrolled.
mfa_grace_until), not as a
duration, and the console is deliberate about when it recomputes that instant:
Editing an unrelated field — an extra pushed route, say — and saving must not
quietly extend a security grace that is already counting down, so the stored
instant is kept unless you actually touched the days field.
The remaining days shown when you reopen a network are derived from the stored
instant and rounded up, so a window with six hours left reads
1 rather
than 0 days while it is still open. Once the instant has passed, the field
reads blank.
MFA readiness before you save
Requiring a second factor is also how you lock out your whole team in one click. So the screen names the affected users before the toggle is saved, usingadminVpn.mfaReadiness({ orgId, networkId }). The panel appears
underneath the grace-period field and reports:
The wording changes with the grace setting:
- No grace window. The panel border turns destructive and reads that those users will be refused as soon as you save, followed by their names.
- Grace window set. The same users are listed, with the note that they keep connecting until the grace period ends and are refused after that.
- Nobody outstanding. The panel confirms that enforcing now locks nobody out.
Sealing enrolled secrets: STREAM_SIGNING_KEK_B64
mfaReadiness also reports whether the server has a signing key-encryption key
configured. When kekConfigured is false, the readiness panel prints a
destructive-coloured warning: enrolled TOTP secrets are stored unsealed.
Set STREAM_SIGNING_KEK_B64 on the server so that enrolled secrets are sealed
at rest, and do that before relying on the second-factor requirement in
production. The requirement itself will function without the KEK — the engine
still enforces TOTP and still refuses unenrolled users — but the secrets it
checks against are not protected.
Tags
Networks are taggable as thevpn_network resource type. The screen resolves
tags for every visible network in one bulk call and writes them per row, and the
tag filter at the top of the page narrows the table, showing how many of the
total networks matched.