Skip to content
infra Artifact [b7c6d559e7]
Login

Artifact [b7c6d559e7]

Artifact b7c6d559e750d21640782e02e640f02de1b3b1e16f4dcfbc28a369380ce84fd9:

Wiki page [NATS Server — Operations Runbook] by dmestas on 2026-03-15 20:21:57.
D 2026-03-15T20:21:57.866
L NATS\sServer\s—\sOperations\sRunbook
N text/x-markdown
U dmestas
W 5736
# NATS Server — Operations Runbook

Day-to-day operations for the NATS server on `vps-nbg1`.
See [NATS Server — Setup & Architecture](NATS Server — Setup & Architecture) for the full design.

---

## Service Management

```sh
# Status
sudo systemctl status nats

# Start / stop / restart
sudo systemctl start nats
sudo systemctl stop nats
sudo systemctl restart nats

# Reload TLS cert only (no restart needed after renewal)
sudo systemctl kill --signal=SIGHUP nats

# Logs (live)
sudo journalctl -u nats -f

# Logs (last 100 lines)
sudo journalctl -u nats -n 100 --no-pager
```

---

## Verify Health

```sh
CREDS=~/Projects/infra/nats/creds/edgesync-admin.creds
SERVER=tls://sync.craftdesign.group:4222

# List streams
nats stream ls --server $SERVER --creds $CREDS

# Stream details
nats stream info EDGESYNC --server $SERVER --creds $CREDS

# Publish a test message
nats pub fossil.edgesync.smoke "test" --server $SERVER --creds $CREDS

# Subscribe (run in a second terminal)
nats sub 'fossil.edgesync.>' --server $SERVER --creds $CREDS
```

---

## TLS Certificate Renewal

Certificates are issued by Let's Encrypt via acme.sh DNS-01 (Cloudflare).
acme.sh installs a cron job under root that auto-renews 30 days before expiry.
On renewal, it runs `systemctl kill --signal=SIGHUP nats` to reload the cert live.

### Manual renewal

```sh
source /etc/nats/cf-token.env   # sets CF_Token (root only)
export CF_Token
/root/.acme.sh/acme.sh --renew -d sync.craftdesign.group --ecc --server letsencrypt
```

### Check cert expiry

```sh
openssl x509 -in /var/nats/tls/sync.craftdesign.group.crt -noout -dates
```

---

## Adding a New Account

Run as `dmestas`:

```sh
ACCOUNT=my-new-account

nsc add account --name "$ACCOUNT"
nsc edit account --name "$ACCOUNT" --js-tier 0 \
  --js-streams -1 --js-consumer -1 --js-disk-storage -1 --js-mem-storage -1

# Add users
nsc add user --account "$ACCOUNT" --name admin --allow-pub '>' --allow-sub '>'
nsc add user --account "$ACCOUNT" --name bridge \
  --allow-pub  "fossil.${ACCOUNT}.>" \
  --allow-sub  "fossil.${ACCOUNT}.>"

# Export creds
nsc generate creds --account "$ACCOUNT" --name admin  > ~/Projects/infra/nats/creds/${ACCOUNT}-admin.creds
nsc generate creds --account "$ACCOUNT" --name bridge > ~/Projects/infra/nats/creds/${ACCOUNT}-bridge.creds
chmod 0600 ~/Projects/infra/nats/creds/${ACCOUNT}-*.creds

# Copy updated account JWT to resolver dir (requires public key)
PUBKEY=$(nsc describe account --name "$ACCOUNT" -R 2>/dev/null | grep 'Account ID' | awk '{print $NF}')
sudo cp ~/.local/share/nats/nsc/stores/fossil-sync/accounts/${ACCOUNT}/${ACCOUNT}.jwt \
        /var/nats/jwt/${PUBKEY}.jwt
sudo chown nats:nats /var/nats/jwt/${PUBKEY}.jwt

# Restart to pick up new account
sudo systemctl restart nats
```

Then create a JetStream stream for the account using `provision-streams.sh` as a template.

---

## Adding a Second Node (Clustering)

The config template has a commented `cluster {}` block. When adding a second node:

1. Uncomment and fill in the cluster block in `nats-server.conf.template`
2. Change `--js-tier 0` to `--js-tier 1` or `--js-tier 3` in account JWTs (R1/R3 replication)
3. Copy the operator keys and account JWTs to the second node
4. Re-run start.sh on both nodes

---

## Known Gotchas

These were found during the initial live install (2026-03-15):

| Issue | Root cause | Fix |
|-------|-----------|-----|
| `CF_Token` not passed to acme.sh subprocess | `source` doesn't export | `export CF_Token` after source |
| ZeroSSL returns 24h retry-after | Default CA rate limit | Use `--server letsencrypt` |
| `nsc list operators` output piped to grep silently fails | nsc writes to stderr | Pipe `2>&1` not `2>/dev/null` |
| Same, but grep skips matches | ANSI color codes in output | Strip with `sed 's/\x1b\[[0-9;]*m//g'` |
| `nsc generate config` fails: "system account not set" | Must create SYS account first | `nsc add account --name SYS && nsc edit operator --system-account SYS` |
| `nsc push` fails: scheme "tls" not supported | nsc push only accepts nats/http/https | Use file-copy approach instead |
| `nats stream add --max-msgs -1` fails | `-1` parsed as a flag | Use `--max-msgs=-1` (equals notation) |
| JetStream not enabled for account (10039) | `--js-tier 1` requires clustered server for R1 | Use `--js-tier 0` (global tier) on single node |
| Account JWT changes not picked up | SIGHUP only reloads TLS certs | `systemctl restart nats` after JWT changes |
| `system_account: ""` in startup log | Cosmetic only — server loads account on-demand | No fix needed; add `system_account: <key>` to conf for clarity |

---

## Credentials Map

| File | Account | User | Permissions |
|------|---------|------|-------------|
| `creds/edgesync-admin.creds` | edgesync | admin | pub/sub `>` |
| `creds/edgesync-bridge.creds` | edgesync | bridge | pub/sub `fossil.edgesync.>` |
| `creds/go-libfossil-admin.creds` | go-libfossil | admin | pub/sub `>` |
| `creds/go-libfossil-bridge.creds` | go-libfossil | bridge | pub/sub `fossil.go-libfossil.>` |

Creds files are in `~/Projects/infra/nats/creds/` (gitignored / fossil-ignored).
The operator signing keys are in `~/.local/share/nats/nsc/keys/` — back these up.

---

## Secrets Locations

| Secret | Location | Owner | Mode |
|--------|---------|-------|------|
| Cloudflare API token | `/etc/nats/cf-token.env` | root | 0600 |
| Operator JWT | `/etc/nats/operator.env` | root | 0600 |
| nsc key store | `~/.local/share/nats/nsc/keys/` | dmestas | 0700 |
| TLS private key | `/var/nats/tls/sync.craftdesign.group.key` | nats | 0640 |

**Back up the nsc key store.** Loss of the operator signing key means you cannot
issue new accounts or users and must rebuild the PKI from scratch.

Z cacb5326285bf8c2c7e50e874a91b329