An application may have a correct authorization policy and still expose protected actions when the system that evaluates that policy is unavailable. The dangerous shortcut is to treat a timeout, network error, or missing policy result as permission so that the application can keep working.
That changes an availability problem into an access-control problem. A temporary dependency failure can then let a requester perform an action that the application never established they were allowed to perform.
The useful mental model is: authorization needs an affirmative grant, not merely the absence of a denial. If a protected action depends on an authorization decision and the application cannot obtain a trustworthy decision, it should not perform that action.
This article explains how to apply that rule without confusing every infrastructure error with an explicit denial, how caching changes the trade-off, and how to preserve useful availability without silently granting authority.
Model authorization as more than allow or deny
Authorization is often described as a Boolean decision:
allow = true
allow = falseThat is convenient after a decision has been made, but a distributed application has another important state: no trustworthy decision is available.
For example, an API may ask a policy service whether user 42 may approve payment 781:
principal: user 42
action: approve
resource: payment 781The policy service can return allow or deny. It can also time out, become unreachable, return malformed data, or fail because required policy data is unavailable.
Those outcomes should not be collapsed into allow.
A more useful model is:
ALLOW -> the policy established permission
DENY -> the policy established no permission
UNKNOWN -> the application cannot establish a trustworthy decisionFor a protected action, both DENY and UNKNOWN stop the action. They may produce different logs, metrics, user messages, retries, and operational responses, but neither supplies authority.
This is what fail closed means in this context: when the control that grants access cannot establish permission, the protected operation remains unavailable rather than becoming permitted by default.
Why failure is not permission
Suppose an internal administration tool uses a separate authorization service. The application contains logic conceptually like this:
if authorization_service_allows(request):
perform_sensitive_action()
else:
reject_request()Now imagine someone changes the error handling because authorization-service outages are causing support incidents:
try:
allowed = authorization_service_allows(request)
except ServiceUnavailable:
allowed = TrueThe intent is availability. The effect is broader: every condition that reaches that error path now bypasses the authorization decision.
The requester does not need to be entitled to the action. The application simply stops asking for proof of entitlement when the dependency is unhealthy.
The defensive change is small but important:
try:
allowed = authorization_service_allows(request)
except ServiceUnavailable:
allowed = False
if allowed:
perform_sensitive_action()
else:
reject_request()This is simplified teaching pseudocode, not production error handling. A real application should distinguish an explicit policy denial from an evaluation failure so operators can diagnose outages correctly. The security property is that only a valid ALLOW result reaches the protected operation.
Define the trust boundary around the decision
Failing closed is easier when the application has a clear answer to one question: what evidence counts as an authorization grant?
That evidence may come from local policy, a policy engine, a database lookup, signed identity attributes, group membership, resource ownership, or several inputs combined. Whatever the design, the component performing the protected action should know which inputs must be trustworthy before it proceeds.
Consider a service that allows a project administrator to delete a project. Its authorization decision might depend on:
authenticated principal
+
requested action
+
project identity
+
current role membership
|
v
decisionIf current role membership cannot be loaded, the service does not know that the principal is an administrator. An old session, a valid username, or the fact that the user reached the endpoint does not fill that gap.
This distinction prevents a common reasoning error: substituting weaker evidence when the required evidence is unavailable.
Keep explicit denial separate from evaluation failure
Stopping the action does not mean every failed evaluation should look identical inside the system.
An explicit denial is an expected authorization outcome. An evaluation failure is an operational problem. Treating both as the same internal event can hide incidents and make outages difficult to diagnose.
A useful internal result type is conceptually:
AuthorizationResult = ALLOW | DENY | INDETERMINATEINDETERMINATE means the application could not produce a trustworthy authorization answer. The exact name is not important; preserving the distinction is.
At the request boundary, a service may map DENY and INDETERMINATE to different responses depending on its protocol and information-disclosure requirements. For example, an evaluation outage may justify a temporary-service-error response rather than pretending the user’s permission was explicitly denied.
Internally, record enough structured information to answer questions such as:
- Which policy dependency failed?
- Which protected action could not be evaluated?
- Was the result an explicit denial or an evaluation error?
- How often is the failure occurring?
Do not put credentials, raw session tokens, or unnecessary sensitive resource data into those logs. Observability should help diagnose the control without creating another sensitive-data store.
Use caching deliberately, not as an accidental bypass
Failing closed does not require every authorization check to depend on a live remote call. Some systems can safely use cached authorization data, but the cache becomes part of the security design.
Suppose a service caches the fact that a user has a project role. During a policy-service outage, it can continue making decisions from that cache. This may improve availability, but it changes the assumption from:
permission is based on current policy data
to:
permission may be based on policy data up to a defined age
That difference matters when permissions can be revoked.
If a user’s administrator role is removed at 10:00 but another service accepts a cached role until 10:30, the removal may take up to that cache window to affect authorization in that service. The system has chosen bounded staleness in exchange for availability.
That can be a reasonable trade-off for some actions. It may be unacceptable for actions where rapid revocation is important.
Make the choice explicit. Define which decisions may use cached data, how old that data may be, what happens after it expires, and whether high-impact actions require fresher evidence than ordinary reads.
Most importantly, do not implement a cache miss as permission. A cache can provide previously established evidence under stated freshness rules; the absence of cached evidence is not an authorization grant.
Match failure behavior to the action’s risk
Not every operation needs the same dependency strategy.
For a low-impact read of non-sensitive data, a product may choose to serve previously authorized cached content during a short control-plane outage. For deleting an account, changing payment details, exporting sensitive records, or granting privileges, the system may require a fresh authorization decision and reject the action when that decision cannot be obtained.
The important point is not that one strategy is universally correct. It is that the availability decision must not silently weaken the authorization requirement.
A practical design separates operations into groups based on what stale or missing authorization information could cause. Ask:
If permission was revoked moments ago,
what happens if this action still succeeds?If the consequence is difficult to reverse, exposes sensitive data, changes authority, or crosses an important trust boundary, fresher authorization evidence is usually easier to justify.
If the consequence is limited and reversible, a bounded-staleness policy may provide a better availability trade-off.
Design retries so they do not bypass the control
Transient failures often deserve retries, but retries should repeat the authorization attempt rather than skip it.
A request handler might retry a policy lookup a small number of times within its latency budget. If no trustworthy decision arrives, it stops the protected action and returns an appropriate failure.
Be careful with asynchronous work. If an API authorizes a job when it is created and a worker performs the job much later, decide whether authorization at submission time is sufficient. For long delays or high-impact operations, permissions may need to be checked again before execution because the principal’s authority or the resource state may have changed.
The correct choice depends on what the authorization is meant to guarantee. The key is to define the decision point deliberately rather than letting queue retries or worker outages create an undocumented bypass.
Avoid fallback identities with broader authority
Another risky availability pattern is replacing a failed user-specific authorization path with a more privileged service identity.
For example, a service cannot verify whether a requester may update a record, so it sends the update through an internal component that has unrestricted database access. The database operation succeeds, but the original authorization question remains unanswered.
Service credentials and database permissions establish what software components can technically do. They do not automatically establish what the initiating user is allowed to do.
Preserve the original principal and requested action across service boundaries where authorization depends on them. If a downstream service is responsible for the final decision, give it the trustworthy context needed to make that decision. Do not treat possession of an internal service credential as a substitute for end-user authorization unless the system’s policy explicitly grants that service independent authority for the operation.
Verify the failure path, not only the success path
Authorization tests often cover obvious cases: an administrator succeeds and an ordinary user receives a denial. That misses the behavior this design is intended to protect.
Test what happens when each required authorization dependency is unavailable or returns unusable data. The protected action should not execute unless the system still has valid evidence under an intentional fallback policy, such as a sufficiently fresh cache.
Useful tests include:
- the policy service times out;
- required role or ownership data cannot be loaded;
- a cached authorization record is expired;
- a dependency returns a response the caller cannot validate;
- a retry budget is exhausted;
- authorization succeeds but the protected operation is delayed until after relevant permission changes.
For each case, verify both security and operations: the action does not gain unintended authority, the failure is observable, and the response does not misrepresent an infrastructure failure as a successful operation.
Understand what fail-closed behavior does not solve
Failing closed protects one specific boundary: it reduces the risk that inability to evaluate authorization becomes an unintended grant.
It does not make the authorization policy correct. A policy can reliably return ALLOW for the wrong users. It does not protect against compromised policy data, stolen privileged credentials, or a component that bypasses authorization entirely. It also does not remove denial-of-service risk; in fact, making authorization dependencies mandatory can reduce availability when those dependencies fail.
That availability cost is real. Address it with resilient architecture: redundant policy services, local evaluation where appropriate, carefully bounded caches, dependency health monitoring, capacity planning, and recovery procedures. Those measures improve the chance of obtaining a trustworthy decision. They should not redefine missing evidence as permission.
Conclusion
A protected action should run because the application established that it is allowed, not because the authorization system failed to say no.
Model authorization outcomes so ALLOW, DENY, and evaluation failure remain distinct. Let only an affirmative, trustworthy grant reach the protected operation. If availability requires cached or local decisions, define their freshness and scope explicitly, especially where revocation matters.
The practical rule is simple: make authorization evidence resilient, but do not make authorization optional when that evidence is unavailable.