The platform engineer from the first story wants that NATS cluster to run under a NATS operator the auth controller owns, with accounts and users declared as resources instead of minted by hand.

NATS operator and system account #

The auth controller generates the NATS operator’s keys and keeps each seed in a Secret. The NATS operator names exactly one system account; others may exist unreferenced, and flipping the reference is how one is rotated.

01-natsoperator.yaml

apiVersion: auth.nats.mikluko.io/v1beta1
kind: NatsOperator
metadata:
  name: demo
  namespace: nats-system
spec:
  # No `keys`: the auth controller generates the identity key and one signing
  # key and keeps each seed in a Secret that outlives this object.
  # `keys.identity` and `keys.signing[]` adopt existing seeds instead, and `jwt`
  # with signing keys only keeps the identity offline. Account JWTs are signed
  # with a signing key, never the identity key.

  # The one system account the NATS operator JWT names; NATS allows exactly one.
  # Other NatsSystemAccounts may exist unreferenced: they are not signed and
  # their users get no creds. Flipping this ref is how a system account is
  # rotated, and `system_account` is restart-only, so every server trusting
  # this NATS operator restarts.
  systemAccountRef:
    name: sys
kubectl apply -f https://nats-operator.io/docs/stories/02-auth-plane/01-natsoperator.yaml

01-status-natsoperator.yaml

status:
  observedGeneration: 1
  conditions:
    - type: Ready
      status: "True"
      reason: Signed
  publicKey: ODEMO...IDENTITY
  signingKeys:
    - ODEMO...SIGNING1
  seedSecrets:
    identity: demo-operator-identity
    signing: [demo-operator-signing-1]
  # Public and signed, not secrets. A NatsOperatorTrust with operatorRef
  # mirrors them; elsewhere they are copied into a literal one (story 6).
  jwt: eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ...
  systemAccount:
    name: sys
    publicKey: ADEMO...SYS
    jwt: eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ...

Accounts #

The system account is a kind of its own so RBAC can grant it to the platform team alone. An ordinary account’s limits are signed into its JWT, and the auth controller configures nothing about JetStream beyond that.

Whoever writes a NatsAccount sets its limits. A limit left out or set to 0 is unlimited, and an account without limits.jetstream has no JetStream. A NatsReferenceGrant admitting NatsAccounts from another namespace to a NatsOperator therefore lets that namespace set its own accounts’ limits.

01-natsaccounts.yaml

# A system account has no JetStream fields.
apiVersion: auth.nats.mikluko.io/v1beta1
kind: NatsSystemAccount
metadata:
  name: sys
  namespace: nats-system
spec:
  operatorRef:
    name: demo
---
apiVersion: auth.nats.mikluko.io/v1beta1
kind: NatsAccount
metadata:
  name: orders
  namespace: nats-system
spec:
  operatorRef:
    name: demo
  # The account JWT expires after this and is re-signed at half of it.
  # 48h is the default; the system account's JWT never expires.
  jwtTTL: 48h
  limits:
    connections: 500
    subscriptions: 10000
    payload: 1Mi
    jetstream:
      memoryStorage: 1Gi
      diskStorage: 50Gi
      streams: 20
      consumers: 200
kubectl apply -f https://nats-operator.io/docs/stories/02-auth-plane/01-natsaccounts.yaml

An account change is pushed to the servers’ resolvers without a restart; status says how many servers hold the current JWT.

02-natsaccount-orders.yaml

# The orders account with its connection limit raised from 500; nothing else
# changes.
apiVersion: auth.nats.mikluko.io/v1beta1
kind: NatsAccount
metadata:
  name: orders
  namespace: nats-system
spec:
  operatorRef:
    name: demo
  jwtTTL: 48h
  limits:
    connections: 1000
    subscriptions: 10000
    payload: 1Mi
    jetstream:
      memoryStorage: 1Gi
      diskStorage: 50Gi
      streams: 20
      consumers: 200
kubectl apply -f https://nats-operator.io/docs/stories/02-auth-plane/02-natsaccount-orders.yaml

02-status-natsaccount-orders.yaml

status:
  observedGeneration: 2
  conditions:
    - type: Ready
      status: "True"
      reason: Distributed
    - type: Distributed
      status: "True"
      reason: AllServersCurrent
      message: 3 of 3 servers hold this JWT
  publicKey: AORDERS...
  jwtHash: 9c41e7
  # The STATSZ roster, each server's CLAIMS.LOOKUP compared to jwtHash.
  distribution:
    servers: 3
    current: 3
    lastPushTime: "2026-09-25T16:20:03Z"

The account JWT expires its jwtTTL after it was signed, 48h here, and the auth controller re-signs it at half that. The auth controller is therefore an availability requirement: down for longer than half a jwtTTL, it may let the orders account expire, and the servers then close its connections. The gauge nats_operator.account.jwt_expiry says when that happens.

Users #

A user’s creds can land in a Secret shaped the way a NatsConnection reads it, so a connection needs only the Secret’s name. The controllers’ own system users take a permission preset instead of a permission list.

01-natsusers.yaml

apiVersion: auth.nats.mikluko.io/v1beta1
kind: NatsUser
metadata:
  name: orders-service
  namespace: nats-system
spec:
  accountRef:
    kind: NatsAccount
    name: orders
  permissions:
    publish:
      allow: ["orders.>", "$JS.API.>"]
    subscribe:
      allow: ["orders.>", "_INBOX.>"]
  # Optional. The Secret takes the shape a NatsConnection reads by default:
  # the creds under key `user.creds`, which `key` overrides.
  credentials:
    secretKeyRef:
      name: orders-service-creds
---
# Bring your own key: the client holds the seed, the auth controller signs a
# JWT for the public key and publishes it in status. No Secret is written, so
# `publicKey` and `credentials` are mutually exclusive.
apiVersion: auth.nats.mikluko.io/v1beta1
kind: NatsUser
metadata:
  name: orders-batch
  namespace: nats-system
spec:
  accountRef:
    kind: NatsAccount
    name: orders
  publicKey: UDXU4RCSJNZOIQHZNWXHXORDPRTGNJAHAHFRGZNEEJCPQTT2M7NLCNF4
  permissions:
    publish:
      allow: ["orders.batch.>"]
---
# What the JetStream controller acts as inside the orders account: an
# ordinary user, declared like any other.
apiVersion: auth.nats.mikluko.io/v1beta1
kind: NatsUser
metadata:
  name: orders-jetstream
  namespace: nats-system
spec:
  accountRef:
    kind: NatsAccount
    name: orders
  permissions:
    publish:
      allow: ["$JS.API.>"]
    subscribe:
      allow: ["_INBOX.>"]
  credentials:
    secretKeyRef:
      name: orders-jetstream-creds
---
# The cluster controller's own identity in this Kubernetes cluster. The
# preset expands to the subjects that controller calls; `permissions` would
# be refused beside it.
apiVersion: auth.nats.mikluko.io/v1beta1
kind: NatsUser
metadata:
  name: cluster-controller
  namespace: nats-system
spec:
  accountRef:
    kind: NatsSystemAccount
    name: sys
  preset: cluster-controller
  credentials:
    secretKeyRef:
      name: cluster-controller-creds
---
apiVersion: auth.nats.mikluko.io/v1beta1
kind: NatsUser
metadata:
  name: jetstream-controller
  namespace: nats-system
spec:
  accountRef:
    kind: NatsSystemAccount
    name: sys
  preset: jetstream-controller
  credentials:
    secretKeyRef:
      name: jetstream-controller-creds
---
apiVersion: auth.nats.mikluko.io/v1beta1
kind: NatsUser
metadata:
  name: auth-controller
  namespace: nats-system
spec:
  accountRef:
    kind: NatsSystemAccount
    name: sys
  preset: auth-controller
  credentials:
    secretKeyRef:
      name: auth-controller-creds
kubectl apply -f https://nats-operator.io/docs/stories/02-auth-plane/01-natsusers.yaml

The auth controller signs without a connection, and pushes to the servers through the one its chart value auth.systemConnection names, with its own system user’s creds:

01-natsconnection-auth-controller.yaml

# How the auth controller reaches the servers it pushes account JWTs to: the
# chart's `auth.systemConnection` names this connection,
# nats-system/auth-controller. Until it resolves, JWTs are signed and nothing
# reaches a server.
apiVersion: nats.mikluko.io/v1beta1
kind: NatsConnection
metadata:
  name: auth-controller
  namespace: nats-system
spec:
  servers: ["nats://demo.nats-system.svc:4222"]
  credentials:
    secretKeyRef:
      name: auth-controller-creds
kubectl apply -f https://nats-operator.io/docs/stories/02-auth-plane/01-natsconnection-auth-controller.yaml

A user that brings its own key gets only a signed JWT, in status:

01-status-natsuser-orders-batch.yaml

status:
  observedGeneration: 1
  conditions:
    - type: Ready
      status: "True"
      reason: Signed
  publicKey: UDXU4RCSJNZOIQHZNWXHXORDPRTGNJAHAHFRGZNEEJCPQTT2M7NLCNF4
  # Public and signed: the client pairs it with the seed it already holds.
  jwt: eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ...

The NATS cluster and the stream #

The NatsCluster gains an auth block. Its trust roots come through a trust object that points at the NATS operator, and its controller connects with its own system user’s creds.

01-natsoperatortrust.yaml

# Whose signatures the NATS cluster accepts. Here, in the same Kubernetes
# cluster as the NatsOperator, it points at the live resource: the auth
# controller writes the NATS operator and system account JWTs into this object's
# status, and the cluster controller reads them from there. Elsewhere the same
# kind carries the JWTs literally (story 6); `operatorRef` and the literal form
# are exclusive.
apiVersion: nats.mikluko.io/v1beta1
kind: NatsOperatorTrust
metadata:
  name: demo
  namespace: nats-system
spec:
  operatorRef:
    name: demo
kubectl apply -f https://nats-operator.io/docs/stories/02-auth-plane/01-natsoperatortrust.yaml

01-natscluster.yaml

# Story 1's NatsCluster, now under the NATS operator. Only `auth` is new.
apiVersion: cluster.nats.mikluko.io/v1beta1
kind: NatsCluster
metadata:
  name: demo
  namespace: nats-system
spec:
  version: 2.15.0
  replicas: 3
  resources:
    requests:
      cpu: "1"
      memory: 4Gi
    limits:
      memory: 4Gi
  jetstream:
    volumeClaimTemplate:
      spec:
        storageClassName: standard
        resources:
          requests:
            storage: 20Gi
  auth:
    # From the trust roots the cluster controller renders `operator`,
    # `system_account` and the resolver with the system account preloaded.
    trustRef:
      name: demo
    # What the cluster controller connects as for reloads and settledness.
    systemCredentials:
      secretKeyRef:
        name: cluster-controller-creds   # key defaults to user.creds
kubectl apply -f https://nats-operator.io/docs/stories/02-auth-plane/01-natscluster.yaml

Pointing the stream’s connection at one whose creds sign into the orders account creates a new, empty ORDERS stream in that account. Nothing deletes the stream story 1 created in the global account, or moves its messages.

01-natsconnection.yaml

# A connection is an identity as well as an address: its credentials decide
# the account every resource using it lands in. One per account the
# JetStream controller manages, plus one with system credentials for
# observation (story 7).
apiVersion: nats.mikluko.io/v1beta1
kind: NatsConnection
metadata:
  name: demo-orders
  namespace: nats-system
spec:
  servers: ["nats://demo.nats-system.svc:4222"]
  credentials:
    secretKeyRef:
      name: orders-jetstream-creds   # key defaults to user.creds
kubectl apply -f https://nats-operator.io/docs/stories/02-auth-plane/01-natsconnection.yaml

01-natsstream.yaml

# Story 1's stream with connectionRef pointing into the orders account;
# everything else is as story 1 declared it.
apiVersion: jetstream.nats.mikluko.io/v1beta1
kind: NatsStream
metadata:
  name: orders
  namespace: nats-system
spec:
  connectionRef:
    name: demo-orders
  adoptionPolicy: Never
  deletionPolicy: Retain
  terminalPolicy: Hold
  name: ORDERS
  subjects: ["orders.>"]
  storage: File
  replicas: 3
  retention: Limits
  maxAge: 72h
  maxBytes: 5Gi
kubectl apply -f https://nats-operator.io/docs/stories/02-auth-plane/01-natsstream.yaml

Connecting a client #

A client of the orders account connects with the creds the auth controller wrote for orders-service:

kubectl -n nats-system get secret orders-service-creds -o jsonpath='{.data.user\.creds}' | base64 -d > orders.creds
kubectl -n nats-system port-forward svc/demo 4222:4222 &
nats -s nats://localhost:4222 --creds orders.creds pub orders.created '{"id": 1}'
nats -s nats://localhost:4222 --creds orders.creds stream info ORDERS