In the desktop build, API tokens are held in the OS keychain and resolved inside Rust. They are never written to instances.json, never written to localStorage, and never cross the webview boundary on a request.
How a request gets its token
The webview calls
invoke('dataplane_fetch', {...})— an IPC message, not a network request.Rust reads the token from the keychain, keyed by instance id (
instance:{id}).Rust makes the HTTP request via
reqwest, with the token attached.The response goes back to the webview. The token does not.
This is what makes the desktop build trustworthy with a production bearer token: there is no code path by which the token reaches the webview, so an XSS in the UI layer cannot exfiltrate it.
Storage per platform
OS | Backend |
|---|---|
macOS | Keychain (Security.framework) |
Windows | Credential Manager |
Linux | Secret Service (gnome-keyring, KWallet) |
First-save prompts
The OS prompts once per host when HAProxy UI first writes a secret. What to expect:
macOS — a one-time “Always Allow / Deny” dialog from Security.framework. It is non-blocking but visible, so foreground the Tauri window before saving the first token. Choose Always Allow or every subsequent save re-prompts.
Windows — Credential Manager is silent on a normal save. A consent dialog appears only under stricter Group Policy.
Linux — requires a running Secret Service. On a headless machine or a fresh box with no desktop session, every read and write fails until one is started. The usual fix is
dbus-launch --exit-with-session gnome-keyring-daemon.
Migration from older versions
An instances.json written before keychain support may contain a plain-token field. On first read it is migrated to the keychain automatically, and the next save scrubs it from disk. No manual step, and no window in which a token sits in both places.
Browser mode is different
The browser build has no keychain available. There, secrets fall back to localStorage — which is browser-readable by anything running on the origin. This is a real limitation, not an oversight.
Treat the browser build as suitable for the mock fixture, the Vite proxy against a local development stack, or a Dataplane you control. For a production bearer token, use the desktop build. The API token is the credential; a web UI that stores it in
localStorageis not the right place for it.
What is stored where
Data | Location (desktop) | Contains secrets? |
|---|---|---|
API tokens | OS keychain, keyed | Yes — and only here |
Instance list (URL, label, auth scheme, mode) |
| No |
Working draft |
| No |
Config snapshots | App data dir | No — but note snapshots contain full configuration, including anything sensitive in it |
SSH keys for GitOps
GitOps SSH authentication uses ssh-agent from the Rust side. Private key files are never read into the webview and never copied into the app data directory.
Updater signing key
Separate from instance credentials: the Tauri updater uses an Ed25519 keypair. The public half is committed in src-tauri/tauri.conf.json; the private half lives only on the maintainer’s machine and in a CI secret. Rotating it requires a two-key transition release — see Installation & Releases.