Synadia Protect
Rules
Rules define the security policies the gateway evaluates against NATS protocol traffic. Every connection and message flowing through the gateway is matched against rules that decide whether to allow, deny, or suspend it.
Rule types
Rules target one of two protocol stages:
| Type | Evaluates | When |
|---|---|---|
connect | the CONNECT protocol message | once, when the client or leafnode connects |
message | PUB, HPUB, MSG, HMSG, LMSG, LHMSG | on every msg protocol |
SUB, UNSUB (and leaf equivalents LS+, LS-) are not evaluated by rules — they are traced and counted but not subject to policy.
Actions
When a rule matches, it produces an action:
| Action | Terminal | Description |
|---|---|---|
allow | no | permit the operation |
deny | yes | reject the operation and close the connection |
suspend | no | park the connection — keep alive but drop the backend connection |
error | yes | a deny initiated by an error processing a rule (logged and recorded in metrics) |
log | no | log the raw protocol to the auditor, continue evaluation |
Terminal actions (deny, error) stop all further rule evaluation immediately. Non-terminal actions accumulate and evaluation continues.
Built-in rules
The gateway ships with built-in rules for common use cases. Using built-in rules requires no expression writing — just activate the rule and provide configuration values.
List available built-in rules:
$ protect admin bundle builtins list --local
Built-in rules follow a naming convention:
com.synadia.protect.builtins.v1.<action>.<category>.<type>
Get details and example configuration for any built-in rule:
$ protect admin bundle builtins info --local <rule-id>
Configuring built-in rules
Create a .conf file with two sections — activations to enable the rules and configurations to provide their settings:
activations:
com.synadia.protect.builtins.v1.deny.payload.message: true
configurations:
com.synadia.protect.builtins.v1.deny.payload.message:
'logs.>': '(?i)password|secret|api_key'
A single .conf file can activate multiple built-in rules:
activations:
com.synadia.protect.builtins.v1.allow.header.message: true
com.synadia.protect.builtins.v1.deny.time.message: true
configurations:
com.synadia.protect.builtins.v1.allow.header.message:
X-Tenant: '^acme$'
com.synadia.protect.builtins.v1.deny.time.message:
- schedule: '* 0-9 * * *'
subject: 'orders.>'
- schedule: '* 17-23 * * *'
subject: 'orders.>'
See the built-in rules reference for all available rules and their configuration schemas.
Available categories
| Category | Type | What it inspects |
|---|---|---|
cidr_client | connect | client IP address |
cidr_server | connect | backend server IP |
consumer_filter | message | JetStream consumer filter subjects |
external_jwt_signer | connect | JWT signature on connection |
header | message | message header existence/patterns |
payload | message | message payload content (regex per subject) |
time | connect, message | cron schedule (UTC) |
protolen | message | protocol message length |
subject_wildcard | message | wildcard subjects |
Custom rules
For cases the built-ins don't cover, write custom rules as .yaml files with expressions in the Expr language. Expr is a simple, safe expression language — not a general-purpose programming language. Custom rules have full access to the connection context, message contents, headers, and metadata.
The Rules Reference has the complete field tables for all evaluation objects, conditions, and custom functions.
Rule file structure
name: <string> # unique within the bundle
description: <string> # optional
facts: # connection-level filters (static for the connection lifetime)
- connection_kind: client # required: client or leaf
conditions: # per-message filters
- rule_type: connect # required: connect or message
default: allow # action when no rule body matches
rules: # one or more rule bodies
- expression: <expr>
success: deny # action when expression returns true
fail: allow # action when expression returns false
message: 'audit log text' # shown in audit logs on deny/suspend
Facts
Facts are static properties of a connection — they never change after connect. The engine uses facts to pre-filter the ruleset so only relevant rules are evaluated on a specific connection.
| Fact | Values | Description |
|---|---|---|
connection_kind | client, leaf | required on every rule |
remote_ip | IP address | exact match on source IP |
Facts use OR logic within the same key and AND across different keys:
facts:
- connection_kind: client
- connection_kind: leaf # matches client OR leaf
- remote_ip: 10.0.0.1
- remote_ip: 10.0.0.2 # matches either IP
The rule applies if the connection is a client or a leafnode and the source IP is either 10.0.0.1 or 10.0.0.2. If either value differs, the rule is not evaluated.
Conditions
Conditions filter per protocol message. They determine whether a rule applies to a given message.
Required:
| Key | Values | Description |
|---|---|---|
rule_type | connect, message | which protocol messages this rule handles |
Direction (message rules):
| Key | Values | Description |
|---|---|---|
direction | to_backend, from_backend, both, inherit | traffic direction filter. inherit (or omitting) uses the port's default_rule_direction |
Connect conditions (available for both connect and message rule types):
| Key | Matches against |
|---|---|
username | Connect.Username |
password | Connect.Password |
token | Connect.Token |
nkey | Connect.Nkey |
jwt | Connect.JWT |
name | Connect.Name |
lang | Connect.Lang |
version | Connect.Version |
Message conditions (available for message rule type):
| Key | Matches against |
|---|---|
subject | exact match on Message.Subject |
subject_match | NATS wildcard match on Message.Subject |
subject_not_match | negative NATS wildcard match |
reply_to | exact match on Message.ReplyTo |
has_header | true if header key exists |
not_header | true if header key does not exist |
account | AccountInfo.Account |
is_system_account | AccountInfo.IsSystemAccount |
Conditions use OR within the same key, AND across different keys.
Rule bodies
Each rule body has an expression that evaluates to a boolean:
rules:
- expression: Connect.Username == "system"
success: deny
fail: allow
message: 'system user not allowed'
Evaluation stops on the first terminal action (deny or error). If no rule body produces a terminal action, the default applies.
Evaluation environment
Rule expressions have access to these objects:
| Object | Description | Available in |
|---|---|---|
Meta | connection metadata — direction, address, remote server, time | connect, message |
Connect | NATS CONNECT protocol fields — username, token, JWT, etc. | connect, message |
Message | NATS message — subject, payload, headers, reply-to | message |
AccountInfo | account name and system account flag | connect, message |
Fields are accessed using PascalCase: Connect.Username, Message.Subject, Meta.Address.
Custom functions
The gateway extends Expr with NATS-specific functions:
| Function | Description |
|---|---|
subjectMatch(subject, pattern) | NATS wildcard match (* and >) |
subjectHasWildcards(subject) | true if subject contains wildcards |
isLiteralSubject(subject) | true if subject has no wildcards |
matchCIDR(address, cidr) | IPv4/IPv6 CIDR match |
matchesTime(schedule, timestamp) | cron schedule match (UTC) |
hasHeader(config, headers) | check headers against config map |
payloadMatches(config, subject, payload) | match payload against regex by subject pattern |
regexMatch(str, pattern) | Go regex match |
bytesToString(data) | convert payload bytes to string |
normalizeJSSubject(subject) | strip domain/prefix from JetStream subjects |
fromJsRequest() | parse JetStream request from message payload |
hasAllowedConsumerFilter(allowed) | check consumer filters against allowed list |
hasDeniedConsumerFilter(denied) | check consumer filters against denied list |
IsTrustedExternalSigner(token, config) | JWT signature verification |
Allow all clients, deny system user
name: client_connect
description: client connection restrictions
facts:
- connection_kind: client
conditions:
- rule_type: connect
default: allow
rules:
- success: deny
message: system user not allowed
expression: |
Connect.Username == "system"
Block large messages on leafnodes
name: message_sizes
description: check message sizes
facts:
- connection_kind: leaf
conditions:
- rule_type: message
default: allow
rules:
- success: deny
message: message too big
expression: |
len(Message.Payload) > 256 * 1024
Protect JetStream streams from deletion
name: protect_streams
description: prevent stream deletion and purge
facts:
- connection_kind: leaf
conditions:
- rule_type: message
- account: production
- subject_match: '$JS.>'
default: allow
rules:
- success: deny
message: stream removal not allowed
expression: |
subjectMatch(Message.Subject, "$JS.API.STREAM.DELETE.>")
- success: deny
message: stream purge not allowed
expression: |
subjectMatch(Message.Subject, "$JS.API.STREAM.PURGE.>")
Office hours with CIDR restriction
name: office_hours_only
description: restrict access to business hours from office IPs
facts:
- connection_kind: client
conditions:
- rule_type: connect
default: deny
rules:
- success: allow
message: office hours from trusted network
expression: |
matchCIDR(Meta.Address, "10.0.0.0/8") && matchesTime("* 9-17 * * 1-5", Meta.Time)