20

Security in System Design

Building systems that stay safe even when one layer fails: defense in depth, least privilege, and trust boundaries.

Defense in depth

Defense in depth is the principle that security should come in many independent layers, so that if one fails, others still stand. No single wall is perfect — a firewall can be misconfigured, a password can leak, a library can have a bug — so you never rely on just one. The goal is that an attacker must defeat several unrelated defenses to cause harm, and each one they hit slows them down and raises the chance of getting caught.

The mindset is to assume any single control will eventually fail and ask: what still protects us when it does? A design that answers "nothing" has a single point of failure for security. Every section below is one of these layers.

   ┌─────────────────────────────────────────┐
   │  Edge: WAF, DDoS, TLS                     │
   │  ┌─────────────────────────────────────┐  │
   │  │  Network: firewalls, segmentation    │  │
   │  │  ┌───────────────────────────────┐   │  │
   │  │  │  App: authN/authZ, validation  │   │  │
   │  │  │  ┌─────────────────────────┐   │   │  │
   │  │  │  │  Data: encryption, ACLs  │   │   │  │
   │  │  │  └─────────────────────────┘   │   │  │
   │  │  └───────────────────────────────┘   │  │
   │  └─────────────────────────────────────┘  │
   └─────────────────────────────────────────┘

The CIA triad

Security has three fundamental goals, remembered as the CIA triad. Almost every security decision is protecting one of these three properties, so naming them keeps you honest about what you are actually defending.

PropertyPlain meaningBroken when
ConfidentialityOnly authorized people can read the dataA leak exposes user records
IntegrityData is not altered by unauthorized partiesAn attacker changes an account balance
AvailabilityThe system stays up and reachable for real usersA DDoS attack takes the service offline

These pull against each other, and that tension is the substance of design. Locking data down tightly (confidentiality) can make it harder to serve quickly (availability). Encrypting everything (confidentiality and integrity) costs performance. Good security is not maximizing all three blindly; it is deciding which matters most for each piece of data and spending effort there. A public marketing page needs availability far more than confidentiality; a table of passwords is the reverse.

Authentication versus authorization

Two words that sound alike and are constantly confused. Getting them straight is foundational, because they are different questions asked at different moments.

  • Authentication (authN)who are you? Proving identity: a password, a passkey, a code from your phone. It happens once, at the door.
  • Authorization (authZ)what are you allowed to do? Checking permissions: this logged-in user may read their own orders but not someone else's. It happens on every action, at every door inside.
   authN: "Who are you?"        →  proven once at login
   authZ: "May you do THIS?"    →  checked on every request

The classic failure is authenticating a user and then trusting them everywhere — checking the front door but leaving every internal door unlocked. A user proving they are logged in (authN) says nothing about whether they may delete this specific record (authZ). Every sensitive action needs its own authorization check, close to the resource, never assumed from a valid login alone.

Encryption in transit and at rest

Encryption scrambles data so that only someone with the right key can read it. There are two distinct places you apply it, and a complete design uses both, because they defend against different attackers.

  • In transit — protecting data as it moves across a network, using TLS (Transport Layer Security, the encryption behind HTTPS). It defends against anyone who can watch the wire: a compromised network, a malicious hop between services. Without it, credentials and data travel as readable text anyone in the path can capture.
  • At rest — protecting data as it sits on disk (databases, backups, file storage). It defends against someone who gets hold of the physical storage: a stolen drive, a leaked backup file, an over-privileged operator. The database is encrypted so the raw files are useless without the key.

Neither replaces the other. In-transit encryption does nothing for a stolen backup; at-rest encryption does nothing for traffic sniffed off the wire. Use both, and remember they only protect data while it is moving or stored — the moment your application decrypts it to work with it, it is plaintext in memory, which is why the other layers still matter.

   In transit                     At rest
   ──────────                     ───────
   data moving over network       data sitting on disk
   TLS / HTTPS                    disk & database encryption
   vs. wire-taps & MITM           vs. stolen drives & backups

Secrets management

A secret is any credential the system needs but must keep hidden: database passwords, API keys, private certificates, encryption keys. How you store these is one of the most common places real systems fall down, so the rules are worth stating flatly.

  • Never in code. A secret committed to a repository is exposed to everyone with read access forever — and git history keeps it even after you "delete" it. This is a leading cause of breaches.
  • Never in an env file checked into the repo. The same trap wearing a different hat. Environment variables are fine; committing the file that holds them is not.
  • Use a secrets vault. A dedicated service (HashiCorp Vault, AWS Secrets Manager, and similar) stores secrets encrypted, hands them to services at runtime, controls who can read each one, and logs every access.
  • Rotate regularly. Rotation means periodically replacing a secret with a new value, so that a leaked credential has a limited useful lifetime. A vault can automate rotation so a stolen key expires before it can be widely abused.

The principle underneath all four: treat every secret as something that will eventually leak, and design so that a leak is contained and short-lived rather than permanent and total.

Least privilege and zero trust

Two closely linked principles that shape how you hand out access.

Least privilege means every user, service, and process gets the minimum permissions it needs to do its job — and nothing more. A reporting service that only reads data gets read-only access, never write. The payoff is blast radius: if that service is compromised, the attacker inherits only its narrow permissions, so the damage is bounded. Over-broad permissions ("just give it admin, it's easier") turn a small compromise into a total one.

Zero trust extends the same suspicion to the network itself. The old model trusted anything inside the corporate network — once past the perimeter, you were considered safe. Zero trust drops that assumption: never trust based on network location alone; verify every request. A request from inside the data center is authenticated and authorized exactly as strictly as one from the public internet.

The connection: least privilege limits what any identity can do; zero trust insists you keep proving the identity on every hop. Together they ensure that getting inside one door does not unlock the building.

   Old "castle" model             Zero trust
   ──────────────────             ──────────
   trust everything inside        trust nothing by default
   hard shell, soft center        verify every request
   one breach = full access       every hop re-checked

Network segmentation

Network segmentation divides your infrastructure into separate zones so that a compromise in one cannot freely reach the others. It is defense in depth applied to network layout. Two building blocks do most of the work.

  • Private subnets. A subnet is a slice of your network; a private one has no direct route to or from the public internet. You put databases and internal services there, so even if an attacker learns a database's address, they cannot reach it from outside — only your own application servers, in a public-facing subnet, can talk to it.
  • Security groups. A security group is a per-resource firewall rule: it says exactly which sources may connect to a resource and on which ports. The database's security group allows connections only from the application servers, on only the database port, and denies everything else by default.

The result is that reaching your application server does not hand an attacker the database. They have crossed one boundary and immediately face another — the essence of layered defense.

   Public subnet              Private subnet
   ─────────────              ──────────────
   ┌──────────────┐           ┌──────────────┐
   │ app servers  │──allowed─►│  database     │
   │ (internet-   │  db port  │  (no internet │
   │  facing)     │           │   route)      │
   └──────────────┘           └──────────────┘
        ▲                          ✗ blocked
        │ internet                 direct internet

Threat modeling with STRIDE

Threat modeling is the practice of systematically asking "how could this be attacked?" before building, rather than reacting after a breach. A popular checklist for this is STRIDE, an acronym naming six categories of threat. You walk through your design and, for each part, ask whether each category applies.

LetterThreatThe property it attacksExample
SSpoofingAuthenticationPretending to be another user
TTamperingIntegrityAltering data in transit or storage
RRepudiationAccountabilityDenying you performed an action
IInformation disclosureConfidentialityLeaking data to the wrong party
DDenial of serviceAvailabilityFlooding the system offline
EElevation of privilegeAuthorizationGaining permissions you should not have

The value is that it is systematic — it forces you to consider threats you would not think of unprompted. You do not need heavy tooling; even sketching the system and asking the six STRIDE questions for each component surfaces gaps far more cheaply than discovering them in production.

Input validation and secure defaults

Two habits that prevent whole classes of bugs at the source.

Input validation means never trusting data that came from outside — from a user, another service, a file — and checking it against strict expectations before use. Most injection attacks (the SQLi and XSS from the WAF discussion, where input crafted to look like data is instead interpreted as code) exist because input was trusted. The rule is to validate on the server, where the attacker cannot bypass it, against an allow-list of what is acceptable rather than a blocklist of what is forbidden.

Secure defaults means the safe choice should be what happens when no one does anything special. A new resource starts private, not public. A permission starts denied, not granted. A feature starts off. The reason is human: people forget to lock things down, but they rarely forget to open something they actively need. If the default is open, every forgotten setting is a hole; if the default is closed, every forgotten setting is merely an inconvenience someone fixes when they hit it.

The confused deputy and SSRF

A confused deputy is a subtle, important attack pattern: a trusted component is tricked into misusing its authority on an attacker's behalf. The attacker cannot reach a resource directly, so they persuade a privileged part of your own system to reach it for them — the "deputy" has the access, and it gets confused into using it wrongly.

The most common concrete form is SSRF — Server-Side Request Forgery. Your server accepts a URL from a user (say, to fetch a preview of a link) and dutifully requests it. An attacker supplies a URL pointing at your internal network — a private database, or a cloud metadata endpoint that hands out credentials. Your server, which can reach those internal addresses, fetches them and returns the result to the attacker. The user could never reach those addresses; your trusted server could, and was tricked into doing so.

The defense is least privilege and validation working together: validate and allow-list which destinations the server may fetch, block requests to internal address ranges, and give the server only the narrow access it truly needs — so even a confused deputy has little authority to misuse.

   Attacker ──"fetch this URL"──► Your server (the deputy)
                                        │ has internal access
                                        ▼
                            internal metadata / private DB
                                        │
   Attacker ◄──── returns secret ───────┘

Audit logging

Audit logging is recording who did what, when, so that actions can be traced after the fact. It does not prevent an attack, which is why it is easy to skip — but it is what lets you detect a breach, understand its scope, and recover. Without it, a compromise is invisible: you cannot tell what was accessed, by whom, or whether it is still happening.

Good audit logs share a few traits. They capture the actor (which identity), the action (what was attempted), the target (on what resource), the time, and the outcome (allowed or denied). Crucially, they are stored where the logged system cannot alter them — an attacker who breaches a server must not be able to erase their own tracks, so logs are shipped to separate, append-only storage. This is what defeats the "repudiation" threat in STRIDE: with tamper-proof logs, no one can credibly deny what they did.

Key takeaways

  • Defense in depth means many independent layers, so one failure is not total; assume any single control will fail and ask what still protects you.
  • The CIA triad — confidentiality, integrity, availability — names what you are defending; they trade off, so decide which matters most per piece of data.
  • Authentication (who are you, once at the door) and authorization (what may you do, on every action) are different questions; a valid login never implies permission for a specific action.
  • Encrypt both in transit (TLS, vs. wire-tapping) and at rest (disk/db, vs. stolen storage); neither replaces the other.
  • Never put secrets in code or committed env files; use a vault and rotate, treating every secret as something that will eventually leak.
  • Least privilege bounds the blast radius of a compromise; zero trust verifies every request regardless of network location.
  • Segment networks with private subnets and security groups so crossing one boundary does not hand over the next.
  • Threat-model early with STRIDE; validate all external input on the server against allow-lists; make the safe choice the default (closed, private, denied).
  • The confused-deputy/SSRF class tricks your trusted server into misusing its access — defend with allow-listed destinations and least privilege.
  • Audit logging does not prevent attacks but makes them detectable and undeniable; store logs where the breached system cannot alter them.

Checklist

  • [ ] I can explain defense in depth and identify single points of failure in a security design.
  • [ ] I can name the CIA triad and describe how the three goals trade off.
  • [ ] I can clearly distinguish authentication from authorization and place each correctly.
  • [ ] I can explain encryption in transit versus at rest and what attacker each stops.
  • [ ] I can state the rules of secrets management: no secrets in code/committed env, use a vault, rotate.
  • [ ] I can apply least privilege and zero trust to bound a compromise.
  • [ ] I can describe network segmentation with private subnets and security groups.
  • [ ] I can run a STRIDE pass over a design and validate input with secure defaults.
  • [ ] I can explain the confused-deputy/SSRF pattern and its defenses.
  • [ ] I can describe what makes an audit log useful and tamper-resistant.