CloudCMD
Trust & Security

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.

Release 0.1.0 2026-08-13 cloudcmd.com

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:

  1. 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.
  2. 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.
  3. Explicit credentials only. A connection uses the key or password you selected in CloudCMD — never a silent fallback to ~/.ssh/id_* or ~/.ssh/config.
  4. 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.
  5. 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.json on 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 ssh options, 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.uid plus 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.

CloudCMD process architecture Sandboxed renderer talks to the Electron main process through a preload context bridge. Main holds the vault and cloud SDKs. A forked PTY host owns node-pty sessions. RENDERER UI (Chromium) sandbox + isolation no vault keys no nodeIntegration ciphertext + handles preload contextBridge MAIN PROCESS Privileged core LocalVault · CEK cloud SDKs by handle in-memory SSH agent IPC sender checks Forked PTY host · node-pty OS browser (allowlisted)
Figure 1. Renderer sandbox, preload bridge, privileged main process, forked PTY host.
  • 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 calls window.vaultApiMain, window.awsApiMain, and similar facades; it cannot reach Node or ipcRenderer directly.
  • 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-pty so 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

Two-secret key derivation Master password is Argon2id-hashed with a stored salt. The Secret Key is mixed in via HKDF and XOR. Two independent keys are produced: the content encryption key and the log-in key. Master password never stored, never sent Hash salt per key, stored Secret Key device + Emergency Kit Account id HKDF salt input Argon2id (K1: 19 MiB, t=3) HKDF-SHA256(Secret Key) XOR → 256-bit key material CEK (encrypt vault) LIK (Auth password)
Figure 2. Password and Secret Key in; content encryption key (CEK) and log-in key (LIK) out. Each key has its own stored hash salt.
  1. 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.
  2. 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.
  3. 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

What is stored where The device vault holds ciphertext and the Secret Key. Firestore holds per-field ciphertext plus non-secret salts and queryable metadata. CloudCMD cannot decrypt Secure Data. ON YOUR DEVICE LocalVault (encrypted items) Secret Key (after unlock: CEK in memory) SSH agent sockets (no key bytes) App-managed known_hosts Keys never leave this machine SYNC BACKEND (FIRESTORE) Per-field JWE ciphertext Salts + keyring recipes (not secrets) Queryable metadata (ids, types) Account email / name (Service Data) No CEK, no master password, no SK
Figure 3. Device vs sync backend. CloudCMD employees do not hold the CEK.

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

SSH private key path Keychain private keys are stored as ciphertext, decrypted only in main, served from an in-memory agent, and consumed by OpenSSH with isolation flags that ignore ~/.ssh. keySecrets JWE ciphertext Main decrypt CEK never in UI In-memory agent 0700 socket / pipe OpenSSH -F /dev/null IdentitiesOnly + IdentityFile app known_hosts
Figure 4. Private keys are never written to a temp file and never enter the renderer in plaintext.

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/config entirely.
  • -o IdentitiesOnly=yes — only keys we name, via the agent.
  • -o IdentityAgent=… — pin our socket, ignoring SSH_AUTH_SOCK in the environment.
  • -o IdentityFile= a temp .pub (key mode) or none (password mode) — this is what suppresses OpenSSH’s compiled-in default ~/.ssh/id_* files.
  • -o UserKnownHostsFile= under CloudCMD userData — 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.