Security
Concrete, mechanical statements about how mb is built. No marketing copy disguised as security claims.
Tenant isolation
Every business table (15+ of them) has Postgres Row-Level Security enabled with the same policy shape:
ALTER TABLE <table> ENABLE ROW LEVEL SECURITY;
CREATE POLICY rls_tenant_isolation ON <table>
USING (tenant_id = NULLIF(
current_setting('app.tenant_id', true),
''
)::uuid);
GRANT SELECT, INSERT, UPDATE, DELETE ON <table> TO mb_app;
The application connects as mb_app — a role that
does not have BYPASSRLS. If the
request handler forgets to set the GUC, the cast fails and zero
rows are returned. There is no application-layer "WHERE tenant_id ="
that a future bug could remove.
Continuously verified by scripts/rls-fuzzer which
seeds two tenants, switches to mb_app, and asserts
cross-tenant invisibility on every CI push.
Authentication
- · Passwords: argon2id (memory-hard, GPU-resistant). Cost: 64 MiB memory, 3 iterations, 2 parallelism — re-tunable, with auto-rehash on next login when params change.
- · Sessions: server-side rows in
sessionstable. Logout actually invalidates. No JWT — we cannot lose-track-of an invalidated token. - · Cookies:
HttpOnly,Secure,SameSite=Lax. - · CSRF: double-submit cookie bound to the session ID, constant-time compared on every state-changing request.
- · Google OIDC: full JWKS verification on the ID token — no shortcut. State cookie 5-minute TTL to prevent replay.
- · Rate limits: 5/hour on signup, 10/5min on signin, 20/hour on Google OAuth start, 20/minute on the public quote endpoint.
Audit trail
Every mutation (signup, login, finalize, payment, credit-note
issue, quotation transition, supplier/purchase change) writes
a row to audit_log with the actor user, tenant,
entity, IP, user-agent, request ID, and before/after JSONB
payloads. Append-only is enforced at the DB:
CREATE OR REPLACE FUNCTION raise_immutable() RETURNS trigger ... $$ BEGIN RAISE EXCEPTION 'audit_log is append-only'; END $$; CREATE TRIGGER audit_log_no_update_delete BEFORE UPDATE OR DELETE ON audit_log FOR EACH ROW EXECUTE FUNCTION raise_immutable();
Same trigger pattern on ledger_entries. Tampering
with history requires the DB owner role, which the application
never has.
Data at rest & in transit
- · TLS: all traffic is HTTPS-only via Cloudflare. HSTS preload-ready.
- · Postgres: nightly snapshots encrypted at rest by DigitalOcean.
- · R2 object storage: PDFs server-side-encrypted; access via short-lived signed URLs only.
- · Secrets: API keys + DB password in
/etc/mb/mb.envon the droplet, mode 0600, never in environment variables exported to subprocess space. - · No client-side keys: there is no offline cryptography. All trust roots are server-side.
Continuous verification
- ·
govulncheckon every push (Go stdlib + module CVE scan, pinned to Go 1.25.10+). - ·
gosecvia golangci-lint — bans risky patterns (writable file modes, weak randomness, SQL injection vectors). - ·
forbidigobansfloat64in money paths (exempted only at GST portal JSON serialisation boundaries). - · RLS fuzzer (custom): seeds two tenants, asserts cross-tenant invisibility for all 15 business tables per push.
- · Integration tests against real Postgres on every push (concurrency, auth flow, cancellation guards).
- · Migration round-trip — every push applies
goose up+down+upto confirm reversibility.
Responsible disclosure
Found a vulnerability? Email vikas@networkershome.com with subject "Security disclosure". We commit to:
- Acknowledge within 2 working days.
- Triage and respond with a fix-or-rationale within 14 days.
- Credit you publicly on a future Security page acknowledgements section (with your permission) once the issue is fixed.
- Not pursue legal action against good-faith research.