Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,21 +121,22 @@ Connect two agents in 3 commands. No JSON editing.
### Prerequisites

- **Node.js 22+** (uses built-in `node:sqlite`)
- **NATS server**:
```bash
docker run -d --name nats -p 4222:4222 nats:2.10-alpine -js --auth YOUR_SECRET
```
- **TLS-required NATS server** with one subject-scoped user per agent. See
[`docs/nats-transport-security.md`](docs/nats-transport-security.md) for the
server policy and isolated integration proof.

### Step 1 — Host generates invite

```bash
git clone https://github.com/alexfrmn/murmur.git && cd mur-mur-v2
npm install && npm run build

AGENT_ID=alice NATS_URL=nats://your-server:4222 NATS_TOKEN=YOUR_SECRET \
AGENT_ID=alice NATS_URL=tls://your-server:4222 \
NATS_USER=alice NATS_PASSWORD=ALICE_SECRET NATS_CA_FILE=/secure/nats-ca.pem \
node scripts/agent-config-init.mjs

node scripts/murmur-invite.mjs
MURMUR_INVITE_NATS_USER=bob MURMUR_INVITE_NATS_PASSWORD=BOB_SECRET \
node scripts/murmur-invite.mjs
# → Prints MURMUR:eyJ... blob — send it to your peer via any channel
```

Expand All @@ -145,8 +146,7 @@ node scripts/murmur-invite.mjs
git clone https://github.com/alexfrmn/murmur.git && cd mur-mur-v2
npm install && npm run build

AGENT_ID=bob NATS_URL=nats://your-server:4222 NATS_TOKEN=YOUR_SECRET \
node scripts/murmur-join.mjs 'MURMUR:eyJ...'
AGENT_ID=bob node scripts/murmur-join.mjs 'MURMUR:eyJ...'
# → Prints MURMUR-REPLY:eyJ... blob — send it back to host
```

Expand Down
2 changes: 1 addition & 1 deletion deploy/docker-compose.messaging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ services:
# JetStream is enabled for durable broker delivery; Murmur daemons still keep SQLite outbox durability locally.
command: ["-js", "-m", "8222", "--auth", "${NATS_TOKEN}"]
ports:
- "4222:4222"
- "127.0.0.1:4222:4222"
- "127.0.0.1:8222:8222"
restart: unless-stopped
18 changes: 12 additions & 6 deletions deploy/kubernetes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ Helm chart.
- `Namespace/murmur`
- `StatefulSet/murmur-nats` with JetStream storage
- `Service/murmur-nats` exposing NATS inside the cluster
- `Secret/murmur-nats-auth` with the NATS token placeholder
- `Secret/murmur-nats-auth` with a per-agent user and bcrypt password hash
- `Secret/murmur-nats-tls` with the server certificate/key and client CA
- `StatefulSet/murmur-agent` with a per-agent SQLite PVC
- `Service/murmur-agent` exposing the optional Prometheus exporter in-cluster
- `Secret/murmur-agent-config` with an example `agent-config.json`

Presence/discovery metadata is public by design, but `agent-config.json` still
contains private keys and the NATS token. Keep it in a secret manager or sealed
contains private keys and a NATS client password. Keep it in a secret manager or sealed
secret in real deployments.

## Build The Daemon Image
Expand All @@ -31,8 +32,9 @@ Update `deploy/kubernetes/agent-daemon.yaml` with your image name.

Edit these placeholders before applying:

- `deploy/kubernetes/nats.yaml`: `CHANGE_ME_NATS_TOKEN`
- `deploy/kubernetes/agent-config.example.yaml`: `agentId`, keys, peers, and token
- `deploy/kubernetes/nats.yaml`: TLS PEM values, user, and bcrypt password hash
- `deploy/kubernetes/agent-config.example.yaml`: `agentId`, keys, peers, and the
matching distinct plaintext client password

The example also exposes the streaming delivery knobs used by the daemon:

Expand All @@ -48,7 +50,8 @@ For real clusters, prefer generating these from your secret manager:

```bash
kubectl -n murmur create secret generic murmur-nats-auth \
--from-literal=NATS_TOKEN="$NATS_TOKEN" \
--from-literal=NATS_USER="$NATS_USER" \
--from-literal=NATS_PASSWORD_BCRYPT="$NATS_PASSWORD_BCRYPT" \
--dry-run=client -o yaml

kubectl -n murmur create secret generic murmur-agent-config \
Expand All @@ -67,7 +70,7 @@ kubectl -n murmur rollout status statefulset/murmur-agent
The in-cluster NATS URL for agents is:

```text
nats://murmur-nats.murmur.svc.cluster.local:4222
tls://murmur-nats.murmur.svc.cluster.local:4222
```

For multiple agents, create one config secret and one agent StatefulSet per
Expand All @@ -80,6 +83,9 @@ agent, or keep this directory as a base and add per-agent Kustomize overlays.
- The agent StatefulSet runs `scripts/prometheus-exporter.mjs` as a sidecar on
port `9464`; remove the sidecar and `murmur-agent` Service if you do not need
metrics.
- This reference requires TLS and one subject-scoped user. Replace every
placeholder before applying it; the example TLS secret is intentionally not a
usable certificate. See `docs/nats-transport-security.md`.
- This reference does not expose NATS outside the cluster. Use an ingress,
LoadBalancer, VPN, or NATS leaf-node topology only after setting explicit
authz boundaries.
Expand Down
13 changes: 4 additions & 9 deletions deploy/kubernetes/agent-config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,11 @@ stringData:
agent-config.json: |
{
"agentId": "agent-k8s-demo",
"natsUrl": "nats://murmur-nats.murmur.svc.cluster.local:4222",
"natsToken": "CHANGE_ME_NATS_TOKEN",
"natsUrl": "tls://murmur-nats.murmur.svc.cluster.local:4222",
"natsUser": "agent-k8s-demo",
"natsPassword": "CHANGE_ME_DISTINCT_CLIENT_PASSWORD",
"natsTls": { "caFile": "/etc/murmur/nats/ca.crt" },
"subject": "msg.agent-k8s-demo",
"jetstream": {
"enabled": true,
"stream": "MURMUR",
"subjects": ["msg.>", "ack.>"],
"maxDeliver": 5,
"ackWaitMs": 30000
},
"streaming": {
"ackTimeoutMs": 15000,
"ackWindow": {
Expand Down
9 changes: 7 additions & 2 deletions deploy/kubernetes/agent-daemon.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ spec:
value: /data
- name: FLUSH_INTERVAL_MS
value: "2000"
- name: MURMUR_JETSTREAM
value: "1"
- name: MURMUR_JETSTREAM_MAX_DELIVER
value: "5"
- name: MURMUR_JETSTREAM_ACK_WAIT_MS
Expand Down Expand Up @@ -71,6 +69,10 @@ spec:
mountPath: /data/agent-config.json
subPath: agent-config.json
readOnly: true
- name: nats-ca
mountPath: /etc/murmur/nats/ca.crt
subPath: ca.crt
readOnly: true
- name: metrics
image: ghcr.io/acme/murmur-v2-daemon:2.2.0
imagePullPolicy: IfNotPresent
Expand Down Expand Up @@ -101,6 +103,9 @@ spec:
- name: agent-config
secret:
secretName: murmur-agent-config
- name: nats-ca
secret:
secretName: murmur-nats-tls
volumeClaimTemplates:
- metadata:
name: agent-data
Expand Down
65 changes: 61 additions & 4 deletions deploy/kubernetes/nats.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,45 @@ metadata:
name: murmur-nats-auth
type: Opaque
stringData:
NATS_TOKEN: CHANGE_ME_NATS_TOKEN
NATS_USER: agent-k8s-demo
NATS_PASSWORD_BCRYPT: CHANGE_ME_BCRYPT_HASH
---
apiVersion: v1
kind: Secret
metadata:
name: murmur-nats-tls
type: Opaque
stringData:
tls.crt: CHANGE_ME_SERVER_CERTIFICATE_PEM
tls.key: CHANGE_ME_SERVER_PRIVATE_KEY_PEM
ca.crt: CHANGE_ME_CA_CERTIFICATE_PEM
---
apiVersion: v1
kind: ConfigMap
metadata:
name: murmur-nats-config
data:
nats.conf: |
port: 4222
http: 8222
jetstream { store_dir: "/data/jetstream" }
tls {
cert_file: "/etc/nats/tls/tls.crt"
key_file: "/etc/nats/tls/tls.key"
timeout: 2
}
authorization {
users: [
{
user: $NATS_USER
password: $NATS_PASSWORD_BCRYPT
permissions: {
publish: ["msg.agent-peer", "ack.agent-peer"]
subscribe: ["msg.agent-k8s-demo", "ack.agent-k8s-demo"]
}
}
]
}
---
apiVersion: v1
kind: Service
Expand Down Expand Up @@ -49,13 +87,18 @@ spec:
- /bin/sh
- -ec
args:
- exec nats-server -js -sd /data/jetstream -m 8222 --auth "$NATS_TOKEN"
- exec nats-server -c /etc/nats/nats.conf
env:
- name: NATS_TOKEN
- name: NATS_USER
valueFrom:
secretKeyRef:
name: murmur-nats-auth
key: NATS_USER
- name: NATS_PASSWORD_BCRYPT
valueFrom:
secretKeyRef:
name: murmur-nats-auth
key: NATS_TOKEN
key: NATS_PASSWORD_BCRYPT
ports:
- name: nats
containerPort: 4222
Expand All @@ -69,6 +112,20 @@ spec:
volumeMounts:
- name: nats-data
mountPath: /data
- name: nats-config
mountPath: /etc/nats/nats.conf
subPath: nats.conf
readOnly: true
- name: nats-tls
mountPath: /etc/nats/tls
readOnly: true
volumes:
- name: nats-config
configMap:
name: murmur-nats-config
- name: nats-tls
secret:
secretName: murmur-nats-tls
volumeClaimTemplates:
- metadata:
name: nats-data
Expand Down
6 changes: 3 additions & 3 deletions docs/codex-mac-wake-relay.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ LaunchAgents or the Codex app-server wake path.
## Current Topology

- Agent id: `agent-codex-mac-kovalyaevo`
- Broker: `nats://nats.server-pilot.ru:4222`
- Broker: `tls://nats.server-pilot.ru:4222`
- Peer that wakes Codex on the Mac: `agent-jarvis`
- Murmur repo on the Mac: `/Users/alex/.local/share/mur-mur-v2`
- Mac data dir:
Expand Down Expand Up @@ -68,8 +68,8 @@ The Mac peer config needs these wake fields:
}
```

Only `natsUrl` should point at the current broker. The mesh token and keys are
not part of this runbook.
`natsUrl` must use `tls://`. The per-agent broker username/password, CA path,
and mesh keys are not part of this runbook.

## Implementation Notes

Expand Down
92 changes: 92 additions & 0 deletions docs/nats-transport-security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# NATS transport security

Murmur permits plaintext NATS only on loopback. Every non-loopback client must
use a `tls://` URL; the shared connection builder then gives nats.js an explicit
TLS policy so the connection fails if TLS is unavailable or certificate and
hostname validation fail.

## Agent config

Use a distinct NATS user/password for every agent. Do not reuse the historical
shared token.

```json
{
"natsUrl": "tls://broker.example:4222",
"natsUser": "agent-a",
"natsPassword": "A_DISTINCT_RANDOM_PASSWORD",
"natsTls": {
"caFile": "/run/secrets/murmur-nats-ca.pem"
}
}
```

Omit `natsTls.caFile` only when the server certificate chains to a normal system
trust root. `certFile` and `keyFile` are available for deployments that also use
mutual TLS. If `natsUrl` contains a literal IP address, set
`natsTls.serverName` to the DNS identity in the certificate; Murmur rejects IP
endpoints without it because nats.js does not otherwise perform an IP hostname
check. The `serverName` field is optional for DNS URLs such as the example
above. Never put credentials in the URL.

## Server policy

TLS must be required; do not set `allow_non_tls`. A minimal two-peer core-NATS
configuration has symmetric, subject-bound permissions:

```hcl
host: "PRIVATE_OR_ALLOWLISTED_INTERFACE"
port: 4222
http: "127.0.0.1:8222"

tls {
cert_file: "/run/secrets/server.crt"
key_file: "/run/secrets/server.key"
timeout: 2
}

authorization {
users: [
{
user: "agent-a"
password: "$2a$11$BCRYPT_HASH_FOR_AGENT_A"
permissions: {
publish: ["msg.agent-b", "ack.agent-b"]
subscribe: ["msg.agent-a", "ack.agent-a"]
}
},
{
user: "agent-b"
password: "$2a$11$BCRYPT_HASH_FOR_AGENT_B"
permissions: {
publish: ["msg.agent-a", "ack.agent-a"]
subscribe: ["msg.agent-b", "ack.agent-b"]
}
}
]
}
```

Add only the proxy/presence/JetStream subjects actually used by that identity.
JetStream management and advisory subjects are intentionally absent from the
core-NATS example; enabling JetStream requires a separately reviewed permission
set. Store the config, server key, client config, and client password files as
owner-only (`0600`). Prefer a Tailscale/private listener. If a public listener is
unavoidable for a peer, firewall it to that peer's fixed address.

## Coordinated cutover

1. Inventory every client and its exact publish/subscribe subjects.
2. Generate the server certificate and separate client passwords. Store bcrypt
password hashes—not plaintext client passwords—in `nats.conf`.
3. Deliver each peer only its own password and the public CA/certificate through
an authenticated, encrypted channel.
4. Update all clients to `tls://`, username/password, and the correct CA file.
5. Stop the clients, replace the broker config, validate it with
`nats-server -t -c`, restart the broker, then restart clients.
6. Prove allowed delivery works, forbidden subjects raise permission violations,
the old token fails, and wrong CA/hostname connections fail.
7. Block arbitrary public TCP/4222 and confirm from an external host.

Run `packages/broker-nats/integration/run-secure-transport-live.sh` on a host
with `nats-server` and `openssl` for an isolated TLS/ACL proof.
8 changes: 5 additions & 3 deletions examples/agent-runner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ Copy `agent-config.example.json` → `agent-config.json` and set:

- `agentId` — your kebab-case id (e.g. `agent-stas`).
- `subject` — `msg.<agentId>` (your inbox).
- `natsUrl` — the **public** broker: `nats://5.181.3.139:4222`
- `natsUrl` — the TLS-required broker: `tls://broker.example:4222`.
(the internal `100.95.23.7` Tailscale address is in-tenant only — use the public one).
- `natsToken` — ask the operator (delivered out-of-band, e.g. via the upload bot).
- `natsUser` / `natsPassword` — the distinct, subject-scoped credential assigned
to this agent and delivered out of band.
- `natsTls.caFile` — local path to the operator-provided CA certificate.
- `keys` — from step 2.
- `peers` — the operator gives you the `agent-jarvis` and `agent-codex-volt`
public keys + subjects. Add anyone you need to message.
Expand Down Expand Up @@ -68,7 +70,7 @@ node agent-runner.mjs send agent-jarvis "HANDSHAKE OK from agent-stas — runner

Within ~30–60s you should see an inbound reply logged by your running agent.
If the reply does not decrypt, the most common causes are a wrong key, a peer
public key mismatch, or a `natsToken`/network issue — re-check those first.
public key mismatch, or a TLS/credential/network issue — re-check those first.

## Files

Expand Down
6 changes: 4 additions & 2 deletions examples/agent-runner/agent-config.example.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
{
"agentId": "agent-stas",
"natsUrl": "nats://5.181.3.139:4222",
"natsToken": "<ASK-OPERATOR>",
"natsUrl": "tls://broker.example:4222",
"natsUser": "<ASK-OPERATOR>",
"natsPassword": "<ASK-OPERATOR>",
"natsTls": { "caFile": "/secure/path/murmur-nats-ca.pem" },
"subject": "msg.agent-stas",
"keys": {
"encryption": {
Expand Down
Loading