README
ΒΆ
NATS Authentication Operator
A Kubernetes operator for managing NATS authentication using JWT and token-based authentication. This operator provides a modern, declarative way to manage NATS users and accounts as Kubernetes custom resources.
Note: This operator manages only authentication - it does NOT manage NATS clusters, streams, or consumers. Use the NATS Helm chart for cluster management and NACK for JetStream resource management.
Features
Authentication Modes
- JWT-based authentication - Full NATS Operator β Accounts β Users hierarchy with automatic JWT generation
- Token-based authentication - Simple username/password credentials
- JetStream support - Built-in JetStream limits configuration for accounts
Key Capabilities
- π Automatic credential generation - Operator keypairs, account JWTs, user JWTs, and
.credsfiles - π― Fine-grained permissions - Publish/subscribe controls with allow/deny lists
- π Seamless NATS Helm integration - Works alongside official NATS Helm chart without conflicts
- π¦ Kubernetes-native - Manage authentication using
kubectland GitOps workflows - π‘οΈ Production-ready - Idempotent reconciliation, finalizers, status conditions, secure secret storage
- β‘ Automatic change propagation - Spec changes (permissions, limits) are detected and re-applied on the next reconcile
- π Force-sync annotation - Trigger an immediate reconcile on any resource without waiting
Quick Start
Installation
Using Helm (Recommended)
helm install nats-auth-operator oci://ghcr.io/jradikk/charts/nats-auth-operator \
--version 0.1.0 \
--namespace nats-system \
--create-namespace
Using kubectl
kubectl apply -f https://raw.githubusercontent.com/jradikk/nats-auth-operator/main/dist/install.yaml
Basic Usage
- Create an auth configuration:
apiVersion: nats.jradikk/v1alpha1
kind: NatsAuthConfig
metadata:
name: main
namespace: default
spec:
natsURL: "nats://nats.default.svc.cluster.local:4222"
mode: jwt
serverAuthConfig:
name: "nats-auth-jwts"
namespace: "default"
type: "Secret"
jwt:
operatorName: "MyOperator"
- Create accounts:
apiVersion: nats.jradikk/v1alpha1
kind: NatsAccount
metadata:
name: myapp-account
namespace: default
spec:
authConfigRef:
name: main
description: "Application account with JetStream"
limits:
conn: 100
subs: 1000
jetstream:
memoryStorage: -1 # Unlimited
diskStorage: -1
streams: -1
consumer: -1
- Create users:
apiVersion: nats.jradikk/v1alpha1
kind: NatsUser
metadata:
name: app-user
namespace: default
spec:
authConfigRef:
name: main
authType: jwt
accountRef:
name: myapp-account
username: "app-user"
permissions:
publishAllow:
- "app.>"
- "$JS.ACK.>"
- "_INBOX.>"
subscribeAllow:
- "app.>"
- "$JS.API.>"
- "_INBOX.>"
- Use the generated credentials:
The operator creates a Secret app-user-user-creds containing:
user.creds- NATS credentials file (JWT + seed)user.jwt- User JWTNATS_URL- NATS server URL
Mount this secret in your pod:
apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
containers:
- name: app
image: myapp:latest
volumeMounts:
- name: nats-creds
mountPath: /var/run/nats
readOnly: true
env:
- name: NATS_CREDS_FILE
value: /var/run/nats/user.creds
volumes:
- name: nats-creds
secret:
secretName: app-user-user-creds
Architecture
Custom Resource Definitions (CRDs)
-
NatsAuthConfig - Global authentication configuration
- Defines authentication mode (JWT/token)
- Specifies NATS server URL
- References where to store generated credentials
-
NatsAccount - NATS account (JWT mode only)
- Defines account limits (connections, subscriptions, payload size)
- Configures JetStream limits (storage, streams, consumers)
- Generates account JWT signed by operator
-
NatsUser - NATS user
- Defines user permissions (publish/subscribe allow/deny lists)
- Generates user JWT or username/password credentials
- Creates Kubernetes Secret with credentials
How It Works
βββββββββββββββββββββββ
β NatsAuthConfig β Creates operator keypair
β (JWT mode) β Stores in Secret
ββββββββββββ¬βββββββββββ
β
βββββββββββββββββββββ
β β
βββββββββΌβββββββββ ββββββββΌββββββββ
β NatsAccount β β NatsAccount β Generate account
β (system) β β (myapp) β JWTs, store in
βββββββββ¬βββββββββ ββββββββ¬ββββββββ individual Secrets
β β
β βββββββββββ΄βββββββββββ
β β β
βββββΌβββββ ββββΌββββββ βββββββΌβββββ
β User β β User β ... β User β Generate user
β (u1) β β (u2) β β (un) β JWTs + creds
ββββββββββ ββββββββββ ββββββββββββ files
β β β
βββββββββββ΄ββββββββββββββββββββ
β
ββββββββββββΌββββββββββββ
β nats-auth-jwts β Aggregated Secret
β Secret β referenced by NATS
β - operator JWT β Helm chart
β - system-account β
β - myapp-account β
ββββββββββββββββββββββββ
Integration with NATS Helm Chart
The operator is designed to work seamlessly with the official NATS Helm chart.
Deploy NATS
After the operator has reconciled and nats-auth-jwts Secret exists:
helm repo add nats https://nats-io.github.io/k8s/helm/charts/
helm upgrade --install nats nats/nats -f examples/nats-values.yaml
See examples/nats-values.yaml for a complete working Helm values file.
Key fields to configure in your values:
config:
merge:
operator: << $NATS_OPERATOR_JWT >>
system_account: "<main-system-account public key>"
resolver_preload:
"<main-system-account public key>": << $NATS_SYSTEM_ACCOUNT_JWT >>
container:
env:
NATS_OPERATOR_JWT:
valueFrom:
secretKeyRef:
name: nats-auth-jwts
key: operator
NATS_SYSTEM_ACCOUNT_JWT:
valueFrom:
secretKeyRef:
name: nats-auth-jwts
key: main-system-account # key = <authconfig-name>-system-account
Get the system account public key after the operator reconciles:
kubectl get natsaccount main-system-account -o jsonpath='{.status.accountId}'
No conflicts: the operator manages credentials (Secrets), the NATS Helm chart manages the server (ConfigMaps, StatefulSet). Each owns distinct resources.
Examples
See the examples/ directory for complete examples:
jwt-auth/- JWT-based authentication with JetStreamtoken-auth/- Token-based authentication
JetStream Permissions
When using JetStream, users need specific permissions:
For JetStream Consumers:
publishAllow:
- "$JS.ACK.<stream-name>.>" # Acknowledge messages
- "$JS.API.>" # JetStream API calls
- "_INBOX.>" # Request-response
subscribeAllow:
- "your.subjects.>" # Stream subjects
- "$JS.API.>" # JetStream API
- "_INBOX.>" # Message delivery
For Stream-Specific Access:
publishAllow:
- "$JS.ACK.mystream.>"
- "$JS.API.CONSUMER.*.mystream.>"
subscribeAllow:
- "$JS.API.CONSUMER.*.mystream.>"
- "$JS.API.STREAM.INFO.mystream"
- "_INBOX.>"
Force-Sync
Annotate any resource with nats.jradikk/force-sync to trigger an immediate full reconcile, bypassing the idempotency guards. The annotation is removed automatically after processing.
# Force re-generate a user JWT (e.g., after manually editing permissions)
kubectl annotate NatsUser my-user nats.jradikk/force-sync=$(date +%s) --overwrite
# Force re-generate an account JWT (e.g., after changing limits)
kubectl annotate NatsAccount my-account nats.jradikk/force-sync=$(date +%s) --overwrite
# Force NatsAuthConfig to re-collect and re-publish all account JWTs
kubectl annotate NatsAuthConfig my-config nats.jradikk/force-sync=$(date +%s) --overwrite
Under normal operation this is not needed β spec changes are detected automatically via the resource
generation. Use force-sync when you have edited secrets manually or need to verify the operator is in sync.
Troubleshooting
Account IDs Keep Changing
Problem: Account IDs rotate constantly, causing authentication failures.
Cause: Infinite reconciliation loop due to account status/secret mismatch.
Solution: The operator now verifies that the account ID in status matches the seed in the secret. Rebuild and redeploy the operator.
"JetStream not enabled for account" Error
Problem: JetStream operations fail with error code 10039.
Solution: Add JetStream limits to your NatsAccount:
limits:
jetstream:
memoryStorage: -1
diskStorage: -1
streams: -1
consumer: -1
Even with all values set to -1 (unlimited), the presence of the jetstream section enables JetStream for the account.
"Authorization Violation" with JetStream
Problem: Client gets "Authorization Violation" when using JetStream consumers.
Possible Causes:
-
Missing publish permissions for acknowledgments:
publishAllow: - "$JS.ACK.>" # Required! - "_INBOX.>" # Required! -
Missing subscribe permissions for JetStream API:
subscribeAllow: - "$JS.API.>" # Required! - "_INBOX.>" # Required! -
Credentials not mounted correctly:
- Ensure secret is mounted as a file
- Application must use
user.credsfile, notuser.jwt - Check NATS server logs for "authentication error"
Secret Missing Account JWT
Problem: The main Secret (e.g., nats-auth-jwts) is missing an account JWT.
Solution:
-
Check if NatsAccount is Ready:
kubectl get natsaccounts -
Check if individual account Secret exists:
kubectl get secret <account-name>-account-jwt -
Trigger reconciliation with the force-sync annotation:
kubectl annotate natsauthconfig main nats.jradikk/force-sync=$(date +%s) --overwriteThe operator will re-collect all account JWTs and rewrite the Secret.
Account ID Mismatch Between JWT and Status
Problem: The account public key in the JWT doesn't match the status.
Solution: Delete the account JWT secret to force regeneration:
kubectl delete secret <account-name>-account-jwt
The operator will regenerate the JWT using the existing seed.
User Credentials Keep Regenerating
Problem: User credentials change on every reconciliation.
Cause: status.observedGeneration is out of sync with the actual resource generation, causing the controller to treat every reconcile as a spec change.
Solution: Check whether the status is being updated correctly. If the problem persists, delete and recreate the user resource to reset the generation counter.
NATS Server Shows "authentication error"
Problem: NATS logs show authentication errors despite correct JWT.
Possible Causes:
- JWT signed by wrong account - Check issuer in JWT matches account ID
- Stale credentials - Application using old credentials file
- Account not in operator's resolver - Check main Secret has the account JWT
Debug:
# Check what's in the main Secret
kubectl get secret nats-auth-jwts -o jsonpath='{.data}' | jq 'keys'
# Decode and inspect a JWT
kubectl get secret <user>-user-creds -o jsonpath='{.data.user\.jwt}' | base64 -d
Development
Prerequisites
- Go 1.24+
- Kubernetes cluster (kind, minikube, or real cluster)
- kubectl
- Docker or Podman
Building
# Build the operator
make build
# Build Docker image
make docker-build IMG=myregistry/nats-auth-operator:latest
# Push Docker image
make docker-push IMG=myregistry/nats-auth-operator:latest
Running Locally
# Install CRDs
make install
# Run operator locally (against your current kubectl context)
make run
Testing
# Run tests
make test
# Run with coverage
make test-coverage
Contributing
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests if applicable
- Submit a pull request
License
Apache 2.0 License. See LICENSE for details.
Acknowledgments
- Built with Kubebuilder
- NATS JWT library: nats-io/jwt
- NATS nkeys library: nats-io/nkeys
Documentation
ΒΆ
There is no documentation for this package.
Directories
ΒΆ
| Path | Synopsis |
|---|---|
|
api
|
|
|
v1alpha1
Package v1alpha1 contains API Schema definitions for the nats v1alpha1 API group +kubebuilder:object:generate=true +groupName=nats.jradikk
|
Package v1alpha1 contains API Schema definitions for the nats v1alpha1 API group +kubebuilder:object:generate=true +groupName=nats.jradikk |
|
internal
|
|