feature: a shell opens with one of the user's passkeys in place of the password and the TOTP code.
Release / release (push) Successful in 17s

This commit is contained in:
slawek committed 2026-10-29 23:13:11 +01:00
1 parent 3e78d520ae
commit 3ed79c7563
7 files changed
+129 -36

No files matched your search

+20 -10
View File
@@ -1080,8 +1080,9 @@ 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:
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
@@ -1090,20 +1091,29 @@ vpn-config-edit set web.shell true
| Request | Body | Answer |
| --- | --- | --- |
| `POST /shell` | `{"password", "code"}`; `{}` within `web.shell_auth_timeout` | `{"ticket", "expires_in": 30}` |
| `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: a wrong password or code gets 403 and counts as a failed login; it gets 403 too while
`web.shell` is `false`, the user's console login is off or the user is not enrolled for TOTP. Without a password 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
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. 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
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.