An application may make access decisions with help from a policy service, identity provider, entitlement database, or another remote dependency. That design works until the dependency times out. At that moment, the application still has to answer a security question: should this request be allowed?

A dangerous fallback is to treat “I could not check” as “allow.” A temporary outage can then become an authorization bypass. But denying every operation whenever any security-related dependency is unavailable can create unnecessary outages and may push teams toward unsafe emergency workarounds.

The useful principle is fail closed: when the system cannot establish that a protected action is authorized, it should not grant that action. This article explains how to apply that principle precisely, distinguish authorization failure from ordinary service failure, and design safe degraded behavior without confusing availability with permission.

Model authorization as a positive decision

A simple authorization check has three meaningful outcomes, not two:

ALLOW   - the system established that this action is permitted
DENY    - the system established that this action is not permitted
ERROR   - the system could not establish either result

The third state matters. A timeout, corrupted policy response, unavailable database, or failed signature verification does not prove that the caller is authorized.

For a protected action, the decision rule should therefore look conceptually like this:

if decision == ALLOW:
    perform_action()
else:
    do_not_perform_action()

DENY and ERROR may produce different responses, logs, metrics, and retry behavior, but neither becomes permission.

This is the core mental model: authorization is evidence that must be established, not a default that survives when the evidence disappears.

The threat is an ambiguous failure becoming authority

Consider an internal application that asks a policy service whether a user may export customer records.

Under normal conditions:

user -> export request -> application -> policy service
                                      <- ALLOW or DENY

Now suppose the policy service is unreachable. The application catches the exception and uses this fallback:

try:
    decision = policy.check(user, "export_customer_records")
except PolicyUnavailable:
    decision = ALLOW

The code may have been written to preserve availability, but it changes the security model. Anyone who can send an export request during that failure window is evaluated under weaker rules than during normal operation.

An attacker does not necessarily need to cause the outage. They may only need to notice or encounter it. Operational failures are common enough that security should not depend on them never happening.

Failing closed changes the consequence of the same failure:

try:
    decision = policy.check(user, "export_customer_records")
except PolicyUnavailable:
    decision = ERROR

if decision != ALLOW:
    reject_request()

The export may become temporarily unavailable, but the outage does not create new authority.

This control reduces the risk of access being granted because an authorization check failed. It does not protect against a policy service that incorrectly returns ALLOW, compromised trusted policy data, stolen authorized credentials, or application code that bypasses the authorization path entirely.

Fail closed at the security decision, not everywhere

“Fail closed” is sometimes interpreted as “shut down the whole application when anything goes wrong.” That is too broad.

The important boundary is the operation whose authorization cannot be established.

Suppose the same application offers three functions:

  • viewing a public status page;
  • reading the signed-in user’s own basic profile;
  • exporting restricted customer records.

If the export policy service is unavailable, the public status page may have no dependency on that decision at all. Disabling it adds no security value. The profile page may use a different, locally enforceable rule. The export operation, however, should not proceed unless its required authorization can be established.

This leads to a more precise rule:

Deny the operation whose required security condition is unknown. Do not automatically deny unrelated operations whose required conditions can still be established.

That distinction improves availability without weakening the protected decision.

Separate denial from system error

Although DENY and ERROR should both stop a protected action, operations teams need to tell them apart.

A normal denial means the authorization mechanism worked and decided that the caller lacked permission. An error means the application could not obtain a trustworthy decision. Treating both as the same operational event can hide outages; treating an error as permission creates the bypass.

A useful internal model is:

ALLOW -> continue
DENY  -> reject; record an authorization denial when useful
ERROR -> reject; record dependency or decision failure; alert when warranted

The external response should avoid revealing unnecessary policy details. The exact HTTP status or user-facing message depends on the application and where the failure occurred. The security requirement is simpler: an error must not silently become an allowed protected action.

Monitoring should make ERROR visible. Track authorization-dependency failures separately from ordinary denials so responders can distinguish a policy outage from a rise in unauthorized requests.

Use cached decisions only when their meaning is bounded

Caching can keep some systems operating when an authorization dependency is briefly unavailable, but a cache is not automatically a safe fail-closed mechanism.

Imagine the application previously received:

ALLOW user 4812 to read project 73

Reusing that decision during an outage may be reasonable only if the system has deliberately defined how long the decision remains valid and what changes can revoke it. Otherwise, a user whose project access was removed could retain access until the dependency recovers.

Before using cached authorization as degraded behavior, answer four questions:

  1. What exactly was authorized? Bind the decision to the relevant subject, action, resource, and security context. A cached read decision must not imply permission to update or export.
  2. How old may the decision be? Choose a lifetime based on how quickly permissions must take effect when changed or revoked.
  3. Which changes invalidate it? Role changes, account disablement, resource ownership changes, or policy-version changes may make old decisions unsuitable.
  4. Which actions are too sensitive to cache? For high-impact operations, requiring a fresh authoritative decision may be preferable to preserving availability.

A cache therefore represents previously established authorization under explicit assumptions. It should not manufacture an ALLOW result for a request that has never been authorized.

Be careful with stale identity information

Authorization often depends on identity attributes such as account status, group membership, tenant, or authentication strength. If those attributes come from another service, stale copies can create the same class of problem as stale policy decisions.

For example, an application might locally cache that a user belongs to an administrator group. If the user’s membership is revoked but the cache remains valid for hours, authorization based on that value can remain permissive after the authoritative state changed.

There is no universal cache lifetime that solves this. The acceptable staleness depends on the consequence of delayed revocation and the availability requirements of the system.

A low-risk read operation may tolerate a short period of stale membership. Creating privileged credentials may require current state. Make that difference an explicit policy rather than an accidental property of cache configuration.

Design degraded modes as smaller sets of authority

A useful degraded mode preserves operations that can still be justified while removing operations whose security conditions are uncertain.

For example, during an authorization-service outage, an application might permit access to already-public information, allow a user to sign out, and keep health endpoints available while temporarily rejecting privileged mutations.

The key property is monotonicity of authority: the failure mode should not give a caller more permission than the normal mode.

A degraded mode can be designed around capabilities such as:

normal mode:
  public reads
  authorized private reads
  authorized writes
  privileged administration

degraded mode:
  public reads
  operations authorized by trustworthy local state, if explicitly designed
  sign-out and recovery-safe operations

The exact boundary is application-specific. The important review question is: which security facts are still trustworthy during this failure, and which operations require facts we no longer have?

Avoid emergency bypasses that become permanent

Strict failure handling can create operational pressure. If a critical business workflow stops whenever a policy service is unavailable, someone may eventually add a global SKIP_AUTHORIZATION=true switch.

That is not a resilience strategy. It creates a second authorization architecture, often with less testing, weaker monitoring, and unclear ownership.

Prefer recovery mechanisms that preserve the security boundary. Examples include making the policy service highly available, using bounded caches for carefully selected low-risk decisions, isolating unrelated features from the dependency, and providing narrowly scoped administrative recovery procedures with strong authentication and audit logging.

If an emergency access mechanism is genuinely required, design it as a security feature: limit who can activate it, limit what it authorizes, make activation visible, record its use, and define how it is revoked. Do not make “ignore authorization errors” the recovery path.

Test failure paths deliberately

Authorization error handling is easy to miss in ordinary tests because the dependency normally works. Test the failure states as first-class security behavior.

For each protected operation, verify what happens when the authorization dependency times out, returns an invalid response, is unreachable, or produces an internal error. The action should not occur unless the application still has another explicitly trusted way to establish authorization.

Also verify that failures are observable. A good test checks not only that the request is rejected, but that the system records enough context to diagnose the dependency problem without logging credentials, session tokens, or other secrets.

Finally, test recovery. When the dependency becomes healthy again, normal authorization should resume without requiring an unsafe manual override or leaving stale degraded-mode state behind.

Know where fail closed is not enough

Fail-closed behavior protects one narrow but important boundary: uncertainty must not turn into permission. Other controls remain necessary.

The authorization service itself must return correct decisions. Calls to it need appropriate authentication and integrity protection when they cross trust boundaries. Application routes must consistently invoke the required authorization checks. Administrative policy changes need suitable access control. Logging and monitoring should help detect failures and suspicious changes.

Availability also remains a security concern. An attacker who can make an authorization dependency unavailable may be able to deny service even when the application correctly fails closed. Redundancy, resource isolation, timeouts, capacity planning, and carefully bounded degraded modes reduce that risk; changing an unknown decision to ALLOW does not.

Make uncertainty reduce authority

When a protected action depends on an authorization decision, model ALLOW, DENY, and ERROR separately. Continue only when the required permission has been positively established. A dependency failure is not evidence of authorization.

Then design availability around that invariant. Keep unrelated features independent, use stale or cached security state only under explicit limits, make decision failures observable, and test degraded behavior before an outage forces the question.

The practical goal is not to make every failure stop the entire system. It is to make sure that when security information becomes uncertain, the system does not respond by granting more authority.