New Ory Agent Security is now live! Claim your complimentary test drive. Get Started!

Skip to main content

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
FieldDescription
idRequired. 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.
conditionRequired. A CEL expression that evaluates to true or false, up to 512 characters.
actionRequired. 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​

ActionEffect
require_aal2If 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.
revokeOry 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​

FieldTypeDescription
idstringThe session ID.
expires_at, authenticated_at, issued_attimestamp
authenticator_assurance_levelstringaal0, aal1, or aal2, computed from the authentication methods.
authentication_methodslistEach entry has method, aal, and completed_at.
deviceslistEach 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​

FieldTypeDescription
ipstringThe client IP address without a port, or empty if it can't be parsed.
ip_labelslist of stringsReputation labels of the client IP address: ory:anonymizer, ory:botnet_cc, ory:malware, ory:open_proxy, and ory:vpn.
locationobjectcity, 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