# 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.

**Message flow — centralizedAuthAnimated:** Interactive NATS flow diagram.

* NATS → server

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](/learn/security/accounts-and-multitenancy.md) 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`](/reference/config/authorization/timeout.md) 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](/reference/config/authorization/.md).

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](/learn/security/authorization.md) 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](/learn/security/decentralized-auth.md) 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](/learn/security/encryption.md) 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](/learn/security/encryption.md) 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](/learn/security/operator-mode.md) 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 `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](/learn/security/authorization.md).

## See also

* [Reference → authorization](/reference/config/authorization/.md) — every field of the `authorization` block, including `timeout` and per-user options.
* [Authorization](/learn/security/authorization.md) — what an authenticated user is then allowed to do.
* [Core Concepts → Security](/concepts/security.md) — the five-minute overview of the same material.
