Security Design White Paper
CloudCMD Security Design
How secrets, credentials, and sessions are protected. This is the design we shipped — keys, encryption, process isolation, sync, and the limits of that design.
Chapter 1
Principles
CloudCMD is a desktop app for operating AWS, Google Cloud, Azure, Tailscale, and Kubernetes. It stores SSH keys, cloud credentials, and workspace configuration in an encrypted vault, then uses those secrets to open terminals and call cloud APIs as you. Security is not a feature bolted onto that job. It is the constraint the rest of the product is built under.
Five rules govern the design:
- Only you have the keys. Sensitive vault fields are encrypted on your device before they are synced. CloudCMD stores ciphertext. We cannot read it, recover it, or hand it to anyone in plaintext — including ourselves under compulsion.
- The master password and derived keys never leave the device. They are not transmitted to CloudCMD, not written to Firestore, not sent to Sentry, and not included in AI prompts.
-
Explicit credentials only. A connection uses the key or password
you selected in CloudCMD — never a silent fallback to
~/.ssh/id_*or~/.ssh/config. - Keys live in a privileged core, not the UI. The Chromium renderer is sandboxed. Vault crypto, cloud SDKs, and the SSH agent run in the Electron main process (and a forked PTY host). The UI holds ciphertext and opaque handles.
- Say what we cannot do. Chapter 10 lists the limits. A security paper that only recites strengths is advertising.
Plain-language Trust pages live at cloudcmd.com/security. The legal Privacy Policy is at /privacy. This paper is the technical account.
Chapter 2
Threat model
We design against attackers who can obtain some of the system, not all of it.
In scope
- Stolen sync database. An attacker who copies Firestore (or receives it via a backend compromise) should see ciphertext, salts, and non-secret metadata — not SSH private keys, cloud refresh tokens, or notes.
-
Stolen local vault file.
vault.jsonon disk is encrypted. Opening it requires the master password and the Secret Key. - Stolen authentication verifier. The Firebase Auth password is a derived log-in key, not the master password. Possessing that verifier does not yield the content encryption key.
- Compromised renderer while you are working. XSS, a malicious dependency, or a future DOM sink should not receive raw vault keys, should not navigate the app to an attacker origin, and should not open arbitrary external URLs. Blast radius is reduced by sandbox, CSP, IPC sender checks, and an external-URL allowlist.
-
Credential confusion on SSH. Cloud metadata and user-typed
usernames must not inject extra
sshoptions, and OpenSSH must not offer keys CloudCMD did not select. -
Cross-account reads. Guessing another user's document id must
fail. Firestore rules bind every path to
request.auth.uidplus a verified email.
Out of scope (deliberate)
- A malicious process with the privilege to read CloudCMD's memory on an unlocked machine. That is the operating system's job (screen lock, Full Disk Access, SIP/Harden). Idle auto-lock shrinks the window; it does not replace the OS.
- A supply-chain compromise of the signed CloudCMD binary itself. Updates are signed; users should install only from cloudcmd.com or the in-app updater.
- The cloud providers you connect to. When you open a terminal or run discovery, AWS/GCP/Azure/Tailscale see that traffic under your account. We are not a content middlebox for SSH.
Chapter 3
Process architecture
CloudCMD is a standard three-process Electron app plus a fourth process for PTY work. The trust boundary is the main process, not the page you look at.
-
Renderer — React UI.
sandbox: true,contextIsolation: true,nodeIntegration: false, no<webview>. It never holds the content encryption key or the Secret Key. -
Preload — a narrow
contextBridge. The UI callswindow.vaultApiMain,window.awsApiMain, and similar facades; it cannot reach Node oripcRendererdirectly. - Main — LocalVault, key derivation, JWE, cloud SDKs (AWS, GCP, Azure, Kubernetes token minting), in-memory SSH agent, permission and navigation guards.
-
PTY host — a forked child running
node-ptyso a wedged session cannot block the UI thread. Isolation flags for SSH are stamped in main and applied inside this host, so a renderer arg-builder cannot forget them.
IPC handlers accept calls only from the app's own top-level frame
(file: in production, the local Vite origin in development). Embedded
or navigated-away frames are rejected.
Chapter 4
Account secrets and key derivation
Unlocking CloudCMD requires two secrets you hold and we never store: a master password you choose, and a Secret Key the app generates at sign-up. Either one alone is insufficient. This is the same two-secret idea 1Password calls 2SKD, implemented for our vault and for Firebase Authentication.
Master password
You choose it. We do not store it, do not hash it for reuse as a login, and cannot reset it. Before derivation it is trimmed and NFKD-normalized so visually identical Unicode does not fork keys across devices.
Secret Key
Generated at account creation: 30 symbols from a 32-character alphabet
(A–Z and 2–7, no ambiguous 0/1/I/O) —
150 bits of entropy. It is printed as a versioned, grouped string
(T0-XXXXX-XXXXX-…) and saved in the local vault. You also get an
Emergency Kit PDF so a new device can be enrolled without copying
files. The Secret Key is not exportable to the renderer over IPC.
Deriving two keys from two secrets
- A hash salt is stored per key (HKDF-SHA256; 32 bytes). Salts are not secrets — they exist so two accounts with the same password do not share derived keys, and so we can version the recipe.
- Argon2id hashes the master password with that salt. Key version K1 uses 19 MiB of memory, 3 iterations, parallelism 1, 32-byte output — at or above the OWASP minimum for this memory cost. The keyring versions that recipe so we can raise cost later without guessing which blobs used which parameters.
- A second 32-byte value is derived from the Secret Key and account id via HKDF-SHA256, then XORed with the Argon2id output. Without the Secret Key, a password-guessing attack on a stolen salt does not produce a usable CEK.
That material becomes two independent keys, each with its own salt and key id:
-
CEK (
kid: cek) — encrypts vault items and Firestore Secure Data (JWE). -
LIK (
kid: lik) — is the Firebase Auth password. The authentication server stores a hash of a derived key, not of your master password.
Why the log-in key exists. Traditional “hash the password and store the hash” lets an attacker who steals the verifier run an offline guess against the human-chosen secret. Mixing in the Secret Key makes that verifier useless without a second secret that is not on the server. Signing in still looks like email + password. Cryptographically it is email + LIK.
Keyrings and labeled ciphertext
Every derived key is a generation in a keyring: algorithm version,
hash salt, Secret Key salt version. Ciphertext is JWE compact
(alg: dir, enc: A256GCM) with authenticated header
fields kid, gen, kv, and fmt.
Decrypting a field looks up the generation, derives that key, then runs AES-256-GCM.
We can raise Argon2id cost or rotate keys without guessing which blob used which
recipe.
Chapter 5
How vault data is secured
Local vault
Encrypted items live in userData/<email>/vault.json, written by
the main-process LocalVault singleton. The CEK is in memory only while
unlocked. After idle auto-lock it is dropped.
Per-field Firestore encryption
Every workspace document is a model with an allow-list of
getPlaintextFields(). Listed fields stay plaintext so they can be
queried (where / orderBy). Every other leaf is encrypted
with the CEK in main before the write. Hydration decrypts through the same IPC.
There is no server-side decrypt.
Classification (shipped)
| Item | At rest | Why |
|---|---|---|
| SSH private keys + passphrases | Encrypted (sibling keySecrets/ docs) |
Never listed in the tree; read raw at connect and forwarded to main |
| Cloud credentials, OAuth refresh tokens, notes | Encrypted | Secure Data — even we cannot read them |
| SSH username on a server | Encrypted | Sensitive identifier |
| Key fingerprint, public key, key type | Plaintext | Non-secret; needed for UI and association |
| Salts and keyring recipes | Plaintext | KDF inputs, not secrets; a new device must read them before it holds the key |
| Account email, name | Plaintext | Service Data — required to run the account |
| Device hostname / OS | Plaintext | Account-scoped device list; not cross-user readable |
Two kinds of data
Secure Data is ciphertext we cannot decrypt. Service Data is what we need to operate the product: email, billing via Stripe (card numbers never enter our vault; Stripe is PCI), support mail, and crash reports after secret scrubbing. Compulsory process can obtain Service Data and ciphertext. It cannot obtain your master password, Secret Key, or CEK from us — we do not have them.
Chapter 6
Authentication, unlock, and devices
Sign-up
The client generates a Secret Key, derives CEK and LIK, creates the local vault, publishes salts and keyrings to Firestore, and registers the LIK as the Firebase Auth password. Email verification is required before full data access.
Sign-in on a device that already has a vault
Email + master password. Main loads the Secret Key from the local vault (it is not fetched from the UI), re-derives CEK/LIK, and authenticates to Firebase with the LIK. If a key-version migration is in flight, the client tries a small candidate set of log-in keys rather than a single guess — Auth and Firestore cannot be updated in one transaction.
New device
There is no vault file yet. You complete an email-link bootstrap, then paste the Secret Key and enter the master password. The client derives the LIK from published salts, signs in, then establishes the CEK version from the published CEK keyring, proving it by decrypting a ciphertext. A wrong Secret Key or password fails closed: no half-initialized vault is left behind.
If salts are missing entirely (account reset / re-enrollment), a new CEK is created. Old ciphertext stays undecryptable. That is intentional.
Lock is not log out
After you stop using CloudCMD (default 15 minutes, configurable 1–240), main
drops the CEK and shows Unlock. Using the app — clicking,
navigating, typing in a terminal — postpones that timer. Firebase stays signed in.
Live PTYs and in-memory SSH agents keep running so an idle lock does not kill an
in-flight session. Log out is the destructive path: Firebase
signOut, teardown of sessions and agents.
Auto-lock is not your laptop’s screen lock. Its job is bounding how long the CEK sits in process memory. Lock the OS when you step away.
Chapter 7
Cloud credentials and terminals
Opaque handles in main
AWS and GCP credentials are registered in main and referred to from the renderer by
handle, not by passing secret material on every call. The AWS SDK runs in main; the
renderer has no @aws-sdk imports. Kubernetes bearer tokens are minted
and cached in main and injected at PTY spawn. CSP connect-src therefore
does not include *.amazonaws.com — the UI does not call AWS.
Discovery, metrics, SSO, and kubectl run because you started them in the app. Traffic goes to the provider you already have an account with. CloudCMD is not on the SSH data path.
In-memory SSH agent
At connect, the renderer reads the keySecrets/{id} document
raw and forwards ciphertext to main. Main decrypts with the CEK, parses
the key in memory, and serves it from a per-session agent on a unique
0700 Unix socket (or Windows named pipe). System ssh
uses SSH_AUTH_SOCK. The socket is torn down on session end, spawn
failure, log out, and app quit. Vault lock does not tear it down — the
session is allowed to finish.
SSH isolation flags
Every plain SSH launch (terminal, metrics shadow, Docker-over-SSH, AI ephemeral VM, Settings auth probe) goes through one chokepoint that prepends:
-F /dev/null— ignore~/.ssh/configentirely.-o IdentitiesOnly=yes— only keys we name, via the agent.-o IdentityAgent=…— pin our socket, ignoringSSH_AUTH_SOCKin the environment.-
-o IdentityFile=a temp.pub(key mode) ornone(password mode) — this is what suppresses OpenSSH’s compiled-in default~/.ssh/id_*files. -
-o UserKnownHostsFile=under CloudCMDuserData— never the user’s~/.ssh/known_hosts.
Hostnames and usernames taken from cloud metadata are argv-validated (no leading
-, no whitespace injection). Args are always arrays, never a shell
string. GCP IAP uses gcloud compute ssh with its own isolated env;
instance/project/zone still go through the same validators.
AI stays out of the vault
Optional command generation sends your prompt and limited terminal/cloud context through a stateless Cloud Function to a model provider. The Anthropic key stays server-side; your cloud credentials do not. Vault secrets, master password, Secret Key, and OAuth tokens are not in that payload. Generated commands render as an approval card; destructive patterns require a typed confirm. Read-only describe tools run on-device against already-loaded providers.
Google Cloud OAuth (Limited Use)
When you connect Google Cloud, you authorize CloudCMD to call Google APIs on your behalf. Use of that data follows the Google API Services User Data Policy, including Limited Use. Refresh tokens are Secure Data (encrypted before sync). Access tokens are minted in the app and are not stored at rest. We do not use Google user data for advertising, sale to brokers, credit decisions, unrelated databases, or training AI/ML models. Revoke in the app and at Google Account permissions.
Chapter 8
Renderer and release hardening
Chromium / Electron checklist
The single BrowserWindow pins safe webPreferences
explicitly so a version bump cannot silently regress them:
sandbox, contextIsolation, no nodeIntegration
(including subframes), no webviewTag, no
allowRunningInsecureContent, no experimentalFeatures.
Packaged builds disable DevTools.
Content Security Policy
Enforcing CSP (not report-only). default-src 'self';
script-src 'self' in production; object-src 'none';
frame-ancestors 'none'. connect-src is the signed-in
app’s real destinations: Firebase Auth/Firestore, callable functions, and Sentry.
Cloud-provider API hosts are absent because those SDKs run in main.
Permissions and navigation
Session permission handlers are default-deny (camera, mic, geolocation, HID/USB, notifications, …). Clipboard read/write is allowed for terminal copy/paste. Display capture is granted only when Sentry user-feedback screenshots are opted in, and only for the app’s own frame.
setWindowOpenHandler / will-navigate refuse to navigate
the window. http(s) URLs are handed to the OS browser, and only if
the hostname is on an allowlist built from real destinations (CloudCMD, AWS
Console/SSO, Google, Azure, Firebase Auth, Stripe, GitLab). Anything else is
dropped; logs record the host, never the path (paths may carry tokens).
Observability without secrets
Sentry in both processes runs a shared scrubber: key-name denylist
(secretKey, tokens, passwords, …) and value-shape redaction (PEM,
AWS keys, Stripe live/test secrets, Google API keys, JWTs). Source maps are
hidden, uploaded at release, then stripped from the package.
What we ship
Desktop releases are code-signed. Auto-update authorization goes through Keygen.
A CI scan-artifact job unpacks app.asar and hard-fails
on shipped source maps, TypeScript sources, dotenv, PEM, or SOPS files from our
build. Secret-shaped strings in vendored examples are advisory so the
AWS SDK’s documented AKIA…EXAMPLE keys do not fail the pipeline.
Chapter 9
What the server can and cannot do
Encryption is client-side, but access control is still enforced on the server. Cryptography without authorization would let any signed-in client ask for another user’s ciphertext.
Firestore rules
Every user-owned path requires request.auth.uid == userId and a
verified email. Full read/write of workspace data additionally requires a
password Auth session (the LIK). Email-link bootstrap sessions
are narrower — enough to enroll a device, not to dump the workspace. Billing
fields on the user document are writable only by Cloud Functions (Stripe
webhooks via the Admin SDK); the client may read them and cannot alter them.
What we store
- Encrypted Secure Data (opaque JWE strings).
- Salts and keyring recipes, so a new device can derive keys.
- Service Data: email, name, device list, subscription state.
- AI usage aggregates (token counts), not prompts or vault contents.
We do not store master passwords, Secret Keys, CEKs, or cloud access tokens at rest. We cannot decrypt Secure Data to “help” with a forgotten password.
Transport
Client ↔ Firebase and client ↔ Cloud Functions use TLS. Vault keys are not in those requests. Cloud API calls that need credentials originate in main, on your machine, to the provider.
Chapter 10
Caveats
This chapter is the honest part. The design above is real and implemented. These are the edges.
Unlocked means the UI can see decrypted workspace fields
While the vault is unlocked, Firestore hydration decrypts documents so the Environments tree can render. Some credential material still crosses the renderer on that path before providers register handles with main. A compromised renderer during an unlocked session is therefore still a serious event — as it is for any password manager whose UI must display or use secrets. The CEK and Secret Key stay in main; the remaining work is narrowing that decrypt surface further.
Idle lock is not an OS lock
Auto-lock fires after you stop using CloudCMD. Using the app postpones it. It does not pause a live SSH session, and it does not replace locking your laptop. Step away: lock the OS.
Plaintext that is supposed to be plaintext
Salts, keyrings, public keys, fingerprints, and some resource metadata are stored unencrypted by design. A Firestore compromise reveals that you have an account, device names, and which clouds you connected — not the keys to them.
We cannot recover your data. Not in any situation.
There is no back door, no password reset, no support tool, and no employee who can decrypt your vault. We do not have the keys. If you forget your master password and lose the Emergency Kit, that Secure Data is gone — we cannot reconstruct it, restore it, or “look it up.” You can start a new vault with new keys; anything already synced stays ciphertext forever.
Malware on your machine
Same limit every local vault has, including apps such as 1Password: malware that can inspect process memory while CloudCMD is unlocked can extract secrets from that process. Malware that can replace the CloudCMD binary and have you run it can do the same. After malware has that kind of power, no app on the device can protect you.
That is still a stricter posture than the Unix default. OpenSSH leaves private
keys on disk as ordinary files (~/.ssh/id_ed25519 and friends) —
readable to any process running as you, whether or not a terminal is open.
CloudCMD never writes the private key to a file. While the app is closed, the
CEK is not in memory and the keys exist only as ciphertext in the vault. If the
machine is compromised, CloudCMD still protects your keys far better: they are
fully encrypted, not sitting as plaintext on disk.
Report a vulnerability
Email security@cloudcmd.com or help@cloudcmd.com. Give us a reasonable window to fix before public disclosure.
Appendix A
Algorithms
| Role | Construction |
|---|---|
| Password KDF (K1) | Argon2id, m=19456 KiB (19 MiB), t=3, p=1, tag=32 bytes |
| Salt / Secret Key mix | HKDF-SHA256 → 32 bytes; XOR with Argon2id output |
| Secret Key | 30 × 5-bit symbols (alphabet A–Z2–7), version T0 |
| Field encryption | JWE compact, dir + A256GCM, labeled kid/gen/kv/fmt |
| Auth password | Derived LIK (not the master password); Firebase Auth stores its hash |
| SSH host keys | OpenSSH, app-managed known_hosts under userData |
| Transport | TLS to Firebase / Cloud Functions; provider APIs from main |
Appendix B
Glossary
| Term | Meaning here |
|---|---|
| CEK | Content encryption key — AES-256-GCM key for vault and Secure Data |
| LIK | Log-in key — derived key used as the Firebase Auth password |
| Secret Key | Account-level second secret; on device and Emergency Kit, never on the server |
| Secure Data | Client-side-encrypted fields CloudCMD cannot read |
| Service Data | Account/billing/support data needed to run the product |
| Keyring | Versioned list of KDF recipes so ciphertext stays decryptable across migrations |
| Handle | Opaque id the renderer uses to name a credential that lives in main |
CloudCMD Security Design, release 0.1.0 (2026-08-13). This document describes the shipped desktop app. It is not a guarantee of fitness for a particular regulatory regime. Questions: security@cloudcmd.com.