Synadia Protect
Configuration server and managed gateways
A standalone gateway manages its own bundles, traces, and audit data. This works well for single deployments, but when running multiple gateways, each one needs to be managed independently.
Running multiple gateways requires keeping bundles and configuration in sync across all instances. The configuration server solves this.
A configuration server centralizes management for a fleet of gateways. Bundles and trace profiles are installed and managed on the configuration server. Managed gateways mirror this configuration from the external NATS server — the configuration server never reaches out to managed gateways directly.
Architecture
┌─────────────────────────┐
│ External NATS Server │
│ │
│ API account (no JS) │
│ DATA account (JS) │
└─────┬───────────┬───────┘
│ │
api + data│ │data only
│ │
┌────────────┘ └──────────────┐
│ │
┌────────┴────────┐ ┌─────────┴────────┐
│ Configuration │ │ Managed Gateway │
│ Server │ │ │
│ │ │ mirrors bundles │
│ manage bundles │ │ mirrors traces │
│ fleet monitor │ │ sends heartbeat │
│ audit stream │ │ forwards audit │
└─────────────────┘ └──────────────────┘
The configuration server and managed gateways connect to the same external NATS server. The NATS server provides the transport and storage (via JetStream) for bundle and trace profile mirroring, audit events, and fleet heartbeats.
External NATS server
The external NATS server needs two accounts:
API— used by the configuration server andprotect admin/ the protect UI. for the management control plane. JetStream is not needed on this account.DATA— used by the configuration server and managed gateways for JetStream-backed data: bundle storage, audit streams, and trace profiles. JetStream must be enabled on this account.
The DATA account is owned by the configuration server. Do not manipulate its streams or KV stores directly — deleting, recreating, or purging data can adversely affect managed gateways. If running multiple configuration servers on the same NATS cluster, dedicate separate API and DATA accounts to each configuration server.
Example NATS server configuration:
listen: 127.0.0.1:4222
jetstream {
store_dir: /tmp/nats-config-server/js
max_mem: 100MB
max_file: 100MB
}
accounts {
API {
jetstream: disabled
users: [
{user: api, password: api}
]
}
DATA {
jetstream: enabled
users: [
{user: data, password: data}
]
}
}
This example uses simple username/password authentication. Production deployments should use NKeys or operator JWTs. When using operator mode, the DATA account needs explicit JetStream limits (disk/mem storage).
Configuration server
The configuration server connects to the external NATS server using credentials for both accounts. It does not have a management block — the management API runs on the external NATS server's API account instead of an embedded NATS server.
The ports section declares the port names managed gateways can expose. Only name and kind are allowed — each managed gateway supplies the network details (host, port, backend, tls, compression). The configuration server does not listen on these ports — it does not proxy any traffic.
name: config-server
ports:
- name: clients
kind: client
configuration_server:
urls:
- nats://127.0.0.1:4222
api_credentials:
user_name: api
password: api
data_credentials:
user_name: data
password: data
rules:
default_rule_direction: to_backend
unmatched_rule_to_backend_action: deny
unmatched_rule_from_backend_action: deny
monitor:
host: 0.0.0.0
port: 8080
logger:
level: info
audit:
log_file_dir: logs
client_trace_dir: traces
processors:
- name: file
type: file
format: text
config:
file: logs/audit.log
The protect admin commands and the protect UI connect to the external NATS server using the API account credentials:
$ nats context add config-admin --server 127.0.0.1:4222 --user api --password api
$ protect admin --context config-admin info
The configuration server does not run any proxies — it does not accept client or leafnode connections. Admins interact with the configuration server using protect admin commands for bundle management, trace profiles, and fleet monitoring:
protect admin bundle— manage bundles (same as standalone)protect admin trace— manage trace profiles (same as standalone)protect admin fleet status— view the status of all managed gatewaysprotect admin info— view configuration server info
Connection-level commands (stats, disconnect, suspend) are only available on managed gateways directly, since the configuration server has no proxy or client connections.
Managed gateway
A managed gateway connects to the external NATS server using only the DATA account credentials. It mirrors bundle and trace profile configuration from the DATA account's JetStream stores, forwards audit events, and publishes periodic heartbeats with its status. The configuration server never connects to the managed gateway — all synchronization happens through the shared NATS infrastructure.
A managed gateway is effectively identical to a standalone gateway — it has its own embedded management API and all admin commands (stats, disconnect, suspend, info, bundle, trace) work as expected when connecting to it directly.
name: gateway-01
ports:
- name: clients
kind: client
host: 0.0.0.0
port: 4222
backend:
urls:
- nats://backend:4222
remote_configuration_server:
urls:
- nats://127.0.0.1:4222
data_credentials:
user_name: data
password: data
heartbeat_interval: 30s
management:
api_key:
- UBZ4SGUAFFUBHNNZJ6K76SWRGC7TE7XIFMRDFHGZHTCQ2BB23FZG42L6
system_key:
- UD6MMRSS6DFTSXHVSV3TYELESX2E2VMCKV6XMKHIILTOXN7IZ2GQG3SY
host: 127.0.0.1
port: 4911
data_dir: data
monitor:
host: 0.0.0.0
port: 8081
logger:
level: info
audit:
log_file_dir: logs
client_trace_dir: traces
processors:
- name: file
type: file
format: text
config:
file: logs/audit.log
All managed gateways in a fleet must be identically configured -- same port names, kinds, and backends. The configuration server is a management front-end for identically configured gateways. Bundles are activated on ports by name, so the port definitions must match exactly across the configuration server and all managed gateways.
Eventual consistency
Managed gateways are designed for eventual consistency. If a managed gateway is partitioned from the external NATS server, it continues operating with its last known configuration — bundles, trace profiles, and rules remain active. On reconnection, it mirrors the latest state from the DATA account and resumes publishing heartbeats.
This means a managed gateway is never left without a policy — a network partition does not disable rule enforcement.
What syncs automatically
| Data | Direction | Description |
|---|---|---|
| Bundles | managed gateway mirrors config server | managed gateways watch JetStream KV on the DATA account and mirror bundle configuration |
| Trace profiles | managed gateway mirrors config server | managed gateways watch and mirror trace profile configuration |
| Audit events | managed gateway → external NATS | managed gateways forward audit events to the AUDIT stream on the DATA account |
| Heartbeats | managed gateway → external NATS | managed gateways publish periodic status (bundle state, client counts, storage info) |
Fleet status
The configuration server reads heartbeats published by managed gateways to determine if they are in sync and healthy:
$ protect admin --context config-admin fleet status
This shows all managed gateways, their health, installed bundles, and last heartbeat time.
Comparison
| Feature | Standalone | Configuration Server | Managed Gateway |
|---|---|---|---|
| Proxies traffic | yes | no | yes |
| Management API | embedded NATS (NKey auth) | external NATS (API account) | embedded NATS (NKey auth) |
| Bundle management | local | external NATS (DATA account) | mirrors from DATA account |
| Connection commands | yes (stats, disconnect, suspend) | no (no proxy) | yes (direct access only) |
| Audit logging | local processors | local + external NATS stream | local + forwarded to DATA account |
| Trace profiles | local | external NATS (DATA account) | mirrors from DATA account |
| Fleet monitoring | n/a | reads heartbeats | publishes heartbeats |
See the Configuration Reference for all fields in configuration_server, remote_configuration_server, and data_limits.
Walkthrough
This section walks through setting up a configuration server with one managed gateway.
Setup
Generate admin, system, and bundle signer NKeys using the nk CLI:
$ nk -gen=user > admin.nk
$ nk -gen=user > system.nk
$ nk -gen=user > bundle-signer.nk
Get the public NKey from the seed files:
$ nk -pubout -inkey ./admin.nk
$ nk -pubout -inkey ./system.nk
$ nk -pubout -inkey ./bundle-signer.nk
1. Start the external NATS server
Create ns.conf:
listen: 127.0.0.1:4333
jetstream {
store_dir: ns_data/js
max_mem: 100MB
max_file: 100MB
}
accounts {
API {
jetstream: disabled
users: [
{user: api, password: api}
]
}
DATA {
jetstream: enabled
users: [
{user: data, password: data}
]
}
}
$ nats-server -c ns.conf
[69343] 2026/04/08 11:02:16.864115 [INF] Starting nats-server
[69343] 2026/04/08 11:02:16.864338 [INF] Version: 2.12.6
...
[69343] 2026/04/08 11:02:16.875406 [INF] ---------------- JETSTREAM ----------------
[69343] 2026/04/08 11:02:16.875407 [INF] Max Memory: 100.00 MB
[69343] 2026/04/08 11:02:16.875434 [INF] Max Storage: 100.00 MB
[69343] 2026/04/08 11:02:16.875436 [INF] Store Directory: "ns_data/js/jetstream"
2. Start the configuration server
Create cs.yaml:
name: cs
ports:
- name: clients
kind: client
configuration_server:
urls:
- nats://127.0.0.1:4333
api_credentials:
user_name: api
password: api
data_credentials:
user_name: data
password: data
rules:
default_rule_direction: to_backend
unmatched_rule_to_backend_action: allow
unmatched_rule_from_backend_action: allow
trusted_bundle_signers:
# ⚠️ replace this
- <public key from bundle-signer.nk>
monitor:
host: 0.0.0.0
port: 8181
logger:
level: info
audit:
log_file_dir: cs/logs
client_trace_dir: cs/traces
processors:
- name: file
type: file
format: text
config:
file: cs/audit.log
$ protect start -c cs.yaml
[184] 2026/04/08 13:57:29.251244 [INF] audit log directory dir=cs/logs
[184] 2026/04/08 13:57:29.253538 [INF] connected url=nats://127.0.0.1:4333
...
[184] 2026/04/08 13:57:29.267789 [INF] fleet started
[184] 2026/04/08 13:57:29.267794 [INF] configuration server started
3. Set up admin context
$ nats context add cs_admin --server 127.0.0.1:4333 --user api --password api
$ protect admin --context cs_admin info
Gateway Information
Name: cs
Version: dev
Time: 2026-04-08 11:04:33
Uptime: 1m51s
Connections: 0
Ports:
clients:
Connection Kind: client
Name: clients
Port: 4222
Backend: nats://notrequired:4222
Rules
No rules loaded
4. Start the managed gateway
Create gw.yaml. The gateway needs its own NKey files for its embedded management API — use protect setup to generate them, or copy from an existing standalone gateway.
name: gw
ports:
- name: clients
kind: client
host: 0.0.0.0
port: 4222
backend:
urls:
- nats://demo.nats.io:4222
management:
host: 0.0.0.0
port: 4911
api_key:
# ⚠️ replace this
- <public key from admin.nk>
system_key:
# ⚠️ replace this
- <public key from system.nk>
data_dir: gw/data
remote_configuration_server:
urls:
- nats://127.0.0.1:4333
data_credentials:
user_name: data
password: data
heartbeat_interval: 5s
rules:
trusted_bundle_signers:
# ⚠️ replace this
- <public key from bundle-signer.nk>
ports:
- name: clients
unmatched_rule_to_backend_action: allow
unmatched_rule_from_backend_action: allow
monitor:
host: 0.0.0.0
port: 8080
logger:
level: info
audit:
log_file_dir: gw/logs
client_trace_dir: gw/traces
processors:
- name: file
type: file
format: text
config:
file: gw/audits/audit.log
The trusted_bundle_signers on the managed gateway must match the configuration server. Both must trust the same signing identities.
$ protect start -c gw.yaml
[1758] 2026/04/08 13:58:00.756211 [INF] audit log directory dir=gw/logs
[1758] 2026/04/08 13:58:00.761505 [INF] embedded nats started port=4911
[1758] 2026/04/08 13:58:00.770417 [INF] connected processor=config_server_forwarder type=nats url=nats://127.0.0.1:4333
...
[1758] 2026/04/08 13:58:00.777852 [INF] fleet client started gateway_id=gw
[1758] 2026/04/08 13:58:00.778442 [INF] heartbeats started
The configuration server discovers the gateway:
[184] 2026/04/08 13:58:00.778 [INF] new gateway discovered component=fleet-monitor id=gw
5. Verify fleet status
$ protect admin --context cs_admin fleet status
╭─────────────────────────────────────────────────────────────────────────────────────╮
│ Fleet Status — cs (1 gateways, 0 connections) │
├──────┬────┬─────────────────────────────────────┬─────────────┬─────────────────────┤
│ Name │ ID │ Status │ Connections │ Last Heartbeat │
├──────┼────┼─────────────────────────────────────┼─────────────┼─────────────────────┤
│ gw │ gw │ healthy │ 0 │ 2026-04-08 14:02:00 │
├──────┼────┼─────────────────────────────────────┼─────────────┼─────────────────────┤
│ │ │ 1 healthy / 0 unhealthy / 0 missing │ 0 │ │
╰──────┴────┴─────────────────────────────────────┴─────────────┴─────────────────────╯
6. Deploy a bundle
Create a bundle with built-in rules. This example allows connections at any time and allows any payload on hello.>. Create mybundle/allow-all.conf:
$ mkdir -p mybundle
# mybundle/allow-all.conf
activations:
com.synadia.protect.builtins.v1.allow.time.connect: true
com.synadia.protect.builtins.v1.allow.payload.message: true
configurations:
com.synadia.protect.builtins.v1.allow.time.connect:
- '* * * * *'
com.synadia.protect.builtins.v1.allow.payload.message:
'hello.>': '.*'
Create, sign, install, and activate:
$ protect admin bundle create --name mybundle --signer-key bundle-signer.nk mybundle 1.0.0
Successfully created bundle: mybundle-1.0.0.zip
$ protect admin --context cs_admin bundle install mybundle-1.0.0.zip
Successfully installed bundle: mybundle@1.0.0
$ protect admin --context cs_admin bundle activate clients mybundle 1.0.0
Successfully activated bundle: mybundle@1.0.0 on port "clients"
The managed gateway automatically mirrors the bundle:
$ nats context add gw_admin --server 127.0.0.1:4911 --nkey admin.nk
$ protect admin --context gw_admin info
...
Rules
mybundle@1.0.0/com_synadia_protect_builtins_v1_allow_payload_message.yaml: f405096c46
mybundle@1.0.0/com_synadia_protect_builtins_v1_allow_time_connect.yaml: 15c36f5f24
7. Test
Messages on hello.> are allowed:
$ nats pub hello.world "test message"
14:05:51 Published 12 bytes to "hello.world"
Messages on other subjects are denied:
$ nats pub orders.new "test message"
14:06:28 Published 12 bytes to "orders.new"
14:06:28 >>> Disconnected due to: EOF, will attempt reconnect
8. Centralized audit
The configuration server's audit log captures events from all managed gateways. Events forwarded from gateways include device=gw to identify the source:
$ cat cs/audit.log
...
time=... type=com.synadia.protect.v1.management.bundle id=... device=cs operation=install bundle_name=mybundle bundle_version=1.0.0 ... success=true
time=... type=com.synadia.protect.v1.management.bundle id=... device=cs operation=activate bundle_name=mybundle bundle_version=1.0.0 port=clients ... success=true
time=... type=com.synadia.protect.v1.management.bundle id=... device=gw operation=activate bundle_name=mybundle bundle_version=1.0.0 port=clients ... success=true
time=... type=com.synadia.protect.v1.connection.start id=... device=gw port=clients src=127.0.0.1 ...
time=... type=com.synadia.protect.v1.policy.action id=... device=gw port=clients action=deny reason=denied by policy policy_ref=mybundle@1.0.0/...
time=... type=com.synadia.protect.v1.connection.end id=... device=gw port=clients ...
The device field distinguishes config server events (device=cs) from gateway events (device=gw). This provides a single audit trail for the entire fleet.