Skip to content

fix(nats): multi-home nats on pmoves_external so published ports bind (fleet-bus unblock) - #1824

Merged
POWERFULMOVES merged 1 commit into
mainfrom
fix/nats-external-network
Jun 16, 2026
Merged

POWERFULMOVES merged 1 commit into
mainfrom
fix/nats-external-network

Conversation

@POWERFULMOVES

@POWERFULMOVES POWERFULMOVES commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Root cause: nats's only network pmoves_bus is internal:true, so Docker installs no published-port DNAT rules and 4222 is connection-refused over the tailnet (recorded 0.0.0.0:4222 bindings never plumbed). This is why NATS has been unreachable fleet-wide — in-stack comms work over the internal bus but no cross-node peer reaches the hub. Fix: multi-home nats onto non-internal pmoves_external in both the monolith and core overlay, keeping pmoves_bus for in-stack comms. Evidence on kvm4-2: iptables nat DOCKER chain had only gateway-agent's rule, none for nats; 4222 refused from z890. Follow-ups: NATS_BIND to node tailnet IP (mesh-only, not public); dedicated published-bus network later. Generated with Claude Code.

Summary by CodeRabbit

Release Notes

  • Bug Fixes
    • Improved NATS messaging service network accessibility, enabling proper connectivity for external nodes and cross-mesh peer communication.

…ally bind

nats declares `ports:` (4222/9223) and the service comment says the 0.0.0.0
bind is "intentional to allow Tailscale mesh peers to connect" — but its only
network, pmoves_bus, is `internal: true`. Docker installs NO published-port DNAT
rules for a container on an internal-only network, so the bindings are recorded
(docker inspect shows 0.0.0.0:4222) yet never plumbed: 4222 is connection-refused
over the tailnet. This is almost certainly why NATS has been unreachable
fleet-wide — in-stack service-to-service works over the internal bus, but no
cross-node peer can reach the hub.

Fix: multi-home nats onto the non-internal pmoves_external (declared external in
core.yml, internal:false in base.yml) in BOTH the monolith and the core overlay,
keeping pmoves_bus for isolated in-stack comms. With a routable interface present,
Docker plumbs the published ports and tailnet peers can reach 4222/9223.

Verified on kvm4-2: nats container healthy but `iptables -t nat -L DOCKER` had
only the gateway-agent DNAT rule (on the normal bridge) and none for nats on
pmoves_bus (172.30.3.x); 4222 connection-refused from a peer node.

Follow-ups (separate): (1) VPS should set NATS_BIND to the node tailnet IP so 4222
is mesh-only, not public; (2) consider a dedicated non-internal published-bus
network for nats/minio/etc. rather than reusing pmoves_external.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Docker Hardening Validation

Hardening Validation Report

Validated: Tue Jun 16 16:14:40 UTC 2026

Services Checked

PMOVES.AI Docker Hardening Validation

[INFO] Checking: pmoves/docker-compose.hardened.yml

[INFO] Validating: hi-rag-gateway-v2
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: extract-worker
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: langextract
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: presign
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: render-webhook
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: retrieval-eval
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: pdf-ingest
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: jellyfin-bridge
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: invidious-companion-proxy
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: ffmpeg-whisper
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: media-video
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: media-audio
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: hi-rag-gateway-v2-gpu
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: hi-rag-gateway-gpu
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: deepresearch
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: supaserch
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: publisher-discord
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: mesh-agent
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: nats-echo-req
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: nats-echo-res
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: publisher
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: analysis-echo
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: graph-linker
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: comfy-watcher
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: grayjay-plugin-host
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: agent-zero
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: archon
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: channel-monitor
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: pmoves-yt
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: notebook-sync
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: supabase_service_role_key
[WARN] No user directive
[WARN] No read_only directive
[WARN] No cap_drop: ["ALL"]
[WARN] No no-new-privileges
[WARN] No resource limits

[INFO] Validating: supabase_jwt_secret
[WARN] No user directive
[WARN] No read_only directive
[WARN] No cap_drop: ["ALL"]
[WARN] No no-new-privileges
[WARN] No resource limits

======================================
Summary: 120 passed, 40 warnings, 0 errors

@POWERFULMOVES
POWERFULMOVES merged commit 31728c6 into main Jun 16, 2026
31 checks passed
@POWERFULMOVES
POWERFULMOVES deleted the fix/nats-external-network branch June 16, 2026 16:16

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8fa76e08ab

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# 4222/9223 cross-node. Multi-home nats onto the non-internal pmoves_external so
# its published ports actually bind, while keeping pmoves_bus for in-stack comms.
- pmoves_bus
- pmoves_external

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep unauthenticated NATS monitoring off the mesh

When this service is attached to pmoves_external, Docker starts plumbing both published ports above, including ${NATS_MONITORING_PORT:-9223}:8222. The NATS HTTP monitoring server enabled by -m 8222 is not protected by the --user/--pass client auth, so any host that uses the default NATS_BIND=0.0.0.0 now exposes /varz, /connz, /subsz, and JetStream metadata to the tailnet/public interface; the same issue is duplicated in the monolithic compose file. Please either stop publishing the monitoring port or bind it separately to localhost/a monitoring-only network while only making 4222 mesh-reachable.

Useful? React with 👍 / 👎.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1a4e41ab-81d8-4c68-87d7-436c0ef5a65c

📥 Commits

Reviewing files that changed from the base of the PR and between 69b5ad6 and 8fa76e0.

📒 Files selected for processing (2)
  • pmoves/docker-compose.core.yml
  • pmoves/docker-compose.yml

📝 Walkthrough

Walkthrough

Two Docker Compose files (docker-compose.core.yml and docker-compose.yml) each add pmoves_external to the nats service networks list alongside the existing pmoves_bus entry. Inline comments document that pmoves_bus is internal-only and suppresses Docker DNAT rules, so pmoves_external is required for the published NATS ports to be reachable from external mesh peers.

Changes

NATS Cross-Node Network Access

Layer / File(s) Summary
NATS service attaches to pmoves_external
pmoves/docker-compose.core.yml, pmoves/docker-compose.yml
Both files extend the nats service networks list to include pmoves_external alongside pmoves_bus. Comments explain that pmoves_bus being an internal network prevents Docker from installing DNAT rules for the published ports, and pmoves_external restores that reachability for cross-node access.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

Poem

Two files, one fix, the rabbit hops with glee,
pmoves_external joins the network spree.
DNAT rules were blocked by walls internal,
Now NATS ports flow free — no more infernal!
🐇 The mesh is happy, ports are open wide~ ✨

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/nats-external-network

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

POWERFULMOVES added a commit that referenced this pull request Jun 16, 2026
… check-gate) (#1826)

Adds a manual deploy job that brings up / restarts the NATS event bus on the
canonical fleet hub (kvm4-2) without interactive SSH — sidestepping the Tailscale
check-mode re-auth gate (#1820) that now fronts all fleet SSH.

- Targets the kvm4-2 runner via the node-unique `kvm4-2` label just added to it
  (both VPS runners share `kvm4`, so that label alone can't pin the node).
- Operates on the durable /opt/PMOVES.AI checkout, reusing the env chain +
  pmoves_external network already provisioned there; NATS creds are static `:-`
  defaults so no secrets are needed.
- Idempotently ensures the external-declared pmoves_external network exists, then
  runs `make overlay-up-bus` (the guard-preserving target from #1813) and verifies
  varz over the node tailnet IP (NATS_BIND is mesh-only, not localhost).
- Uses only `run:` steps (no `uses:`) → exempt from the sha-pinning gate.

Companion to #1813 (overlay-up-bus) and #1824 (multi-home nats on pmoves_external).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
POWERFULMOVES added a commit that referenced this pull request Jun 18, 2026
…lueprint (#1831)

* docs(architecture): Tailscale-native fleet access + NATS hub design blueprint

Maps Tailscale's official AI-infrastructure access pattern (tagged infra +
deny-by-default grants + per-tenant tags + service sidecars) onto PMOVES, as a
staged blueprint. Keystone = tag the fleet as infrastructure, which fixes the
owner-SSH-check issue idiomatically (tagged nodes leave autogroup:self), ends
server re-auth churn, and is the prerequisite for real multi-tenant isolation.

Captures the 2026-06-16 decision: NATS is a DEDICATED HUB (separate concern),
kept on its working host-publish + tailnet-bind path; net-new services (JuiceFS,
inference) use Tailscale sidecars + tags (hybrid), which sidesteps the
internal-network/DNAT bug class that #1824 worked around. pmoves_bus_pub (the
earlier dedicated-published-network idea) is dropped in favor of the hub + sidecars.

No live changes — execution is staged (SSH stopgap #1828 → tag fleet → acls→grants
→ sidecar JuiceFS/inference → onboard tenants). Companion to #1828 and the NATS
restoration PRs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(architecture): correct §8 — gitops-apply exists (#1832), not console-only

The "no gitops-apply / apply manually" caveat was wrong. PR #1832 wires
tailscale/gitops-acl-action (test-on-PR diff, apply-on-merge); the policy can also
be applied via the Tailscale API. Allowlist done; TS_TAILNET secret pending.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(architecture): define new role tags in tagOwners first + kernel networking for sidecars

Codex P2 x2:
- The rollout references tag:hub/tag:storage/tag:inference but those are not in
  tagOwners; Tailscale rejects an auth key advertising an undefined tag, so the
  staged re-enroll would block. Add a note to define them in
  tailscale-acl-policy.json tagOwners first.
- The sidecar example left TS_USERSPACE at its Docker default (userspace), which
  only handles inbound port-forwards; outbound MagicDNS/100.x from the shared
  namespace needs kernel networking. Add TS_USERSPACE=false + /dev/net/tun.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
POWERFULMOVES added a commit that referenced this pull request Jun 23, 2026
…ack) (#1873)

The VPS gateway-agent deploy failed:
  network pmoves_external was found but has incorrect label
  com.docker.compose.network set to "" (expected: pmoves_external)

Root cause: pmoves_external is shared by TWO compose stacks — the
overlay/NATS stack (which multi-homes nats onto it, PR #1824) and the
monolith. The overlay declares it external:true (so core.yml parses
standalone) and provisions it via docker network create (UNLABELED).
The monolith declared it compose-MANAGED, so when it ran it found an
existing network without compose's ownership label and refused to adopt
it. A compose-managed network can't be shared across stacks.

Fix: declare pmoves_external external:true in docker-compose.yml to match
core.yml — compose then ADOPTS the externally-created network without
label-checking. Teach clean_networks.py to ENSURE the external network
exists (create-if-missing, never remove it — external nets carry no label
by design and removing it would break the stack holding it open), so the
make-target up paths provision it on fresh nodes. CI deploy nodes already
have it. base.yml left as-is (overridden by core.yml in overlay paths).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant