An application may have correct authorization rules and still lose its security boundary when the component that evaluates those rules fails. A policy service can time out. A role lookup can return an error. Configuration can be unavailable. A programmer can catch the exception and continue because keeping the application online seems preferable to rejecting the request.
If that fallback grants access, an availability problem has become an authorization bypass. The application is no longer saying, “this principal is allowed.” It is saying, “I could not determine whether this principal is allowed, so I will allow the action anyway.”
The defensive principle is fail closed: when a protected operation requires an authorization decision and the application cannot establish an explicit grant, it should not perform that operation. This article explains how to apply that principle without confusing an authorization failure with an ordinary denial, and how to design for availability without weakening the boundary.
Treat authorization as a three-outcome decision
Authorization code is often written as though only two outcomes exist:
allowed
not allowedOperational systems have a third outcome:
allowed
not allowed
could not decideThe third state can come from a timeout, an unreadable policy, a failed database query, an invalid response from an authorization service, or another condition that prevents the application from evaluating the rule reliably.
The important mental model is that unknown is not allow.
Suppose a document service asks a policy component whether user 42 may read document 817:
request: user 42 reads document 817
policy result: ?A timeout provides no evidence that the user has permission. Converting that timeout into allow creates authority from missing information.
Failing closed means the protected action does not proceed unless the decision reaches an accepted allow state. It does not require pretending that every failure is a normal permission denial. The application can distinguish the reason internally while preserving the same security property at the enforcement point.
See the failure in a small example
Consider this simplified pseudocode:
allowed = true
try:
allowed = policy.can_read(user, document)
catch error:
log(error)
if allowed:
return documentThe dangerous detail is the initial value. If the policy lookup throws an exception, execution continues with allowed still set to true. The exceptional path therefore grants access even though no successful authorization decision occurred.
A safer shape starts from no authority:
allowed = false
try:
allowed = policy.can_read(user, document)
catch error:
log(error)
if not allowed:
do_not_return_document()
return documentThis teaching example deliberately compresses several concerns. Production code usually benefits from representing deny and error separately so monitoring, retries, user responses, and incident investigation can distinguish them. The security invariant is narrower: neither state may silently become a grant.
An even clearer interface makes all three outcomes explicit:
decision = policy.authorize(user, "read", document)
match decision:
ALLOW -> perform protected action
DENY -> reject as unauthorized
ERROR -> reject because authorization is unavailableThis structure makes the intended behavior visible during code review and easier to test.
State the threat model precisely
Fail-closed authorization is intended to reduce access that occurs because the application cannot complete a required security decision.
The relevant failure may be accidental. A dependency can be unavailable during a deployment or network incident. It may also be influenced by an attacker if the attacker can cause unusual inputs, resource exhaustion, or another condition that makes authorization evaluation fail. The defense should not depend on knowing why the failure occurred.
The control assumes the protected action reaches an enforcement point that can stop it. If one code path bypasses authorization entirely, fail-closed error handling elsewhere does not repair that missing check.
It also does not make the authorization policy itself correct. A policy that explicitly grants excessive access still returns allow. Nor does failing closed protect against a compromised authorization service that deliberately issues false grants.
Finally, failing closed trades some availability for preservation of the access-control boundary. During an authorization outage, legitimate users may be unable to perform protected operations. That is a real operational cost, not an implementation detail to hide.
Separate denial from inability to decide
A normal denial and an authorization-system failure have the same immediate security outcome: the protected action does not run. They have different operational meanings.
A denial means the system evaluated the relevant policy and concluded that the principal lacks the required permission. An error means the system could not establish the answer reliably.
Keep that distinction in internal control flow:
ALLOW = policy evaluated and granted the action
DENY = policy evaluated and rejected the action
ERROR = policy could not be evaluated reliablyThis prevents two common mistakes.
First, developers do not need to grant access merely to avoid presenting an ordinary “permission denied” message during an outage. The service can return an availability-oriented error while still withholding the protected resource.
Second, operations teams can alert on a surge in ERROR outcomes without treating ordinary authorization denials as infrastructure failures. A high error rate may indicate a policy-service outage, configuration problem, dependency failure, or another condition requiring investigation.
Be careful with external error details. Clients usually do not need stack traces, policy internals, or sensitive configuration information. Preserve useful detail in controlled telemetry while returning an appropriate, limited response to the requester.
Put the fail-closed rule at the enforcement point
The component that performs the protected action should require affirmative authorization before crossing the security boundary.
A fragile design can look like this:
controller -> authorization helper -> service -> protected dataIf the controller catches an authorization error and then calls the service anyway, the helper’s behavior no longer matters. Likewise, a user-interface check cannot protect an API that accepts the same operation directly.
The decisive rule should be difficult to bypass accidentally:
request
|
v
trusted enforcement point
|
+-- explicit ALLOW -> protected operation
|
+-- DENY ----------> no protected operation
|
+-- ERROR ---------> no protected operationCentralized authorization helpers, middleware, gateways, or policy-enforcement components can reduce duplicated logic, but architecture varies. The portable requirement is that every protected path reaches an enforcement point whose default is no grant.
This also affects new functionality. If a developer adds an endpoint but forgets to declare its permission rule, a deny-by-default design rejects the request until access is intentionally configured. An allow-by-default design turns missing policy into unintended exposure.
Design dependency failures before they happen
Failing closed is easiest when the failure behavior is an explicit part of the design rather than an emergency decision during an outage.
For each authorization dependency, ask what the application needs in order to issue a trustworthy grant. Examples include a valid local policy, current relationship data, a verified token with the required claims, or a successful decision from a policy service.
Then define what happens when that evidence is unavailable.
For a high-impact write operation, such as changing another user’s privileges, the answer may simply be to reject the operation until authorization can be evaluated again. For a lower-risk read, the same rule may still apply if the data is confidential: inability to check permission does not make the data public.
The user experience can acknowledge the temporary condition. A response such as “authorization service unavailable; try again later” is operationally different from “you do not have permission,” even though neither response releases the protected data.
Set bounded timeouts on remote authorization calls so requests do not wait indefinitely. A timeout should produce the defined error state, not an implicit grant. Retry policy also needs care: retries may improve resilience for transient failures, but they should be bounded and should not change the security decision from unknown to allow merely because attempts were exhausted.
Improve availability without inventing permission
Teams sometimes choose fail-open behavior because an authorization dependency has become a single point of failure. That diagnosis can be correct while the remedy is wrong.
If authorization availability is important, improve the availability of the evidence or decision path rather than bypassing it. Depending on the architecture, useful approaches can include redundant policy-service instances, local evaluation of distributed policy, replicated authorization data, or carefully designed caches.
Caching deserves special attention. A cached authorization decision is not automatically equivalent to a current decision. Permissions can be revoked, object ownership can change, and policy can be updated.
If cached grants are part of the availability design, define their validity conditions deliberately:
- which decisions may be cached;
- how long a grant may remain valid;
- which policy or data version the decision depends on;
- how revocation requirements affect acceptable staleness;
- whether sensitive operations require a fresh decision regardless of cache state.
A short-lived cached grant may be an acceptable trade-off in one system and unacceptable in another. The question is not whether caching is universally secure. It is whether the maximum stale-authority window fits the threat model and recovery requirements.
Do not treat “use the last answer forever” as a cache strategy. An indefinitely reusable old grant can preserve privileges long after they should have been removed.
Do not confuse fail closed with shutting down everything
The principle applies to actions whose authorization cannot be established. It does not mean every unrelated feature must stop whenever one security dependency fails.
Suppose a service contains public documentation and private account records. If the authorization service is unavailable, public documentation may remain available because it does not require a per-user grant. Private records should not become public merely to keep the endpoint responsive.
This distinction is useful during system design. Classify operations by the evidence they require:
public operation
-> no user authorization decision required
protected operation
-> explicit grant required
protected operation + decision unavailable
-> operation withheldSome systems can also provide a deliberately reduced mode during outages. For example, a feature may permit access only to information that is already public while disabling private or privileged actions. That is different from treating unknown authorization as permission.
A reduced mode must be designed in advance so developers know which operations remain valid under the degraded trust assumptions.
Watch for hidden fail-open paths
Fail-open behavior is not limited to a variable initialized to true. It can appear anywhere an exceptional path skips a security requirement.
One pattern is a broad exception handler that logs an error and continues into the protected operation. Another is treating a missing policy record as unrestricted access. A third is accepting an incomplete response from a remote policy service because some fields were present.
Boolean interfaces can also hide ambiguity. If a library returns false for both denial and transport failure, the security outcome may still be correct, but observability suffers. If it throws on failures, every caller must handle that exception without continuing into the protected action.
Configuration defaults matter too. A service that starts with an empty policy store should not interpret “no rules loaded” as “no restrictions.” If authorization configuration is required to protect the service, failure to load it should leave protected operations unavailable.
Background jobs need the same reasoning as interactive requests. A queued job that performs a privileged action should not skip its required authorization or authority validation merely because the policy dependency is temporarily unreachable. Whether the job should retry later, expire, or require a fresh decision depends on how quickly authorization can change and how sensitive the action is.
Test failures as first-class security cases
A happy-path authorization test proves too little. You need evidence that the application preserves its boundary when the decision mechanism misbehaves.
At minimum, exercise each meaningful outcome:
policy returns ALLOW -> permitted action succeeds
policy returns DENY -> protected action does not occur
policy times out -> protected action does not occur
policy returns error -> protected action does not occur
policy response invalid -> protected action does not occurAlso verify side effects. A handler can return an error after a database write has already happened. The test should confirm that the protected read, write, message publication, privilege change, or other side effect did not occur before authorization completed successfully.
For remote dependencies, fault-injection tests can simulate timeouts, connection failures, malformed responses, and unavailable configuration in a controlled environment. The goal is defensive validation: prove that ordinary infrastructure failures do not create new authority.
Monitoring should make authorization errors visible. Track error outcomes separately from denials, and alert when their rate indicates that legitimate protected operations are being blocked. Failing closed preserves the boundary, but an unnoticed outage can still become a serious availability incident.
Choose the trade-off consciously
There are systems where availability during partial failure is extremely important. That requirement does not remove the need for an authorization model; it changes the architecture needed to satisfy both goals.
For low-risk operations, a bounded stale decision may be acceptable if the business explicitly accepts the period during which revocation may not take effect. For high-impact operations, such as privilege administration or access to highly sensitive data, requiring a fresh authoritative decision may justify temporary unavailability.
The decision should state the residual risk in concrete terms. “Cache grants for five minutes” means a revoked permission may remain usable for up to roughly that cache lifetime under the assumed invalidation model. “Require a live decision” means an authorization outage can block legitimate operations. Those are understandable trade-offs that can be reviewed against the application’s threat model.
What should be avoided is an accidental policy created by error handling: “when security is uncertain, grant access.”
Conclusion
Authorization is not complete merely because the application knows how to handle allow and deny. Real systems also encounter timeouts, invalid state, missing configuration, and unavailable dependencies.
Model that uncertainty explicitly. Require an affirmative grant before a protected action crosses its enforcement point, keep denial separate from operational error, and test failure paths for both responses and side effects. If authorization availability is a business requirement, improve the resilience of the decision path or use deliberately bounded stale evidence whose risk is understood.
Failing closed does not eliminate outages, policy mistakes, or compromised authorization infrastructure. It does something narrower and essential: a failure to establish permission does not create permission by itself.