Skip to main content

Config server

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

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}
]
}
}
note

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
warning

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
warning

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.