Back to Spy NATS
Spy NATS Blog/ Multi-Tenant Isolation for a Banking Platform with NATS Accounts and JWT

Multi-Tenant Isolation for a Banking Platform with NATS Accounts and JWT

A banking/fintech use case: isolating each bank client's messaging with NATS Accounts, NKeys and JWT so tenants can never see or subscribe to each other's subjects, even on a shared server.

4 min read
  • nats
  • banking
  • security
  • accounts
  • jwt
  • nkeys
  • tutorial

The scenario

A banking-as-a-service platform runs one NATS deployment shared by multiple bank clients ("tenants"). Under no circumstances may Tenant A see or publish to Tenant B's subjects — even though both connect to the same physical server, for cost and operational simplicity.

NATS Accounts provide this isolation at the server level: subjects are scoped per account by default, so two accounts' clients simply cannot see each other's subjects, without any application-side filtering logic that could have a bug.

Step 1 — generate identities with NKeys

NKeys are Ed25519 keypairs used instead of (or alongside) passwords — a client proves identity by signing a server-issued challenge with its private key, so the private key itself never crosses the network:

terminal 1 — one-time setup
nsc --help >/dev/null 2>&1 || echo "install nsc: https://github.com/nats-io/nsc/releases"
nsc add operator BANKING_PLATFORM
nsc add account TENANT_A
nsc add account TENANT_B
nsc add user --account TENANT_A teller-app
nsc add user --account TENANT_B teller-app

Step 2 — generate the server config from the operator/account setup

terminal 1
nsc generate config --nats-resolver > resolver.conf
nats-server.conf (excerpt)
include resolver.conf

Step 3 — start the server with account-aware config

terminal 1
docker run -d --name nats-server \
  -p 4222:4222 -p 8222:8222 \
  -v "$(pwd)/nats-server.conf:/etc/nats/nats-server.conf:ro" \
  -v "$(pwd)/resolver.conf:/etc/nats/resolver.conf:ro" \
  nats:latest -c /etc/nats/nats-server.conf

Step 4 — prove isolation: Tenant A cannot see Tenant B's subject

terminal 2 — Tenant A's user connects using its generated JWT/credentials
nats context add tenant-a --server="nats://localhost:4222" \
  --creds="$(nsc generate creds --account TENANT_A --name teller-app)" --select
nats sub "ledger.transaction" --context tenant-a
terminal 3 — Tenant B publishes on the SAME subject string, in its own account
nats context add tenant-b --server="nats://localhost:4222" \
  --creds="$(nsc generate creds --account TENANT_B --name teller-app)" --select
nats pub "ledger.transaction" '{"amount":500}' --context tenant-b

Terminal 2 (Tenant A) never receives Tenant B's message — even though both used the identical literal subject string — because subjects are scoped per account by default; this is the core isolation guarantee, not something either tenant's application code has to implement.

Step 5 — deliberately sharing one subject across tenants (imports/exports)

Sometimes tenants genuinely need to share a slice of data (e.g. a shared fraud-alert feed from the platform operator). NATS supports this explicitly via exports/imports rather than collapsing accounts together:

terminal 1 — the platform operator exports a shared alert subject
nsc add export --account BANKING_PLATFORM_OPS --subject "alerts.fraud.>" --name fraud-alerts
terminal 1 — Tenant A explicitly imports it
nsc add import --account TENANT_A --src-account BANKING_PLATFORM_OPS --remote-subject "alerts.fraud.>"

Only subjects deliberately exported/imported cross account boundaries — everything else remains isolated by default, so a misconfiguration tends to fail closed (no access) rather than open (accidental exposure).

Troubleshooting

SymptomLikely causeFix
Client fails to connect: nats: error: Authorization ViolationCredentials file (.creds) mismatched, expired, or wrong accountRegenerate with nsc generate creds for the exact account/user, and confirm the operator config was actually loaded (resolver.conf included)
Tenant A unexpectedly receives Tenant B's messagesBoth were accidentally provisioned under the same account instead of separate accountsAudit nsc list accounts/nsc describe account to confirm each tenant has its own account, not just its own user within a shared account
An intentionally shared subject (fraud alerts) isn't reaching an importing tenantExport/import subject strings don't match exactly, or the export wasn't public/wasn't granted to that specific importernsc describe account on both sides to compare the exact exported vs. imported subject and any accepts-list
JWT-based users stop connecting after a whileJWT expiry (nsc supports --expiry) elapsedRotate/reissue credentials before expiry, monitored via the platform's own account-expiry alerts

Recap

ConceptTakeaway
NKeysEd25519 keypairs — clients prove identity by signing a challenge, private key never transmitted
AccountThe real isolation boundary — subjects don't cross accounts unless explicitly exported/imported
JWTSigned by the operator, encodes a user/account's permissions and (optionally) expiry
Export/ImportThe explicit, opt-in mechanism for deliberately sharing a subject slice across tenants

SpyNATS is an independent tool and is not affiliated with, sponsored by, or endorsed by Synadia Communications or the NATS.io project.