feat(admin): OIDC sign-in, role sync, deploy/delete UX, and docs
This commit lights up the missing pieces of the admin dashboard and the
OIDC flow that the desktop client already speaks. It bundles several
independent fixes that share enough touch points (oidc/callback,
admin/mod, database schema) that splitting was more churn than help.
OIDC — desktop client
- /api/login: when TOTP is enrolled, return type:"email_check"
+ tfa_type:"tfa_check" instead of type:"tfa_check". The Flutter
client's switch only branches on access_token / email_check; the
prior shape silently fell into "bad response from server".
- /api/login dispatcher: route the second leg to login_tfa_code when
tfaCode + secret are both present, regardless of the declared type.
The desktop client sends type:"email_code" for both email-code AND
TOTP second legs and distinguishes by which field is set.
- /api/oidc/auth-query: drop the bogus extra {"body": "..."} envelope.
The desktop client's http_request_sync already wraps every response
in {status_code, headers, body}, and HbbHttpResponse::parse expects
the auth payload at that level. Our extra envelope made the parser
fail silently as DataTypeFormat and the poll loop spun until the
180 s client timeout.
- UserPayload: add a required info: {} field; the Rust-side polling
deserializer at src/hbbs_http/account.rs expects it (no
#[serde(default)]). Without it the AuthBody parse failed on every
poll, producing the same forever-pending symptom as above.
- Add an always-on info-level log line at the poll handler so this
family of "client never advances" bugs is observable from hbbs.log.
OIDC — admin dashboard
- New unauthenticated entry points:
GET /admin/oidc/providers JSON list for login.html
GET /admin/login/oidc/:provider 302 → IdP authorization endpoint
The session is marked admin-flow via a sentinel ("__admin_ui__") in
client_id_str / client_uuid so the existing /oidc/callback can tell
it apart from a desktop device flow.
- /oidc/callback finishes admin sessions by setting the
rd_admin_session cookie + 303 to /admin/. Non-admin users get a
helpful error page instead of a session.
- Admin-flow callbacks SKIP device_claim() so the dashboard sign-in
no longer inserts a phantom "__admin_ui__" device row in
device_sysinfo, and the token's peer_id / peer_uuid columns stay
blank instead of carrying the sentinel.
- admin_ui/login.html fetches the providers list on load and renders
one button per enabled provider beneath the password form.
OIDC — role-based admin sync
- New per-provider config fields admin_role + roles_claim (in
oidc.toml AND oidc_providers, via soft ALTER TABLE). When set, the
callback evaluates the userinfo claim and forces users.is_admin
accordingly on every login. Promotion AND demotion at the IdP
propagate. Two claim shapes supported:
- object key match (Zitadel:
urn:zitadel:iam:org:project:roles -> { "admin": {...} })
- string-array contains (generic: roles -> ["admin","user"])
- user_upsert_oidc gains a desired_admin: Option<bool> arg so the same
upsert path handles "leave admin alone" (desired_admin = None) and
"force from IdP" (Some(bool)). Three unit tests cover both shapes
plus the missing-claim case.
Admin dashboard — Address books
- Full CRUD for shared books from the dashboard:
create, list shares, add/upgrade/remove a per-user share with
read / read+write / full rules, delete the book.
- Personal books also get a Delete action — confirms with a stronger
message that the user's desktop client will recreate an empty
personal book on next sync if it's still signed in (deletion is
effectively "reset to empty", not "permanently revoked"). Use in
combination with user-delete to fully revoke.
- New DB methods: ab_create_shared, ab_delete (cascades peers/tags/
peer_tags/shares), ab_get_owner_kind, ab_list_shares, ab_share_set
(idempotent upsert), ab_share_remove.
Admin dashboard — Devices
- Delete action in the per-row menu. device_delete cascades through
device_sysinfo, peer (rendezvous identity), heartbeat_commands and
peer-scoped strategy_assignments. Audit logs, recordings, and AB
entries that reference the peer are intentionally preserved
(historical/manual data).
Admin dashboard — Deploy page
- New page that generates the unsigned CustomServer blob the desktop
client accepts via `rustdesk --config <blob>` (see
rustdesk/src/custom_server.rs:get_custom_server_from_config_string;
the unsigned-JSON path is a real codepath, no Pro signing key
needed). Form prefills the public key from id_ed25519.pub in CWD.
- Also emits the equivalent renamed-installer filename
(rustdesk-host=...,key=... .exe). Strips api= from the filename
when it equals the default http://<host>:21114 (Windows can't store
: or / in filenames); warns when the API URL is non-default.
Login form fixes
- TOTP form field: rename serde wire field from tfa_code to tfaCode
so the dashboard's HTMX form (input name="tfaCode") actually
populates it. The previous mismatch silently dropped the code and
the server kept asking for it.
- TOTP redirect guard: only redirect on empty 2xx body (real login).
The TOTP-required path returns 2xx with an HTML prompt fragment
that must NOT be redirected away from.
Docs
- New docs/CONFIGURATION.md covering all CLI flags, OIDC setup
(generic + Zitadel walk-through), role-based admin sync, TOTP,
strategies, address books, dashboard URL map, DB notes, and a
pre-prod security checklist.
Schema
- soft ALTER TABLE oidc_providers ADD COLUMN admin_role / roles_claim
(guarded by the duplicate-column-name swallower for SQLite < 3.35).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,340 @@
|
||||
# RustDesk Server (hbbs) — Configuration Guide
|
||||
|
||||
This document covers the runtime flags exposed by `hbbs`, the file formats
|
||||
it reads (notably `oidc.toml`), and the operator workflows that string
|
||||
those together — bootstrap admin, OIDC sign-in, TOTP, address books,
|
||||
strategies, recordings, the admin dashboard.
|
||||
|
||||
The matching desktop-client API surface is documented separately in the
|
||||
`rustdesk` repo at `docs/CONSOLE_API.md`.
|
||||
|
||||
---
|
||||
|
||||
## CLI flags
|
||||
|
||||
Pass flags directly on the command line. There is no config file for
|
||||
hbbs itself (only the optional `oidc.toml` referenced from a flag).
|
||||
|
||||
### Networking & rendezvous
|
||||
|
||||
| Flag | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `--port=<NUM>` | `21116` | TCP/UDP rendezvous port. |
|
||||
| `--rendezvous-servers=<HOSTS>` | unset | Peer rendezvous servers (comma-separated). |
|
||||
| `--relay-servers=<HOSTS>` | unset | Default relay hosts handed to clients. |
|
||||
| `--rmem=<BYTES>` | platform default | UDP recv buffer size. Bump along with `net.core.rmem_max`. |
|
||||
| `--mask=<CIDR>` | unset | LAN mask (e.g. `192.168.0.0/16`) used to flag local connections. |
|
||||
| `--key=<B64>` | derived from `id_ed25519` | Force a specific public key; clients must match. Leave unset to auto-load `id_ed25519` next to the binary. |
|
||||
|
||||
### HTTP API & dashboard
|
||||
|
||||
| Flag | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `--http-port=<NUM>` | `21114` | HTTP API port (`/api/*`) and admin dashboard (`/admin/*`). `0` disables both. |
|
||||
| `--admin-ui-dir=<PATH>` | `./admin_ui` | Hint at where the dashboard's static HTML lives. The HTML is *embedded* in the binary; this flag is informational. Setting it to empty (`--admin-ui-dir=`) disables the dashboard entirely. |
|
||||
| `--public-base-url=<URL>` | unset | The externally-reachable HTTP base of this server, e.g. `https://rustdesk.example.com:21114`. **Required when OIDC is enabled** — used to build `/oidc/callback` redirect URIs. |
|
||||
|
||||
### Bootstrap admin
|
||||
|
||||
| Flag | Purpose |
|
||||
|---|---|
|
||||
| `--bootstrap-admin-username=<USER>` | On first startup, if the `users` table is empty *and* both flags are set, insert one admin user. Subsequent restarts ignore these flags (no overwrite). |
|
||||
| `--bootstrap-admin-password=<PASS>` | Same. Bcrypt-hashed at insert time. |
|
||||
|
||||
If you forget to bootstrap, hbbs logs a warning at startup ("no users in users table"); recover by either restarting with the flags or `INSERT INTO users` directly via `sqlite3`.
|
||||
|
||||
### Address books
|
||||
|
||||
| Flag | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `--ab-legacy-mode=<on\|off>` | `off` | When `on`, `/api/ab/personal` returns 404. Forces clients into the legacy single-blob AB mode. |
|
||||
| `--ab-max-peers-per-book=<NUM>` | `100` | Surfaced via `/api/ab/settings.max_peer_one_ab`. Soft cap; the client uses it for UI hints. |
|
||||
|
||||
### Recordings
|
||||
|
||||
| Flag | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `--recording-dir=<PATH>` | `./recordings` | Root for `/api/record` uploads. One subdirectory per peer. |
|
||||
| `--recording-max-size-mb=<NUM>` | unset (=unlimited) | Per-file ceiling. Aborts oversized parts. |
|
||||
|
||||
> **Note:** Stock OSS RustDesk clients **do not upload** recordings to `/api/record` — the uploader's `ENABLE` flag at `src/hbbs_http/record_upload.rs` has no setter in OSS source. Server-side recording requires a custom client build that flips that flag. The `Recordings` admin tab will stay empty for stock clients; the endpoint is provided for wire parity with Pro clients.
|
||||
|
||||
### Audit retention
|
||||
|
||||
| Flag | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `--audit-retention-days=<NUM>` | `0` (=keep forever) | Hourly task deletes `audit_conn` / `audit_file` / `audit_alarm` rows older than N days. |
|
||||
|
||||
### Email-code login (`/api/login` with `type:"email_code"`)
|
||||
|
||||
| Flag | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `--smtp-host=<HOST>` | unset | If unset, codes are *logged to stdout* (dev mode) instead of mailed. The `email_code` login option is also dropped from `/api/login-options` until SMTP is configured. |
|
||||
| `--smtp-port=<NUM>` | `587` | |
|
||||
| `--smtp-user=<USER>` | unset | Omit for unauthenticated relays. |
|
||||
| `--smtp-pass=<PASS>` | unset | |
|
||||
| `--smtp-from=<ADDR>` | `noreply@<smtp-host>` | From: header. |
|
||||
| `--smtp-tls=<on\|off>` | `on` | STARTTLS on the SMTP transport. |
|
||||
|
||||
### OIDC
|
||||
|
||||
| Flag | Purpose |
|
||||
|---|---|
|
||||
| `--oidc-config=<PATH>` | TOML file (see below). Providers are upserted into `oidc_providers` at startup. Re-run with a different file to change providers; rows missing from the new file remain in the DB but can be `enabled=0`'d via SQL. |
|
||||
| `--public-base-url=<URL>` | **Required** if any provider is configured. Determines the redirect URI registered with the IdP. |
|
||||
|
||||
---
|
||||
|
||||
## OIDC integration
|
||||
|
||||
The server speaks standard OIDC Authorization Code flow with discovery
|
||||
(`/.well-known/openid-configuration`). Tested against Zitadel; should work
|
||||
with any standards-compliant IdP (Keycloak, Auth0, Google, Okta, Authelia,
|
||||
Dex, etc.).
|
||||
|
||||
Two entry points are wired:
|
||||
|
||||
1. **Desktop client** — `/api/login-options` advertises `oidc/<name>` per enabled provider. The Flutter login dialog renders a button per advertised name. Clicking starts the device-flow polling cycle (`/api/oidc/auth` → browser → `/oidc/callback` → `/api/oidc/auth-query` poll).
|
||||
2. **Admin dashboard** — `/admin/login.html` fetches `/admin/oidc/providers` and renders a "Sign in with X" button per provider. Clicking jumps the browser to `/admin/login/oidc/<name>` which 302-redirects to the IdP. After the IdP returns to `/oidc/callback`, the server detects the admin-flow sentinel and finishes by setting the dashboard session cookie + redirecting to `/admin/`.
|
||||
|
||||
### `oidc.toml` schema
|
||||
|
||||
Pass via `--oidc-config /path/to/oidc.toml`.
|
||||
|
||||
```toml
|
||||
[[providers]]
|
||||
# Slug used in URLs (`/admin/login/oidc/<name>`, `/api/login-options`
|
||||
# advertises `oidc/<name>`). Lowercase, no spaces.
|
||||
name = "zitadel"
|
||||
|
||||
# Display label on the sign-in button.
|
||||
display_name = "Sign in with Zitadel"
|
||||
|
||||
# Optional. Square icon URL shown next to the label (not used yet by all
|
||||
# UIs; reserved for future button rendering).
|
||||
# icon_url = "https://example.com/zitadel.svg"
|
||||
|
||||
# OIDC issuer. The server fetches `<issuer_url>/.well-known/openid-configuration`
|
||||
# and caches the discovery doc in-process. Trailing slash is stripped.
|
||||
issuer_url = "https://idp.example.com"
|
||||
|
||||
# Application credentials from the IdP.
|
||||
client_id = "..."
|
||||
client_secret = "..."
|
||||
|
||||
# Scopes requested at the authorization endpoint. Most setups want
|
||||
# "openid email profile". Zitadel additionally needs the project audience
|
||||
# scope to receive role claims (see Role-based admin sync below).
|
||||
scopes = "openid email profile"
|
||||
|
||||
# Optional. If unset, computed as `<--public-base-url>/oidc/callback`.
|
||||
# Override only when you reverse-proxy under a different host.
|
||||
# redirect_url = "https://rustdesk.example.com/oidc/callback"
|
||||
|
||||
# Optional. Defaults to true.
|
||||
enabled = true
|
||||
|
||||
# --- Role-based admin sync (optional) ---
|
||||
# When `admin_role` is set, every successful sign-in via this provider
|
||||
# evaluates the userinfo claim at `roles_claim` and forces the local
|
||||
# user's `is_admin` to (role present in claim). Promotion AND demotion
|
||||
# at the IdP propagate. Leave both unset to manage admin status manually
|
||||
# from the dashboard.
|
||||
# admin_role = "admin"
|
||||
# roles_claim = "roles" # or e.g. "urn:zitadel:iam:org:project:roles"
|
||||
```
|
||||
|
||||
`oidc.toml` may contain multiple `[[providers]]` blocks for multi-IdP setups.
|
||||
|
||||
### Walk-through: Zitadel
|
||||
|
||||
#### In Zitadel
|
||||
|
||||
1. **Project → New project** (or pick an existing one).
|
||||
2. **New application** under the project:
|
||||
- Type: **Web**
|
||||
- Authentication flow: **Code** (Authorization Code with client secret)
|
||||
- Auth method: **Basic** *or* **Post** (server sends `client_id` + `client_secret` in the form body — both modes accept that)
|
||||
- **Redirect URIs**: `<public-base-url>/oidc/callback` — character-exact, including scheme. Zitadel rejects `http://` redirects on non-localhost unless dev mode is on, so use TLS in production.
|
||||
3. **Authorizations** — assign the project's roles to whichever users you want to be admins.
|
||||
4. **Project → General**: turn on **"Assert Roles On Authentication"** so roles flow into the userinfo response.
|
||||
5. Copy **Client ID** and **Client Secret** from the application's overview page.
|
||||
|
||||
#### `oidc.toml`
|
||||
|
||||
```toml
|
||||
[[providers]]
|
||||
name = "zitadel"
|
||||
display_name = "Sign in with Zitadel"
|
||||
issuer_url = "https://your-instance.zitadel.cloud"
|
||||
client_id = "PASTE_FROM_ZITADEL"
|
||||
client_secret = "PASTE_FROM_ZITADEL"
|
||||
# `urn:zitadel:iam:org:project:id:zitadel:aud` is required for the project's
|
||||
# roles to be included in the userinfo response.
|
||||
scopes = "openid email profile urn:zitadel:iam:org:project:id:zitadel:aud"
|
||||
admin_role = "admin"
|
||||
roles_claim = "urn:zitadel:iam:org:project:roles"
|
||||
```
|
||||
|
||||
#### `hbbs` flags
|
||||
|
||||
```sh
|
||||
./hbbs --http-port 21114 \
|
||||
--public-base-url 'https://rustdesk.example.com:21114' \
|
||||
--oidc-config /etc/rustdesk/oidc.toml
|
||||
```
|
||||
|
||||
#### Verify
|
||||
|
||||
After hbbs starts, look for:
|
||||
|
||||
```
|
||||
oidc: provider "zitadel" configured
|
||||
oidc: loaded 1 providers from /etc/rustdesk/oidc.toml
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```sh
|
||||
# 1. Provider visible to the desktop client
|
||||
curl -s http://127.0.0.1:21114/api/login-options
|
||||
# expect a list including "oidc/zitadel"
|
||||
|
||||
# 2. Provider visible to the admin dashboard
|
||||
curl -s http://127.0.0.1:21114/admin/oidc/providers
|
||||
# expect [{"name":"zitadel","display_name":"Sign in with Zitadel",...}]
|
||||
|
||||
# 3. Discovery is reachable (IdP-side)
|
||||
curl -s https://your-instance.zitadel.cloud/.well-known/openid-configuration | jq .issuer
|
||||
```
|
||||
|
||||
### Role-based admin sync
|
||||
|
||||
When `admin_role` is set on a provider, every successful sign-in evaluates
|
||||
the userinfo claim at `roles_claim` (defaults to `"roles"` if unset) and
|
||||
forces `users.is_admin` accordingly. **Promotion and demotion at the IdP
|
||||
propagate on the next login.**
|
||||
|
||||
Two claim shapes are supported:
|
||||
|
||||
- **Object** (Zitadel default at `urn:zitadel:iam:org:project:roles`): role names are keys.
|
||||
```json
|
||||
"urn:zitadel:iam:org:project:roles": {
|
||||
"admin": {"123": "myorg"},
|
||||
"user": {"123": "myorg"}
|
||||
}
|
||||
```
|
||||
- **Array of strings** (generic, common with Keycloak, Auth0 custom claims):
|
||||
```json
|
||||
"roles": ["admin", "user"]
|
||||
```
|
||||
|
||||
Set `admin_role = "admin"` and either set `roles_claim` to the exact claim
|
||||
name (Zitadel) or omit it to default to `"roles"` (generic).
|
||||
|
||||
> **Sharp edge:** when role-sync is configured, manually-granted admin
|
||||
> rights in the dashboard get **revoked** on the next OIDC login if the
|
||||
> role isn't present at the IdP. This is the correct contract for a
|
||||
> single source of truth, but surprising if you forget. Manage admin
|
||||
> status in *one* place at a time.
|
||||
|
||||
### Troubleshooting OIDC
|
||||
|
||||
- **"Sign-in complete" page in browser but desktop client stays at "Waiting account auth"**: usually a state mismatch between server and client. Check `hbbs.log` — the poll endpoint logs every tick at INFO. If you see `status=success` lines that don't stop, suspect a wire-shape mismatch. (This was a real bug we hit and fixed; see git log for `oidc envelope`.)
|
||||
- **Browser shows "identity provider returned an error"**: check `oidc_sessions.error` for the row that just failed. Most common: `redirect_uri` mismatch between Zitadel and `--public-base-url`.
|
||||
- **No "Sign in with X" button in the dashboard or desktop client**: check `oidc_provider_list_enabled()` returns rows. If `--public-base-url` is empty, `/admin/oidc/providers` and `/api/login-options` both suppress OIDC entries (the redirect URI would be unbuildable).
|
||||
- **Admin landing on the "no admin access" error after first OIDC sign-in**: expected if `admin_role` isn't configured. Either configure role-sync (preferred), or have the user sign in once to create their row, then promote them on the Users page. The next OIDC sign-in resolves to that row.
|
||||
|
||||
---
|
||||
|
||||
## TOTP / 2FA
|
||||
|
||||
Per-user TOTP is enrolled from the dashboard:
|
||||
|
||||
1. Sign in as an admin → **Users** page.
|
||||
2. Pick a user → action menu → **Enroll TOTP**.
|
||||
3. Scan the QR code into an authenticator (1Password, Authy, Google Authenticator, etc.). The secret is shown once and stored in `user_totp_secrets`.
|
||||
|
||||
After enrollment, the next desktop-client login flow is:
|
||||
|
||||
1. Username + password → server returns `{"type":"email_check","tfa_type":"tfa_check","secret":<nonce>}`.
|
||||
2. Client opens its verification-code dialog → user enters the 6-digit code → re-POSTs `/api/login` with `type:"email_code"` (yes, that's what the desktop client sends for both email and TOTP second legs), `tfaCode` set, `secret` echoed back.
|
||||
3. Server verifies the code against `user_totp_secrets`, mints an access token, returns `{"type":"access_token", ...}`.
|
||||
|
||||
For dashboard logins, the inline form at `/admin/login.html` shows the TOTP field after the first password submit returns the prompt fragment.
|
||||
|
||||
---
|
||||
|
||||
## Strategies (server-pushed config)
|
||||
|
||||
Strategies push `config_options` to peers via heartbeat replies. They are
|
||||
managed entirely from the dashboard's **Strategies** page. Resolution
|
||||
order per peer:
|
||||
|
||||
1. Direct peer-scoped assignment (`strategy_assignments.peer_id`)
|
||||
2. Device-group assignment via the peer's owner
|
||||
3. User assignment
|
||||
|
||||
The peer's `Config::get_option` calls reflect the resolved values within
|
||||
~15 s of any change to `modified_at` on the strategy row.
|
||||
|
||||
---
|
||||
|
||||
## Address books
|
||||
|
||||
- **Personal books** are owned per-user and managed from the user's desktop client. The dashboard surfaces them read-only.
|
||||
- **Shared books** are server-side artifacts. Create from the dashboard's **Address books** page → "Manage shares" → grant per-user `read` / `read+write` / `full` access. Clients pick up shared books on their next AB sync (~30 s).
|
||||
|
||||
If you set `--ab-legacy-mode=on`, `/api/ab/personal` 404s and clients fall back to the single-blob `/api/ab` path. Use only if a stock client misbehaves on the modern path.
|
||||
|
||||
---
|
||||
|
||||
## Admin dashboard URLs
|
||||
|
||||
| Path | Auth | What |
|
||||
|---|---|---|
|
||||
| `/admin/`, `/admin/index.html` | none (login page redirects in JS) | Single-page shell |
|
||||
| `/admin/login.html` | none | Sign-in form (password / TOTP / OIDC buttons) |
|
||||
| `/admin/login` | none (POST form) | Password+TOTP submit → sets `rd_admin_session` cookie |
|
||||
| `/admin/logout` | cookie | Clears cookie |
|
||||
| `/admin/me` | cookie | Sidebar's logged-in-as widget |
|
||||
| `/admin/oidc/providers` | none | JSON list of enabled providers, used by login.html |
|
||||
| `/admin/login/oidc/:name` | none | Starts admin OIDC flow (302s to IdP) |
|
||||
| `/admin/pages/users` | cookie + admin | Users page fragment |
|
||||
| `/admin/pages/devices` | cookie + admin | Devices (incl. delete) |
|
||||
| `/admin/pages/groups` | cookie + admin | Device groups |
|
||||
| `/admin/pages/strategies` | cookie + admin | Strategy management |
|
||||
| `/admin/pages/address-books` | cookie + admin | Personal + shared books |
|
||||
| `/admin/pages/oidc` | cookie + admin | Read-only OIDC provider listing |
|
||||
| `/admin/pages/audit` | cookie + admin | Audit log browser |
|
||||
| `/admin/pages/recordings` | cookie + admin | Recording file listing |
|
||||
| `/admin/pages/deploy` | cookie + admin | `--config` blob + renamed-installer generator |
|
||||
|
||||
The session cookie (`rd_admin_session`) is HttpOnly + SameSite=Strict.
|
||||
The middleware accepts the same cookie *or* `Authorization: Bearer …`,
|
||||
so the same auth covers `/api/*` for the desktop client and `/admin/*`
|
||||
for the dashboard with no separate session model.
|
||||
|
||||
---
|
||||
|
||||
## Database
|
||||
|
||||
SQLite, file `db_v2.sqlite3` in hbbs's working directory. Tables created
|
||||
at startup with `CREATE TABLE IF NOT EXISTS`; column additions use
|
||||
`ALTER TABLE ADD COLUMN` guarded by a duplicate-column-name swallower
|
||||
(SQLite < 3.35 lacks `ADD COLUMN IF NOT EXISTS`).
|
||||
|
||||
Backup is a plain file copy while hbbs is stopped, or `sqlite3
|
||||
db_v2.sqlite3 .dump > backup.sql` while running. There is no
|
||||
multi-instance HA; run a single hbbs against a single SQLite file.
|
||||
|
||||
---
|
||||
|
||||
## Security checklist before exposing to the internet
|
||||
|
||||
- TLS in front of `--http-port` (Caddy / nginx / Traefik). Required for OIDC redirect URIs in production.
|
||||
- `--public-base-url` set to the *externally* reachable URL, including the scheme.
|
||||
- `--bootstrap-admin-password` rotated immediately after first login (Users page → reset password).
|
||||
- `--key` / `id_ed25519` not committed to source control. Treat the private key as a deploy secret.
|
||||
- Audit retention (`--audit-retention-days`) set to a value that matches your data-retention policy.
|
||||
- If running behind a reverse proxy: forward the original `Host:` header so OIDC redirect-URI validation matches.
|
||||
Reference in New Issue
Block a user