.
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