2. Systemd Service

Save to /usr/lib/systemd/system/nats-server-hardened.service:

[Unit]
Description=NATS Server
After=network-online.target ntp.service

[Service]
Type=simple
EnvironmentFile=-/etc/default/nats-server
ExecStart=/usr/sbin/nats-server -c /etc/nats-server.conf
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s SIGUSR2 $MAINPID

User=nats
Group=nats

Restart=on-failure
RestartSec=5
TimeoutStopSec=150

LimitNOFILE=800000

CapabilityBoundingSet=
LockPersonality=true
MemoryDenyWriteExecute=true
NoNewPrivileges=true
PrivateDevices=true
PrivateTmp=true
PrivateUsers=true
ProcSubset=pid
ProtectClock=true
ProtectControlGroups=true
ProtectHome=true
ProtectHostname=true
ProtectKernelLogs=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectSystem=strict
ReadOnlyPaths=
RestrictAddressFamilies=AF_INET AF_INET6
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources
UMask=0077
InaccessiblePaths=/etc/ssh
ProtectProc=invisible
PrivateIPC=true

ReadWritePaths=/var/lib/nats

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now nats-server-hardened.service
sudo systemctl status nats-server-hardened.service

3. Server Configuration

/etc/nats-server.conf — single-node baseline with JetStream and monitoring:

server_name: "nats-prod-01"
host: "0.0.0.0"
port: 4222

max_connections: 65536
max_payload: 8388608   # 8 MB

logtime: true
debug: false
trace: false

http_port: 8222

jetstream {
    store_dir: "/var/lib/nats"
    max_mem:  2G
    max_file: 50G
}

# Authentication (uncomment for non-clustered single-node)
# authorization {
#   users: [
#     { user: "admin", password: "$2a$11$your_bcrypt_hash_here" }
#   ]
# }
sudo systemctl restart nats-server-hardened.service

NATS HTTP Monitoring Panel

NATS HTTP monitoring JSON endpoint response


4. Clustering

4.1 Set Hostnames (once per node)

hostnamectl set-hostname nats-prod-1
hostnamectl set-hostname nats-prod-2
hostnamectl set-hostname nats-prod-3

4.2 Append Cluster Block to /etc/nats-server.conf

Update server_name per node (nats-prod-1, nats-prod-2, nats-prod-3). Append:

cluster {
    name: "PRODUCTION-CLUSTER"   # Must be identical across all nodes
    listen: "0.0.0.0:6222"

    routes: [
        "nats://192.168.1.45:6222",
        "nats://192.168.1.46:6222",
        "nats://192.168.1.48:6222"
    ]
}
sudo systemctl restart nats-server-hardened.service

4.3 Verify Routes

curl -s http://localhost:8222/routez | grep -A 5 "routes"

5. Account & Security Model

Append to /etc/nats-server.conf on all nodes. Replaces any authorization {} block.

system_account: SYS

no_auth_user: anonymous

accounts {
    SYS {
        users: [
            { user: "vinz", password: "shamballa" }
        ]
    }

    APP {
        users: [
            { user: "anonymous" }
        ]
        jetstream: enabled
    }
}

Note: Replace plaintext passwords with BCrypt hashes in production.
Generate a hash: nats server passwd

sudo systemctl restart nats-server-hardened.service

6. CLI Contexts

nats context add prod-cluster \
    --server 127.0.0.1:4222 \
    --user vinz \
    --password shamballa \
    --select

nats context add app-dev \
    --server 127.0.0.1:4222 \
    --select
Task Context
nats server info prod-cluster
nats stream ls app-dev
nats kv / nats obj app-dev
nats context list
nats context select prod-cluster

Runbook7. Validation & Monitoring

# Server vitals
curl http://localhost:8222/varz

# JetStream stats
curl http://localhost:8222/jsz

# Cluster routes
curl -s http://localhost:8222/routez | grep -A 5 "routes"

# Server identity (admin context)
nats server info --context prod-cluster

NATS cluster routing table verification across nodes

NATS server cluster metadata and connection status

nats-top

curl -sf https://binaries.nats.dev/nats-io/nats-top@latest | sh
sudo mv nats-top /usr/local/bin/
nats-top

NATS CLI benchmark throughput test and latency stats


8. KV & Object Store

Distributed Key-Value Store

# Create a bucket with 5 revision history entries
nats kv add MY_CONFIG --history 5

# Write
nats kv put MY_CONFIG db_url "postgres://localhost:5432"

# Read
nats kv get MY_CONFIG db_url

# Watch for real-time changes
nats kv watch MY_CONFIG

NATS JetStream Key-Value store put and watch verification

Distributed Object Store

# Create a bucket
nats obj add SYSTEM_FILES

# Upload
nats obj put SYSTEM_FILES /etc/nats-server.conf

# List
nats obj ls SYSTEM_FILES

# Download
nats obj get SYSTEM_FILES /etc/nats-server.conf --output /tmp/downloaded-config.conf

NATS JetStream Object Store put and get verification


9. mTLS Hardening

9.1 Generate Certificates

mkdir -p /tmp/nats-certs && cd /tmp/nats-certs

Save as gen-certs.sh and run:

#!/bin/bash

# Root CA
openssl req -x509 -newkey rsa:4096 -days 3650 -nodes \
    -keyout ca-key.pem -out ca.pem \
    -subj "/O=NATS/CN=NATS-Root-CA"

# Server certificate with SANs for all cluster node IPs
openssl req -newkey rsa:4096 -nodes \
    -keyout server-key.pem -out server.csr \
    -subj "/O=NATS/CN=nats-server"

cat > extfile.cnf << EOF
subjectAltName = DNS:localhost, IP:127.0.0.1, IP:192.168.1.45, IP:192.168.1.46, IP:192.168.1.48
EOF

openssl x509 -req -in server.csr -days 3650 \
    -CA ca.pem -CAkey ca-key.pem -CAcreateserial \
    -out server.pem -extfile extfile.cnf

# Client certificate (admin)
openssl req -newkey rsa:4096 -nodes \
    -keyout client-key.pem -out client.csr \
    -subj "/O=NATS/CN=vinz-admin"

openssl x509 -req -in client.csr -days 3650 \
    -CA ca.pem -CAkey ca-key.pem -CAcreateserial \
    -out client.pem

9.2 Deploy Certificates (all nodes)

sudo mkdir -p /etc/nats/certs
sudo cp /tmp/nats-certs/ca.pem /tmp/nats-certs/server*.pem /etc/nats/certs/
sudo chown -R nats:nats /etc/nats/certs
sudo chmod 400 /etc/nats/certs/*key.pem

9.3 Update Server Configuration

Replace the cluster and base config on each node:

server_name: "nats-prod-1"   # Change per node
host: "0.0.0.0"
port: 4222
http_port: 8222

# Client mTLS
tls {
    ca_file:   "/etc/nats/certs/ca.pem"
    cert_file: "/etc/nats/certs/server.pem"
    key_file:  "/etc/nats/certs/server-key.pem"
    verify:    true   # Enforces mutual TLS for all clients
    timeout:   2.0
}

cluster {
    name: "PRODUCTION-CLUSTER"
    listen: "0.0.0.0:6222"

    tls {
        ca_file:   "/etc/nats/certs/ca.pem"
        cert_file: "/etc/nats/certs/server.pem"
        key_file:  "/etc/nats/certs/server-key.pem"
        verify:    true
        timeout:   5.0
    }

    routes: [
        "tls://192.168.1.45:6222",
        "tls://192.168.1.46:6222",
        "tls://192.168.1.48:6222"
    ]
}
sudo systemctl restart nats-server-hardened.service

9.4 Update CLI Context for mTLS

nats context save prod-cluster \
    --server tls://127.0.0.1:4222 \
    --user vinz \
    --password shamballa \
    --tlscert /tmp/nats-certs/client.pem \
    --tlskey /tmp/nats-certs/client-key.pem \
    --tlsca  /tmp/nats-certs/ca.pem \
    --select

nats context save app-dev \
	--server tls://127.0.0.1:4222 \
	--tlscert /tmp/nats-certs/client.pem \
	--tlskey /tmp/nats-certs/client-key.pem \
	--tlsca /tmp/nats-certs/ca.pem \
	--select

nats server info

NATS mutual TLS encrypted communication handshake confirmation


At this point we have a fully operational 3-node NATS cluster with JetStream enabled across all nodes, mutual TLS enforced for both client connections and inter-node routing, account-based isolation between system and application traffic, and persistent KV and object store backed by a dedicated data volume. The cluster is managed via named CLI contexts and exposed through a hardened systemd service unit.