Posture
Posture
Deny-by-default, everywhere
The consistent rule: an unset secret disables the thing it gates, never opens it.
| Surface | With nothing configured |
|---|---|
API auth (api.token) |
No matching token → 401. |
| Per-org scope | A token for org A acting on org B → 403. |
| Webhook verification | No secret → the verifier refuses every payload. |
| CORS | No allow-list → no cross-origin browser access (never *). |
| Capability gate | No consume-license → all commercial capabilities locked (free tier). |
| Licensable plans | A plan absent from licensing.profiles → cannot be minted. |
| Enforcement (semantics) | Unknown/disabled meter or exhausted allowance → denied. |
| Console features | A feature never registered → page hidden, routes 404. |
Enforcement on a dependency outage is the one deliberately tunable case: it
fails open by default (CBOX_BILLING_INFRA_FAILURE=allow) so an infra blip does
not throttle paid traffic — the durable ledger reconciles the truth — or fails closed
(deny) for strict tenants. Semantics always fail closed. See
Metering & enforcement.
Secrets handling
- Never in git.
APP_KEY, DB creds, mailer creds, gateway keys, and the license signing key go in the environment / secret store..envis gitignored;.env.examplecarries only empty placeholders. - The signing key is never logged and never displayed. Only the license public key is ever shown (Licenses → Distribution, for offline hand-off).
- Build secrets are not layers. The commercial composition's
composer_authGitHub token is a BuildKit tmpfs mount, present only for thecomposer requirelayer — never written into an image layer. - No secrets in URLs. Hosted checkout/portal use opaque, expiring session tokens;
the enforcement/management APIs use bearer tokens in the
Authorizationheader.
Webhook signature verification
Inbound payment webhooks (/webhooks/{gateway}) are public — they carry no
bearer token — because authenticity is proved by the gateway signature the bound
WebhookVerifier checks:
- The manual verifier is deny-by-default: no
CBOX_BILLING_WEBHOOK_SECRET→ every payload refused. - The Stripe adapter binds its own verifier from
STRIPE_WEBHOOK_SECRET. - Ingest is exactly-once (durable
ProcessedEventStore/SettledPaymentStore), so a re-delivery is a safe no-op. - Do not disable signature verification.
The webhook path is rate-limited per source IP (cbox-webhook) so a flood of forged
callbacks cannot exhaust the settlement path.
Tenant scoping
Every enforcement and management controller calls denyUnlessMayActFor($request, $org) — the authenticated token's identity must be allowed to act for the target org,
or the request is refused (403). A product-scoped token additionally refuses another
product's plan. One tenant's token can never read or mutate another's data, even
though all tenants share the same /api/v1 surface. See
API → Authentication.
Operating securely
- Keep
APP_DEBUG=falsein production; terminate TLS in front of the app. - Hold gateway keys and webhook secrets in the environment, never in git.
- Keep the durable stores on the database (they are the money source of truth).
The supply chain is gated
CI enforces a permissive-only dependency license policy
(composer license-check → MIT/BSD/Apache/ISC/0BSD), composer audit for known
advisories, and a drift-checked CycloneDX SBOM (composer sbom). See
Getting started → Running the tests.