> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useduro.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Security Model

> Tenant isolation, RBAC, secret handling, and the real findings from an adversarial audit pass — with fixes.

Security in a billing system isn't a feature, it's the substrate. Duro's posture rests on a few structural choices and a documented audit pass that found — and fixed — real issues.

## Isolation by construction

The strongest control is the one you can't forget to apply. Tenant isolation is structural, not a runtime check:

* **Modes are separate schemas**, not a column. A `data('test')` client cannot read live rows. [Data model →](/architecture/data-model)
* **Every repository is tenant-scoped** at construction. There's no repository method that omits `tenantId`. [Request lifecycle →](/architecture/request-lifecycle)
* **Idempotency keys are per-tenant** (`@@unique([tenantId, key])`) — closing a cross-tenant replay vector found early in the build.

## RBAC

Dashboard members have one of five roles; the `PermissionGuard` gates every write:

```mermaid theme={null}
flowchart TB
    ROLE["Member role"] --> OWNER["owner — everything"]
    ROLE --> ADMIN["admin — everything except tenant:manage"]
    ROLE --> DEV["developer — plans, customers, subs,<br/>invoices, api keys, webhooks, appearance"]
    ROLE --> FIN["finance — plans, customers, subs,<br/>invoices, refunds"]
    ROLE --> RO["read_only — read"]
```

Machine credentials (`sk_` keys, OAuth tokens) carry scope `*` — a server integration acts with full authority within its one tenant. Human members are role-gated. Sensitive roles (owner, admin) are flagged for 2FA enforcement. Team changes — invite, role change, removal — are written to an **audit log**, and the owner is protected (can't be demoted or removed, can't remove yourself).

## Secrets, hashed and timing-safe

* **API keys and OAuth secrets** are stored as `sha256` hashes; the plaintext is shown once at creation and never again.
* **OTP codes** are stored hashed; verification is a timing-safe compare.
* **Webhook signing secrets** are shown once; signatures are HMAC-SHA256 with a timing-safe verify and timestamp tolerance.
* **Real provider credentials** (WhatsApp token, SMTP, R2) live only in gitignored `.env.local` — never committed. The staff KYC key is compared timing-safe.

## The audit pass — real findings, real fixes

A deliberate adversarial review found issues a casual build would ship. Each was fixed and is covered by tests.

<AccordionGroup>
  <Accordion title="HIGH — Webhook SSRF → metadata exfiltration" icon="triangle-exclamation">
    A webhook `url` was validated only as a URL, and the delivery **response body is stored and shown** in the inspector. A merchant could point it at `169.254.169.254` and read cloud-metadata IAM credentials. **Fixed** with `WebhookUrlGuard`: require https, block private/loopback/link-local/CGNAT/metadata hosts (v4 + v6) at creation, and re-resolve DNS at delivery to defeat rebinding. [Webhooks →](/webhooks/delivery)
  </Accordion>

  <Accordion title="MEDIUM — Blacklist was decorative" icon="ban">
    Customers could be flagged `blacklisted` but nothing enforced it. **Fixed**: subscription creation and checkout both reject blacklisted customers — and checkout rejects **before** the charge, so no money moves for a banned customer.
  </Accordion>

  <Accordion title="Cross-tenant idempotency replay" icon="clone">
    The idempotency record was globally unique on `key`. **Fixed** to `@@unique([tenantId, key])`.
  </Accordion>

  <Accordion title="RBAC bypass on merchant writes" icon="lock-open">
    Members originally received a wildcard scope. **Fixed**: members get role-derived scopes and merchant writes are behind `PermissionGuard`.
  </Accordion>

  <Accordion title="Mode-scoping leaks" icon="layer-group">
    Two request-scoped paths (the portal listing and checkout token lookup) iterated **both** modes. **Fixed**: the portal takes the current mode explicitly; checkout derives mode from the token prefix. Only background scanners loop both modes.
  </Accordion>
</AccordionGroup>

## Performance is part of the posture

A few fixes from the same pass that keep the system honest at scale: recovered/at-risk revenue is summed **in the database** rather than loaded into Node; the portal listing was de-N+1'd to four batched queries; and indexes were added for the new hot paths (customer phone/email, subscription `customerId`). [Multi-tenancy →](/architecture/multi-tenancy)

## What's deliberately deferred

The charge gateway is a deterministic simulator; the live payment-rail integration (and the money-movement side of refunds and payouts) lands in the July window. Everything in this page — isolation, RBAC, secrets, the audit fixes — is in place today.

Next: a deeper look at the [SSRF guard and egress controls](/security/ssrf-and-egress).
