Fundamentalsfail closedfail openavailabilitygraceful degradationerror handling

Fail Open vs Fail Closed

When the control cannot make a decision — the policy service is down, the token cannot be verified, the rate limiter is unreachable — the system must do something, and choosing which way it fails is a design decision with no universally right answer.

▶ Run the labFollow the failure

Frame the problem

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

Asset
Both the data behind the control and the availability of the service — the two things this decision trades against each other.
Attacker & capability
One who can cause the failure. If a control fails open, making it fail becomes the attack: exhaust the policy service, block the token endpoint, cause a timeout.
Trust boundary
The authorization or validation boundary, at the moment it cannot answer.
AssetThreatAttack SurfaceTrust BoundaryVulnerabilityExploit PathImpactMitigationDefense in DepthResidual Risk

The decision, stated honestly

The authorization service times out. The request is holding a user, a resource and an action, and no verdict. Option A: allow the request and log it. Option B: deny the request and return an error. Both are defensible and both have shipped in serious systems.

Fail closed preserves confidentiality and integrity and sacrifices availability. It is right when the asset is worth more than the uptime: payments, admin operations, anything that moves money or grants access, and anything where a wrong allow is unrecoverable. Its failure mode is a full outage caused by a dependency that is not on the critical path in anyone's mental model — which is exactly how a policy service becomes the least reliable component in the system.

Fail open preserves availability and sacrifices the control. It is defensible for controls that are advisory rather than authoritative — a rate limiter, a bot check, a recommendation filter, a soft content policy. It is indefensible for authorization, because an attacker who can cause failures has just discovered an authentication bypass with a denial-of-service prefix.

The critical insight is that the attacker chooses when the failure happens. Any fail-open control is only as strong as your ability to keep it available under attack, and availability under attack is precisely what an attacker is willing to spend effort on.

Which way should each control fail?
ControlFails toWhy
Authorization decisionClosedA wrong allow is unrecoverable; the attacker controls when the failure happens
Token signature verificationClosedAn unverifiable token is an unauthenticated request, not a trusted one
Payment authorizationClosedMoney moved wrongly is a business incident, not a latency blip
Rate limiterOpen, with alarmsAdvisory; failing closed converts a limiter outage into a total outage
Bot / abuse scoringOpen, with alarmsHeuristic; a wrong deny is a lost customer, a wrong allow is one abusive request
Audit loggingClosed for privileged actionsAn action you cannot record is an action you cannot investigate
Feature flag serviceOpen to last known goodAvailability matters and the risk is behavioural, not security
Certificate validationClosed, alwaysThe whole point is refusing an endpoint you cannot verify

Making fail-closed survivable

The reason teams choose fail open is almost never a considered risk decision; it is that fail closed took the site down once and nobody wants to repeat it. The right response is to make fail closed cheap rather than to abandon it.

Cache decisions with a short TTL so a brief outage is invisible: if the policy service answered "allow" for this principal and resource 30 seconds ago, that answer is usually still valid. Ship the policy to the caller — a signed token carrying scopes, or a policy bundle evaluated locally — so the network call is not on the request path at all. Give the control a short timeout and a local fallback that is *more* restrictive rather than less: deny writes, allow reads.

And degrade in a shaped way instead of failing uniformly. When authorization is degraded, a system can continue serving cached read paths and refuse every write and every privileged action. Users see a read-only product for four minutes; nobody's data leaks and nothing is wrongly modified. That is a far better outcome than either extreme, and it requires that the code paths were designed to be separable in advance.

1async function authorize(principal: Principal, action: Action, resource: Resource): Promise<Decision> {
2 try {
3 return await policyService.decide({ principal, action, resource }, { timeoutMs: 150 })
4 } catch (err) {
5 metrics.increment('authz.unavailable') // this must page someone
6 const cached = decisionCache.get(principal, action, resource) // 30 s TTL
7 if (cached) return { ...cached, degraded: true }
8
9 // No cached answer. Reads degrade, writes and privileged actions fail closed.
10 if (action.kind === 'read' && !resource.sensitive) {
11 audit.log('authz.degraded.allow', { principal, action, resource })
12 return { allow: true, degraded: true }
13 }
14 audit.log('authz.degraded.deny', { principal, action, resource })
15 return { allow: false, reason: 'authorization-unavailable' }
16 }
17}

Key points

  • Authorization, signature verification and certificate validation fail closed. There is no version of the argument that survives review.
  • Advisory controls (rate limits, bot scoring) may fail open, but only with an alarm loud enough that the degraded state is temporary.
  • The attacker chooses when the failure happens, so a fail-open control is only as strong as its availability under attack.
  • Make fail closed survivable with short-TTL decision caches, locally-evaluated policy, and shaped degradation (reads yes, writes no).
  • Every degraded decision must be logged and counted; a silent fallback becomes permanent.

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 → identify a fail-open control: cause a timeout, exhaust a quota, or block a dependency and observe whether the request still succeeds.
  2. 2
    Failure → bypass: repeat the request under the induced failure, now without the control.
  3. 3
    Bypass → asset: perform the action the control was there to prevent, at whatever rate the failure can be sustained.
Blast radius
  • A fail-open authorization control is an authentication bypass triggered on demand — the worst class of bug, because it is invisible in normal operation.
  • A fail-closed control on a hot path is an availability incident that looks like a total outage and is often mis-diagnosed for a long time.
  • A silent degraded mode is the worst of both: the control is off and nobody knows for how long.

Defend, detect, recover

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

Prevent
  • • Default to fail closed for anything authorization-shaped; require an explicit, reviewed exception with a named owner to fail open.
  • • Remove the dependency from the request path where possible: signed tokens with embedded scopes, locally evaluated policy bundles.
  • • Cache positive decisions briefly so transient failures do not become user-visible.
  • • Design shaped degradation in advance so "read-only mode" is a supported state rather than an improvisation.
Detect
  • • Count and alert on every degraded decision, separately from errors; the metric should be zero in normal operation.
  • • Alert on the *ratio* of degraded to normal decisions, so a partial failure is visible before it becomes total.
  • • Audit-log any action allowed under degradation with a distinct event type so it can be reviewed retroactively.
Respond & recover
  • • For a fail-open bypass: treat every request during the window as unauthorized and audit what was accessed.
  • • For a fail-closed outage: switch to the shaped degradation path rather than disabling the control entirely under pressure.
  • • Record the duration precisely; both remediation and notification depend on the window.
Residual risk
  • • Cached decisions are stale by construction; a revoked permission remains usable for the TTL.
  • • Shaped degradation requires classifying every action as read or write correctly, and a misclassification hides in the fallback path where it is never exercised.
  • • The fallback path is the least tested code in the system precisely because it only runs during incidents.

Misconceptions

Claim
“Failing open is more user-friendly.”
Reality
It is more available and less safe, and the trade only makes sense for advisory controls. For authorization it converts a dependency outage into a data breach.
Claim
“Fail closed means the site goes down.”
Reality
Only if the control is on the request path with no cache and no shaped degradation. All three are solvable engineering problems.