
Yesterday I ran a multi-tenant security review on Eurobase. Six findings — five Critical, one High. By the end of the day, all six were patched in production, all six were covered by regression tests, and the platform was running across four new defensive layers it didn't have the morning before.
This is what closed beta is for.
What we looked at
Eurobase is a multi-tenant backend. Every project gets its own Postgres schema, its own S3 bucket, its own row-level-security policies, its own JWT signing key. The architecture is designed so one tenant cannot see, modify, or even infer the existence of another tenant's data. That's the contract.
The security review's only job was to test whether the implementation matched the contract. Two parallel passes ran end-to-end — one focused on tenant isolation specifically, one on general OWASP-class concerns — walking every code path that touches tenant data: the SDK query path, the SQL endpoint, storage uploads, edge functions, vault, OAuth, cron, webhooks, realtime.
What we found
I'll name the issues directly, because the patches are live, the surfaces are documented, and "we found a bunch of things and quietly fixed them" is the kind of communication style I don't trust as a customer.
Six issues, all closed in production:
1. Cron jobs accepted multi-statement SQL on a privileged pool. Any project member could schedule UPDATE public.projects SET owner_id = ... and take over arbitrary tenants on the next fire.
2. Storage bucket name was derived from a request header. An authenticated SDK caller could send X-Project-Slug: victim and upload, list, or delete files in another tenant's S3 bucket.
3. OAuth callback auto-linked existing accounts by email. No email_verified check. An attacker who could create a Google or Microsoft or Apple account claiming a victim's email could take over the victim's tenant account at sign-in.
4. The SDK SQL endpoint allowed cross-schema reads under a secret key. The runtime gateway role had broad cross-schema DML; the SQL handler validated SELECT-only but not the schemas being referenced. A leaked secret key for any project gave the holder read access to every other tenant.
5. The edge functions runner exposed cross-tenant SQL to user JS. Tenant code could read Deno.env.get("DATABASE_URL") and connect to Postgres directly, then reach any tenant's data. Layered on top: the runner trusted unsigned identity headers from the gateway, so anything inside the cluster could call it directly with forged identity.
6. Permissive RLS policies on tenant system tables (refresh_tokens, email_tokens, vault_secrets, user_identities) were USING (true). Any code path that touched them outside service-role context — like #5 — leaked across tenants.
What we shipped
Issues 1, 2, and 3 were narrowly-scoped fixes — header trust replaced with authenticated-context lookup, OAuth flow refusing to auto-link by email, cron running in a transaction with rejected multi-statement input and forbidden-schema guards.
Issue 6 was a migration that tightened the RLS policies across every existing tenant. (And then a regression introduced in that migration broke vault writes for half a day until a beta tester caught it. The hotfix shipped this morning. Closed beta works.)
Issue 4 got a textual cross-schema validator on the SDK SQL endpoint plus a published advisory. The structural fix — per-tenant database roles for runtime traffic — is on the roadmap.
Issue 5 was the largest. It needed three independent defensive layers, none of which existed yesterday morning:
- ▸Per-tenant Postgres roles created at provisioning, with grants only on their own schema. The runner connects as a non-privileged role and
SET LOCAL ROLEs into the tenant's role for each invocation. Cross-tenant SQL fails at the role-permission layer, not at search_path. - ▸Per-invocation Web Worker isolates with
permissions: 'none'. User JavaScript can't read environment variables, can't open sockets, can't read the filesystem. Database access goes throughpostMessageRPC back to the runner, which executes queries under the per-tenant role. - ▸HMAC-signed gateway-to-runner traffic. The runner refuses any request without a valid signature from the gateway's shared secret. Anything inside the cluster that isn't the gateway gets a 401.
All three are live. A fourth layer — pinning the runner to a dedicated Kubernetes node pool so a kernel-level container escape can't laterally reach gateway memory — is documented and queued.
Why I'm comfortable writing this
Two reasons.
First, closed beta is the right window for this kind of review. The whole point is to find issues with paying-attention testers and a small surface, before you find them with production traffic and a large one. A security review that finds zero issues on a young multi-tenant backend either isn't looking hard enough or is being run on a system too small to matter. I'd rather know now than later.
Second, the architecture made the response straightforward. The role split we shipped two weeks ago — eurobase_gateway for runtime traffic, eurobase_developer for platform admin, eurobase_migrator for DDL ownership — was pre-positioning for exactly this kind of layered defense. When the runner needed a fourth role, the pattern was already there. Transaction-scoped role elevation, service-role RLS bypass, in-tx SET LOCAL search_path — these primitives existed before the review. The review found gaps in how they were applied, not gaps in what was available. That's a much shorter fix list.
That's the value proposition of a sovereign EU backend that takes its architecture seriously: when something needs to be hardened, you have the levers to do it cleanly, on infrastructure you control, under the legal regime you operate in.
What we logged
Six private security advisories were filed in the repository's security tab while patches were rolling out. They will be published this week, now that all underlying fixes are in production. The post-fix codebase — including the new test suites that pin the regression class for each finding — is on main.
If you're a beta tester and you spot something we missed: every email Eurobase sends now carries an "Open an issue on GitHub" link. The issue tracker is public, and security-sensitive reports can use GitHub's private security-advisory flow.
We're building this in the open. That includes the parts that are uncomfortable to write about.