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 and protect 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 gateways
  • protect 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

DataDirectionDescription
Bundlesmanaged gateway mirrors config servermanaged gateways watch JetStream KV on the DATA account and mirror bundle configuration
Trace profilesmanaged gateway mirrors config servermanaged gateways watch and mirror trace profile configuration
Audit eventsmanaged gateway → external NATSmanaged gateways forward audit events to the AUDIT stream on the DATA account
Heartbeatsmanaged gateway → external NATSmanaged 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

FeatureStandaloneConfiguration ServerManaged Gateway
Proxies trafficyesnoyes
Management APIembedded NATS (NKey auth)external NATS (API account)embedded NATS (NKey auth)
Bundle managementlocalexternal NATS (DATA account)mirrors from DATA account
Connection commandsyes (stats, disconnect, suspend)no (no proxy)yes (direct access only)
Audit logginglocal processorslocal + external NATS streamlocal + forwarded to DATA account
Trace profileslocalexternal NATS (DATA account)mirrors from DATA account
Fleet monitoringn/areads heartbeatspublishes 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.

Previous
Audit
Next
UI