Sign-on policies and the decision trace
Ordered rules, first match wins, fail closed — and a per-rule trace naming the rule that decided and why every other one did not.
An authentication policy is an ordered list of rules. Each rule has conditions — who, from where, at what risk, on what kind of device — and an outcome: allow, ask for another factor, or deny. On every sign-in the rules are evaluated in order and the first one whose conditions all match decides. Nothing after it runs.
The part of this that matters most is not the engine. It is that every evaluation records a per-rule trace naming the winning rule and, for each rule that did not win, the specific condition that rejected it. An administrator who cannot see which rule locked a colleague out will not trust the product, and will be right not to.
First: there are two policy surfaces, and both are consulted
Three properties of that surface deserve their own lines, because each of them changes what you should write.
- A `require_mfa` policy against an account with no factor produces an enrolment challenge, not a refusal
- The sign-in answers
200carryingmfa_required, a five-minute single-use token, a TOTP secret and a QR code, and a message reading — verbatim —Your organization requires a second factor. Add Qorionix to your authenticator app using the code below, then enter the six digits it shows to finish signing in.The authenticator entry is likewise namedQorionix. An authenticator enrolled before the rename still shows the old issuer name for that entry, because changing the issuer string retroactively would invalidate every already-enrolled authenticator — only newly enrolled ones pick up the current name. Completing the enrolment enables the factor and returns ten recovery codes, once. This replaced a refusal that told people to ask an administrator to enrol an authenticator for them — a route that does not exist, because an administrator can only ever clear a factor, never create one. - The status value is compared as the exact literal `ACTIVE`
- Upper case, no surrounding whitespace. A policy saved with a status of
activeis stored, listed and displayed, and is silently never applied. If a policy you believe is on has changed nobody's experience, check the case of that field first. - This surface fails **open**
- If the policy rows cannot be read — a database problem, a missing table — the sign-in proceeds with no denial and no second-factor requirement, and the failure is logged. That is the deliberate opposite of the ordered authentication policy described in the rest of this article, which fails closed and answers
503. Two policy engines with opposite failure modes is not a design we are defending; it is the current state and you should know which one you are relying on.
sign-on policy would lock this organization out: it refuses every administrator of this organization ([email protected], [email protected]), from inside the office and outside it, and there would be nobody left who could sign in to undo it. Narrow it to the people or the networks you actually mean to block, or leave it INACTIVE until you have a second administrator who is not covered by it
The names are inside the message string rather than in a structured field, so if you are scripting against this, parse the sentence or — better — do not, and show it to the person. A separate and stricter check refuses an active policy that denies with no condition at all, with HTTP 400: a policy that denies with no user or network condition would refuse every sign-in for this organization, including yours. Narrow it to specific people or a network zone first.
Two limits on the lockout guard. It only considers denials — a policy requiring a second factor is not counted, because that is now satisfiable by enrolling during the sign-in rather than a dead end. And it cannot see a later change: the last unaffected administrator being removed afterwards, or rows written straight into the database, will still strand you, and a tenant in that state needs an operator to delete the policy row. See A locked-out tenant.
We would rather publish that than let you configure a security control that quietly does nothing. It is also the reason the trace exists: a policy you cannot observe deciding is indistinguishable from a policy that is not running.
The two surfaces combine in one direction only. Sign-on policies can add a requirement — refuse a sign-in, or demand a second factor — and can never take one away: they cannot waive a factor somebody has enrolled, and they cannot overrule an authentication policy rule that asked for a step-up. Among themselves they merge by order number, lowest number winning, except that a refusal beats an allow whatever the numbers say.
The layer that is live
Policies come in three types. As of this update two of the three are exercised, and it is honest to say exactly how much of each.
| Type | Intended for | Status |
|---|---|---|
GLOBAL_SESSION | Every sign-in to the organisation. One per organisation. | Live. This is what evaluates when anybody signs in. |
APP_AUTHENTICATION | Per-application rules, for an application assigned to a user. | Built, but not reachable — the sign-in path never names an application, so these rules are never selected. |
MFA_ENROLLMENT | Which factors a person may or must enrol, and (folded together with the authenticator on/off switch) which factors they may use at all. | Live for TOTP, and live for on/off enforcement of every factor. This was previously unenforced; it is not any more. See Authenticator settings and MFA enrolment policy for exactly what is and is not wired — requiring push, SMS or email at first sign-in is the one piece still not built. |
So when you write a rule for session length, risk or step-up behaviour, write it on the global session policy — a per-application rule will save, will display, and will never fire. MFA enrolment rules are the one exception: write those on the MFA_ENROLLMENT layer described in the linked article above, which now does something.
Conditions and outcomes
A rule's conditions are combined with AND: every condition you have set must match, and a condition you leave empty is unconstrained. Within a single condition you can include and exclude, and the exclusion is evaluated first and wins. 'Everyone in Engineering except the contractors' has to be expressible, and the exclusion must not be overridable by the inclusion. Matching ignores case throughout.
- Users and groups
- Named accounts, or group names. Your organisation role counts as a group here — a rule that names
adminmatches everyone holding the admin role, without you having to create a group of that name. Without that behaviour the most important default rule would silently never match. - Network
- Names of policy zones the request fell into. See Network zones. A country-based zone is now actually resolvable — see Recovering from a country-based lockout for what changed and the fail-safe behaviour when it still can't be resolved for a given request.
- Risk
- A list of levels, a minimum level, a minimum score, or a list of specific signals. Naming a signal lets a rule say 'step up on a new device' directly, rather than reverse-engineering a score threshold that happens to imply it.
- Device
- Platform, posture, and two three-state flags for registered and trusted — set, unset, or don't care.
- Authentication method
- How the person is signing in:
password,saml,oidc,social,passkeyorapi_key. - User type
- Employee, contractor, vendor, partner or service account.
The outcome is ALLOW, STEP_UP or DENY, and it carries a requirements payload: which factors satisfy a step-up, whether any of them or all of them are needed, whether a phishing-resistant factor is required, how long a re-authentication remains fresh, and a message. On a denial that message is the only thing the person sees, so a rule that denies without one produces an unexplainable failure. The platform will refuse to save a step-up rule that lists no factors and does not set the phishing-resistant flag, and will refuse one whose factor list contains only factors that are not phishing-resistant while claiming to require phishing resistance.
The order of evaluation
- A network blocklist is applied first, outside the rules entirely. No rule can reach around it.
- Rules are sorted by priority ascending, with default rules last within a tier and older rules before newer ones at the same priority. The engine re-sorts defensively, so a mis-ordered list cannot silently change your effective policy.
- An inactive rule is recorded as skipped, not omitted. An administrator debugging a lockout needs to see that the rule they thought should have matched was switched off.
- The first rule whose conditions all match sets the outcome and evaluation stops.
- If nothing matches, the answer is deny. The engine fails closed.
There is one exception to the last line, and it is a decision about migration rather than about security: if your organisation has no active global session policy at all, sign-in is allowed and recorded as such. Deny-by-default on upgrade would have locked out every existing customer at the moment the engine shipped. The evaluation is still written down, with the reason no active authentication policy is configured for this organization; sign-in allowed unchanged and a trace entry reading (no policy configured), so 'we are not enforcing policy for this tenant' shows up in the console as a fact rather than as an absence of data.
The decision trace
Every evaluation writes a trace, and the trace is not optional. Each entry names a rule, its priority, whether it matched, whether it was skipped, the outcome if it matched, and — the single most useful field — the first condition that rejected it, with a detail explaining the comparison.
The condition names are literal: users, groups, user_type, network, auth_method, risk, device. Details are written to be read by a person: risk level is LOW, rule requires MEDIUM or above, device platform is "ios", rule requires one of [windows, macos], none of [Finance] matched [Engineering].
The whole decision is stored, and its identifier is handed to the client as part of a step-up challenge. So a colleague can send you the identifier from the prompt they saw and you can look up exactly what happened, rather than reconstructing it from timestamps.
A worked example
A member of your sales team signs in at 03:12 UTC from a hotel in a country they have not visited before, on a laptop we have not seen. Risk scores 55: new device, new country, off hours. Here is what the trace records, using the five rules a new organisation starts with.
priority 10 Block high risk from anonymising networks
matched: false failed_condition: risk
detail: risk level is MEDIUM, rule requires HIGH or above
priority 20 Administrators require a phishing-resistant factor
matched: false skipped: true
detail: rule is INACTIVE
priority 30 Trusted device on the corporate network
matched: false failed_condition: network
detail: none of [Corporate Network] matched []
(address 198.51.100.9 resolved to zones [])
priority 40 Step up on elevated risk or a new device
matched: true action: STEP_UP
detail: rule "Step up on elevated risk or a new device"
(priority 40) matched and yields STEP_UPFour things are legible from that without knowing anything about the implementation. The anonymising-network rule did not apply because the risk was medium and it wants high. The administrator rule is switched off. The office rule did not apply because this address is in no zone. And the fourth rule is the one that prompted them. The catch-all default rule at priority 999 never ran, because evaluation stopped at the first match.
The person on the other end sees a request for their second factor, a message reading Additional verification is required for this sign-in., and the reason. They are not left guessing, and neither are you.
The five rules you start with
| Priority | Rule | Outcome |
|---|---|---|
| 10 | Block high risk from anonymising networks — high risk together with the anonymising-proxy signal | DENY, with Sign-in from anonymising networks is not permitted at this risk level. |
| 20 | Administrators require a phishing-resistant factor — the admin and Administrators groups | STEP_UP requiring a passkey. Ships inactive on purpose. |
| 30 | Trusted device on the corporate network — inside the Corporate Network zone, registered and trusted device, low risk | ALLOW with no second factor |
| 40 | Step up on elevated risk or a new device — medium risk or above | STEP_UP accepting a passkey or a one-time code |
| 999 | Default: allow — no conditions | ALLOW. The terminal catch-all; default rules always sort last so a low priority number cannot make one shadow your real rules. |
Rule 30 has the same shape of dependency: it names the Corporate Network zone, which also ships inactive and with a placeholder address range. Until you edit both, rules 20 and 30 cannot match, and your live policy is effectively rules 10, 40 and the default.
Two limits to know before you rely on a rule
Both are cases where a rule stores a setting that is displayed back to you and is not acted upon. We would rather name them than let you build around them.
- Session lifetime and re-authentication interval on the global session policy are now enforced — this is a correction. An earlier version of this article said these were inert; they are not any more. A session lifetime and a separate re-authentication interval now genuinely bound a session end to end. See Session policy and sign-out behaviour for what changed and the one setting (a bare
require_reauth: truewith no interval) that is still unenforced, and Sign-on policy settings that are stored but not enforced for the separate, simpler sign-on-policy surface's own settings. - The phishing-resistant requirement covers sign-in and not step-up. Both legs of a sign-in now enforce it — an earlier version of this article said only the first did, and that has been fixed. What still does not read it is the step-up path that authorises privileged writes, which accepts a one-time code or a printed recovery code regardless of what your policy demands at the door. So a rule requiring a passkey governs how your administrators get in, not what they may then do. Step-up authentication sets that out in full, and Multi-factor authentication factors covers the second half of the same problem: a passkey does not currently register as a second factor at all.
Changing a policy without breaking a Monday morning
Simulate first
The policy has a simulation surface — a Simulate panel right on the Session policies screen, not a separate tool — that runs the real evaluator against inputs you supply and persists nothing. It also folds the legacy
signon-policiessurface into its answer, so a simulated result reflects both engines the way a real sign-in would experience them combined. Live sign-ins do not double up this way:Loginevaluates the ordered authentication policy and the legacy sign-on-policy surface separately, each with its own precedence, exactly as the two-surfaces section above describes — the merge is a convenience for simulation, not a second pass the real sign-in path performs. Use the simulator for your own account, one ordinary member and one administrator, from an office address and an outside one.Insert rather than renumber
Priorities are plain integers and the catch-all sits at 999, far above anything you would author by hand. There is always room to insert a rule ahead of an existing one without renumbering everything.
Add the rule inactive, then activate it
An inactive rule appears in the trace as skipped, so you can confirm it is in the position you meant before it can decide anything.
Watch the evaluations for an hour
Every sign-in stores its trace. If a rule is matching people you did not expect, the failed-condition field on the rules above it tells you why they got that far.
Policy administration and the stored evaluations are gated on iam:auth_policies.read, .write and .delete. Read access to the evaluations is worth granting more widely than write access to the rules: it is the difference between somebody being able to answer a colleague's question and having to escalate it to you.