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.

Configure rules​

kratos.yml
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 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.

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​

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 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