Skip to content

fix(neo4j): internal-only Neo4j fronted by a Tailscale forwarder on a dedicated graph front; LOAD CSV blocklist; fleet test - #3201

Merged
POWERFULMOVES merged 13 commits into
mainfrom
ops/knuckles-neo4j-external
Sep 27, 2026
Merged

POWERFULMOVES merged 13 commits into
mainfrom
ops/knuckles-neo4j-external

Conversation

@POWERFULMOVES

@POWERFULMOVES POWERFULMOVES commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Lane: ops/knuckles-neo4j-from-fork (Neo4j Phase 1b follow-up). Branch ops/knuckles-neo4j-external. No live change.

Why

Phase 1b step 7 (2026-09-27): the compose neo4j came up healthy, but Docker published nothing. pmoves_app, pmoves_bus and pmoves_data are all internal: true, and Docker does not publish ports for a container attached only to internal networks. The render and dry-run gates could not see this: they read REQUESTED bindings.

Design: operator OPTION A (after the networking-doc and upstream reconciliation)

Neo4j stays internal-only (no egress, no host port), fronted by a Tailscale forwarder. This is DOCKER_NETWORK_HARDENING.md Rule 5: "There is no 'publish without egress.' Front it with a gateway." (preferred)

Rejected alternatives:

  • Neo4j on pmoves_external. It violates Rule 1 (data services never on pmoves_external). network-planes-and-package-sharing-2026-09-03.md calls it "a security regression".
  • Masquerade-off bridges. They do not block egress; they only remove SNAT.

The first attempt (neo4j joins pmoves_external, commit 21526bb) is reverted (e8ded62), as is the blocklist commit that had edited that block (a39991a). History is kept honest.

Commits

Commit What
f646643 Fleet test: a published service must be on a non-internal network
ae2c46f P2-A + P3: external networks' internal flag comes from their Makefile create sites
a39991a, e8ded62 Reverts: the pmoves_external change and the blocklist that had edited it
14ed3b6 Neo4j is internal-only by design; invariants pinned
e45c8fc The LOAD CSV blocklist, re-applied on its own (defense in depth)
9b64c67 The forwarder, on a dedicated pmoves_graph_front
5889d40 Review fixes: --no-deps + graph_front preflight (P2); dedicated scoped key + TS_AUTH_ONCE; state-volume rename; registry rows; continuation-line parser
ea6461e Nits: the preflight also refuses a stopped Neo4j (.State.Running); accurate inspect-failure message; 4-state behavioural test

The forwarder: docker-compose.neo4j-tailnet.yml (an overlay, NOT in STACK_FILES)

Pattern: it reuses the repo's sidecar pattern (docker-compose.yt-egress.yml) and TAILSCALE_FLEET_POSTURE.md.

Service neo4j-tailnet:

  • tailscale/tailscale:stable pinned by digest (sha256:c507f3a2…, tailscale 1.102.5). The precedent uses the moving :latest, which validate-composes-images flags.
  • TS_USERSPACE=true, and TS_STATE_DIR on the named volume graphfront-tailnet-state (see review fixes for why the name avoids "neo4j").
  • TS_AUTHKEY comes from a dedicated NEO4J_TAILNET_AUTHKEY (:? guarded), with TS_AUTH_ONCE=true.
  • The auth key from the secrets funnel, failing closed at compose parse (the same :? guard as yt-egress).
  • TS_EXTRA_ARGS=--advertise-tags=tag:neo4j.
  • TS_ACCEPT_DNS=false, so neo4j resolves via Docker DNS.
  • cap_drop: [ALL] + no-new-privileges. Measured: userspace tailscaled starts with no capabilities (throwaway, no key, --network none). yt-egress's NET_ADMIN was for its proxy listeners, which this does not use.
  • No host ports, and no network_mode: Neo4j does not share its netns.

Forwarding: config/tailscale/neo4j-tailnet-serve.json via TS_SERVE_CONFIG sends only TCP 7687 to neo4j:7687. The 7474 browser UI is not forwarded, because fleet AGInTs need bolt only.

Networks (least privilege, per the steward's ruling):

  • pmoves_graph_front (NEW, internal: true, 172.30.9.0/24, gateway .1): joined only by neo4j and the forwarder. A compromised forwarder reaches Neo4j and nothing else on the data tier; the forwarder is deliberately NOT on pmoves_data.
    • The subnet is permanent per Rule 2. .1–.8 are allocated (repo and live); .9 was free.
  • pmoves_external is the one egress network, for the Tailscale control plane and DERP. This is the documented dual-attach pattern (agent-zero, archon, hi-rag-gateway-v2, flute-gateway). Rule 1 keeps DATA services off pmoves_external; the forwarder is a gateway, not a data store.

Neo4j: it joins pmoves_graph_front (internal) and gains nothing else. It stays on app/bus/data, all internal, with no alias needed (compose registers the service name on every network).

Make targets (mk/neo4j-tailnet.mk; no recipe nests make):

  • up-neo4j-tailnet: preflight is $(DC) -f <overlay> config -q, which trips the overlay's own key guard without printing it.
  • neo4j-tailnet-status.
  • down-neo4j-tailnet: compose stop, then a non-forced docker container rm.

Open questions: evidence

  • Does TS_DEST_IP work in userspace? NO. Containerboot on the pinned image says "invalid configuration: TS_DEST_IP is not supported with TS_USERSPACE". It would need kernel mode (/dev/net/tun + NET_ADMIN), which departs from the repo's userspace precedent.
  • Does serve TCP accept a non-localhost target?
    • The CLI path refuses it: "only localhost or 127.0.0.1 proxies are currently supported" (ipn/serve.go ExpandProxyTargetValue, in our PMOVES-Tailscale fork).
    • A ServeConfig applied via TS_SERVE_CONFIG is dialled by tailscaled with b.dialer.SystemDial(ctx, "tcp", backDst) (ipn/ipnlocal/serve.go), with no localhost check. So TCPForward: "neo4j:7687" resolves via Docker DNS.
    • Source-reasoned from the fork (2026-04), not runtime-tested (that needs a logged-in node). It MUST be proven at the live step.

Rendered evidence (read-only; the shared checkout's env over this tree's compose; env values never printed)

Field Rendered value
neo4j networks pmoves_app, pmoves_bus, pmoves_data, pmoves_graph_front
neo4j-tailnet networks pmoves_external, pmoves_graph_front
image tailscale/tailscale:stable@sha256:c507f3a2…
ports NONE
network_mode none
cap_drop ALL
security_opt no-new-privileges:true
TS_USERSPACE true
TS_SERVE_CONFIG /config/serve.json (mounted ro)
TS_EXTRA_ARGS --advertise-tags=tag:neo4j
TS_ACCEPT_DNS false
auth key resolves as set on this node (value not shown)
pmoves_graph_front internal=True, 172.30.9.0/24
internal flags app/bus/data/graph_front True; external False

The LOAD CSV blocklist: defense in depth (e45c8fc)

NEO4J_internal_dbms_cypher__ip__blocklist=0.0.0.0/0,::/0 on neo4j. Even internal-only, it stops Cypher LOAD CSV from reaching the host gateway, the LAN or other containers.

Evidence (pinned 5.26.30 Community image; throwaway containers only; the live container untouched):

  • Strict validation recognises the key ("Configuration file validation successful"); a bogus key is rejected ("Configuration contains errors").
  • Without it, on a private --internal bridge, LOAD CSV FROM 'http://127.0.0.1:1/x' gives "Couldn't load the external resource". That is a connection error.
  • With it, http and https IP literals AND a resolvable hostname give "access to … is blocked via the configuration property internal.dbms.cypher_ip_blocklist". That is a policy refusal.
  • Re-proved with the service's full rendered env (24 keys; values passed via the docker CLI env).
  • Caveats:
    • internal.* is unsupported by Neo4j; the digest pin keeps it stable.
    • Upstream notes the source documents coverage of LOAD CSV and apoc.load.json. Other apoc.load.* procedures and HTTP redirects are unverified.
    • --network none is unusable for offline Neo4j tests: Neo4j 5.26 exits immediately (code 3, "shutdown initiated by request", no error) without a network. Use a private --internal bridge.

Fleet test: tests/test_published_ports_need_a_non_internal_network.py

Every published service in the default STACK_FILES must join at least one non-internal network.

  • Measured: 35 violators at main. On Knuckles, 15/15 RUNNING violators had requested bindings and zero effective ones, while cipher-api, minio and presign published.
  • The known violators are listed in KNOWN_VIOLATORS as strict xfail. Neo4j is listed by design (see below).
  • P2-A: pmoves_external and pmoves_db_egress are external: true, so their internal flag comes from the Makefile create sites. EXTERNAL_NON_INTERNAL asserts that every docker network create site for them omits --internal (Makefile:725/5815 for db_egress; 5144/5191/5813 for external), with a positive control.
  • P3: network keys resolve to name:. Scope: STACK_FILES only; !reset/!override are not modelled.
  • Option-A invariants:
    • neo4j joins exactly app/bus/data/graph_front, all internal;
    • neo4j has no network_mode;
    • pmoves_graph_front is internal, and its only STACK member is neo4j (the forwarder joins from its overlay).

Forwarder test (tests/test_neo4j_tailnet_forwarder.py), 7 structural assertions: digest pin; userspace and no TS_DEST_IP; the serve mount; bolt-only forward; the tag; graph_front + external only, with no netns, ports or caps; not in STACK_FILES; no recipe nests make.

  • Dropped deliberately (the steward's ruling): two assertions that the auth-key line uses the funnel-sourced :? guard. Their text named the auth-key variable, which the zero-access gate refuses. They are not rephrased to avoid the name. Coverage stays adequate: the compose line's :? makes compose itself refuse to start without the key.

Counts (targeted): forwarder 7 + fleet + bind passthrough + neo4j safety + no-bare-compose = 146 passed, 35 xfailed, 0 failed.

Gates:

Gate Result
compose-networks-check in sync (109)
compose-split-check rc=0
validate-composes clean

Review fixes (5889d40)

P2: the forwarder never recreates Neo4j. Before this fix, up-neo4j-tailnet would have recreated the live pmoves-neo4j implicitly: through depends_on: neo4j, compose sees neo4j's config changed by this PR, since it joins pmoves_graph_front.

  • The target now runs up -d --no-deps neo4j-tailnet.
  • Before that, a names-only docker inspect preflight refuses (exit 1) unless the running pmoves-neo4j is already attached to pmoves_graph_front. It prints network names only, never addresses.
  • depends_on stays, for full-stack ordering.
  • Tests:
    • a structural check: --no-deps is present and the preflight precedes the up;
    • a behavioural check: real make runs against build_stub_env stubs. Before the recreate the target refuses and no compose up runs; after it, every up carries --no-deps.

P3:

  • Registry rows: DOCKER_NETWORK_HARDENING.md gains pmoves_db_egress (172.30.8.0/24, not internal) and pmoves_graph_front (172.30.9.0/24, internal). The "Six networks" line was already stale; it now reads nine.
  • Comment fix: the overlay comment now says neo4j resolves on pmoves_graph_front, not pmoves_data.
  • State volume: renamed to graphfront-tailnet-state. Any name containing neo4j would still match volume-reset's (^pmoves_.*neo4j|neo4j$), and that includes the suggested pmoves_tailnet-neo4j-state. A match means make volume-reset SERVICE=neo4j would wipe the forwarder's tailnet identity. A test asserts that the name cannot match.
  • Create-site parser: it now joins backslash-continued lines. Failing-before: the old single-line regex missed an --internal placed on a continuation line. There is a new test for this.
  • Key scoping: the forwarder uses the dedicated NEO4J_TAILNET_AUTHKEY (a one-off, pre-authorised key scoped to tag:neo4j) plus TS_AUTH_ONCE=true. Registering it in the secrets manifest is an operator step: the manifest path is zero-access to agents (even a protection check naming the path was refused), so I did not attempt the edit by another route.

Tests (targeted only), at 5889d40: test_neo4j_tailnet_forwarder, test_published_ports_need_a_non_internal_network, test_neo4j_bind_passthrough, test_neo4j_container_safety and test_no_bare_compose_calls give 151 passed, 35 xfailed. The commit message says 157; that figure is wrong, and 151 is the measured count. Gates: compose-networks-check OK (109 services), validate-composes clean, compose-split-check rc 0.

Nits (ea6461e):

  • The preflight reads .State.Running together with the network names, in the same single inspect call (still no addresses). It refuses when pmoves-neo4j is stopped: --no-deps will not start it, and a forwarder in front of a stopped Neo4j fronts nothing.
  • An inspect failure now reads "could not inspect pmoves-neo4j (missing, or daemon/permission error)".
  • The behavioural test covers four stub states, each asserting the refusal reason and that no compose up ran: before the recreate; after it (the only one that runs up, and with --no-deps); recreated but stopped; inspect failing with rc 1.
  • Same targeted set at ea6461e: 153 passed, 35 xfailed; compose-networks-check OK (109 services); validate-composes clean; compose-split-check rc 0.

Operator decision (left as is): the overlay keeps ${NEO4J_TAILNET_AUTHKEY:?}. With TS_AUTH_ONCE=true the key is only used at first login, but the :? means the variable must stay set for compose to render the overlay at all; relaxing it once the state volume holds an identity is the operator's call. The main stack is unaffected, because the overlay is not in STACK_FILES.

Findings (not fixed here)

  1. APOC (corrected). Upstream verified from the neo4j Docker entrypoint (docker-image-src/5/coredb/docker-entrypoint.sh:468-473) that NEO4JLABS_PLUGINS is still honoured in 5.x as a fallback (: ${NEO4J_PLUGINS:=${NEO4JLABS_PLUGINS}}, with a deprecation warning), and core apoc installs from the local labs/ jar with no download. So APOC is most likely LOADED, and dbms.security.procedures.unrestricted=apoc.* is LIVE. This corrects my earlier "may never load" reading, which looked only at the empty plugins/ of the bare image. Suggestion, not in this PR: rename to NEO4J_PLUGINS.
  2. Neo4j's ports: / NEO4J_BIND are INERT under option A. On internal-only networks nothing publishes. They are kept only because feat(neo4j): NEO4J_BIND pass-through (option B) + Neo4j on app/bus/data for every AGInT (compose half pending road) #3196's pass-through reads them. Dropping them (and with them feat(neo4j): NEO4J_BIND pass-through (option B) + Neo4j on app/bus/data for every AGInT (compose half pending road) #3196's pass-through) is a follow-up decision for the operator, not this PR.
  3. yt-egress sidecar. It sits only on pmoves_app, which is internal: true, so it may never reach the Tailscale control plane. Unmeasured.
  4. Guard findings (for the damage-control owner), three:
    • (a) A compose container removal is misread as a filesystem removal. A compose container removal with the force flag, in a down target mirroring down-yt-egress, was misread by the Bash guard as a filesystem recursive/force removal and refused. The same pattern later matched this PR description's own prose describing it: the finding reproduced itself. The approved down target uses compose stop plus a non-forced docker container rm.
    • (b) Zero-access name matching is inconsistent. The overlay compose file and mk/neo4j-tailnet.mk, both containing the auth-key variable NAME, were written WITHOUT refusal. A test-file heredoc with the same NAME was refused by the zero-access pattern for that variable, probably Write-tool content vs Bash command-text scanning. The two assertions were then dropped (the steward's ruling), not rephrased.
    • (c) A grep pattern tripped the gate by itself. A verification grep whose own PATTERN named the variable was refused, even though the file it checked no longer contained the name. The self-check moved to the Read tool.
    • Not investigated further.
  5. Runbook-gate lessons from 1b:
    • step 4: sha256 tree identity, not du;
    • step 6: a precondition that the service has at least one non-internal network, or is gateway-fronted;
    • step 7: an effective-publish gate (docker port / NetworkSettings.Ports);
    • step 2: "Old password and new password cannot be the same" means equal, i.e. a no-op.

OPERATOR STEPS (not done by this PR)

  1. Auth key: mint a one-off, pre-authorised Tailscale auth key scoped to tag:neo4j. This needs a tagOwners entry for tag:neo4j in the tailnet policy. Deliver it as NEO4J_TAILNET_AUTHKEY through the secrets funnel (make -C pmoves secrets-funnel), and register NEO4J_TAILNET_AUTHKEY in the secrets manifest. Agents cannot do that last part because the manifest is zero-access to them. With TS_AUTH_ONCE=true, the key is used only for the first login; after that, the state volume carries the node identity.
  2. Tailnet grant: allow the fleet tag to reach Neo4j, e.g. src <fleet AGInT tag> → dst tag:neo4j, ip: tcp:7687 (in pmoves/configs/tailscale-acl-policy.json, or wherever the operator manages the policy).
  3. NEO4J_BIND: keep it at a loopback value on this node (e.g. 127.0.0.1 via the .env.local Known Road). No host publish is needed under option A, and a loopback value stays safe if a non-internal network is ever added.
  4. Gated Neo4j recreate, BEFORE the forwarder. This is operator-approved and runs on the shared checkout after fix(neo4j): internal-only Neo4j fronted by a Tailscale forwarder on a dedicated graph front; LOAD CSV blocklist; fleet test #3201 merges. It follows the Phase 1b runbook in fix(neo4j): Phase 1 stabilise: never start a second or volumeless Neo4j; one name source (compose pin pending road) #3193, step for step:
    • 4a. Pre-counts. Run the Phase 1b step-1 node and relationship count queries, authenticated as in Phase 1b step 8, and record them.
    • 4b. Clean stop, verified. Run docker stop -t 60 pmoves-neo4j, then the fix(neo4j): Phase 1 stabilise: never start a second or volumeless Neo4j; one name source (compose pin pending road) #3193 Rollback A.1 log check (INFO Stopped. after the last INFO Started.). If the stop was not clean: STOP.
    • 4c. Backup: the Phase 1b mechanism. Copy the stopped volume, mounted :ro, into a dated volume whose name does not start with pmoves_ (so volume-reset SERVICE=neo4j cannot match it). The -pre3201 suffix keeps the 1b backup intact.
      docker volume create knuckles-neo4j-backup-YYYYMMDD-pre3201
      docker run --rm -v pmoves_neo4j-data:/from:ro -v knuckles-neo4j-backup-YYYYMMDD-pre3201:/to busybox:1.36 sh -c 'cp -a /from/. /to/'
      
      Then verify sha256 tree identity, with both sides mounted read-only. It prints two digests per side: the file-content digest and the path-list digest. Each pair must be identical, or STOP.
      docker run --rm -v pmoves_neo4j-data:/from:ro -v knuckles-neo4j-backup-YYYYMMDD-pre3201:/to:ro busybox:1.36 sh -c 'for d in /from /to; do cd $d; echo "$d files=$(find . -type f -exec sha256sum {} + | sort -k2 | sha256sum | cut -c1-16) tree=$(find . | sort | sha256sum | cut -c1-16)"; done'
      
    • 4d. Dry run, the LITERAL compose command. Use the Phase 1b step 6 command verbatim: the same -p pmoves --project-directory "$PWD", and the same --env-file and -f lists, re-derived on the day with the print-dc one-liner from Phase 1b step 6. The subcommand is:
      --dry-run up -d neo4j
      
      It must show ONLY Network pmoves_graph_front Creating/Created and Container pmoves-neo4j Recreate/Recreated/Starting/Started. Anything else is a STOP: another container, a volume creation, or orphan removal. Do not use make -n up-data-tier as the dry run: its nested $(MAKE) … wait-data line executes even under -n.
    • 4e. LIVE: make -C pmoves up-data-tier DATA_SERVICES=neo4j.
    • 4f. Checks.
      • The container is healthy and the image digest is unchanged.
      • Auth is ON: run the Phase 1b step 8 wrong-password control, which must fail for the auth reason, and confirm the compose password is accepted.
      • The post-counts equal the 4a counts.
      • A names-only docker inspect lists pmoves_graph_front.
      • cipher-api still resolves neo4j.
  5. Forwarder bring-up:
    • make -C pmoves up-neo4j-tailnet. Its preflight refuses until step 4 has attached pmoves-neo4j to pmoves_graph_front, and it runs with --no-deps, so it never touches Neo4j;
    • verify neo4j-tailnet-status;
    • from a fleet node, bolt to the forwarder's MagicDNS name on 7687. It must authenticate, while a non-granted source is refused. This also proves the Q2 source reasoning.

Rollback (steps 4 and 5)

Step 5 failed, or needs undoing: run make -C pmoves down-neo4j-tailnet. This stops the forwarder and removes it without force. Neo4j is untouched, and nothing else needs doing: the internal, now-empty pmoves_graph_front can remain.

Step 4 failed, or needs undoing. Re-up Neo4j from the pre-PR compose, through the same gated path:

  • R1. If the forwarder was started, run make -C pmoves down-neo4j-tailnet first.
  • R2. Materialise the pre-PR file. docker-compose.yml is the only file in the step-6 list that this PR changes, so write its pre-merge copy to scratch:
    git -C <shared checkout> show <3201-merge-sha>^1:pmoves/docker-compose.yml > <scratch>/docker-compose.pre3201.yml
    
  • R3. Gated dry run. Use the same literal command as 4d, with -f <scratch>/docker-compose.pre3201.yml in place of -f docker-compose.yml. Keep --project-directory "$PWD" so relative paths resolve exactly as before. It must show ONLY Container pmoves-neo4j Recreate/Recreated/Starting/Started; anything else is a STOP. Compose does not delete a network a service merely left, so pmoves_graph_front stays: internal, and empty.
  • R4. LIVE. Run the same command without --dry-run. This is the one literal compose up in the runbook: no make target renders a substituted compose file.
  • R5. Checks. The same as 4f, except the network list must not include pmoves_graph_front.

Data damaged. That means post-counts differ from 4a, or debug.log shows store, recovery or corruption ERRORs. Nothing starts on pmoves_neo4j-data until the restore has finished. Restore from knuckles-neo4j-backup-YYYYMMDD-pre3201 using #3193 Rollback B:

  • announce no compose activity to the other sessions;
  • take the compose container down;
  • run the single gated restore command, which refuses while any container, running or stopped, references the volume, and exits 3 when that query itself fails;
  • the counts must match.

Only then run R3 and R4.

🤖 Generated with Claude Code

Docker publishes NOTHING for a container attached only to `internal: true`
networks, with no error. Phase 1b of the Neo4j migration hit exactly that:
the compose neo4j came up healthy on pmoves_app/bus/data, all internal, and
its tailnet bind silently did not exist. The render and dry-run gates could
not see it; they read the REQUESTED bindings.

This test parses the Makefile's default STACK_FILES (the YAML directly, no
docker or env files), merges services by name, and asserts that every
published service joins at least one non-internal network.

Measured fleet-wide on origin/main f9d8a82: 35 violators. Empirically, on
Knuckles, 15 of 15 RUNNING violators had requested bindings and ZERO
effective ones, while cipher-api, minio and presign (each also on a
non-internal network) published. The 34 other than neo4j are listed in
KNOWN_VIOLATORS as xfail(strict=True): recorded, not silently passed, and a
fix makes the case XPASS-fail until the name is removed. A staleness test
keeps the list honest.

neo4j is deliberately NOT listed. This test FAILS on it at main
(1 failed, 58 passed, 34 xfailed) until the compose change in this PR (it
joins pmoves_external, like cipher-api) lands under its grant.

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

coderabbitai Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (1)
  • coderabbit

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository: POWERFULMOVES/PMOVES.AI/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 47973923-d931-4943-92ac-75a21d8d03fd

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@kilo-code-bot

kilo-code-bot Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

The review did not run because the selected model is no longer available.

Choose another model in Kilo Code review settings: https://app.kilo.ai/code-reviews

Previous Review Summaries (8 snapshots)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review

The review did not run because the selected model is no longer available.

Choose another model in Kilo Code review settings: https://app.kilo.ai/code-reviews

Previous review

The review did not run because the selected model is no longer available.

Choose another model in Kilo Code review settings: https://app.kilo.ai/code-reviews

Previous review

The review did not run because the selected model is no longer available.

Choose another model in Kilo Code review settings: https://app.kilo.ai/code-reviews

Previous review

The review did not run because the selected model is no longer available.

Choose another model in Kilo Code review settings: https://app.kilo.ai/code-reviews

Previous review

The review did not run because the selected model is no longer available.

Choose another model in Kilo Code review settings: https://app.kilo.ai/code-reviews

Previous review

The review did not run because the selected model is no longer available.

Choose another model in Kilo Code review settings: https://app.kilo.ai/code-reviews

Previous review

The review did not run because the selected model is no longer available.

Choose another model in Kilo Code review settings: https://app.kilo.ai/code-reviews

Previous review

The review did not run because the selected model is no longer available.

Choose another model in Kilo Code review settings: https://app.kilo.ai/code-reviews

@github-actions

Copy link
Copy Markdown
Contributor

Fleet review: kilocode (kilo/z-ai/glm-5.3)

reviewer: tier 2/3 kilo-primary - model kilo/z-ai/glm-5.3 - fell back after tier 1 spark-local: not-configured

  1. CORRECTNESS
  • Diff contradicts its own title: the neo4j→pmoves_external compose hunk is absent — this diff ships only the test. At this state neo4j is in PUBLISHED but not KNOWN_VIOLATORS, so test_a_published_service_is_on_a_non_internal_network[neo4j] fails; body concedes CI is red "intended", but merging before the awaited compose PR leaves main red, and the merge-order coupling is unenforced. Either include the compose change or retitle/split the PR.
  • _stack() runs at import time (pmoves/tests/test_published_ports_need_a_non_internal_network.py:79): a Makefile reformat or missing stack file becomes a collection-time assert/IO error for the whole file, not a clean failure. Regex hardcodes the exact format (STACK_FILES ?=\ + tab-indented -f + \ continuations) — breaks on spaces/CRLF/reformat (loud, but brittle).
  • cur["ports"] += sv.get("ports") or []: long-syntax dict ports append dict keys ("target","published") into the list — detection still works by accident (truthy), but failure messages will print garbage; same for a string scalar ports.
  • Static-only view: NETWORKS.get(n, False) treats undeclared networks as non-internal; extends:/include:/profiles and env-interpolated ports ("${X}:80") are invisible — possible false pass/fail vs runtime; acceptable given the stated "no docker, no env files" design, but undocumented edge.
  • supaserch in KNOWN_VIOLATORS (and body) looks like a typo — the staleness test fails loudly if the name doesn't publish, so self-catching.
  • Merge semantics (ports append, networks union, network_mode last-wins) match compose multi-file merge; xfail(strict=True) + staleness test correctly pin the 34-name list (no silent pass, XPASS-fail on fix, stale-detect on removal).
  • No shell code, no YAML edits, no anchors touched; single test file — no scope creep.
  1. SECURITY / TOPOLOGY
  • LEAK: docstring "Measured on Knuckles 2026-09-27" (line 16) bakes an internal fleet hostname into a public-repo test file; the same host name and "tailnet" wording repeat in the title/body (public PR metadata). No IPs (tailnet/LAN/bridge gateway), no host-port mappings, no secrets/tokens/credentials in the diff; pmoves_* network names and service names are already public repo content. Otherwise clean.
  1. VERDICT: REQUEST_CHANGES — the advertised compose fix is missing from the diff (test is red on neo4j until a separate, unenforced companion PR lands), and an internal machine name is hardcoded into public test prose.
reviewer chain
tier reviewer model outcome detail
1 spark-local - not-configured SPARK_REVIEW_URL is not set
2 kilo-primary kilo/z-ai/glm-5.3 ok review produced
3 kilo-alternate - not-reached

_ lane: fleet-review chain (self-hosted kvm4) - spark-local (when online) -> kilo-primary -> kilo-alternate _

…lish (#3201)

Under KNOWN_ROAD compose:pr:3201, a file grant written by the operator.

Phase 1b step 7: the compose neo4j came up healthy but published NOTHING.
pmoves_app, pmoves_bus and pmoves_data are all internal:true, and Docker
does not publish ports for a container attached only to internal networks.
cipher-api publishes because it is ALSO on pmoves_external.

- docker-compose.yml (source): services.neo4j adds pmoves_external, with NO
  alias. Measured: every Neo4j consumer already shares app/bus/data with it,
  and cipher-api carries no alias there either.
- The networks injector was run directly, not `make compose-networks`. It
  added pmoves_external to neo4j's PMOVES_NETWORKS.
- docker-compose.core.yml was regenerated by `make compose-split`.

Checks:
- compose-networks-check: in sync (109 services);
- test_published_ports_need_a_non_internal_network: the neo4j case now
  PASSES; the 34 known violators still xfail;
- with bind passthrough + safety + no-bare-compose: 134 passed, 34 xfailed.

Rendered (read-only; the operator's real NEO4J_BIND carried unprinted):
- networks: app/bus/data with the neo4j alias, plus pmoves_external (null);
- external is internal=false;
- ports 7474 and 7687 on the TAILNET address;
- container_name pmoves-neo4j; image at the 5.26.30 digest;
- volume pmoves_neo4j-data.

No recreate here. That is a separate operator-approved step with its own dry
run and effective-publish gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the compose Compose files / service Dockerfiles label Sep 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Docker Hardening Validation

Hardening Validation Report

Validated: Sun Sep 27 08:04:43 UTC 2026

Services Checked

PMOVES.AI compose hardening ratchet

[INFO] Checking: /home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.hardened.yml
[INFO] Merged onto (base-first):
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.comfyui.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.ultimate-tts-studio.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.archon.submodule.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.apps.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.activepieces.yml

[INFO] Validating: agent-zero
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: archon
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: channel-monitor
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: comfy-watcher
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: deepresearch
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: extract-worker
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: ffmpeg-whisper
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: grayjay-plugin-host
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: hi-rag-gateway-gpu
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: hi-rag-gateway-v2
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: hi-rag-gateway-v2-gpu
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: invidious-companion-proxy
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: jellyfin-bridge
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: langextract
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: media-audio
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: media-video
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: mesh-agent
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: nats-echo-req
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: nats-echo-res
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: notebook-sync
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: p7-room-orchestrator
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: pdf-ingest
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: pmoves-yt
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: presign
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: publisher-discord
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: render-webhook
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: retrieval-eval
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: supaserch
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

===================================
Summary: 28 services x 5 properties = 140 evaluated; 140 passed + 0 findings = 140
Findings: 0 (0 baselined, 0 new)

@github-actions

Copy link
Copy Markdown
Contributor

Fleet review: kilocode (kilo/z-ai/glm-5.3)

reviewer: tier 2/3 kilo-primary - model kilo/z-ai/glm-5.3 - fell back after tier 1 spark-local: not-configured

  1. CORRECTNESS
  • Core mechanism is right: pmoves_external: (null) under service networks is valid compose and attaching to a non-internal network is what makes publishing take effect; hunks in docker-compose.yml and its split artifact docker-compose.core.yml are identical. Since core is generated, confirm the split regen byte-reproduces these edits (claimed in body, not provable from diff).
  • Empty-var fail-open now activated: ports: ${NEO4J_BIND:-0.0.0.0}:... was inert on main (internal-only nets never published); this PR makes it live. If NEO4J_BIND is unset/empty on any runner/host doing a real up, ports publish on 0.0.0.0 (all interfaces). Consider a fail-safe guard in a gate or a safer default.
  • Network-surface side effect: aliases or not, compose DNS resolves neo4j to every other service on pmoves_external — the body only measured consumers via app/bus/data. Auth is ON per body, but the added adjacency should be a conscious call.
  • Test parser is brittle: ^STACK_FILES \?= \ regex is tab-anchored, ?=-only; any Makefile reformat trips the assert at import/collection, failing the whole module (loud by design, but noisy CI).
  • _all_internal hardcodes the compose "default" network as non-internal and short-circuits if "default" is explicitly listed — if default is ever defined internal: true, silent false negative. Also last-file-wins on network defs across stack files only approximates compose merge; port append overstates it too. Neither affects this rule today.
  • CI edges: module-top import yaml + import-time _stack() → PyYAML (and pytest) must be in the test runner's deps; ImportError/collection error, not per-test failure.
  • xfail(strict=True) + staleness test is the right pattern (XPASS catches fixed violators; renames shrink the list). Body's claim "this PR's CI will show the one neo4j failure" is wrong — the fix ships in the same PR, so CI here should be green.
  1. SECURITY / TOPOLOGY
  • Leak: hostname "Knuckles" hardcoded in the new test's docstring (test_published_ports_need_a_non_internal_network.py:10–12: "Measured on Knuckles 2026-09-27 against running containers…"), plus live operational state ("15 of 15 running"). Public repo — scrub to a neutral phrase ("measured on a fleet host").
  • Borderline: compose comments say NEO4J_BIND is "(the tailnet address)" — names the VPN overlay and the bind design; no literal IP/host. Suggest softening to "the private bind address".
  • The 0.0.0.0 fallback (above) doubles as an exposure issue.
  • Otherwise clean: no IPs, no bridge gateway addrs, no credentials/tokens/secrets; ports 7474/7687 already in-file and standard; the 34 KNOWN_VIOLATORS names are derived from the repo's own public compose files. No instruction smuggling in the diff; nothing asked the reviewer to execute.
  1. VERDICT: REQUEST_CHANGES — the "Knuckles" hostname/operational detail must be scrubbed from the public test docstring, and the now-live 0.0.0.0 default for NEO4J_BIND needs a fail-safe.
reviewer chain
tier reviewer model outcome detail
1 spark-local - not-configured SPARK_REVIEW_URL is not set
2 kilo-primary kilo/z-ai/glm-5.3 ok review produced
3 kilo-alternate - not-reached

_ lane: fleet-review chain (self-hosted kvm4) - spark-local (when online) -> kilo-primary -> kilo-alternate _

@POWERFULMOVES POWERFULMOVES left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Independent review (Control Body, B850 steward), head reviewed 21526bb: request changes (0 P1, 2 P2)

The compose change is clean. Both files show zero top-level diffs, and exactly one service (neo4j) differs. It gains pmoves_external plus the matching PMOVES_NETWORKS entry. Ports, image digest and volumes are byte-identical to main, and the regenerated core.yml mirrors the source. The new test gives 59 passed, 34 xfailed; it is pure YAML parsing with no subprocess or docker.

P2-A, contract correctness: the test takes external networks' internal flag on trust

An external: true network's internal flag lives wherever docker network create ran, not in the YAML. So the assertion that pmoves_external is non-internal is vacuously true.

  • 53 passing services pass ONLY via pmoves_external, including neo4j and cipher-api. supabase-db passes only via pmoves_db_egress.
  • Today every sanctioned create site is non-internal, so today's verdicts are right. A future --internal at any create site would still turn the guard green on exactly the failure it exists to catch.
  • Fix: assert structurally that the Makefile's create lines for these external networks carry no --internal, or keep an explicit table of external-network flags checked against those sites.

P2-B, defense in depth: joining pmoves_external gives Neo4j outbound egress

app, bus and data are internal: true, so Neo4j had no route out. pmoves_external is a normal NAT'd bridge, so any client holding the password could make Neo4j fetch arbitrary URLs through LOAD CSV (or APOC load procedures, if present). That includes the host gateway and host-published ports: an SSRF and exfiltration channel. cipher-api already has this egress.

Operator decision: keep pmoves_external for publishing, and restrict Neo4j's outbound URL loading in this PR. The setting must be verified for Neo4j 5.26 Community, and the PR must show LOAD CSV from a URL being refused.

In-network reach added: in STACK_FILES, only pmoves-ollama and supabase-gotrue newly share a network with neo4j. In other compose files, if deployed: chit-tour, persona-room and traefik. Anything attached to pmoves_external by hand also gains reach.

P3

  • The comment "No alias here" is misleading: compose gives every container its service name and container_name as DNS names on every network it joins.
  • Network keys are not resolved to their name:. For example comfyui's api_tier maps to the internal pmoves_api, which is masked today.
  • Only STACK_FILES is scanned. Overlays using !reset or !override tags don't safe_load. Say both in the docstring.

Operator decision: keep pmoves_external, and restrict Neo4j's outbound URL
loading in this PR. On a non-internal network, a Cypher `LOAD CSV FROM
'http(s)://…'` could otherwise make Neo4j fetch from anywhere it can route to.

- NEO4J_internal_dbms_cypher__ip__blocklist=0.0.0.0/0,::/0 on the neo4j
  service (docker-compose.yml; core overlay regenerated by compose-split).
- Verified on the pinned 5.26.30 Community image, with throwaway containers
  only; the live container was not touched:
  - strict validation RECOGNISES the key, while a bogus key is rejected;
  - on a private --internal bridge WITHOUT it, LOAD CSV of
    http://127.0.0.1:1/x fails with a connection error ("Couldn't load the
    external resource");
  - WITH it, http and https IP literals AND a resolvable hostname fail by
    POLICY: "access to … is blocked via the configuration property
    internal.dbms.cypher_ip_blocklist";
  - re-proved with the service's full rendered environment (24 keys), whose
    values were passed via the docker CLI's env, never argv or a file.
- `internal.*` means unsupported by Neo4j; the digest pin keeps it stable.
  (--network none could not be used: Neo4j 5.26 exits immediately with code 3.)

Also P3: the pmoves_external comment is corrected. Compose registers the
service name `neo4j` (and container_name) on every network the service
joins, so no explicit alias is needed there.

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

Copy link
Copy Markdown
Contributor

Docker Hardening Validation

Hardening Validation Report

Validated: Sun Sep 27 08:21:39 UTC 2026

Services Checked

PMOVES.AI compose hardening ratchet

[INFO] Checking: /home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.hardened.yml
[INFO] Merged onto (base-first):
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.comfyui.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.ultimate-tts-studio.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.archon.submodule.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.apps.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.activepieces.yml

[INFO] Validating: agent-zero
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: archon
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: channel-monitor
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: comfy-watcher
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: deepresearch
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: extract-worker
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: ffmpeg-whisper
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: grayjay-plugin-host
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: hi-rag-gateway-gpu
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: hi-rag-gateway-v2
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: hi-rag-gateway-v2-gpu
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: invidious-companion-proxy
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: jellyfin-bridge
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: langextract
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: media-audio
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: media-video
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: mesh-agent
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: nats-echo-req
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: nats-echo-res
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: notebook-sync
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: p7-room-orchestrator
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: pdf-ingest
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: pmoves-yt
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: presign
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: publisher-discord
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: render-webhook
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: retrieval-eval
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: supaserch
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

===================================
Summary: 28 services x 5 properties = 140 evaluated; 140 passed + 0 findings = 140
Findings: 0 (0 baselined, 0 new)

…te sites (#3201 P2-A, P3)

Review of #3201:
- P2-A: pmoves_external and pmoves_db_egress are `external: true` in compose,
  so compose never states whether they are internal. That is decided where
  the Makefile/mk CREATE them. New: EXTERNAL_NON_INTERNAL pins both, and
  test_external_networks_are_created_without_internal asserts that EVERY
  `docker network create` site for each (Makefile:725/5144/5191/5813/5815)
  omits --internal. A positive control proves the check catches --internal.
- The published-ports rule now derives an external network's internal flag
  from those create sites (it no longer assumes non-internal). It FAILS if a
  used external network has no create site to check.
- P3: network keys are resolved to their real `name:`. The docstring states
  the scope: only the default STACK_FILES are scanned, and !reset/!override
  overlays are not modelled.

The parser finds 3 create sites for pmoves_external and 2 for
pmoves_db_egress, none internal, and derives pmoves_api/app/data as internal
from theirs.

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

Copy link
Copy Markdown
Contributor

Docker Hardening Validation

Hardening Validation Report

Validated: Sun Sep 27 08:22:56 UTC 2026

Services Checked

PMOVES.AI compose hardening ratchet

[INFO] Checking: /home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.hardened.yml
[INFO] Merged onto (base-first):
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.comfyui.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.ultimate-tts-studio.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.archon.submodule.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.apps.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.activepieces.yml

[INFO] Validating: agent-zero
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: archon
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: channel-monitor
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: comfy-watcher
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: deepresearch
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: extract-worker
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: ffmpeg-whisper
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: grayjay-plugin-host
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: hi-rag-gateway-gpu
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: hi-rag-gateway-v2
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: hi-rag-gateway-v2-gpu
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: invidious-companion-proxy
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: jellyfin-bridge
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: langextract
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: media-audio
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: media-video
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: mesh-agent
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: nats-echo-req
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: nats-echo-res
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: notebook-sync
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: p7-room-orchestrator
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: pdf-ingest
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: pmoves-yt
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: presign
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: publisher-discord
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: render-webhook
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: retrieval-eval
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: supaserch
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

===================================
Summary: 28 services x 5 properties = 140 evaluated; 140 passed + 0 findings = 140
Findings: 0 (0 baselined, 0 new)

@github-actions

Copy link
Copy Markdown
Contributor

Fleet review: kilocode (kilo/z-ai/glm-5.3)

reviewer: tier 2/3 kilo-primary - model kilo/z-ai/glm-5.3 - fell back after tier 1 spark-local: not-configured

Diff reviewed in full (no repo checkout in this lane — findings are from the diff text only; the blocklist value is confirmed 0.0.0.0/0,::/0).

1. CORRECTNESS

  • Core mechanism and fix are right: Docker silently publishes nothing for containers attached only to internal: true networks; adding a non-internal network is the correct remedy, and it's applied consistently to docker-compose.yml (source) and the regenerated docker-compose.core.yml.
  • Blocklist value 0.0.0.0/0,::/0 is a correct deny-all for both families, and the env name maps correctly per the __→_ convention (matches the adjacent allow__csv__import line).
  • Mitigation fragility: the egress blocklist is an unsupported internal.* key AND NEO4J_server_config_strict__validation_enabled=false already sits next to it — on any image bump the key can be dropped/renamed and silently no-op with no startup error, no healthcheck coverage, and no gate in the new test. The whole security justification for joining a non-internal network rides on one comment-verified unsupported setting ("verified on 5.26.30" is author claim, and the digest pin lives outside this diff).
  • Interplay with the immediately adjacent context lines: this diff is what makes the previously-dead ports: live, and ${NEO4J_BIND:-0.0.0.0} still defaults to wildcard. Any deploy where that env var is unset now publishes 7474/7687 on every host interface; only operator env discipline prevents it. Worth hardening in the same PR (e.g. :? instead of :-0.0.0.0).
  • Test parser fragility: the Makefile regex hardcodes STACK_FILES ?= + tab + -f file \ continuation format; the docker network create parser assumes name-last, single-line commands, and literal (non-$(VAR)) names. Any Makefile drift fails as a module-import assert → pytest collection error for the entire file, masking all ~100 cases in CI rather than one red test.
  • Fail-open default: NETWORKS.get(n, False) treats unknown/undeclared network keys as non-internal, so a parser miss or out-of-stack declaration can hide future violators (the guard should fail closed). !reset/!override unmodelled is at least documented.
  • xfail(strict) design is coherent (a fix XPASS-fails until removed; staleness test catches removals/renames), accept the intended cross-lane coordination cost.
  • No shell added anywhere; the test is pure file reads — no quoting/pipefail exposure.

2. SECURITY / TOPOLOGY

  • Leak: internal hostnames committed to the new public test file (added lines) — docstring "Measured on Knuckles 2026-09-27" and overlay list "per-node, vps, elder-melchor, …" name internal fleet nodes/deployment targets in a public repo. Strip or sanitize these.
  • Minor disclosure: added compose comments state pmoves_external is "the tailnet address" — publishes infra-class description; network/service names themselves are not new (already in-repo).
  • Attack-surface note the PR underplays: joining pmoves_external is bidirectional — every peer container on that network can now resolve and reach neo4j:7474/7687 directly (ingress), not just via the published host bind; the blocklist mitigates egress only. Auth is on, but "no consumer reaches it via pmoves_external alone" is a body-only claim.
  • Otherwise clean: no secrets, tokens, plaintext credentials, tailnet/LAN IPs, bridge gateway IPs, or new port-layout disclosure added; the blocklist literal 0.0.0.0/0 is a CIDR, not a bind; NEO4J_PASSWORD referenced only via :? required syntax (context); the KNOWN_VIOLATORS inventory is derived from already-public compose files — no new disclosure.
  • No scope creep: all three files serve the stated PR purpose; the blocklist hardening is same-file/same-purpose but is not disclosed in the PR body — give it explicit maintainer signoff.

3. VERDICT: REQUEST_CHANGES

Sound compose fix and a well-guarded fleet test, but the public test file commits internal hostnames, the load-bearing egress blocklist rests on an unsupported setting with no runtime gate, and the NEO4J_BIND:-0.0.0.0 default becomes live-exposure the moment this publish fix lands.

reviewer chain
tier reviewer model outcome detail
1 spark-local - not-configured SPARK_REVIEW_URL is not set
2 kilo-primary kilo/z-ai/glm-5.3 ok review produced
3 kilo-alternate - not-reached

_ lane: fleet-review chain (self-hosted kvm4) - spark-local (when online) -> kilo-primary -> kilo-alternate _

POWERFULMOVES and others added 4 commits September 27, 2026 04:25
… the invariants

Operator decision after the networking-doc and upstream reconciliation: neo4j
stays INTERNAL-ONLY (app/bus/data: no egress, no host port) and is fronted by
a Tailscale forwarder. That is DOCKER_NETWORK_HARDENING Rule 5 ("gateway-front
it"). The pmoves_external commit is reverted: Rule 1 says no data service on
pmoves_external.

- neo4j is added to KNOWN_VIOLATORS BY DESIGN, with the reason inline. Its
  ports: stay only to carry ${NEO4J_BIND} (#3196), and are inert on
  internal-only networks.
- New invariants:
  - neo4j joins exactly pmoves_app, pmoves_bus and pmoves_data, all internal,
    and no new network;
  - neo4j has no network_mode, so it never shares a sidecar's netns.

test_published_ports_need_a_non_internal_network: 63 passed, 35 xfailed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pth (#3201)

Re-applied on its own, after the pmoves_external revert. It no longer edits
any network block.

NEO4J_internal_dbms_cypher__ip__blocklist=0.0.0.0/0,::/0 on the neo4j service
(docker-compose.yml; core overlay regenerated). Even with neo4j internal-only
it stops Cypher LOAD CSV from reaching the host gateway, the LAN or other
containers.

Verified on the pinned 5.26.30 Community image, with throwaway containers
only (evidence in the PR body):
- strict validation recognises the key, while a bogus key is rejected;
- WITHOUT it, LOAD CSV of http://127.0.0.1:1/x fails with a connection error;
- WITH it, http and https IP literals and a resolvable hostname fail by
  POLICY ("access to … is blocked via … internal.dbms.cypher_ip_blocklist");
- re-proved with the service's full rendered env.
`internal.*` means unsupported by Neo4j; the digest pin keeps it stable.

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

Copy link
Copy Markdown
Contributor

Docker Hardening Validation

Hardening Validation Report

Validated: Sun Sep 27 08:27:31 UTC 2026

Services Checked

PMOVES.AI compose hardening ratchet

[INFO] Checking: /home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.hardened.yml
[INFO] Merged onto (base-first):
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.comfyui.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.ultimate-tts-studio.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.archon.submodule.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.apps.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.activepieces.yml

[INFO] Validating: agent-zero
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: archon
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: channel-monitor
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: comfy-watcher
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: deepresearch
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: extract-worker
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: ffmpeg-whisper
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: grayjay-plugin-host
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: hi-rag-gateway-gpu
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: hi-rag-gateway-v2
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: hi-rag-gateway-v2-gpu
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: invidious-companion-proxy
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: jellyfin-bridge
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: langextract
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: media-audio
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: media-video
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: mesh-agent
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: nats-echo-req
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: nats-echo-res
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: notebook-sync
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: p7-room-orchestrator
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: pdf-ingest
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: pmoves-yt
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: presign
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: publisher-discord
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: render-webhook
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: retrieval-eval
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: supaserch
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

===================================
Summary: 28 services x 5 properties = 140 evaluated; 140 passed + 0 findings = 140
Findings: 0 (0 baselined, 0 new)

@github-actions

Copy link
Copy Markdown
Contributor

Fleet review: kilocode (kilo/z-ai/glm-5.3)

reviewer: tier 2/3 kilo-primary - model kilo/z-ai/glm-5.3 - fell back after tier 1 spark-local: not-configured

PR #3201 Review (kilocode lane)

1. CORRECTNESS

  • Diff contradicts the title/body. The advertised fix — neo4j joins pmoves_external so its ports publish on NEO4J_BIND — is absent from the diff. No services.neo4j.networks hunk exists; instead the new test pins neo4j to internal-only app/bus/data and xfails it (option A). Body claims "neo4j is deliberately NOT listed" in KNOWN_VIOLATORS; the diff lists it (35 entries vs the body's 34). The two states are mutually exclusive: if the body's compose change is merged, test_neo4j_joins_only_internal_networks_and_no_new_ones fails and the neo4j xfail goes strict-XPASS-fail. Title, body, and diff must be reconciled before merge.
  • neo4j keeps dead ports: config. On internal-only networks they are inert (the body itself says the alternative fix is to drop them); the comment's "stay only to carry ${NEO4J_BIND}" rationalizes config that does nothing.
  • _CREATE parser hole (P2-A guard). [^\n|;&]* cannot cross backslash-newline recipe continuations: a multi-line docker network create \ --internal \ <net> records the site without --internal (silent false negative). It also matches the phrase inside comments/docs (false --internal sites) and mis-keys quoted or $(var) names. The positive control only covers the single-line happy path. Strip comments, join continuations (e.g. shlex over un-continued recipe lines) before scanning.
  • Import-time side effects. SERVICES, NETWORKS = _stack()/CREATE_SITES at module level: a benign Makefile reformat (:= vs ?=, spaces vs tabs), a renamed external network, or a variable-named create site errors the entire module at collection, red-flagging all tests instead of one gate.
  • Blocklist fragility. NEO4J_internal_dbms_cypher__ip__blocklist (escaping and 0.0.0.0/0,::/0 syntax are correct) sits next to pre-existing strict__validation_enabled=false: if the unsupported internal.* key is ever renamed, it will be ignored with no startup failure — protection rests solely on the digest pin. The body's "strict validation would catch a rename" was verified on throwaway containers with validation ON, not this service's config.
  • Fleet-wide behavior change: the blocklist kills all http/https LOAD CSV for every consumer of this neo4j (post-resolution); needs a consumer check before the live recreate.
  • Minor: ports += duplicates entries across files (compose dedupes — only affects message text); extends:/include: unmodelled (docstring covers only !reset/!override); unknown network keys default non-internal (NETWORKS.get(n, False)), masking violators defined in unscanned overlays. supaserch in KNOWN_VIOLATORS looks like a typo of supasearch — if so, the staleness test fails.
  • No shell/Makefile changes in this diff, so nothing new to audit for quoting/pipefail.

2. SECURITY / TOPOLOGY

  • Not clean. The public-repo test docstring leaks internal topology: node/host codename "Knuckles" (the host the fleet runs on) and overlay name "elder-melchor", plus the forwarder filename docker-compose.neo4j-tailnet.yml confirming a tailnet publishing path. Strip these from the committed file.
  • Otherwise clean: no IPs, bridge gateways, ports, or internal-layout mappings in the diff; only policy literals (0.0.0.0/0,::/0); no tokens or plaintext credentials — ${NEO4J_PASSWORD:?...} is interpolated, never literal.

3. VERDICT: REQUEST_CHANGES

The diff implements the opposite design from the title/body (option A: internal-only neo4j + blocklist; the advertised pmoves_external publish fix is missing, and they cannot coexist), and the new public test file leaks internal host codenames.

reviewer chain
tier reviewer model outcome detail
1 spark-local - not-configured SPARK_REVIEW_URL is not set
2 kilo-primary kilo/z-ai/glm-5.3 ok review produced
3 kilo-alternate - not-reached

_ lane: fleet-review chain (self-hosted kvm4) - spark-local (when online) -> kilo-primary -> kilo-alternate _

…ion A)

Operator decision: Neo4j stays INTERNAL-ONLY (no egress, no host port) and is
fronted by a Tailscale forwarder. That is DOCKER_NETWORK_HARDENING Rule 5
("There is no 'publish without egress.' Front it with a gateway").

- pmoves_graph_front: a new INTERNAL network (the *network-internal-only
  anchor), subnet 172.30.9.0/24, gateway .1. It is permanent per Rule 2;
  .1 to .8 are allocated in the repo and live. Joined ONLY by neo4j and the
  forwarder, so a compromised forwarder reaches Neo4j and nothing else on
  the data tier.
- neo4j joins pmoves_graph_front. Its other networks are unchanged
  (app/bus/data, all internal). No alias: compose registers the service
  name on every network it joins.
- docker-compose.neo4j-tailnet.yml is an OVERLAY, not in STACK_FILES. It
  follows docker-compose.yt-egress.yml:
  - image tailscale/tailscale:stable pinned by digest (the precedent uses
    the moving :latest);
  - userspace mode; the state dir on a named volume;
  - the auth key from the secrets funnel, failing closed at compose parse;
  - --advertise-tags=tag:neo4j;
  - TS_ACCEPT_DNS=false, so `neo4j` resolves via Docker DNS;
  - cap_drop ALL + no-new-privileges (measured to start with no caps);
  - networks: pmoves_graph_front + pmoves_external (the documented
    dual-attach pattern; a gateway, not a data store), NOT pmoves_data;
  - no host ports, and no network_mode (Neo4j does not share its netns).
- Forwarding: config/tailscale/neo4j-tailnet-serve.json via TS_SERVE_CONFIG,
  TCP 7687 to neo4j:7687 only (the 7474 browser UI is not forwarded).
  - TS_DEST_IP is refused in userspace; measured on the pinned image.
  - The CLI `serve --tcp` accepts only localhost targets, but a ServeConfig's
    TCPForward is dialled with SystemDial (ipn/ipnlocal/serve.go) with no
    localhost check. That is source-reasoned and must be proven at the live
    step.
- mk/neo4j-tailnet.mk: up-neo4j-tailnet (preflight `config -q`, which trips
  the overlay's own key guard without printing it), neo4j-tailnet-status,
  and down-neo4j-tailnet (compose stop, then a non-forced
  `docker container rm`). No recipe nests make.
- Tests:
  - tests/test_neo4j_tailnet_forwarder.py makes 7 structural assertions:
    digest pin; userspace and no TS_DEST_IP; serve mount; bolt-only forward;
    the tag; graph_front + external only, with no netns, ports or caps; not
    in STACK_FILES; no nested make.
  - The fleet test's invariant is now neo4j on app/bus/data/graph_front, all
    internal, and graph_front's only stack member is neo4j.
  - Result: 146 passed, 35 xfailed (targeted).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the config pmoves/config(s)/ changes label Sep 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Docker Hardening Validation

Hardening Validation Report

Validated: Sun Sep 27 08:34:00 UTC 2026

Services Checked

PMOVES.AI compose hardening ratchet

[INFO] Checking: /home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.hardened.yml
[INFO] Merged onto (base-first):
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.comfyui.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.ultimate-tts-studio.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.archon.submodule.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.apps.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.activepieces.yml

[INFO] Validating: agent-zero
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: archon
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: channel-monitor
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: comfy-watcher
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: deepresearch
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: extract-worker
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: ffmpeg-whisper
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: grayjay-plugin-host
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: hi-rag-gateway-gpu
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: hi-rag-gateway-v2
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: hi-rag-gateway-v2-gpu
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: invidious-companion-proxy
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: jellyfin-bridge
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: langextract
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: media-audio
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: media-video
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: mesh-agent
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: nats-echo-req
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: nats-echo-res
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: notebook-sync
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: p7-room-orchestrator
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: pdf-ingest
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: pmoves-yt
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: presign
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: publisher-discord
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: render-webhook
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: retrieval-eval
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: supaserch
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

===================================
Summary: 28 services x 5 properties = 140 evaluated; 140 passed + 0 findings = 140
Findings: 0 (0 baselined, 0 new)

@POWERFULMOVES POWERFULMOVES changed the title fix(neo4j): publish via pmoves_external like cipher-api; fleet test: published services need a non-internal network fix(neo4j): internal-only Neo4j fronted by a Tailscale forwarder on a dedicated graph front; LOAD CSV blocklist; fleet test Sep 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Fleet review: kilocode (kilo/z-ai/glm-5.3)

reviewer: tier 2/3 kilo-primary - model kilo/z-ai/glm-5.3 - fell back after tier 1 spark-local: not-configured

Read the diff (/review/kilo-review.diff, 555 lines). Here's the kilocode-lane review of PR #3201.

1. CORRECTNESS

  • Diff contradicts the PR title/body. Title/body say "neo4j joins pmoves_external" with "neo4j deliberately NOT in KNOWN_VIOLATORS"; the diff implements the opposite design ("operator option A"): neo4j stays internal-only, IS in KNOWN_VIOLATORS (test_published_ports_need_a_non_internal_network.py:409-419), and adds a whole tailnet-forwarder subsystem (new network, overlay, mk targets) the body never mentions. None of the body's gate/commit/count claims can be verified against this diff; treat the body as stale.
  • Dead ports: kept on neo4j (docker-compose.core.yml, docker-compose.yml: ${NEO4J_BIND:-0.0.0.0}:7474/7687) "to carry the variable" — with internal-only networks these are inert, so every render/dry-run shows requested bindings that never exist: the exact silent-trap failure mode this PR's own test warns about. Remove them or gate them.
  • up-neo4j-tailnet is not self-contained (mk/neo4j-tailnet.mk): the overlay references networks defined only in stack files (pmoves_graph_front, pmoves_external) and depends_on: neo4j, which it doesn't define. If $(DC) doesn't carry STACK_FILES, config -q/up fail ("undefined network"/"service neo4j not defined"); if it does, up -d will recreate the live pmoves-neo4j (its config changed in this PR + depends_on), contradicting "No live change / recreate is a separate step". Resolve: verify $(DC), consider --no-deps or declare networks external: true in the overlay.
  • Mitigation contradicts config: the internal.* blocklist caveat claims "strict validation would catch a rename", but compose sets NEO4J_server_config_strict__validation_enabled=false — after an image bump the unsupported cypher__ip__blocklist key could silently vanish (dead no-op) with nothing flagging it.
  • Stale comment (docker-compose.neo4j-tailnet.yml, TS_ACCEPT_DNS=false): "forward target must resolve on pmoves_data" — the forwarder is deliberately not on pmoves_data; it resolves on pmoves_graph_front.
  • Silent false-negative in the fleet test: _all_internal uses NETWORKS.get(n, False) — a published service referencing an unknown/typo'd network key counts as non-internal and passes for the wrong reason. Unknown keys should fail loudly (only the neo4j-specific test does, via NETWORKS[n]).
  • Create-site parser is fragile (_create_sites): takes the last non-flag word, so variable-named networks ($(NET)), backslash-continuation lines, or prose in any comment/guard string matching docker network create … --internal in Makefile/mk/*.mk produce wrong/missing sites or spurious CI failures; create sites living outside those files hit a hard import-time assert.
  • No gate validates the overlay's compose semantics: compose-networks-check/split-check/validate-composes scan STACK_FILES only; the new test asserts structure, so network-name/depends_on typos in the overlay surface only at operator runtime.
  • Duplicated brittle STACK_FILES regex in two test files (reformats of the Makefile break both — loudly, but DRY/maintenance cost).
  • Verify 172.30.9.0/24 has no collision with any other declared subnet (docker create hard-fails on overlap; can't check from the diff).
  • Nit: volume neo4j-tailnet-state breaks the pmoves_ prefix convention seen on pmoves_neo4j-data.
  • Positive: YAML is consistent across base/core/combined (identical blocks, safe_load resolves the <<: *network-internal-only anchor); shell recipes are clean (no pipes, guarded rm, :? catches empty vars, key never echoed).

2. SECURITY / TOPOLOGY

Not clean:

  • Hardcoded bridge subnet/gateway added to a PUBLIC repo: 172.30.9.0/24 / gateway 172.30.9.1 (docker-compose.base.yml, docker-compose.yml) map internal Docker layout (matches the pre-existing 172.30.4.0/24 pattern — the whole family is disclosed topology).
  • Internal hostnames leaked in committed docstrings: "Measured on Knuckles 2026-09-27" and "other overlays (per-node, vps, elder-melchor, …)" in test_published_ports_need_a_non_internal_network.py disclose node names in a public repo.
  • Default tailnet node name disclosed: TS_HOSTNAME=${NEO4J_TAILNET_HOSTNAME:-pmoves-neo4j} reveals a tailnet node identifier (mild).
  • Risky pre-existing default: ${NEO4J_BIND:-0.0.0.0} — if a non-internal network is ever re-added (the PR's own xfail guard), neo4j would publish on all interfaces.
  • Good: no secrets/tokens/credentials anywhere — TAILSCALE_AUTHKEY is flow-controlled via :? and never printed ("Knuckles"/"elder-melchor" appear only in prose, and tailscale digest pin + cap_drop: ALL + no-new-privileges + no published ports + read-only serve config are correctly enforced by test_neo4j_tailnet_forwarder.py.

3. VERDICT

REQUEST_CHANGES — the diff implements a different design than the title/body describe (internal-only neo4j + tailnet forwarder vs "publish via pmoves_external"), so the description is untrustworthy and the newly added forwarder subsystem ships with an unresolved make-target failure/recreate risk, inert ports: on neo4j, and internal hostname/subnet disclosures in a public repo.

reviewer chain
tier reviewer model outcome detail
1 spark-local - not-configured SPARK_REVIEW_URL is not set
2 kilo-primary kilo/z-ai/glm-5.3 ok review produced
3 kilo-alternate - not-reached

_ lane: fleet-review chain (self-hosted kvm4) - spark-local (when online) -> kilo-primary -> kilo-alternate _

@POWERFULMOVES POWERFULMOVES left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Review of the option-A rework (21526bb..9b64c67): request changes (0 P1, 1 P2). The design is sound and the reverts left no residue.

Method:

  • structural YAML diffs of the compose files against the merge-base;
  • a client-side, read-only config --no-interpolate of the base file plus the overlay, printing structural fields only;
  • the targeted structural tests: 71 passed, 35 xfailed.

P2: up-neo4j-tailnet silently recreates the live Neo4j

The forwarder has depends_on: neo4j, and this PR changes neo4j's config (the graph_front network plus the blocklist env). So the first up-neo4j-tailnet after merge would bring up the dependency and recreate pmoves-neo4j as a side effect: gracefully and on the same volume, but unannounced, and bypassing the Phase 1b dry-run and MATCH gates. This is reasoned from compose's behaviour with a diverged dependency, not measured.

Fix:

  1. Make neo4j's recreate an explicit runbook step, gated by a dry run that must show a Recreate of pmoves-neo4j only, plus creation of the graph_front network.
  2. Give the forwarder target --no-deps.
  3. Add a preflight to the forwarder target that fails if pmoves-neo4j is not yet attached to pmoves_graph_front.

Verified clean

  • graph_front is internal, compose-managed, declared the same way as app, bus and data, and uses a CIDR (172.30.9.0/24) found nowhere else in the repo.
  • The forwarder: networks exactly [pmoves_external, pmoves_graph_front]; no ports; no network_mode; all capabilities dropped; no-new-privileges; the serve config mounted read-only.
    • The serve config forwards only TCP 7687 to neo4j:7687, with no Funnel. In userspace mode the handler lives in tailscaled's netstack on the tailnet address, so containers on pmoves_external cannot reach Neo4j through it (reasoned from Tailscale's design, not measured).
    • graph_front's members are exactly neo4j and the forwarder.
  • Residue: neo4j differs from main only by +graph_front and the blocklist env line. pmoves_external is gone from it. Ports, image and volumes are unchanged.
  • Tests: exact equality, not vacuous (91 published services parsed). External networks now resolve through name: to their create sites.

P3

  • Network registry: add rows for 172.30.8 (db_egress) and 172.30.9 (graph_front) to the subnet table in DOCKER_NETWORK_HARDENING.md. Rule 2 makes them permanent.
  • A stale comment says neo4j must resolve on pmoves_data; it resolves on graph_front.
  • The auth key:
    • It sits in the container environment, visible to docker inspect.
    • The same variable feeds three Tailscale sidecars, so anyone holding it can enrol a node as tag:neo4j.
    • Use a dedicated variable holding a one-off, pre-authorised key scoped to tag:neo4j, and set TS_AUTH_ONCE=true.
  • The state volume name may match the volume-reset pattern for neo4j. Rename it or document that.
  • The create-site parser stops at a newline, so a multi-line docker network create with --internal on a continuation line would be missed. Join continuation lines.
  • Neo4j's dead ports: (inert on internal-only networks) could be dropped, together with NEO4J_BIND. That is the operator's decision, already noted in the PR body.

…ey; review P3s (#3201)

Review of #3201 (REQUEST-CHANGES, 1 P2).

P2: `up-neo4j-tailnet` would have RECREATED the live pmoves-neo4j through
`depends_on: neo4j`, because this PR changes neo4j's config (it joins
pmoves_graph_front).
- The target now runs `up -d --no-deps neo4j-tailnet`.
- A names-only preflight REFUSES unless the running pmoves-neo4j is already
  attached to pmoves_graph_front, which only a deliberate, gated Neo4j
  recreate does (the runbook step in the PR body).
- `depends_on` stays, for full-stack ordering only.
- Tests: a structural check (--no-deps present, preflight before the up);
  and a behavioural one, running real make with build_stub_env stubs. Before
  the recreate it REFUSES and no compose `up` runs; after it, every `up` is
  --no-deps.

P3:
- DOCKER_NETWORK_HARDENING.md inventory rows for pmoves_db_egress
  (172.30.8.0/24) and pmoves_graph_front (172.30.9.0/24). Its "Six networks"
  line was already stale (7 listed); now nine.
- The overlay comment is fixed: `neo4j` resolves on pmoves_graph_front, not
  pmoves_data.
- The state volume is renamed to `graphfront-tailnet-state`. Any name
  containing "neo4j" (including the suggested pmoves_tailnet-neo4j-state)
  still matches volume-reset's ^pmoves_.*neo4j, so `make volume-reset
  SERVICE=neo4j` would have wiped the forwarder's tailnet identity. A test
  asserts it cannot match.
- The create-site parser joins backslash-continued lines. The old regex
  stopped at the newline and missed an --internal on a continuation line
  (demonstrated). New test.
- Key scoping: a DEDICATED variable, NEO4J_TAILNET_AUTHKEY (a one-off,
  pre-authorised key scoped to tag:neo4j), plus TS_AUTH_ONCE=true, with the
  :? guard kept. Registering it in the secrets manifest is an OPERATOR step:
  the manifest path is zero-access (even a protection check naming it was
  refused).

Targeted: 157 passed, 35 xfailed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the docs Documentation label Sep 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Docker Hardening Validation

Hardening Validation Report

Validated: Sun Sep 27 08:42:33 UTC 2026

Services Checked

PMOVES.AI compose hardening ratchet

[INFO] Checking: /home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.hardened.yml
[INFO] Merged onto (base-first):
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.comfyui.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.ultimate-tts-studio.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.archon.submodule.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.apps.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.activepieces.yml

[INFO] Validating: agent-zero
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: archon
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: channel-monitor
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: comfy-watcher
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: deepresearch
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: extract-worker
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: ffmpeg-whisper
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: grayjay-plugin-host
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: hi-rag-gateway-gpu
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: hi-rag-gateway-v2
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: hi-rag-gateway-v2-gpu
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: invidious-companion-proxy
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: jellyfin-bridge
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: langextract
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: media-audio
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: media-video
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: mesh-agent
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: nats-echo-req
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: nats-echo-res
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: notebook-sync
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: p7-room-orchestrator
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: pdf-ingest
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: pmoves-yt
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: presign
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: publisher-discord
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: render-webhook
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: retrieval-eval
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: supaserch
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

===================================
Summary: 28 services x 5 properties = 140 evaluated; 140 passed + 0 findings = 140
Findings: 0 (0 baselined, 0 new)

@github-actions

Copy link
Copy Markdown
Contributor

Fleet review: kilocode (kilo/z-ai/glm-5.3)

reviewer: tier 2/3 kilo-primary - model kilo/z-ai/glm-5.3 - fell back after tier 1 spark-local: not-configured

  1. CORRECTNESS
  • mk/neo4j-tailnet.mk invokes $(DC) -f docker-compose.neo4j-tailnet.yml with a single file, but the overlay (docker-compose.neo4j-tailnet.yml:73-81) declares no top-level networks: and has depends_on: neo4j. With a bare docker compose, both fail model validation ("refers to undefined network", "depends on undefined service") at the config -q preflight, so up-neo4j-tailnet dead-ends with the misleading "deliver the key" hint. Only correct if $(DC) bakes in STACK_FILES — not visible in this diff; must be verified.
  • The forwarder test stubs docker/make (_destructive_docker_guard, INSPECT_BEHAVIOUR), so CI green cannot catch the above; also tests/_destructive_docker_guard.py is not added in this diff — must already exist or collection errors.
  • Core forward path is runtime-unproven: non-localhost TCPForward via TS_SERVE_CONFIG rests on a source-read of a fork (PR admits this); the healthcheck checks login only (tailscale status), not the forward — a broken forwarder reports healthy.
  • Blocklist nuance: the stack keeps NEO4J_server_config_strict__validation_enabled=false, so an unrecognized internal.* key would be silently dropped (not failed); "strict validation recognises the key" evidence came from strict-enabled throwaway runs, not this compose config. Digest pin mitigates; apoc.load.* beyond apoc.load.json and HTTP redirects unverified (acknowledged).
  • Fleet test parses STACK_FILES and docker network create sites with strict regexes at module import — any Makefile reformat fails the whole module at collection; heuristics (names[-1], mk/Makefile only) are brittle but scoped as stated.
  • Minor: preflight-1 2>/dev/null maps any docker failure (daemon down, perms) to "container not found"; status/down targets swallow all errors (|| true) so they never report failure — acceptable for helpers, but the status target always exits 0.
  • Shell itself is clean: single-shell \-continued recipes with correct $$ escaping, guarded case in one shell, no pipes/eval (no pipefail needed), non-forced docker container rm on a stopped container. YAML: anchor use matches siblings; base/core and monolith edits are mirrored identically (sync gate reportedly green); NEO4J_*__* env→config key mapping for the blocklist is correct; xfail-strict list has a staleness guard and a positive control — good.
  1. SECURITY / TOPOLOGY (repo is public)
  • Bridge topology added: 172.30.9.0/24 + gateway 172.30.9.1 in docker-compose.base.yml, docker-compose.yml, and the hardening doc; the doc also adds a pmoves_db_egress 172.30.8.0/24 row tying it to the supabase-db tailnet port publish (feat(juicefs): Step 4 — plumb the tailnet-bound DB port the cross-node lane needs #2728). The public inventory table maps every network's subnet, internal flag, and role — the pattern pre-exists, but this PR extends it.
  • Tailnet/naming disclosure: default MagicDNS hostname pmoves-neo4j (TS_HOSTNAME), container names pmoves-neo4j/pmoves-neo4j-tailnet, ACL tag tag:neo4j (advertise-tags, tagOwners narrative); serve.json + docs disclose that fleet agents reach Neo4j bolt over the tailnet on 7687 and that the 7474 UI is deliberately not forwarded — internal exposure map in a public repo.
  • CLEAN on secrets/tokens/credentials: NEO4J_TAILNET_AUTHKEY and NEO4J_PASSWORD appear only as :? guards, never values; image digest is not a secret; no tailnet/LAN IPs or host addresses leaked beyond the RFC1918 bridge subnets above.
  1. VERDICT
    REQUEST_CHANGES — the DC -f <overlay> resolution (undefined networks/depends_on) likely breaks up-neo4j-tailnet and is masked by stubbed tests, the forward path is unverified at runtime, and the diff adds new internal topology (subnets, gateways, tailnet hostnames/tags) to a public repo.
reviewer chain
tier reviewer model outcome detail
1 spark-local - not-configured SPARK_REVIEW_URL is not set
2 kilo-primary kilo/z-ai/glm-5.3 ok review produced
3 kilo-alternate - not-reached

_ lane: fleet-review chain (self-hosted kvm4) - spark-local (when online) -> kilo-primary -> kilo-alternate _

@POWERFULMOVES POWERFULMOVES left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Re-review of 9b64c67..5889d40: approve-with-nits (the P2 is closed; no new P1 or P2)

Tests: forwarder plus fleet test files, 76 passed / 35 xfailed. The new behavioural test uses build_stub_env, and the others are structural. The steward independently re-ran the five targeted files: 151 passed / 35 xfailed.

Verified:

  • The preflight fails closed on any inspect error (missing container, daemon down, permission, wrong context). The match is an exact token, so it cannot be satisfied by a similarly named network. It prints network names only. The make escaping is valid.
  • --no-deps has no side effect on the forwarder's own volume or network attachment. The external network must already exist, or compose fails loudly. The preflight guarantees graph_front exists.
  • The runbook ordering is gated: key, then grant, then the gated Neo4j recreate, then the forwarder. The forwarder's preflight enforces the order. Network creation precedes the container, so a CIDR overlap aborts with Neo4j untouched.
  • The rest of the delta:
    • the network registry rows are correct;
    • the stale comment is fixed;
    • the dedicated key uses a fail-closed guard plus TS_AUTH_ONCE;
    • the renamed state volume cannot match the reset pattern;
    • the continuation-joining parser has a real failing-before case;
    • the 151 correction is noted in the body.

Nits, fold in before the operator runs step 4:

  • Step 4: give the literal compose dry-run command rather than "up-data-tier in dry-run form", because a make dry run of that target executes its nested make line.
  • Backup: name the mechanism (the Phase 1b stopped read-only volume copy, or neo4j-backup).
  • Rollback: write one for steps 4 and 5: bring the forwarder down, then re-up neo4j from the pre-PR compose through the same gated path. The internal graph_front network can remain.
  • The preflight message: say "could not inspect (missing, or daemon/permission error)", not "not found". Add a test for the inspect-error branch.
  • A stopped-but-attached Neo4j passes the preflight. That is harmless; note it, or check .State.Running.
  • Key lifetime: the :? guard keeps the auth key as a permanent requirement, even though it is only used once. That is the operator's call.

…e inspect-failure message (#3201 nits)

- The preflight now reads .State.Running together with the network names, in
  the same single inspect call (still no addresses). It refuses when
  pmoves-neo4j is stopped: --no-deps will not start it, and a forwarder in
  front of a stopped Neo4j fronts nothing.
- An inspect failure now says "could not inspect pmoves-neo4j (missing, or
  daemon/permission error)" instead of "not found". It is the same exit 1.
- The behavioural test covers four stub states, each checking the refusal
  reason and that no compose `up` ran: before the recreate; after it; recreated
  but stopped; inspect failing with rc 1.

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

Copy link
Copy Markdown
Contributor

Docker Hardening Validation

Hardening Validation Report

Validated: Sun Sep 27 08:47:42 UTC 2026

Services Checked

PMOVES.AI compose hardening ratchet

[INFO] Checking: /home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.hardened.yml
[INFO] Merged onto (base-first):
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.comfyui.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.ultimate-tts-studio.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.archon.submodule.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.apps.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.activepieces.yml

[INFO] Validating: agent-zero
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: archon
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: channel-monitor
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: comfy-watcher
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: deepresearch
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: extract-worker
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: ffmpeg-whisper
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: grayjay-plugin-host
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: hi-rag-gateway-gpu
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: hi-rag-gateway-v2
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: hi-rag-gateway-v2-gpu
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: invidious-companion-proxy
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: jellyfin-bridge
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: langextract
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: media-audio
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: media-video
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: mesh-agent
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: nats-echo-req
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: nats-echo-res
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: notebook-sync
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: p7-room-orchestrator
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: pdf-ingest
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: pmoves-yt
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: presign
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: publisher-discord
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: render-webhook
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: retrieval-eval
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: supaserch
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

===================================
Summary: 28 services x 5 properties = 140 evaluated; 140 passed + 0 findings = 140
Findings: 0 (0 baselined, 0 new)

@github-actions

Copy link
Copy Markdown
Contributor

Fleet review: kilocode (kilo/z-ai/glm-5.3)

reviewer: tier 2/3 kilo-primary - model kilo/z-ai/glm-5.3 - fell back after tier 1 spark-local: not-configured

I've reviewed the full diff (702 lines, all files). Here is the review.

1. CORRECTNESS

  • Unverified core mechanism: the entire forwarder depends on ServeConfig.TCPForward: "neo4j:7687" dialing a non-localhost, Docker-DNS-resolved target. The PR itself admits this is source-reasoned from the fork, not runtime-tested; containerboot's CLI path explicitly refuses non-localhost targets. If SystemDial has any localhost/serve-domain gate in the pinned 1.102.5 image, the forwarder accepts tailnet connections and fails to dial — a silent, healthcheck-invisible failure (tailscale status stays healthy while forwarding is dead). The target should probe the forwarded port after bring-up before this ships; the runbook step only says "bolt from a fleet node" — add a failure-mode check (non-granted refused vs. connection refused).
  • Blanket blocklist can break existing imports: 0.0.0.0/0,::/0 in internal.dbms.cypher_ip_blocklist (docker-compose.core.yml:1215 area, mirrored in docker-compose.yml) kills every http(s) LOAD CSV and — per the PR's own admission — probably apoc.load.json, with no allowlist. Before this diff, LOAD CSV demonstrably reached sibling containers; any existing job importing from an internal URL (minio/presign etc.) breaks with a policy refusal. The PR presents it as pure defense-in-depth with no inventory of current LOAD CSV users.
  • internal.* config is unsupported-by-vendor and only "stabilized" by the image digest pin — a behavior-pinned-on-an-internal-knob risk; strict-validation=false already in compose, so a future image could silently drop the key. NEO4J_PLUGINS rename (APOC currently live via deprecated NEO4JLABS_PLUGINS) deferred is fine.
  • down-neo4j-tailnet keeps graphfront-tailnet-state with restart: unless-stopped container removed — fine, but docker container rm ... || true also masks real failures (e.g., daemon down); acceptable for stop/rm semantics.
  • Test-quoting nit: test_the_make_targets_never_nest_make only checks $(MAKE), not a literal make in recipes; also "norecipe" detection includes tab-prefixed @# comment lines (harmless but the parser is imprecise).
  • CHILD_OF TODO: KNOWN_VIOLATORS contains supaserch — looks like a typo ("supasearch"?); it's guarded by test_known_violators_is_not_stale, so it fails rather than passes silently if wrong — verify said test actually ran in the "146 passed" claim (PR body's own count was once wrong: 151 vs 157).
  • Make preflight logic (case " $$out " in *" pmoves_graph_front "*)) is quoting-correct; hardcodes pmoves-neo4j/pmoves-neo4j-tailnet container names in three places (mk + tests) — a rename in core compose breaks preflight fail-closed, not fail-open. OK.
  • Both docker-compose.base.yml and docker-compose.yml now declare pmoves_graph_front identically (subset must keep passing compose-split-check); the <<: *network-internal-only merge plus explicit name:/ipam: matches the sibling networks' style. OK.

2. SECURITY / TOPOLOGY

  • Subnet topology published: the diff adds 172.30.9.0/24, gateway 172.30.9.1 (base.yml, docker-compose.yml), the serve JSON, the hardening guide, and newly documents pmoves_db_egress 172.30.8.0/24. This is internal-bridge RFC1918 layout consistent with the file's pre-existing inventory (172.30.4–7 rows), so it matches accepted practice in this public repo — but it is still map-your-LAN-by-subnet exposure; only .9/.8 are new.
  • Hostnames disclosed: pmoves-neo4j (both the container_name and TS_HOSTNAME default) is an internal-service hostname that becomes the MagicDNS node name; tag:neo4j discloses tailnet ACL structure. Low severity; call out for the public record.
  • neo4j-tailnet-status makes the node's Tailscale IP + MagicDNS name operator-visible — runtime only, not repo content.
  • No secrets in the diff: auth is ${NEO4J_TAILNET_AUTHKEY:?...} and ${NEO4J_PASSWORD:?...} — :?-guarded, values never rendered; make preflights print state/network names only, inspect stderr discarded; nothing echoable in tests.
  • The forwarder itself (cap_drop ALL, no-new-privileges, digest-pinned, bolt-only forward, off pmoves_data, no host ports, no netns share) is the cleanest shape available for this policy.
  • Not clean as to topology (subnets/hostnames above), clean as to secrets.

3. VERDICT

REQUEST_CHANGES — the one load-bearing behavior (TCPForward to a non-localhost target in userspace) is admitted-untested, and the blocklist may break existing LOAD CSV/apoc imports; both need runtime proof at the live step (or a scoped-allowlist fallback) before merge/approve.

reviewer chain
tier reviewer model outcome detail
1 spark-local - not-configured SPARK_REVIEW_URL is not set
2 kilo-primary kilo/z-ai/glm-5.3 ok review produced
3 kilo-alternate - not-reached

_ lane: fleet-review chain (self-hosted kvm4) - spark-local (when online) -> kilo-primary -> kilo-alternate _

@github-actions

Copy link
Copy Markdown
Contributor

Docker Hardening Validation

Hardening Validation Report

Validated: Sun Sep 27 08:56:30 UTC 2026

Services Checked

PMOVES.AI compose hardening ratchet

[INFO] Checking: /home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.hardened.yml
[INFO] Merged onto (base-first):
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.comfyui.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.ultimate-tts-studio.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.archon.submodule.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.apps.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.activepieces.yml

[INFO] Validating: agent-zero
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: archon
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: channel-monitor
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: comfy-watcher
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: deepresearch
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: extract-worker
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: ffmpeg-whisper
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: grayjay-plugin-host
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: hi-rag-gateway-gpu
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: hi-rag-gateway-v2
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: hi-rag-gateway-v2-gpu
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: invidious-companion-proxy
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: jellyfin-bridge
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: langextract
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: media-audio
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: media-video
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: mesh-agent
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: nats-echo-req
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: nats-echo-res
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: notebook-sync
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: p7-room-orchestrator
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: pdf-ingest
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: pmoves-yt
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: presign
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: publisher-discord
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: render-webhook
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: retrieval-eval
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: supaserch
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

===================================
Summary: 28 services x 5 properties = 140 evaluated; 140 passed + 0 findings = 140
Findings: 0 (0 baselined, 0 new)

@github-actions

Copy link
Copy Markdown
Contributor

Fleet review: kilocode (kilo/z-ai/glm-5.3)

reviewer: tier 2/3 kilo-primary - model kilo/z-ai/glm-5.3 - fell back after tier 1 spark-local: not-configured

No repo checkout is available — reviewing the diff itself. Quick scans for leaks and consistency:
One quick verification of a load-bearing assumption in the new fleet test — that PyYAML safe_load resolves the <<: *network-internal-only merge key the internal flags depend on, with explicit keys overriding the anchor:
No Python/PyYAML in this sandbox — merge-key behavior noted from knowledge (and implicitly proven by the PR's own green gate counts, since the fleet test asserts pmoves_graph_front internal=True via safe_load).

1. CORRECTNESS

  • Overlay is not standalone-renderable. docker-compose.neo4j-tailnet.yml has no top-level networks: and references depends_on: neo4j — it only validates because $(DC) already carries the default STACK_FILES (the P2 narrative "compose sees neo4j's config changed" and the rendered-evidence table imply it does). Anyone running bare docker compose -f docker-compose.neo4j-tailnet.yml gets undefined-service/network errors; worth one doc line pinning that the overlay must run via $(DC) only.
  • down-neo4j-tailnet fails on a clean node. The stop line has no || true; if the forwarder was never created, compose exits nonzero and make aborts before the deliberately-tolerant container rm. The "no such container is fine" comment holds only for the rm, not the stop.
  • Preflight gates .State.Running, not health. up will start a forwarder in front of a running-but-unhealthy/recovering Neo4j; runbook 4f covers it operationally, and the stopped-vs-running fix (ea6461e) is correct.
  • TCPForward: "neo4j:7687" remains source-reasoned, not runtime-proven (author flags this; CLI path refuses non-localhost, only the SystemDial serve path works). The behavioral tests cannot regress-catch this; keep it as a hard live-step gate as planned.
  • Blocklist coverage is partial. Verified for LOAD CSV only; apoc.* unrestricted is live (PR finding 1) and other apoc.load.*/redirects are unverified. Also internal.* is unsupported config — with strict validation off, removal/renaming in any future image digest is silent, not a startup failure.
  • Guard assertions dropped = unpinned invariant. Nothing now pins the TS_AUTHKEY line to a :? funnel var; swapping :? → :- or a literal keeps all tests green (acknowledged steward ruling, but it's a real drift hole).
  • _create_sites heuristics are brittle: name = last non-flag/non-digit token; post-name args (e.g. --label k=v after the name) or $(DOCKER)-style invocations would be silently missed. Bounded today by the site-existence asserts + positive control. Also double-check "supaserch" in KNOWN_VIOLATORS is the real service key, not a typo the stale-name gate would have caught.
  • Stub behavior: the behavioural stub exits 0 for every non-inspect docker call, so config -q/up failure paths (missing key, bad overlay) are unexercisable; the 4-state test proves preflight-1 logic only — read the coverage claim that narrowly.
  • Verified fine: make recipe shell (quoted $$out, 2>/dev/null, fail-closed || { } chains, no pipelines needing pipefail); preflight prints network names only, never addresses; YAML <<: *network-internal-only merge with explicit name:/ipam overrides correctly (explicit keys beat merge keys); ports: blocklist env-name decomposition (cypher__ip__blocklist → cypher_ip_blocklist) is correct; identical hunks in core/yml and base/yml keep the split-check pair in sync; no scope creep (all files serve the PR purpose; the db_egress doc row is registry backfill, justified).

2. SECURITY / TOPOLOGY

  • No secrets/tokens/credentials leak: ${NEO4J_TAILNET_AUTHKEY} appears by name only, :? fails closed at parse, config -q never echoes values; no tailnet IPs (100.64/10), no ts.net domain, no LAN/public IPs, no key material in the diff.
  • FLAGGED (low): adds internal bridge topology to a public repo — 172.30.9.0/24 + gateway 172.30.9.1 in both compose files, plus docs rows publishing pmoves_db_egress 172.30.8.0/24 (an existing live network newly documented) and graph_front. Non-routable RFC1918 Docker bridges, and consistent with the .4–.7 rows already public in the same files — but this PR does keep enlarging the public map of internal layout.
  • FLAGGED (informational): TS_HOSTNAME default pmoves-neo4j fixes the tailnet node name, and --advertise-tags=tag:neo4j reveals the ACL tag scheme; no tailnet domain/IP itself is present.
  • neo4j-tailnet-status prints tailscale status (self line includes the node's tailnet IP) to the operator's terminal only — not committed; acceptable.
  • Ports are standard 7474/7687, forwarder publishes nothing, bolt-only serve is the intended design; the 0.0.0.0 default bind on neo4j's ports: is pre-existing and inert under internal-only networks. Other than the two flags above: clean.

3. VERDICT: APPROVE

Design is least-privilege and evidence-driven, every risky action is gated (inspect preflight + --no-deps, strict-xfail fleet test, operator runbook with rollback), and all findings are low-severity robustness/coverage nits plus precedent-consistent Docker-internal subnet data — no secrets or genuinely sensitive topology disclosed.

reviewer chain
tier reviewer model outcome detail
1 spark-local - not-configured SPARK_REVIEW_URL is not set
2 kilo-primary kilo/z-ai/glm-5.3 ok review produced
3 kilo-alternate - not-reached

_ lane: fleet-review chain (self-hosted kvm4) - spark-local (when online) -> kilo-primary -> kilo-alternate _

Regenerated with `python pmoves/tools/topology/build_docker_matrix.py`
(85 real services, 56 overlays). The diff is +20 lines, and all of it is
#3201's own change:
- the new neo4j-tailnet overlay;
- its unmapped keys (cap_drop, graphfront-tailnet-state, neo4j-tailnet,
  security_opt);
- pmoves_graph_front as an unmapped key in the base and main overlays.

No unrelated drift.

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

Copy link
Copy Markdown
Contributor

Docker Hardening Validation

Hardening Validation Report

Validated: Sun Sep 27 09:07:39 UTC 2026

Services Checked

PMOVES.AI compose hardening ratchet

[INFO] Checking: /home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.hardened.yml
[INFO] Merged onto (base-first):
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.comfyui.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.ultimate-tts-studio.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.archon.submodule.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.apps.yml
/home/runner/work/PMOVES.AI/PMOVES.AI/pmoves/docker-compose.activepieces.yml

[INFO] Validating: agent-zero
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: archon
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: channel-monitor
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: comfy-watcher
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: deepresearch
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: extract-worker
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: ffmpeg-whisper
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: grayjay-plugin-host
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: hi-rag-gateway-gpu
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: hi-rag-gateway-v2
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: hi-rag-gateway-v2-gpu
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: invidious-companion-proxy
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: jellyfin-bridge
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: langextract
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: media-audio
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: media-video
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: mesh-agent
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: nats-echo-req
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: nats-echo-res
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: notebook-sync
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: p7-room-orchestrator
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: pdf-ingest
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: pmoves-yt
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: presign
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: publisher-discord
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: render-webhook
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: retrieval-eval
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

[INFO] Validating: supaserch
[PASS] user: user: 65532:65532
[PASS] read_only: read_only: true
[PASS] cap_drop: cap_drop: [ALL]
[PASS] no_new_privileges: security_opt: no-new-privileges:true
[PASS] resource_limits: deploy.resources.limits: ['cpus', 'memory']

===================================
Summary: 28 services x 5 properties = 140 evaluated; 140 passed + 0 findings = 140
Findings: 0 (0 baselined, 0 new)

@POWERFULMOVES

Copy link
Copy Markdown
Owner Author

Steward checkpoint (B850-CLAUDE, restart stop point, 2026-09-27 05:13 EDT). Head b1e644e44 (regenerated docker_matrix, +20 lines, all from this PR). Operator approved the merge at 04:55. At this stop point: python-tests + kilo-review pending; Kilo Code Review (external app) failed — the only waiver planned. Resume: once CI settles, ADMIN_REVIEW_BYPASS=1 make -C pmoves pr-closeout-audit PR=3201 EXPECTED_HEAD=<full sha>, then pr-closeout-merge with ALLOW_ADVISORY_FAILURE="Kilo Code Review". After merge: the operator withdraws the compose:pr:3201 grant, provisions NEO4J_TAILNET_AUTHKEY + the tag:neo4j grant, then runs the gated bring-up.

@github-actions

Copy link
Copy Markdown
Contributor

Fleet review: kilocode-alternate (kilo/z-ai/glm-5.2)

reviewer: tier 3/3 kilo-alternate - model kilo/z-ai/glm-5.2 - fell back after tier 1 spark-local: not-configured; tier 2 kilo-primary: empty

Let me verify a few key assumptions by locating the actual repo files referenced (Makefile DC definition, the network-internal-only anchor, STACK_FILES, and the yt-egress precedent overlay).
I've reviewed the diff. Here are my findings.

1. CORRECTNESS

  • Overlay network resolution (non-blocking, by design): docker-compose.neo4j-tailnet.yml declares no top-level networks: section; the service references pmoves_graph_front (compose-managed, internal) and pmoves_external (external:true), both defined only in docker-compose.base.yml/docker-compose.yml. This is the same shape as yt-egress, and it is safe: depends_on: neo4j forces the default STACK_FILES to be loaded — a bare docker compose -f overlay … config would fail loudly (undefined neo4j) rather than silently auto-create wrong networks. Defining pmoves_graph_front in both base.yml and docker-compose.yml is a defensive choice against which stack file is included. Recommend (improvement, not blocker) adding a self-documenting networks: block in the overlay (external: true, real name:) so the attachment is explicit and mis-use fails sooner.
  • LOAD CSV blocklist silent-no-op: NEO4J_internal_dbms_cypher__ip__blocklist=0.0.0.0/0,::/0 is added while NEO4J_server_config_strict__validation_enabled=false stays false. With strict validation OFF, an unrecognized internal.* key is silently swallowed (no startup error), so if a future Neo4j build drops this unsupported key the blocklist quits working with no signal. The image is pinned by digest (the mitigant), and this is defense-in-depth only (the primary control is internal-only networking), but the live config cannot self-detect the no-op — consider a runtime assertion or enabling strict validation for this pinned image.
  • Duplicate env across core.yml + docker-compose.yml: the blocklist line is added to both neo4j definitions — harmless (compose merges environment by key, last wins; no duplication at runtime), so only one of the two files matters per STACK_FILES selection; confirm the test invariants run against whichever is the live one.
  • mk/neo4j-tailnet.mk preflight shell is safe: single-quoted Go template, double-quoted case vars, exit 1 inside each case, no pipes (no pipefail needed), inspect prints .State.Running + network names only (never addresses). —→ docker_container.mk lines are quoted correctly.
  • Behavioural test is shallow: the stub only intercepts docker inspect; $(DC) config -q and up -d --no-deps hit the stub and return rc=0 with no output, so the 4-state test verifies the preflight/refusal logic and that recorded up calls contain --no-deps, but does not exercise the real compose :? guard or real --no-deps compose semantics. Acceptable for intent; do not read it as "tested live".
  • Parser brittleness: the STACK_FILES \?= \\<... regex in both test files will break on any Makefile reformat (tabs vs spaces, changed continuation) — both tests depend on it.
  • down-neo4j-tailnet uses a non-forced docker container rm after compose stop (|| true on removal failure). If stop times out, rm fails silently and a stopped container lingers — minor, matches the body's stated intent.

2. SECURITY / TOPOLOGY

  • No secrets / tokens / credentials in the diff. Auth key is ${NEO4J_TAILNET_AUTHKEY:?…}, password is ${NEO4J_PASSWORD:?…} — env-var references with fail-closed :? guards; no values are printed or hard-coded. The serve config and blocklist contain no secrets. Clean on credentials.
  • Internal-bridge topology present, consistent with existing repo convention: subnets 172.30.8.0/24 and 172.30.9.0/24 and gateway .1 IPs are added to the compose files and the DOCKER_NETWORK_HARDENING.md inventory table — exactly mirroring the repo's already-public .1–.7 inventory. This is not a new class of exposure; it extends the established public practice. Worth noting since the repo is public, but not introduced as a fresh leak here.
  • No tailnet/LAN IPs, no host IPs/router IPs, no internal host hostnames in the diff. The default tailnet node name pmoves-neo4j is the forwarder's own identity (not an internal host); tag:neo4j advertises a tag, not topology.
  • Least privilege is correct: cap_drop: [ALL] + no-new-privileges + userspace Tailscale (no TUN, no NET_ADMIN) + no network_mode + no host ports + bolt-only forward (7474 UI not exposed) + read-only serve-config mount.
  • Scope note: DOCKER_NETWORK_HARDENING.md gains a registry row for pmoves_db_egress, which is from feat(juicefs): Step 4 — plumb the tailnet-bound DB port the cross-node lane needs #2728 (a different PR). Tiny scope creep — documenting another PR's network here — defensible since the same inventory line is being corrected, but technically unrelated to fix(neo4j): internal-only Neo4j fronted by a Tailscale forwarder on a dedicated graph front; LOAD CSV blocklist; fleet test #3201's purpose.
  • PR body (untrusted, not the diff) discloses substantial topology — host context ("Knuckles", "elder-melchor"), MagicDNS naming shape, backup-volume naming knuckles-neo4j-backup-…, violator counts. That's the author's choice in the PR description and lands in PR history; flag for platform visibility expectations. The diff itself stays clean of new credential/token disclosure.

3. VERDICT

APPROVE — diff is correct and security-clean (no secrets/tokens; least-privilege forwarder; networks resolve via the stack and a bare-overlay run fails loud, not silent). The blocklist's strict-validation-off silent-no-op, the missing self-documenting networks: block, test-parser brittleness, and the db_egress doc scope nibble are improvements worth a follow-up, not blockers.

reviewer chain
tier reviewer model outcome detail
1 spark-local - not-configured SPARK_REVIEW_URL is not set
2 kilo-primary kilo/z-ai/glm-5.3 empty produced no review output (model 'kilo/z-ai/glm-5.3')
3 kilo-alternate kilo/z-ai/glm-5.2 ok review produced

_ lane: fleet-review chain (self-hosted kvm4) - spark-local (when online) -> kilo-primary -> kilo-alternate _

@POWERFULMOVES
POWERFULMOVES merged commit dbadfc0 into main Sep 27, 2026
46 of 47 checks passed
@POWERFULMOVES
POWERFULMOVES deleted the ops/knuckles-neo4j-external branch September 27, 2026 12:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

compose Compose files / service Dockerfiles config pmoves/config(s)/ changes docs Documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant