Start with your own audit
The work started with a review of the whole platform, run by the people who know it best. Findings were grouped by severity and fixed in small, reviewable changes, not in one large security release. Every change could be switched off on its own, so a fix that misbehaved in production could be withdrawn without withdrawing the rest.
A security fix must not break a customer
One rule governed the work. A fix that changes what a paying customer's integration can do is a product decision, not a security fix. Where a fix would have changed the contract customers build against, we kept the contract and narrowed what sits behind it. Hardening that breaks integrations gets rolled back, and then nothing is more secure.
Guards that fail by default
The durable result is not a list of patches. It is a set of guards that refuse by default and allow only what has been explicitly permitted. Something new starts closed and has to be opened on purpose. When a step is forgotten, the result is an error during testing, not an exposure in production.
Tenant isolation that cannot be forgotten
On a multi-tenant platform, the question behind every request is whose data this is. The answer is decided on the server side, never taken from what the client sends. Data access is written so that the customer it belongs to has to be supplied, and permission checks look at the actual record and who owns it, not only at the role of the person asking. Customers who need it run in a dedicated environment of their own.
Sign-in, sessions and keys
Single sign-on and two-factor authentication were hardened against reuse: a proof of identity that has been used once is not accepted again. Sessions end when they should. Keys used by integrations are kept apart from the sessions of people and carry narrower powers, so an integration key is not a way into an account.
Uploads and outbound calls
Every uploaded file goes through the same checks, in the same order, before anything in the platform tries to read it, and the platform decides what a file is by looking at the file, not at its name. Rejections stay neutral, so an error does not explain how to get past it. Calls the platform makes to customer systems are signed, so the receiver can verify where they came from.
Sensitive data and secrets
Sensitive fields are encrypted in the database itself, field by field, and secrets that belong to a customer are encrypted per customer. Encryption keys are kept apart from the data they protect, in a dedicated vault, and never in the repository.
Checked by tooling, not by memory
Several of these rules are verified automatically. Tests guard the permission model and the list of public pages, so neither can change by accident. Static analysis bans the patterns that cause trouble, dependencies are scanned for known vulnerabilities, and an automated reviewer flags data access that is not tied to a customer.
Two things we would tell any team
- Never load a record by an id the client sent and then trust that record for the rest of the request. Tie every request to the account of the person asking.
- Never ship a safety switch that fails closed without shipping its setting with it. The first deploy turns the feature off for everyone.