Secure design principles that still hold
Saltzer and Schroeder's eight principles applied to cloud, APIs and microservices, plus secure defaults and attack surface reduction, worked through real design reviews.
01Eight principles from 1975, still undefeated
- Name the Saltzer and Schroeder principles and give a modern example of each
- Spot which principle a design violates in a review
- Argue for secure defaults and attack surface reduction with concrete changes
- Turn principles into review questions a team can actually answer
Saltzer and Schroeder wrote their design principles for time-sharing systems in 1975. Almost every modern breach write-up maps to one of them being broken. Treat them as a shared vocabulary for design reviews: "this violates complete mediation" is faster and less personal than "this is insecure".
| Principle | Means | Modern example |
|---|---|---|
| Least privilege | Every subject gets only the rights it needs, only for as long as it needs them | Per-service IAM role scoped to one bucket prefix; JIT admin that expires in an hour |
| Fail-safe defaults | Access is denied unless explicitly granted | Default-deny network policy; authz middleware that returns 403 when the policy engine is unreachable |
| Complete mediation | Every access to every object is checked, every time | Object-level authz on each read, not just at login or at the route; no long-lived cached grants |
| Economy of mechanism | Keep security mechanisms small and simple enough to review | One shared authz library instead of forty hand-rolled checks |
| Separation of privilege | Sensitive actions need more than one condition or person | Two-person approval for production IAM changes; MFA plus device posture |
| Least common mechanism | Minimise mechanisms shared between users or tenants | Per-tenant encryption keys and queues instead of one shared worker with every tenant's secrets |
| Psychological acceptability | Security must be easy to use correctly or people route around it | Passkeys instead of 20-character rotation rules; a paved-road deploy that is faster than the unsafe one |
| Open design | Security must not depend on the design being secret | Published crypto (AES-GCM, Argon2id), secret keys; no home-made obfuscation |
Expect "give me an example of least privilege going wrong" or "what does fail-safe mean in practice?". Answer with a system you have seen, the principle it broke and the specific change that fixed it.
02Secure defaults and attack surface
Most production misconfigurations are not decisions; they are defaults nobody changed. If the safe setting is opt-in, a fraction of teams will forget it forever. Make the safe setting the default and make the unsafe one require a visible, reviewed override.
Attack surface is everything an attacker can send input to or reach: ports, endpoints, parameters, file uploads, admin panels, debug routes, third-party webhooks, IAM roles that can be assumed. Every item is code that must be correct forever. Removing it is the only fix with zero maintenance cost.
- Delete what is not used: old API versions, debug endpoints, unused IAM roles, stale DNS records.
- Disable optional features by default: XML external entities, server-side template evaluation, verbose errors.
- Move admin and management interfaces off the public internet, behind the zero trust gateway.
- Narrow inputs: accept a schema, not "any JSON"; accept an enum, not a free string.
// Fail-safe authorisation middleware: errors and timeouts deny, never allow.
export async function authorise(req: Request, action: string, resource: string) {
try {
const decision = await policy.check({ subject: req.user, action, resource }, { timeoutMs: 200 });
if (decision.allow !== true) throw new Forbidden(decision.reason ?? "denied");
} catch (err) {
log.warn({ action, resource, err: String(err) }, "authz denied");
throw err instanceof Forbidden ? err : new Forbidden("policy unavailable");
}
}
// The anti-pattern this replaces:
// const ok = await policy.check(...).catch(() => true); // fail open "for availability"Some controls should fail open (a rate limiter in front of a login page during a provider outage, for example). That is fine when it is written down, alarmed and reviewed. A silent catch(() => true) is how authz quietly disappears during an incident.
03Principles in a design review
A design review is not a hunt for bugs; it is a hunt for decisions that make whole classes of bugs likely. Walk the data flow, then ask one question per principle. Three worked examples:
| Proposal | Principle broken | Better design |
|---|---|---|
Report worker gets a role with s3:* on all buckets because "it needs to read reports" | Least privilege | s3:GetObject on reports/tenant-* only; writes go to a separate output prefix |
Mobile app hides the admin API by not linking it; backend trusts an isAdmin flag in the request | Open design, complete mediation | Server derives role from the token and checks it on every admin call; assume every endpoint is discovered |
| One shared Redis for all tenants' sessions and job payloads | Least common mechanism | Per-tenant key prefixes with ACLs at minimum; separate instances for regulated tenants |
| Prod deploy can be triggered by any engineer from a laptop | Separation of privilege | Deploys only from CI on protected branches, with a second approver for production |
- Who can call this, and how does the service know? (complete mediation)
- What is the smallest permission set that works? (least privilege)
- What happens when a dependency is down or slow? (fail-safe defaults)
- Could a reviewer read the whole security mechanism in one sitting? (economy of mechanism)
- What is shared between tenants, and what leaks if it is compromised? (least common mechanism)
- Will engineers and users actually follow it under deadline pressure? (psychological acceptability)
Saltzer and Schroeder also discussed work factor and compromise recording. Modern lists add defence in depth and zero trust. They complement the eight; they do not replace them.
"I review designs against a short list: least privilege, fail-safe defaults and complete mediation first, because those catch most real issues. Then I ask what is shared between tenants and whether the secure path is also the easy path. If it is not, people will route around it."
A service caches a user's permissions for 24 hours after login to save policy lookups. Which principle is most directly at risk?
Q1Is security through obscurity always bad?+
Obscurity as the only control is bad. As an extra layer (non-default ports, hidden admin paths) it can cut noise, but the design must stay secure if the obscured detail is published.
Q2How do least privilege and developer speed coexist?+
Generate policies from observed access (access analysers), grant broad rights in dev and narrow ones in prod, and make JIT elevation fast. Least privilege that takes a week to request becomes standing admin.
Q3Which principle is violated most often in the cloud?+
Least privilege, by far: wildcard IAM actions, long-lived keys and roles reused across services. Fail-safe defaults is next, through public-by-default resources in older accounts.
04When principles collide
Principles pull against each other. Separation of privilege adds approvals, which costs psychological acceptability. Economy of mechanism argues for one shared authz service, which least common mechanism might question. The engineering job is to choose deliberately and write down why.
| Tension | Usual resolution |
|---|---|
| Separation of privilege vs speed | Two-person rule only for high-blast-radius actions; everything else is single approver with audit |
| Economy of mechanism vs least common mechanism | One shared authz library (simple, reviewed) with per-tenant data and keys underneath |
| Fail-safe vs availability | Fail closed for authz and data access; fail open only for controls whose failure is not a security boundary, and alarm it |
- EDGE
- SERVICE
- DATA
- STREAM
- THREAT
- Live flow
- Lookup / async