Skip to content

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 token or token_env,
  • plain username/password authentication with user and password or password_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.url and every nats.urls entry must use one of nats, tls, ws, or wss.
  • URL-embedded credentials such as wss://user:password@example are rejected. Use the normal authentication fields or environment-sourced headers instead.
  • nats.urls must not mix WebSocket and non-WebSocket transports in the same seed list.
  • wss:// builds an ssl.SSLContext and honors tls_ca_file, tls_cert_file, tls_key_file, and tls_verify.
  • websocket_headers and websocket_headers_env are accepted only for ws:// or wss:// 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.

References