Azalea security model
Edit on GitHubTrust boundaries, what we defend against, what we do not, and how to report issues.
Reporting
Email security@rexsystems.me with:
- Component (
apps/desktop,services/azalea-server,apps/azalea-server-web, telemetry, or CI). - Version or commit.
- Reproduction steps.
- Expected impact.
Do not open a public GitHub issue for anything you believe is exploitable. We aim to acknowledge within 72 hours. Coordinated disclosure is welcome; we credit you in the release notes unless you ask otherwise.
Components and trust boundaries
| Component | Runs as | Trust level | Notes |
|---|---|---|---|
apps/desktop (Tauri client) |
End user | User-trusted | Holds decrypted secrets in memory. |
services/azalea-server |
User (self-host) | Sees only ciphertext | Never handles vault plaintext. |
| Personal cloud (Drive / OneDrive / Dropbox) | User’s provider | Sees opaque vault file + metadata | OAuth app-folder only; no Azalea server in the path. |
apps/azalea-server-web (in-repo) |
Static export behind nginx | Public | Login and admin UI for the sync API. |
~/projects/azalea-web (marketing) |
Cloudflare Pages | Public | No secrets; no auth surface. |
azalea-telemetry (private) |
Rexsystems only | Opt-in, aggregate | Not part of the sync trust chain. |
Users pick personal cloud storage or a self-hosted sync server. The desktop app never sends vault plaintext; remote storage holds an opaque encrypted blob. Telemetry is a separate service. Sync, auth, and updates do not consult it.
Threat model
We assume the attacker can:
- Observe the network. All production traffic uses TLS. The desktop app
refuses
http://sync URLs unless the host islocalhostor an RFC1918 private IP. - Compromise the sync server. They see ciphertext and metadata, not vault
contents. AEAD (AES-256-GCM) with AAD binds each ciphertext to
(account_id, version, kdf_salt)so blobs cannot be swapped between accounts, rolled back to older versions, or replayed across accounts. - Own the remote SSH server the user connects to. They can show arbitrary
text in the terminal (do not paste that blindly). We pin modern SSH KEX /
cipher / MAC lists and drop HMAC-SHA1 by default (opt out with
AZALEA_ALLOW_LEGACY_SSH=1). SFTP downloads stay under$HOME; uploads reject reads from~/.ssh,~/.gnupg,~/.aws,~/.azure,~/.kube,~/.docker, and~/.password-store. Terminal hyperlinks (OSC 8) accept onlyhttps://,mailto:, orhttp://www.. - Trick the user through the web UI. Web login uses HttpOnly Secure SameSite refresh cookies; access tokens live 10 minutes and stay out of JavaScript. Desktop handoff uses PKCE (S256) with single-use handles and 60-second codes; the desktop client checks the returned callback state and refuses non-loopback callback ports. Reset-password tokens are stripped from the URL bar on load.
- Guess passwords. Auth endpoints are rate-limited per IP and per identifier-hash. Login and register run in constant time (missing users still hit a dummy Argon2id verify) and do not reveal whether an account exists. Password KDF is Argon2id with server-side parameters chosen to burn more than 100 ms per attempt.
- Reach the marketing site or admin panel. Both sites ship CSP,
X-Frame-Options: DENY,Referrer-Policy: strict-origin-when-cross-origin, and a locked-downPermissions-Policy. The self-host dashboard CSP allows'unsafe-inline'scripts because Next.js static export injects flight payloads; Turnstile is limited tochallenges.cloudflare.com. The desktop Tauri window setsframe-ancestors 'none'.
We do not defend against:
- A compromised end-user device. Root on the user's machine can read decrypted secrets from process memory or the keychain. Encryption at rest helps against offline theft of the app data directory, not against an active local attacker.
- A malicious SSH server exfiltrating what the user types in that server's terminal. SSH cannot sandbox what the remote shell shows.
- Users who paste installer commands from untrusted sources. We publish
our own
install.shand recommend digest-pinning docker pulls. A shell one-liner still runs on trust.
Cryptography summary
- Password to KEK. Argon2id with per-account salt (>= 16 bytes,
OsRng). - Vault encryption. AES-256-GCM AEAD. Blob layout
[0x02 | nonce(12) | ciphertext]for the current v2 format. AAD ="azalea-vault-v1|" + account_id + "|" + version + "|" + kdf_saltand binds every ciphertext to its account and monotonically increasing version. Legacy v1 blobs (no AAD) still decrypt for one-shot migration; new writes are always v2. - Sync rollback protection.
keep_cloudrefuses any remote vault whoseversion <= last_known_version. - Sync auth. JWT access tokens (10 min). Refresh tokens live in HttpOnly Secure SameSite=Lax cookies on the web client and in the OS keychain on desktop. Sessions are revoked on password change and admin disable.
- CAPTCHA. Cloudflare Turnstile on register / forgot / reset endpoints.
- CSRF. SameSite=Lax cookies plus a bespoke
x-azalea-clientheader requirement on refresh; desktop clients sendx-azalea-client: desktop.
Build and release integrity
- All GitHub Actions are SHA-pinned in
.github/workflows/*.yml. - Tauri signing secrets (
TAURI_SIGNING_PRIVATE_KEY[_PASSWORD]) exist only in the env of the specific build and signer steps, not job-wide. services/azalea-server/install.shrefusescurl | shof external installers, requires Docker via the OS package manager, and prints the resolved image digest afterdocker compose up -d. Consumers can pin withghcr.io/rexsystems/azalea-server@sha256:....- Sensitive filename patterns (
*.pem,*.key,*.p12,id_rsa*,minisign.key, and similar) are in.gitignoreto reduce accidental commits.
Dependency audits
See `security-audit.md` for the current cargo audit /
npm audit findings and the acceptance rationale for each. Re-run before every
release:
cd services/azalea-server && cargo audit
cd apps/desktop/src-tauri && cargo audit
cd apps/desktop && npm audit --omit=dev
cd apps/azalea-server-web && npm audit --omit=dev
Deployment checklist
When you operate a self-hosted azalea-server:
- Set
AZALEA_JWT_SECRETto a random 32-byte value; the server refuses to boot in prod without one. - Set
AZALEA_ALLOWED_ORIGINSto the origins that will host the web client; the server sendsAccess-Control-Allow-Credentials: trueand requires an explicit allowlist. - Terminate TLS in front of the server (nginx, Caddy, Cloudflare, or similar).
Refresh cookies are set
SecureunlessAZALEA_ALLOW_INSECURE_COOKIE=1explicitly enables HTTP cookies for a localhost/LAN dashboard, including Docker bridge deployments. The installer leaves that flag disabled for HTTPS/Cloudflare modes. - If you fork the telemetry service, rotate
TELEMETRY_ADMIN_TOKEN. It must be at least 24 characters, and the service refuses to boot in prod without one.