Skip to main content

Encryption

This page documents VolatiCloud's current encryption posture — what's encrypted, how, and where the keys live. The roadmap to put more of the trust boundary on your side (passphrase, passkey-bound E2EE, BYOK) is in the Threat Model — Roadmap.

The short version: sensitive config fields are encrypted at rest with AES-256-GCM before they reach the database, TLS 1.2+ everywhere in transit, and audit logs on every sensitive-field read — surfaced to organization admins and exportable.

What Is Encrypted at Rest Today

Sensitive fields are encrypted at the application layer (via ENT hooks and interceptors) before they reach the database. The cipher is AES-256-GCM with a fresh per-field nonce, and ciphertext is base64-encoded with a $vc_enc$v1$ prefix so the storage layer can distinguish encrypted from any pre-encryption plaintext during the migration window.

The fields covered today:

  • Exchange credentialsexchange.config.api_key, api_secret, password, private_key
  • Bot config secretsbot.secure_config.* paths declared by the bot package
  • Runner connection credentialsrunner.config.docker.certPEM, keyPEM, caPEM (Docker TLS materials), full kubeconfig payloads (Kubernetes runners)
  • S3 backup credentialsrunner.s3_config.accessKeyId, secretAccessKey

Architectural decision: ADR-0026 — Field-Level Encryption. Implementation: api/internal/secrets/.

Why field-level, not column-level

Field-level encryption keeps non-secret fields (exchange name, pair whitelist, timeframe, runner namespace, ingress class) plaintext and queryable. Column-level encryption would have made debugging operations and inspection queries dramatically harder for no security gain — the secrets are isolated to specific JSON paths regardless. ADR-0026 walks through the trade-off.

What Is Not Encrypted at Rest Today

We're explicit about this because the encryption story matters more for what it doesn't cover than for what it does:

  • Strategy source code (strategy.code) — plaintext in our database
  • Bot non-secret configuration — pair lists, timeframes, leverage, callbacks
  • Backtest results and trade history
  • Telemetry and monitoring data

These will move under user-controlled encryption progressively in Wave 1 and Wave 2 of the trust feature ladder. Until they do, defense rests on operational controls and audit logs (see below).

What Is Encrypted in Transit

Everything user-facing or service-to-service runs over TLS:

  • TLS 1.2+ on the public API and dashboard, with HSTS preload
  • mTLS on internal cluster service-to-service traffic, managed by the service mesh
  • Webhooks from Stripe are validated with Stripe's signed webhook secret
  • Runner connections use TLS:
    • Docker daemons must be exposed over mTLS (port 2376) — see Docker Runner
    • Kubernetes API access uses kubeconfig-embedded CA verification
    • Bot ingress URLs use TLS via cert-manager-issued certificates

Key Management Today

The master encryption key for field-level AES-256-GCM is supplied to the backend at startup via the VOLATICLOUD_ENCRYPTION_KEY environment variable (a base64-encoded 32-byte AES key). In production this env var is sourced from a Kubernetes Secret hydrated by the deployment workflow from GitHub Actions Secrets — the chosen secret-management strategy per ADR-0045 — Secret Management Strategy.

If the env var is not set on startup, encryption is disabled and operations become no-ops — this is the bootstrap path; in production deployments the key is always present.

Key rotation

Key rotation is supported via a dual-key reader pattern. The VOLATICLOUD_OLD_ENCRYPTION_KEYS env var can carry one or more previous keys; new writes always use the primary, but reads transparently fall back to old keys until all data has been re-encrypted under the new primary. This means key rotation can happen without downtime — old data is rewritten lazily on next write.

Per-field nonces

Each encryption operation generates a fresh GCM nonce. Reusing a nonce with the same key catastrophically breaks GCM — by generating a fresh nonce per field on every write, we avoid that pitfall.

Audit Logs

Every read of a sensitive resource is logged with:

  • Requester identity (user, role, or API client)
  • Resource ID and type
  • Timestamp and source IP
  • User agent

Audit logs are visible to organization admins via the dashboard and exportable to CSV for archival. They are the primary mitigation today against insider misuse: even if an operator can technically read your encrypted fields (because we control the master key), every read is logged and surfaced to you.

What's Coming — Customer-Controlled Encryption

The phased roadmap in the Threat Model tracks every encryption-related improvement. The most consequential near-term changes:

Trust Tier 1 — Customer-Supplied Passphrase

Your organization sets a passphrase. The decryption key is derived from it via Argon2id (the implementation already lives in api/internal/passphrase/), and combined with the server-held key via envelope encryption. Server alone cannot decrypt; passphrase alone cannot decrypt. The PHC-encoded Argon2id verifier itself is stored on the organization settings record so the platform can validate passphrase entry without ever holding the passphrase.

Trust Tier 2 — Passkey-Bound E2EE

Strategy code, bot secrets, and exchange keys are encrypted in your browser, with the wrapping key bound to your hardware passkey. We mathematically cannot decrypt without your authorization — even a hostile insider with full database access has no path to plaintext.

Trust Tier 3 — Bring Your Own Key (BYOK)

You bring your own master key (Lite) or federate into your own KMS — AWS KMS, GCP KMS, Azure Key Vault — for the wrapping key (Standard / Advanced). You can revoke our decrypt access with one IAM policy change.

How to Verify Today

You don't have to take our word for any of this. The verifiable artifacts: