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:
- CLI
#!/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:
- CLI
#!/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:
- CLI
#!/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
authorizationblock, both in the global account$G:order-svcandanalytics-reader, password-authenticated — and you know how to swap any stored password for anats server passwdbcrypt hash. - Proof the passwords work:
order-svcpublished the canonical order message toorders.created, and a wrong password was rejected withAuthorization 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
- Reference → authorization — every
field of the
authorizationblock, includingtimeoutand per-user options. - Authorization — what an authenticated user is then allowed to do.
- Core Concepts → Security — the five-minute overview of the same material.