[ ← blog ]

Fail closed: default-deny as a design discipline

A control that fails open is a control that isn't there when it matters. Notes on building systems where the error path is also the safe path.

Ask what a security control does when it breaks. If the honest answer is “it lets the request through,” you don’t have a control — you have a control-shaped object that disappears under exactly the conditions where you needed it.

Fail closed means the default outcome, when something goes wrong or undefined, is to deny. It’s a discipline that shows up in a hundred small decisions, and the tempting choice is almost always the open one, because open is what keeps traffic flowing when you’re mid-incident.

The authorization check that swallows its own errors

The canonical example:

def is_allowed(user, resource):
    try:
        return policy.evaluate(user, resource)
    except Exception:
        # "Don't let the authz service take down the app."
        return True   # <-- fails OPEN

The comment is sympathetic and the code is wrong. When the policy engine is degraded — the exact moment your assumptions are least trustworthy — this grants everyone access to everything. The fix is one word:

    except Exception:
        log.error("authz evaluation failed; denying")
        return False  # fail closed

Yes, a broken policy engine now causes user-visible denials. That’s the point: the failure is loud, bounded, and safe, instead of silent and catastrophic.

Default-deny is the same idea, structurally

The pattern generalizes to how you define the rules at all. An allowlist — “deny everything, then permit these known-good cases” — fails closed by construction: anything you forgot to consider is denied. A denylist fails open: anything you forgot to enumerate is allowed, and attackers are in the business of finding the things you forgot.

  • Network policy: start from deny-all, open specific flows.
  • API surface: unmatched routes return 403/404, not a permissive default.
  • Feature flags gating access: absent or unreadable flag means “off.”

When fail-closed is the wrong default

Discipline isn’t dogma. Fail-closed on a control that gates safety systems — the door that must open in a fire — is how you get hurt. The rule is about security boundaries, where the cost of wrongly allowing exceeds the cost of wrongly denying. Name which kind of boundary you’re on before you pick a direction.

The habit

Whenever you add a check, write down its behavior on three inputs: the clear yes, the clear no, and the unknown — timeout, exception, malformed data, missing config. The unknown case is the whole game. If you can’t say what happens there, you haven’t finished designing the control.