Skip to content

feat(supabase): the scoped role never stopped a superuser from logging in - #2702

Merged
POWERFULMOVES merged 3 commits into
mainfrom
feat/pg-hba-tailnet-role-scope
Aug 24, 2026
Merged

POWERFULMOVES merged 3 commits into
mainfrom
feat/pg-hba-tailnet-role-scope

Conversation

@POWERFULMOVES

Copy link
Copy Markdown
Owner

Step 3.5 of the juicefs cross-node lane — a step the handoff doesn't have, and Step 4 shouldn't ship without.

The gap

The lane orders its steps around one stated property:

"The scoped role below is what keeps this from being a superuser surface."

It doesn't. A scoped role changes which credential JuiceFS uses. It does nothing to stop a superuser credential from being accepted. What governs that is pg_hba.conf, which the handoff never mentions:

host  all  all  0.0.0.0/0  scram-sha-256      ← every role, from anywhere

Once :5432 is tailnet-published, that admits both superusers as password logins over the tailnet:

role rolsuper rolbypassrls live connections
pmoves ✅ ✅ 35
supabase_admin ✅ ✅ 1
juicefs_meta — — 1

pmoves is the initdb superuser (POSTGRES_USER=pmoves) and carries nearly all traffic. Rotating only supabase_admin, as the lane specifies, would have left the busier superuser untouched and the surface open.

The change

Four rules above the catch-alls — first match wins:

host  all  juicefs_meta  100.64.0.0/10        scram-sha-256
host  all  all           100.64.0.0/10        reject
host  all  juicefs_meta  fd7a:115c:a1e0::/48  scram-sha-256
host  all  all           fd7a:115c:a1e0::/48  reject

In-cluster traffic is untouched: it arrives on 172.30.x and still matches the existing 172.16.0.0/12 rule, which sits below these.

Why an IP-scoped rule is sound here

Checked rather than assumed. Docker's only MASQUERADE rules are -s <bridge-subnet> ! -o <bridge> — container→outside. The DOCKER chain does pure DNAT --to-destination with no inbound source rewrite, so a remote peer arrives as its real 100.x. Had it been SNAT'd to the bridge gateway, tailnet traffic would be indistinguishable from in-cluster traffic and pg_hba could not have been the control at all.

Why repo-managed instead of edited in place

The image ships pg_hba.conf at /etc/postgresql/ — the container's writable layer, not the data volume. An in-container edit survives restart and is destroyed by recreate. Step 4 of this same lane requires a recreate (multi-homing onto pmoves_external to publish the port).

A live edit would have been silently reverted by the very step it exists to protect.

Verified

  • Diff vs. the running file: additions only, 0 removals
  • supabase/postgres:17.6.1.108 loads the file, starts, and matches rules from it (Connection matched file "/tmp/hba/pg_hba.conf" line 83) with no parse errors — Postgres refuses to start on a malformed hba
  • Compose parses; supabase-db gains exactly one volume entry

Activation

Takes effect on the next supabase-db recreate. Step 4 supplies that, so it costs no extra disruption — and the cross-node confirmation (5090 reaching :5432 as juicefs_meta, and not as a superuser) lands naturally as Step 4's acceptance test.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FkwiW3VY1xWmahTAtVioxz

…g in

The juicefs cross-node lane orders its steps around one stated property:
"the scoped role below is what keeps this from being a superuser surface."
It does not. A scoped role changes which credential JuiceFS USES. It does
nothing to stop a superuser credential from being ACCEPTED.

What actually governs that is pg_hba, which the handoff never mentions:

    host  all  all  0.0.0.0/0  scram-sha-256

Every role, from anywhere. Once :5432 is tailnet-published that admits
`pmoves` and `supabase_admin` -- both rolsuper AND rolbypassrls -- as
password logins over the tailnet. `pmoves` is the initdb superuser and
carries 35 of the 37 live connections; `supabase_admin` carries one. So
rotating only `supabase_admin`, as the lane specifies, would have left the
busier superuser untouched and the surface open.

Adds four rules above the catch-alls (first match wins):

    host  all  juicefs_meta  100.64.0.0/10        scram-sha-256
    host  all  all           100.64.0.0/10        reject
    host  all  juicefs_meta  fd7a:115c:a1e0::/48  scram-sha-256
    host  all  all           fd7a:115c:a1e0::/48  reject

Only juicefs_meta may authenticate from the tailnet; every other role is
rejected there. In-cluster traffic is untouched -- it arrives on 172.30.x
and still matches the existing 172.16.0.0/12 rule, below these.

An IP-scoped rule only works if the source IP survives, so that was checked
rather than assumed. Docker's only MASQUERADE rules are `-s <bridge> ! -o
<bridge>` -- container to outside. Inbound published-port traffic is pure
DNAT with no source rewrite, so a remote peer arrives as its real 100.x.

Repo-managed, not edited in place, because the image ships pg_hba.conf at
/etc/postgresql/ -- the container's WRITABLE LAYER, not the data volume.
An in-container edit survives a restart and is destroyed by a recreate,
and Step 4 of this same lane REQUIRES a recreate (multi-homing onto
pmoves_external to publish the port). A live edit would have been silently
reverted by the very step it exists to protect.

Verified: file diffs against the live one as additions only, zero removals;
supabase/postgres:17.6.1.108 loads it, starts, and matches rules from it,
with no parse errors -- postgres refuses to start on a malformed hba.

Takes effect on the next supabase-db recreate. Step 4 supplies that, so it
costs no extra disruption.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkwiW3VY1xWmahTAtVioxz
@coderabbitai

coderabbitai Bot commented Aug 23, 2026 •

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3f2936a9-e717-4392-8a15-2de3d1ad90e4


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.

@github-actions github-actions Bot added the compose Compose files / service Dockerfiles label Aug 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Docker Hardening Validation

Hardening Validation Report

Validated: Sun Aug 23 22:59:09 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: 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: p7-room-orchestrator
[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

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

======================================
Summary: 112 passed, 43 warnings, 0 errors

docker-compose.core.yml is generated from docker-compose.yml by
scripts/split_compose.py; editing the source alone left the two out of
sync and `Validate Compose Files` failed as designed.

Regenerated with the pinned toolchain the Makefile specifies
(uv run --with ruamel.yaml==0.19.1) so the rendering is byte-stable.
The only delta is the supabase-db pg_hba mount from the previous commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkwiW3VY1xWmahTAtVioxz

@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: 869f16ca84

ℹ️ 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".

Comment thread pmoves/docker-compose.yml
@github-actions

Copy link
Copy Markdown
Contributor

Docker Hardening Validation

Hardening Validation Report

Validated: Sun Aug 23 23:02:35 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: 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: p7-room-orchestrator
[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

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

======================================
Summary: 112 passed, 43 warnings, 0 errors

@POWERFULMOVES

Copy link
Copy Markdown
Owner Author

This one is stale — it reviewed 869f16ca8, the branch's first commit. The overlay was regenerated in the follow-up commit 2fa9866d8 after Validate Compose Files failed for exactly this reason.

Current branch head:

$ git show origin/feat/pg-hba-tailnet-role-scope:pmoves/docker-compose.core.yml | grep -c 'supabase/config/pg_hba.conf'
1

Regenerated with the Makefile's pinned toolchain (uv run --with ruamel.yaml==0.19.1) so the rendering is byte-stable; the only delta is the supabase-db pg_hba mount. Validate Compose Files and validate-composes-ratchet both pass on the current head.

Your underlying point was right, though — overlay-up-core is the path that matters, and the source-only edit would have left the control absent there.

@github-actions

Copy link
Copy Markdown
Contributor

Docker Hardening Validation

Hardening Validation Report

Validated: Mon Aug 24 00:09:42 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: 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: p7-room-orchestrator
[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

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

======================================
Summary: 112 passed, 43 warnings, 0 errors

@POWERFULMOVES
POWERFULMOVES merged commit cfe81b6 into main Aug 24, 2026
39 checks passed
@POWERFULMOVES
POWERFULMOVES deleted the feat/pg-hba-tailnet-role-scope branch August 24, 2026 00:16
POWERFULMOVES added a commit that referenced this pull request Aug 24, 2026
…#2705)

* feat(secrets): juicefs_meta's password had no route to any other node

B850 mounts pmoves-media as the scoped `juicefs_meta` role. The role exists,
the cutover happened, the mount is healthy. The credential, however, never
entered the pipeline:

    /home/pmoves/.pmoves-secrets/juicefs_meta_pw
      -> bind -> /run/secrets/jfs_meta_pw

A hand-placed file, under a different user's home, referenced NOWHERE in this
repo and holding no CHIT registry entry. So B850 works and no second node can
mount without someone hand-copying a secret -- the exact failure the funnel
exists to prevent, and the blocker the 5090 is currently parked on.

This is load-bearing now rather than tidy. PR #2702 made pg_hba admit ONLY
juicefs_meta from the tailnet and reject every other role there, so a remote
mount has no fallback credential: it authenticates as this role or not at all.

Registered through the sanctioned path only -- REGISTRY entry, then
chit-manifest-register, then chit-manifest-sync. No manifest was hand-edited.

    "JUICEFS_META_PASSWORD": {"tier": "data", "required": False}

tier=data because juicefs-mount and juicefs-gateway both read env.tier-data.
required=False unlike its neighbours: only nodes that actually mount
pmoves-media need it, and under `--merge` (strict) a required slot fails the
whole funnel on every node that legitimately lacks it. The usual argument
against required=False -- that it buys silence -- does not apply here: an
absent value fails loudly at mount time with an auth error.

v2 also emits `docker_secret: pmoves_juicefs_meta_password`, which is the
shape B850's hand-bound secret already has, so the pipeline can materialize
what is currently placed by hand.

Necessary but not sufficient, in #2700's sense: this routes the label. The
value still has to be put into CHIT, and the compose defaults for
juicefs-format/juicefs-gateway still name supabase_admin -- both tracked
separately.

Verified: chit-manifest-check reports in sync; chit-manifest-register --check
reports complete with no constraint drift; re-running sync is byte-stable;
both manifests changed by additions only (+23, 0 deletions).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkwiW3VY1xWmahTAtVioxz

* fix(secrets): the route terminated one hop short of the node that needs it

Codex, correctly: registering the manifest slot does not deliver the value.
sync-secrets-local.yml builds its CGP bundle SOLELY from the workflow's
explicit env map, and the v1 sync discards github_secret targets -- so a
registered label with no row in that map funnels nothing on any node
hydrated from the bundle.

#2700 hit this exact trap and wrote the warning inline five lines above
where this row now goes. I read that PR and still shipped the half that
does not reach anyone.

It matters more here than it did there, because the nodes hydrated from the
bundle are precisely the ones this slot exists for: the 5090 has no Actions
runner and hydrates via `secrets-funnel-from-prod`. Without this row the
operator could create the Prod secret, the funnel would report success, and
env.tier-data would still have no password -- an optional entry skipped in
silence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkwiW3VY1xWmahTAtVioxz

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
POWERFULMOVES added a commit that referenced this pull request Aug 24, 2026
…it targets

The META_ROLE forwarding in this PR is the important half and is correct — the
script has supported META_ROLE since #2683 but the target never passed it, so
the canonical make path silently used supabase_admin, which pg_hba now REJECTS
from the tailnet (#2702). Step 5 would have failed with an auth error that reads
like a bad secret rather than a rejected role.

The password fallback, though, could not fire. `$(JUICEFS_META_PASSWORD)` is a
MAKE variable reference, and make populates its variables only from the
environment and from Makefiles — nothing includes the generated tier files. The
funnel writes this key to env.tier-data and .env.generated, not to env.shared,
so it is never exported into an operator's session either. Net effect: the
operator hits "DB_PASS or JUICEFS_META_PASSWORD required" on precisely the node
the funnel just delivered to, and the documented remedy is to read the value out
of the tier file by hand — which the script's own error text tells them not to
do.

Switched to the recipe-time shell read this file already uses ~line 229:

    DB_PASS="$(or $(DB_PASS),$$(grep -m1 '^JUICEFS_META_PASSWORD=' env.tier-data ...))"

`$$(...)` not `$(...)`: shell at recipe time, which can read generated files.
`cut -d= -f2-` not `-f2`, so a value containing '=' is not truncated at the
first one (the neighbouring line has that bug; not inheriting it).

Dropped the $(error): the script already fails with a better message that names
both DB_PASS and the funnel path, and a make-level abort pre-empts it.

Verified all three paths:
  funnel-delivered  key in env.tier-data, no DB_PASS -> script receives
                    META_ROLE=juicefs_meta with a non-empty DB_PASS
  explicit          DB_PASS=... on the command line wins
  neither           script's own "no metadata password" error, no make abort
and separately that -f2- preserves `abc=def==` where -f2 yields `abc`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkwiW3VY1xWmahTAtVioxz
POWERFULMOVES added a commit that referenced this pull request Aug 24, 2026
…re that (#2707)

* fix(db): anon could execute as a superuser, and a rebuild would restore that

Three SECURITY DEFINER functions in `public` -- claim_/complete_/fail_
studio_board_publish -- are owned by supabase_admin (rolsuper), so they run
as a superuser and bypass RLS. PUBLIC and anon both held EXECUTE.

Nobody granted that. All three defining migrations
(20260325110000_publisher_publish_state, 20260326000400, 20260326001000)
contain ZERO GRANT statements; it is Supabase's stock default privilege on
`public`. Which is exactly why this has to be a migration: the fix was applied
by hand on B850 and a volume reset would silently undo it.

The schema already states the intent, and it is the opposite:

    publisher_audit_svc        service_role   USING true  WITH CHECK true
    publisher_audit_auth_read  authenticated  read-only
    publisher_audit_anon_deny  anon           USING false WITH CHECK false

and detections / segments / emotions / studio_board all have RLS enabled with
NO anon policy at all. So anon is denied everywhere it is named -- while
holding EXECUTE on three functions that write studio_board as a superuser.

No consumer breaks. The publisher (services/publisher + services/common/
supabase.py) does direct inserts into those RLS-protected tables, which an
anon-key client cannot do at all; it has to be service_role, and service_role
keeps its explicit grant. authenticated keeps its own. That is what settled
the key-class question the review was blocked on: it was answerable from RLS,
not from the env.

PUBLIC is revoked for the same reason one level up. Every role inherits
PUBLIC, which is how juicefs_meta -- a LOGIN role that pg_hba now admits from
the tailnet (#2702) -- ended up holding superuser-execution rights it was
created specifically not to have.

Idempotent, and asserts its own end state in both directions: nothing
reachable by anon or juicefs_meta, and nothing lost by service_role or
authenticated. Verified by re-applying against the already-hardened B850 DB.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkwiW3VY1xWmahTAtVioxz

* fix(db): the migration named roles that do not exist yet on a fresh database

Codex, correctly: supabase-bootstrap runs `apply_dir migration
supabase/migrations` and only THEN `apply_dir seed supabase/initdb`, and
00_3_juicefs_meta_role.sql -- which creates juicefs_meta -- is a seed. So on a
fresh database this migration ran before the role existed.

has_function_privilege() raises `role "x" does not exist`. The apply loop
catches a failing file, prints "FAILED (skipped)" and moves on WITHOUT
recording it in pmoves_bootstrap_history -- so the bootstrap looks like it
succeeded and the hardening silently never applied, on precisely the rebuild
path this file exists to secure.

REVOKE has the same problem, so both halves are guarded now:

  - the REVOKE only names anon when anon exists (it is created by
    scripts/supabase/bootstrap_db.sh, a different entrypoint that is not
    ordered against supabase-bootstrap)
  - the assertion CONTINUEs past any role that does not exist
  - the PUBLIC half stays unconditional, which is what preserves the
    guarantee: PUBLIC always exists, and revoking from PUBLIC removes what
    juicefs_meta would otherwise inherit whenever it is created

Also replaced the CROSS JOIN LATERAL with a plain FOREACH -- its `r` alias
collided with the outer unnest and raised "column reference r is ambiguous".

Verified on a throwaway postgres:17 with NEITHER role present:
  - migration exits 0 and revokes PUBLIC (acl goes to `postgres=X/postgres`,
    the `=X/` PUBLIC entry gone)
  - negative test: create anon, grant it EXECUTE, and the assertion raises
    "revoke incomplete: anon still holds EXECUTE on 1 secdef function(s)"
  - re-running the full migration then re-revokes and passes
and against the live already-hardened B850 DB, where it is a no-op.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkwiW3VY1xWmahTAtVioxz

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
POWERFULMOVES added a commit that referenced this pull request Aug 24, 2026
… JUICEFS_META_PASSWORD fallback) (#2708)

* fix(juicefs): forward META_ROLE through the make target + JUICEFS_META_PASSWORD fallback

#2683 added META_ROLE to juicefs-cross-node-setup.sh but the make target never
forwarded it, so the canonical path (make juicefs-cross-node-setup META_ROLE=juicefs_meta)
silently defaulted to supabase_admin — the scoped-role cutover was unreachable on remote
nodes (the 5090 step-5 mount). Forward META_ROLE (default supabase_admin, back-compat).

Also: DB_PASS now falls back to the funnel-delivered JUICEFS_META_PASSWORD (registered
in chit_manifest_register.py, #2705), so a node that received the secret via the pipeline
runs with just META_ROLE=juicefs_meta. Both pass as sub-process env (not argv) -> handed
to JuiceFS via META_PASSWORD, never in ps. Same 'wired end-to-end?' class as the node-local
fixes B850 surfaced tonight.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017g8jC7dupS2ubafo6zPQY6

* fix(juicefs): $(JUICEFS_META_PASSWORD) is empty on exactly the nodes it targets

The META_ROLE forwarding in this PR is the important half and is correct — the
script has supported META_ROLE since #2683 but the target never passed it, so
the canonical make path silently used supabase_admin, which pg_hba now REJECTS
from the tailnet (#2702). Step 5 would have failed with an auth error that reads
like a bad secret rather than a rejected role.

The password fallback, though, could not fire. `$(JUICEFS_META_PASSWORD)` is a
MAKE variable reference, and make populates its variables only from the
environment and from Makefiles — nothing includes the generated tier files. The
funnel writes this key to env.tier-data and .env.generated, not to env.shared,
so it is never exported into an operator's session either. Net effect: the
operator hits "DB_PASS or JUICEFS_META_PASSWORD required" on precisely the node
the funnel just delivered to, and the documented remedy is to read the value out
of the tier file by hand — which the script's own error text tells them not to
do.

Switched to the recipe-time shell read this file already uses ~line 229:

    DB_PASS="$(or $(DB_PASS),$$(grep -m1 '^JUICEFS_META_PASSWORD=' env.tier-data ...))"

`$$(...)` not `$(...)`: shell at recipe time, which can read generated files.
`cut -d= -f2-` not `-f2`, so a value containing '=' is not truncated at the
first one (the neighbouring line has that bug; not inheriting it).

Dropped the $(error): the script already fails with a better message that names
both DB_PASS and the funnel path, and a make-level abort pre-empts it.

Verified all three paths:
  funnel-delivered  key in env.tier-data, no DB_PASS -> script receives
                    META_ROLE=juicefs_meta with a non-empty DB_PASS
  explicit          DB_PASS=... on the command line wins
  neither           script's own "no metadata password" error, no make abort
and separately that -f2- preserves `abc=def==` where -f2 yields `abc`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkwiW3VY1xWmahTAtVioxz

* fix(juicefs): resolve the funnel password through with-env.sh, the canonical loader

The previous commit read env.tier-data with an inline grep. That works, but it
is not the house pattern and it only reads one file.

scripts/with-env.sh is the canonical loader — "env.shared* -> tier env files ->
.env* overlays", mirroring compose layering — so it honours precedence instead
of hard-coding which tier file the value happens to land in today. Same idiom as
mk/yt-cookies.mk:18, and infra.mk:603 already carries a comment recording this
exact lesson as a prior Codex P1 ("uses with-env.sh to load tier files").

Using it also drops the `cut -d= -f2-` parsing entirely, so there is no longer a
quoting or embedded-'=' edge case to get right.

Verified all three paths with a stubbed script:
  tier-delivered   key in env.tier-data, no DB_PASS -> DB_PASS_len=27
  explicit         DB_PASS=explicit                 -> DB_PASS_len=8
  neither          script's own "no metadata password" error, no make abort

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkwiW3VY1xWmahTAtVioxz

---------

Co-authored-by: Mavis <Mavis@pmoves.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
POWERFULMOVES added a commit that referenced this pull request Aug 24, 2026
…procedure (#2709)

* docs(ops): the metadata credential has an owner and a vendor-sourced procedure

"Someone creates the GitHub Prod secret" is not an assignment. Provisioning the
JuiceFS metadata credential is fleet infrastructure — it gates every node's
ability to mount pmoves-media — so it belongs to Z890 (Infrastructure
Coordinator), not to whichever node happens to notice it is missing. B850 holds
the metadata DB and should not become the ambient owner of a secret it merely
consumes.

Written against JuiceFS community documentation rather than our own habits, and
the comparison found four things:

  ALIGNED    Dedicated user + network-scoped pg_hba is the vendor's own
             recommended shape. PR #2702 landed it scoped to the tailnet. Worth
             recording that this was vendor practice and not local invention --
             and that the doc pairs the role WITH the hba rule, which is the
             pairing the original lane handoff was missing.

  PARTIAL    META_PASSWORD is documented and we use it. META_PASSWORD_FILE is
             also documented, and B850 already bind-mounts the secret as a file
             before reading it back through a shell. Adopt on next recreate.

  DIVERGENT  sslmode=disable. Unremarkable container-to-container; not
             unremarkable once :5432 is on the tailnet. WireGuard covers the
             transport, but the session has no TLS of its own. Flagged as a
             decision to make AT Step 4 rather than inherit.

  GAP        The vendor asks for restore TESTS, not just backups. B850 dumps
             metadata hourly to MinIO and has never restored one. An untested
             backup is a hypothesis. Z890 task, tracked separately.

Plus a constraint worth writing down before someone proposes the obvious fix:
PostgreSQL metadata must stay single-server per vendor guidance, so B850 is an
SPOF by design and availability work belongs in restore, not replication.

Also corrects the mount runbook, which still told operators DB_PASS was "the
Supabase DB password". That predates the scoped-role cutover and is now actively
wrong: pg_hba REJECTS supabase_admin from the tailnet, so following it produces
an auth error that reads like a bad secret rather than a rejected role. It now
passes META_ROLE explicitly, since the script still defaults to supabase_admin
for back-compat.

The vendor's pg_hba example is paraphrased rather than quoted -- it carries a
private LAN CIDR, and the no-LAN-IPs guard is right to block that in a public
repo even when the address is someone else's documentation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkwiW3VY1xWmahTAtVioxz

* docs(ops): the runbook told operators to leak the password it was protecting

Two Codex P1s, both correct.

1. ROTATION CONTRADICTED ITSELF. Section 2.1 says never paste the value on a
   CLI, then 2.2-alt handed over a `psql -c "ALTER ROLE … PASSWORD '<new>'"`
   that puts it in shell history AND in the container's argv. `psql -v` is no
   better — same argv. Replaced with a stdin form: `read -rs` (no echo, no
   history) piped through a shell builtin, so the value never becomes another
   process's argv. Validated against the live DB on a throwaway role: ALTER ROLE
   applied and the role authenticated with the piped value over a real scram
   path. Also noted where it belongs long-term — behind a Make target reading
   stdin, next to secrets-rotate.

2. DELIVERY ORDER WAS SILENTLY WRONG FOR RUNNERLESS NODES. `secrets-funnel-from-prod`
   calls pull_chit_bundle.sh, which takes the newest ALREADY-successful run:

       gh run list --workflow "$WORKFLOW" --status success --limit 1

   That run can predate the Prod secret you just created. The pull succeeds, the
   funnel reports success, and JUICEFS_META_PASSWORD is absent or stale — on the
   5090, which is exactly the runnerless node this slot exists for. The runbook
   now requires a producer run FIRST, an explicit wait, and a createdAt
   comparison against when the secret was set. That timestamp check is the whole
   guarantee.

The third finding is a merge-order dependency, not a doc defect: the mount
snippet's `META_ROLE=juicefs_meta` with no DB_PASS needs the target change in
#2708. Merge #2708 first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkwiW3VY1xWmahTAtVioxz

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
POWERFULMOVES added a commit that referenced this pull request Aug 25, 2026
…e lane needs

The compose has published this port since March:

    ports: - ${SUPABASE_DB_BIND:-127.0.0.1}:${SUPABASE_DB_PORT:-54322}:5432

but it has never been reachable, and the reason is not the bind. supabase-db sits
on pmoves_data and pmoves_api, both `internal: true`. A published port on a
container attached only to internal networks maps and then answers nothing.
Multi-homing onto pmoves_external is what actually plumbs it — the pattern the
NATS bus already uses, and the one the lane handoff specified.

This does not put a database on the open internet:

  - SUPABASE_DB_BIND is set per node to that node's TAILNET address, never
    0.0.0.0, so the listener exists only on the tailnet interface. The default
    stays 127.0.0.1, so any node that does not set it publishes nothing new and
    this change is inert there.
  - pg_hba (supabase/config/pg_hba.conf, #2702) admits ONLY juicefs_meta from
    100.64.0.0/10 and REJECTS every other role there. That is the control that
    keeps this from being a superuser surface. The scoped role alone never was —
    it changes which credential JuiceFS uses, not which one is accepted.

sslmode is a recorded decision rather than an inherited default, which is what
JUICEFS_META_CREDENTIAL_RUNBOOK.md §1.3 asked for at this step: the cross-node
DSN carries sslmode=disable, JuiceFS's own PostgreSQL best-practices advise
against it, and the justification for keeping it is that WireGuard encrypts the
tailnet transport and the metadata role is non-superuser and pg_hba-scoped.
Revisit if the DB is ever reachable off-tailnet.

Operators should publish on 5432, not the 54322 default: PORT_REGISTRY already
assigns 5432 to this service, and juicefs-cross-node-setup.sh:28 defaults
DB_PORT=5432, so the canonical port means the remote node needs no override.
Registry row updated from "internal only" to reflect that.

Verified: compose parses; supabase-db resolves to
[pmoves_data, pmoves_api, pmoves_external]; split overlays regenerated with the
pinned toolchain; both 5432 and 54322 are free on this host.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkwiW3VY1xWmahTAtVioxz
POWERFULMOVES added a commit that referenced this pull request Aug 25, 2026
…e lane needs (#2728)

* feat(juicefs): Step 4 — plumb the tailnet-bound DB port the cross-node lane needs

The compose has published this port since March:

    ports: - ${SUPABASE_DB_BIND:-127.0.0.1}:${SUPABASE_DB_PORT:-54322}:5432

but it has never been reachable, and the reason is not the bind. supabase-db sits
on pmoves_data and pmoves_api, both `internal: true`. A published port on a
container attached only to internal networks maps and then answers nothing.
Multi-homing onto pmoves_external is what actually plumbs it — the pattern the
NATS bus already uses, and the one the lane handoff specified.

This does not put a database on the open internet:

  - SUPABASE_DB_BIND is set per node to that node's TAILNET address, never
    0.0.0.0, so the listener exists only on the tailnet interface. The default
    stays 127.0.0.1, so any node that does not set it publishes nothing new and
    this change is inert there.
  - pg_hba (supabase/config/pg_hba.conf, #2702) admits ONLY juicefs_meta from
    100.64.0.0/10 and REJECTS every other role there. That is the control that
    keeps this from being a superuser surface. The scoped role alone never was —
    it changes which credential JuiceFS uses, not which one is accepted.

sslmode is a recorded decision rather than an inherited default, which is what
JUICEFS_META_CREDENTIAL_RUNBOOK.md §1.3 asked for at this step: the cross-node
DSN carries sslmode=disable, JuiceFS's own PostgreSQL best-practices advise
against it, and the justification for keeping it is that WireGuard encrypts the
tailnet transport and the metadata role is non-superuser and pg_hba-scoped.
Revisit if the DB is ever reachable off-tailnet.

Operators should publish on 5432, not the 54322 default: PORT_REGISTRY already
assigns 5432 to this service, and juicefs-cross-node-setup.sh:28 defaults
DB_PORT=5432, so the canonical port means the remote node needs no override.
Registry row updated from "internal only" to reflect that.

Verified: compose parses; supabase-db resolves to
[pmoves_data, pmoves_api, pmoves_external]; split overlays regenerated with the
pinned toolchain; both 5432 and 54322 are free on this host.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkwiW3VY1xWmahTAtVioxz

* fix(compose): regenerate PMOVES_NETWORKS mirror for the external network

Step 4 hand-edited the networks: lists but not the generated
PMOVES_NETWORKS env that topology.TopologyContext.from_env() reads —
the injector drift gate failed. Re-ran the injector and compose-split;
one mirrored env line per file, no other changes.

* fix(juicefs): dedicated DB-egress bridge instead of the shared network

Review P1: joining supabase-db to pmoves_external handed every
internet-facing container on that bridge a direct route to
supabase-db:5432, and pg_hba's 172.16.0.0/12 catch-all accepts every
role from any docker bridge -- the tailnet scoping never sees those
sources.

Replaced with pmoves_db_egress (172.30.8.0/24, internal: false,
external network carrying ONLY the database):

- pg_hba scopes the new subnet above the catch-all: juicefs_meta only,
  everything else rejected -- identical treatment to the tailnet rules.
  Legitimate inbound still DNATs in with its real 100.x source and hits
  the 100.64.0.0/10 block; only a container that joined this bridge
  sources from 172.30.8.x, and that bridge is supposed to hold the
  database alone.
- Subnet is .8, not .7: pmoves_public is compose-declared at
  172.30.7.0/24 (docker-compose.yml) -- .7 would collide.
- Network ensured alongside the others in ensure-networks and
  ensure-overlay-networks; overlays regenerated via injector + split.

The 5090's reachability Monitor is unaffected: SUPABASE_DB_BIND still
pins the listener to the node's tailnet address, and the dedicated
bridge is what makes the published port answer at all.

---------

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

Labels

compose Compose files / service Dockerfiles

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant