Gates
Rules & conditions
A gate is a stack of rules. Each rule checks one or more conditions and suggests an action: allow, block or challenge. The evaluation model below explains how the gate turns matching rules into one decision.
Anatomy of a rule
Each rule has these fields:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Short, descriptive label shown in the UI and logs |
description |
string | no | Why this rule exists or what it targets |
message |
string | no | Returned in the decision when this rule sets the verdict |
match |
enum: all, any |
no | all = every condition must be true, any = at least one must be. Defaults to all when you create a rule. |
action |
enum: allow, block, challenge |
yes | The outcome when the rule matches |
stop |
boolean | no | If true, stop evaluating further rules when this one matches. Defaults to false. Shown as Stop processing in the dashboard. |
enabled |
boolean | no | If false, the rule is ignored. Defaults to true. |
conditions |
array<Condition> | yes | One or more field comparisons |
Evaluation model
- Rules are processed top to bottom. Disabled rules are skipped.
- For each rule, its conditions are evaluated using the rule's
matchmode (allorany). - If the rule matches, its
actionbecomes the current decision. A later matching rule replaces it. - If a matching rule has
stop: true, evaluation ends and its action is final. - If no rule matched, the gate's
default_actionis returned andruleisnullin the response.
So when every rule has stop: true, the first matching rule decides. When no rule has it, the last matching rule decides.
Rule order
A gate created in the dashboard starts with six starter rules, and all of them have Stop processing on (the rule list shows Exit on match). So in a new dashboard gate, the first matching rule decides. A gate created through the Management API starts with no rules.
New rules, from the dashboard or the API, are added at the bottom, with Stop off unless you turn it on.
To make an exception win, such as always allowing a partner's domain:
- Turn Stop on for it and place it above the rules it overrides. Use this on any gate created in the dashboard.
- Or, if none of the rules use Stop, place it below them. The last match wins.
An exception added at the bottom of a gate whose rules use Stop never runs for the cases it was meant to cover: a rule above it has already matched and ended evaluation. Drag rules to reorder them in the dashboard, or use the reorder endpoint.
Conditions
A condition is an object with three keys: the field to read, the op to apply, and the value to compare against.
{ "field": "domain.tld", "op": "in", "value": ["xyz", "top"] }
Condition operators
Which operators a field accepts depends on its type, shown in the field tables below.
| Operator | Matches when the field | Field types |
|---|---|---|
eq |
equals the value | boolean, number, string |
ne |
does not equal the value | boolean, number, string |
lt, lte, gt, gte |
is less than, at most, greater than, or at least the value | number |
contains |
contains the value | string |
starts_with |
starts with the value | string |
ends_with |
ends with the value | string |
in |
equals one of the values in the list | string |
not_in |
equals none of the values in the list | string |
regex |
matches the pattern | string |
not_regex |
does not match the pattern | string |
is_null |
has no value | string |
not_null |
has a value | string |
The value follows the field and operator:
- Boolean fields take
trueorfalse. Number fields take an integer. inandnot_intake a list of strings, as in the example above. Number fields have noin: to match several ASNs, write amatch: anyrule with oneeqcondition per ASN.is_nullandnot_nulltake novalue.- A string value, each item of an
inlist, and a pattern can each be up to 255 characters. - String comparisons are case-insensitive, for every operator.
regex and not_regex take a bare pattern, such as ^admin$. Do not wrap it in slashes or add flags: /^admin$/ is read as a pattern that includes the slashes. Inline options such as (?-i) work. Length checks such as ^.{21,}$ count bytes, not characters, so accented or non-Latin text reaches the limit sooner.
A pattern is checked for errors when you save the rule. If a valid pattern still fails on a particular value during a decision, for example because it backtracks too much on a long input, the condition does not match, whether it uses regex or not_regex. Keep patterns short and anchored (^...$), and avoid nested repetition such as ([a-z]+)+.
Common recipes
Each recipe is a rule body you can send to Add a rule, or build the same way in the dashboard. They all use stop: true, like the starter rules, so the first matching rule decides and exceptions go at the top.
Block disposable addresses
{
"name": "Block disposable",
"match": "any",
"action": "block",
"stop": true,
"conditions": [
{ "field": "email.disposable", "op": "eq", "value": true },
{ "field": "domain.disposable", "op": "eq", "value": true }
]
}
email.disposable also catches known throwaway addresses on ordinary domains. domain.disposable covers requests that send only a domain. Gates created in the dashboard start with this rule.
Challenge relay and forwarding addresses
{
"name": "Challenge relays",
"match": "all",
"action": "challenge",
"stop": true,
"conditions": [
{ "field": "domain.relay_domain", "op": "eq", "value": true }
]
}
Relay addresses such as Apple's Hide My Email forward to a real inbox, so a challenge usually fits better than a block. See Relay Domains.
Always allow partner domains
{
"name": "Allow partners",
"match": "any",
"action": "allow",
"stop": true,
"conditions": [
{ "field": "domain.name", "op": "in", "value": ["partner.com", "partner.co.uk"] },
{ "field": "domain.name", "op": "ends_with", "value": ".partner.com" }
]
}
Move this rule to the top of the gate, above the rules it should override. The ends_with condition covers subdomains such as mail.partner.com. The leading dot keeps it from matching otherpartner.com, and is why partner.com itself is in the list.
Block datacenter IPs without blocking iCloud Private Relay
{
"name": "Block datacenter IPs",
"match": "all",
"action": "block",
"stop": true,
"conditions": [
{ "field": "ip.hosting.detected", "op": "eq", "value": true },
{ "field": "ip.anonymity.relay.detected", "op": "eq", "value": false }
]
}
iCloud Private Relay sends Safari users' traffic through hosting IPs, so ip.hosting.detected alone would also block them. The second condition lets them through. The rule only applies to requests that send an ip.
Supported fields
A condition can read any of the fields below.
Which groups are populated depends on what the decision request carried. The domain.* fields are always present. The email.* fields are only present when the request sent an email, and the ip.* fields only when it sent an ip. An absent field reads as null, which satisfies is_null, ne, not_in and not_regex. To make a rule apply only when the input was sent, add an email.address not_null or ip.address not_null condition to it. Fields written with a * hold a list rather than a single value and follow the separate rules in Multi-value fields.
Email fields
| Field | Type | Details |
|---|---|---|
email.address |
string | The full email address. |
email.normalized |
string | The canonical form of the address. See Email Normalization. |
email.local_part |
string | Local part of the email address. For example, user in [email protected]. |
email.subaddress |
string | Subaddress of the email address. For example, newsletter in [email protected]. |
email.disposable |
boolean | The address is disposable, either because its domain is a disposable service or because the specific address is a known throwaway on an ordinary domain (e.g. Gmail or Outlook). |
email.role_account |
boolean | Local part is a role account (admin@, support@). See Role Accounts. |
Domain fields
| Field | Type | Details |
|---|---|---|
domain.name |
string | The full domain name. Use sparingly, for one-off allow or block entries. |
domain.tld |
string | The public suffix, which can have several parts: com in example.com, co.uk in acme.co.uk. |
domain.sld |
string | The label just before the suffix: acme in mail.acme.co.uk. |
domain.subdomain |
string | Everything before the SLD, or null: mail in mail.acme.co.uk. |
domain.authority |
number | A 0–100 score indicating how established and credible a domain is; null when no score is available. Higher scores indicate well-established domains. Lower scores may signal newly created or less reputable domains. See Domain Authority. |
domain.tld_trust |
number | A 1–5 score indicating how strict the TLD's registration requirements are. Higher scores mean the registry performed more identity verification. null for a suffix we have not scored. See TLD Trust. |
domain.age_days |
number | Days since the domain was registered. null when the registration date is unknown. |
domain.mx |
boolean | Domain has valid MX records. null when the DNS lookup got no answer. |
domain.mx_records.*.hostname |
string | Hostnames of MX records. Useful for filtering based on MX record hostnames. |
domain.mx_providers.*.slug |
string | Slug identifiers of detected MX providers. Useful for filtering by specific provider (e.g., google, mimecast). See MX Providers. |
domain.mx_providers.*.type |
string | Category of MX provider. Values: mailbox, hosting, email_api, security_gateway, forwarding. |
domain.mx_providers.*.grade |
string | Provider grade. Values: enterprise, professional, standard, basic. |
domain.spf |
string | The domain's SPF posture. Values: strict, relaxed, neutral, permissive, missing, invalid, or null when not yet known. See SPF & DMARC. |
domain.dmarc |
string | The domain's DMARC policy. Values: reject, quarantine, none, missing, invalid, or null when not yet known. See SPF & DMARC. |
domain.disposable |
boolean | Domain is a disposable email provider. See Disposable Domains. |
domain.disposable_provider |
string | The service operating the domain. Only present when domain.disposable is true, and null even then when the operator is unidentified. |
domain.public_domain |
boolean | Domain is a public mailbox provider (gmail.com, outlook.com, yahoo.com). See Public Domains. |
domain.relay_domain |
boolean | Domain's mail is handled by a forwarding or relay service. See Relay Domains. |
domain.free_subdomain |
boolean | Domain is a subdomain of a free subdomain or dynamic DNS service like duckdns.org. The parent domain itself is not flagged. See Free Subdomains. |
domain.spam |
boolean | Domain flagged for spam or other abusive activity. See Spam Domains. |
domain.blocklisted |
boolean | Domain is present in your account blocklist. |
Unknown values
A signal we have no data for is null, not zero or false. This affects domain.authority, domain.age_days and domain.tld_trust when unknown, domain.spf and domain.dmarc before we have checked the domain (which can happen the first time we see it), and domain.mx when the DNS lookup got no answer.
A null never satisfies eq, lt, lte, gt, gte, in, contains, starts_with or ends_with. It does satisfy ne and not_in. regex and not_regex read it as empty text, so not_regex matches it unless the pattern also matches empty text. So:
- An authority-below-10 rule skips domains with no score, because
domain.authoritylt10never matchesnull. The same applies to an unknowndomain.age_days. domain.mxeqfalselets a domain through when its DNS lookup got no answer, whiledomain.mxnetruecatches it.
is_null and not_null are available only for string fields. Number and boolean fields do not accept them.
Multi-value fields
Four fields hold a list rather than a single value: domain.mx_records.*.hostname, domain.mx_providers.*.slug, domain.mx_providers.*.type and domain.mx_providers.*.grade. Most domains publish several MX records, and a domain can resolve to more than one provider: Google Workspace behind a Mimecast gateway is both.
A condition on one of these reads the whole list:
- Positive operators (
eq,in,contains,starts_with,ends_with,regex) match when any entry satisfies them. - Negative operators (
ne,not_in,not_regex) match when every entry satisfies them, that is, when no entry matches the value.
For a domain resolving to both google and mimecast:
| Condition | Result |
|---|---|
domain.mx_providers.*.slug eq google |
matches |
domain.mx_providers.*.slug ne google |
does not match |
domain.mx_providers.*.slug not_in ["google"] |
does not match |
An empty list matches nothing. When we recognise no provider for a domain, domain.mx_providers.*.slug is an empty list and every comparison against it is false, ne and not_in included. Test for it with is_null: on a multi-value field, is_null means "no entry has a value" and not_null means "at least one entry has a value".
The distinction matters when the list is present but the values are not. domain.mx_providers.*.slug and domain.mx_providers.*.type always carry a value, so for those fields "no entry has a value" and "the list is empty" are the same thing. domain.mx_providers.*.grade does not: a provider we recognise but have not graded contributes an entry whose value is null. A grade is_null condition therefore matches both "we recognise no provider" and "we recognise one but have no grade for it". If you mean only the first, test slug or type rather than grade.
That makes "only allow Google Workspace" two conditions rather than one:
{
"match": "any",
"action": "block",
"conditions": [
{ "field": "domain.mx_providers.*.slug", "op": "ne", "value": "google" },
{ "field": "domain.mx_providers.*.slug", "op": "is_null" }
]
}
Without the second condition the rule allows every domain whose mail provider we cannot identify.
Conditions are not tied to the same entry. Each is evaluated against the whole list on its own. In a match: all rule, grade eq basic alongside slug ne yahoo asks "some provider is basic" and "no provider is Yahoo", not "some provider is basic and is not Yahoo". A domain served by both Yahoo and another basic-grade provider satisfies the first condition and fails the second, so the rule does not fire.
IP fields
| Field | Type | Details |
|---|---|---|
ip.address |
string | The IP address. |
ip.abuse.detected |
boolean | IP address has been flagged for abuse. |
ip.anonymity.proxy.detected |
boolean | IP address is detected as a proxy. |
ip.anonymity.relay.detected |
boolean | The IP is an exit point of a privacy relay. Currently this means iCloud Private Relay. |
ip.anonymity.relay.provider |
string | The relay's name, currently always iCloud Private Relay. null when the IP is not a relay. |
ip.anonymity.tor.detected |
boolean | IP address is detected as a Tor exit node. |
ip.anonymity.vpn.detected |
boolean | IP address is detected as a VPN. |
ip.geo.city |
string | The city where the IP is located. |
ip.geo.continent |
string | The continent name, for example North America. |
ip.geo.continent_code |
string | The two-letter continent code, for example NA or EU. |
ip.geo.country |
string | The country name, for example United States. |
ip.geo.country_code |
string | The two-letter ISO country code, for example US. |
ip.hosting.detected |
boolean | The IP belongs to a hosting or datacenter range. iCloud Private Relay IPs are hosting IPs too, so a rule that blocks on this field also blocks Safari users with iCloud+. Add ip.anonymity.relay.detected eq false to the same match: all rule to exclude them, as in this recipe. |
ip.network.asn |
number | The Autonomous System Number (ASN) of the IP's network. |
ip.network.aso |
string | The Autonomous System Organization (ASO) name. |
ip.network.domain |
string | The domain associated with the IP's network. |
ip.network.type |
string | The network type reported for the IP's ASN, for example hosting. |