NATS Connections And Authentication
This page documents how nats-sinks connects to a NATS server and how current
authentication settings are represented in JSON configuration.
The implementation uses nats-py connection options under the hood. The shared
connection-option builder loads already-validated JSON settings, redacts
secrets for display, resolves secret environment variables only when opening a
connection, then passes the resulting options to nats.connect.
In controlled mission and defence networks, connection policy is often as important as application code. Keep NATS authentication, TLS trust, account layout, and subject permissions aligned with the classification and operational domains carried by the stream.
Supported In This Release
The current release supports these production-use connection patterns:
- unauthenticated local development connections,
- token authentication with
tokenortoken_env, - plain username/password authentication with
userandpasswordorpassword_env, - server-side bcrypted username/password credentials, using the same client configuration as plain username/password,
- NATS credentials-file authentication through
creds_file, including decentralized JWT user credentials generated by NATS tooling, - NKEY challenge authentication through
nkey_seed_file, - TLS server verification with a local CA file, including private or self-signed NATS server CAs,
- TLS client certificate/key transport settings for mutual TLS deployments,
- optional multiple NATS seed URLs for clustered deployments,
- reconnect tuning for connection timeout, reconnect wait, maximum reconnect attempts, ping behavior, pending buffer size, and drain timeout,
- connection event metrics for disconnect, reconnect, close, discovered-server, and asynchronous error callbacks, and
- least-privilege NATS permission templates for runtime workers, DLQ publish rights, optional consumer management, and advisory readers.
nats-sinks certifies the client-side configuration, validation, redaction,
and option construction for these workflows. NATS server-side account resolver
configuration, operator signing keys, certificate identity mapping, and
authorization policy remain administrative responsibilities for the NATS
operator and should be validated in the target environment.
Connection Flow
sequenceDiagram
participant CLI as nats-sink CLI
participant Cfg as JSON config
participant Env as Environment
participant TLS as SSLContext
participant NP as nats-py
participant NS as NATS server
CLI->>Cfg: load config.json
CLI->>CLI: validate with Pydantic
CLI->>Env: resolve token_env or password_env when needed
CLI->>TLS: build SSLContext when tls:// or TLS files are configured
CLI->>NP: nats.connect(servers, options)
NP->>NS: authenticate and establish connection
NP-->>CLI: connection event callbacks
CLI->>CLI: increment connection metrics
The resolved token or password is never included in redacted config output. If the required environment variable is missing, startup fails before the runner begins consuming messages.
Local Development Without Authentication
For local-only development:
{
"nats": {
"url": "nats://localhost:4222",
"stream": "ORDERS",
"consumer": "oracle-orders-sink",
"subject": "orders.*"
}
}
This is appropriate for a developer machine or isolated test environment. It is not recommended for production.
Token Authentication
Token authentication uses a single shared secret. Prefer token_env so the
secret is injected by the service manager, container platform, or secret store.
{
"nats": {
"url": "tls://nats.example.com:4222",
"stream": "ORDERS",
"consumer": "oracle-orders-sink",
"subject": "orders.*",
"token_env": "NATS_TOKEN",
"tls_ca_file": "/etc/nats/certs/ca.crt"
}
}
Then configure the environment:
export NATS_TOKEN='example-client-token'
Direct token values are supported for tests and disposable local examples, but
should not be committed to repository files.
Token authentication is mutually exclusive with the other client
authentication methods. A configuration that combines token or token_env
with username/password, creds_file, or nkey_seed_file fails validation
before any connection attempt is made.
Plain Username/Password Authentication
Plain username/password authentication uses user plus either password_env or
password.
{
"nats": {
"url": "tls://nats.example.com:4222",
"stream": "ORDERS",
"consumer": "oracle-orders-sink",
"subject": "orders.*",
"user": "orders_sink",
"password_env": "NATS_PASSWORD",
"tls_ca_file": "/etc/nats/certs/ca.crt"
}
}
Then configure the environment:
export NATS_PASSWORD='example-client-password'
Use TLS for username/password authentication in production. Without TLS, the client credential can be exposed to the network path.
The username and password source must be configured together. A password source
without user, or a user without password or password_env, is rejected as
an incomplete authentication mode.
Bcrypted Username/Password Credentials
NATS can store bcrypted passwords in the server configuration. This protects the server-side configuration file from storing clear-text passwords.
The client configuration is unchanged: nats-sinks still sends the clear-text
client password to the server, and the server verifies it against the bcrypt
hash.
{
"nats": {
"url": "tls://nats.example.com:4222",
"stream": "ORDERS",
"consumer": "oracle-orders-sink",
"subject": "orders.*",
"user": "orders_sink",
"password_env": "NATS_PASSWORD",
"tls_ca_file": "/etc/nats/certs/ca.crt"
}
}
A server-side configuration might contain a bcrypt hash similar to:
authorization {
users = [
{
user: "orders_sink"
password: "$2a$11$..."
}
]
}
Do not put the bcrypt hash in the nats-sinks client config. The hash belongs
on the NATS server. The client receives the clear-text password through
NATS_PASSWORD, and TLS protects that secret in transit.
Credentials-File Authentication And Decentralized JWT
NATS credentials files commonly contain the user JWT and signing material used
by decentralized JWT authentication. nats-sinks passes the configured file to
nats-py as user_credentials:
{
"nats": {
"url": "tls://nats.example.com:4222",
"stream": "ORDERS",
"consumer": "oracle-orders-sink",
"subject": "orders.*",
"creds_file": "/run/secrets/nats/orders-sink.creds",
"tls_ca_file": "/etc/nats/certs/root-ca.crt"
}
}
The credentials file path is redacted in effective configuration output because it points to client identity material. Do not commit credentials files and do not paste their contents into issues, logs, documentation examples, or tickets.
creds_file is mutually exclusive with token, username/password, and
nkey_seed_file authentication. It is treated as a complete NATS
authentication mode. The NATS operator remains responsible for account
resolver configuration, signing-key lifecycle, account exports/imports, and
subject-level authorization.
NKEY Challenge Authentication
NKEY challenge authentication uses Ed25519 key material. Configure a seed file
mounted at runtime and let nats-py handle the challenge response:
{
"nats": {
"url": "tls://nats.example.com:4222",
"stream": "ORDERS",
"consumer": "file-orders-sink",
"subject": "orders.*",
"nkey_seed_file": "/run/secrets/nats/orders-sink.nk",
"tls_ca_file": "/etc/nats/certs/root-ca.crt"
}
}
The seed-file path is redacted by show-effective-config. Store the seed file
outside the repository, mount it with restrictive file permissions, and rotate
the identity according to the NATS operator policy. nkey_seed_file is
mutually exclusive with token, username/password, and credentials-file
authentication.
TLS Client Certificate Authentication
Some NATS deployments require a client certificate and private key as part of
the TLS handshake. Configure tls_cert_file and tls_key_file together:
{
"nats": {
"url": "tls://nats.example.com:4222",
"stream": "ORDERS",
"consumer": "oracle-orders-sink",
"subject": "orders.*",
"tls_ca_file": "/etc/nats/certs/root-ca.crt",
"tls_cert_file": "/run/secrets/nats/client.crt",
"tls_key_file": "/run/secrets/nats/client.key"
}
}
tls_key_file requires tls_cert_file. The client certificate and private-key
paths are redacted in effective configuration output because they identify the
client. The project builds a Python ssl.SSLContext, loads the client
certificate chain, and passes the context to nats-py. Server-side certificate
identity mapping and authorization policy remain NATS deployment concerns.
TLS With A Local CA Certificate
Private NATS deployments often use a private CA or self-signed development CA.
Configure tls_ca_file with the local CA certificate and use a tls:// URL:
{
"nats": {
"url": "tls://nats.internal.example:4222",
"stream": "ORDERS",
"consumer": "oracle-orders-sink",
"subject": "orders.*",
"token_env": "NATS_TOKEN",
"tls_ca_file": "/etc/nats/certs/root-ca.crt",
"tls_verify": true
}
}
The shared NATS option builder creates an ssl.SSLContext with that CA file:
flowchart LR
Config[config.json tls_ca_file] --> Context[ssl.create_default_context]
Context --> Trust[Trust private CA]
Trust --> Verify[Verify NATS server certificate]
Verify --> Connect[nats-py connect]
Keep tls_verify set to true in production. Setting tls_verify to false
disables hostname and certificate verification and should be limited to
short-lived local development experiments.
For private mission networks, prefer importing the relevant CA certificate into the service configuration over weakening TLS verification. A local CA is a normal pattern for internal infrastructure; disabling verification should not become the workaround for certificate lifecycle issues.
Multiple Seed URLs
NATS clients can receive more than one server URL. This helps the client reach
a clustered deployment when one server is temporarily unavailable. Configure
nats.urls when you want an explicit seed list:
{
"nats": {
"urls": [
"tls://nats-a.internal.example:4222",
"tls://nats-b.internal.example:4222",
"tls://nats-c.internal.example:4222"
],
"stream": "ORDERS",
"consumer": "orders-file-sink",
"subject": "orders.*",
"token_env": "NATS_TOKEN",
"tls_ca_file": "/etc/nats/certs/root-ca.crt"
}
}
When urls is present, it is passed to nats-py as the servers option and
takes precedence over the single url value. Keep the single url field for
simple local development or single-endpoint deployments.
Every URL in urls is validated with the same scheme allow list as url:
nats, tls, ws, or wss. If any configured seed URL uses tls://, or if
TLS certificate files are configured, nats-sinks builds a TLS context and
passes it to nats-py, even when the fallback url field remains at its
default value.
ws:// and wss:// are accepted by configuration validation because NATS and
nats-py support WebSocket transport. nats-sinks adds project guardrails on
top of that client behavior: credentials in URLs are rejected, WebSocket seed
lists cannot be mixed with nats:// or tls:// seed URLs, wss:// uses the
same verified TLS context and local CA support as tls://, and optional
WebSocket headers are validated and redacted. See
WebSocket Connection Evaluation.
Do not embed credentials in any URL. Use token_env or password_env so
secrets stay out of configuration files, process listings, logs, and support
bundles.
Reconnect Tuning
Automatic reconnect is enabled by default. The defaults are intentionally close
to the nats-py client defaults so a basic deployment does not need to tune
anything immediately:
{
"nats": {
"url": "tls://nats.internal.example:4222",
"stream": "ORDERS",
"consumer": "orders-file-sink",
"subject": "orders.*",
"no_echo": false,
"allow_reconnect": true,
"connect_timeout_seconds": 2,
"reconnect_time_wait_seconds": 2,
"max_reconnect_attempts": 60,
"ping_interval_seconds": 120,
"max_outstanding_pings": 2,
"pending_size_bytes": 2097152,
"drain_timeout_seconds": 30
}
}
| Field | Passed to nats-py |
Default | Guidance |
|---|---|---|---|
no_echo |
no_echo |
false |
Optional server-side echo suppression for messages published and subscribed on the same NATS connection. It is usually unnecessary for normal JetStream pull-consumer sink workers, but the explicit setting is available for specialized connection policies. |
allow_reconnect |
allow_reconnect |
true |
Keep enabled for production unless a supervisor should fail fast and restart the process. |
connect_timeout_seconds |
connect_timeout |
2 |
Increase when connecting across slower controlled networks. |
reconnect_time_wait_seconds |
reconnect_time_wait |
2 |
Increase to reduce retry pressure during planned outages. |
max_reconnect_attempts |
max_reconnect_attempts |
60 |
Use a bounded value for fail-fast service supervision, or -1 for unlimited attempts when the process should wait for NATS to return. |
ping_interval_seconds |
ping_interval |
120 |
Lower values detect broken connections sooner, at the cost of more heartbeat traffic. |
max_outstanding_pings |
max_outstanding_pings |
2 |
Controls how many unanswered pings are tolerated before the client treats the connection as unhealthy. |
pending_size_bytes |
pending_size |
2097152 |
Bounds client pending bytes. Increase carefully for high-throughput deployments after measuring memory growth. |
drain_timeout_seconds |
drain_timeout |
30 |
Bounds client drain behavior during shutdown. |
For operational or mission-support deployments, reconnect settings should be
chosen together with service manager restart policy, JetStream consumer
AckWait, destination write latency, and alerting. A reconnect does not weaken
commit-then-ACK: messages that were not ACKed remain eligible for redelivery.
Connection Event Metrics
The runner installs nats-py connection callbacks and increments metrics when
the client reports connection state changes:
| Metric suffix | Meaning |
|---|---|
nats_connection_disconnected_total |
The client reported a disconnect. |
nats_connection_reconnected_total |
The client successfully reconnected. |
nats_connection_closed_total |
The client reported the connection closed. |
nats_discovered_servers_total |
The client discovered an additional server. |
nats_async_errors_total |
The client reported an asynchronous connection/client error. |
These metrics appear in any configured metrics recorder, including the local
JSON snapshot inspected by nats-sink-metrics:
nats-sink-metrics show .local/nats-sinks/metrics.json \
--kind counter \
--metric "nats_*"
Example output:
KIND METRIC VALUE DESCRIPTION
counter nats_async_errors_total 0 NATS asynchronous error callback events observed by the runner.
counter nats_connection_disconnected_total 1 NATS client disconnect events observed by the runner.
counter nats_connection_reconnected_total 1 NATS client reconnect events observed by the runner.
Embedding applications can still provide their own nats-py callbacks in
nats_options. The runner wraps those callbacks instead of replacing them:
metrics are recorded first, then the application callback runs. If the
application callback raises, the runner logs that callback failure without
printing secrets.
Optional Client Certificate Files
The config model includes:
{
"nats": {
"tls_cert_file": "/etc/nats/certs/client.crt",
"tls_key_file": "/etc/nats/private/client.key"
}
}
When present, the shared option builder loads the certificate chain into the
Python SSL context and passes it to nats-py. nats-sinks certifies this
client-side configuration path, redaction behavior, and option construction.
Full TLS certificate identity mapping, trust-store lifecycle, and authorization
policy remain NATS server and operator responsibilities.
Least-Privilege NATS Permissions
Authentication proves which client is connecting. Authorization decides which subjects that client may publish or subscribe to. Production sink workers should use both: strong authentication for identity and narrow subject permissions for blast-radius control.
The recommended production pattern is to pre-create the stream and durable
consumer with an administrative account, then run nats-sinks with a runtime
account that can only:
- request batches from
$JS.API.CONSUMER.MSG.NEXT.<STREAM>.<CONSUMER>, - receive request/reply responses on the configured inbox pattern,
- publish ACK/NAK responses under
$JS.ACK.<STREAM>.<CONSUMER>.>, and - publish to the configured DLQ subject only when DLQ is enabled.
Full templates, diagrams, and validation checklists are documented in NATS Least-Privilege Permissions.
Secret Redaction
nats-sink show-effective-config redacts:
password,password_env,token,token_env,credentials,creds,- URLs containing embedded credentials.
Prefer this pattern for services:
/etc/nats-sinks/config.json non-secret runtime config
/etc/nats-sinks/nats-sink.env secret environment variables
Authentication Decision Guide
flowchart TD
Start[Need NATS auth?] --> Local{Local isolated dev?}
Local -->|yes| NoAuth[No auth may be acceptable]
Local -->|no| Simple{Simple shared secret acceptable?}
Simple -->|yes| Token[Use token_env plus TLS]
Simple -->|no| UserPass{Per-client identity needed?}
UserPass -->|yes| Password[Use user plus password_env plus TLS]
Password --> Bcrypt[Store bcrypt hash server-side when desired]
UserPass -->|JWT account model| Creds[Use creds_file plus TLS]
UserPass -->|challenge response| Nkey[Use nkey_seed_file plus TLS]
UserPass -->|mutual TLS| Mtls[Use tls_cert_file and tls_key_file]
Certification Test Hook
The repository includes an environment-gated integration test for live NATS authentication workflows. It is skipped by default so ordinary unit tests never need real credentials, private endpoints, seed files, credentials files, or client certificates.
Example for a controlled credentials-file lab:
export NATS_SINKS_NATS_AUTH_INTEGRATION=1
export NATS_SINKS_AUTH_MODE=credentials_file
export NATS_SINKS_AUTH_URL=tls://nats.example.com:4222
export NATS_SINKS_AUTH_CREDS_FILE=/run/secrets/nats/orders-sink.creds
export NATS_SINKS_AUTH_TLS_CA_FILE=/etc/nats/certs/root-ca.crt
python -m pytest tests/integration/test_nats_auth_workflows.py -q
Supported NATS_SINKS_AUTH_MODE values are none, username_password,
token, credentials_file, nkey_seed_file, and
tls_client_certificate. Use fake or lab-only values in examples and never
include real endpoints, passwords, tokens, seed contents, credentials-file
contents, private keys, or certificate material in test reports or issue
comments.
Current Field Reference
| Field | Purpose | Secret? | Recommendation |
|---|---|---|---|
nats.url |
NATS server URL. Use tls:// for TLS. |
Sometimes | Do not embed credentials in URLs. |
nats.user |
Username for username/password auth. | Usually no | Use with password_env. |
nats.password |
Direct client password. | Yes | Avoid outside disposable local tests. |
nats.password_env |
Environment variable containing the client password. | Env var name only | Preferred for username/password auth. |
nats.token |
Direct client token. | Yes | Avoid outside disposable local tests. |
nats.token_env |
Environment variable containing the client token. | Env var name only | Preferred for token auth. |
nats.creds_file |
Local NATS credentials file passed to nats-py as user_credentials. |
Yes, sensitive identity path | Use for decentralized JWT credentials and mount from a secret location. |
nats.nkey_seed_file |
Local NKEY seed file passed to nats-py as nkeys_seed. |
Yes, sensitive identity path | Use for NKEY challenge authentication and protect file permissions carefully. |
nats.tls_ca_file |
Local CA certificate used to verify the NATS server. | No | Use for private or self-signed CAs. |
nats.tls_verify |
Enables certificate and hostname verification. | No | Keep true in production. |
nats.tls_cert_file |
Optional client certificate chain. | Sensitive identity path | Use with tls_key_file for mutual TLS deployments. |
nats.tls_key_file |
Optional client private key file. | Yes | Protect file permissions carefully. |
nats.no_echo |
Optional server-side same-connection echo suppression. | No | Leave false unless a reviewed deployment has a same-connection publish/subscribe reason to suppress echoes. |
WebSocket Transport Status
NATS supports WebSocket connections, and the nats-sinks URL validator accepts
ws:// and wss://. WebSocket support is a transport option, not a separate
delivery mode. The runner still receives JetStream messages, writes them
through the sink, and ACKs only after durable success.
Use this current status table when reviewing deployments:
| Transport | Current status | Recommendation |
|---|---|---|
nats:// |
Supported. | Use only in isolated local development or protected networks where plaintext transport is explicitly accepted. |
tls:// |
Supported and documented for production. | Preferred production transport today. Use tls_ca_file for private CAs. |
ws:// |
Supported with guardrails. | Use only for local labs or explicitly approved controlled networks because it is plaintext WebSocket transport. |
wss:// |
Supported with guardrails. | Preferred WebSocket transport. Keep tls_verify enabled and use tls_ca_file for private CAs. |
WebSocket transport must not change commit-then-acknowledge behavior. A WebSocket connection failure is a transport event; it is not sink success and must never trigger an ACK by itself.
WebSocket Guardrails
The WebSocket configuration model fails closed before a connection attempt when the transport shape is ambiguous or unsafe:
nats.urland everynats.urlsentry must use one ofnats,tls,ws, orwss.- URL-embedded credentials such as
wss://user:password@exampleare rejected. Use the normal authentication fields or environment-sourced headers instead. nats.urlsmust not mix WebSocket and non-WebSocket transports in the same seed list.wss://builds anssl.SSLContextand honorstls_ca_file,tls_cert_file,tls_key_file, andtls_verify.websocket_headersandwebsocket_headers_envare accepted only forws://orwss://connections.
Example local-lab WebSocket configuration:
{
"nats": {
"url": "ws://127.0.0.1:8080",
"stream": "ORDERS",
"consumer": "file-orders-sink",
"subject": "orders.*"
},
"sink": {
"type": "file",
"directory": ".local/file-sink/events",
"fsync": false
}
}
Example approved wss:// configuration with a private CA and a non-sensitive
proxy routing hint:
{
"nats": {
"url": "wss://nats.example.com:8443",
"stream": "ORDERS",
"consumer": "oracle-orders-sink",
"subject": "orders.*",
"tls_ca_file": "/etc/nats/certs/private-ca.crt",
"websocket_headers": {
"X-Route-Hint": "approved-edge"
},
"websocket_headers_env": {
"Authorization": "NATS_WS_AUTHORIZATION"
}
},
"sink": {
"type": "oracle",
"dsn": "example_low",
"user": "NATS_SINK_APP",
"password_env": "ORACLE_PASSWORD",
"table": "NATS_SINK_EVENTS"
}
}
Authorization, Cookie, Proxy-Authorization, X-Api-Key, and
X-Auth-Token style headers must be configured through
websocket_headers_env. The effective configuration view redacts both direct
and environment-sourced WebSocket header values.
export NATS_WS_AUTHORIZATION='replace-with-local-test-value'
nats-sink validate config.json
nats-sink show-effective-config config.json
The redacted output uses the same secret handling as other authentication fields:
{
"nats": {
"websocket_headers": {
"X-Route-Hint": "********"
},
"websocket_headers_env": {
"Authorization": "********"
}
}
}
Live Connection Probe
The repository includes a tracked manual probe script:
scripts/nats-live-probe.py
The script intentionally has no hardcoded server, username, password, token, or
CA certificate. Put real runtime material under .local/, which is ignored by
git.
Prepare local files:
mkdir -p .local/nats-live
chmod 700 .local/nats-live
# Save your local CA certificate here:
$EDITOR .local/nats-live/ca.crt
# Save local secrets here:
cat > .local/nats-live/nats-sink.env <<'EOF'
NATS_PASSWORD=replace-with-test-password
EOF
chmod 600 .local/nats-live/ca.crt .local/nats-live/nats-sink.env
Subscribe without publishing:
python scripts/nats-live-probe.py \
--server tls://nats.example.com:4222 \
--user example_user \
--password-env NATS_PASSWORD \
--env-file .local/nats-live/nats-sink.env \
--ca-file .local/nats-live/ca.crt \
--subject example.test.subject
Publish and receive a test message:
python scripts/nats-live-probe.py \
--server tls://nats.example.com:4222 \
--user example_user \
--password-env NATS_PASSWORD \
--env-file .local/nats-live/nats-sink.env \
--ca-file .local/nats-live/ca.crt \
--subject example.test.subject \
--publish \
--message '{"probe":"nats-sinks","kind":"live-test"}'
The probe prints connection status, subscription status, publish status, and
received payload size. It does not print payload content unless
--print-payload is explicitly set.
For a compact walkthrough, see the tracked live NATS probe example.