Security model

Aroovo is multi-tenant and white-label, which means the platform's security model has to make “a hostile Brand cannot reach another Brand's data” and “a compromised Tenant cannot reach a sibling Tenant” structurally true, not operationally enforced. The controls below are what the platform ships today.

Per-tenant audit chain

Every action that mutates state writes an AuditEntry row. Each row stores an HMAC-SHA-256 of (content + previous-row-hash) keyed by a per-deployment secret (AUDIT_HMAC_SECRET) only the operator holds. The chain is scoped per Tenant, so verifying one Tenant's chain doesn't need to read another's. verifyChain(tenantId) recomputes the HMACs end-to-end and detects any tamper. In production, AUDIT_HMAC_SECRET is mandatory — the platform refuses to boot without it.

Encrypted secrets vault

Every Secret row's value is AES-256-GCM-encrypted keyed by SECRETS_ENC_KEY. Rows are uniquely indexed on (tenantId, name); the Brand layer wraps the vault by always scoping reads to the Brand-Owner's active Brand. Plaintext values are never logged and never returned over the API — only existence + creation metadata.

Per-tenant rate limit + spend cap

Inbound chat traffic on /api/agents/:slug/chat is rate-limited per (tenantId, userId); a compromised seat in one Tenant cannot burn another Tenant's quota. Spend is capped per Tenant against the SPEND_CAP_USD threshold; when a Tenant crosses 100% of cap the auto-pause fires and scopes to the offending Tenant only — siblings keep running.

SSRF protection on MCP bootstrap

Registering an MCP server URL is the largest external-input attack surface. We enforce: hostname allowlist for Zapier-managed endpoints, private IPv4 + IPv6 + loopback-name blocks, and HTTPS-only on production. The block list lives in src/platform/mcp/url-safety.ts.

Console security headers

The Next.js console emits a strict CSP that allows MSAL + the platform's bot host only, HSTS preload, X-Frame DENY, X-Content nosniff, Referrer strict-origin-when-cross-origin, and a Permissions policy restricting microphone to self. The /admin/* tree adds Cache-Control: no-store and X-Robots-Tag: noindex.

Bot body size cap

BOT_BODY_MAX_BYTES (default 1 MiB) caps the inbound request body on /api/messages/*. Slack and Teams webhooks both stay well under this; the cap is a DoS mitigation, not a feature limit.

What we don't yet have

  • SOC 2. Deferred from the roadmap as a cash-conservation call. Revisit when the design partner motion surfaces a buyer requirement.
  • Public status page. Sprint 6 item.
  • Per-Brand SSO break-glass. Today the platform superadmin is the recovery role if a Brand-Owner loses their IdP; a per-Brand emergency-access flow is on the backlog.

References