How SSH Guru is built
A browser based SSH client with an AI assistant and an optional hardware bridge for private networks. This page describes how it is built and what it does and does not protect against.
01Summary
The SSH client runs in the browser. Credentials are encrypted in the browser with a key derived from the user's passphrase. The service stores ciphertext it cannot open. Every command proposed by the assistant needs a click from the user before it runs.
02Architecture
browser ──WebSocket (TLS)──▶ relay ──TCP──▶ server (public hosts)
browser ──WebSocket (TLS)──▶ relay ──tunnel──▶ ESP32 bridge ──TCP──▶ server
(on the LAN) (private hosts)
The SSH client is Go compiled to WebAssembly and runs in the page. Key exchange, authentication and encryption happen there. The relay forwards bytes it cannot read. The bridge does the same on the far side.
There is no server side SSH client anywhere in the product.
03Trust boundaries
| Component | Holds | Can read the SSH session |
|---|---|---|
| Browser | vault key, SSH credentials, bridge pairing secret | yes, it is the endpoint |
| Backend API | ciphertext, connection metadata, model provider credentials (readable, see limitations) | no |
| Relay | one HMAC key for its own tokens | no |
| Bridge (ESP32) | Wi-Fi credentials, its device token, its pairing secret, its rules | no |
The backend and the relay are not trusted with credentials and cannot open a connection on their own.
04Vault
Private keys, passphrases and passwords are encrypted in the browser before they are sent anywhere.
- Key derivation: Argon2id, 64 MiB, 3 passes, random salt per account.
- Cipher: AES-256-GCM via WebCrypto.
- The server stores the ciphertext, the salt and the parameters. It never holds the key.
- Changing the vault passphrase re-encrypts every stored secret in the browser and uploads them in one request, so an account cannot end up half rotated.
If the passphrase is lost, the stored secrets are lost. There is no recovery.
05Command approval
The assistant proposes commands. It cannot run them. Approval happens in the browser.
Each proposed command gets a risk tier from a fixed rule set, not from the model:
| Tier | Meaning | To run |
|---|---|---|
| 0 | read only | one click, or autopilot if the user enables it |
| 1 | changes state | one click |
| 2 | destructive or irreversible | type the hostname to confirm |
Unrecognised commands are tier 1. If the user edits a command before running it, the tier is recomputed.
Command output goes through a deterministic secret scanner before any model sees it. Tokens, keys, passwords and connection strings are replaced with typed placeholders. IP addresses, hostnames and email addresses can be scrubbed as well.
The assistant is instructed to treat command output as data, not instructions. That is a mitigation. The approval step in the browser is what actually stops a command from running.
06Host keys
The server host key is pinned on first use and a change produces a warning. The check runs inside the WASM client, so the relay cannot suppress or forge it. This applies to bridged connections the same way as direct ones.
07Bridges
A bridge is an ESP32-S3 flashed from the browser over USB and plugged into power on the private network.
The board opens one outbound TLS WebSocket to the relay and keeps it open. Nothing is forwarded on the router and nothing listens on the LAN. Unplugging the board removes the path.
During setup the user writes an allow list of subnets or hosts with ports, and an optional deny list. Default is deny. The rules are stored on the device and enforced there, after DNS resolution. They can only be changed over USB. The app keeps a copy for display.
A pairing secret is generated in the browser during setup, written to the board over USB, and kept in the user's vault. Every connection request is signed in the browser with that secret over the bridge id, destination host, port, timestamp and a nonce. The board checks the signature, the timestamp and the nonce before it connects anywhere. The server forwards that signature but cannot create one.
Wi-Fi credentials go from the browser over USB to the board. They are not sent to the service.
08Platform
- Three containers: API, relay, static frontend. The relay is on its own network and holds no database credentials or third party API keys.
- Every container runs non-root with a read only root filesystem, all Linux capabilities dropped, no-new-privileges, a non-executable temp mount, and CPU, memory and process limits.
- Row level security on every database table. The application database role is not a superuser and cannot bypass it.
- The public relay refuses private, loopback, link-local and cloud metadata address ranges.
- Model credentials the user connects (ChatGPT sign-in tokens, API keys) are stored server side, encrypted with a server key. See the limitations section.
09Limitations
- Anyone who can open a session has whatever access that SSH user has on the target machine.
- The browser is the endpoint. Malware or a malicious extension on the user's machine defeats the model.
- The vault passphrase is the root of trust. A weak passphrase is the weak point, and it cannot be reset.
- The firmware image is checked against the checksum in its manifest before flashing, but the manifest comes from the same origin as the image. Signed images and ESP32 Secure Boot are not implemented yet.
- The risk tier is computed on the backend, so the backend is trusted to label commands correctly. It still cannot run anything.
- Physical access to a bridge should be treated as full control of the board and disclosure of the Wi-Fi network it joined.
- Model provider credentials are the exception to the vault model. The backend has to send them to the provider on the user's behalf, so it stores them encrypted with its own key and can read them back. Unlike SSH secrets, an attacker with both the database and that key would obtain them. They are used only for that user's own requests, refreshed server side, and deleted when the user disconnects the provider. Moving them into the browser vault, with the backend seeing them only per request and storing nothing, is planned.
- The ChatGPT option uses OpenAI's device code sign-in for Codex, the same login the Codex CLI uses. It is not a partner integration, and OpenAI can change or withdraw it.
- The relay sees metadata: timing, volume, the user's address, and the destination host and port. It does not see session content.
- Prompt injection through command output is reduced by deterministic tiering and browser side approval, not eliminated.
- The project has not had an external audit.
10Review
I am most interested in findings on:
- the bridge protocol and firmware
- any way the server, the relay or a network attacker could obtain a credential or open a connection without the user's browser
- bypassing or misleading the approval flow
- tricking the tier classifier into under-rating a command
Detailed internal review notes are available privately on request. If you find a live issue, please report it to me before publishing it.