Skip to content

NATS JetStream Design

Date: 2026-01-04 Status: Approved Issue: #497

Deploy NATS with JetStream as a general messaging backbone for the cluster, with IoT/Home Assistant integration as a future consumer.

  1. Messaging foundation — Central pub/sub and persistence layer for cluster services
  2. High availability — 3-node cluster with JetStream replication
  3. Security — NKey authentication, TLS encryption, Vault-managed credentials
  4. Observability — Prometheus metrics, Grafana dashboard, Loki logs
┌─────────────────────────────────────────────────────────────┐
│ NATS 3-Node Cluster │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ nats-0 │◄──►│ nats-1 │◄──►│ nats-2 │ (Raft consensus)│
│ │ │ │ │ │ │ │
│ │JetStream│ │JetStream│ │JetStream│ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ │
│ │Longhorn │ │Longhorn │ │Longhorn │ (encrypted) │
│ │ PVC │ │ PVC │ │ PVC │ │
│ └─────────┘ └─────────┘ └─────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────┐ ┌─────────────────────┐
│ nats.nats.svc │ │ Vault │
│ (ClusterIP) │ │ - NKey seeds │
│ Port 4222 (client) │ │ - Account JWTs │
│ Port 6222 (cluster)│ │ - Server config │
│ Port 7777 (metrics)│ │ │
└─────────────────────┘ └─────────────────────┘
ComponentChoice
Namespacenats
Helm chartnats/nats v2.12.3 (latest stable)
Replicas3 (HA cluster)
Storage10GB longhorn-encrypted per node
AuthNKey with NATS Accounts
TLScert-manager, cluster-ca-issuer (internal CA)
SecretsVault + ExternalSecret
OPERATOR: fzymgc-house
└── ACCOUNT: SYS (system account - monitoring)
└── ACCOUNT: SERVICES (internal cluster services)
└── ACCOUNT: IOT (future: Home Assistant, MQTT bridge)

Single secret with properties (matches repository conventions):

secret/fzymgc-house/cluster/nats
├── operator_seed # Operator NKey seed (private)
├── operator_public # Operator public key
├── sys_account_seed # SYS account NKey seed
├── sys_account_jwt # SYS account signed JWT
├── services_account_seed # SERVICES account NKey seed
├── services_account_jwt # SERVICES account signed JWT
├── iot_account_seed # IOT account NKey seed
├── iot_account_jwt # IOT account signed JWT
└── resolver_config # Account resolver config
# Policy: nats-server
path "secret/data/fzymgc-house/cluster/nats" {
capabilities = ["read"]
}
path "secret/metadata/fzymgc-house/cluster/nats" {
capabilities = ["read"]
}

Generate credentials using the nsc CLI (one-time setup):

Terminal window
# Install nsc
brew install nats-io/nats-tools/nsc # or go install github.com/nats-io/nsc/v2@latest
# Create operator and accounts
nsc init --name fzymgc-house
nsc add account SYS
nsc add account SERVICES
nsc add account IOT
# Export credentials for Vault
nsc describe operator -J > /tmp/operator.jwt
nsc describe account SYS -J > /tmp/sys.jwt
# ... repeat for other accounts
# Store in Vault (manual or via tf/vault module)
vault kv put secret/fzymgc-house/cluster/nats \
operator_seed="$(nsc keys --operator --private)" \
operator_public="$(nsc keys --operator)" \
sys_account_seed="$(nsc keys --account SYS --private)" \
sys_account_jwt="$(cat /tmp/sys.jwt)" \
# ... additional properties

Note: Operator seed is root of trust. Storing in Vault is acceptable for automation but consider security implications for production environments.

  • Default deny ingress to nats namespace
  • Allow from namespaces with label nats-client: "true"
  • Allow cluster-internal traffic between NATS pods

Initial client namespaces: None (no consumers yet per YAGNI)

network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: nats-ingress
namespace: nats
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: nats
policyTypes:
- Ingress
ingress:
# Allow NATS cluster traffic (pod-to-pod)
- from:
- podSelector:
matchLabels:
app.kubernetes.io/name: nats
ports:
- port: 6222
# Allow clients from labeled namespaces
- from:
- namespaceSelector:
matchLabels:
nats-client: "true"
ports:
- port: 4222
# Allow Prometheus scraping
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
ports:
- port: 7777

Application manifest: argocd/cluster-app/templates/nats.yaml (References the app-config below, follows app-of-apps pattern)

App configuration:

argocd/app-configs/nats/
├── kustomization.yaml
├── namespace.yaml
├── helm-release.yaml # Helm chart reference
├── values.yaml # Helm values
├── external-secret.yaml # NKeys from Vault
├── certificate.yaml # TLS cert via cert-manager
├── network-policy.yaml # Ingress rules
└── prometheus-rules.yaml # Alert rules
cluster:
enabled: true
replicas: 3
jetstream:
enabled: true
domain: fzymgc
memoryStore:
maxSize: 256Mi
fileStore:
pvc:
size: 10Gi
storageClassName: longhorn-encrypted
natsBox:
enabled: true # Debugging/admin pod
config:
cluster:
tls:
enabled: true
ResourcePer PodTotal (3 pods)
CPU request100m300m
CPU limit500m1500m
Memory request256Mi768Mi
Memory limit512Mi1536Mi
Storage10Gi30Gi
  • NATS exposes Prometheus metrics on port 7777
  • ServiceMonitor scrapes /metrics endpoint
  • Key metrics: connections, messages in/out, JetStream stream/consumer stats
  • Use official NATS dashboard (ID: 14605) as base
  • Deploy as ConfigMap to argocd/app-configs/grafana/dashboards/nats.yaml
  • Dashboard will be auto-imported via Grafana sidecar

Deploy as PrometheusRule CRD in argocd/app-configs/nats/prometheus-rules.yaml:

Critical:

  • JetStream storage > 80% capacity
  • Cluster lost quorum (< 2 nodes healthy)
  • No leader elected for > 30s

Warning:

  • Message delivery failures
  • Slow consumers (pending > threshold)
  • Connection errors spike
  • NATS logs collected by Alloy (existing infrastructure)
  • Label: app=nats
  • MQTT bridge — Add when Home Assistant deploys (#496)
  • External ingress — Internal-only for now
  • Leafnodes — Single cluster sufficient
  • WebSocket gateway — No browser clients yet
  1. Verify storage class: kubectl get sc longhorn-encrypted
  2. Generate NKey credentials using nsc CLI (see Initial NKey Setup)
  3. Create Vault secrets (operator, accounts, server config)
  4. Create Vault policy and Kubernetes auth role
  5. Update docs/reference/secrets.md with NATS secret path
  6. Deploy ArgoCD application
  7. Verify cluster health and JetStream (see Verification table)
  8. Deploy Grafana dashboard ConfigMap
  9. Deploy PrometheusRule for alerts
  10. Update docs/reference/services.md with NATS entry
TestCommand/ActionExpected
Storage classkubectl get sc longhorn-encryptedStorageClass exists
Cluster healthynats-box: nats server checkAll nodes connected
Cluster quorumnats-box: nats server report jetstream3/3 nodes, leader elected
JetStream enablednats stream lsEmpty list, no errors
TLS workingnats server info --tlsTLS connection succeeds
Auth workingConnect without credsConnection refused
Account listnats-box: nats account infoShows configured account
Metrics exposedcurl :7777/metricsPrometheus metrics
ServiceMonitorCheck Prometheus targetsNATS target is Up
PersistenceCreate stream, delete pod, verifyStream retained after pod restart
  1. Delete ArgoCD application
  2. PVCs retained by default (manual cleanup if needed)
  3. Vault secrets retained (no automatic cleanup)