Fundamentalsdefaultsdeny by defaultprivate by defaultconfigurationguard rails

Secure Defaults

The default configuration is the configuration most of your system will actually run, so the security question is not "can it be configured safely?" but "what happens when nobody configures it at all?"

▶ Run the labFollow the failure

Frame the problem

Security starts with a concrete asset, attacker capability and trust crossing.

Asset
Every resource created by someone who was not thinking about security at the time — which is most resources, created by most engineers, on most days.
Attacker & capability
An opportunist scanning for the predictable results of defaults: open buckets, default credentials, debug endpoints, permissive CORS, wildcard policies.
Trust boundary
The boundary between "explicitly decided" and "whatever the framework did", which is where most production exposure lives.
AssetThreatAttack SurfaceTrust BoundaryVulnerabilityExploit PathImpactMitigationDefense in DepthResidual Risk

The default is the policy

Any security property that requires an engineer to remember something will be absent from a predictable fraction of your system. Not because engineers are careless, but because the number of decisions per feature is large and security is one of many. The only reliable lever is to change what happens when nobody decides.

This shows up most sharply in framework and platform design. A router where a route without an auth decorator is public produces public endpoints. A router where an unannotated route raises at startup produces annotated routes. Same team, same care, opposite outcome — the difference is entirely in which direction the default points.

The design test is straightforward: write down what a new endpoint, a new storage bucket, a new database role, a new service identity and a new cookie look like when created with zero security-specific effort. If any of those answers is "open", that is your real security posture, regardless of what the documentation says.

Defaults that decide outcomes
ThingBad defaultSecure default
New API routePublic unless a decorator is addedFails at startup unless an explicit policy is declared
New storage bucketInherits an account-wide permissive policyPrivate, with public access blocked at the account level
New database roleInherits broad schema grantsNo grants; permissions added per table and verb
Session cookieSameSite unset, no Secure, no HttpOnlyHttpOnly; Secure; SameSite=Lax, short lifetime
CORSAccess-Control-Allow-Origin: * for convenienceNo CORS headers; specific origins added deliberately
Token lifetimeLong, because refreshing is annoyingMinutes, with refresh handled by the framework
Error responseStack trace and SQL in the bodyCorrelation id in the body, detail in the log
Debug endpointsPresent, gated on an environment variableNot compiled into the production build

Make the secure path the easy path

Secure defaults fail when they are annoying, because engineers route around annoyance reliably and creatively. A policy that makes local development painful produces a shared "dev" credential with broad permissions that eventually reaches production. A deny-by-default IAM system without a fast path to request a permission produces wildcard policies attached in frustration.

So the discipline has two halves and the second one is usually skipped: point the default at safety, *and* make the safe path fast. A helper that creates a properly-scoped role in one line. A session cookie helper that sets the attributes so no one writes Set-Cookie by hand. A test that fails when a route has no declared policy, with an error message that says exactly what to add. A template repository where the safe configuration is what you get by cloning.

The strongest version of this is making insecure configurations *unrepresentable*: a storage module that does not expose a "public" flag, a query builder with no string-concatenation entry point, an HTTP client that will not follow redirects to private address ranges. What cannot be expressed cannot be misconfigured under deadline, which is the only condition that matters.

1type Policy = { public: true } | { requires: Permission }
2
3// Every route must declare a policy. There is no third option, and no default.
4export function route<T>(path: string, policy: Policy, handler: Handler<T>) {
5 registry.push({ path, policy, handler })
6}
7
8// At boot: refuse to start rather than serve something undeclared.
9export function mount(app: App) {
10 for (const r of registry) {
11 if (!r.policy) throw new Error(`route ${r.path} has no policy — declare { public: true } or { requires }`)
12 app.handle(r.path, async (req, res) => {
13 if ('requires' in r.policy && !req.principal?.can(r.policy.requires)) return res.sendStatus(403)
14 return r.handler(req, res)
15 })
16 }
17 // A public route is now a visible, greppable, reviewable decision:
18 // route('/health', { public: true }, healthHandler)
19}

Key points

  • The default configuration is your real security policy, because most resources are created without security-specific thought.
  • Point defaults at deny and private, and make forgetting a startup error rather than a silent exposure.
  • A secure default that is annoying gets routed around; the safe path must also be the fast path.
  • Strongest form: make the insecure configuration unrepresentable in the API you give engineers.
  • Public, permissive or long-lived should always be an explicit, greppable, reviewable line of code.

Boundary control exercise

This lesson uses the shared boundary-control exercise.

Boundary control check
Untrusted input / identity
Trust boundary
Privileged asset
Prevention may fail silently.

Follow the attack

Safe conceptual simulation: capability → missing control → crossed boundary → asset impact.

  1. 1
    Attacker → scan for defaults: open buckets with predictable names, default credentials, exposed dashboards, permissive CORS, verbose errors.
  2. 2
    Default → access: no exploitation needed; the resource is simply readable or the credential simply works.
  3. 3
    Access → escalation: use what the default exposed (a config file, a token, an admin UI) to obtain a real identity.
Blast radius
  • Exposure without exploitation: the most common large data leaks require no vulnerability at all, only a permissive default that nobody changed.
  • Because there is no attack, there is usually no log entry that looks unusual — the access is indistinguishable from legitimate use.

Defend, detect, recover

One prevention is a single point of security failure. Layer it and make failure observable.

Prevent
  • • Deny by default at every layer: routes, storage, roles, network, and the cloud account itself.
  • • Enforce defaults in code and infrastructure templates rather than in documentation or review checklists.
  • • Fail closed at startup on missing declarations, so misconfiguration is caught in CI rather than by a scanner.
  • • Remove the capability entirely where it is not needed: no debug endpoints in production builds, no public flag in the storage wrapper.
Detect
  • • Continuous configuration scanning for public storage, wildcard IAM, `0.0.0.0/0` rules and missing cookie attributes.
  • • Alert on the *change* that made something public, not only on the state, so you catch it in minutes rather than at the next audit.
  • • Fail CI on infrastructure diffs that widen exposure without an explicit approval marker.
Respond & recover
  • • Close the exposure immediately; assume everything reachable during the exposure window was read.
  • • Determine the window from creation time, not discovery time.
  • • Fix the template or module that produced the default, since the same resource will be created again next week.
Residual risk
  • • Third-party services have their own defaults, and yours cannot govern them.
  • • Legacy resources created before the safe default existed keep their original configuration silently.
  • • A safe default can be overridden legitimately, and the override is where the exposure returns.

Misconceptions

Claim
“We document the secure configuration.”
Reality
Documentation is a default of "whatever the framework does" plus a hope. If the insecure configuration is expressible, it will be produced.
Claim
“Developers can be trusted to set this correctly.”
Reality
They can, and will, most of the time. Security properties that hold "most of the time" across thousands of resources hold nowhere in particular.