Skip to content

NATS Least-Privilege Permissions

This page gives NATS administrators and platform security teams practical least-privilege permission templates for running nats-sinks workers. It is written for production and mission-oriented deployments where a sink worker should be able to consume the exact JetStream workload it owns, acknowledge messages only after durable sink success, publish to a configured dead-letter subject when needed, and do nothing else.

The templates use placeholders such as <STREAM>, <CONSUMER>, <SOURCE_SUBJECT>, and <DLQ_SUBJECT>. Replace those placeholders with values from your own server configuration. Do not paste real credentials, internal addresses, or private operational subject names into public issue comments, examples, or support tickets.

Why Permissions Matter

nats-sinks is deliberately conservative about delivery semantics. The default runtime fetches messages from a pull-based JetStream consumer, hands normalized NatsEnvelope objects to a sink, waits for the sink to complete durable work, and only then ACKs JetStream. The optional push-consumer mode uses the same ACK rule with a bounded manual-ACK callback queue. NATS permissions should support the chosen flow without granting broad account access.

sequenceDiagram
    participant W as nats-sinks worker
    participant JS as JetStream API
    participant C as Durable consumer
    participant S as Sink
    participant D as DLQ subject

    W->>JS: Request next batch
    JS-->>W: Deliver messages through inbox reply
    W->>S: write_batch(envelopes)
    S-->>W: Durable success
    W->>C: Publish ACK response subject
    Note over W,D: On permanent failure only
    W->>D: Publish DLQ payload
    D-->>W: Publish success
    W->>C: ACK original after DLQ publish

There are three separate permission groups:

Permission group Required by default? Purpose
Runtime pull and ACK permissions Yes Let the worker request messages from one durable pull consumer and publish ACK/NAK responses for messages it actually received.
DLQ publish permissions Only when dead_letter.enabled is true Let the worker publish a permanent-failure record to the configured DLQ subject before ACKing the original message.
Management and advisory permissions Optional Let a controlled account create or inspect consumers, or let a separate observer subscribe to JetStream advisories.

Keep these groups separate. A sink worker that writes to Oracle or the file system does not normally need NATS stream administration, broad subscription rights, or permission to publish source events.

Runtime Worker Responsibilities

A standard nats-sinks worker needs to perform these NATS operations:

  • connect using the configured authentication mechanism,
  • request messages from $JS.API.CONSUMER.MSG.NEXT.<STREAM>.<CONSUMER>,
  • subscribe to generated inbox subjects so NATS request/reply responses can be delivered,
  • publish ACK, NAK, or delayed NAK responses to JetStream message reply subjects under $JS.ACK.<STREAM>.<CONSUMER>.>, and
  • optionally publish DLQ records to exactly the configured DLQ subject.

The worker does not need to publish to <SOURCE_SUBJECT>. Producers publish source events; the sink worker consumes persisted messages through JetStream.

Template A: Pre-Created Durable Consumer

This is the recommended production model. A platform or release process creates the stream and durable consumer ahead of time. The sink runtime account can fetch from the consumer and ACK received messages, but it cannot create, delete, purge, or modify streams.

# NATS server authorization template.
# Replace every angle-bracket placeholder before use.
authorization {
  users = [
    {
      user: "<SINK_RUNTIME_USER>"
      password: "<SERVER_SIDE_PASSWORD_OR_HASH_PLACEHOLDER>"
      permissions: {
        publish: {
          allow: [
            "$JS.API.CONSUMER.MSG.NEXT.<STREAM>.<CONSUMER>",
            "$JS.API.CONSUMER.INFO.<STREAM>.<CONSUMER>",
            "$JS.ACK.<STREAM>.<CONSUMER>.>"
          ]
        }
        subscribe: {
          allow: [
            "_INBOX.>"
          ]
        }
      }
    }
  ]
}

The publish.allow entry for $JS.API.CONSUMER.INFO... is included because startup validates the durable consumer before fetching messages. It is also required when delivery.in_progress.enabled=true and consumer_management.mode=bind_only, because the runtime must inspect the effective AckWait and BackOff policy before it may send progress heartbeats. If your deployment has a separate validation account and the runtime worker never performs those checks, you may remove it only after testing that your chosen runtime mode still starts safely.

The subscribe.allow entry for _INBOX.> is necessary for NATS request/reply patterns used by JetStream API requests and pull delivery. In more sensitive installations, consider account isolation or a custom inbox prefix strategy if your NATS client and server policy support it.

The templates intentionally rely on allow lists rather than a broad deny: ">" next to allowed subjects. NATS deny rules have priority when allow and deny overlap, so a broad deny can accidentally block the very subjects the worker needs. Keep the allow list narrow, then test that unauthorized subjects are rejected.

Template B: Runtime Worker With DLQ Enabled

When dead_letter.enabled is true, add publish permission for the exact DLQ subject. Keep this grant separate from source-subject access so operators can review what failure records may leave the main stream.

permissions: {
  publish: {
    allow: [
      "$JS.API.CONSUMER.MSG.NEXT.<STREAM>.<CONSUMER>",
      "$JS.API.CONSUMER.INFO.<STREAM>.<CONSUMER>",
      "$JS.ACK.<STREAM>.<CONSUMER>.>",
      "<DLQ_SUBJECT>"
    ]
  }
  subscribe: {
    allow: [
      "_INBOX.>"
    ]
  }
}

If the DLQ subject is part of a different stream, account, export, import, or operational compartment, configure that routing intentionally on the NATS side. The nats-sinks worker only needs permission to publish the DLQ message. It does not need to subscribe to the DLQ subject.

flowchart LR
    Source[Source subjects] --> Stream[JetStream stream]
    Stream --> Consumer[Durable consumer]
    Consumer --> Worker[nats-sinks worker]
    Worker -->|durable sink success| Ack[ACK reply subject]
    Worker -->|permanent failure| DLQ[Configured DLQ subject]

Template C: Controlled Consumer Creation

Some early deployments allow the runtime worker to create or bind the durable consumer automatically. This is more convenient, but it grants more authority than the pre-created-consumer model. Use it only when consumer_management.mode is create_if_missing or reconcile and the operational process accepts that tradeoff. For bind_only, prefer Template A.

When consumer_management.filter_subjects contains multiple filters, NATS uses the general durable consumer create subject rather than the single-filter API subject that embeds {filter}. That means the permission boundary is less granular. If subject-level authorization is critical, prefer pre-creating the consumer with an administrative identity and running the worker in bind_only mode.

permissions: {
  publish: {
    allow: [
      "$JS.API.CONSUMER.MSG.NEXT.<STREAM>.<CONSUMER>",
      "$JS.API.CONSUMER.INFO.<STREAM>.<CONSUMER>",
      "$JS.API.CONSUMER.DURABLE.CREATE.<STREAM>.<CONSUMER>",
      "$JS.API.CONSUMER.CREATE.<STREAM>.<CONSUMER>.<SOURCE_SUBJECT>",
      "$JS.ACK.<STREAM>.<CONSUMER>.>"
    ]
  }
  subscribe: {
    allow: [
      "_INBOX.>"
    ]
  }
}

NATS supports a granular consumer-create API subject for a single filter subject. If a future deployment uses multiple filter subjects, the NATS API may not be able to encode each filter in the permission subject. In that case, prefer pre-creating the consumer with an administrative account and granting the runtime worker only fetch and ACK permissions.

When reconcile is enabled, the worker may submit updated consumer settings for an already compatible durable pull consumer. Keep that permission scoped to the configured <STREAM>, <CONSUMER>, and <SOURCE_SUBJECT>. Never use a wildcard consumer-management grant for the sink worker unless the account is a separate deployment automation identity rather than the long-running runtime identity.

Template D: Separate Stream Management Identity

Stream management is a separate operational role. The ordinary sink runtime account should not need to create, update, purge, delete, restore, or snapshot streams. Use a separate administrative account, Terraform identity, release pipeline identity, or controlled platform service to apply stream settings.

The offline helper can prepare a reviewable plan:

nats-sink stream-plan /etc/nats-sinks/config.json --format json

That command does not need the following permissions because it never connects to NATS. The permissions below are for the separate identity that applies the plan through normal NATS administration tooling.

permissions: {
  publish: {
    allow: [
      "$JS.API.STREAM.CREATE.<STREAM>",
      "$JS.API.STREAM.UPDATE.<STREAM>",
      "$JS.API.STREAM.INFO.<STREAM>",
      "$JS.API.STREAM.NAMES",
      "$JS.API.CONSUMER.DURABLE.CREATE.<STREAM>.<CONSUMER>"
    ]
  }
  subscribe: {
    allow: [
      "_INBOX.>"
    ]
  }
}

Only grant delete, purge, restore, snapshot, or broader stream-management subjects when an operational runbook explicitly requires them. Those actions can affect retention, audit history, replay readiness, and evidence custody.

Template E: Separate Advisory Reader

JetStream advisories are useful for operations teams, but they are not required for the sink worker to preserve commit-then-acknowledge semantics. nats-sinks can observe selected advisories when advisories.enabled is true, but a separate read-only observer account is still preferred where operational policy allows it. If the delivery worker observes advisories itself, grant only the exact advisory subjects listed in advisories.subjects.

authorization {
  users = [
    {
      user: "<ADVISORY_READER_USER>"
      password: "<SERVER_SIDE_PASSWORD_OR_HASH_PLACEHOLDER>"
      permissions: {
        publish: {
          deny: [
            ">"
          ]
        }
        subscribe: {
          allow: [
            "$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.<STREAM>.<CONSUMER>",
            "$JS.EVENT.ADVISORY.CONSUMER.MSG_TERMINATED.<STREAM>.<CONSUMER>"
          ]
        }
      }
    }
  ]
}

Advisories and metrics can reveal operational tempo. Treat them as sensitive metadata in mission, defence, or national-security environments. Only export or forward them when the observability policy explicitly allows it.

Mapping Config To Permissions

Use this table when translating nats-sinks JSON configuration into NATS server permission templates.

nats-sinks field Permission impact
nats.stream Replaces <STREAM> in JetStream API and ACK subjects.
nats.consumer Replaces <CONSUMER> in JetStream API and ACK subjects.
nats.subject Replaces <SOURCE_SUBJECT> only when runtime consumer creation is allowed. Pre-created consumer mode should enforce this on the consumer configuration instead.
dead_letter.enabled Controls whether a DLQ publish grant is needed.
dead_letter.subject Replaces <DLQ_SUBJECT> when DLQ is enabled.
nats.urls or nats.url Does not change subject permissions, but should use TLS for production.
nats.token_env, nats.password_env, or credentials files Controls authentication material, not subject authorization.

What Not To Grant By Default

Avoid these grants for the ordinary sink runtime account:

  • publish: ">",
  • subscribe: ">",
  • stream create, update, delete, purge, restore, or snapshot APIs,
  • consumer delete APIs,
  • $SYS.> system events,
  • broad access to all JetStream advisories,
  • permission to publish source subjects,
  • permission to subscribe to destination DLQ subjects unless this worker also has a separate, reviewed DLQ replay role.

If an operator needs one of these capabilities, create a separate account or user for that operational function and record why it is required.

Failure Behavior With Insufficient Permissions

Permission failures should fail visibly. They must not become early ACKs. If the worker cannot fetch, publish a DLQ record, or publish the ACK response after sink success, the error is surfaced through logging and metrics, and the message remains governed by JetStream redelivery behavior.

sequenceDiagram
    participant W as Worker
    participant N as NATS server
    participant S as Sink

    W->>N: Fetch or ACK publish
    N-->>W: Authorization failure
    W->>W: Log sanitized failure and increment error metric
    Note over W,S: No durable success means no ACK. Durable success with ACK failure may redeliver and must be idempotent.

Validation Checklist

Before running a production worker, validate the permission set in a controlled environment:

  • Confirm the stream and durable consumer are created by the intended administrative process.
  • Confirm the sink runtime user can connect, fetch a small batch, write to the destination, and ACK after durable success.
  • Confirm the runtime user cannot publish to source subjects.
  • Confirm the runtime user cannot create, delete, purge, or modify streams.
  • If DLQ is enabled, confirm a permanent-failure test can publish to the exact DLQ subject and then ACK the original message.
  • If DLQ is disabled, confirm the runtime user has no DLQ publish grant.
  • Confirm advisory subscriptions are assigned to a separate observer account unless a deliberate exception is approved.
  • Confirm all tests use placeholders or local-only values in documentation and public evidence.

References