🔐 scout auth — WebAuthn passkeys + HTTPS·
The dashboard is sealed behind WebAuthn passkeys (passwordless). Only enrolled admin devices can drive the rover.
Why HTTPS is required·
WebAuthn only runs in a secure context: https://… or http://localhost.
Plain http://<ip>:<port> makes the browser refuse the passkey ceremony. So for
any LAN / field use the dashboard must serve HTTPS.
DASH_TLS=true mints a self-signed cert on first boot (cached in
.scout_tls/, valid for this host's name + LAN IPs). You accept a one-time
"not private" browser warning (Advanced → Proceed) — the secure-context
requirement is then satisfied and passkeys work.
make dashboard-tls # local: https://localhost:8443
# docker: DASH_TLS=true is the default in docker-compose.yml
⚠️ The rpId gotcha — use a HOSTNAME, not a raw IP·
WebAuthn's relying-party id (rpId) must be a registrable domain or
localhost. A raw IP address (192.168.1.50) is not a valid rpId — the
browser will reject enrollment. https://<ip>:8443 gives you a secure context
but passkeys still won't enroll.
Fixes (pick one):
1. mDNS / .local name — reach the rover at https://scout.local:8443.
2. hosts entry — add 192.168.1.50 scout.local to the client's /etc/hosts,
then open https://scout.local:8443.
3. private domain — point a DNS name at the rover and set
SCOUT_AUTH_RP_ID=rover.example.com + SCOUT_AUTH_ORIGIN=https://rover.example.com.
The login screen detects a raw-IP / insecure origin and shows a clear warning instead of failing cryptically.
Real certs (optional, nicer UX — no warning)·
Drop a trusted cert (mkcert, private CA, or Let's Encrypt for a public name):
DASH_TLS=true \
DASH_TLS_CERT=/path/fullchain.pem \
DASH_TLS_KEY=/path/privkey.pem \
make dashboard-tls
Env reference·
| var | default | meaning |
|---|---|---|
DASH_TLS |
false (local) / true (docker) |
serve HTTPS |
DASH_TLS_DIR |
./.scout_tls |
where the self-signed cert is cached |
DASH_TLS_HOSTS |
— | extra SAN hostnames (space/comma sep) |
DASH_TLS_CERT / DASH_TLS_KEY |
— | use a real cert instead of self-signed |
SCOUT_AUTH_ENABLED |
true |
master auth switch |
SCOUT_AUTH_RP_ID |
derived from Host | force rpId (set to your domain) |
SCOUT_AUTH_ORIGIN |
derived | force expected origin |
SCOUT_AUTH_STORE |
./.scout_auth.json |
passkey + JWT-secret store |
SCOUT_AUTH_TOKEN_TTL |
86400 |
session length (s) |
SCOUT_AUTH_BOOTSTRAP_TOKEN |
— | one-time secret to gate first enrollment |
Multi-admin·
Settings drawer → 🔑 Admin passkeys: enroll a teammate's device, rename, or revoke. The last passkey can't be removed (would lock everyone out).
🎫 Field setup kit (mDNS + mkcert + QR)·
Three pieces make "scan & drive" work in the field:
1. mDNS — scout.local resolves automatically·
The dashboard advertises itself as scout.local on the LAN (pure-Python
zeroconf, auto-started). macOS/iOS/Windows10+/most Linux resolve .local with
zero client config. No more per-device /etc/hosts edits.
make mdns # standalone test
# auto-runs inside the dashboard; name via SCOUT_MDNS_NAME (default 'scout')
Docker: mDNS needs to reach the LAN, so run the dashboard with
network_mode: host(commented hint indocker-compose.yml). In bridge mode the IP-fallback URL on the QR card still works;scout.localjust won't resolve from other devices.
2. mkcert — trusted cert, NO browser warning (optional)·
Self-signed works but shows a one-time warning. For a polished fleet, install mkcert's local CA on your team's machines — then scout's cert is trusted:
make mkcert-install # installs mkcert + trusts a local CA
make dashboard-tls # tls.py auto-detects mkcert → trusted cert, no warning
tls.py priority: DASH_TLS_CERT/KEY → mkcert (if installed) → self-signed.
Opt out with DASH_TLS_MKCERT=off.
3. Printable QR card — scan to enroll·
Generates a QR + printable card pointing at https://scout.local:PORT:
make field-card # prints ASCII QR + writes PNG/HTML
BOOTSTRAP=field-secret make field-card # embeds the one-time setup token
.scout_tls/:
- field_setup_qr.png — the QR image
- field_setup.html — a printable card (open → 🖨️ Print, or screenshot)
Live, always-current card is also served at /field-card (public — it only
contains the access URL + whatever bootstrap token you chose to embed).
Field flow·
- Power on scout → dashboard advertises
scout.local, mints its cert. - Print/show the card (
make field-cardor open/field-card). - Teammate joins the same Wi-Fi → scans QR → accepts cert once (or zero warnings if their machine trusts the mkcert CA) → enrols passkey.
- Driving. 🛞
New env·
| var | default | meaning |
|---|---|---|
SCOUT_MDNS |
true |
advertise scout.local on the LAN |
SCOUT_MDNS_NAME |
scout |
advertised name (→ <name>.local) |
DASH_TLS_MKCERT |
auto |
use mkcert if installed (off to disable) |
📱 Trust scout on iOS / Android (zero warnings)·
mkcert is baked into the Docker image. When the dashboard runs with mkcert (default), it issues a locally-trusted cert — but a phone still needs to trust the mkcert root CA once to drop the warning entirely.
The dashboard serves a self-contained helper:
/ca— downloads the mkcert root CA (scout-rootCA.crt)./trust— a phone-friendly page with a QR to/ca+ OS-detected step-by-step (iOS profile install + Certificate Trust Settings; Android CA-cert install). Linked from the field card and the settings drawer (🔏 trust on iOS / Android).
iOS·
- Open
/trustin Safari, tap the CA → allow the profile. - Settings → Profile Downloaded → Install.
- Settings → General → About → Certificate Trust Settings → toggle ON.
Android·
- Open
/trust, downloadscout-rootCA.crt. - Settings → Security → Encryption & credentials → Install a certificate → CA certificate → pick the file.
CA persistence·
CAROOT=/app/.scout_auth_vol/mkcert-ca (the persisted volume) so the CA — and
therefore every phone's trust — survives image rebuilds. Don't delete that
volume or phones must re-trust.
make ca # show the CA path on the host
make mkcert-install # (host/dev) install mkcert + trust the CA locally
Self-signed mode (no mkcert):
/trustexplains there's no CA to install — just tap Advanced → Proceed on the one-time warning.