Check sessions against risk rules
Session risk rules let you act on a session that's used from a risky network.
Ory Kratos Identities evaluates them every time it checks a session at
/sessions/whoami or /sessions/whoami-jwt/{template}. A rule is a condition
written in the Common Expression Language (CEL) and an
action.
Session risk rules are available to Ory Network projects on Enterprise plans. A project that loses the plan keeps its rules and can remove them, but can't add or change a rule.
With Edge Sessions enabled, rules run when the edge revalidates a session with the origin, not on every session check that the edge answers from its cache.
Configure rules
Rules live at session.whoami.rules in the Ory Kratos Identities configuration
of your project:
session:
whoami:
rules:
- id: botnet
condition: |
'ory:botnet_cc' in request.ip_labels
action: revoke
- id: anonymizer-step-up
condition: |
// Anonymized traffic needs a second factor.
'ory:anonymizer' in request.ip_labels &&
session.authenticator_assurance_level == 'aal1'
action: require_aal2
| Field | Description |
|---|---|
id | Required. Up to 64 lowercase letters, digits, -, and _, starting with a lowercase letter or digit, unique in the list. Logs, events, and error messages name the rule by it. |
condition | Required. A CEL expression that evaluates to true or false, up to 512 characters. |
action | Required. require_aal2 or revoke. |
Write each condition as a YAML literal block (|), as above. YAML then takes
the condition as written: unquoted, a leading ' or a : breaks the YAML, and
# silently cuts the condition short. Comments in a condition start with //.
Line breaks count toward the 512 characters.
You can configure up to 5 rules. Without rules, a session check only checks that the session is still valid.
To change the rules, patch the configuration with the Ory CLI. This command adds a rule to the end of the list:
ory patch identity-config --project <project-id> --workspace <workspace-id> \
--add '/session/whoami/rules/-={"id":"botnet","condition":"\"ory:botnet_cc\" in request.ip_labels","action":"revoke"}'
In the JSON value, escape each " in the condition as \". This command
changes the action of the first rule:
ory patch identity-config --project <project-id> --workspace <workspace-id> \
--replace '/session/whoami/rules/0/action="require_aal2"'
This command removes all rules:
ory patch identity-config --project <project-id> --workspace <workspace-id> \
--remove '/session/whoami/rules'
When you
manage the project through the API, the
rules are the kratos_session_whoami_rules field of the normalized project
revision.
Saving the configuration fails with a 400 Bad Request error if a rule's ID is
used twice, its action is unknown, or its condition doesn't compile, doesn't
evaluate to a boolean, or passes matches() a pattern that compiles to more
than 500 instructions. The error names each rule that fails, for example
rules[1] (anonymizer-step-up): …. Saving also fails if the rules exceed the
limits above.
Cost limit
Each rule may cost at most 100,000 units on one session check. A unit is roughly
one operation, such as a comparison or one iteration of exists(). A rule that
exceeds the limit stops. It counts as not matched, and Ory Kratos reports it
like any other rule that fails.
matches() is the most expensive function. Its cost grows with the length of
the string and the length of the pattern, and a pattern applied to every entry
of a list costs the most. A pattern that compiles to more than 500 instructions,
such as [a-z]{1,500}, is rejected. To lower the cost, keep patterns short,
avoid large repetition counts, and match fewer strings.
Actions
| Action | Effect |
|---|---|
require_aal2 | If the session is below AAL2, it must reach AAL2 for the rest of its lifetime. The session check returns 403 with session_aal2_required, or session_aal2_enrollment_required if the identity has no second factor. Sessions that are already AAL2 are not affected. |
revoke | Ory Kratos revokes the session. The session check returns 401, the same response as for any session that's no longer active. |
Ory Kratos evaluates every rule, in order. If several rules match, the strongest
action applies: revoke beats require_aal2. Ory Kratos logs every rule that
matches a session, including rules whose action doesn't apply. Each Ory Kratos
instance logs a rule's matches on a session at most once an hour. The responses
are the same as for any other session that must reach AAL2 or is no longer
active.
Rule input
Rules read two variables. They contain no identity data. Use login and registration hooks for rules about the identity.
session
| Field | Type | Description |
|---|---|---|
id | string | The session ID. |
expires_at, authenticated_at, issued_at | timestamp | |
authenticator_assurance_level | string | aal0, aal1, or aal2, computed from the authentication methods. |
authentication_methods | list | Each entry has method, aal, and completed_at. |
devices | list | Each entry has id, ip_address, user_agent, and location. A device is recorded when the session is created; refreshing a session or completing a second factor adds none. |
request
| Field | Type | Description |
|---|---|---|
ip | string | The client IP address without a port, or empty if it can't be parsed. |
ip_labels | list of strings | Reputation labels of the client IP address: ory:anonymizer, ory:botnet_cc, ory:malware, ory:open_proxy, and ory:vpn. |
location | object | city, region, country, and the optional latitude and longitude. Use has(request.location.latitude) to check for them. |
Missing strings and lists are empty. devices lists the newest device first.
Ory Network determines the client IP address, its location, and its IP labels at
the edge, and records a device's ip_address and location the same way when
the session is created. You don't have to configure anything.
When your backend calls /sessions/whoami for a user, request.ip is your
backend's address, so exempt your backend's addresses in conditions that use it.
When a rule fails
A rule that fails on a session check counts as not matched, and the other rules
still apply. For example, {'DE': true, 'FR': true}[request.location.country]
fails with no such key: US for a request from the United States; use
request.location.country in ['DE', 'FR'] instead. Ory Kratos logs the error
and emits a SessionRiskRuleFailed event that names the rule in
SessionRiskRuleID and describes the error in SessionRiskRuleError. Each Ory
Kratos instance reports a failing rule at most once an hour.
A rule that can't run at all, for example because its condition is no longer valid after an upgrade, is skipped on every session check.
A skipped rule counts as not matched. Ory Kratos reports it the same way as a rule that fails, with the session that first hit it.
SessionRiskRuleError can contain values from the request, such as IP
addresses. Like ExternalID, it's delivered to your
event stream only, and Ory doesn't keep it in its
own analytics.
Examples
rules:
# Require a second factor for traffic from VPNs and anonymizers.
- id: anonymized-step-up
condition: |
request.ip_labels.exists(l, l in ['ory:vpn', 'ory:anonymizer'])
action: require_aal2
# Require a second factor for sessions used from a country other than the one
# they were created in.
- id: country-change
condition: |
request.location.country != '' &&
!session.devices.exists(d,
d.location == request.location.country ||
d.location.endsWith(', ' + request.location.country))
action: require_aal2