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.
Configure rules
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 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.
Ory Kratos refuses to start if a rule's ID is used twice, or if a condition
doesn't compile, doesn't evaluate to a boolean, or passes matches() a pattern
that compiles to more than 500 instructions. If you change the configuration
file while Ory Kratos runs, the new rules apply from the next session check, and
Ory Kratos logs and skips each rule with one of these problems.
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 logs it.
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 Kratos takes request.ip from the first of the True-Client-IP,
Cf-Connecting-IP, X-Real-IP, and X-Forwarded-For headers that's set, or
from the address of the connection if none is set. It takes request.location
from the Cf-Ipcity, Cf-Region-Code, Cf-Ipcountry, Cf-Iplatitude, and
Cf-Iplongitude headers. A device's ip_address and location come from the
same headers when the session is created.
Your reverse proxy must set or remove these headers on every request, so that callers can't set them. See the caller's location for a reverse proxy configuration that sets the location headers.
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.
ip_labels comes from headers your reverse proxy sets. See
IP labels
for the headers and for how to keep callers from setting them.
When a rule fails
A rule that fails at runtime doesn't match. 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 at
most once an hour on each instance, and the other rules still apply.
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