Skip to content

Security model

Every repository gets its own storage namespace, and every object key is derived from it:

<storage_namespace>/objects/<oid[0:2]>/<oid[2:4]>/<oid>
<storage_namespace>/cas/<tree relative path>

No API path lets a caller name a key outside the namespace of the repository it resolved. Path traversal is rejected explicitly: the static-tree handlers reject any relative path containing an empty segment, . or .. before a key is ever built.

Authorization runs before any storage interaction. A request for a repository you can’t reach fails at the guard, so it never reaches the object store — and never reveals whether the object exists.

Data At rest In transit
Object bytes (managed storage) Cloudflare R2 server-side encryption TLS 1.2+ to R2
Object bytes (BYOS) your bucket’s configuration — yours to set TLS to your endpoint
Metadata (MongoDB) provider-managed encryption at rest TLS to the database
BYOS credentials AES-256-GCM, application-level TLS
API tokens SHA-256 hash only — the secret is never stored TLS
Invitation tokens hashed TLS

BYOS secret access keys are encrypted with a master key held outside the database, are never returned by any API response, and are never written to logs. Rotating that master key requires re-encrypting the stored connections.

API tokens — generated from a cryptographically secure random source, stored only as a SHA-256 hash, compared in constant time. The secret is shown once. Every use updates last-seen time and IP so you can spot a token being used from somewhere it shouldn’t be.

Presigned URLs — the data plane’s mechanism for keeping bytes off the control plane. They are bearer credentials for one operation, expiring in 15 minutes by default. CAVS never logs them, and neither should you: not in error reports, not in CI output, not in a support ticket.

SDKs and CLI are required to redact tokens from string representations, exception messages and logs, and never to log full request headers.

TLS is required for cavsnode.com. HSTS is emitted with a one-year max-age and includeSubDomains on any request that arrived over TLS.

Custom CA bundles are supported for self-hosted deployments and corporate proxies via CAVS_CA_BUNDLE in the SDKs. Clients cap redirects at five and always set connect and read timeouts.

The API is a JSON service and never serves an HTML application surface, which lets the response headers be locked all the way down:

Header Value Why
Content-Security-Policy default-src 'none'; frame-ancestors 'none' An echoed payload can never be treated as a live document
X-Content-Type-Options nosniff No MIME sniffing
X-Frame-Options DENY No framing
Referrer-Policy no-referrer URLs — including ones with ids — don’t leak
Cross-Origin-Resource-Policy same-origin No cross-origin reads
Cache-Control no-store Authenticated JSON is never retained by a shared cache
Strict-Transport-Security max-age=31536000; includeSubDomains TLS requests only

CORS allows only the configured dashboard origins, with an explicit method and header allowlist (Authorization, Content-Type, Idempotency-Key, Accept).

Request limits — control-plane bodies are capped at 1 MiB and unknown JSON fields are rejected rather than ignored, so a typo’d field name fails loudly instead of being silently dropped. Every request has a 60-second timeout.

Rate limiting — 30 requests/second sustained with a burst of 60, keyed per token, else per user, else per IP. Exceeding it returns 429 rate_limited.

Error responses never leak internals: an untyped server-side error becomes a generic 500 with a request_id, and the detail stays in the server logs.

Five inputs compose on every guarded request, and any one can deny:

organization role + repository override + token scope
+ account status + organization status

Two properties worth calling out:

  • Token scopes are a hard cap. A repo:admin token held by a VIEWER cannot push. Losing a role narrows every personal token you hold, immediately.
  • Sensitive operations are unreachable by tokens. Billing, audit reads, token management, storage-connection management and organization deletion require a session-authenticated role. No token scope reaches them.

Full detail: Roles & permissions.

Content addressing makes integrity structural rather than bolted on:

  • An object’s name is the SHA-256 of its bytes.
  • Chunks are addressed by BLAKE3-256, with a global Merkle root per object.
  • Clients hash while streaming and reject anything that doesn’t match its oid — the file is deleted rather than written to its final path.
  • Reconstruction is byte-identical or it fails. There is no partial success.
  • A corrupt local chunk cache is detected, quarantined and repaired in place.

A malicious or buggy storage layer therefore cannot hand you the wrong bytes without the client noticing.

Category Examples
Identity email, display name, avatar URL, auth provider — from Firebase
Organization name, slug, plan, subscription state
Repositories names, descriptions, visibility, storage config
Objects oids, sizes, physical sizes, chunk counts, timestamps, download counts
Git index commit metadata and messages, tree entries and paths, refs, tags
Operational audit events with IP and user agent, activity events, usage ledger
Billing subscription and invoice references held by the payment provider

CAVS does not store your passwords (Firebase handles authentication) or your payment card details (the payment provider does).

CAVS is responsible for: platform availability, control-plane and metadata security, tenant isolation, encryption of BYOS credentials at rest, presigned-URL scoping and expiry, managed-storage administration (lifecycle, integrity, GC, backups), and the HTTP hardening above.

You are responsible for: who you invite and at what role, token hygiene (scope, expiry, rotation, storage), what you put in your repositories, your BYOS bucket’s configuration (encryption, versioning, lifecycle, public-access settings, IAM), verifying webhook signatures before acting on payloads, and not leaking presigned URLs.

Report security issues privately, not in a public issue tracker. See the SECURITY.md in the engine repository for the disclosure process, or contact the platform operators directly. Include the request_id from any relevant response.