Security Design: How to Build Systems That Don't Get Hacked
Too many engineering teams treat security like a fresh coat of paint: they write their entire application first, and then right before launch, someone asks, “Wait, is this secure?” Bolting security on at the end never works. In this guide, we break down security design from a developer's perspective — Zero Trust, STRIDE threat modeling, AuthN vs. AuthZ, and how to stop attackers from stealing your database.
👉 Macro Blueprint: Want to see how security boundaries apply to network gateways and APIs? Read our guide on System Integration & API Design.
The Death of the “Castle and Moat” Model
For decades, companies protected software using the “Castle and Moat” approach:
- You built a massive firewall around your office or private datacenter (the moat).
- Anyone outside was distrusted.
- Anyone inside the network was trusted 100%.
Why this fails today: In modern cloud environments with remote employees and microservices running across different AWS regions, an attacker only needs to compromise one developer laptop or phishing email to get inside the castle. Once inside, they can freely browse every unencrypted internal database and server!
Modern security design follows Zero Trust: Never trust, always verify. Even if a request comes from an internal microservice running on the exact same server rack, it must prove its identity and permissions every single time.
flowchart TD
subgraph ZeroTrust["Zero Trust Fabric"]
Client(["Incoming Request"])
Gateway["API Gateway / Proxy\n(Checks TLS and Validates JWT Token)"]
Policy["Policy Engine (OPA)\n(Is user allowed to view this record?)"]
Service["Target Service\n(Scoped ephemeral token)"]
Vault[("Secrets Manager\n(Vault / AWS KMS)")]
Client -->|"1. HTTPS + Signed JWT"| Gateway
Gateway -->|"2. Check Permissions"| Policy
Policy -->|"3. Allow with Read-Only Scope"| Gateway
Gateway -->|"4. Mutual TLS (mTLS)"| Service
Service <-->|"5. Retrieve DB Password"| Vault
end
Threat Modeling with STRIDE: Thinking Like an Attacker
Before you write code, take 30 minutes with your team and ask: “How could someone break this?”
The easiest way to brainstorm attacks is the STRIDE framework developed by Microsoft:
| Threat | The Attacker's Goal | How to Prevent It in Code |
|---|---|---|
| S - Spoofing | Pretending to be someone else (e.g., forging an admin session). | Cryptographically signed JWT tokens, Multi-Factor Authentication (MFA). |
| T - Tampering | Modifying data in transit (e.g., changing price to $0.01 in checkout). | TLS 1.3 encryption everywhere, HMAC signatures, server-side price validation. |
| R - Repudiation | Denying an action occurred (“I never authorized that bank transfer!”). | Immutable, append-only audit logs with timestamps and IP records. |
| I - Information Disclosure | Stealing sensitive data (e.g., dumping user passwords or credit cards). | Strong password hashing (Argon2 / bcrypt), encrypting database disks at rest. |
| D - Denial of Service | Crashing your servers by flooding them with requests. | Rate limiters (Token Bucket), Cloudflare edge protection, request size limits. |
| E - Elevation of Privilege | A regular customer gaining access to the admin dashboard. | Server-side Role-Based Access Control (RBAC); never trust role flags sent by the browser. |
AuthN vs. AuthZ: The Difference Every Developer Must Know
People mix up Authentication and Authorization all the time, but they are two completely separate problems:
- Authentication (AuthN) = Who are you? When you show your passport at the airport, they verify your identity. In code: verifying username, password, biometric FaceID, or OAuth token.
- Authorization (AuthZ) = What are you allowed to touch? Just because the airport knows who you are doesn't mean you can walk into the cockpit! In code: checking if user #42 has permission to edit project #99.
sequenceDiagram
autonumber
actor User as User Browser
participant IdP as Auth Server (Google / Okta)
participant API as Backend API
participant AuthZ as Authorization Policy
User->>IdP: 1. Login with password + MFA (AuthN)
activate IdP
IdP-->>User: 2. Return cryptographically signed JWT Token
deactivate IdP
User->>API: 3. GET /projects/99 {Bearer: JWT}
activate API
API->>API: 4. Verify JWT signature (Is it authentic? Yes!)
API->>AuthZ: 5. Does this user have permission for project 99? (AuthZ)
activate AuthZ
AuthZ-->>API: 6. Access GRANTED (User is project member)
deactivate AuthZ
API-->>User: 7. Return project data!
deactivate API
The Golden Rules of Defensive Coding
-
Never concatenate SQL strings: If you write
"SELECT * FROM users WHERE email = '" + email + "'", an attacker can input' OR 1=1 --and dump your entire customer table. Always use parameterized queries (prepared statements). - Hash passwords properly: Never use MD5 or SHA-256 for passwords — computers can calculate billions of SHA-256 hashes per second with modern GPUs. Use intentionally slow, memory-hard hashing algorithms like Argon2id or bcrypt with salt.
-
Store tokens in HttpOnly cookies: Storing user session tokens in JavaScript
localStoragemeans any Cross-Site Scripting (XSS) vulnerability can steal the token instantly. Keep session tokens inHttpOnly, Secure, SameSite=Strictcookies that JavaScript cannot touch.
- To configure secure API gateways, rate limits, and OAuth2, read System Integration & API Design.
- To secure cloud infrastructure and automated CI/CD pipelines, read Deployment Design.
- To dive into database encryption at rest and role permissions, explore Database Design.
Final Thoughts
Building secure software does not require a degree in cryptography. It requires good engineering hygiene: assuming the network is hostile, modeling threats before you code, sanitizing all external inputs, and strictly separating authentication from authorization.