Security by Design
Security comes from how a system is built, not from a layer added at the end. Security added late — a scanner in the pipeline, a WAF in front, a pen test before launch — catches only a small part of what secure design prevents from the first line of code. Assume an attacker will study every feature more closely than any of your users.
Designing for security means you assume the request is hostile, the network is watched, the dependency is compromised, and the insider is curious — and you still keep the system safe. That assumption shapes the architecture, not just the code. The cheapest place to remove a vulnerability is the design stage. The most expensive is a breach notice to a regulator.
For a regulated AML platform, the stakes are real. One design flaw — an unsigned webhook, a missing tenant filter, a forgeable identity claim — can let the wrong person through KYC, expose biometric data, or destroy the evidence trail we are legally required to keep. Build as if our licence depends on it, because it does.
Principles to design against
- AlwaysDefault to deny. Every gate — auth, tenancy, feature flag, rate limit — starts closed and is opened on purpose, never the other way round.
- DoReduce the attack surface. The safest endpoint, parameter, permission, or stored field is the one that does not exist. Remove it before you try to harden it.
- DoUse defence in depth. No single control should be the only thing between an attacker and customer data. Assume each layer will fail one day.
- DoGrant least privilege everywhere. Each identity, service, token, and key gets the narrowest scope and shortest lifetime that still works.
- ConsiderDesigning as if a breach will happen. Split the system so that compromising one component does not give up the rest (separate tenants, separate keys, limited blast radius).
- Do notRely on obscurity — an undocumented endpoint, a hard-to-guess secret URL, a client-side check — as if it were a security control. It is not.
- NeverTrust the client. Identity, role, tenant, price, and permission are decided on the server from a validated token, never from anything the caller can set.
Building it in
- DoMake the secure path the easy path: shared auth middleware, a secure-by-default base controller, helpers that parameterise queries. Doing it right should take less effort than doing it wrong.
- DoValidate and normalise untrusted input at the trust boundary, then carry it as typed, trusted data inside. Do not re-decide trust in an ad hoc way deep in the stack.
- DoFail closed on every security-relevant decision. If a check is missing, errored, or returned something you do not recognise, deny it or escalate.
- ConsiderThreat-modelling each new feature before you build it (see Threat Modelling). One hour of "how would I break this?" often saves a sprint of fixes.
- Do notPut off security to "a hardening pass later". Later rarely comes, and security added afterwards leaves gaps the original design would not have.
- NeverWeaken or remove a security control to meet a deadline or unblock a demo. Raise the trade-off and make it an owned decision, never a silent one.
var tenantId = request.Headers["X-Tenant-Id"];
var customers = db.Query("... WHERE TenantId = @t", new { t = tenantId });
Any caller can set the header to another tenant's id and read their data. The tenant must come from the validated token, on the server, never from a header the caller controls.
var tenantId = User.GetTenantId(); // from validated claims
var customers = db.Query("... WHERE TenantId = @t", new { t = tenantId });
The boundary decides who you are. Everything inside trusts that decision and nothing else.
Self-review checklist
- AskIf an attacker controlled every input to this feature, what is the worst they could do?
- AskWhat happens when this control fails? Does the system fail closed or fail open?
- AskIs any security decision here made from data the client can change?
- AskIf this component were fully compromised, what else could the attacker reach?