Spin up a NATS server, expose it on a public URL, and by default anyone with the address can connect, publish, subscribe, and read every stream you have.No password. No token. Nothing.If your JetStream is holding job payloads, user events, or anything you'd rather not see on the internet, that's a problem. Here's how we locked ours down using JWT plus NATS's own NKEY auth.
Why the Defaults Aren't Enough
Out of the box, NATS is wide open. You can slap on a username and password, but that's a shared secret sitting in config files, environment variables, and every client that ever connects. Rotate it and everything breaks at once.Token auth has the same problem. TLS gets you encryption in transit, not identity.What you actually want is per-client credentials that can be issued, scoped, and revoked without touching the server config. That's what JWT plus NKEY gives you, and it's built right into NATS.
The Building Blocks
Two things travel with every client:
- A JWT. Signed by an account key you control. Carries the client's public identity and its permissions (which subjects it can publish to, which streams it can read, expiry, and so on).
- An NKEY seed. A private Ed25519 key. Never leaves the client. Never sent over the wire. Used only to prove the client actually owns the identity in the JWT.
Think of the JWT as the passport and the seed as the fingerprint. Anyone can steal a passport. You can't steal a fingerprint.
The Connection Flow
Three parties, four messages, two independent checks:
Here's what each step does:
- 1Orchestrator issues credentials. A JWT and a fresh NKEY seed, scoped to exactly what this client needs.
- 2Client presents the JWT. NATS validates the signature against the account's public key and reads the permissions.
- 3NATS sends a nonce. A random challenge, freshly generated for this connection.
- 4Client signs the nonce with its seed. Proves it holds the private key matching the public identity in the JWT.
- 5NATS verifies the signature. Using the public NKEY embedded in the JWT. If it checks out, the connection is permitted.
A stolen JWT alone is useless without the seed. A stolen seed alone is useless without a valid JWT. And because the seed signs a fresh nonce every time, there's nothing replayable on the wire.
Why This Beats Static Auth
Concretely, what we get:Problem with static authHow JWT + NKEY solves itShared secrets in every configEach client gets its own credentialsRotation breaks everythingRotate one client without touching othersNo way to scope permissionsJWT carries per-client publish/subscribe rulesRevocation means editing server configRevoke the JWT, client is out immediatelyCredentials sit in logs and env dumpsSeed never leaves the client, never hits the wire
The Orchestrator Pattern
We don't hand out long-lived credentials. Our orchestrator issues short-lived JWTs and seeds on demand, right before a client needs to connect. That means:
- Compromised creds have a small blast radius. They expire fast.
- We can bake permissions into the JWT that match exactly what the client is doing right now.
- Revocation is instant. Stop issuing to that client and the existing token dies on its own.
The orchestrator holds the account signing key. Clients never see it. Even if a client is fully compromised, the attacker gets one short-lived JWT and one seed, not the keys to your kingdom.
The Takeaway
Public NATS JetStream without proper auth is an open door. Basic username and password isn't a lock, it's a sign that says "please don't come in."JWT plus NKEY gives you real, cryptographically verified, per-client identity, with permissions baked in and revocation that actually works. It's built into NATS, it's the pattern the NATS team recommends, and once your orchestrator is issuing creds, adding a new client is a single API call.If your streams are worth anything, this is the minimum.
Himanshu Sharma