Security

Keywords

security, authentication, authorization, secrets management, vulnerability scanning, defense in depth, threat model, least privilege, zero trust, supply chain

Introduction

The post-mortem ran four pages, and the root cause was one line. Eighteen months earlier, a hurried engineer had committed a config file with a live AWS access key in it — caught in review, reverted in the next commit, apologized for in the channel. The fix felt complete. It was not. The revert removed the key from the working tree; it left the key sitting in the repository’s history, recoverable by anyone with a clone and git log -p. The repo was later open-sourced. A scanner that crawls public GitHub for credentials found the key in minutes, and the attacker who owned that scanner spun up a fleet of crypto-mining instances on the company’s account before anyone noticed the billing anomaly. The damage was a five-figure cloud bill and a frantic key rotation across every service that had ever used it. The cause was a secret in git — the most boring mistake in the catalog.

This is the shape of almost every breach you will read about. Not a zero-day in the TLS stack, not a nation-state chaining four novel exploits — a secret committed to a repo, a dependency with a published CVE nobody patched, a logged-in user who could read another tenant’s invoices by changing a number in a URL. The exotic attacks make the conference talks; the mundane ones make the incidents. And the mundane ones have a common thread: they are missing fundamentals, not missing cleverness. Security is not a feature you bolt on before launch, like a login page or a TLS certificate. It is a cross-cutting discipline — a way of building that runs through every service, every pipeline, every credential, every line of code that touches user input. This chapter is about that discipline: the principles, the mental model, and the handful of practices that prevent the boring breaches.

The Core Insight

The insight that makes security tractable is that it is not about being unbreakable — nothing is — but about managing risk through layered defense. You will never reduce your attack surface to zero, and you will never prove your system has no vulnerabilities. What you can do is make a successful attack require defeating several independent controls instead of one, and ensure that when a control fails, the damage is contained rather than total. Three load-bearing principles carry that program.

The first is least privilege: every actor — a user, a service, a CI job, an IAM role — gets the minimum access it needs to do its job, and nothing more. A service that reads from one S3 bucket should not hold credentials that can write to all of them. The narrower each grant, the smaller the prize when one is stolen.

The second is defense in depth: no single control is trusted to be perfect, so you stack layers such that one failure is survivable. The firewall will eventually let something through; the authentication layer behind it catches what the firewall missed; the authorization layer behind that limits what an authenticated attacker can reach; encryption at the center means even raw data access yields ciphertext. Each layer assumes the ones outside it have already failed.

The third is assume breach, the operating posture behind zero trust: stop treating the network perimeter as a security boundary. The old model — “inside the firewall is trusted, outside is hostile” — collapses the moment one inside machine is compromised, because the attacker then moves laterally through a network that trusts them by location. Zero trust verifies every request, even internal ones, on the assumption that the attacker is already inside. You do not “finish” security and move on. You continuously reduce attack surface and shrink blast radius, the way you continuously pay down technical debt.

A mental model

Picture a medieval keep, not a single wall. A wall is a binary: it holds or it falls, and when it falls the enemy is in the throne room. A keep is concentric — an outer curtain wall, an inner bailey, a gatehouse, the keep itself, and the treasury at the center behind its own locked door. Breaching the outer wall puts you in the courtyard, not the treasury. That is defense in depth: each ring buys time and forces another fight, and the defender survives because no single breach is fatal.

Least privilege is the discipline of the keys inside that keep. The kitchen staff have keys to the kitchen, not the armory; the guard at the east gate cannot open the west. Every door opens only what that role needs — so a stolen key compromises one room, not the castle. And the threat model is the mindset of the defender who walks the walls thinking like the attacker: where would I come over? Which gate is weakest? What am I actually after, and what is the shortest path to it? You cannot defend what you have not imagined being attacked.

How to reason: the threat model and the layers

Security work begins not with a tool but with a question: what are we protecting, from whom, and how would they get in? That question is what threat modeling answers, and it turns an infinite, paralyzing problem (“make it secure”) into a finite, prioritized one. The discipline is a short, repeatable sequence. Identify the assets worth protecting — user data, credentials, the ability to spend money, the integrity of the build. Map the entry points — every place untrusted input crosses into your system: public endpoints, file uploads, message consumers, third-party dependencies, the CI pipeline itself. Enumerate the threats at each entry point — a useful checklist is STRIDE: Spoofing identity, Tampering with data, Repudiation, Information disclosure, Denial of service, and Elevation of privilege. Then map controls to threats, layer by layer, and ask the defense-in-depth question of each: if this one control fails, what catches it?

Those layers are the spine of this chapter, and Figure 45.1 shows them as the concentric rings of the keep — network and edge at the perimeter, then authentication, then authorization with least privilege, then application hardening, with secrets management wrapped around the data at the center. The sections that follow walk from the outside in.

What you’ll learn

  • Why authentication (proving identity) and authorization (granting access) are different problems, and why confusing them is the source of a whole class of breaches
  • How sessions, tokens, and OAuth/OIDC actually establish identity, at a level you can reason about across services
  • Why secrets must never live in code, images, or git history, and what vaults, rotation, and short-lived credentials buy you instead
  • How dependencies became your largest attack surface, and how CVE scanning, SBOMs, and signing turn the supply chain into something you can verify
  • Where TLS, mTLS, and encryption at rest fit, and why service identity is the modern network boundary
  • How least privilege and defense in depth show up concretely in IAM scoping, network policies, and input validation — and how to threat-model a service yourself

Prerequisites

  • Software Engineering Fundamentals — dependency management, build-vs-runtime, and why reproducibility matters; the supply-chain material here builds directly on it
  • Comfort with how web requests work: HTTP headers, cookies, status codes, and the idea of a client talking to a server
  • Basic familiarity with the cloud and container vocabulary (IAM, images, environment variables) used throughout

Authentication versus authorization

The single most important distinction in application security is also the most commonly fumbled, and it fits in one sentence: authentication proves who you are; authorization decides what you may do. Logging in is authentication. Being allowed to delete this particular invoice is authorization. They are sequential — you generally authenticate first, then authorize each action — but they are not the same check, and treating them as one is where a large fraction of real breaches live.

Authentication is the act of establishing identity. The classic mechanism is a password, which must be stored as a slow, salted hash — Argon2id or bcrypt, never a fast hash like SHA-256 and never plaintext — so that a stolen database does not hand the attacker every password. On top of the password sits multi-factor authentication, which adds “something you have” (a TOTP code, a hardware key) to “something you know,” so that a leaked password alone is not enough. Once identity is established, the server needs a way to remember it across subsequent requests, and here two designs dominate. A session is server-side state: the server stores the session, hands the client an opaque random ID in a cookie, and looks it up on every request. Sessions are trivially revocable — delete the row and the user is logged out — but they require the server to hold state. A token, typically a JWT, is the inverse: a signed, self-contained claim the server can verify with a key without any lookup. Tokens scale horizontally and work cleanly across services, but the price is revocation — a JWT is valid until it expires, so you keep access tokens short-lived (minutes) and pair them with refresh tokens.

For identity across organizations — “Sign in with Google,” enterprise SSO — the standard is OAuth 2.0 with the OpenID Connect (OIDC) layer on top. The mental model is a triangle of three actors: an identity provider that issues tokens, a client application the user is trying to use, and a resource server (your API) that verifies tokens and enforces access. The crucial property is that the IdP issues and the resource server verifies — your API never sees the user’s Google password. OIDC’s specific contribution is to separate two tokens that are easy to conflate: the access token says “this caller may invoke /orders,” while the ID token says “this caller is alice@example.com.” Using an access token as if it were proof of identity is one of the most common production mistakes, and OIDC exists precisely to keep the two straight.

Authentication, in the service: The mechanics of password hashing, session stores, JWT issuance and verification, and wiring OAuth flows into a running app are covered in the per-language web chapters — Python: Web Development and Go: Web Services. This chapter stays at the cross-cutting level: the distinctions and the failure modes.

Authorization is the second, separate question, and it is where the subtle bugs cluster. The two dominant models are RBAC — role-based access control, where permissions attach to roles (“admin,” “billing,” “viewer”) and roles attach to users — and ABAC — attribute-based access control, where decisions are computed from attributes of the user, the resource, and the context (“a user may edit a document if they own it and it is not locked”). RBAC is simpler and covers most needs; ABAC handles the fine-grained, ownership-aware cases RBAC strains under. But the most dangerous authorization bug needs neither: it is the missing check entirely. The classic is the confused deputy — the system performs an action on behalf of a user without verifying the user was entitled to it — and its everyday face is broken object-level authorization, where an endpoint authenticates the caller but never confirms the caller owns the object they asked for. GET /api/invoices/1043 returns invoice 1043 to anyone logged in, so a user changes the number to 1044 and reads another tenant’s invoice. The user was perfectly authenticated. They were never authorized to see that object, and nobody checked. This is the cross-tenant data leak that closes companies, and it is invisible to authentication alone — which is exactly why the two must be reasoned about as distinct layers.

Secrets management

A secret is anything that grants access: a database password, an API key, a private signing key, a cloud credential. The first rule of secrets is a prohibition — they never live in code, in container images, or in git — and the war story in the introduction explains why git is uniquely unforgiving. Git is a content-addressed history, not a mutable file. Deleting a secret in a later commit does not remove it; it is still in the object that the earlier commit points to, recoverable forever by anyone who can clone the repo, exactly the way a deleted file persists in a lower Docker layer. “I removed it in the next commit” is not remediation. The only real remediation once a secret has touched a repo is to rotate it — assume it is compromised and replace it everywhere — because the old value may already be in a hundred clones and a dozen scanners.

Images have the same disease. A secret COPYd into a Docker layer and deleted two lines later is still in the earlier layer, fully recoverable from the image by anyone who pulls it. So the secret has to be injected at runtime rather than baked in at build time, and this is the narrow case where environment variables earn their reputation: an env var set by the orchestrator at container start never touches a layer, never touches the repo, and is gone when the container dies. They are not perfect — env vars leak into process listings, crash dumps, and child processes — but they beat anything baked into the artifact, and for the highest-sensitivity secrets you go further still.

That further step is a secrets manager — HashiCorp Vault, AWS Secrets Manager, or the Kubernetes-native equivalents — a dedicated, audited, encrypted store that becomes the single source of truth for credentials. The application authenticates to the manager (ideally with a workload identity it already has, not another secret) and fetches what it needs at startup. Three properties make this more than a fancier env var. Rotation: the manager can change a secret on a schedule, and well-designed managers issue short-lived, dynamic credentials — Vault can mint a database username and password that exist for one hour and then expire, so a leaked credential is worthless within the hour and there is no long-lived secret to steal in the first place. Audit: every access is logged, so you can answer “who read this credential, and when,” which is the difference between detecting a breach and discovering it on the cloud bill. Least privilege: access is scoped per path or per role, so a compromised service reaches only its own secrets. Secrets management is the innermost ring in Figure 45.1, wrapped tightly around the data, because a secret is the key that bypasses every layer outside it — which is exactly why it gets the most protection.

Vulnerability and supply-chain scanning

Here is the uncomfortable arithmetic of modern software: you wrote a few thousand lines, and you shipped a few hundred thousand. The rest came from your dependency tree — direct dependencies, their dependencies, and the OS packages in your base image — and all of it runs with your application’s privileges. Your largest attack surface is not the code you reviewed; it is the code you imported and never read. When a vulnerability is found in a popular library, it is published as a CVE (Common Vulnerabilities and Exposures) with a severity score, and from that moment every system still running the vulnerable version is a known, catalogued target. The Log4Shell incident — a single remotely-exploitable flaw in a near-ubiquitous Java logging library — is the canonical case: the dependency was four layers deep in most affected systems, and teams spent frantic weeks just discovering whether they used it.

The discipline that prevents this is scanning, in the pipeline, automatically. Software Composition Analysis (SCA) tools — Trivy, pip-audit, Snyk, Dependabot — read your lockfiles, resolve the full dependency tree, and flag any package with a known CVE, so a vulnerable transitive dependency is caught at pull-request time rather than in production. Container image scanning does the same for the OS packages in your base image, which is the other half of the tree and the reason minimal base images matter: the smaller the image, the shorter the scan report, because there is simply less installed to be vulnerable. You wire these into CI as a quality gate — fail the build on HIGH and CRITICAL findings, triage the rest — so security shifts left, into the cheap part of the lifecycle where a fix is a version bump instead of an incident.

Knowing what you depend on is the prerequisite for the deeper supply-chain controls. An SBOM — Software Bill of Materials — is the machine-readable manifest of every component in a build: the answer to “are we affected by the next Log4Shell?” computed in seconds instead of weeks. Signing and provenance close the loop in the other direction: frameworks like Sigstore and SLSA let you cryptographically sign an artifact and attest how and from what source it was built, so a consumer can verify that the image they are about to run is the genuine output of your pipeline and not something an attacker swapped in. Together, scanning, SBOMs, and signing turn the supply chain from an act of blind faith into something you can verify — and because it all lives in the pipeline, it is inseparable from the CI/CD discipline.

Build it → A full authentication and authorization stack in a real application: Project 05: SaaS Web Platform implements sessions, tokens, RBAC, and multi-tenant isolation end to end, and Project 02: Microservice Platform shows gateway-level auth with Kong fronting a fleet of gRPC services.

Transport and data security

Two requests carry your users’ data: the one over the wire and the one to disk. Both must assume an eavesdropper. Encryption in transit is the job of TLS, which should be everywhere — not just at the public edge but on internal hops too, because the assume-breach posture means you cannot trust the network between your own services. The zero-trust version of this is mTLS (mutual TLS), where both sides present certificates and verify each other, so a service-to-service call carries cryptographic proof of which service is calling, not merely an encrypted channel to an anonymous peer. In a service mesh this becomes the basis for identity: each workload gets a signed identity document (the SPIFFE standard formalizes this), the mesh handles certificate issuance and rotation, and authorization policies are written in terms of “service A may call service B” rather than IP addresses — which are exactly the network-location trust that zero trust rejects.

Encryption at rest protects the data on disk and in backups, so that a stolen disk, a misconfigured snapshot, or a compromised database host yields ciphertext rather than records. The keys live in a key-management service (a KMS or HSM), separate from the data they protect, so that access to the storage does not imply access to the plaintext — least privilege applied to cryptography. For the most sensitive fields — payment data, health records — field-level encryption inside an already-encrypted database adds yet another ring, the innermost protection on the most valuable assets.

Build it → Service identity and encrypted service-to-service traffic in practice: Project 13: Service Mesh implements mTLS with SPIFFE-style workload identity, so every internal call is authenticated and encrypted rather than trusted by network position.

Defense in depth and least privilege in practice

The principles become concrete in a handful of everyday controls, each one a layer in Figure 45.1. At the network edge, network policies (a default-deny posture where a service can talk only to the specific peers it needs) and a WAF filter the crudest attacks before they reach the application. Inside the cloud, IAM scoping is least privilege made operational: a role granted exactly the actions it needs on exactly the resources it needs, with a permission boundary capping what it could ever be granted, so a compromised service cannot escalate to control the account. In the application, input validation treats every byte from a client as hostile until proven otherwise — parameterized queries instead of string-concatenated SQL, output encoding instead of raw interpolation into HTML — which neutralizes whole categories of the OWASP Top 10, the industry’s running list of the most common web vulnerabilities: injection, broken access control, cryptographic failures, security misconfiguration, and the rest.

The point of stacking these is not redundancy for its own sake; it is that each layer assumes the others have failed. The WAF will miss a novel payload — validation catches it. Validation will have a gap — least-privilege IAM means the exploit reaches only one narrow resource. The credential will leak — short-lived rotation means it is dead within the hour. No single control is trusted, so no single failure is fatal. That is the whole argument for defense in depth, and it is why a security review asks not “is this control perfect?” but “when this control fails, what catches it?”

War story: the dependency that everyone had and nobody patched

A mid-size company ran a Java service that, like most Java services of its era, pulled in Log4j somewhere in its dependency tree — not directly, but four levels deep, imported by a library imported by a library. When Log4Shell broke, the security team’s first problem was not patching; it was discovery. They had no SBOM, so they could not answer “do we even use this?” without grepping every build of every service by hand, and the answer took days during which the vulnerable version was reachable from the public internet. The exploit needed nothing exotic — a crafted string in a header that the logger would helpfully resolve into a remote code fetch. By the time the team confirmed which services were affected and rolled a patched version, the window had been open for a week. The fix afterward was unglamorous and exactly the discipline in this chapter: an SCA scanner wired into CI that would have flagged the vulnerable version the day the CVE published, an SBOM generated per build so “are we affected?” is a one-second query, and a policy that fails the pipeline on CRITICAL findings. Every one of those controls existed and was well understood before the incident. The breach was not a failure of knowledge; it was a failure to do the boring, automatable thing in advance.

Build it → Supply-chain and image-scanning controls in a CI pipeline are the bridge between this chapter and CI/CD — the same Project 05: SaaS Web Platform wires dependency and container scanning into its build, failing the pipeline on critical findings.


Practical exercise

Difficulty: Level I · Level II · Level III

  1. Level I — Get a secret out of code. Find a hardcoded secret in a small app (a DB password or an API key in a config file or source) and move it out: into an environment variable injected at runtime, then into a secret manager (a local Vault dev server or your cloud’s secrets manager). Then write a paragraph explaining why the original approach was unrecoverable — specifically, why removing the secret in a later commit does not remove it from git history, and why the only real fix for a secret that ever touched a repo is rotation, not deletion.
  2. Level II — Scan and triage. Add dependency scanning (Trivy or pip-audit) and container image scanning (Trivy) to a CI pipeline for a real project. Run it, then triage the findings: for each HIGH or CRITICAL, decide whether it is exploitable in your usage, whether a patched version exists, and what the fix is. Configure the pipeline to fail on CRITICAL but allow MEDIUM through with review, and justify that threshold in terms of the cost-of-fix curve across the lifecycle.
  3. Level III — Threat-model a service. Take a small service (say, a multi-tenant API with a database and a public endpoint) and produce a threat model: list the assets, map the entry points, enumerate the threats at each (use STRIDE as a checklist), and specify the layered controls — authentication, object-level authorization, secrets management, mTLS between services, and least-privilege IAM. Then make the defense-in-depth argument explicit: pick two controls, and for each, describe a plausible single failure and argue precisely which other layer contains the blast radius so the failure is not catastrophic.

Summary

Security is not a feature you add at the end; it is a cross-cutting discipline of risk management through layered defense, and almost every real breach traces to a missing fundamental rather than an exotic exploit. Three principles carry the program: least privilege (every actor gets the minimum access it needs), defense in depth (layers so that no single failure is fatal), and assume breach / zero trust (verify every request; the network is not a boundary). The threat-modeling sequence — assets, entry points, threats, controls — turns “make it secure” into a finite, ordered problem. The load-bearing distinction is authentication (who you are) versus authorization (what you may do); confusing them produces the cross-tenant leaks that sink companies. Secrets belong in a manager with rotation and short-lived credentials, never in code or images or git history. Dependencies are your largest attack surface, so you scan them — and your images — in CI, generate SBOMs, and sign artifacts. And TLS, mTLS, and encryption at rest protect the data in motion and at rest, with service identity replacing network location as the trust boundary.

Key takeaways

  • Most breaches are missing fundamentals, not novel exploits — a secret in git, an unpatched CVE, a missing authorization check. Get the boring things right first.
  • Authentication proves identity; authorization grants access. They are separate checks, and the most dangerous bug is the authorization check that is simply absent.
  • A secret that ever touched code, an image, or git history must be rotated, not deleted — git and image layers keep deleted content forever. Use a secrets manager with short-lived, rotated credentials.
  • Your dependencies and base images are your biggest attack surface; scan them in CI, fail on critical CVEs, keep an SBOM, and sign your artifacts.
  • Defense in depth means every layer assumes the others have failed — so design controls by asking “when this one fails, what catches it?”, and scope every grant to least privilege so one failure stays contained.

Connections to other chapters

  • CI/CD (sibling): the supply-chain controls in this chapter — dependency and image scanning, SBOM generation, artifact signing, and secure secret handling — live in the pipeline. Security shifts left precisely by making CI the place where these gates run automatically on every change; the two disciplines are inseparable.
  • Python: Web Development and Go: Web Services (siblings): where the in-service mechanics of authentication and authorization are actually implemented — password hashing, session stores, JWT issuance and verification, OAuth wiring, and per-endpoint authorization checks. This chapter frames the distinctions; those chapters write the code.
  • Orchestration with Kubernetes (Part V, extension): the cluster is where least privilege and defense in depth become operational — RBAC for service accounts, network policies for default-deny segmentation, secrets injected rather than baked, and a service mesh providing mTLS identity. The principles here are the why behind those Kubernetes controls.
  • Containerization with Docker (Part V, prerequisite): minimal, non-root images are a direct security control — a smaller image has less to exploit and a shorter scan report, and the warning there about secrets surviving in deleted layers is the same hazard this chapter raises about git history.

Further reading

Essential

  • OWASP Top 10 and the OWASP Application Security Verification Standard (ASVS) — the industry’s running catalog of the most common web vulnerabilities, and a structured checklist of verifiable security requirements graded by assurance level.
  • NIST SP 800-207, Zero Trust Architecture — the authoritative articulation of “never trust, always verify” and why the network perimeter is no longer a security boundary.

Deep dives

  • Anderson, Security Engineering — the field’s definitive textbook on how real systems fail and how to reason about adversaries; long, but the canonical reference.
  • SLSA (Supply-chain Levels for Software Artifacts) and Sigstore — the modern frameworks for build provenance and artifact signing that turn the supply chain into something verifiable rather than trusted.

Historical context

  • Saltzer & Schroeder, “The Protection of Information in Computer Systems” (1975) — the paper that named least privilege, fail-safe defaults, and defense in depth; fifty years on, every principle in this chapter descends from it.