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.
- 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:
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-appStep 2 — generate the server config from the operator/account setup
nsc generate config --nats-resolver > resolver.confinclude resolver.confStep 3 — start the server with account-aware config
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.confStep 4 — prove isolation: Tenant A cannot see Tenant B's subject
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-anats 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-bTerminal 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:
nsc add export --account BANKING_PLATFORM_OPS --subject "alerts.fraud.>" --name fraud-alertsnsc 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
| Symptom | Likely cause | Fix |
|---|---|---|
Client fails to connect: nats: error: Authorization Violation | Credentials file (.creds) mismatched, expired, or wrong account | Regenerate 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 messages | Both were accidentally provisioned under the same account instead of separate accounts | Audit 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 tenant | Export/import subject strings don't match exactly, or the export wasn't public/wasn't granted to that specific importer | nsc describe account on both sides to compare the exact exported vs. imported subject and any accepts-list |
| JWT-based users stop connecting after a while | JWT expiry (nsc supports --expiry) elapsed | Rotate/reissue credentials before expiry, monitored via the platform's own account-expiry alerts |
Recap
| Concept | Takeaway |
|---|---|
| NKeys | Ed25519 keypairs — clients prove identity by signing a challenge, private key never transmitted |
| Account | The real isolation boundary — subjects don't cross accounts unless explicitly exported/imported |
| JWT | Signed by the operator, encodes a user/account's permissions and (optionally) expiry |
| Export/Import | The explicit, opt-in mechanism for deliberately sharing a subject slice across tenants |