Skip to main content

Authentication basics

The index page left you with a bare nats-server: it admits every connection, and anyone who can reach the port can publish and subscribe. This page adds the first user list.

That's authentication: the server deciding which user a connection is. This page covers the simplest way to do it, where the list of valid users lives in the server's own config file.

Centralized authentication

In centralized authentication, the server holds the full list of users in its own config file, nats.conf.

When a connection presents credentials, the server walks its config list, finds the matching user, and admits the connection. It never consults an external service.

Centralized authentication is the right tool when one team owns the server config and the user list is small and slow to change. It lives entirely in one file, so it's the easiest model to read and reason about.

Giving order-svc a credential

The chapter's order platform needs a user for its order service, order-svc, and one for its reporting side, analytics-reader.

A centralized user lives in the users array of the server's authorization block. Each entry names a user with a user field and carries that user's credential:

authorization {
users: [
{ user: order-svc, password: s3cr3t }
{ user: analytics-reader, password: an4lytics }
]
}

The user and password fields are the credential. A connection that presents order-svc / s3cr3t is authenticated as order-svc. Both users share the global account, called $G; the Accounts and multitenancy page later gives each its own space.

For a server with exactly one user, you can skip the array and put a single user and password pair directly in the authorization block.

The authorization block also takes a timeout field: how long the server gives a client to finish authenticating, 2 seconds by default. Plain numbers are seconds; duration strings need quotes (timeout: "500ms" — an unquoted 1m parses as a number, not a minute). The full field list is in Reference.

Start the server with that config:

nats-server -c nats.conf

The -c flag points the server at the config file. Once it's running, order-svc can connect.

Connecting as order-svc

A client authenticates by sending its credentials at connect time. On the CLI that's two flags; in a client library it's two fields on the connect call. The user publishes the canonical order message to orders.created:

#!/bin/bash
# Connect as the centralized user `order-svc` and publish the canonical
# order message to orders.created.
#
# --user / --password send the credentials at connect time. The server
# matches them against its config user list and accepts the publish.
# Wrong credentials are rejected at connect time, before any publish,
# with: nats: error: nats: Authorization Violation
nats pub orders.created \
--user order-svc \
--password "s3cr3t" \
'{"order_id":"ord_8w2k","customer":"acme-co","total_cents":4200,"ts":"2026-05-22T10:14:22Z"}'
# Expected output:
# 14:18:38 Published 91 bytes to "orders.created"
14:18:38 Published 91 bytes to "orders.created"

The server matched the credentials and accepted the 91-byte order payload. A wrong password fails at connect time, before any publish:

nats pub orders.created "test" --user order-svc --password wrong
nats: error: nats: Authorization Violation

An unauthenticated connect — no flags at all — fails with the same Authorization Violation error. The server gives the same answer for a wrong password and an unknown user, so a failed login doesn't reveal which half was wrong.

A client offers credentials once, when it connects. Authentication decides the user for the whole life of that connection. What the user may then publish or subscribe to is a separate question: authorization, covered on the Authorization page.

Other ways a user entry can authenticate

order-svc used a password, but config auth offers three credential styles in all: user/password, nkey, and token. The model doesn't change; only the field differs.

user/password is the pair you just used: the client sends a username and a password, and the server compares the password against the stored value.

nkey is a public-key credential: the user entry holds only a public nkey — it replaces the whole user/password pair, and the server rejects an entry that mixes them. The client holds the matching private seed and proves ownership by signing a server-issued nonce, so nothing secret crosses the wire:

users: [
{ nkey: UAPZQH4MNJCOVEJFERB3NFSIROQ5RE7CGBEPKAZSB6QB7IQHBKXHZPVP }
]

Generate the keypair, add the printed public key to the user list, and connect with the seed file:

#!/bin/bash
# Generate a user nkey and authenticate with it. The server config
# holds only the public key; the private seed stays with the client.

# Generate the keypair. The private seed goes to user.nk and nothing
# is printed; `show` prints the matching public key for the config.
nats auth nkey gen user --output user.nk
nats auth nkey show user.nk
# UAPZQH4MNJCOVEJFERB3NFSIROQ5RE7CGBEPKAZSB6QB7IQHBKXHZPVP

# Put that public key in the server's user list:
# authorization { users: [ { nkey: UAPZQH4MNJCOVEJFERB3NFSIROQ5RE7CGBEPKAZSB6QB7IQHBKXHZPVP } ] }

# Connect with the seed file. The client signs the server's nonce with
# the seed, so the secret never crosses the wire. A seed the server
# doesn't know fails with: nats: error: nats: Authorization Violation
nats pub orders.created \
--nkey user.nk \
'{"order_id":"ord_8w2k","customer":"acme-co","total_cents":4200,"ts":"2026-05-22T10:14:22Z"}'
# Expected output:
# 18:02:29 Published 91 bytes to "orders.created"
UAPZQH4MNJCOVEJFERB3NFSIROQ5RE7CGBEPKAZSB6QB7IQHBKXHZPVP
18:02:29 Published 91 bytes to "orders.created"

gen writes the private seed to user.nk and prints nothing, show prints the public key the config entry above holds, and the publish authenticated with only the seed file — a seed the server doesn't know fails with the same Authorization Violation as a wrong password. We come back to nkeys on the Decentralized authentication page; here they're just one more way to authenticate a config user.

token is a single shared secret with no username, set on the server's top-level authorization block: authorization { token: "shared-secret-rotate-me" }. Any client presenting the right token is admitted. It's the one style that can't be per-user, which makes it a server-wide secret — usable for quick internal setups but little else. (When this chapter says "token" it always means this, never a JWT.)

One CLI trap to know: the --user flag doubles as a token field — its help text reads "Username or Token". A lone --user with no --password is sent as a token, so it can appear to work against a token-configured server and mask a misconfiguration.

Storing passwords

The config above stored order-svc's password in plaintext. That's fine for a laptop, but not for a config file others can read.

The server flags this itself: on startup it scans the user list, and if any password is plaintext it logs a warning:

[WRN] Plaintext passwords detected, use nkeys or bcrypt

The fix is to store a bcrypt hash instead of the raw password. bcrypt is a one-way hash: the server keeps the hash, the client still sends the plaintext password, and the server hashes the input to compare. The stored value reveals nothing usable if the config leaks.

Generate a hash with the CLI:

nats server passwd --pass "s3cr3t-rotate-me-later"
$2a$11$4I9tIK1JVbttZYtn.F.Jse5iY5ves4EtYWIpjlwyvgVYHJc8yTvk.

Without --pass it prompts interactively; --generate invents a strong passphrase and hashes it in one step, and --cost raises the hashing cost above the default 11. The command refuses passwords shorter than 10 characters (password should be at least 10 characters long), which is why this example hashes the longer s3cr3t-rotate-me-later.

The printed hash starts with $2a$11$ — Go's bcrypt prefix at cost 11. The server recognizes any value matching $2a$, $2b$, $2x$, or $2y$ as a bcrypt hash and compares everything else as plaintext.

Paste the hash into the config in place of the plaintext password; quotes around it are optional:

authorization {
users: [
{ user: order-svc, password: "$2a$11$4I9tIK1JVbttZYtn.F.Jse5iY5ves4EtYWIpjlwyvgVYHJc8yTvk." }
{ user: analytics-reader, password: an4lytics }
]
}

order-svc now authenticates with s3cr3t-rotate-me-later — the password the hash was generated from. The client still sends the plaintext; only the stored form is hashed. Once every password in the list is a hash, the startup warning goes away. (The rest of the chapter returns to the short plaintext s3cr3t so the listings stay readable.)

Tokens can be stored the same way — a bcrypt hash in the token field goes through the same comparison, and the client sends the clear token.

Because the client still sends the plaintext over the wire, bcrypt protects only the config file at rest. Pair it with TLS, which the Encryption & TLS page sets up.

What this page does not cover

A client certificate can also be a credential: the server can map a certificate identity straight to a user with mTLS, so the cert is the credential. That ties into TLS, so the Encryption & TLS page covers it.

The other open question is scale. Centralized auth keeps every user in one config file, which is exactly what breaks down when many tenants manage their own users. The Operator mode page introduces the model built for that.

Pitfalls

A few things catch people when credentials live in a config file.

Running with no authentication in production. A server with no authorization block admits every connection. That's convenient on a laptop, but on a shared network anyone who can reach the port can publish and subscribe, so don't ship it. Give every server at least one user list, so an unauthenticated connect fails with nats: error: nats: Authorization Violation instead of silently succeeding.

Leaving plaintext passwords in a deployed config. The server logs Plaintext passwords detected, use nkeys or bcrypt on startup, and the fix — a nats server passwd hash in place of the raw value — is covered above. The pitfall is treating that warning as noise. On any server someone else can read, store the bcrypt hash, not the plaintext.

Committing credentials to git. A nats.conf with a password (even a bcrypt hash) is a secret. Once it lands in history, rotating the password is the only real fix, because the old value lives in every clone. Keep the credential out of the committed file: reference an environment variable or a secret store, and add the real config to .gitignore.

Putting the password in the connection URL. A URL like nats://order-svc:s3cr3t@localhost:4222 puts the credential into shell history, process listings, and any log that records the connection string. Store it in a named context instead — passing the password as "$NATS_PASSWORD" so your shell history records only the variable name — then connect by context name with no credential on the command line:

#!/bin/bash
# Keep order-svc's credential out of the connection URL (and out of shell
# history and server logs) by storing it in a named context.
#
# A URL like nats://order-svc:s3cr3t@localhost:4222 leaks the password
# into every log line and your shell history. A context holds the
# credential separately and is selected by name.

# Save the credential once, in a named context. Passing it as
# "$NATS_PASSWORD" keeps the literal value out of shell history;
# only the variable name is recorded.
export NATS_PASSWORD=s3cr3t
nats context add orders \
--server localhost:4222 \
--user order-svc \
--password "$NATS_PASSWORD" \
--description "ORDERS platform, order-svc user"

# Unset the variable once the context is saved. While it's exported the
# CLI warns: WARNING: Shell environment overrides in place using NATS_PASSWORD
unset NATS_PASSWORD

# Make it the active context.
nats context select orders

# Now connect with no credential on the command line at all. The publish
# uses the stored credential from the `orders` context.
nats pub orders.created \
'{"order_id":"ord_8w2k","customer":"acme-co","total_cents":4200,"ts":"2026-05-22T10:14:22Z"}'
# Expected output:
# 14:19:37 Published 91 bytes to "orders.created"
14:19:37 Published 91 bytes to "orders.created"

The context holds order-svc's password; the publish command carries none. While NATS_PASSWORD is exported the CLI prints WARNING: Shell environment overrides in place using NATS_PASSWORD, so unset it once the context is saved.

Where you are

You have:

  • A server started with nats-server -c nats.conf.
  • Two users in a top-level authorization block, both in the global account $G: order-svc and analytics-reader, password-authenticated — and you know how to swap any stored password for a nats server passwd bcrypt hash.
  • Proof the passwords work: order-svc published the canonical order message to orders.created, and a wrong password was rejected with Authorization Violation.

What's next

order-svc can prove who it is, but nothing yet limits what it may do: it can publish and subscribe anywhere on the server. The next page adds those limits.

Continue to Authorization.

See also