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.yaml01-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.yamlAn 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.yaml02-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.yamlThe 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.yamlA 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.yaml01-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.yamlPointing 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.yaml01-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.yamlConnecting 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