NEW: The Edge Autonomy Gap report. AI is arriving at the edge — 500 practitioners say the infrastructure isn't ready.
All posts

A community member asked how to authenticate an unknown number of distributed NATS clients when each client can generate its own NKey pair and share only its public identity through a separate secure channel.

The short answer: use NATS decentralized JWT authentication for this pattern. The client can keep its private NKey local, your trusted onboarding or identity service can issue a signed user JWT containing the client public NKey and permissions, and the NATS server can validate both the trusted signer and the client’s proof that it holds the matching private key.

Why JWT authentication fits this use case

If you have an unknown or changing number of clients, adding every user directly to nats-server configuration does not scale well. Each add, remove, or permission change becomes a server configuration management problem.

With NATS JWT authentication, the server is configured to trust an operator rather than a static list of every end user. Accounts are signed by that operator, and users are signed by an account. A client presents its user JWT when it connects, and the server validates that the JWT chains back to the trusted operator before applying the claims in that JWT, including authorization permissions.

This pattern requires running nats-server in operator mode (decentralized JWT authentication) with a configured account resolver, rather than the static authorization block used for in-config users.

That means onboarding a new user can be handled by your own trusted service without editing server config for each new client.

A common architecture looks like this:

  1. The client generates an NKey pair locally.
  2. The client sends its public NKey, plus whatever application identity proof you require, to a trusted onboarding service over a separate secure channel.
  3. The onboarding service validates the client using your external identity process.
  4. The onboarding service issues a NATS user JWT. The JWT’s subject is the client’s public NKey, it carries the appropriate publish/subscribe permissions, and it is signed with an account NKey — the account identity key, or preferably a dedicated account signing key — for an account in the server’s trust chain.
  5. The client connects to NATS using credentials that include the user JWT and access to the private NKey seed.
  6. During connection, NATS sends a nonce challenge.
  7. The client signs the nonce with its private NKey.
  8. The server verifies that signature against the public NKey in the user JWT.
  9. If the JWT is trusted and the nonce signature is valid, the client is authenticated and receives the permissions encoded in the JWT.

In simplified form:

1
Client generates NKey pair
2
|
3
| public NKey + external identity proof
4
v
5
Trusted onboarding service
6
|
7
| signed user JWT
8
v
9
Client connects to NATS
10
|
11
| JWT + nonce signature
12
v
13
NATS validates the JWT trust chain and nonce signature, then enforces permissions

What does the client sign?

The client does not sign the JWT during every connection. The trusted issuer signs the user JWT.

When the client connects, the NATS server provides a nonce challenge. The client signs that nonce with the private NKey corresponding to the public NKey in the user JWT. The server then verifies the signature against the public key from the JWT.

This matters because it prevents the user JWT from acting as a simple bearer token. Possession of the JWT alone should not be enough; the connecting client also needs the private key that matches the public NKey in the JWT.

What does the server validate?

At connection time, the server validates two different things:

  • Issuer trust: Was the user JWT signed by an account key (the account’s identity key or one of its signing keys) for an account that chains back to the operator the server is configured to trust?
  • Client key possession: Can the connecting client sign the server nonce with the private key matching the public NKey in the JWT?

After those checks pass, the server uses the authorization claims in the JWT to determine what the client can publish, subscribe to, or otherwise access.

Where do account resolvers fit?

In decentralized JWT authentication, account resolvers are how servers obtain and cache account JWTs. They do not hold user JWTs: a user JWT is presented by the client when it connects and is not preloaded or stored on the server side.

The client presents its user JWT during connection. The server uses its configured trust chain and account information to validate that user JWT. This is what allows you to add clients without a server configuration update for each user.

Should the private key ever leave the client?

It does not have to.

A traditional administrative flow may generate a full credentials file and deliver it to a user through a secure channel. That credentials file includes both the user JWT and the private seed needed to sign nonce challenges.

For a client-generated-key flow, the client can generate the private key locally and send only the public NKey to your onboarding service. The onboarding service builds a user JWT with that public key as its subject, signs it with an account signing key, and returns the JWT. The client then combines that JWT with its local private key material when connecting.

The important invariant is: the client must have access to the private key at connection time, and the server must be able to verify the nonce signature against the public key in the JWT.

Should you use nsc for this?

nsc is useful for administrative workflows, especially when an operator or account administrator is creating users and distributing credentials.

For a dynamic onboarding service or auth callout-style integration, avoid shelling out to nsc as the core implementation mechanism. Use the NATS JWT or JWT builder libraries appropriate for your language/runtime so your service can issue user JWTs directly.

The standard nsc user-creation workflow generates the user key pair as part of creating the user. That is convenient for administrator-driven credential distribution, but it does not match a model where the private key must never leave the client. If keeping the private key exclusively on the client is a hard requirement, build user JWTs programmatically with the NATS JWT libraries, passing the client-supplied public key as the JWT subject and signing with an account signing key.

Where does auth callout fit?

NATS auth callout can be useful when authentication and authorization need to be delegated to an external service. It works differently from the pre-issued flow above: instead of the client arriving with a user JWT it obtained earlier, the server invokes your callout service on each connection attempt, and your service returns the user JWT the server should apply for that connection. In this model, your service verifies the client using whatever identity proof your environment requires, then assigns the NATS identity and permissions that should apply.

For this use case, think of auth callout as an integration point for your onboarding or identity system, not as a reason to store every client in static server configuration. The callout service still needs to make careful decisions about identity proof, permissions, key handling, and token lifetime.

If you are exploring this path, the Synadia example repository synadia-io/callout.go is a useful reference point.

Authorization: put the policy in the user JWT

Authentication proves who the client is. Authorization determines what the client can do.

For distributed subscribers, your issuer should generate JWT claims that grant only the subjects the client should access. For example, rather than granting broad subscription rights to all subjects, issue permissions scoped to the specific subject hierarchy that client is allowed to consume.

The exact permission model depends on your application, but the pattern is the same:

  • validate the external identity,
  • map that identity to NATS permissions,
  • sign a user JWT containing those permissions,
  • let NATS enforce the claims at connection time.

Operational considerations

A dynamic JWT onboarding flow removes the need to edit server config for every new user, but it does not remove the need for lifecycle management. Plan for:

  • Expiration: Decide how long issued user JWTs should be valid.
  • Revocation: Decide how to disable a client that should no longer connect. In JWT auth, user revocation is handled at the account level — the account JWT carries a revocation list, and the updated account JWT must be pushed to the account resolver.
  • Permission updates: Decide whether clients receive new JWTs when their access changes.
  • Key rotation: Decide how clients and issuers rotate keys safely.
  • Issuer protection: Treat signing keys as highly sensitive operational secrets.
  • Auditability: Record why a given public NKey received a given set of permissions.

Short-lived JWTs can simplify some operational problems, but they may require clients to refresh credentials more often. Longer-lived JWTs reduce refresh frequency, but make revocation and permission changes more important to design carefully.

Practical guidance

For an unknown number of distributed NATS clients, prefer decentralized JWT authentication over per-user server configuration. Let each client generate its NKey pair locally, send only the public NKey to a trusted onboarding service, and have that service issue a signed user JWT with the right permissions.

On connect, NATS validates the trusted signer and challenges the client to sign a nonce. That nonce signature proves the client holds the private key corresponding to the public key in the JWT, while the JWT claims define what the client is allowed to do.


Want help from the NATS experts? Meet with our architects to get help tailored to your use case and environment.

Get the NATS Newsletter

News and content from across the community


Cancel