Reverse proxy: Traefik on demand — implementation plan #281
Open
opened 2026-11-01 08:04:49 +00:00 by kayg
·
192 comments
No Branch/Tag specified
main
j470b-server-mesh-rejoin
mesh-compat-382
j444b-d381-key-wins-flood
j461b-macos-utun-reader-thread
j475b-remove-host-motion
fix-reconnect-new-link
j481-d380
dev
j479-mesh-soak
fix/j476-host-facts
j475-motion-pill-split
j470-mac-node-join
root-upgrade-d376
j474-review-merge
latency-targets-120hz
nightly-suite-shards
gotatun-pin-j469
macos-utun-reader-thread
fix-flood-echo-stall
wip/j416
motion-fast-ease-out
mesh-addhost-j259
takeover-close-terminals-d368
kbd-instant-followups
fix-trust-key-double-click
nightly-suite-partial-report
appwin-icons-j207
rusqlite-slice1
swipe-content-first-d374
mesh-mtu-381
addhost-instant-open
glass-contrast-d365-mock
glass-contrast-d365
signout-followups-442
fix-startup-toast-red
research-gotatun-matrix-j317
themes-sync-j304
fix-load-flakes
fix-nix-zig-patch
fix-compute01-browser-baseline
ratchet-size-baseline-419
predict-auto-d130
revoke-coverage-d367
nightly-suite-queue-timeout
nightly-browser-suite
fix-rustsec-2026-0459
fonts-ioskeley-obel
log-retention-d375
fix-kbd-no-anim-427
fix-dev-red-specs-2
takeover-proof-305-j205
wip/j410-server-pump
ui-consistency-d372
fix-osc52-clipboard
libghostty-vt-1.0
forget-retired-host-d369
j404-merge-426
fix-setup-lock-race
fix-touch-select-426
revoke-access-report-ui
fix-predictive-echo-specs
fix-selinux-agent-exec
wip/j395-checkpoint2
wip/j395
j391-review-scratch
ui-consistency-j195-deferred-rebased
fix-rustc-paths-421
decisions-wording-341
fix-install-suite-red
fix-dev-rtt-specs
fix-pending-removal-align
fix-privileged-skip
fix-install-suite-404
review-j368
mesh-dns-j254
revoke-access-report-d356b
revoke-privilege-round5
fix-removal-confirm
review-j360
gotatun-rotation-367
deps-refresh-340
review-j354
review-J351
review-j350
addhost-takeover-d359
revoke-report-items-347
revoke-end-sessions-d362
review-j348
size-mesh-ssh-346
size-trim-337
revoke-access-diff-d356
j259-pre-rebase-b06c5a6
j305-ended-at-start-spec
addhost-sudo-consent
fix-layout-real-specs-411
fix-shell-open-411
fix-console-hosts-373
fix-shell-exit
fuzz-host-name-353
fix-host-details-410
macos-filter-j197
research-gotatun-tricks
revoke-p3-j216l
stale-state-pill
revoke-followups-j216k
revoke-progress-j216e
backup/macos01-20261106/lume/j197
backup/macos01-20261106/lume/appwin-icons-j207-stash0
backup/macos01-20261106/claude/stash-0
backup/macos01-20261106/claude/stash-1
backup/macos01-20261106/claude/J80-macos-s1
backup/macos01-20261106/claude/J80
backup/macos01-20261106/claude/J80-slice1-20261102
backup/macos01-20261106/claude/J16b-appwin-on-dev
backup/macos01-20261106/claude/appwin-launch-idle-146
backup/macos01-20261106/claude/J80-s1-run
wip/j259
trust-285-p3c
runtime-lock-j216f
mesh-ssh-p3-j175
tun-proof-p3b
trust-285-p3
terminal-polish-j271
tun-proof-p3
ssh-install-flakes-j216i
fuzz-fixes-j264
trust-285-a
trust-285-b
restart-oversized-fixture-j198
backup/macos01-20261106/lume/appwin-icons-j207
restart-bound-j216h
up-preflight-p3-j198
backup/macos01-20261106/lume/j251
up-preflight-j198
wip/up-preflight-j198-j245-p2
wip/up-preflight-j198-j245-p1
agent-fsync-offloop-j196
wip/up-preflight-j198-j245
settings-offscreen-j231
wip/up-preflight-j198-j226-inspection
wip/j103f
themes-j206
wip/up-preflight-j198-j226
flakes-j216c
backup/macos01-20261106/claude/j197
flakes-j216b
backup/macos01-20261106/claude/j197-red
wip/up-preflight-j198-review
flakes-j216
backup/macos01-20261106/claude/appwin-icons-j207
engineering-j208b
appwin-mock-j209
toast-stack-j165
engineering-j208
decisions-d351
decisions-d350
decisions-d349
appearance-presets-mock-j129
decisions-d348
docs-audit-spec
doc-coverage-baseline-raise
decisions-d347
decisions-d346b
decisions-d346
j88h-profile
ui-consistency-j195-deferred
ui-consistency-j195
engineering-practices-j194
preferred-login-ui-j166
signin-pools-263-j174
macos-node-265-s1
decisions-d345
decisions-d344
proxy-cgroup-j164
verify-quick-fuzz-329
proxy-start-io-j168
timing-flakes-j163
up-263-s1
mcp-events-j147
verify-quick-fuzz-j158
proxy-p3-j159
agent-resource-j157-detect
wip/j103d-final
remote-cancel-hang-j155
proxy-traefik-281-j62d
j149-verify-quick
proxy-traefik-281-j62b
j150-drop-unnamed
wip/j79d-red-r4
wip/j103d-d332
rootless-toast-j142
wip/j103d-head
wip/j103d
j125c-mcp
macos-build-j136
backup/macos01-20261106/claude/j136
wip/j79d-red-cli
wip/j79d-red-server
j121-link-liveness
j118c-tracer-failures
wip/j79c-head
release-desktop-j131
j124c-signed-macos
wip/j79c-red-p3
wip/j79c-red-p23
wip/j79c-red-p22
merge-j128-check
perf-deferred-j128
sidebar-toggle-260
j127-shortcut-changed-dot
j125-mcp
j118-tracer-failures
j126-account-summary
settings-sections-mock-j92
j120-pinch-lock
sidebar-292
backup/macos01-20261106/claude/j120
taste-2026-11-03
row-actions-242-215
backup/macos01-20261106/claude/stash-0-untracked
backup/macos01-20261106/claude/J80-265-s1-run
backup/macos01-20261106/claude/J80-macos-s1-20261102
j112-canvas-commands
ui-no-overshoot-scroll
fix-recovery-restart
backup/macos01-20261106/claude/stash-1-untracked
taste-2026-11-02
size-baseline-ratchet
machine-cards-structure-j108
j109-boot-copy-mockups
fix-dev-red
wip/up-263-s1
j95b-settings-rows
proxy-traefik-281
j95-option-rows
docs-human-j91
docs-keys-palette
fix-flood-1013-270-j38d
mesh-ssh-s1
mesh-ssh-s0
addhost-d291-tunnel
test-stability-load
mesh-relay-220
repro-296
shim-thp-off-24
mesh-pins-258-v2
mesh-dns
signin-d311-merged
docs-pages
ui-size-283
fix-ui-batch-specs
wip/j12b-edge-update
gate-on-arm
fix-rename-sync-293
trust-285-j60b
fix-focus-ring-clip
integ-ui-1101
edge-update-v2
signin-d311
taste-2026-11-01
layout-per-tab-250
ui-approved-splitopt-224
ui-approved-splitopt-224-on-dev
icon-text-centre-282
ui-approved-189-212-216
research-reverse-proxy
transfer-budget-320
review-mesh-pins-258-r2
sidebar-pill-259
review-mesh-relay-220
wip/j12-edge-update-v2
nix-package
status-crash-loop-237
fix-dev-test-failures
fix-fixture-socket-path
wip/mesh-relay-220-pre-dev-advance-rebase
fix-canvas-side-clip
research-head-to-head
frosted-real-256
review-mesh-pins-258
wip/mesh-relay-220-pre-final-rebase
review-layout-per-tab-250-r2
broad-suite-triage
nix-devshell
review-status-crash-loop-237-r3
wip/nix-devshell-pre-p3
taste-frosted-real
wip/code-serve-demo-2026-10-30
wip/code-serve-glass-2026-10-30
wip/code-serve-clusters-2026-10-30
wip/code-merge-release94-dev-2026-10-30
wip/rproxy-delta-scratch-2026-10-30
wip/stale-note-257-staged-2026-10-30
wip/d241-size-staged-2026-10-30
review-broad-suite-207
wip/mesh-pins-258-pre-split
wip/mesh-relay-220-pre-rebase
mesh-measure-220
plan-211-update
review-edge-autoupdate-r3
perf24-reconnect
review-stale-note-r2
release-prod-live
d241-shell-marker
review-mesh-pins-server-258
review-revocation-r3
review-setup-token-r2
review-sync-journal-r3
wip/review-mesh-pins-258-prune-review-1995fee46
frosted-scroll-256
sync-journal-r3
revocation-per-account-wip
review-tokio-asyncfd-0386
review-sync-p3s
wip/stash-8by64-stash0-2026-10-30
mesh-drag-r2
setup-token-data-wip
release-9-4
auth-busy-1013
mesh-view-overlap
sync-p3s
review-auth-busy-wake
review-readd-r8-wip
mesh-drag-bounds
audit-auth-93
app-windows-stream-start
review-authority-cap-r3
app-windows-r7-measure
readd-minimal-r6
readd-minimal-r5
app-windows-r7-soak
readd-minimal-r4
app-windows-r7
app-windows-r6
readd-minimal-r3
readd-minimal-r2
readd-minimal
merge-release8-dev
review-busy-page-liveness-r2
review-busy-page-liveness
mesh-view-trim
review-authority-read-retry-r2
review-remove-uninstall-r2
release-retry-reattach
release-retry
review-remove-uninstall
review-authority-read-retry
app-windows-ship
mesh-autoswitch-p2
mesh-ssh
mesh-relay
mesh-macos-node
screen-and-control
session-labels
review-real-logins-r5
review-real-logins-r4
review-real-logins-r3
shim-memory-r2
review-real-logins-r2
shim-memory
shadow-addhost-instant
shadow-real-logins
shadow-same-session-flood
shadow-prod-perf-fix
hosts-from-server
app-windows-real
app-windows-transport
release-prod-6w
mesh-policy-verify
quiet-close-toast
dev-merge-r4
review-mesh-agent-transport
review-app-windows-transport
settings-roundtrip
review-release-pipeline-r4
mesh-agent-transport
ended-strip-fix
review-audit-ca-update-d244
settings-rows
phone-width
signout-clear
fix-restart-tests
dev-merge-rp
signer-hardening
review-audit-ca-update
terminal-fidelity
review-audit-ca-update-wip
mesh-integrate
review-mesh-agent-transport-515d3712
review-mesh-dataplane-feec1988
mesh-dataplane
signout
research-canvas-node
wip/review-renderer-cost
wip/review-terminal-fidelity
padlock-glyph
review-release-pipeline-2e95b689
phone-polish
sidebar-polish
inbox-generic
review-mesh-dataplane-378d4055
prod-batch
ui-inbox
ui-ca-unlock
ui-machine-sections
review-mesh-dataplane-a5f744fb
tls-session-prod
session-bounds-prod
fix-dead-session
session-bounds
tls-session
palette-polish
shortcuts-d222
research-appmeta
phone-gutter
ui-centre-button
wip/ui-centre-button
wip/tls-session
review-addhost-breakit
ui-platform
renderer-cost
remote-agents
mesh-api
ssh-bootstrap
prod-hotfix2
prod-fixes
review-remote-agents
review-mesh-dataplane
session-life
research-browser
review-mesh-api
research-capture
review-mesh-api-round3
review-mesh-api-round1
gate-stability
review-gate-stability
review-relay-direct
review-mesh-api-findings
proto-sessions
font-picker
proto-addhost
proto-windows
mesh-core
latency-gate
research-bsd
research-discovery
keytray-mods
mock-core
ui-tweaks
fix-motion-sel
fix-motion
integrate2
fix-robust
t-web
t-server
vt-ghostty
fix-snap-echo
t-web-auth
review-vt2
t-agent
lane-protocol
lane-shim
lane-auth
s1-base
s1-contracts
s1-workspace
No results found.
No labels
launch-qa
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set
Reference
kayg/infraternal#281
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Reverse proxy on demand: engine-neutral implementation plan
Status: P5 proposal for kayg to critique before code. The engine choice is OPEN following the owner's Caddy question. #261 comment 72438 selected Traefik earlier; this plan does not silently extend that selection to the reopened comparison. Keep the requested issue title for continuity.
Goal: expose an authorized service through a managed HTTPS name, downloading and running a proxy only on nodes with enabled exposures.
Architecture: the infraternal server owns desired state, authorization and audit; the local infraternal agent acquires and supervises one external proxy process per serving node. A narrow engine adapter renders our exposure model, applies revisions and reports observations. Only the selected adapter ships initially; this is a boundary for choice and future migration, not a commitment to maintain two engines.
Stack: existing Rust action/plugin framework, mesh coordinator and update trust; selected external Go executable; existing TypeScript product shell and shared UI. No new foundational library is selected here. All new packages/crates remain AGPL-3.0-only; review the engine and module licenses separately.
Inputs: D316 and repository decisions, R2 design, earlier owner selection, J20 measurements, edge updates #211, mesh DNS #266. Snapshot:
origin/devatf4b61cd6c3f52078cad7573c5367d7a6e75f84f0, branchresearch/p5-traefik-plan. No implementation changes.Owner grill, recommendation first
xcaddybuilds with an approved DNS module set rather than dynamically building on hosts. Is this acquisition trust and build responsibility acceptable?.internalremain the default mesh suffix; publicly trusted TLS requires an optional owned domain and DNS-01. For.internal, offer explicit private-CA TLS only after its issuance/trust-distribution contract is approved, or explicitly chosen HTTP. Never imply a public CA can issue.internal. Should private-CA TLS ship in slice 1, and which existing PKI authority is permitted to issue it?expose <machine> <service> --name <dns-name>, plusexpose list,expose status <id>,expose remove <id>, and--jsoneverywhere via D16/D314. Isexposethe right noun/verb family? Details remain an action-schema grill, not a second command implementation.Evidence and engine-specific differences
J20 tested Traefik 3.7.17, executable 182,120,608 bytes, idle RSS 93.30 MiB, warm 95.77 MiB, 1,000 held connections 150.27 MiB, HTTP high-water 265.84 MiB. Idle CPU sampled 0.00%. Three reload trials lost 0 / 0 / 0 requests; route switch 27 / 561 / 1041 ms. These are standalone Linux x86 results, not macOS numbers or a universal zero-loss guarantee. HTTP/1 c64 was 32,956 req/s, p99 7.296 ms under J20's allocation and generator ceiling. Caddy cells stay unmeasured, including provider-enabled builds.
github.com/traefik/traefik/releases, reviewed hashes, then our signed manifest/mirror. R2 inspected 3.7.17 without detached checksum signatures. Recheck the exact chosen release assets.POST /loadthrough a private permissioned Unix socket. API blocks until success/failure and documents rollback on failure without downtime. A JSON file is a durable source, not an automatic file watcher;caddy reloadalso uses the admin API. With admin disabled, use a controlled restart and report that interruption.api@internalroute. Private ping only, separate from data listeners.localhost:2019with a permissioned private Unix socket, or disable admin if accepting restart-only changes. No public or mesh binding, remote admin, arbitrary module/config API or proxying the socket. Disable unintended automatic port-80 listeners and unapproved on-demand TLS.acme.jsonwith mode 0600 and exclusive writer. Never share it among nodes.acme.json; treat all contents as secret and prohibit arbitrary storage modules.Sources: Traefik file provider, Traefik ACME, Traefik API/dashboard; Caddy JSON API, Caddy DNS challenge, Caddy build instructions, Caddy provider modules, Caddy asset signatures. Documentation describes capabilities; J20 supplies our measurements. Freeze versioned docs/source alongside the selected release recipe.
1. Acquisition, installation and release coupling
One signed component manifest per infraternal release binds release identity/channel, component ID, engine version, OS/architecture, module-set digest, source URL, archive/executable SHA-256, exact sizes, configuration/storage compatibility versions and license/provenance receipts. Release CI verifies upstream evidence where present, scans advisories/dependencies/licenses and signs the approved resulting bytes using the existing release trust policy. Keep identity-bearing manifest verification separate from a bare checksum list. No
latest, host-sidexcaddy, automatic engine self-update or arbitrary URL input.The existing
release_verify.rs::verify_release_fileandrelease_manifest.rs::verify_release_manifestare useful trust precedents, not drop-in component installers: the former hard-caps payloads at 128 MiB, below measured Traefik size, and the latter authenticates infraternal release identity. Propose a separate bounded component payload policy, initially maximum 256 MiB archive / 512 MiB extracted executable, exact signed lengths stricter than those caps. Do not increase the existing application-update limit globally. Extract verifier primitives into a shared small module only where both server and infraternal agent genuinely need them; add no server/SQLx dependency to host-only builds.On first enabled exposure: acquire an installer lock, stream to a private staging file, enforce time/byte/disk bounds, verify signed manifest and archive digest before extraction, extract exactly the allowlisted executable, verify its digest/target, then fsync and rename within the destination filesystem. Reject absolute/parent paths, duplicate entries, symlinks/hardlinks, special files, bombs and wrong-architecture artifacts. Verify the same immutable file ultimately executed; the install parent must not be writable by other users. No shell expansion or untrusted executable search path.
Use the existing resolved infraternal data root: rootful
/var/lib/infraternal, rootless~/.local/share/infraternalwhere applicable, and the established macOS resolver rather than assuming Linux paths. Proposed layout:components/proxy/<artifact-digest>/bin,proxy/config/,proxy/sealed/,proxy/tls/, and private runtime socket directory. Directories 0700; immutable executable 0500; manifests/config/certificates 0600. Keep secrets separate from engine configuration history.D251/#211: stage the component selected by the candidate infraternal release before activating it on an already exposing node; keep the current working process/artifact until candidate readiness. Inactive nodes retain the manifest and need no download. Rollback selects the previous release's pinned artifact and compatible rendered config, never an arbitrary old engine. Signed downgrade authorization follows existing release rules. Retain previous artifact and metadata until rollback window ends; never garbage-collect the active binary. Cert storage compatibility is a gate: use a quiesced protected snapshot or approved forward/backward-compatible migration; never overwrite newer ACME account/cert state with a stale backup blindly.
Air-gapped install imports a release-signed component bundle from a file through exactly the same checks, no bypass flag. A verified mirror/cache is optional and content-addressed. Full isolation cannot contact public ACME or provider APIs: support explicit pre-provisioned certificates or approved private PKI, show renewal responsibility and expiry, and never promise public DNS-01 will work offline. Packaging must include engine/module license notices and verification evidence.
2. Supervision, limits and health
Desired enabled exposure count, not browser attachment or traffic, determines process lifetime. State machine:
inactive → acquiring → verifying → starting → ready, withapplying,degraded,retrying,draining, and typedfailedobservations. Download failure leaves desired revision pending, with a retry action. Disabled rows and cached artifacts alone do not start a process.One owned process per node, not per service/session. Start with an absolute verified path, allowlisted environment, fixed working directory and restrictive umask. Acquire a node-scoped ownership lock before bind. Readiness requires process survival, private control/ping readiness and a synthetic expected-route/certificate probe; distinguish engine-ready, upstream-unhealthy and certificate-pending. Stop: retire routes/deny new traffic, signal graceful drain, wait the proposed 10 s, terminate the owned process group if needed, reap and remove private runtime material. When re-enabled, inspect retained certificates immediately before reporting ready.
Proposed crash backoff: 200 ms doubling to 30 s with jitter, reset after 60 s healthy; five failures in 60 s enter a cooled-down failure state with bounded automatic retries and explicit Retry. Retry timers exist only while enabled exposures require the engine. Never download again on every crash. Failed reload keeps the last permitted working revision; a security revocation is different and must fail closed even if that requires withdrawing a listener.
Linux: use existing process/descriptor ownership primitives, child rlimits and, when delegated, a cgroup for hard memory/task bounds. Proposed active-process
MemoryMaxreview budget 512 MiB, file descriptor cap 8192 and capped connection/inflight settings; test overload behavior before approval. macOS: use supported rlimits and process-group/launchd lifecycle integration; do not claim Linux-style hard resident-memory enforcement exists there. A Go memory target is advisory. Observe RSS and pressure; report limit support truthfully. Do not impose process-wide rlimits on the infraternal agent merely to constrain its child.Abrupt infraternal-agent exit and update ownership need real tests on both platforms. Linux parent-death/cgroup cleanup and macOS service-manager behavior differ. Track process identity and owned control descriptors; never kill/re-adopt a numeric PID alone. #211 generation handoff must transfer exclusive child ownership, log readers and the immutable component/config revision, or explicitly drain/restart. No candidate callbacks may alter exposure state before adoption commits. Seamless proxy survival across host updates is a gate dependency, not implemented in current dev. If unmanaged macOS SIGKILL leaves an orphan, require a scoped cleanup owner or surface that limitation before shipping; do not hand-wave portability.
Rootless listeners bind above 1024. DNS-01 requires no inbound port 80. For conventional 80/443, propose an explicit narrowly scoped OS port mapping on Linux/macOS, audited install/removal without touching unrelated rules. Linux
CAP_NET_BIND_SERVICEcan be an owner-approved alternative on the verified executable or constrained launcher; it grants low-port bind broadly, needs reapplication after replacement, and must not spread to the infraternal binary. macOS has no Linux file capabilities; use an approved privileged mapping/helper or high ports. Router PCP/NAT-PMP/UPnP mapping is a separate exposure consent, not a substitute for local privileged bind. No silent root promotion or global unprivileged-port sysctl change.Engine stdout/stderr feed the existing infraternal log path with node/exposure/revision IDs, bounded line size/queue/rate and dropped-count summaries. Never forward arbitrary raw errors/configs to the browser. Keep only safe structured fields; redact Authorization/Cookie/query credentials, never log provider tokens/private keys. Access logs opt-in/minimized; health and certificate observations are pushed on change, not UI polling at idle. A proposed 30 s private active-engine health probe is measured separately from zero inactive wakeups.
3. Product model and adapter contract
Proposed
ExposureSpec: stable exposure ID, owning node, stable service target (protocol/address/port), normalized exact DNS name, optional literal path prefix, listener audience/port, TLS mode (disabled,managed_dns,provided, future approved private PKI), certificate/provider references, access-rule reference, enabled flag and monotonically increasing desired revision. Status separately records applied revision, engine artifact ID, process/upstream/certificate states, expiry, safe error code and next action. No credentials in either public model. Product copy uses rules, following D252; the requested “policy” concept maps to those existing rules.Server authorizes the actor, node, service and audience; normalizes/validates the whole transaction, stores it and audits intent. Node validates the authenticated revision and target bindings again. No raw engine JSON/YAML, arbitrary expressions, templates, file paths, exec hooks, middleware snippets or module names in the public API. Exact names are canonicalized using the existing mesh name rules; initially reject wildcards, overlapping host/path claims and credentials embedded in URLs. TCP/UDP and arbitrary middleware are separate future scope.
Proposed adapter boundary in the proxy plugin:
render(snapshot, capabilities) -> RenderedRevision;apply(rendered) -> Applied | Pending | Rejected;observe() -> ProxyObservation; plus lifecycle startup requirements.RenderedRevisionidentifies nonsecret config bytes/hash, secret references and whether restart is required. Capabilities explicitly report compiled DNS providers and supported limits. Only one writer applies snapshots serially; stale generations cannot supersede a newer one.Traefik: serialize a complete dynamic snapshot to a temporary sibling outside the watched directory, fsync, rename to the one canonical watched file and fsync the directory. This avoids mixed multi-file revisions. Watch the parent directory rather than a replaced-file mount. Keep a durable desired revision and last-known-good rendered snapshot. Because watch events are asynchronous/coalesced, observe expected route/cert behavior before acknowledging applied; process health alone is insufficient. API/dashboard stay disabled. Test a private synthetic revision route restricted to local probing if needed; do not expose a management route to clients. Startup-only resolver/listener changes are marked restart-required in status.
Caddy: serialize native JSON atomically for durable recovery, validate with the exact installed build, then submit the whole snapshot via private Unix-socket
/load. One successful response is the engine acknowledgment, followed by route/TLS probes. Use our durable committed desired/applied revisions as authority; either disable autosave or protect and reconcile it, never blindly--resumea stale config after revocation. Disable the admin endpoint only if the owner accepts restart-only application. File-only mode is not claimed to hot-reload automatically. Long-lived WebSocket and HTTP/2 behavior must be measured for the chosen stream-drain settings.For either engine, invalid nonsecurity edits retain the last working authorized state and report desired/applied mismatch. Revocation first removes authorization at the dataplane enforcement boundary and closes forbidden live flows; restoration of a last-known-good config must never restore revoked access. Partial writes, crash between persistence/apply and rapid rename/edit/remove races must converge deterministically after restart.
4. Certificates and custody
Managed custom-domain TLS uses engine DNS-01, explicitly disabling unwanted HTTP-01/TLS-ALPN fallback and implicit public listeners. The optional owned suffix is editable, while
.internalcan remain available as the default alias per D316. DNS-01 needs public authoritative TXT control even if A/AAAA records and services exist only on mesh. Normalize suffix boundaries, authorize domain use, and explain propagation/CAA/rate-limit/account/provider failures with a safe retry action. Stage new names/certificates before removing an old working custom-domain route where rules permit; suffix changes must also retire old DNS and access state coherently.Provider tokens are sealed at rest and delivered only to the designated issuing node over the existing authenticated channel. Store only opaque references/status centrally. Scope tokens to the challenge zone; offer delegated
_acme-challengezones where supported. Unseal just for the owned process, via provider-supported credential files or private config; environment-only providers require an explicitly documented same-user/root inspection risk and scrubbing, never argv. Use file-backed provider options where supported, not a claim every provider supports them. Rotation writes new private material, validates issuance/renewal, switches, then retires old material. Node migration requires explicit custody reassignment, no mesh-wide replication of tokens.Unattended renewal cannot depend on the user's passkey being present. Node sealing requires a reviewed persistent machine key/platform protection and recovery semantics; do not repurpose the passkey-wrapped SSH vault as if it were already suitable. OpenBao later implements the same
credential reference → node-local materialboundary; no new secret engine is selected by this plan.Traefik owns mode-0600
acme.jsonin a 0700 directory, one writer, protected backups; Caddy owns an equivalently private storage tree. Both contain private keys and ACME accounts. Protect path ownership, no symlinks, no export in diagnostics, no unrestricted backup attachments. Backups are encrypted and explicitly included in disaster recovery scope. Certificate metadata extraction must not return key bytes. Never share a mutable engine certificate store across nodes. Stopping the last exposure stops renewal; retained certificates are checked on next activation, with safe issuance before ready and visible expiry failures..internalis not publicly registrable. Do not make public ACME attempts for it and do not silently install an engine-generated root CA into OS/browser trust. Private certificate issuance, separate server-auth EKUs and trust distribution require owner-approved integration with existing PKI; mesh/device identity keys must not be reused as website keys. Provided certificates are an explicit alternative with expiry/status tracking.5. Mesh names and enforcement
Reuse
crates/infraternal-mesh/src/names.rs::NameTable, coordinator canonical claims and #266's negotiated, revisioned name snapshots. #266 is active work, not a guarantee those listeners/suffix APIs are in this base. D316 requires<hostname>.internaland an optional custom suffix; a service also needs a unique routing key. Propose one root service per host name initially, explicit path prefix or explicit service alias for extras, with owner confirmation in the contract slice. DNS returns the actual serving node's mesh address, not an unreachable upstream or invented global gateway.Use the same accepted mesh rule revision for name visibility, exposure authorization and dataplane admission. Denied peers must learn no protected aliases from a proxy status response. DNS denial is insufficient: direct-IP Host/SNI requests, stale DNS caches and existing connections must still be denied. Shared HTTPS 443/8443 also cannot collapse distinct service-port rules into “can reach the proxy”. Render per-route permitted peer sources from the original service rule, enforce mesh identity at ingress, and retain backend egress restrictions. Mesh source addresses are trusted only on the authenticated mesh interface. Never trust client-supplied
X-Forwarded-For/Forwarded headers as identity; configure engine source matching accordingly.Tests must prove a peer permitted for service A cannot reach B on the same listener, even with forged Host/SNI/forwarded headers or via an allowed backend. Address reassignment must not inherit an old peer's privileges. For requests requiring person-level authentication or public access, mesh IP is not enough: integrate an approved identity flow separately or deny. Close existing forbidden flows on revocation under the mesh cutoff contract, and fail closed when a newly accepted rule revision cannot be installed. Preserve last accepted rules during coordinator outage only as already allowed by the mesh contract; do not invent expiry semantics here.
6. UI and CLI proposal
Only a behavior brief here; D171 leaves design/mockups to the orchestrator. Read
TASTE.md; reusepackages/uicomponents (entity-chip,pane-stepper,status-line,spinner,toast,chip-input, focus/keyboard helpers), not new primitives. Owner-approved mockups precede UI implementation.Entry point beside a service in the host's existing view: Expose service. Show the exact service target, choose name/suffix, TLS and who can reach it, then preview the full URL including nonstandard port and audience before enabling. Domain/provider setup appears only when required. First activation shows downloading → verifying → starting → getting certificate → ready in the service's own surface. A pending certificate must not appear ready merely because the child is alive.
Exposure details show copy/open URL, desired/applied state, upstream health, certificate issuer/expiry/renewal state and a plain-language next action for errors. Disable/remove and domain/access edits stay beside the exposure. Provider secret controls show configured/rotate/remove, never reveal. Errors use appropriate icons, retry near the failed step, pending approvals in the existing toast/Inbox, and technical paths/log details behind Details. No engine branding/schema/admin dashboard in the normal flow. On mobile use one column, 44 pt targets, no hover-only controls, no colliding labels or long URL overflow; ink/paper and empty/loading/offline/retry/expired/revoked states get equal care.
CLI is the same registered action schema as UI/API/MCP, not a parallel parser or management channel. Proposed commands are in grill Q8.
--jsonuses the existing envelope, stable typed errors and desired/applied revisions; an accepted mutation returns pending status rather than waiting indefinitely for DNS. Credentials use secure input/file-reference mechanisms, never command arguments. A read-only list/status command must not trigger acquisition. Add usage/examples and action availability/permissions throughActionDefinition/ActionRegistryand existing CLI output helpers.7. Security review and budgets
Independent review focus: supply-chain authenticity vs hashes; malicious archives and replacement after verification; host/path/config injection; SSRF/unauthorized upstreams; shared-listener authorization bypass; revocation on live WebSockets/h2; secret custody and raw error leakage; ACME abuse; child/adoption ownership and privileged mapping; storage rollback/key loss. No arbitrary dynamic engine modules, discovery socket mounts, public admin endpoints or unbounded API/config bodies.
Required malicious cases: backticks/quotes/newlines in DNS/path/header fields, escaped separators/IDNA and suffix confusion, duplicate names,
../absolute/symlink archives, oversized compressed/uncompressed inputs, wrong target/digest/key/replayed release, writable-parent executable swap, Unix-socket ownership attacks, forged proxy headers, unknown SNI/default virtual host, loopback/admin/metadata upstream SSRF, malicious provider error containing a canary secret, cancellation after commit, concurrent enable/remove/update, resource exhaustion and disk-full/fsync failure. Parser/config limits are fuzzed; no invalid input creates a partially authorized route.Budget distinction: external engine adds +0 embedded engine bytes to the infraternal binary; acquisition/verification/supervisor/UI glue has an unknown nonzero potential delta and must be measured per slice for x86_64/aarch64. Never claim whole feature binary delta is zero. No dormant child, probe timer or background download; idle infraternal-agent RSS stays under D41's 15 MB budget measured separately. Keep UI lazy-loaded with no idle polling.
Provisional selected-Traefik regression triggers: idle RSS ≤110 MiB per active node; 1,000-connection RSS ≤180 MiB; HTTP peak ≤320 MiB under the matched J20 workload; no idle CPU materially above its 0.00% sampled baseline without investigation. These are proposed, not owner-approved caps. At 50 active nodes J20 idle is 4,665 MiB (~4.56 GiB) across the fleet; 1,000 held connections each is 7,513.5 MiB (~7.34 GiB). Fifty terminals on one node share one proxy. Zero active exposures means zero engine RSS. Retaining two measured Traefik executable versions costs 364,241,216 bytes, excluding archives/state. Obtain Caddy figures for the actual provider module variant before setting its thresholds.
Reload gate: zero dropped/failed requests in three repeated hot-update trials with h1 keep-alive, h2 streams, in-flight responses and WebSockets; record switch latency, p50/p99, CPU and RSS, no universal zero-downtime claim from one benchmark. Proposed control-only route-application p99 ≤3 s under the matched workload (excludes certificate issuance); compare throughput/p99 within 5% of selected-engine baseline or investigate with paired reruns. Engine restart/binary upgrade is separately measured and labeled, never folded into a hot-reload claim. No public ACME time bound is promised.
8. Implementation slices and gates
Execution begins only after the relevant grill answers. Each slice starts with named failing regression tests, runs them to RED, implements the smallest change, reruns to GREEN, then creates an atomic issue-referenced commit. Run
scripts/verify iterateduring iteration andscripts/verify faston exact final head on a build VM throughbuild-slot, quoting its output verbatim. Build on compute01/dev-02, benchmark only perf-01 throughperf-run; never build/test on the control host. No pushing, merging or deploying is authorized by this planning task.S0: contracts and selected-engine acceptance
contracts/proxy.mddefining ExposureSpec, status/revisions, access and TLS semantics, secure secret references, ownership/adoption and bounded limits. Propose a narrowProxyAdapterinterface as above.S1: verified acquisition and offline import
Proposed files:
crates/plugins/proxy/{Cargo.toml,src/lib.rs,src/component.rs,src/acquire.rs,tests/acquisition.rs}; shared verification extraction beside existing release verifier only if needed; release component recipe/manifests underrelease/. Wire workspace/features without linking server-only dependencies into the host.acquire_component(release_binding, source) -> VerifiedComponentwith immutable descriptor/path identity and atomic publication; online/offline use the same verifier.S2: owned lifecycle, status and logs
Proposed files:
crates/plugins/proxy/src/{supervisor.rs,logs.rs,health.rs},tests/lifecycle.rs; integrate via the existing plugin host registration incrates/infraternal-agent/src/and installed-service ownership paths, rather than treatingremote_supervisor.rs(server-link reconnect logic) as a child-process manager.S3: exposure state and selected adapter
Proposed files:
crates/plugins/proxy/src/{model.rs,descriptor.rs,server.rs,store.rs,reconcile.rs,adapter.rs}, one selected factual integration moduletraefik.rsorcaddy.rs,tests/{model,render,reload}.rs; additive migration undercrates/infraternal-server/migrations/and action registration followingcrates/plugins/mesh/src/descriptor.rs.S4: TLS and sealed credential lifecycle
Proposed files:
crates/plugins/proxy/src/{certificates.rs,custody.rs},tests/certificates.rs; reuse reviewed node-secret facilities once their actual integration is established. Do not create new cryptography in the plugin..internalrequests refused; missing/wrong provider module reported; wrong/expired cert, DNS timeout/CAA/provider failure and token rotation retain safe state; stop/re-enable after expiry, permissions, encrypted backup and no-secret diagnostics asserted.S5: mesh DNS/rules, privilege mapping and release integration
Proposed files: proxy mesh adapter plus narrow extensions in
crates/plugins/mesh/src/{configuration.rs,access_updates.rs}and #266's accepted DNS interfaces; integration tests in both plugins. Coordinate with #266/#211 branches before editing their seams.S6: shared actions/CLI and approved UI
Proposed files: proxy action descriptor plus existing
crates/infraternal/src/cli/adapters/tests; lazyapps/tracer/src/canvas/proxy/view/transport modules and production browser cases; extend sharedpackages/uicomponents only where an existing primitive lacks a needed capability.--jsonenvelope/errors/pending revisions agree across CLI/UI/MCP; read-only status never downloads; secure credential entry cannot leak into command history/argv.Planning verification and handoff
Self-review: all eight requested areas map to sections and slices above. Key unresolved dependencies are engine measurements/selection, unattended node sealing contract, private
.internalPKI, shared-listener rule enforcement, #266 names, #211 generation adoption and macOS privileged mapping/cleanup behavior. These are owner questions or concrete acceptance gates, not permission to invent a foundation.This issue is the reviewable deliverable. No code, dependency, release pin or DECISIONS entry has been changed; only kayg may settle the open choices. Gate receipt and exact planning-worktree status follow in a separate comment, with no claim the proposed implementation has passed tests.
P5 started: branch
research/p5-traefik-plan, base/headf4b61cd6cfromorigin/dev. Planning only in isolated worktree. Owner reopened the engine question; acquisition, supervision, exposure model, custody and UI will support either engine. No Caddy measurements assumed; parallel J20 extension is a dependency. No code changes or builds started.P5 findings before plan publication:
f4b61cd6c:crates/infraternal-server/src/release_verify.rs::verify_digest_expectedrefuses files above 128 MiB. J20 measured a 182,120,608-byte Traefik executable. Component payload limits must be separate; do not globally weaken app-update bounds./loaddocuments synchronous success/failure and rollback; Traefik file-provider reload is asynchronous and throttled. Desired/applied state needs an engine-specific acknowledgment strategy. Sources: https://caddyserver.com/docs/api and https://doc.traefik.io/traefik/reference/install-configuration/providers/others/file/P5 planning deliverable posted and read back byte-for-byte. Engine choice is the first owner question; Caddy remains unmeasured pending the parallel J20 extension. Acquisition/custody/supervision/model/UI remain engine-neutral; only the selected adapter is proposed for initial implementation. No code, dependency, decision or release pin changed. No push/deploy/commit performed.
Branch:
research/p5-traefik-plan. Base/head:f4b61cd6c3f52078cad7573c5367d7a6e75f84f0. Planning worktree is clean. Plan/evidence copy:/home/kayg/.cache/codex-scratch/p5/evidence/implementation-plan.md.Gate attempt on compute01, unmodified exact-base worktree, through the required build-slot wrapper, with a 60-second queue limit:
Wrapper exit:
124(timeout).scripts/verify fastdid not start; no green gate or test claim. This is a capacity limitation, not a test failure. The raw log timestamp is the host's output, quoted verbatim. VM log:/home/claude/.cache/codex-scratch/p5/evidence/verify-fast.log. No compiler/test/build was left running by this job; temporary VM source checkouts are being removed. No target directory was created.Owner critique is requested on the recommendation-first grill in the issue body; no implementation begins from this research task. Issues remain open.
Cleanup confirmed: both temporary VM source checkouts removed; no P5 VM processes remain. The local planning worktree and small evidence files are retained for owner critique. No builds or tests ran, and no target directory was created.
Owner decision (kayg, 2026-11-01), final: reverse-proxy engine = Traefik, downloaded on demand (R3 comparison 72700 on #261). Caddy idle RSS measured 48 MiB (not 17), half of Traefik's 94 MiB, but it drops existing connections on every config reload (Traefik kept all, 6/6), is slower under load (c512 13k vs 21–23k req/s; p99 70–74 vs 42–53 ms) and needs a custom build for Cloudflare DNS-01. Connection-preserving reloads are required because the UI changes proxy config while terminals/canvas run over long-lived WebSockets.
Owner answers (kayg, 2026-11-01) on the #281 grill:
traefik_vX_checksums.txt(SHA-256) for the pinned release AND signs our own manifest of the verified bytes; nodes fetch from our immutable mirror and verify our manifest. Both checks.<service>.ssh.kayg.orgunder*.ssh.kayg.orgvs per-machine wildcards).expose; orchestrator proposedserve(pending).J62 started on branch
proxy-traefik-281, based on7bf03044f0003b4b5280be93570ec0e1fe5b356d(origin/dev). I’m checking the owner’s #281 engine, trust, access, TLS and ports answers plus the #261/#266/#211 dependencies before writing failing tests. Next are the requested acquisition, supervision and CLI slices, then the compute01 build-slot gates and size report. No code changes yet.J62 audit finding (base
7bf03044f): still enforces the 128 MiB app payload cap; Traefik 3.7.17 is 182,120,608 bytes per J20/R3, so component payloads need their own strict cap. Mesh coordination exposes a , but the current server/node config does not yet carry #266's public-hostname-derived suffix; I will keep naming behind one function that consumes that suffix and avoid duplicating #266's DNS publication path.\n\nCustody finding: there is no generic node-sealed credential store in this base. The existing CA wrapper is user-scoped server-side key custody, not a node secret API. Per kayg's answer in this session, J62 will implement the interim interface with owner-only file storage under agent state (0700 directory; 0600 exclusive temp file + atomic rename; verify owner/mode), Traefik receives only provider paths, and provider removal deletes its files. No token values enter config, argv, environment, or logs. The module will state that node sealing/OpenBao replaces this implementation behind the interface (#287 follow-up).J62 audit finding (base
7bf03044f): `release_verify::verify_digest_expected` still enforces the 128 MiB app payload cap; Traefik 3.7.17 is 182,120,608 bytes per J20/R3, so component payloads need their own strict cap. Mesh coordination exposes a `dns_suffix`, but the current server/node config does not yet carry #266's public-hostname-derived suffix; I will keep naming behind one function that consumes that suffix and avoid duplicating #266's DNS publication path.Custody finding: there is no generic node-sealed credential store in this base. The existing CA wrapper is user-scoped server-side key custody, not a node secret API. Per kayg's answer in this session, J62 will implement the interim `DnsTokenCustody` interface with owner-only file storage under agent state (0700 directory; 0600 exclusive temp file + atomic rename; verify owner/mode), Traefik receives only provider `*_FILE` paths, and provider removal deletes its files. No token values enter config, argv, environment, or logs. The module will state that node sealing/OpenBao replaces this implementation behind the interface (#287 follow-up).
Correction to the previous J62 audit notes; their shell formatting damaged the evidence text.
At base
7bf03044f, crates/infraternal-server/src/release_verify.rs::verify_digest_expected keeps the 128 MiB app payload limit. J20/R3 measured the Traefik executable at 182,120,608 bytes, so component downloads need their own bounded policy.Mesh coordination currently has a dns_suffix, but the public-hostname-derived suffix from #266 is not carried by this base's server/node configuration. J62 will keep name construction behind one function that accepts the configured suffix.
Per kayg's custody decision in this session, J62 will define DnsTokenCustody with a temporary owner-only file implementation under the agent state directory: create and verify a 0700 directory, use 0600 exclusive temporary files and atomic rename, refuse owner/mode mismatches, pass only provider *_FILE paths to Traefik, and delete provider files on removal. The module will document that node sealing or OpenBao replaces the backend without changing callers (#287 follow-up).
J62 port-mapping investigation (#281): do not install a generic nftables redirect for occupied 80/443.
Linux destination NAT in a prerouting chain runs before route selection and local socket lookup. A rule scoped to the mesh interface and mesh destination can therefore rewrite packets before the kernel selects a listener. If another service explicitly binds the mesh address on port 80/443, a redirect to 10080/10443 would take that service's mesh traffic too; the explicit bind does not protect it. Interface scoping preserves non-mesh ingress, but it does not preserve the existing mesh listener. A safe rule would need an additional owner-controlled port-binding contract and lifecycle ownership of the nftables table/rules, which this slice does not have.
J62 will bind root-mode 80/443 only when those ports are available on the selected mesh address. If occupied, it will use 10080/10443 and report the explicit ports in the serve result. No host firewall rules are changed. This conclusion follows nftables' documented prerouting DNAT order; it is not a live host firewall experiment.
J62 base alignment:
proxy-traefik-281now starts at currentorigin/dev5418f93574c6be84766f9a7dcc196b34b27ff3bc, which includes the D243 report-only size gate. The existing uncommittedcrates/plugins/proxycustody scaffold and workspace entry were preserved. A targeted custody RED run remains queued on compute01; no test result yet. Next: observe the expected custody assertion failure, then implement the owner-only file store behindDnsTokenCustody.Crash-safety checkpoint pushed:
proxy-traefik-281at1bf578a8ed2c6e271e1787b17cab2e95a6be6420, based onorigin/dev5418f9357. This is an explicitly unqualified WIP containing the proxy crate scaffold and custody RED tests; implementation remains a refusing stub. The assigned compute01 test has waited inbuild-slotfor about 90 minutes and is currently reported at queue position 39. Log:/home/claude/.cache/codex-scratch/j62/custody-red.log. No test result is claimed; I’m waiting for its assertion output before implementing.J62 build-admission finding: compute01 reports
/dev/sda1at 457 GB used of 492 GB, 16 GB free (97%).build-slotis refusing admission until at least 25 GB is free. I canceled only the stale J62 test wrapper because its snapshot lacked the new proxy package inCargo.lock; it never reached Cargo or ran an assertion. No other jobs' targets were removed. J62 has no test result yet. Please arrange cleanup of an appropriate finished-job target on compute01; I will resubmit the corrected RED test after admission is available.J62 progress on branch
proxy-traefik-281(base5418f93574c6be84766f9a7dcc196b34b27ff3bc, head1e3eaba89).Owner direction is recorded in code:
DnsTokenCustodywithput/get/delete/rotate, backed for now by owner-only files. The module documents that node-key sealing or OpenBao under #289 can replace the implementation without changing callers. The store requires a 0700 state directory, uses 0700 provider directories and 0600 token files, stages with exclusive creation and atomically renames, and returns only the path used by a provider*_FILEvariable. Debug output is redacted.Compute01 red/green evidence via build-slot: expected red was 3 passed / 8 failed against the stub; green was 11 passed / 0 failed for
cargo test --locked -p infraternal-plugin-proxy --lib -- --nocapture. The per-job target is/home/claude/.cache/codex-scratch/j62/target.Next: acquisition manifest and installer, Traefik supervision/configuration and mesh grant wiring, CLI, lifecycle/namespace coverage, then the fast gate and size report.
J62 resumed from pushed WIP and rebased
proxy-traefik-281onto currentorigin/dev37bbda45554f6e603a6856fc23319e17d8a1b14f0. Current pushed head:9a9dafbea6aad3d1eb0ce8d79bce5990533d6063.Release acquisition checkpoint: release builds now download pinned Traefik 3.7.17 platform archives, verify the upstream checksum list, inspect the allowlisted executable and MIT licence, and place archives under the on-demand
components/<release>/proxy/...path. The existing release signer includes the small per-target JSON manifests in its signed checksum list; the 128 MiB application payload limit stays unchanged.scripts/release/build, publish, verify and the fast gate now include the component files/tests.Compute01 evidence:
python3 -m unittest discover -s scripts/release -p traefik_component_test.py→Ran 5 tests in 0.009s; OK;python3 scripts/release/test.py→Ran 8 tests in 11.443s; OK. The detachedcargo test --locked -p infraternal-plugin-proxyis still waiting inbuild-slotat queue position 3; no Rust test result yet. Log:/home/claude/.cache/codex-scratch/j62/proxy-test-resubmitted.log.Next: observe the Rust test result, finish installer verification and the mesh-only Traefik render/supervisor tests, then run the final VM gate and size report. Interim DNS-token custody remains the
DnsTokenCustodyowner-only file implementation per #289; sealing/OpenBao stays behind that interface.J62 acquisition RED finding, compute01 through
build-sloton 2026-11-01. The test fixture originally failed while constructing../outside, before the installer boundary; I changed it to write the malicious tar header directly and reran the focused suite.The corrected RED result against the installer stub is:
The failing path returns
Kind(Unsupported)from the current stub. The traversal/symlink test now reaches the installer boundary. Log:/home/claude/.cache/codex-scratch/j62/installer-red-fixed.log; target:/home/claude/.cache/codex-scratch/j62/target. Next: implement locked bounded staging, checksum-before-extract, allowlisted extraction, executable re-verification and atomic digest-path publication.Owner answers, continued (kayg, 2026-11-01):
publish;servereads like it changes how the infraternal server serves. Open: how to tell internal publishing from external. Orchestrator proposal:publishdefaults to mesh-only, and--publicexposes to the internet.Owner answers (kayg, 2026-11-01):
*.<machine>.<suffix>) is the default, and its private key never leaves that machine. Flat<service>.<suffix>names get exact per-service certificates issued on the serving node. No wildcard key is ever copied between nodes. Accepted trade-off: exact names appear in public Certificate Transparency logs.publish.infraternal publish <port>is mesh-only: reachable at the mesh name by members with access.--publicalso exposes it to the internet, with confirmation the first time.infraternal publishwith no argument lists what is published and where.J62 resumed from pushed head
9a9dafbea6aad3d1eb0ce8d79bce5990533d6063onproxy-traefik-281, based onorigin/dev37bbda45554f6e603a6856fc23319e17d8a1b14f0.The latest owner decisions are recorded here:
publishis the verb, barepublishlists entries, a port publishes mesh-only by default, and--publicneeds first-use confirmation. Both certificate shapes are supported; per-machine wildcard is the default and stays on its node, while flat names use exact per-service certificates whose names enter CT logs. I added a focused plan and test-first coverage for both naming forms and the one-command table.Compute01 currently reports 8 GB free (build-slot floor: 25 GB). The focused red run was submitted detached as PID 1033773 and is waiting for disk admission; it has not started compiling. Log:
/home/claude/.cache/codex-scratch/j62/proxy-owner-decisions-red.log. No result is claimed. Once space is available I will observe the expected red result, then implement and verify the naming/CLI behavior.J62 owner-decision RED evidence on pushed head
521a4b347:This is the expected red result: the two owner-approved naming forms and the single
publishaction are still stubs. Log:/home/claude/.cache/codex-scratch/j62/proxy-owner-decisions-red-5.log. No run is pending. I am implementing the tested contracts now.Crash-safe checkpoint pushed:
proxy-traefik-281atc701f9423.The naming and CLI RED evidence remains the expected 22 passed / 3 failed recorded above. The implementation now maps both owner-approved certificate shapes, defaults to node-local per-machine wildcards, exposes one optional-port
publishaction with--public,--confirm-public, and--flat-name, and documents CT-log visibility for exact names.A focused crate GREEN rerun is queued on compute01 as PID 1209626, behind four active build-slot jobs. Log:
/home/claude/.cache/codex-scratch/j62/proxy-naming-green-2.log. It uses the VM copy that contains the naming implementation and excludes the newer config/download stubs; no result is claimed yet. Next I will observe that run, then drive the acquisition and config tests RED/GREEN before implementing their behavior.J62 resumed from pushed
proxy-traefik-281headc701f9423, based on currentorigin/dev37bbda45554f6e603a6856fc23319e17d8a1b14f. The compute01cargo test -p infraternal-plugin-proxy --librun remains queued (PID 1209626); four slots are busy and free disk is 208 GB, so there is no result yet.Source audit findings:
download.rs::ComponentDownloader::install,installer.rs::install_stream, and all threeconfig.rsrender/write entry points still returnUnsupported; the proxy crate has no supervisor module or process integration yet. The signed manifest, offline bundle import, custody store, installer extraction tests, naming function, and publish action schema are present. These findings are from the current branch source, not test claims.Plan: use the queued crate result as the first RED receipt; finish downloader/config behavior and add lifecycle/security coverage; wire one process owner to the existing agent component lifecycle and action registry; run the crate tests, required fast gate and size report on VMs; investigate/record occupied-port behavior (the existing owner note rejects a generic nftables redirect because it would capture an explicitly bound mesh listener); then clean the J62 target. Estimate: about 2 hours of implementation/verification time, with slot wait separate. No gate is claimed yet.
J62 resumed on
proxy-traefik-281at pushed headc701f9423, based onorigin/dev37bbda455.The resubmitted VM test completed with the expected installer RED on the pushed head:
test result: FAILED. 17 passed; 2 failed. It identified the unsupported installer path and a traversal archive fixture rejected by the tar builder before reaching production parsing. The test log is/home/claude/.cache/codex-scratch/j62/proxy-test-resubmitted.log; the current WIP fixes the installer path and fixture.The working tree now also contains implementations for bounded mirror/upstream streaming and atomic static/dynamic config rendering, following the already-committed RED tests. Next: run those tests GREEN on a VM, add the owned Traefik process lifecycle and CLI/agent integration, then exercise log redaction and staging ACME. Occupied 80/443 falls back to 10080/10443: an nftables redirect on the mesh interface would still divert traffic addressed to a server explicitly bound to that mesh IP, with no safe way to identify ownership, so no redirect is planned.
J62 resumed from pushed head
c701f9423onproxy-traefik-281, based onorigin/dev37bbda455. No J62 build/test process is active in either VM queue.Plan and estimate: about 2 hours of implementation plus required gates; build-slot wait excluded. (1) Run the current supervisor tests RED on dev-02 through build-slot. (2) Complete the owned Traefik process lifecycle and wire it through the existing infraternal-agent plugin/action path, including private ping, drain/backoff and redacted logs. (3) Close security/config gaps for provider
*_FILEcustody, listener selection, exact grant-bound routing, offline bundle parity and staging ACME coverage. (4) Run focused proxy tests,scripts/verify faston dev-02, the x86 static-size report on compute01, then commit/push and remove only the J62 target.Current branch worktree has uncommitted acquisition, config, signature-verification and supervisor scaffolding. Logs/evidence remain under
/home/claude/.cache/codex-scratch/j62/; the VM target will be kept to one path and removed at completion.J62 resumed from pushed head
c701f9423onproxy-traefik-281, based onorigin/dev37bbda45554f6e603a6856fc23319e17d8a1b14f(D243 report-only is in the base history).The focused
supervisor_testsrun remains queued on dev-02 as PID 3078693;build-slot --statusreports 19 GB free versus the 25 GB admission floor, so it has not started. I am leaving it queued.Plan (about 2 hours of implementation and verification; queue wait separate): finish the current config/downloader WIP, connect the proxy supervisor and
publishaction to the infraternal agent, add lifecycle and secret-redaction coverage, run focused and fast gates on VMs, record static-size deltas, remove the J62 target, then push and post the exact gate output. I will preserve #266 grant/suffix checks and the #211 update-adoption boundary; no interruption-free update claim without adoption evidence. The occupied-port investigation rejects an nftables redirect because it would also divert traffic explicitly bound to the mesh address.DNS tokens continue to use the owner-approved interim
DnsTokenCustodyfile backend from #289: provider*_FILEpath only, owner-only mode/ownership checks, deletion on provider removal, with sealing/OpenBao left behind the same interface.J62 progress: the previous focused-run PID 3078693 was no longer present on either VM, and its referenced log was absent. I pushed the crash-safe WIP checkpoint
bfb2902d6and resubmittedcargo test --offline -j4 -p infraternal-plugin-proxy --lib supervisor_tests -- --nocaptureon compute01 through build-slot as PID 1946713; log:/home/claude/.cache/codex-scratch/j62/supervisor-resubmit.log. The pre-existing compute01 J62 source snapshot lacked the new supervisor module, so the run uses a clean detached checkout of the pushed head and reuses the existing J62 target. I am tracing the agent registry and mesh permission path while it compiles.J62 supervisor RED/GREEN checkpoint: before the lifecycle implementation,
supervisor_testsreached the assertions and failed 4/4 as expected (startup/drain/backoff were no-ops; provider token remained in logs). After implementing the state transitions, bounded exponential backoff, and longest-first log redaction, the fresh compute01 run passed:test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 36 filtered out; finished in 0.00s. The renderer compile blockers are fixed; its TOML formatter is included in the same focused crate compile.J62 resumed on
proxy-traefik-281, pushed head54010eabc5e3828dec25edf8923f3751364821c2, based onorigin/dev37bbda45554f6e603a6856fc23319e17d8a1b14f0.I re-read the J62 brief, #281 plan/owner decisions, D316/D314/D303, and the current branch state. The focused config log on compute01 confirms the last renderer run was 4 passed / 2 failed: the private ping renderer emits
manualRouting = truewhere the test and health contract require automatic routing, and the HTTPS router is nameddocs-httpswhile the route contract expects one stable service key. The supervisor transition tests were reported green in the prior checkpoint. No J62 build is queued or running now; compute01 has 234 GB free, while dev-02 has 41 GB free and a queued unrelated build.Next: fix the two renderer findings test-first; connect the proxy lifecycle and publish action to the real infraternal-agent path; complete the remaining provider-file/config and exposure lifecycle wiring; add namespace/Pebble coverage; run focused suites and
scripts/verify faston dev-02, then x86 size reporting on compute01. The nftables investigation remains closed: a mesh-interface redirect could still capture a service explicitly bound to the mesh IP, so no redirect will be installed.Estimate: about 3 hours of implementation and verification, excluding slot wait. I will keep one J62 target and remove it at completion. Current log:
/home/claude/.cache/codex-scratch/j62/config-ping-red.log.J62 crash-safe checkpoint: pushed proxy-traefik-281 at
607c97459. Added failing-first tests for proxy registry presence and fail-closed mesh context, disjoint router keys, and credential redaction. The compute01 focused router test is still queued as PID 2057611 at position 3; no result is claimed. Log: /home/claude/.cache/codex-scratch/j62/config-router-red.log. Next: observe RED, fix the renderer, then finish agent and process wiring. #266 suffix/grant context remains fail-closed until supplied by the trusted coordinator.J62 WIP checkpoint pushed at
ff57a8b44. Added a real child-process test fixture asserting provider credentials are passed only via *_FILE paths, no token bytes reach argv, child logs are scrubbed, and the owned child is stopped/reaped. The targeted renderer GREEN rerun remains queued on compute01 as PID 2451894, currently behind two jobs; no result yet. Log: /home/claude/.cache/codex-scratch/j62/config-router-green.log. Next: observe renderer GREEN, then run the proxy and agent RED tests before implementing child ownership and registry wiring.Correction on the queued config GREEN run (PID 2451894): it was admitted, then exited before Cargo because its shell started in /home/claude rather than /home/claude/worktrees/J62-build. The log ends with: build-slot: [18:34:37] got slot 1/4; path "/home/claude" does not contain a 'flake.nix', searching up; error: could not find a flake.nix file. No test ran and no result is claimed. I am pushing the two-file renderer WIP checkpoint, then will resubmit with an explicit cd to the repo. Existing target remains /home/claude/worktrees/J62/target; free disk was 132G at the last status.
Crash-safe progress checkpoint: pushed renderer WIP on . The queued GREEN resubmission is PID 2644471, with an explicit and the existing single target ; log: . At 18:37 UTC, build-slot showed four active jobs, J62 at queue position 4, and 130G free. No test result is claimed. Next: observe the crate GREEN, then run the committed registry, redaction, and child-boundary RED checks before implementing those paths.
Correction to my previous progress comment: its inline Markdown backticks were interpreted by the shell, so that comment's command output may be incomplete. Current state: renderer WIP commit
2054659a1is pushed to proxy-traefik-281. The corrected GREEN run is PID 2644471, launched after cd to /home/claude/worktrees/J62-build and using the existing single target /home/claude/worktrees/J62/target. Log: /home/claude/.cache/codex-scratch/j62/config-router-green.log. At 18:37 UTC, build-slot showed four active jobs, J62 at queue position 4, and 130G free. No test result is claimed. Next: observe the crate GREEN, then run the committed registry, redaction, and child-boundary RED checks before implementing those paths.J62 finding from the corrected compute01 run: the proxy renderer tests passed, but the crate suite returned 38 passed / 4 failed. Three downloader failures share one cause:
ComponentDownloader::installreceives release version0.1.0, whilefetch_metadatajoinsproxy-traefik-linux-amd64.jsondirectly to the mirror root. The release fixture and signed bundle place metadata at<mirror>/<release_version>/...; the transport therefore returnsNotFoundbefore signature verification, and the untrusted-manifest test never reaches its expectedPermissionDenied. Evidence is in/home/claude/.cache/codex-scratch/j62/config-router-green.log; the tests aredownload_tests::{mirror_archive_is_streamed_after_the_product_signature_is_verified,upstream_archive_fallback_uses_the_signed_manifest_and_verified_digest,an_untrusted_manifest_never_triggers_an_archive_request}. I will keep archive URLs rooted at the signed mirror path and scope only metadata fetches under the validated release version. The fourth failure is the expected generic credential-redaction RED; next is to implement the sanitizer and then observe all downloader, redaction, renderer, and new private-ping tests GREEN.Progress checkpoint: pushed test-first commit
cd6173c63with loopback-only private ping coverage. Compute01 is running none of these tests yet; detached RED job PID 2886438 waits in build-slot at queue position 4. It will run the proxy agent registry tests and child process boundary test against /home/claude/worktrees/J62-build, using the single target /home/claude/worktrees/J62/target. Master log: /home/claude/.cache/codex-scratch/j62/proxy-red-tests.log; per-test logs: agent-registry-red.log and proxy-process-red.log. At 18:50 UTC there were four active jobs and 116G free. Next: observe these RED results, implement the child boundary, health probe and log sanitizer, then run the proxy suite GREEN.J62 progress on proxy-traefik-281, base
37bbda4555. The prior crate run confirmed the mirror metadata URL must include the validated release-version directory; the exact failures and request paths are in /home/claude/.cache/codex-scratch/j62/config-router-green.log. I added release-version/path-traversal regression tests locally and am continuing the downloader fix. The current registry/child RED run, PID 2886438, remains queued at position 4; compute01 reported 66 GB free. Its checkout and target remain untouched until it completes. No result is claimed for this queued run. Next: observe its RED output, finish the downloader and private child boundary, then run the required focused suites and gates.J62 crash-safe checkpoint: pushed WIP commit
a7ce898bcon proxy-traefik-281. It scopes signed metadata requests under the release-version directory, validates stable and edge versions against the release publisher grammar before URL construction, and adds path-format tests. The existing mirror/fallback tests had already shown the metadata URL RED in the previous compute01 run; this new commit has not been built yet. The queued registry/child RED run PID 2886438 remains at position 2 against the unchanged VM checkout atcd6173c63. I will let it finish before updating that checkout or submitting another run. Next: observe registry/child RED, validate this commit with focused VM tests, then implement the process boundary and registry wiring.J62 checkpoint: pushed WIP
e3209c881on proxy-traefik-281. It adds credential-shaped log redaction for Authorization, Cookie, and sensitive query fields; the existing focused run had reproduced a leak when no known token values were supplied. This sanitizer commit still needs its VM GREEN run. The registry/child RED run PID 2886438 remains queued at position 2 against checkoutcd6173c63, so that checkout remains unchanged. The one target remains /home/claude/worktrees/J62/target. Next: observe the queued RED, move the VM checkout to the pushed head, run the focused download and sanitizer suites, and implement the child/registry boundary from its failing output.J62 finding — failing-first run on compute01
The queued RED script finished against
cd6173c6367aad437107892b15c18426c26d786e(logs:~/.cache/codex-scratch/j62/agent-registry-red.logandproxy-process-red.log).Evidence:
proxy_registry_tests: 1 passed, 2 failed.proxy.publish must be present in the compiled node registryfailed; publication without accepted mesh context also failed its expectedRejectedresult.unresolved import infraternal_plugin_proxy::supervisor::TraefikChild.I’m implementing the registry entry, private loopback ping, and owned child boundary on
proxy-traefik-281(current pushed heade3209c881). Next: run the focused green tests on a VM, then finish namespace/Pebble coverage.J62 resumed from the pushed WIP branch
proxy-traefik-281. I rebased its 13 J62 commits onto currentorigin/dev65d9861914e8cd4a734d533d64ed1f4ea5c34119; local head is4dc7d2f81b06dce95ffdcd0a1d05132e7510274ewith the in-progress supervisor and registry changes still uncommitted. The rebased head has not been pushed yet.I resubmitted the proxy crate suite detached through compute01 build-slot. It is queued at position 3; log:
/home/claude/.cache/codex-scratch/j62/proxy-resume-test.log. Existing single target:/home/claude/worktrees/J62/target.Finding: the accepted Mesh snapshot has directional peer policies and addresses, but the current
origin/devagent/config boundary does not provide the public-host-derived DNS suffix toproxy.publish. The action remains fail-closed while I trace a trusted integration path; no caller-supplied grant or suffix will authorize a route.Plan (about 3 hours of implementation and verification, excluding slot wait): verify and finish the child/lifecycle WIP, finish publication/config integration that can be based on the accepted Mesh state, add namespace + Pebble staging coverage, run focused and fast gates on VMs, report the x86 static-size delta, then push and clean the sole J62 target.
J62 crash-safe WIP checkpoint pushed on
proxy-traefik-281:5a096f41595f2213306fa4d48729eace0e8ab48b, based onorigin/dev65d9861914e8cd4a734d533d64ed1f4ea5c34119.This checkpoint captures the single-child Traefik supervisor, atomic route snapshots, private health ping, provider
*_FILEpath handling, log redaction, node registry wiring, and the namespace/Pebble test harness. The prior compute01 log is a compile failure from an older source snapshot (raw libc calls rejected byforbid(unsafe_code)); the current source replaces those calls with safe nix locking and process-group setup. No current J62 test is running and no green result is claimed.Next: refresh the VM checkout from this pushed head, run the focused proxy tests, finish trusted mesh name/suffix/grant propagation and actual
publishbehavior, then complete the acquisition/lifecycle/staging tests, VM gates and size report. I will remove the sole J62 target after final verification.J62 resumed on
proxy-traefik-281at5a096f41595f2213306fa4d48729eace0e8ab48b, based onorigin/dev65d9861914e8cd4a734d533d64ed1f4ea5c34119.The compute01 build-slot item PID 4175181 remains queued and untouched; it runs
compiled_mesh_snapshot_carries_stable_proxy_name_and_dns_suffixagainst the existing VM source snapshot. Its log is/home/claude/.cache/codex-scratch/j62/mesh-red.log.Source audit finding:
ProxyAgentPlugin::callcurrently returnsRejectedfor every non-list publication, so the already-built supervisor/config paths are not yet reachable throughpublish <port>. The accepted Mesh config carries directional peer policies but this branch has no accepted machine-name/DNS-suffix context. The queued integration test is expected to expose the missing snapshot fields. Publication must derive source addresses from the accepted inbound TCP grant for that port; request JSON cannot supply grants or suffixes.Plan: observe that VM RED, then add the narrow accepted-Mesh context and connect
publishto the existing supervisor; finish the namespace/Pebble and installer/lifecycle coverage; run focused tests andscripts/verify faston the VMs; report the x86 static-size delta; remove the one J62 target; push and post the exact gate output. Occupied 80/443 falls back to 10080/10443. No nftables redirect is safe when another process may bind the mesh address explicitly. Estimate remains about 3 hours of code and verification, excluding slot wait.Finding: the first compute01 queue run completed with
0 passed; 5 filtered out, so it did not exercise the intended regression. The VM source copy was stale:crates/plugins/mesh/tests/configuration.rswas SHA-25647384abc99841ea8452de8d4dd29ab402e3122bbe768fdc1c5d7da872a6e3c32versus local31e10c45ca65d74c55982eb093b5c294e42b17b74a9f3ebb6d624f915d5acd37. I synchronized the branch sources (kept the VM target) and resubmitted the focused test; PID 157710, log/home/claude/.cache/codex-scratch/j62/mesh-red.log.Orchestrator choice for slice 1, pending owner rename if desired: use a separate
infraternal unpublish <port|name>command as the inverse ofpublish. It removes the route, prints what stopped being reachable, is idempotent, and a missing target returns D303missing(exit 4) with a one-line message.--jsonuses the shared CLI envelope. I will record this in the proxy module doc and test both selector forms and the missing result.Follow-up finding: the synchronized source SHA matched, but the second run also reused the old Cargo test binary because tar preserved the source timestamp; it again reported
0 passed; 5 filtered out. I touchedcrates/plugins/mesh/tests/configuration.rson compute01 to invalidate the stale harness and resubmitted. Current queued PID 228639, same log/home/claude/.cache/codex-scratch/j62/mesh-red.log.Orchestrator CLI choice for #281: use a separate
infraternal unpublish <port|name>inverse topublish. It reports what stopped being reachable; a missing target is idempotent D303missing(exit 4) with a one-line message, and--jsonuses the shared envelope. The module doc and CLI tests record this choice pending any owner rename.J62 continuation: compute01 test PID 228639 remains queued at position 1;
/home/claude/.cache/codex-scratch/j62/mesh-red.logstill contains only the queue notice. I preserved that VM source snapshot and added one more local regression requiring name-only Mesh config changes to reach the proxy. No production code changed in this continuation. Next: observe the intended RED, then implement accepted identity propagation and finish the publish/unpublish action wiring.J62 resumed on
proxy-traefik-281. I rebased the pushed WIP onto currentorigin/dev(0067e1255407f0f3ed0f1d6c5e5caf51667e00a0) and pushed the rebased checkpoint at42a919e06de62611d3a84da3cb3f22a346fb49ff.The accepted-Mesh RED completed on compute01 as expected:
compiled_mesh_snapshot_carries_stable_proxy_name_and_dns_suffixfailed because the serialized snapshot hasnullformachine_nameinstead ofhost-2. I am adding identity to the existing chunked revision path and wiring the proxy only from an accepted complete config. This keeps the same inbound TCP grants as Mesh and prevents caller JSON from asserting grants or suffixes.Next: propagate the server-origin DNS suffix, connect the accepted context to
publish, finishunpublish <port|name>and its D303missingbehavior, then run focused proxy/Mesh/CLI and namespace Pebble verification on the VMs. The current estimate is about three hours of implementation and VM verification, excluding build-slot wait. I will keep one J62 target and report the report-only static size delta. The occupied-port investigation still rejects an nftables redirect because it would capture another service explicitly bound to the mesh address.J62 continuation on
proxy-traefik-281. The latest focused compute01 run confirmed the new unpublish action and command-table entry, with 50 tests passing and one expected-message assertion still failing; its log is/home/claude/.cache/codex-scratch/j62/proxy-unpublish-green.log. The remaining mismatch was the missing-port line omitting the wordport.I have corrected the local D303 response and added action-level cases for unpublish by port/name and accepted-grant revocation. Current work wires Mesh's retained accepted config into the proxy controller, then connects on-demand verified acquisition and supervision to publish/unpublish. No UI or real ACME certificates are in this slice.
Next: sync and run the focused proxy/agent/Mesh tests on a VM, run the namespace Pebble staging case, fix any failures, run
scripts/verify fast, report the D243 size delta, push the branch, and remove the sole target. Estimate: about 90 minutes of implementation and verification, plus slot wait.J62 port-conflict investigation for #281: do not add an nftables redirect in slice 1. The current rootful probe tries to bind the node's exact Mesh address on 80 and 443 and uses 10080/10443 when either conventional listener is occupied. A redirect limited to the Mesh interface and Mesh destination address still runs before local socket delivery. It would therefore capture Mesh-IP:80/443 packets even when another server explicitly owns that address and port, diverting that server's Mesh traffic into Traefik. The redirect cannot establish socket ownership or distinguish the other server's routes. Without explicit coordination or an exclusive reserved listener, the behavior is not safe. Keep direct address binding and high-port fallback; no host firewall rules are changed.
Orchestrator choice for the CLI: use a separate
infraternal unpublish <port|name>command as the inverse ofpublish. It accepts the local port or exact published name, reports what stopped being reachable, and removing the last publication starts the 10-second drain. Missing targets are idempotent and return D303missing(exit 4) with a one-line message;--jsonfollows thepublishenvelope. The module doc and command table record this as the orchestrator’s choice, open to kayg renaming it.J62 checkpoint pushed:
cf0bef881onproxy-traefik-281, based on currentorigin/dev6093cb8a. The last focused proxy run on compute01 completed with 50 passing tests and one expected message assertion failure; the port-label fix is now in the commit. I am rerunning the crate suite from an isolated VM worktree, then will run the namespace Pebble case andscripts/verify fast. The occupied-port analysis is recorded in comment 73730; no nftables redirect is added.The focused run on
cf0bef881completed with 54 passing tests and 2 failures. Both expose one formatting gap: the expected rootless:10443listener port is absent frompublishURLs and theunpublishreachability message. Theunpublishport-label assertion now passes. I am tracing the selected listener ports into the controller and will rerun the failing tests after the fix.The listener-port propagation change compiled far enough to identify one type mismatch: the scope-restart helper still returned
Result<()>on its ineligible branch after changing toResult<ListenerPorts>. No tests ran on that attempt. I am correcting the branch and removing the associated unused imports/state assignments before rerunning.J62 resumed on branch
proxy-traefik-281atcf0bef881(merge baseorigin/dev6093cb8a). Existing WIP adds signed Traefik acquisition, owner-only DNS token custody, mesh-grant route enforcement, lifecycle supervision, and CLIpublish/unpublishactions. I confirmed the current module doc and command table record the orchestrator's separateunpublish <port|name>choice, pending owner rename.Plan: (1) complete positional CLI and D303 missing/JSON regression coverage; (2) resolve the pending proxy crate test on compute01 and run the required fast gate on the emptier VM; (3) report-only size deltas, inspect the nftables finding, clean targets, commit/push atomic changes, and post the final receipts. Estimated remaining work: 60 minutes, subject to VM gate duration. The previous J62 test process is being checked before resubmission; I will keep its log path in the next progress note.
J62 update: the detached compute01 proxy crate suite finished on the four-file listener-port snapshot with
TEST_EXIT=0; receipt:test result: ok. 57 passed; 0 failed; 0 ignored; finished in 0.09s(doc tests also green; namespace/Pebble remains a separate ignored test). I committed and pushede3fd798f6afd898aa4997cfc6a043ec6a2d89a13, which carries the selected HTTP/HTTPS listener ports into publication and drain URLs. Next: update the compute01 checkout to this exact head, run the full fast gate and the staging Pebble namespace test, capture size rows, then clean the J62 target and post final receipts.J62 custody audit finding:
FileDnsTokenCustody::deleteis implemented and unit tested, but no production caller invokes it.DnsProviderProfileis loaded from the agent environment and the supervisor only callsget; removingINFRATERAL_DNS_PROVIDERtherefore leaves its owner-only token file behind. I will add a failing regression and cleanup of inactive provider directories when the agent loads its current provider profile. I also found theTraefikChild::spawndoc says credential bytes are never read there, while the code reads them into zeroizing memory only to redact logs; the child still receives only_FILEpaths. I’ll correct that comment for accuracy.J62 gate finding: the exact-head
scripts/verify fastlog reportserror[E0433]: failed to resolve: use of unresolved module or unlinked crate tracingatcrates/infraternal-agent/src/plugins.rs:91, where proxy child output is connected to the existing tracing pipeline. The agent crate did not declare the workspace tracing dependency. I added the existing workspace dependency locally; I will validate it on the VM after the current exact-head gate exits, then rerun the final gate on the fixed head.J62 fast-gate finding: the current Clippy stage reports 25 errors in the new proxy crate and 3 in the Mesh snapshot integration. The proxy findings are mostly naming/ownership lints plus 64 KiB stack buffers and two overlong functions (
render_dynamic_config,extract_executable, andrun_supervisor); the Mesh findings are two clone assignments and one complex callback type. The log is/home/claude/.cache/codex-scratch/j62/verify-fast.log. I’m tracing each to the branch diff and will fix the code rather than add lint allowances.J62 resumed on proxy-traefik-281 at
e3fd798f6a, based on current origin/dev6093cb8a83. I reread BRIEF-COMMON.md, the #281 plan and selected owner answers, D316/D314/D303, #261 R2/J20/R3, #266 DNS naming, and #211 edge-update context. The current local changes add the tracing dependency used by proxy child logs and a failing DNS-token cleanup regression. Compute01 still has the older exact-head scripts/verify fast run active in its J62 worktree; I have not submitted a competing build. Next: finish provider-removal cleanup and resolve the remaining installer test failures, then run the proxy suite, namespace/Pebble case, and exact-head fast gate through build-slot. The orchestrator's separate unpublish command is already in the module doc and recorded in #281 comments 73591 and 73753.J62 resumed on
proxy-traefik-281ate3fd798f6afd898aa4997cfc6a043ec6a2d89a13, based on currentorigin/dev6093cb8a83b810a119269d6045e8720a3e3922b6.The prior J62 compute01 test is no longer present in the build-slot queue. Current status shows all slots occupied on both compute01 and dev-02, with unrelated J72 and J40 jobs queued. I have not started a competing build. The local worktree has follow-up changes for the tracing dependency, provider-token cleanup, and compile/lint fixes.
Plan: (1) finish and split the current WIP into atomic test-first commits; (2) once a slot opens, run the proxy crate suite and namespace/Pebble test on a VM; (3) run
scripts/verify faston the emptier VM and report the D243 static-size delta; (4) push the branch, post exact receipts, and remove the single J62 target. Estimate: about 2 hours of active work, excluding build-slot wait. Theunpublish <port|name>choice is already recorded as the orchestrator's choice pending owner rename; the nftables finding remains that redirection is unsafe when another server explicitly binds the mesh address.J62 resumed on
proxy-traefik-281from pushed heade97be5b5a, whose merge base isorigin/dev6093cb8a83b810a119269d6045e8720a3e3922b6. Currentorigin/devhas advanced tod1ec515e4with the #266 mesh-DNS merge; I will integrate that base before final verification.The stale-token custody regression is now RED on the pre-fix commit:
agent_start_removes_custody_files_for_a_removed_dns_providerfailed because the removed provider's token file survived startup (0 passed; 1 failed, expected assertion). Log:/home/claude/.cache/codex-scratch/j62/custody-cleanup-red-final.log. Earlier retries did not reach Cargo: one used a missing nix path and the other ran outside the flake worktree.Next: wire provider cleanup behind
DnsTokenCustody, verify configured-provider retention, then run the focused suite, namespace/Pebble case andscripts/verify faston the least-loaded VM. I will report D243 size deltas and remove the J62 target before handoff.Focused custody GREEN follow-up found a lockfile mismatch before tests started: the local WIP adds the existing
tracingworkspace dependency to the agent'sproxyfeature, but the current Cargo.lock lacks that agent dependency edge.cargo test --lockedstopped withcannot update the lock file; no test ran. I am resolving the lock metadata on compute01 with the same source snapshot, then will rerun with--lockedand sync the resulting Cargo.lock back.The full proxy crate run on the current WIP reported
58 passed; 1 failed: the configured DNS provider's credential file did not surviveProxyController::for_agentstartup. The removed-provider cleanup RED/GREEN passes, so I have not treated the cleanup as complete. I added safe in-child checks for the configured provider profile and token-file path before and after controller startup to isolate the mismatch; the focused diagnostic run is queued through compute01's build-slot.J62 resume checkpoint on
proxy-traefik-281: pushed heade97be5b5abbccd9d0411228f7acd9f827530fa73; baseorigin/devhas advanced from merge base6093cb8a83b810a119269d6045e8720a3e3922b6tod1ec515e44dabccb640801c4a1ef159b32e004fe.The custody diagnostic completed on compute01 and failed after
ProxyController::for_agentstartup: the selected provider token path was missing. Log:/home/claude/.cache/codex-scratch/j62/custody-retain-diagnostic.log. I added local assertions that distinguish the configured provider identity and direct cleanup from controller startup; those assertions still need a VM run. The prior queued J62 test is gone; current queue entries are unrelated.Next: finish the custody regression, rebase onto current
origin/devfor the merged #266 DNS suffix/grant path, run the proxy and Mesh focused tests plus the namespace Pebble staging case throughbuild-slot, then runscripts/verify fast, report both static-size deltas, push and remove the J62 target. Estimated active implementation and verification: about 2 hours, excluding queue waits. The separateunpublish <port|name>choice and its D303 missing/exit-4 response remain recorded in the module doc, command tests, and prior #281 comments.J62 resumed from pushed head
fe43a8314onproxy-traefik-281, rebased ontoorigin/devd1ec515e44dabccb640801c4a1ef159b32e004fe. The worktree is clean. I am continuing with the custody restart regression and tracing the server-derived Mesh DNS suffix into the accepted publication context without enabling the default-off Mesh DNS feature. Plan remains about 2 hours of active work, excluding build-slot waits: finish those regressions, run focused proxy/Mesh and namespace/Pebble checks, runscripts/verify fast, report static-size deltas, push and post exact receipts, then remove J62 targets. Current queue check: compute01 is full with 122 GB free; dev-02 has free slots but only 16 GB free, below the build floor, so compute01 is the only usable Linux VM right now. No J62 build is pending.Finding on #281: the feature-off Mesh store discards
public_origin, thenSnapshot::configcallsconfiguration::compile, which hardcodesinternalforExposureIdentity. The proxy therefore receives the wrong naming suffix in normal builds. RED evidence on compute01 atfe43a8314:cargo test --locked -p infraternal-plugin-mesh --test store publication_identity_tracks_server_public_host_across_restart -- --exact --nocapturereturnedtest result: FAILED. 0 passed; 1 failedwithleft: "internal",right: "ssh.example.test". I will pass the public-host-derived suffix through the accepted exposure identity and advance the configuration revision when that origin suffix changes, while keeping resolver fields andmesh-dns-spikedefault-off.J62 diagnostics: the custody restart regression was configured incorrectly. It set
INFRATERAL_DNS_*(missing theN), whileDnsProviderProfile::from_environmentreadsINFRATERNAL_DNS_*; the profile was thereforeNone, and startup correctly deleted the provider as unconfigured. I am correcting the test fixture before judging the implementation. The first suffix GREEN attempt also stopped at compile time becauseDEFAULT_DNS_SUFFIXis currently gated behindmesh-dns-spike; I will make the shared compatibility constant available to the server snapshot compiler without enabling resolver fields or the default-off feature.J62 resumed from pushed head
fe43a8314onproxy-traefik-281; the branch merge base isd1ec515e4, and currentorigin/devisffebd0561.The resumed compute01 checks in
/home/claude/.cache/codex-scratch/j62/proxy-recheck.lognow pass:publication_identity_tracks_server_public_host_across_restart(MESH_SUFFIX_EXIT=0) andagent_restart_retains_the_configured_dns_provider_files_for_renewal(PROXY_CUSTODY_EXIT=0). The earlier custody failure came from a test fixture spellingINFRATERAL_*; the corrected fixture now verifies the configured provider survives agent restart. No J62 process remains queued or running.The local WIP includes the default-off Mesh DNS suffix propagation and the private-ping negative health regression. The module documentation and command table record the orchestrator's CLI choice:
infraternal unpublish <port|name>removes one publication, reports what stopped being reachable, returns D303missing/ exit 4 for an absent target, and uses the shared--jsonenvelope. Removing the last route starts the existing 10-second drain. This choice remains open for an owner rename. Next: rebase onto currentorigin/dev, run the focused proxy/Mesh/CLI and namespace/Pebble checks, then the required fast gate and report-only size comparison.J62 branch update:
proxy-traefik-281is now rebased onto currentorigin/devffebd0561a010690583481abc4d583ecaca4542e; pushed checkpoint head0d8e4b6a56ffc5109e116c1f51f859de4a6a305f. The seven-file local WIP remains uncommitted: it carries the public-host-derived Mesh suffix in normal builds and tightens the provider-restart/private-ping regressions. The two targeted GREEN results remain inproxy-recheck.log; the stale queued run is complete.I am checking both VM slots and will run the full proxy suite, Mesh regression, namespace/Pebble staging case, and final fast gate on a VM with a free slot and enough disk. I will consolidate the crash-safety WIP commits before the final push.
The first full-format preflight on the rebased snapshot stopped before compiling:
cargo fmt --all --checkfound existing formatting drift inmesh/src/node_actions.rsand proxy controller/supervisor files (the new health regression was also unformatted). I ran rustfmt only for the touched Mesh and proxy packages; no behavior changed and no test ran in that attempt. I am resyncing the formatted source and rerunning the focused checks throughbuild-slot.Focused dev-02 suite finding on the current source:
58 passed; 2 failed.live_process_without_a_healthy_private_ping_is_reported_unhealthyfailed becauseIsRunningonly checkedChild::try_waitand never probed the configured private/ping; the source path returnedtruefor a live child even after HTTP 503. I added the loopback probe to that health observation, with a 250 ms bound.The existing first-route lifecycle test also timed out once in the full suite, but a fresh isolated rerun passed (
1 passed; 0 failed; finished in 1.15s) and its fixture answered the first healthy request. The test helper now reports how many requests it served; I will keep this finding open until a full rerun determines whether the timeout repeats.J62 checkpoint on
proxy-traefik-281, pushed headd267360998ba71b8e80038927776d8dd35accb44, based on currentorigin/devffebd0561a010690583481abc4d583ecaca4542e. The checkpoint preserves the normal-build public-host-derived proxy suffix, private-ping health observation/regression, and corrected custody-restart fixture. The same snapshot is queued on dev-02 in~/.cache/codex-scratch/j62-final/focused-tests.sh(fmt, proxy tests/clippy, Mesh store test/clippy); its log is~/.cache/codex-scratch/j62-final/focused-tests.log. After it completes I’ll run the namespace/Pebble check and finalscripts/verify fast, then split this WIP checkpoint into atomic commits. The separateunpublish <port|name>command remains recorded in the module doc and #281 as the orchestrator’s choice, open to owner rename.J62 focused dev-02 run completed from the queued snapshot. Formatting passed; the proxy crate reported
test result: ok. 60 passed; 0 failed; 0 ignoredand its ordinary integration target reported1 passed. Proxy Clippy then exited 101 with seven Rust 1.99 pedantic findings: similar listener variable names, missing backticks around OpenBao, one extraction function over 100 lines, two 64 KiB stack buffers, a derivable runtime default, and a collapsible lifecycle match. The script stopped there, so the Mesh store test/clippy did not run. Full receipt:/home/claude/.cache/codex-scratch/j62-final/focused-tests.log; no J62 process remains. Next I’m splitting the extraction helper and fixing the lint findings, then I’ll rerun the focused checks through build-slot.After the first focused run, I split the long archive walk into per-entry/executable/license helpers and moved its 64 KiB copy buffers to the heap; I also fixed the remaining strict Clippy findings without changing behavior. WIP checkpoint
9b6c5ff84239470e866e352051a3658d059c5049is pushed onproxy-traefik-281. A fresh focused run is queued on dev-02 throughbuild-slotat~/.cache/codex-scratch/j62-final/focused-tests-2.sh; the receipt will be~/.cache/codex-scratch/j62-final/focused-tests-2.log. Current queue position was 2 with all slots busy and 29 GB free; compute01 also had all slots occupied.J62 focused Clippy finding on branch head
9b6c5ff(the lint-only edits are still uncommitted): the proxy unit suite passed 60/60 and the child process test passed 1/1.cargo clippy --locked --jobs 3 -p infraternal-plugin-proxy --all-targets -- -D warningsthen failed only in integration-test crates: 13expect_useddiagnostics intests/process.rs, and 30expect_useddiagnostics plustoo_many_linesintests/namespace_pebble.rs. The earlier five lints in the proxy library/unit tests no longer appeared. Full receipt:/home/claude/.cache/codex-scratch/j62-final/focused-post-lint.logon dev-02 (FOCUSED_EXIT=101).I added test-crate-scoped failure-context allowances and one function-scoped line-count allowance; production linting remains unchanged. Those changes have not yet been rerun. Compute01 has no free slot and dev-02 is below its 25 GB admission floor, so I’m syncing the current source to the existing compute01 checkout and will submit the next focused run detached through build-slot.
J62 continuation: the current six-file Clippy cleanup is synced by SHA-256 to the detached dev-02 checkout at
9b6c5ff84239470e866e352051a3658d059c5049. The focused run covers formatting, proxy tests and Clippy, the #266 suffix persistence regression, Mesh Clippy, CLI and agent action tests, plus release component manifest/publication tests. It is queued throughbuild-slotat position 2; no tests have started yet. Log:/home/claude/.cache/codex-scratch/j62-final/focused-final.log. Next: finish the focused run, run the x86 Pebble namespace test and exact-head fast gate, then post the final head and verbatim gate output.Focused-run launcher finding: the first detached invocation exited before tests with
path "/home/claude" does not contain aflake.nix``. Root cause was invokingnix developbefore changing into the J62 checkout; no compilation or test began. I added the missing checkoutcdto the runner, confirmed dev-02 has free build slots and 30 GB free, and am resubmitting throughbuild-slot.Focused verification finding: Mesh Clippy reports
compile_innerat 101/100 lines after the public-host-derived suffix was added. I added a regression test for server-default, legacy custom, and explicit suffix selection; it failed as expected at compile time because the extracted helper was not yet defined (RED_EXIT=101, unresolvedselected_dns_suffix). I extracted the existing selection rule into that helper without changing its cases. Next: run the regression and Mesh Clippy GREEN before restarting the full focused suite.Mesh suffix lint fix is verified RED→GREEN: the new
server_dns_suffix_is_default_and_explicit_or_legacy_choices_wintest first failed on the absent helper, then passed1/1after extracting the existing selection rule.cargo clippy --locked --jobs 3 -p infraternal-plugin-mesh --all-targets -- -D warningsalso passed. The full focused proxy/Mesh/CLI/release-test script has been resubmitted throughbuild-sloton dev-02; log:/home/claude/.cache/codex-scratch/j62-final/focused-final-3.log.The full focused script reached the CLI check after these passed: formatting, all proxy tests, proxy Clippy, the #266 public-host suffix persistence test, and Mesh Clippy. The CLI check exited immediately with
error: no library targets found in packageinfraternal``; source inspection confirms this package is a single binary and the publication tests are nested undercomponent_cli::tests. I’m correcting the test invocation to--bin infraternal component_cli::tests, then I’ll rerun the complete focused command.CLI test finding: compiling
component_cli::testson the J62 branch produced three errors: the new test imported missingadd_cli_values, andrun_actionreturnedexecute'sResult<Exit>throughblock_on_io, whose existing contract isResult<()>. The missing mapper also meant registered positional and named values were never put into the request. I added schema-typed argument mapping (including positional port/name values and flags) and wrapped theExitresult while retaining the existing reactor helper contract. The new CLI tests have not yet passed; next is the focused RED→GREEN run.The focused CLI regression is GREEN:
cargo test --locked --jobs 3 -p infraternal --bin infraternal component_cli::tests -- --nocapturepassed5/5, including positional publish/unpublish mapping and D303 exit 4.run_actionnow captures the typed exit while keepingblock_on_io's existing unit-return contract. The compiler also exposed anunused_mutwarning in the J62 Mesh feature wiring; I restored the cfg-scoped mutation pattern and am checking Mesh with both agent/server features plus the default-off DNS feature before the full focused rerun.Mesh feature verification is GREEN after the cfg-scoped mutation adjustment:
cargo clippy --locked --jobs 3 -p infraternal-plugin-mesh --all-targets --features agent,server -- -D warningspassed, andcargo check --locked --jobs 3 -p infraternal-plugin-mesh --features mesh-dns-spikepassed. The first retry exposed a missing semicolon in the cfg-only binding; that syntax error is corrected. The Mesh suffix selector regression and the five CLI tests are green. Next: rerun the full focused suite on the exact current source, then the x86 Pebble integration and final gate.The full focused runner stopped at
cargo fmt --all --checkbefore compilation, with two formatting diffs in the newcomponent_cli.rsargument mapper. No tests ran in that invocation. I’m applying the repository formatter to the adapter and will rerun the full focused command.J62 focused rerun is active on dev-02 (PID 3362040); log: /home/claude/.cache/codex-scratch/j62-final/focused-final-7.log. The preceding run reached the agent Mesh grant test and stopped at compile time:
assert_eq!could not infer the type for expected.parse()values compared withVec<IpAddr>. The expectation now uses explicitparse::<std::net::IpAddr>(); this is a test-fixture type inference fix, not a runtime behavior change.\n\nCommand-table choice from the orchestrator: use separateinfraternal unpublish <port|name>. It prints what stopped being reachable, is idempotent with D303missing/ exit 4 and a one-line message, and supports--json. Removing the last publication enters the existing 10-second drain. This is an orchestrator choice pending any owner rename and is documented in the proxy module doc and command table.J62 is aligned with current origin/dev
9f36dd8e0a5e9e799cd3264cb6e1e7605071c5e2. The merge had one Cargo.lock conflict; I kept the latest dev dependency pins and added only the proxy component graph (101 added lock lines, no unrelated dependency upgrades).cargo check --locked --jobs 3 -p infraternal-plugin-proxypassed on compute01. The full focused proxy/Mesh/CLI/release suite is now running on that merged source through build-slot; log:/home/claude/.cache/codex-scratch/j62/focused-merged.log. Next: x86 namespace/Pebble staging ACME,scripts/verify fast, static-size report, cleanup, final push.Namespace/Pebble setup RED finding on compute01: both pinned archives passed their SHA checks, but the runner expected binaries at archive root.
tar -tzfshowedpebble-linux-amd64/linux/amd64/pebbleandpebble-challtestsrv-linux-amd64/linux/amd64/pebble-challtestsrv. I updatedscripts/test-proxy-pebble.shto extract those exact allowlisted paths to private test files, then chmod and validate them. The corrected staging ACME test is running through build-slot; log:/home/claude/.cache/codex-scratch/j62/pebble-e2e-2.log.Pebble runner follow-up: after the archive paths were corrected, the wrapper exited before the test with
cargo: command not foundbecause a nested login shell reset the PATH supplied bynix develop. No ACME scenario ran. I am rerunning the same script directly from a non-login shell inside the dev shell, retaining the existing job target.Pebble runner finding on
proxy-traefik-281: the corrected allowlisted archive paths still failed on rerun because shell redirection attempted to overwrite an existing0500binary. Evidence:/home/claude/.cache/codex-scratch/j62/pebble-e2e-3.logended withPermission deniedat the binary path andPEBBLE_E2E_3_EXIT=1; no ACME scenario ran. I changed the runner to extract both binaries into a unique private staging directory, validate and chmod them, then atomically rename them into place.Next: run the Pebble namespace staging-ACME test on compute01 through
build-slot, runscripts/verify faston the VM with the best free-slot/disk capacity, report the D243 static-size delta, then push and post the final head plus verbatim gate output. Active estimate: about 45 minutes, excluding build-slot waits. The separateunpublish <port|name>command choice and D303 missing/exit-4 behavior are already documented in the module and prior #281 comment as the orchestrator's recommendation pending owner rename.Second Pebble rerun finding: staging and replacing the two pinned Pebble executables now succeeds, then extraction failed on the cached Traefik executable at
scripts/test-proxy-pebble.sh:95withPermission denied(PEBBLE_E2E_4_EXIT=1). The root cause was the same: shell redirection tried to truncate the existing0500executable before checking the signed-manifest executable digest. The runner now stages Traefik under a unique same-filesystem name, verifies its digest, chmods it, then atomically renames it into place; one exit trap cleans all remaining staging paths. I am resyncing and rerunning the namespace staging-ACME test, then the final fast gate. Estimate remains about 45 minutes active, excluding build-slot waits.Pebble ACME finding: the runner now replaces the cached Pebble and Traefik executables and starts the namespace scenario, but the test times out after 45 seconds waiting for issuance and the mesh route (
PEBBLE_E2E_5_EXIT=101). Its privateacme.jsonremained the initial 2-byte{}, so no certificate was issued. The fixture currently drops all Traefik child logs, leaving the provider/ACME failure unlocalized. I will capture only a bounded tail of the supervisor's already-redacted log lines on test failure, then rerun before changing production behavior. No real CA endpoint is used.Pebble diagnostic result: on the 45-second failure, the HTTPREQ fixture recorded zero provider calls and no adapter errors; verified curl received Traefik's default certificate, with no SAN for
docs.node.example.org. The persisteddynamic.jsoncontains the exact Host router andcertResolver: dns, and the static config contains thednsACME resolver. I am adding a failure-only--insecurerequest to report the HTTP status while preserving certificate verification as the success condition; that will distinguish a missing file-provider route from an ACME activation failure before any production change.Confirmed file-provider bug: Traefik v3.7.17's
isFileSupportedaccepts only.toml,.yaml, and.yml, so it silently ignores.jsonsnapshots (source: https://github.com/traefik/traefik/blob/v3.7.17/pkg/provider/file/file.go#L516-L523). The namespace test'sdynamic.jsontherefore never loaded: the failure-only request returned HTTP 404 and HTTPREQ saw zero calls. Production also wrote JSON bytes toservices.toml, which is not TOML. I am aligning both paths on oneservices.yamlfilename and keeping the JSON-serialized snapshot, which is valid YAML, then rerunning the Pebble ACME case.The
services.yamlfilename resolves the routing failure: the latest Pebble run returns HTTP 200 from the granted backend when certificate verification is bypassed. The remaining ACME failure is localized after the DNS provider hook: HTTPREQ received one/presentcall with no adapter error, the node-local Pebble account was registered, but its certificate list stayed empty; the verified request still saw the default certificate. Evidence:/home/claude/.cache/codex-scratch/j62/pebble-e2e-9.log. I am checking the fixture's TXT publication and Pebble validation path before changing production code.J62 reviewer-finding triage on the merged worktree. Evidence: release producer
inspect_archive()accepts regularREADME.md, while the runtime installer allowlist is exactlytraefikandLICENSE.md; the new producer regression ran on compute01 and failed as expected (ValueError not raised). Mixed public/mesh routes currently share wildcard-bound entrypoints, while mesh routes retain their exact-host and grant middleware. Log reading retains only the first 8 KiB before full-secret redaction, although provider values can be 64 KiB. I also checked the backoff claim:recv_timeoutreturns as soon as a replacement command is queued, so the review’s claimed full-delay wait is not reproduced by this std mpsc path; I am adding a regression that proves queued removal is handled before its deadline. Next: make archive and mixed-listener regressions pass, discard oversized log lines, complete Pebble/focused tests, then run the final VM gate.Additional #281 acquisition evidence: compute01 downloaded the pinned Traefik 3.7.17 linux/amd64 archive, verified its SHA-256 against
traefik_v3.7.17_checksums.txt(UPSTREAM_CHECKSUM_MATCH=True), and listed its tar members:CHANGELOG.md,LICENSE.md,traefik. The producer and runtime importer need matching explicit allowlists including the signed changelog, while only the executable is extracted; arbitrary regular files remain rejected.J62 resumed on
proxy-traefik-281atedbaffba10c98f8090471d2bae1e054038f41e59. The separateinfraternal unpublish <port|name>action is in the root command table; it reports the stopped URL and 10-second drain, returns one-line D303missing/ exit 4 when absent, and shares--json. This remains the orchestrator’s choice pending owner rename, as noted in the proxy module doc.Pending local changes cover upstream archive allowlisting, mixed public/mesh listener separation, oversized log-record discard, and Pebble fixture diagnostics. The latest staging-ACME run on compute01 reaches the granted backend over HTTP but still serves Traefik’s default certificate (HTTPREQ
/present=1, no adapter errors). I’m fixing the fixture resolver path and syncing the merged source before rerunning focused tests andscripts/verify fast.API-disabled test discrepancy on
proxy-traefik-281atedbaffba10c98f8090471d2bae1e054038f41e59: the earlier 1/1 pass came from a reused component target with stale test artifacts. The copied source hashes matched the current worktree, but the result was not reproducible after clearing the package artifacts. Through compute01'sbuild-slot,cargo clean -p infraternal-plugin-proxyremoved 2,728 files (638.5 MiB); rebuilding and rerunning the same named test then failed as expected atconfig_tests.rs:69:assertion failed: !rendered.contains("[api]")(exit 101). The renderer still emits the[api]table. I am removing that table so Traefik's API/dashboard stay at their disabled defaults, then will rerun this test green.J62 command table choice (orchestrator decision, pending an owner rename): use the separate
infraternal unpublish <port|name>command as the inverse ofpublish. It reports what stopped being reachable and allows the supervisor's 10-second drain. A missing target returns D303missing(exit 4) with a one-line message;--jsonuses the standard command envelope. The root action table and module docs now record this choice, with CLI and controller coverage.Pebble ACME root-cause finding: fresh namespace runs consistently serve the granted backend over HTTPS with an unverified curl, but return Traefik's default certificate after 45.5 seconds; the HTTPREQ fixture records one successful
presentcall and no adapter errors. Traefik v3.7.17's ACME source configures both authoritative-name-server and recursive DNS propagation checks by default, while this test publishes its TXT record only through the local challenge server for a syntheticexample.orgname. I’m adding a test-only setting to skip the authoritative check while retaining recursive validation against the explicit local resolver; production keeps the default checks.ACME test diagnosis correction: disabling Traefik's authoritative-name-server check exposed the next boundary. Debug logs show the challenge provider completes (
HTTPREQ calls=2), then Pebble rejects validation because its DNS lookup goes to the host stub127.0.0.53:53and is refused. The Pebble v2.9.0 README documents-dnsserver <host:port>; the fixture used an unsupportedPEBBLE_VA_DNS_SERVERenvironment variable. I’ll point Pebble at the isolated challenge DNS server with that flag. The ACME CA's DNS validation remains active.Pebble resolver flag fix confirmed ACME issuance: the served certificate now matches
docs.node.example.org, and HTTPREQ records bothpresentandcleanup. The remaining test failure is trust-chain verification: curl reports issuerCN=Pebble Intermediate CA 7c23a4. Pebble v2.9.0 documents that its ACME roots and intermediates are regenerated on every launch and exposes the current root at/roots/0;pebble.minica.pemonly trusts Pebble's HTTPS service certificate. I’ll fetch the ephemeral issuance root over Pebble's verified management HTTPS endpoint and use it only for the staging curl checks.Mesh-IP redirect investigation: a rule limited to the mesh interface and mesh destination IP would still be unsafe when another server owns TCP 80/443 on that address. The nftables NAT reference states that
redirectis DNAT to the local host and only applies in prerouting/output NAT chains; its prerouting example rewrites inbound port 22 before local delivery. Thus mesh-IP:80/443 packets would be rewritten to 10080/10443 before the existing socket is selected, even when the other server explicitly binds the mesh IP. This steals that server's mesh traffic. No redirect is implemented; the proxy keeps the high-port fallback whenever 80/443 are occupied. Reference: https://wiki.nftables.org/wiki-nftables/index.php/Performing_Network_Address_Translation_(NAT)J62 progress: WIP commit
538ba1506is pushed toproxy-traefik-281. Freshcargo test -p infraternal-plugin-proxypassed on compute01 (68 unit tests plus the process test);scripts/release/traefik_component_test.pypassed 6/6. The Pebble test now uses its documented-dnsserverflag and fetches its per-run issuance root from the verified local management endpoint. That run is queued behind all four compute01 slots (queue PID 8579); its log is/home/claude/.cache/codex-scratch/j62/proxy-pebble-root.log. Next: collect the e2e result, run the final gate, report static sizes, and clean the target.J62 resumed from pushed WIP
538ba1506, rebased onto currentorigin/dev9f36dd8e0, and pushed the rewritten branch at1572c89d8. The one lockfile conflict was resolved by retaining current-dev entries and the J62 component entries. Theunpublish <port|name>decision, D303 missing result, and 10-second drain are already recorded in the module and earlier #281 comments. The last pushed-head Pebble root run passed 1/1; focused tests andscripts/verify faststill need fresh receipts on the rebased head. Next: run them through compute01build-slot, report both static sizes, then remove the J62 target.J62 final-review findings on
proxy-traefik-281at1572c89d8(independent review against base9f36dd8e0): seven important end-to-end gaps remain. I verified the first root cause in source: the release producer omitsexecutable, which RustComponentManifestrequires; the other findings trace to the publisher/downloader path mismatch, no HTTPS port allowance in restricted Mesh grants, imported artifacts not being reused offline, child lifetime not tied to the infraternal agent, stale listener address after accepted Mesh identity changes, and redirect middleware defaulting to port 443. The existing component unit tests and the namespace fixture do not cross these boundaries.Plan and estimate: add one failing regression for each boundary, fix producer and published mirror keys, wire restricted Mesh grants to the exact proxy listener ports, reuse signature-verified imported/current artifacts without network, make proxy child lifetime crash-safe, reconcile listener addresses on accepted Mesh updates, and render the audience-specific HTTPS redirect port. Then rerun proxy/CLI/Mesh/agent/release tests and the namespace Pebble ACME test, run
scripts/verify fast, report static sizes, commit the fixes atomically, push, and post exact receipts. Estimated active work: about 2 hours, excluding build-slot waits. The exact-head proxy run passed 69 unit tests plus 1 process test; CLI tests passed 5/5. The fresh namespace run is queued/running on compute01; no final-gate claim yet.J62 progress on #281. Working worktree:
proxy-traefik-281-final, based onorigin/dev9f36dd8e0; final push remains authorized only toproxy-traefik-281.Test-first review reproductions on compute01:
cargo test --locked --jobs 4 -p infraternal-plugin-proxy --lib: 68 passed, 6 failed for the expected redirect-port, mesh-address listener restart, offline imported bundle reuse, existing archive reuse, and abrupt parent-exit child ownership gaps. Log:/home/claude/.cache/codex-scratch/j62/proxy-review-red.log.cargo test --locked --jobs 4 -p infraternal-plugin-mesh --features agent proxy_listener_ports_are_limited_to_peers_with_tcp_ingress_grants --lib: 0 passed, 2 failed in nft and iptables as expected. Log:/home/claude/.cache/codex-scratch/j62/proxy-ingress-agent-red.log.The first firewall invocation omitted
--features agentand ran zero matching tests; it is not treated as evidence. Fixes are in progress for the five review boundaries. The installer now persists the original signed release metadata beside the digest-addressed executable so offline startup can reverify it without adding a signature format. Current VM scratch target:/home/claude/.cache/codex-scratch/j62/target. No final gate has run yet.J62 resumed on
proxy-traefik-281from pushed head1572c89d8in the current-dev worktree. The focused compute01 run completed: proxy unit tests reported 73 passed, 1 failed. The failure isdownload_tests::an_untrusted_manifest_never_triggers_an_archive_request: the offline installed-component lookup createdcomponents/before there was any trusted signed metadata. I changed the lookup to validate an existing tree without creating it; next I am syncing that one-file fix and rerunning the regression and focused suite throughbuild-slot.Command-table choice for this slice: use separate
infraternal unpublish <port|name>as the inverse ofpublish. It reports what stopped being reachable, triggers the supervisor's 10-second drain, supports--json, and a missing target returns one-line D303missing/ exit 4. This is the orchestrator's choice pending owner rename; the proxy module doc records it.Focused regression after the change:
download_tests::an_untrusted_manifest_never_triggers_an_archive_requestnow passes 1/1 on compute01 throughbuild-slot. The lookup returns without creatingcomponents/when the local store is absent; the untrusted manifest is rejected before archive fetch. I am rerunning the complete focused proxy/Mesh/agent-context/release set against this tree next.J62 progress on
proxy-traefik-281-final(baseorigin/dev9f36dd8e0, latest pushed checkpoint1572c89d8).Two test-first findings are fixed in the local WIP:
Some(old) → None → Some(new)reused the stale listener. RED:losing_mesh_identity_and_rejoining_on_a_new_address_restarts_the_proxy_listenerexpected10.254.0.9to start, observed only10.254.0.2; GREEN: 1 passed. On context loss the controller now drops the old supervisor and clears stale routes while retaining publication intent.{}. Both new initialization and existing-empty-file RED tests observed[]instead of[123,125]; GREEN: the controller test subset reports 10 passed. The initializer now creates or repairs an empty owner-only file as{}.Receipts on compute01:
/home/claude/.cache/codex-scratch/j62/mesh-transition-red-2.log/home/claude/.cache/codex-scratch/j62/mesh-transition-green.log/home/claude/.cache/codex-scratch/j62/acme-init-red.log/home/claude/.cache/codex-scratch/j62/acme-existing-red.log/home/claude/.cache/codex-scratch/j62/controller-green.logNext: run focused proxy/Mesh/agent/CLI/release checks, the namespace Pebble ACME test, then
scripts/verify fast; report D243 sizes, split/push the WIP fixes atomically, post final receipts, and remove the J62 target. The separateunpublish <port|name>choice, D303 missing/exit-4 behavior, JSON support, and 10-second drain are already documented in the module and prior #281 comment.J62 progress on
proxy-traefik-281-final, continuing from pushedproxy-traefik-281checkpoint1572c89d8(base9f36dd8e0). The local WIP now includes the review fixes and updates the Pebble namespace fixture to use the listener ports selected by the supervisor; local/VM hashes match for the fixture, runner, and controller.The earlier queued package-test PID is gone. Current build-slot check: compute01 has 3 free slots and 75 GB free; dev-02 has all 3 slots busy. I am resubmitting the proxy package test detached through compute01 build-slot, reusing
/home/claude/.cache/codex-scratch/j62/target(14 GB). No test or final-gate result is claimed yet. Next: collect focused package/Pebble receipts, runscripts/verify fast, report D243 size rows, push the finished changes, and post the exact gate output.J62 progress on
proxy-traefik-281(active worktree:proxy-traefik-281-final), based on pushed checkpoint1572c89d8/origin/dev9f36dd8e0.The resubmitted VM command
cargo test -p infraternal-plugin-proxycompleted on compute01: 77 library tests passed, the process redaction test passed, and the namespace Pebble test was ignored by its explicit fixture gate. Log:/home/claude/worktrees/J62-proxy-test-20261102.log.A new failing regression then reproduced the rootless redirect-port issue:
supervisor_tests::owned_supervisor_starts_on_first_exposure_drains_routes_and_keeps_acme_stateexits 101 because dynamic config contains redirect port443while the selected listener is10443. The supervisor detects ports after the controller has built the route and currently renders the stale fallback port. Log:/home/claude/worktrees/J62-proxy-redirect-red.log. I will update every runtime route to the selected audience-specific HTTPS port before rendering and preserve those values in the supervisor snapshot.J62 final-gate finding on
proxy-traefik-281ate9a9b8a84:scripts/verify faston dev-02 found that the workspace test and clippy builds both omitted the newly optional meshexposurefield inremote_supervisor_review_tests.rs. Exact compiler diagnostic:error[E0063]: missing field exposure in initializer of infraternal_plugin_mesh::dataplane::Configat line 286.The static-build row passed and is report-only under D243: x86_64 22,539,952 bytes (+1,096,128); aarch64 20,551,144 bytes (+921,904). Full log:
/home/claude/.cache/codex-scratch/j62-dev02/verify-fast-final.log.Added
exposure: Noneto the test fixture because this case does not publish a service. The correctly feature-gated regression passed 1/1 withcargo test --locked -p infraternal-agent --features remote,mesh --lib acknowledged_network_access_survives_superseded_worker_completion. Fix committed and pushed as3372c85aa037d15e334ff7b1772f76d1f017ce17; the full gate is being rerun on that head.J62 progress on
proxy-traefik-281. The second full gate at3372c85aaconfirmed two independent findings: Clippy rejected duplicate D303 mappings formissingandnot_found, anddefault_build_registers_no_unbuilt_commandrejectedpublisheven thoughplugin-proxyis enabled in the shipping default feature set. The workspace tests also confirmed the optional mesh exposure fixture fix.Both fixes are now pushed as separate commits:
b326417a9merges the D303 aliases and tests exit 4 for both;1fa66b199expectspublishandunpublishwhen proxy is compiled, while preserving the compiled-out case. Focused evidence on compute01:default_build_registers_no_unbuilt_commandpassed 1/1;missing_component_action_results_use_the_d303_not_found_exitpassed 1/1; package Clippy and fmt check passed. Log:/home/claude/.cache/codex-scratch/j62/final-touched-checks.log.D243 static rows passed as report-only: x86_64 22,539,952 bytes (+1,096,128); aarch64 20,551,144 bytes (+921,904). Second gate log:
/home/claude/.cache/codex-scratch/j62-dev02/verify-fast-final-fixed.log.Next: rerun
scripts/verify faston head1fa66b1995e8bb69f447f9a130a2c8c6e0635157through an available build slot.J62 resume check: the exact unmodified task base is
9f36dd8e0a. I created a detached VM worktree at that SHA and submitted the full agent_daemon integration target through compute01 build-slot (PID 2343516), reusing the existing J62 target. Log: /home/claude/.cache/codex-scratch/j62/agent-daemon-origin-base.log. Both VM queues were checked before submission; compute01 had one free slot and dev-02 was full. Next: collect the baseline result, then rerun the final head gate if appropriate.Baseline finding on unmodified J62 base
9f36dd8e0a: the fullagent_daemonintegration target passed all 11 tests, includingreal_binary_rootless_cleanup_survives_sender_disconnect_before_readiness(34.66 s; log/home/claude/.cache/codex-scratch/j62/agent-daemon-origin-base.log). This narrows the prior full-gate failure to an intermittent case or a J62 interaction. I am running the same full target on head1fa66b1995e8bb69f447f9a130a2c8c6e0635157; log/home/claude/.cache/codex-scratch/j62/agent-daemon-j62-full-target.log.J62 comparison result: the full
agent_daemonintegration target passes on both exact task base9f36dd8e0a5e9e799cd3264cb6e1e7605071c5e2(11/11, 34.66 s) and J62 head1fa66b1995e8bb69f447f9a130a2c8c6e0635157(11/11, 34.67 s). The oneresource_unavailablefrom the earlier full gate did not reproduce in either full target run. Logs:/home/claude/.cache/codex-scratch/j62/agent-daemon-origin-base.logand/home/claude/.cache/codex-scratch/j62/agent-daemon-j62-full-target.log. I will run a fresh fast gate on the J62 head and record its exact result.J62 fresh gate finding on head
1fa66b1995e8bb69f447f9a130a2c8c6e0635157: thescripts/verify fastrun on dev-02 completed its summary. Verbatim Rust gate table:The workspace test row reported
remote_service::tests::link_failure_does_not_wait_forever_for_blocking_workandremote_service::tests::polling_panic_reaches_bounded_shutdown; both hit their 5-second receive timeout.crates/infraternal/src/remote_service.rsis unchanged from the task base. I will compare those cases against the exact base and then run the required x86_64 Pebble namespace receipt when compute01 has disk headroom.D243 size rows passed report-only: x86_64 22,539,952 bytes (+1,096,128); aarch64 20,551,144 bytes (+921,904). Full log:
/home/claude/.cache/codex-scratch/j62-dev02/verify-fast-resumed.log.J62 follow-up evidence on head
1fa66b1995e8bb69f447f9a130a2c8c6e0635157:9f36dd8e0a5e9e799cd3264cb6e1e7605071c5e2,cargo test --locked --jobs 4 -p infraternal --bin infraternalpassed: 121 passed, 0 failed, 1 ignored;BASE_BINARY_TEST_EXIT=0. Log:/home/claude/.cache/codex-scratch/j62/remote-service-origin-base.log.J62_BINARY_TEST_EXIT=0. Log:/home/claude/.cache/codex-scratch/j62/remote-service-j62-x86.log.scripts/test-proxy-pebble.sh: 1 passed, 0 failed;PEBBLE_E2E_EXIT=0. Log:/home/claude/.cache/codex-scratch/j62/proxy-pebble-final.log.The two 5-second
remote_servicetimeouts from the full dev-02 gate did not reproduce in either x86 binary suite;crates/infraternal/src/remote_service.rsis unchanged on this branch. The dev-02 gate remains recorded as a failed test row, while static builds passed report-only. I will rerun the full gate on x86 when compute01 has enough free disk; it currently has 42 GB free, and J62 has no active VM process.J62 slice 1 is complete on pushed branch
proxy-traefik-281at1fa66b1995e8bb69f447f9a130a2c8c6e0635157.The proxy module records the orchestrator's CLI choice, pending any owner rename:
infraternal unpublish <port|name>is the separate inverse ofpublish. It accepts a local port or exact published name, reports what stopped being reachable, triggers the 10-second drain when the last publication is removed, supports--json, and returns one-line D303missing/ exit 4 for an absent target. Tests cover both selector forms and missing results.The module also records interim
DnsTokenCustodyfile custody under owner-only permissions, replaceable by node-key sealing or OpenBao without changing callers. Default per-machine wildcard keys remain on their node; flat exact-service certificate names appear in CT logs. The unsafe nftables redirect case is documented; no redirect is installed.Verification on compute01 at this head:
cargo test --locked --jobs 4 -p infraternal-plugin-proxy: 77 passed, 0 failed; process test 1 passed; the namespace test is separately run against Pebble.scripts/test-proxy-pebble.sh: namespace serving test 1 passed, 0 failed, staging ACME only.remote_clienttests fail under load. Both passed when run individually with theremotefeature; the full gate passed withRUST_TEST_THREADS=1.scripts/verify fastresult:VERIFY_FAST_EXIT=0. Log:/home/claude/.cache/codex-scratch/j62/verify-fast-threads1-at-1fa66b1.log.J99 independent review started. Target:
proxy-traefik-281at1fa66b1995e8bb69f447f9a130a2c8c6e0635157; comparison ref: currentorigin/devat16bf736c7cc6b474474d6b148077b516b0893cf1.Plan: inspect the acquisition, supervisor/config, Mesh exposure, DNS-token custody, certificate, and CLI paths against D316 and the owner answers in this issue/#289; run a focused test only if needed to confirm a concrete finding; measure and attribute static size against current
origin/devby crate; post the numbered verdict with file/line evidence. Estimate: about 60 minutes active review and measurement, excluding VM build-slot waits.J99 review finding 1 — P1 — Mesh grant revocations are acknowledged before Traefik applies them.
Evidence:
crates/plugins/proxy/src/controller.rs:675-690rebuilds routes and source allowlists from the newly accepted Mesh context.crates/plugins/proxy/src/supervisor.rs:784-802returns success afterwrite_dynamic_snapshot;:995-1000only renders and atomically replaces the watched file.crates/plugins/proxy/src/config.rs:206-208enables the asynchronous file provider, and there is no applied-config acknowledgment or fail-closed listener transition on this path.Impact: after a peer's service grant is revoked, its prior router and IP allowlist can remain active until Traefik reloads the file (or remain active if that reload is rejected). The caller has already received success, so the old grant can still reach the service after revocation. This misses the fail-closed behavior described in the #281 plan for authorization changes.
J99 review finding 2 — P2 — Rootless mixed Mesh routes use listener ports the Mesh firewall does not allow.
Evidence:
crates/plugins/proxy/src/config.rs:131-141selects 10080/10443 for rootless listeners. With public and Mesh routes together,detect_mixed_listener_portsskips that pair and selects 10081/10444 (:148-170); the supervisor installs those selected ports and writes them into routes (crates/plugins/proxy/src/supervisor.rs:906-924). Both Mesh firewall backends add only[80, 443, 10080, 10443]as proxy ingress ports (crates/plugins/mesh/src/firewall.rs:16,359-369;crates/plugins/mesh/src/iptables.rs:21,345-365).Impact: in the rootless mixed-public/Mesh case, published Mesh routes are advertised on 10081/10444 but Mesh ingress grants open only the fixed ports. Mesh peers therefore cannot reach those routes. The ingress rule needs to cover the selected listener pair.
J99 review finding 3 — P2 — Switching between Mesh-only, public-only, and mixed exposure scopes kills active proxy connections.
Evidence:
crates/plugins/proxy/src/supervisor.rs:784-801restarts the child wheneverlistener_scopechanges.restart_for_listener_scopestops the old child before starting the new one (:962-981), andTraefikChild::stopkills and reaps it (:460-465). A scope transition occurs when adding or removing the last publication for either audience (:701-716).Impact: publishing or unpublishing a public route while Mesh routes remain, or vice versa, terminates every connection through the proxy, including long-lived WebSockets. That conflicts with the #281 engine rationale that configuration changes must preserve existing connections. The dynamic route-update path preserves them; listener-scope transitions do not.
Round 1 verdict: REQUEST CHANGES
Target:
proxy-traefik-281at1fa66b1995e8bb69f447f9a130a2c8c6e0635157. The review comparison snapshot wasorigin/devat16bf736c7cc6b474474d6b148077b516b0893cf1.P1 — Mesh grant revocations are acknowledged before Traefik applies them.
controller.rs:675-690rebuilds the routes and source allowlists from the accepted Mesh context.supervisor.rs:784-802,995-1000returns success after rendering and atomically replacing the watched file;config.rs:206-208enables Traefik's asynchronous file-provider reload. There is no applied-config acknowledgement or fail-closed transition. A revoked peer can therefore continue using the prior router and allowlist until reload, or longer if reload is rejected.P2 — Rootless mixed Mesh routes select ports that the Mesh firewall does not allow.
config.rs:131-170selects 10081/10444 when public and Mesh routes coexist;supervisor.rs:906-924installs those ports in the routes. Both firewall backends permit only 80, 443, 10080, and 10443 (firewall.rs:16,359-369;iptables.rs:21,345-365). Mesh peers cannot reach the mixed-scope Mesh routes.P2 — Exposure-scope changes kill active proxy connections.
supervisor.rs:701-716,784-801restarts Traefik when the listener scope changes;supervisor.rs:962-981,460-465stops and kills the old child before starting the replacement. Publishing or unpublishing the last route for one audience while another remains can terminate active requests and WebSockets.Static size, x86_64 musl, default
infraternalbinary: canonicalscripts/static-cargo build --locked --release --package infraternal --target x86_64-unknown-linux-muslmeasured 22,413,328 bytes at the review baseline and 22,548,432 bytes at the target, a +135,104-byte delta.cargo bloat --release --target x86_64-unknown-linux-musl --package infraternal --bin infraternal --crates -n 0gave approximate LTO.textattribution (its output warns these values are estimates). Largest increases:infraternal_plugin_proxy+168.5 KiB;std+102.4 KiB;serde_json+33.1 KiB;miniz_oxide+21.8 KiB;tar+13.7 KiB;flate2+7.7 KiB;infraternal_plugin_host_facts+6.5 KiB;serde_core+5.7 KiB;infraternal_plugin_mesh+5.2 KiB;crc32fast+3.6 KiB;infraternal_plugin_canvas+3.2 KiB;infraternal_plugin_api+1.3 KiB. Largest reductions:infraternal_agent-115.5 KiB;infraternal_server-102.4 KiB;axum-20.5 KiB;tungstenite-18.6 KiB;tokio-12.5 KiB;infraternal-12.3 KiB;infraternal_plugin_ssh-6.7 KiB. The reported total.textsection rounds from 16.3 to 16.4 MiB; crate attribution is approximate and does not sum exactly under LTO.No additional actionable findings in the reviewed acquisition/extraction, proxy hardening, DNS-token custody, certificate scope, or D303/D314 CLI paths. No tests were run; the findings are supported by the code paths above, and the review brief limits test runs to confirming findings.
J62b started on branch
proxy-traefik-281-j62b, based onorigin/proxy-traefik-281at1fa66b1995e8bb69f447f9a130a2c8c6e0635157.Plan (estimated 120 minutes active work, excluding VM build-slot waits):
I will run the proxy integration tests and
scripts/verify faston whichever VM has the available build slot, push only this branch, and report the exact gate output here.J62b finding 1 evidence: the new test
mesh_grant_revocation_removes_the_route_before_narrowing_its_allowlistfails on base1fa66b1995e8bb69f447f9a130a2c8c6e0635157before production changes. The fake loopback provider confirms the supervisor never requests an applied-config generation after replacing a narrowed Mesh grant:J62b start, continuing the review fixes on branch
proxy-traefik-281-j62b, based on1fa66b1995e8bb69f447f9a130a2c8c6e0635157.Plan (estimate: 3 hours active work, excluding VM build-slot waits):
scripts/verify faston the final head, then push this branch and post the final evidence.J62b finding 2 RED/GREEN evidence (commit
0068a441c):config_tests::every_mixed_listener_port_is_allowed_by_the_mesh_firewall_backendsfailed before the fix on missing ingress port10081. After the fix,cargo test --jobs 3 -p infraternal-mesh -p infraternal-plugin-mesh -p infraternal-plugin-proxyon dev-02 passed; the proxy package reported 81 passed, 0 failed, and the new coverage test passed. Shared listener-pair/range definitions now drive config selection, nftables and iptables.J62b progress: P2 is committed as
0068a441c; the Mesh/proxy package suite passed on dev-02. P3’s test-only long-lived WebSocket scenario is in the local worktree and synced to compute01. The RED run usesscripts/test-proxy-pebble.sh, is waiting in compute01’s build-slot queue, and writes/home/claude/.cache/codex-scratch/j62b/pebble-red.log. The compute01 worktree target is/home/claude/worktrees/j62b/target. Next: confirm the WebSocket failure, implement stable entrypoints/dynamic-only updates, then rerun the integration green.Correction to my previous progress note:
scripts/test-proxy-pebble.shdefaultsCARGO_TARGET_DIRto the parent of its fixture root. For subsequent J62b runs I’ll set the fixture root to/home/claude/.cache/codex-scratch/j62b/pebbleandCARGO_TARGET_DIR=/home/claude/.cache/codex-scratch/j62b/target. The completed first diagnostic run used the script default; it is no longer running.J62b P1 integration finding: the first real-Traefik run timed out before any route was published. Its captured log shows the file provider rejecting the generation router rule:
unsupported function: Headers(run log:/home/claude/.cache/codex-scratch/j62b/pebble-diag.log, lines 107-108). I’m adding a regression assertion for Traefik’s exactHeadermatcher, then I’ll fix the renderer and fold it into the P1 commit before resuming P3.J62b P1 correction is folded into commit
65de8feca. RED:cargo test --locked --jobs 4 -p infraternal-plugin-proxy config_generation_probe_is_gated_on_the_loopback_health_entrypointfailed because the generated rule contained unsupportedHeaders(...). GREEN: the same test passed with the exactHeader(...)matcher (1 passed, 0 failed). The real-Traefik run then applied the initial generation and reached P3, where the existing restart closed route A’s WebSocket during route B publish.J62b finding 3 RED evidence: the real Pebble integration held a verified TLS WebSocket on route A while publishing route B. With the pre-fix listener-scope restart, route A closed during that publish:
Log:
/home/claude/.cache/codex-scratch/j62b/pebble-p3-red.log. The same scenario now exercises both publish and unpublish of route B while requiring route A frames to continue.J62b finding 3 GREEN on pushed head
b36bec2b9.cargo test --locked --jobs 4 -p infraternal-mesh -p infraternal-plugin-mesh -p infraternal-plugin-proxy: proxy package, 81 passed, 0 failed.scripts/test-proxy-pebble.sh:namespace_serves_a_granted_mesh_backend_with_pebble_dns01, 1 passed, 0 failed.The Pebble test keeps a real verified TLS WebSocket on route A and confirms it continues receiving frames across both publishing and unpublishing route B. The final
scripts/verify fastrun is next.J62b gate correction on the first final-head run: static builds passed, while fmt and clippy identified formatting plus one
unnested_or_patternslint in the stable listener config. The workspace test row exposed the P2 rule-count assertion still expecting 40 after the IPv4 and IPv6 proxy-ingress accept rules were added; the renderer produced 42.The P2 assertion is now 42 and is squashed into the P2 commit (
d76686d88); its focused test passes (1 passed, 0 failed). P3 formatting and the Clippy pattern are corrected in98892f633. The three-finding history remains one commit per finding; I am rerunning proxy integration and the fast gate on98892f633.J62b finished on branch
proxy-traefik-281-j62bat7e944b1caf3b9dd7bc4009178ea112182f982c4b.The three J99 findings are fixed in separate commits:
Proxy verification on compute01:
cargo test --locked --jobs 4 -p infraternal-mesh -p infraternal-plugin-mesh -p infraternal-plugin-proxypassed (proxy: 81 passed, 0 failed);scripts/test-proxy-pebble.shpassed (1 passed, 0 failed).Final
scripts/verify fastoutput:Static D243 result for this slice: x86_64 22,564,784 bytes (+16,352 vs task head
1fa66b199); aarch64 20,574,712 bytes (+17,088 vs task head1fa66b199). Full gate log:/home/claude/.cache/codex-scratch/j62b/verify-fast-final-head.log.Starting read-only review of proxy-traefik-281 at
7e944b1cafagainst merge base9f36dd8e0a. Reviewing #281 decisions and J99/J62b findings plus the full slice; no code changes.REQUEST CHANGES — read-only review of
proxy-traefik-281at7e944b1caf3b9dd7bc4009178ea112182f982c4bagainst merge base9f36dd8e0a5e9e799cd3264cb6e1e7605071c5e2.The three J99 findings are closed:
supervisor.rs:819-858, 1020-1050first applies a route-removal snapshot for narrowed Mesh source lists, then applies the narrower snapshot; each write gets a new UUID and waits for its own header-gated/api/rawdatarouter on loopback (config.rs:335-345,supervisor.rs:344-415). An old config cannot match the new UUID. Timeout stops the child and returns an error (supervisor.rs:831-841, 871-876); the Mesh observer propagates that error as a rejected action (node_actions.rs:475-480). The supervisor backs off, restarts, and renders/reapplies the retained desired services with a fresh generation (supervisor.rs:894-923, 950-1007). Full route removal also waits on the empty snapshot before success.infraternal-mesh/src/proxy_ports.rs:29-65is consumed by the proxy selector and both nftables/iptables renderers;config_tests.rschecks produced listener ports against the shared ingress set.config.rs:140-143,supervisor.rs:843-858). J62b’s real Pebble test holds route A’s TLS WebSocket across publishing and unpublishing route B; its passing result is recorded below.New findings:
P2 — The firewall exception opens every candidate proxy port to any peer with any TCP grant.
proxy_ports.rs:44-65defines broad ranges for all 256 candidate listener pairs, while the selector chooses only one available pair per process (config.rs:147-174). The nftables renderer includes a peer inincoming*_proxy_sourcesif it has any inbound TCP port (firewall.rs:392-415), then accepts that source to every port in the full ranges (firewall.rs:438-439); iptables does the same (iptables.rs:425-443). Scenario: a peer has a narrow grant for TCP/3000 and another local service listens on the Mesh address at TCP/10082. If Traefik selected a different free pair, the peer can still reach 10082 through the proxy exception, despite having no grant for 10082. This is an access-policy bypass introduced by allowing the whole candidate range.P2 — Traefik inherits the rootful infraternal agent’s full privileges. Rootful mode is an approved host mode (
DECISIONS.md:73-78)); with effective root and free conventional ports, the proxy selects 80/443 (supervisor.rs:955-960,config.rs:127-138). The child launch atsupervisor.rs:481-500` clears the environment and sets lifecycle controls, but does not drop uid/gid or capabilities or apply a child sandbox. Scenario: a rootful node publishes a Mesh service, and a remotely reachable request exploits Traefik; the process has host-root privileges. Binding 80/443 does not require retaining all root privileges while processing network traffic.P2 — Mesh-only publication still binds public entrypoints on wildcard addresses.
config.rs:240-260renders the public HTTP/TLS listeners on unspecified addresses on every start; a Mesh-only service is attached only tomesh-http/mesh-https(config.rs:391-395). Scenario: the first publication is Mesh-only, but a host with ordinary inbound access still accepts unauthenticated TCP/TLS on wildcard 80/443 in rootful mode (or 10080/10443 rootless); those requests reach Traefik’s parser and return no-route responses without--public. This exposes the proxy’s network parser outside the Mesh even when no public route exists.Artifact acquisition and release verification match the owner’s two-check answer: CI verifies the pinned upstream checksum list and signs our manifest; runtime verifies our release signature, signed tuple, archive and executable hashes, bounds, and allowlisted extraction. Token custody is owner-only, the child receives only
*_FILEpaths afterenv_clear, and token values are not put in argv. The generation API route is on the loopback-only health entrypoint; dashboard and insecure API are disabled. Per-machine wildcard keys and ACME state stay node-local; flat names request exact certificates. I did not rerun tests or builds on the read-only review host.J62b’s exact-head verification from #281, quoted verbatim:
No source changes made.
Correction and one additional low-priority finding for my review above:
DECISIONS.md:73-78.custody.rs:140-159), and child startup retains those bytes in the redaction value (supervisor.rs:454-475). The log reader removes trailing CR/LF before exact-value replacement (supervisor.rs:577-580,177-187). Scenario: a token file ends in a newline and a provider error/debug line emits the normalized raw token; after line-ending removal, the exact secret string no longer matches and the value can reach the log sink. This is conditional on a provider emitting the raw value, but the sanitizer does not uphold the stated invariant for that accepted file shape.The verdict remains REQUEST CHANGES; the three P2 findings and evidence are in the review comment above.
J62b round 3 started on branch
proxy-traefik-281-j62b, currently at7e944b1caf3b9dd7bc4009178ea112182f982c4b; rebasing ontoorigin/devbaseb5099e3be1721d01f12f8e7782723e9f23ab2350.Plan (about 4 hours active work, excluding VM build-slot waits):
J62b round 3 finding 1 evidence: both firewall backends currently admit the whole candidate interval to any peer with any inbound TCP grant. The corrected RED run on compute01 failed both new assertions: nft still rendered
10080-10335and iptables still rendered--ctorigdstport 10080:10335for a peer whose only grant is TCP/3000 and with no published proxy route. Log:/home/claude/.cache/codex-scratch/j62b/p2-red.log.I’m changing the exception to use the selected Mesh listener pair and only backend ports of currently published Mesh routes; each backend will derive sources from the accepted grant for those route ports. The regression also checks the granted route remains reachable through that pair.
Finding 1 is committed as
34cd2e69cand pushed. Mesh ingress now uses the selected listener pair and only peers whose accepted TCP grant matches an applied non-public backend route; the controller updates it after the Traefik generation is confirmed. Evidence: infraternal-plugin-mesh --lib: 45 passed, 0 failed, 13 ignored; infraternal-plugin-proxy --all-targets: 82 passed, 0 failed, 1 process integration passed, with namespace_pebble ignored because its pinned fixture is run separately.J62b round 3 resumed on
proxy-traefik-281-j62b. I fetched origin and foundorigin/devat816f9ab3e8dc708d88a5f774d605cced22d82d22; the current branch is based onb5099e3be1721d01f12f8e7782723e9f23ab2350, so I am rebasing before continuing.Plan (about 4 hours active work, excluding build-slot waits):
J62b round 3 finding 2 (P2) is committed and pushed as
5ef6390db.RED on the pre-fix source, from
/home/claude/.cache/codex-scratch/j62b/p2-rootless-red.log:GREEN on compute01,
cargo test --locked --jobs 4 -p infraternal-plugin-proxy --test process traefik_child_obeys_the_rootful_identity_and_capability_boundary -- --exact --nocapture:Rootful launch now drops to UID/GID 65532, keeps only CAP_NET_BIND_SERVICE in the effective, permitted, inheritable, ambient and bounding sets, and enables no_new_privs. It reuses the shim's existing signal/fd3 isolation and parent-bound child hooks. The executable and config remain inaccessible through private state ancestors; DNS token custody files are owned by the child at mode 0600. Safe prior root-owned executable/config/custody files are migrated during verified installation, config replacement, or custody open.
The integration test reads
/proc/self/statusfrom the running child and asserts the rootful UID/GID and capability sets when the runner is root. This compute01 run was unprivileged, so it exercised the rootless process assertions;sudo -nis unavailable there, and its user namespace lacksnewuidmap.J62b round 3 finding 3 RED on
5ef6390db, dev-02:The renderer produced wildcard public listeners for rootless selected ports. The controller's first firewall observer call happened after the route snapshot was already recorded. I will keep rootful public listeners stable and gate their selected pair in the owned Mesh firewall: close before starting the first listener, open after a public route is applied, and close before removing the last public route. Rootless mode will bind those entrypoints to loopback and reject
--public, since it cannot manage the host firewall or provide remote public reachability while keeping listener config stable.J62b round 3 finding 3 is committed as
ab7b79b3c.Rootful public entrypoints keep one stable selected port pair. The owned nftables and iptables backends block non-loopback traffic to that pair before Traefik starts and before the last public route is removed; they open the gate only after the public route's dynamic snapshot is applied. Mesh routes continue using dynamic config, so this does not restart Traefik. Rootless mode binds those entrypoints to
127.0.0.1and rejects--public: it can keep the parser off remote interfaces, but cannot control the host firewall or offer remote public reachability.GREEN on dev-02,
cargo test --locked --jobs 3 -p infraternal-mesh -p infraternal-plugin-mesh -p infraternal-plugin-proxy --all-targets: exit 0. The proxy package ran 85 tests with 0 failures;namespace_pebbleremains ignored there and is run separately throughscripts/test-proxy-pebble.sh.J62b round 3 continued. The branch is rebased onto current origin/dev
bce4164636; I preserved the newer Linux remote-install cfg and bytes dependency while carrying the proxy registry wiring. The J116 fixes and three of four J62b round-three findings are already recorded above. Remaining: add newline and CRLF DNS-token redaction regression cases, observe RED, implement raw-plus-trimmed replacement, then run Pebble integration and scripts/verify fast, push and record the exact gate receipt. Estimate: 45 minutes of active work, excluding build-slot waits.J62b round 3 finding 4 RED on the pre-fix source, dev-02, through build-slot:
cargo test --locked --jobs 3 -p infraternal-plugin-proxy --lib newline_terminated_provider_values_are_redacted_after_log_normalization -- --nocapturefailed as expected. Output:running 1 test;supervisor_tests::newline_terminated_provider_values_are_redacted_after_log_normalization ... FAILED; assertion!redacted.contains("secret-value-123")failed;test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 85 filtered out; exit 101. The test passes a line-normalized log entry and a raw newline-terminated custody value. I am changing the replacement set to include both the raw value and its CR/LF-trimmed form, then will rerun this test.J62b round 3 finding 4 is fixed in
1d5a8d2b3. RED was recorded above. GREEN on compute01 against the committed source,cargo test --locked --jobs 4 -p infraternal-plugin-proxy --lib newline_terminated_provider_values_are_redacted_after_log_normalization -- --nocapture:The sanitizer now matches both stored bytes and the value after trailing CR/LF removal. Next: run the mesh/proxy package integration tests, the Pebble WebSocket fixture, and
scripts/verify faston the final head.Additional finding 2 regression found by the required Pebble integration. Inside its
unshare --user --map-root-userfixture,/proc/self/uid_mapandgid_mapeach contain only0 1500 1; UID/GID 65532 are unavailable. The currentidentity::rootful()checks only effective UID 0, so Traefik takes the host-root path and rejects the private fixture config directory before startup. RED on compute01:identity::tests::user_namespace_root_without_traefik_id_mappings_is_rootlessfailed atassert!(!super::rootful())(0 passed, 1 failed, exit 101). I will require both fixed child IDs to be mapped before Linux selects rootful mode, and fold this correction into the existing rootful finding 2 commit.Finding 2 user-namespace boundary is corrected in the working tree. GREEN on compute01:
cargo test --locked --jobs 4 -p infraternal-plugin-proxy --lib user_namespace_root_without_traefik_id_mappings_is_rootless -- --nocapture:Linux rootful mode now requires euid 0 plus mappings for the fixed Traefik UID and GID. A one-ID user namespace therefore stays in rootless mode and keeps its private 0700 config directory. I am rerunning the Pebble integration now; this remains part of finding 2 and will be folded into its commit.
The Pebble regression that exposed finding 2 now passes on compute01 after the user-namespace root check:
CARGO_TARGET_DIR=/home/claude/worktrees/j62b-r3-codex/target scripts/test-proxy-pebble.sh:The test now runs in the intended rootless user namespace instead of taking the host-root file ownership path. The identity boundary and its RED/GREEN test will be folded into the existing finding 2 commit before final verification.
J62b round 3 rebased onto latest at ; candidate head is pushed. The four round-three fixes remain separate commits with RED/GREEN evidence above. Final proxy integration and are next on compute01; dev-02 currently has only 9 GB free, below the 25 GB build floor.
J62b round 3 has been rebased onto latest origin/dev at
299b7ad30b. Candidate head9d4040b005is pushed on proxy-traefik-281-j62b. The four finding fixes remain separate commits with RED/GREEN evidence above. Final proxy integration and scripts/verify fast are next on compute01; dev-02 currently has 9 GB free, below the 25 GB build floor.Queue update for J62b round 3: my compute01 build-slot job remained queued for 27 minutes and never entered a slot; I terminated it cleanly. Dev-02 now has 77 GB free and a shorter queue, so its worktree is prepared at
9d4040b005for package tests and scripts/verify fast. The pinned Pebble integration is x86_64-only and will run on compute01 separately.The first post-rebase fast gate on
9d4040b005found fmt and Clippy failures in the rootful child changes plus an inverted conditional lint in the public listener gate. I corrected those and folded the formatting/lint changes into the existing rootful and listener-gate commits; the history remains one commit per finding. The static-build row passed. The package suite passed (proxy: 87 passed; process: 2 passed), but the workspace vt_credit integration failed once withcredit probe failed: t1-gate-approximate-1after 25.45s while other build jobs were active. I am rerunning that test and the full gate on the updated pushed head before reporting completion. Gate log: /home/claude/.cache/codex-scratch/j62b/verify-fast-final-head.log.J62b round 3 continuation: I fetched and rebased onto latest origin/dev,
7add126405(parenta6812c3cf5), with no conflicts. The latest fast gate completed with VERIFY_EXIT=1: fmt, VT native fmt, compile-out, package tests, static builds and the remaining rows passed; Clippy failed because registry_at is 103 lines after the new Mesh observer wiring in finding 1. I’ll extract that observer into a focused helper and fold the lint fix into finding 1’s commit, then rerun the focused Mesh/proxy tests, Pebble integration, and full gate. Current VM queues are full; estimate 45 minutes of active work, excluding queue time.J62b-c takeover of J62b round 3 (the previous job stopped on a provider rate limit at about 12:30 IST).
State found:
proxy-traefik-281-j62b, pushed head5b76657c4, based on7add12640. All four J116 round 3 fixes are committed separately, with RED/GREEN evidence above:da4aa7320(scoped proxy ingress),5bca1bed9(rootful privilege drop),857cb3856(public listener gate),5b76657c4(newline token redaction).~/Developer/Worktrees/infraternal-J62b,compute01:~/worktrees/j62b-r3-codexordev-02:~/worktrees/j62b-r3-codex.~/.cache/codex-scratch/j62b/verify-fast-r3-final.log): clippy PASS, static builds PASS, test FAIL (101) on one test outside the slice:infraternal-serverauth::tests::socket_tests::transient_authority_stall::terminal_stream_survives_authority_stalls_without_loss_or_reorder(748 passed, 1 failed). The earlier 06:00 run failed clippytoo_many_linesonregistry_at(crates/infraternal-agent/src/plugins.rs, 103 lines); the J62b note about extracting it is still open.Remaining plan (about 40 min active work, plus queue time):
7c082bddf(clean, no conflicts).registry_atand fold it into the finding 1 commit (da4aa7320).transient_authority_stallfailure also happens on unmodified origin/dev, and rerun it on the branch.scripts/verify faston the final head; push; record the receipts here.J62b-c progress: the branch is rebased onto origin/dev
7c082bddfand pushed asb67e7a879; the force-push was leased on5b76657c4.The
registry_atclippytoo_many_linesfix is folded into the finding 1 commit (nowa7bfa336c, "scope proxy ingress to applied routes"). Proxy plugin construction moves toproxy_plugin_at, and the two-way Mesh/proxy observer wiring moves toconnect_proxy_and_meshincrates/infraternal-agent/src/plugins.rs. There is no behaviour change: both hooks are plain setters. The tree outside that file is identical to the pre-rebase head.Running now:
scripts/verify faston dev-02 atb67e7a879; log~/.cache/codex-scratch/j62b/verify-fast-c.log.scripts/test-proxy-pebble.shon compute01 atb67e7a879; log~/.cache/codex-scratch/j62b/pebble-c.log.Target dirs:
~/worktrees/j62b-r3-codex/targeton both VMs.J62b-c finish on branch
proxy-traefik-281-j62b, headb67e7a87916af4a2aed0bad0d4842d6b3f165598, based on origin/dev7c082bddfa36a7ea464e406522dbe67feda1630f.scripts/verify faston dev-02 atb67e7a879(nix develop -c, through build-slot), verbatim (logdev-02:~/.cache/codex-scratch/j62b/verify-fast-c.log):The only failing row is
test, and it has one failure outside the slice:-p infraternal --test session_exit_events(idle_daemon_discovers_unattached_and_adopted_shim_exit_without_requests). The firstclient.open()on the real agent socket hit the fixture's 1 s receive timeout (TimeoutError: timed out,agent_fixture.py:129). The empty-agent idle window just before it passed (seconds=2.210). On the previous head, the previous run failed a different timing test ininfraternal-server(transient_authority_stall) and this one passed.Evidence that it is load flakiness, not this branch:
b67e7a879, load about 7: 3 passed, 2 failed.scripts/verify-session-exit-events.py --samples 8(unchanged by this branch): 12/12 passed, 6 per binary. Idle windows were identical and exit-census latency was the same. The second column is the 1-minute load average.Static sizes (report only, D243 amended): x86_64 23028624 bytes, delta +610368 bytes against the gate's baseline; aarch64 20969840 bytes, delta +516560 bytes.
Proxy integration on compute01 at
b67e7a879:CARGO_TARGET_DIR=/home/claude/worktrees/j62b-r3-codex/target scripts/test-proxy-pebble.sh:J116 round 3 findings, one commit each, with RED/GREEN evidence in the comments above:
a7bfa336cfix(mesh): scope proxy ingress to applied routes. The nft and iptables renderer tests failed on10080-10335/--ctorigdstport 10080:10335for a peer granted only TCP/3000. Theregistry_atclippy extraction (connect_proxy_and_mesh,proxy_plugin_at) is folded into this commit.03892bcadfix(proxy): drop rootful Traefik privileges. Tests:process::traefik_child_obeys_the_rootful_identity_and_capability_boundaryandidentity::tests::user_namespace_root_without_traefik_id_mappings_is_rootless. The rootful identity assertions run only when the test runner is root; the compute01 run was unprivileged.b57814b02fix(proxy): gate wildcard listeners until public routes apply. Tests:config_tests::rootless_public_entrypoints_bind_to_loopbackandcontroller::tests::public_listener_gate_is_installed_before_the_first_route_snapshot.b67e7a879fix(proxy): redact newline-terminated DNS tokens. Test:supervisor_tests::newline_terminated_provider_values_are_redacted_after_log_normalization.Not done: a fully green gate receipt. Both rerun options are on the orchestrator: rerun
verify fastatb67e7a879on a quiet slot, or accept the A/B above. The behaviour changes in rounds 2–3 that need the independent review: rootless mode now binds the public entrypoints to127.0.0.1and rejects--public; rootful mode drops Traefik to UID/GID 65532 with onlyCAP_NET_BIND_SERVICE. No merge, no deploy.J133 independent security review started (read-only). Target:
proxy-traefik-281-j62batb67e7a87916af4a2aed0bad0d4842d6b3f165598, diffed againstorigin/dev. Plan (~2 h): verify J116's four round-3 fixes against their repros, review the whole slice (firewall, privilege drop, binds, revocation timing, secrets, config injection, traversal, supervision), and run the root-gated process tests as root in a disposable worktree under/rooton compute01 (removed afterwards). No code changes.J133 independent security review: REQUEST CHANGES
Target:
proxy-traefik-281-j62batb67e7a87916af4a2aed0bad0d4842d6b3f165598, diffed againstorigin/dev. This was a read-only review; no code was pushed. The root runs used a disposable clone in/root/j133on compute01. That clone, its target dir, the netns namespaces and the temp files are all removed. Logs are kept on code-minipc01 at~/Developer/Worktrees/j133-evidence/{run,run2}.log.J116 round-3 fixes, checked against their repros
.2, granted TCP/3000 only, is BLOCKED to the selected pair 10081/10444 and to an unrelated Mesh service on 10082. No candidate ranges are rendered any more.traefik_child_obeys_the_rootful_identity_and_capability_boundary ... ok: UID/GID 65532, NoNewPrivs=1, only CAP_NET_BIND_SERVICE.:80/:443BLOCKED while the gate is closed and CONNECTED once it is open; a Mesh peer without a 443 grant stays BLOCKED. Rootless binds loopback (code).supervisor.rs:193Root test run (compute01,
cargo test -p infraternal-plugin-proxy, euid 0)The round-3 GREEN receipts were all unprivileged runs. As root, 6 of the slice's tests fail.
Findings
P1-A: Mesh routes are unreachable for peers with port grants. The firewall drops the handshake after the first SYN.
crates/plugins/mesh/src/firewall.rs:475,484,569-603;crates/plugins/mesh/src/iptables.rs:431,574-610.The proxy exception (
@incoming*_proxy_sources . @proxy_ingress_ports) is only evaluated forct state new. Established and reply packets jump toincoming_stateandoutgoing_statein nft, or hit the ESTABLISHED checks in iptables. Those only accept@incoming*_allor the exact granted(peer, port)tuples, and then drop. Only peers withTrafficPolicy::Allcan use a proxy route.Repro: netns, branch-rendered rules, peer
.3granted TCP/8080, route backend 8080, selected pair 10081/10444:Neither the unit tests nor the Pebble test (which has no Mesh firewall) cover this path.
A second effect:
connection_state::retire(connection_state.rs:34,104) judges every flow bypermits(peer.inbound, dport). A listener-pair port is never a grant, so after the state chains are fixed, any grant narrowing anywhere would delete every proxied flow from every port-granted peer. That would break the WebSockets that D-engine choice 72714 must keep.Fix:
(proxy source, selected pair)in both backends.retiretreat a flow to the selected pair as permitted while its source is still in the proxy source set.P1-B: Rootful mode cannot start Traefik.
crates/plugins/proxy/src/controller.rs:723-776andcrates/plugins/proxy/src/supervisor.rs:1350-1357.ensure_proxy_directorycreatesproxy/with mode 0750 ownedroot:root, then rejects it because the group is not 65532. Even with a correct group,acquire_supervisor_lockrequires its parent (that sameproxy/dir) to be 0700. Root probe from a disposable test:Every rootful
proxy publishfails, and the privilege drop has never run end to end.Fix:
fchownit toroot:65532and chmod it to 0750 through the fd.P2-A: The rootful Traefik loses PR_SET_PDEATHSIG, so an agent crash leaves an orphan Traefik.
crates/infraternal-shim/src/pty.rs:932sets the parent-death signal in an earlierpre_exechook. Thendrop_proxy_rootcallssetuid(pty.rs:1041), and the kernel'scommit_credsclearspdeath_signalwhen euid changes. Root probe, run as a fake Traefik child:Scenario: the agent is OOM-killed or SIGKILLed. Traefik (65532) keeps the wildcard 80/443 and the Mesh listener, along with its last routes and allowlists. The restarted agent's Mesh firewall starts with default proxy ingress, so the public gate is gone. The new Traefik cannot bind its old pair, and nothing owns the orphan.
abrupt_infraternal_parent_exit_stops_the_traefik_child(supervisor_tests.rs:130) does not exercise the rootful path.Fix:
getppid()after it.P2-B: The fixed shared identity 65532 has no collision check.
crates/plugins/proxy/src/identity.rs:4-6. 65532 is the conventionalnonrootUID in common container images. Any other process on the host running as 65532, such as a rootful container without userns, can:execvebecause no privilege is gained;/proc/<pid>/fd/*and environ;A compromised Traefik also gains every host file owned by 65532.
Fix: allocate a dedicated system user and group at install, or a recorded unused ID, and refuse to start if other processes or files already use that ID.
P2-C: A per-route revocation does not end live upgraded connections.
controller.rs:440says "existing connections may drain for up to 10 seconds". In fact, Traefik keeps hijacked WebSockets across dynamic reloads. Once P1-A is fixed, here is the case. Peer X keeps a grant for route B but loses route A. Its open WebSocket or terminal on A then stays open with no time limit, because the firewall still admits X to the shared pair andretiresees no reach change for the pair.Fix: when a source is removed from a Mesh route, delete conntrack flows from that source to the selected pair. This also drops X's other proxied flows, which is acceptable for a revocation. Also correct the message.
P3-A: Redaction misses other whitespace.
supervisor.rs:193only strips CR/LF. The DNS client library Traefik uses trims all leading and trailing whitespace from*_FILEvalues, so a file like"tok \n"or"\ttok"still leakstok. Fix: also addsecret.trim()(Unicode whitespace) to the replacement set.P3-B: The public gate outlives the listener and has a broad scope.
controller.rs:525keepspublic_listener_pairfor as long as the controller holds a supervisor. That includes after the drain stops Traefik, so any later service on 80/443 is silently dropped on every non-lointerface. The nft rule (firewall.rsinput chain,tcp dport {80,443} drop) also covers IPv6 services that bind specific addresses. Fix: clear the gate when the child stops, and scope it to the addresses Traefik actually binds.P3-C: The health port is chosen by a TOCTOU bind-and-release.
controller.rs:690. A local user who squats the port before Traefik binds it can answer/pingand/api/rawdata, which forges apply acks. Traefik then exits on the bind failure, so the impact is DoS rather than stale routes. Fix: verify that the listening socket belongs to the child (match the socket inode against its fds) before trusting acks.P3-D: A test fails as root.
tests/process.rs:168(traefik_child_receives_only_credential_file_paths_and_redacts_its_logs) fails as root (log above). The capture path is not writable or reachable for 65532.Clean areas
[a-z0-9-], dynamic config is serde JSON, and static config uses JSON-escaped TOML strings./proc/self/fdpaths are passed.fchown,O_NOFOLLOWandnlinkchecks. A compromised child that holds the provider dir fd can only cause DoS, not escalate.For D330 (rootless
--publicallowed, wildcard 10080/10443 or the next free pair)Things that would make the next round unsafe, for that job to design against:
controller.rs:525only creates the public gate whenstate.rootful. If rootless binds the wildcard pair for the whole process lifetime, J116#3 comes back: Traefik's parser is exposed to the network while only Mesh routes exist. The wildcard must accept only while a public route is applied, and turning it on or off must not restart the Mesh listeners. One way is a separate public-only Traefik process, started after its first snapshot is acked on loopback and stopped after the last public route.http/https. Todayrender_published_serviceputs non-public routes only onmesh-http/mesh-https, including the redirect router. Keep a test that asserts this, because the rootless public entrypoint would now be reachable from the network.config.rs:133returns 10080/10443 for rootless without probing. "Next free pair" has to probe the wildcard on both v4 and v6 (Go listens dual-stack on0.0.0.0) and must exclude the Mesh pair.J62c (round 4 of slice 1) started on branch
proxy-traefik-281-j62b(worktree branchj62con code, pushed back toproxy-traefik-281-j62b).b67e7a879. Rebased cleanly onto origin/dev00ec71000(37 branch commits replayed, no conflicts).--public).A plan with time estimates follows once I've read the controller and supervisor.
J62c plan (one commit per item, each with a test that fails first; RED and GREEN posted here):
(proxy source, selected pair)in the*_statechains.connection_state::retiregets the old and newMeshProxyIngress: a flow to the selected pair stays permitted while its source is still a proxy source. Tests: rendered-rule unit tests for nft and iptables, aretiredecision unit test, and an ignored netns test (real Mesh firewall, port-granted peer, data after the SYN) run as root.fchownroot:child-group and chmod 0750 through the fd. The supervisor lock moves into the agent-only 0700 state dir (alock_directoryfield inSupervisorSettings). The 5 supervisor tests pass as root.getppid(). Every child inherits a child-lock fd. On start, while the agent holds the supervisor lock, a held child lock means an orphan from a dead agent: its holders are found by/proc/*/fdinode match, killed and reaped before Traefik starts. Root test: the child reports a nonzero PDEATHSIG after the drop, and a planted orphan holding the child lock is reaped.infraternal-proxy, resolved at runtime and created withuseradd --system(busyboxadduser -Sfallback) on first rootful start. Start is refused if the UID is 0, if another passwd/group entry shares the ID, or if any process outside our own child runs with it.rootful()requires the namespace to map the whole 0–65535 range. A full file-system scan for files owned by the ID is not done (cost); I note this in the commit.secret.trim(); clear the public gate when Traefik stops, and scope the nft/iptables drop to the IPv4 and IPv6 addresses Traefik binds; health-port ack is trusted only when the listening socket's inode is among the child's fds;tests/process.rscapture works as root.--publicruns a separate public-only Traefik process (own static/dynamic config, ACME file, health port, lock). It starts with its first public snapshot already written and acked, and is stopped (listener closed, child reaped) before the last public route's removal is reported. The rootless Mesh process gets no public entrypoints, so it never restarts for public changes. The public pair is probed on 0.0.0.0 and [::] from 10080/10443 and skips the Mesh pair. The publish result warns with the port, and says the Mesh firewall does not filter it: only this machine's firewall or the router limits who can reach it. Tests cover the rendered configs (Mesh routes never onhttp/https), the controller ordering, and the probe.scripts/verify fast.Total ~12 h. A known residual for D330: Traefik binds its entrypoints a few milliseconds before its file provider loads the first snapshot, so a connection in that window gets a 404 from the default handler. No route other than the intended public one is ever served. I'll document it rather than build a socket handoff.
J62c progress. All fixes and D330 are committed and pushed on
proxy-traefik-281-j62b, head9073b90ca, rebased on origin/dev00ec71000. Each commit is fmt-clean on its own.84040190aretirekeeps them4f792ba4da34aa40d62e39ea816infraternal-proxysystem account; collision checks on passwd, group and running processes3433cf9daac9dcf497secret.trim()a2ff0e583e7346e2998bbdfcbcd--publicthrough a separate public-only processf97d2d9f09073b90caNew finding (P1), found while running the P1-A RED against the unfixed branch. On the nft backend,
Firewall::applyfails with PermissionDenied on every node with no Mesh proxy route, so the Mesh table never installs. The renderer emittedset proxy_ingress_ports { type inet_service; elements = { }; }, which nft rejects:A unit test pinned the broken string. Fixed in
84040190a, which is now the first commit of this round.P3-D root cause. Under
nix develop, root'sTMPDIRis private, and the dropped child can't traverse it to write its capture file. The other boundary tests reach their files only through inherited descriptors, so they passed.P1-A unit RED is recorded (compute01, unfixed source plus the new tests):
Queued now:
Logs:
~/j62c-logs/on both VMs,/root/j62c-root.logon compute01. One target dir per VM:~/.cache/codex-scratch/j62c/target.J62c: red-to-green evidence per finding
Branch
proxy-traefik-281-j62b, head36384e212. Each RED ran the fix's own new tests on the commit before that fix (as an overlay). Each GREEN ran at head. Some REDs ran before a later rebase and name the SHAs current then. Since then the commits changed only by formatting, clippy and the test fixups noted below. Hosts: compute01 (x86; namespace runs underunshare -Urn, root runs as real root in a throwaway clone under/root), and dev-02 (arm64, unprivileged).New P1: nft Mesh table could not install with no Mesh route (
84040190a)RED (compute01, P1-A namespace test on
b2965d89e):Root cause:
nftrejectselements = { }(repro in the comment above). GREEN: with the fix alone (84040190a), the same test gets pastapplyand fails only on P1-A's own assertion (next section).P1-A: whole proxied flows for port-granted peers (
fa894071f)RED (compute01, tests on
84040190a):GREEN (compute01
unshare -Urn, and again as real root underunshare -n). The test covers several round trips on one flow from a peer granted only TCP/8080, a peer with no route grant refused, and the flow surviving an unrelated narrowing:P1-B: rootful proxy state (
e13b6267a)RED (root, compute01, proxy code before the fix):
GREEN (root, head
36384e212): all six... ok, and the proxy libtest result: ok. 102 passed; 0 failed.P2-A: parent-death signal after the drop, orphan reaping (
da2f7ed3d)RED. The probe reads the signal on the exec'd main thread of the child:
GREEN (root and unprivileged, head):
P2-B: dedicated account (
ce9c401b2)RED (root, on
da2f7ed3d):the_rootful_child_identity_is_not_the_shared_conventional_id left: 65532 right: 65532.GREEN (root, head):
the_rootful_child_runs_as_the_dedicated_account ... ok. The run createdinfraternal-proxy:x:999:987::/nonexistent:/usr/sbin/nologinthroughuseradd --system. The cleanup removed it (account after: none). Two issues were found and fixed during the root runs:useraddfailed. Creation is now serialized, and the account is looked up again after a failed tool run.P2-C: revoked route ends its source's flows (
73cc570b5)RED (compute01, namespace test on the commit before):
GREEN (compute01 userns, and as real root): both
... ok. The test covers several cases:P3-A: trimmed token (
c9ebab8ea)RED (dev-02):
provider_values_are_redacted_as_the_dns_client_trims_them ... FAILED. GREEN:... ok.P3-B: public gate tied to the child (
a89756dce)No RED can run on the old code: the gate moved from the controller to the supervisor, so the test drives an API that didn't exist. Before, the gate was set when the controller created a supervisor and stayed until the controller dropped it, the case J133 traced. GREEN (root and unprivileged):
the_public_gate_is_closed_before_a_child_starts_and_cleared_after_it_stops ... ok,the_public_gate_is_removed_once_the_proxy_child_is_gone ... ok.P3-C: health acks only from the child's socket (
ea00e7ec9)RED (dev-02, on the commit before):
a_squatter_on_the_health_port_cannot_acknowledge_routes ... FAILED. A separate process answering 200 confirmed the route. GREEN (root and unprivileged):... ok.P3-D:
process.rs:168as root (36384e212)RED (root, before):
traefik_child_receives_only_credential_file_paths_and_redacts_its_logs ... FAILED read child invocation: Os { code: 2, kind: NotFound }.GREEN (root, head):
test result: ok. 2 passed; 0 failedfortests/process.rs, including the rootful identity and capability boundary.D330 (
842908ace)GREEN (unprivileged and root):
These cover:
http/httpsentrypoints or wildcard bind; the public process has no Mesh entrypoints.mesh-*.[::]is skipped, and so is the Mesh pair.warningnaming the port: "Public routes listen on ports 10080 and 10443 on every address of this machine. The Mesh firewall does not filter these ports; only this machine's firewall or your router limits who can reach them, and either may still block them."Residual (documented in the commit): Traefik binds its entrypoints a few milliseconds before its file provider loads the first snapshot. A connection in that window gets Traefik's default 404; no other route is served.
J62c progress. Head is
b5dcdd3a9; the per-finding evidence is in my comment above. Since then I've made clippy and test-fixture fixups only, squashed into their commits, plus two P2-B fixes found by the root runs:Root GREEN at
36384e212(compute01, real root): proxy lib102 passed; 0 failed,tests/process.rs2 passed, shim lib102 passed. The account was created asinfraternal-proxy:x:999:987, then removed with the clone. The only failure isinfraternal-shim --test mouse, which fails only outside the gate becausezigisn't on PATH (FileNotFoundError: 'zig').scripts/verify fastat head: every row passes excepttest. That row fails ininfraternal-agent --lib:remote_client::tests::unavailable_udp_does_not_add_seconds_before_tcp_admission;winning_quic_drops_the_unfinished_tls_dial_before_control_dispatch;server_path_capability_is_advertised_on_native_and_fallback_transports.It happens on both VMs (dev-02 four times out of six; compute01 once out of once).
I'm treating this as caused by the branch until shown otherwise:
00ec71000passed 3 of 3. A later 12-pair run of the module alone at load 6 passed 12 of 12 on both.remote_clienttests, and dev passed219 passed; 0 failed.A localization run is queued on dev-02: full agent lib, interleaved over dev, the commit before P2-C, P2-C and head. Log:
~/j62c-logs/loc.log.J62c finished: round 4 of slice 1
Branch
proxy-traefik-281-j62b, headad2148fb91d3296d809db48d443de42bd5cfdce0, on origin/dev00ec71000. Pushed; not merged, not deployed. Per-finding RED/GREEN evidence is in my comment above. The commits from this round, in order:84040190aelements = { })fa894071fe13b6267ab4964427b2fda076479e804def5f5a2a565e840c0e87b3db1e1b61e075d1e1cb5dcdd3a9ad2148fb9Every commit is fmt-clean on its own.
Gate
scripts/verify fastatad2148fb9(dev-02, load 9–12), verbatim:The
testfailures in that run:git diff origin/dev...HEADtouches none of these files (restart,tests/agent_daemon.rs,remote_client*). Across eight gate runs today, the failingtestcases changed from run to run:status_restart(passes alone);replay_size(passes alone);cli_sessions;agent_daemon;remote_client.The cargo test runner stops at the first failing binary, so I also ran the whole workspace with
--no-fail-fast. 210 test binaries passed, and every failure was one of these timing tests.remote_clientneeds a look by someone with a quieter host. It failed in most gate runs today, on both VMs. Interleaved fullinfraternal-agent --libruns on dev-02, at load 9–12:remote_clientfailure00ec710002fda076479e804def5An earlier pair at load 6: head failed, dev passed. Run on its own module, 12 interleaved pairs at load 6 passed 24 of 24, and the single test passes in 0.12 s at head. So the flake exists on unmodified origin/dev. With this sample I can't rule out that the branch makes it more likely under load; I found no startup work in this round's diff that would cause it.
Other runs
Real Traefik + Pebble DNS-01 (
scripts/test-proxy-pebble.sh, compute01,b9a0f9de9; the only change since moves a test const):test namespace_serves_a_granted_mesh_backend_with_pebble_dns01 ... ok,1 passed; 0 failed. This covers P3-C's socket-ownership check against Traefik's own listener, the child lock and the supervisor changes.Root (compute01, real root, throwaway clone
/root/j62c, head36384e212; later changes are lint and test-only):The Mesh namespace tests (4) pass as real root under
unshare -n. The run createdinfraternal-proxy:x:999:987, and the cleanup removed it:account after: none; clone: removed.Unprivileged: the Mesh lib and namespace tests pass (compute01
unshare -Urn), and the proxy lib passes (dev-02).infraternal-shim --test mousefails only outside the gate, becausezigisn't on PATH (FileNotFoundError: 'zig'). Insideverify fast, VT fmt and the shim run normally.Size (report only, D243)
For the security reviewer
#[cfg(test)]only).Ops notes
~/infraternal-ops/slotsis missing, sobuild-slotrun as root assumes 2 slots. At the head of the queue it then waits while slots 3 and 4 are free, and blocks everyone behind it. I hit this and re-queued with claude's slot config./root/j79d-root/run.sh(not mine) was queued the same way.suwrapper, so scp to root exits 1. Usessh root@… 'cat > file' < file.Cleanup: worktrees, target dirs, refs and bundles are removed on both VMs, and the root clone and account on compute01. Small logs stay in
~/j62c-logs/on both VMs.J152 independent security review (round 4) started. Read-only: no code is pushed.
Target:
proxy-traefik-281-j62batad2148fb91d3296d809db48d443de42bd5cfdce0, diffed againstorigin/deve81eb5bb4(merge basebcca3054f).Plan (~3 h): read the round-4 diff against J133's findings (76880) and D330; run the proxy and Mesh suites as real root in a throwaway clone under
/root/j152on compute01, through build-slot; runremote_client(infraternal-agent) 5× interleaved at head and at origin/dev; post one verdict comment here.J152 independent security review, round 4: REQUEST CHANGES
Target:
proxy-traefik-281-j62batad2148fb91d3296d809db48d443de42bd5cfdce0, diffed againstorigin/deve81eb5bb4(merge basebcca3054f). This was a read-only review; no code was pushed. Root runs used a throwaway clone in/root/j152on compute01, through build-slot. The clone, its target dir, and theinfraternal-proxyaccount the tests created are all removed (userdel ok; account after: none; group after: none). Logs are on code-minipc01 in~/Developer/Worktrees/j152-evidence/.One open P2 (the dedicated account's ID policy) keeps this at REQUEST CHANGES. Everything J133 raised is fixed or narrowed. No P1 remains.
Round-4 fixes, checked
firewall.rs:604-612,iptables.rs:562-585). New flows still need SYN, and conntrack retirement keeps the same test (connection_state.rs:66-72). Inside the flow, routes are still enforced by Traefik's per-routeipAllowListon RemoteAddr, as before. Real-root netns runs: nft and iptables-nft pass.102 passed; 0 failed.the_parent_death_signal_survives_the_credential_drop ... ok,a_new_supervisor_stops_an_orphan_that_holds_the_child_lock ... ok. The reaper itself has a new problem (P3-1). Containment of a compromised child is still open (P3-4).nft_/nf_tables_revoking_a_route_ends_the_sources_open_proxied_flows ... ok. One bookkeeping residual remains (P3-3).redact_log_lineaddstrim(). Oversized lines are dropped, not truncated.verify_health_listenerruns before the first ack. An empty listener set and listeners on both the exact and wildcard address are both rejected. Root:a_squatter_on_the_health_port_cannot_acknowledge_routes ... ok. Later acks can only come from a process holding the bound port, and only the child holds it.tests/process.rs2 passed; 0 failed.Conntrack races. Rules are installed before retirement in every path. A deleted flow can't come back: a non-SYN
ct state newis dropped in both directions. The second sweep after Traefik's ack (againstproxy_config) closes the reload window. The one gap is thatproxy_configalso advances on updates Traefik never confirmed (P3-3).D330 (rootless
--public)No findings.
replace(), with its snapshot already written, and the reply waits for that generation's ack.MeshOnlystatic config has nohttp/httpsentrypoints, and thePublicOnlyprocess is only ever givenservice.publicroutes (controller.rs:482-489, 553-600).0.0.0.0and[::]. Std sets SO_REUSEADDR, so a port that another process listens on, on any address, counts as taken. EAFNOSUPPORT and EADDRNOTAVAIL count as free when there is no IPv6. The Mesh pair is skipped.wait()s the child beforejoinreturns, all before the unpublish reply. A failed public apply also drops the owner.Secret redaction: no new findings. Token values never reach argv or the environment, and log lines are redacted as raw, CR/LF-trimmed and fully trimmed values, plus named credential keys.
Root runs (compute01, euid 0, head
ad2148fb9)The namespace tests ran as real root under
unshare -n. The first proxy-lib pass had 5 failures. That run was contaminated by my own identity probe: its uid-999 workload correctly made other tests' starts refuse (another process runs as the proxy account). The clean rerun above skips the two probes.Two throwaway probe tests were added to the root clone only (sources in the evidence dir):
Findings
P2-1: The dedicated account gets UID 999, a UID that common container images also use. Rootful publishing either refuses permanently or shares that UID without noticing.
crates/plugins/proxy/src/identity.rs:182-195(useradd --system),:13-15(rationale),:73-105(verify_unshared),:226(check_unique).useradd --systemtakes the highest free ID at or below SYS_UID_MAX. On a stock host that is 999. It was 999 here, and in J62c's run. The official postgres and redis images (and many others) also run as UID 999. Without a user namespace, a container's processes and files have host UID 999.Scenario A (deterministic): the host already runs such a container. Every rootful start then fails with "another process runs as the proxy account". The probe above reproduces this.
proxy publishcan't work on that host until the operator renumbers something.Scenario B: the container starts after Traefik. Nothing checks again until the next child start, so the UID is silently shared. A compromised Traefik can then read and write anything that container wrote to bind mounts on the host, such as a
./pgdataowned by 999 under a world-traversable/srvor/home/<user>. The module doc says "IDs allocated fresh by the account tools have no prior files". That is false: the account tools consult only/etc/passwd, not the file system.Also,
check_uniqueignores the member list of theinfraternal-proxygroup, and other users whose primary GID is the proxy GID. Such users can traverse the 0750 proxy dir and read the 0640 config.Fix:
useradd -r -u <uid> -g <gid>) from a high band that images and/etc/subuid//etc/subgiddon't use, and record the choice in agent state.maps_system_rangewill need adjusting.P3-1: The orphan reaper SIGKILLs any process that holds the lock's inode, without checking who it is.
crates/plugins/proxy/src/supervisor.rs:1652-1686.The comment says a match "identifies an orphaned child, never an unrelated process". The probe disproves this: a bystander running as UID 65534 that held only a read descriptor (no flock) was killed alongside the real orphan.
How a descriptor can reach another process:
/procscan andkill.After the next agent crash, the root agent kills that process. This needs an agent crash to trigger, so it's P3.
Fix:
pidfd_openthe candidate, then re-verify that it still holds the inode, that its UIDs are the child identity, and that/proc/<pid>/exeis the installed Traefik inode. Then signal it withpidfd_send_signal.P3-2 (regression from P3-B): after a crash restart, rootful public routes stay behind the gate.
crates/plugins/proxy/src/ingress.rs:76-80,supervisor.rs:1266-1296, 1353, 1456-1468,controller.rs:605-633.Reaping the crashed child calls
gate(None), which also setspublic_route_active = false. The restart callsgate(Some(pair)), which closes the gate but never re-opens it. Only the controller'supdate_mesh_ingresssets the flag again, and that runs only on the next publish, unpublish or Mesh context change.Scenario: Traefik is OOM-killed. The supervisor restarts it with the same public routes, but every non-loopback packet to 80/443 is dropped until some unrelated event happens. This fails closed, so it is availability only.
Fix: have the supervisor restore the previous
public_route_activeonce the restarted child acks a snapshot that contains a public route. Add a test that crashes the fake child and asserts the gate opens again.P3-3: P2-C's "last confirmed" reference also advances on unconfirmed updates.
crates/plugins/mesh/src/firewall.rs:170-189.apply_proxy_ingresssetsproxy_configto the node's current config on every call. That includes gate-only updates from the supervisor thread (ingress.rs:76) and the controller'supdate(false, …)(controller.rs:505-507, andupdate_mesh_ingress(…, false)).Scenario:
complete()deletes X's flows.replace, the supervisor restarts a crashed Traefik from its retained, pre-narrowing services. Its gate update advancesproxy_configto the new config.This needs a restart inside the window, so it's P3.
Fix: pass
confirmedthrough to the node, and advanceproxy_configonly on confirmed snapshots. Better still, tag each snapshot with the Mesh config version it was rendered from, and advanceproxy_configto that version.P3-4 (design): a compromised child can escape orphan reaping.
The child holds a duplicate of the agent's open file description, so
flock(LOCK_UN)from the child releases the agent's lock. A child process forked from Traefik doesn't inherit PDEATHSIG and can keep the 80/443 listening sockets. After an agent restart, the Mesh firewall comes up with no gate, andverify_unsharedrefuses to start a new Traefik rather than reclaiming the ports. The escaped process keeps serving the public ports with the node's ACME keys. Only a cgroup (cgroup.kill) or a PID namespace contains this. This belongs in the threat model below, not in another patch.Narrowing or new classes?
Judged by area, not by round count:
Mesh firewall and conntrack: narrowing, converged. In round 3 this area had a P1 (whole flows) and a P2 (revocation). Both fixes hold under real root on both backends, and what's left is one P3 bookkeeping race (P3-3).
D330 public process: a new area, but clean. It follows J133's notes point by point.
Containment and identity of the Traefik child: new classes in new code, not narrowing. Each round added a mechanism here:
/procidentity scan, PDEATHSIG re-armed, and a/procfd-ownership killer.Each mechanism brought its own class of defect: directory modes and lost PDEATHSIG (round 3), and now an ID-allocation policy (P2-1), a kill without identity check (P3-1), and a compromised child escaping (P3-4). The cause is that nothing written says what containment has to guarantee.
Recommendation: before more patching in this area, write a short threat model and design note. It should answer three questions:
It should then pick one mechanism with measured costs. Options:
Delegate=yes);The owner decides. Fixing P2-1 alone in the current design is fine as a stopgap.
remote_client(infraternal-agent --lib), head vs origin/dev, interleaved, compute01Features matched
cargo test --workspaceunification (headnative-streams,mesh,proxy,plugin-host-facts; dev the same withoutproxy), withINFRATERNAL_TEST_STATE_ROOTset. A watchdog killed any test binary running past 240 s and recorded the hung test.Head fails
remote_clientin 3 of 5 runs, dev in 0 of 5. The cause is not branch code.remote_clientfailures happen in runs wherecancelling_remote_dispatch_retires_the_borrowed_authenticated_channelhangs.remote_clienttests:213 passed; 10 failed, and the 10 are the lifecycle/sessions tests that fail on dev too in this environment.timeout 60: head 3/3 hung, dev 3/3 hung) and whendaemon::remote_testsruns as a module (head 3/3, dev 3/3).So it is a pre-existing, order-dependent hang. Dev's full-lib order happens to avoid it, and head's slightly different test set doesn't. The branch doesn't touch
daemon*orremote*code except one line inremote_supervisor_review_tests.rs. Even so, the gate will be flakier after this merges, so the hang needs its own issue: a test-isolation fix and a timeout on that test's awaits.No code was changed, so the gate is not applicable. The size delta is unchanged from J62c's report.
J62d start (round 5)
Branch
proxy-traefik-281-j62d, rebased onto origin/deve8c7acd44(clean, 49 commits), head01f6d8b6f(= j62bad2148fb9rebased).Plan (~3–4 h):
useradd --systemallocation). Failing-first test. (~60 min)pidfd_send_signal. (~40 min)proxy_configadvances only on confirmed snapshots. (~30 min)docs/design/proxy-containment.md(threat model + 3 options + recommendation), no implementation. (~30 min)cancelling_remote_dispatch_retires_the_borrowed_authenticated_channelhang.scripts/verify fast.Each item is one commit, pushed as it lands.
J62d progress
Branch
proxy-traefik-281-j62d, heada0c0f3d47(rebased onto deve8c7acd44). Pushed commits:7523941ecP2-1: explicit proxy UID/GID from 60514–60999, neveruseradd --system's choicee071902fcP3-1: orphan reaper checks identity through a pidfd (pidfd_send_signal)6695cc9a5P3-2: the public gate reopens when a restarted child confirms its routesee579e9c8P3-3: the Mesh flow reference moves only on a confirmed snapshota0c0f3d47Part B:docs/design/proxy-containment.md(for the owner's decision)Done so far (dev-02, ordinary user): proxy lib 106 passed, mesh lib 79 passed, and the namespace test binary builds. Each commit passes
cargo checkon its own. Workspace clippy and fmt PASS inverify quick. Its unit stage fails only ininfraternal-agentsessions/lifecycle/remote_client tests (NotFound), the same environment failures J152 saw on dev.verify faston dev will show it.Running now: root failing-first and fixed runs on compute01 (unit
j62d-root2, log/root/j62d-root.log), andverify faston compute01 (~claude/.cache/codex-scratch/j62d/fast-full.log). One target dir per VM:~/.cache/codex-scratch/j62d/target, plus/root/j62d/target.The pre-existing hang is filed as #333.
J62d finish: Part A and Part B done; ready for review (not merged)
Branch
proxy-traefik-281-j62d, heada0c0f3d47, on origin/deve8c7acd44. Commits:7523941ecP2-1: the proxy account gets an explicit ID, the lowest free one in 60514–60999 with UID == GID. A candidate is free only when no passwd UID or primary GID uses it, no group uses it, no subuid/subgid range covers it and no running process uses it. The ID is passed to groupadd/useradd (or BusyBox addgroup/adduser); the tools' own allocation is never used. An existing account outside the band is refused with a clear message. A group with members, or one that is another user's primary group, is refused. The passwd/group entries are the record.e071902fcP3-1: the orphan reaper pins each candidate withpidfd_open. It then re-checks that the process holds the lock inode, that all its UIDs are the child identity's, and that/proc/<pid>/exeis the installed Traefik inode. The signal goes throughpidfd_send_signal(new shim wrapper).6695cc9a5P3-2: the gate now carriesClosed/Open/Cleared. A restarted child that confirms a snapshot with a public route reopens the gate.ee579e9c8P3-3:confirmedis passed from the publisher to the node to the firewall, andproxy_configadvances only on confirmed snapshots.a0c0f3d47Part B:docs/design/proxy-containment.md. It proposes a threat model, compares three options, and recommends option 1 (a cgroup per child) now, with option 2 (namespaces) later for rootless. The owner's decision; nothing is built.Separate issue for the pre-existing hang: #333. (It passed in this gate run:
cancelling_remote_dispatch_retires_the_borrowed_authenticated_channel ... ok.)Root, compute01, throwaway clone
/root/j62d(run as a systemd unit through build-slot; removed afterwards, account removed)Each fix was first reverted to show its test failing, then restored.
Reading it:
infraternal-proxy:x:999:987.left: "echo" right: "lost"on nft and iptables-nft. With the fix, all 6 namespace tests pass.Ordinary user, dev-02: proxy lib 106 passed, mesh lib 79 passed. Each commit passes
cargo checkon its own.scripts/verify fast, compute01, heada0c0f3d47Static size, D243 report only:
These deltas are for the whole #281 branch against the gate's baseline. Round 5 adds only a few hundred lines of logic.
Full gate log on compute01:
~claude/.cache/codex-scratch/j62d/fast-full.log; root log:~claude/.cache/codex-scratch/j62d/root.log.Open items: one owner decision (Part B containment option). P3-4 (a compromised child that forks away) is deliberately left to that decision.
J156 independent security review of the round-5 commits: APPROVE
Target:
proxy-traefik-281-j62data0c0f3d47. Scope: only7523941ec,e071902fc,6695cc9a5andee579e9c8, against J152's round-4 review (77190). The design notea0c0f3d47is the owner's decision and is not reviewed here. This review was read-only; nothing was pushed.The four fixes do what they claim. None of them breaks an item J152 passed. Two findings are non-blocking P3s, both on edges where the code fails closed.
Root run (compute01, euid 0,
/root/j156git-archive ofa0c0f3d47, through build-slot)The tests leave the account behind, so I removed it afterwards (
after: [] []). No process was left running as 60514, and/root/j156is gone. I did not rerun the mesh namespace tests:ee579e9c8is a one-bit change, and J62d's fail-first evidence for it on both backends is convincing.Checked
7523941ec: fixed. The ID is chosen from 60514–60999 with UID == GID./etc/subuidand/etc/subgidranges (u64, saturating), and all Uid/Gid/Groups of running processes. The ID is then passed explicitly to groupadd/useradd or addgroup/adduser.choose_idanduseradd. useradd and adduser then fail on the duplicate ID without-o, the group is rolled back, and start fails. A concurrent agent that wins is caught by the secondlookup(), andcheck_in_rangeandcheck_uniquerun on whatever account exists.verify_unsharedbefore every child start (unchanged from round 4).maps_proxy_rangenow requires the mapping from 0 to reach 61000.e071902fc: fixed.acquire_child_lock(supervisor.rs:1056-1066), unchanged.pidfd_open, then re-check after the pin: the process holds the inode, all four UIDs are the child UID, and/proc/<pid>/exematches the installed Traefik's dev and inode. The signal then goes through the pidfd./proc/<pid>reads described it. A PID reused before the pin fails the re-check. A PID reused after it gives ESRCH.executable: None, returns early). A holder that doesn't match is never signalled, and start fails closed with the existing timeout.signal_process_descriptor(infraternal-shim/src/pty.rs:1590-1614): sound.BorrowedFd<'_>that outlives the call, and theOwnedFdfromprocess_exit_descriptorstays alive in the caller across the call.nix::sys::signal::Signalis a closed enum of valid signals, so no out-of-range number can be built.last_os_error()is read right after the syscall, with nothing in between.c_intand flags isu32; the kernel takesint/unsigned intand truncates, so the upper register bits don't matter.pidfd_openorpidfd_send_signal. The caller ignores the error and nothing is signalled, so this fails closed.6695cc9a5: the gate can't open for an unconfirmed config.Openis sent only fromstart_runtime_childafterstart_managed_childreturns Ok (supervisor.rs:1266-1278).verify_health_listener(child-owned port) andwait_for_config_generationacked the snapshot written for these exact services (:1368,:1415-1420). It is sent only when one of them ispublic.Cleared, and a failedOpenleaves the gateClosed.Closednow also resetspublic_route_active, which is correct: a new child isn't live until it confirms.Clearedon reap or failed start,Closedbefore spawn.confirmedflag),ee579e9c8: fixed.proxy_configadvances only whenconfirmedis true (mesh/src/firewall.rs:193-195).Open, go throughupdate(false, …), so a crash restart from retained routes can't move the reference.Findings (non-blocking)
P3-a: ID collision checks see only local files, while the account lookup uses NSS.
crates/plugins/proxy/src/identity.rs:178-187(choose_id) and:384-411(check_unique) parse/etc/passwdand/etc/group.lookup()(:269-271) goes through NSS.useraddchecks only local files and createsinfraternal-proxywith 60514. The proxy now shares an identity with a real user's files on NFS or home directories.choose_id'stakenclosure and incheck_unique, also reject an ID whenUser::from_uid(id)orGroup::from_gid(id)returns an entry whose name isn'tinfraternal-proxy. This is cheap, at most a few hundred NSS lookups the first time only.P3-b: an orphan from an older Traefik release is never reaped, and start then fails permanently.
crates/plugins/proxy/src/supervisor.rs:1060-1065and:1700-1702. Releases are installed content-addressed (installer.rs:261), so an upgrade givessettings.executablea new inode.Gate not applicable: no code was changed by this job.
Finding from J103e (#260):
13b6836a5slows the remote supervisor's first dialOn a fresh agent state dir, the proxy plugin creates
proxy-secretswith a durable parent-directory fsync during plugin start. The remote supervisor dials only after plugin start. On compute01, beside thelogin_binarytests, that fsync took 6.5 s (strace), and deve0633114bfailsremote_client::tests::unavailable_udp_does_not_add_seconds_before_tcp_admission/winning_quic_drops_the_unfinished_tls_dial_before_control_dispatchin 3 of 5 runs ofcargo test --workspace --locked --lib -- remote_client:: login_binary::.Full evidence: #260 (the J103e dev finding). The same class of bug was fixed for the mesh engine record on
mesh-gotatun-linux(92f4c1dba). Not fixed here.J159 done for both P3s (J156, comment 77247). Branch
proxy-p3-j159, headb985e2b1d5be329e26d3824adf8c7c8a89547503, base91112e9da(dev). Not merged, not deployed.Commits (atomic, pushed):
028a4bd3bproxy: look up candidate IDs through the system user directory.choose_idandcheck_uniquenow also call getpwuid_r/getgrgid_r (nix) through a smallDirectorytrait. An ID the directory knows is skipped, our account must resolve to its own name only, and a failing lookup counts as taken or refused. Rejected: agetentsubprocess. Test:ids_held_by_a_directory_service_are_never_chosen_or_shared, using a fake lookup.b985e2b1dproxy: stop an orphan left by an older Traefik release. The orphan check now matches any installed release's executable (<store>/<digest>/bin/traefiksiblings of the current one, only when the path has that exact shape). The pidfd, lock-holder and UID checks are unchanged. A holder that still isn't ours is named in the error with its PID and executable. Tests:an_orphan_from_an_older_traefik_release_is_stopped_tooanda_holder_that_is_not_an_installed_traefik_is_named_in_the_error. The existing orphan test is unchanged in behaviour.Real-environment proof (root, compute01, throwaway clone /root/j159):
cargo test -p infraternal-plugin-proxy -- --test-threads=1gavetest result: ok. 109 passed; 0 failed. That includedthe_rootful_child_runs_as_the_dedicated_account, all three orphan tests, the directory test andtraefik_child_obeys_the_rootful_identity_and_capability_boundary. Afterwards I removed the clone, its target dir and theinfraternal-proxyuser and group (checked with getent).verify quickon compute01: fmt, clippy and unit tests (changed) all PASS,VERIFY_EXIT=0.Gate status:
scripts/verify fastis NOT green, and I am not claiming it is. Three full runs failed intest, each on a different test outsidecrates/plugins/proxy. Every other row passed, including clippy and static builds (x86_64 +102400 B accepted; aarch64 +611664 B vs baseline, report-only).remote_client::tests::winning_quic_drops_the_unfinished_tls_dial_before_control_dispatch(Elapsed at remote_client_tests.rs:205). Passes alone:cargo test --workspace -- winning_quicgave ok.status_restart::service_states_are_truthful_in_text_and_json(state "unknown", 1.85 s). Passes alone on my branch (ok, 4.97 s) and on unmodifiedorigin/dev91112e9da(ok, 4.93 s).remote_client::tests::unavailable_udp_does_not_add_seconds_before_tcp_admission(remote_client_tests.rs:167) andwinning_quic_…again. Both are wall-clock assertions.agent_queues::saturated_terminal_queues_preserve_every_byte_and_daemon_liveness(agent_queues.rs:14, 53 s).Both VMs were at load average 8–16 with other gates running. These look like load-sensitive timing flakes. I did not prove them on
origin/devunder identical load, since that needs a full dev gate per failure. My diff touches onlyidentity.rs,supervisor.rs,supervisor_tests.rsand the visibility ofvalid_component_digestininstaller.rs. Logs: compute01~/j159-fast-run1.log,~/j159-fast-run2.log,~/j159-fast.log; dev-02~/j159-fast.log.Needs the orchestrator's call: accept the flakes as known, or rerun
faston a quieter VM.J164 start: a cgroup per Traefik child (D336, option 1 of
docs/design/proxy-containment.md)Branch
proxy-cgroup-j164, based onorigin/dev109e14cb7. Not merged, not deployed.Plan (estimate ~5 h):
claude, time creating a leaf cgroup, moving a process into it (cgroup.procsandCLONE_INTO_CGROUP),cgroup.killplus waiting forcgroup.events populated 0, andrmdir. Compare that with today's spawn and stop path, plus steady-state CPU and RSS. Numbers go here before I build anything further.<agent cgroup>/proxy-<generation>. Stop meanscgroup.kill, wait for it to empty, thenrmdir. Rootful usesDelegate=yeson the infraternal agent unit, withKillMode=processkept. Rootless uses the user manager's delegation. If neither is writable, it falls back to today's pidfd reaping, andmesh status/proxy statussay so.proxy-*leaf is killed and removed. J159's executable-based check stays as the fallback path only./root/j164on compute01, a fake Traefik that double-forks a grandchild must leave nothing behind after a stop and after an agent crash (SIGKILL). Rootless too, ifclaudehas delegation.J164 measurements: cost of a cgroup per Traefik child
The benchmark is
cgbench.rs, a standalone file with no dependencies, built with the flake's rustc. Each iteration startssh -c 'sleep 1000 & exec sleep 1000', so it has a direct child plus a forked grandchild.mkdira leaf under the process's own cgroup. The pre-exec hook writes0to an inheritedcgroup.procsdescriptor, so the child is inside the leaf beforeexecand before it can fork. Stop iscgroup.kill, reap the direct child, pollcgroup.eventsuntilpopulated 0, thenrmdir.Rootful: perf VM,
perf-run --root(cpus 1–3), kernel 6.12.113, 500 iterations, run 2 of 2 (run 1 within ±5%)cgroup.procswrite in the child). Traefik's own start-up to a healthy ping takes hundreds of milliseconds, so this is below 0.1% of a child start.Steady state (same perf VM run)
Rootless: compute01,
systemd-run --user -p Delegate=yes, 300 iterations (load average 5–17, so only the relative numbers mean anything)Same shape as rootful: the deltas are mkdir plus rmdir plus one write. This is the same kernel path, so I use the perf VM numbers as the reference.
Delegation, observed on compute01 (systemd 257)
Delegate=yesunit's cgroup carries thetrusted.delegate=1anduser.delegate=1xattrs. A unit without it carries neither. Rootful detection uses this xattr.Delegate=yes, no delegate xattr is set. The wholeuser@1500.servicetree is owned byclaude, though, while a loginsession-*.scopeis owned by root. Rootless therefore uses the agent's own cgroup when that cgroup is owned by the agent's user and writable, and otherwise falls back.What this means for the build
proxy-<n>). No controllers are enabled in it (subtree_controlstays empty), so the cgroup v2 "no internal processes" rule never applies, and the infraternal agent itself stays where systemd put it.Delegate=yesto both generated units (the server unit with its embedded infraternal agent, and the host bootstrap unit).KillMode=processstays. Session shims already run in their owninfraternal-session-*.scope, so delegation doesn't touch them.Delegate=yes, so they fall back to today's pidfd reaping, andmesh statusandproxy statussay so until the unit is rewritten.cgroup.procsand leave the leaf. Moving it requires write access to the common ancestor, which the user has. The crash case and ordinary forks are still contained. A rootful child (UID 60514, no capabilities apart from NET_BIND_SERVICE) can't write the root-owned files. This matches the design note's "rootless: accepted for now" line, and option 2 (a cgroup namespace inside a user namespace) is the fix.Raw logs: perf VM
~/j164/rootful/. Benchmark source and binary: compute01~/.cache/codex-scratch/j164/.J164 progress. Branch
proxy-cgroup-j164, headfd9a4c803, four atomic commits on base109e14cb7:8a635b94fproxy: each Traefik child runs in its own cgroup leaf. It joins in the shim's pre-exec hook. Stop iscgroup.kill, then wait for empty, then rmdir. On start the supervisor adopts its orphaned leaves; J159's reaper stays as the second line. Tests are insupervisor_cgroup_tests.rs.2522d94e3status: containment is reported ininfraternal publish(the proxy listing) and inmesh status(a "Proxy" row).2c80bd8f9units:Delegate=yeson the generated server and host units.KillMode=processis unchanged.fd9a4c803docs: the design note now records what was built, and corrects its rootless claim.verify quickon dev-02 (1b1da8238, the same tree): fmt and clippy passed. Unit tests stopped ininfraternal-agenton threeremote_clienttiming tests (Elapsed, andfallback_local_admission_us=2003775) at load average 9–15. These are the same flakes J152 and J159 recorded on dev. The fast gate will tell.Queued now: the root run on compute01 (
/root/j164, red = undelegated login scope, green =Delegate=yesunit, plus the whole proxy crate), and the rootless run on dev-02 (session scope against asystemd-run --user -p Delegate=yesunit). Target dirs: dev-02~/worktrees/j164/target, compute01/root/j164/target.J164 real-environment proof: a double-forking Traefik leaves nothing behind
Head
31fabf153. The test issupervisor_cgroup_tests.rs. The fake Traefiksetsid-forks a grandchild,/bin/sleep <unique>, which leaves the process group and session and has no parent-death signal. The test looks for that process by its unique argument, not through the leaf. Two scenarios:Red is today's mechanism: no delegation, so fallback, with
INFRATERNAL_REQUIRE_PROXY_CGROUP=1. Green is aDelegate=yesunit.Rootful: root on compute01, throwaway clone
/root/j164, through build-slot (Debian 13, systemd 257, 6.12)red-orphan.txtcomes from an earlier run whose crash test had a bug (fixed before this head) and so never cleaned up. It shows what P3-4 looks like for real: the escaped process runs as 60514 under PID 1 and still holds the child lock (fd 65), while sitting in the login scope with no cgroup of its own. The new agent's J159 reaper refuses to kill it, because it isn't an installed Traefik. With containment it is killed on start.Rootless:
claudeon dev-02 (Ubuntu 24.04, systemd 255, 6.8),systemd-run --user -p Delegate=yesagainst the login session scopeNotes:
0::/, an init without systemd) was refused and fell back. It is fixed ina15f9c43c, and the rerun above passes 115/115.tests/process.rs::traefik_child_receives_only_credential_file_paths_and_redacts_its_logsfailed once on dev-02 under load. It's a pre-existing race: the shell creates the capture file with>beforeprintfwrites it, and the test reads it as soon as it exists. That path spawns without a cgroup. Rerunning the same binary 10× gavepass=10 fail=0, and as root it passed (2 passed).nix develop … | grepcan hang after the command exits. nix's detachedcrashpad_handlerkeeps the pipe's write end open. Redirect to a file instead.Cleanup:
/root/j164src, target and state removed (logs kept). Noproxy-*leaves remain on either VM, no processes run as the proxy account, andinfraternal-proxywas removed ([] []).J164 gate:
scripts/verify faston compute01, head31fabf1537048b4fb6b6c25a5b0c578291a257ecNot green. Every row passes except
test. That row stopped ininfraternal-agent --libon tworemote_clientwall-clock tests:unavailable_udp_does_not_add_seconds_before_tcp_admissionandwinning_quic_drops_the_unfinished_tls_dial_before_control_dispatch. They are the same tests J152 and J159 recorded as load flakes, and the load average was 25–40 during this run. This branch touches nothing inremote_client. Its only agent change is the probe wiring inplugins.rs. A base-against-head run of those tests on this machine is queued, and I'll post it.The tests the
testrow didn't reach after that stop were run separately, and all passed:Delegate=yesunit: lib 115/115 and integration 2/2;Log: compute01
~/j164-fast.log. Excerpt, verbatim:Size: the x86 and aarch64 rows report against the gate's stale baseline (+756544 B and +633184 B), so they are not this slice's delta. The same queued job builds the base
109e14cb7x86 static binary to give the real delta.J164 finished: a cgroup per Traefik child (D336). Not merged, not deployed; ready for the security review.
Branch
proxy-cgroup-j164, head31fabf1537048b4fb6b6c25a5b0c578291a257ec, base109e14cb7:a15f9c43cproxy: run each Traefik child in its own cgroup (shim pre-exec join,cgroup.killstop, adoption on start, tests)1820bb15dproxy, mesh: say how Traefik is contained in statusda5685db4units:Delegate=yeson the generated server and host units31fabf153docs(proxy): record option 1 as built, and the rootless limitMeasurements: comment 77343. Root and rootless proof: comment 77394. Gate, verbatim: comment 77408. Every row passes except
test, which stopped on tworemote_clientwall-clock tests.Are the
remote_clientfailures ours? Base against head, compute01, the samecargo test --workspacebinaries, filtered toremote_client::tests, interleavedinfraternal-agentlib under heavy load, which is J152's order- and load-dependent flake.infraternal-agenttests. Its only agent change is the probe closure inplugins.rs, so the lib's test set and order are the same as base.faston a quieter VM.Size
x86_64 static: base 23,155,568 B, head 23,174,800 B, +19,232 B for this slice. The gate's
+756544is measured against its stale baseline.xattrwas already linked throughtar.For the security review
CAP_SYS_ADMIN).Delegate=yes, every existing install until its unit is rewritten, a rootless agent outside a user service, a hybrid cgroup v1 system, and a kernel older than 5.14. Status then says "limited: ; a process Traefik forks may outlive it".Cleanup: compute01 and dev-02 worktrees and targets removed.
/root/j164holds logs only. No proxy leaves or test processes are left, and theinfraternal-proxyaccount was removed.J172 start: security review of
proxy-cgroup-j164Reviewing head
31fabf1537048b4fb6b6c25a5b0c578291a257ecagainst base109e14cb7(D336, J164 comments 77343/77394/77408). Read-only: the branch is not changed.Plan (about 2 h, most of it build-slot queue):
cgroup.rs, the supervisor/shim diff, the units and the status wiring. Done.Delegate=yes+KillMode=processlifecycle, xattrs, root cgroup files) and rootless on dev-02 (escape reach). Done, findings follow in the verdict.supervisor_cgroup_tests.rs(never pushed): a child that writes itself into the parent cgroup, a child that makes a nested cgroup, a dump of the child's descriptors and cgroup. Root:/root/j172on compute01 (queued, position 9). Rootless:~/worktrees/j172on dev-02 (queued, position 3).J172 verdict on
proxy-cgroup-j164@31fabf1537048b4fb6b6c25a5b0c578291a257ec: REQUEST CHANGESNo P0 or P1. Rootful containment holds. The reaping cannot reach outside the infraternal agent's own subtree, and the join happens before exec with no window. There is one P2: a rootless child that leaves its leaf makes stop hang forever. Five P3s follow.
Method. I read the diff. Then I appended review-only probe tests to a throwaway copy of
supervisor_cgroup_tests.rs; they were never pushed. Each probe uses a fake Traefik that records its uid, cgroup and fds, records the cgroup of a forked grandchild, then (by mode) tries to move itself into the parent cgroup (escape) ormkdirs a nested cgroup inside its leaf (nested). It then setsid-forks and execssleep. Root ran on compute01 in/root/j172(built through build-slot, removed afterwards). Rootless ran asclaudeon dev-02 in asystemd-run --user -p Delegate=yesunit. Logs: compute01/root/j172/root-run.log, dev-02~/j172/user-run.log. No leftovers on either VM (0 marker processes, 0proxy-*leaves). I left the existinginfraternal-proxyaccount on compute01 in place.What holds (with evidence)
uid=60514 cg=/system.slice/run-p2265822-i52592164.service/proxy-2d80a5-0/grandchild cg=…/proxy-2d80a5-0. The write happens first in the pre-exec hook, so nothing runs as Traefik before it. A failed write fails the spawn, and the leaf is removed.cgroup.procsfd reaches the child (it isO_CLOEXEC), and the write runs beforedup2, so a remap onto its number can't matter. Same on both hosts.escape-exit=2 … cannot create …/cgroup.procs: Permission denied, andmkdir … nested: Permission denied. The stop then empties and removes the leaf (grandchildren=[] direct-child=[] leaves=[]), and the next start is fine.own_cgroup/valid_recorded_basereject..,.and empty segments, and anything outside/sys/fs/cgroup. Rootful, the record lives in the root-only lock directory. Rootless, the child can rewrite the record (same uid), but then the kernel limitscgroup.killto that user's own tree.user.slice: refused; touser@1500.service(internal node): refused.Delegate=yes+KillMode=process(root, systemd 257, shell). The unit getstrusted.delegate=1anduser.delegate=1.subtree_controlstays empty, so systemd enables no controllers.systemctl stopkills only the main process: the unit readsinactivewhile the leaf and its members live on, and the next start reuses the cgroup, whereadopt_orphanskills them. This matches today's KillMode=process behaviour; the forked grandchild escaped before this branch too. Existing installs have no xattr, so they fall back, which the status reports./j172-nosdwith/run/systemdhidden getskind=cgroup, and a stop leaves nothing.P2-1. A rootless child that leaves its leaf hangs
stop()forever, and status still says "contained"Evidence (dev-02,
mode=escape):TraefikChild::stopskipschild.kill()whenever thecgroup.killwrite succeeds, then blocks inchild.wait(). A direct child that is no longer in the leaf never gets SIGKILL. So the supervisor worker thread hangs, and so doesTraefikSupervisor::drop(it joins the worker), which wedges proxy reconfiguration and infraternal agent shutdown. The escaped process keeps its listeners. This needs a compromised rootless Traefik (accepted in the design note), but the result should be "the escapee runs, and stop still finishes", not a wedged infraternal agent.Fix: always
kill()the direct child as well (it is pidfd-safe: std still holds the unreaped child), whether or notcgroup.killsucceeded. Optionally, before waiting, check that/proc/<pid>/cgroupstill names the leaf, and if not, log that the child left its cgroup.P3-1. A nested cgroup made by a rootless child blocks every later start (EBUSY)
Evidence (dev-02,
mode=nested):rmdirof a leaf with an empty child cgroup fails with EBUSY, andadopt_orphanspropagates that through?, so the proxy never starts again until someone removes the cgroup by hand. Only a compromised rootless child can do this (rootful mkdir is denied, see above).Fix: in
kill_and_remove, afterpopulated 0, remove the child cgroups depth-first (this is safe:cgroup.killalready covered the whole subtree, andpopulatedreflects the whole subtree). Then remove the leaf.P3-2. Rootless status overstates containment
Containment::Cgroup.describe()gives the same text rootful and rootless: "each Traefik process runs in its own cgroup; stopping it stops everything it started". Rootless, P2-1 shows that isn't true for a compromised child. The design note and comment 77343 are honest about this, butmesh status/publishare not.Fix: add a rootless variant (e.g.
kind: "cgroup-rootless"), with text like "…; a compromised Traefik running as your user could still leave it".P3-3. Root in the root cgroup (no systemd) gets a false reason
Evidence (compute01, mount ns without
/run/systemd, agent moved to/):J172 own=0::/ kind=fallback detail=limited: the kernel is older than Linux 5.14 (no cgroup.kill)on kernel 6.12.The root cgroup never has
cgroup.kill(orcgroup.events), on any kernel. Falling back is safe, but the stated reason is wrong.Fix: say "the infraternal agent runs in the root cgroup". Or support it: leaves under
/are legal, and the root cgroup is exempt from the no-internal-processes rule. Detectcgroup.killsupport by checking a created leaf, or/sys/kernel/cgroup/features, rather than the base.Kernels before 5.14 (no
cgroup.killanywhere): I found that by reading the code, not by running it (no such kernel was available). They take the same check and fall back with a correct message.P3-4. Leaves that outlive a unit losing
Delegate=yesare never reapedEvidence (root, shell): a
Delegate=yes+KillMode=processunit left a populatedproxy-*leaf on stop. AfterDelegate=yeswas removed and the unit restarted, the xattr wasabsent 61, the leaf was still there, and all its members were alive.detectthen falls back, andadopt_orphansnever runs, so those processes live for as long as the unit's cgroup does. This happens on a downgrade, or when a hand-written or distro unit replaces a generated one.Fix: in fallback, when the agent's own cgroup is writable (root, or the owner), still run
adopt_orphanson its own cgroup and the recorded base. Killing your own tagged leaves needs no delegation.P3-5. Leaf tag uses the inode without the device
tag = ino(lock_directory). Two infraternal agents in the same cgroup, with lock directories on different filesystems that share an inode number, would reap each other's live leaves at start. This is unlikely (one agent per unit), but cheap to rule out.Fix:
proxy-<dev:x>-<ino:x>-<n>.Also noted, not a finding: systemd versions that set no
trusted.delegate/user.delegatexattr would report "lacks Delegate=yes" even when the unit has it. That is safe, but the message is misleading. I didn't test it (both VMs run systemd 255/257).Gate: no code changed in this review, so the gate is not applicable. The branch's own gate is in 77408.
J164 round 2 (J172 review, comment 77452) started and pushed. Branch
proxy-cgroup-j164, headb9467d190, on top of31fabf153. One commit per finding, each with its test:5ccab8cb5P3-5: the leaf is namedproxy-<dev>-<ino>-<n>. Test:the_leaf_name_carries_device_and_inode.a13d7f65dP2-1: stop always kills the direct child as well. Test:a_child_that_leaves_its_cgroup_is_still_stopped(the child writes itself into the parent cgroup; stop must finish within 10 s).f1bbf1399P3-1: nested cgroups are removed deepest first, bounded at 4096, on both stop and adoption. Test:a_cgroup_made_inside_the_leaf_never_blocks_removal.0f2414a7fP3-2: a rootless variant, kindcgroup-rootless, whose text says a compromised Traefik running as your user could still move out. Test:rootless_status_says_what_is_really_contained.815f36353P3-3: "the infraternal agent runs in the root cgroup". Test:the_root_cgroup_is_named_as_the_fallback_reason.c7a933ee6P3-4: adoption also runs in fallback, viaProxyCgroups::for_reaping, which makes no new leaves. Test:leftover_cgroups_are_reaped_even_without_delegation.b9467d190the design note.Queued: red (the final tests over the old
cgroup.rs/supervisor.rs) and then green.verify quick./root/j164), in an undelegated login scope and in aDelegate=yesunit.Gate: waiting for J168 to merge, then rebase and run
verify fast.J164 round 2 finished: all six J172 findings fixed. Head
bc6e5880ad3dc4f09ce4d3295bb35a8bf490d5a6. Not merged, not deployed.Commits on
31fabf153, one per finding, each with its test:85b605446P3-5: tag is device and inodedf283a507P2-1: stop always kills the direct childe2ce145feP3-1: nested cgroups removed deepest first, boundeda31f9973cP3-2:cgroup-rootlesskind and status texta2ae82a7cP3-3: root cgroup named as the reason0f83fb820P3-4: adoption also in fallback (for_reaping)bc6e5880adocsI replayed the series twice while testing (the heads in comment 77494 are superseded). Once to fix a bug in the tag test: it computed the expected prefix after removing its fixture. Once because
verify quickfoundassigning_clonesin the P3-1 loop.How red was made: the final test file over the pre-fix
cgroup.rsandsupervisor.rsfrom31fabf153. Green is the head. The tests find processes by a uniquesleepargument, never through the code under test.:339)escape: stop=Ok(()) direct child alive=falsestop=Err(…ResourceBusy), leaf left, adopt EBUSY. Root: adopt EBUSYstop=Ok(()),adopt=Ok(1), nothing leftkind=cgroup, "stops everything it started"kind=cgroup-rootless, "…a compromised Traefik running as your user could still move out of it". Root unchangedcontainment=fallback survivors=[3306605] leaf left=truecontainment=fallback survivors=[] leaf left=falseproxy-313601-0proxy-22-313c77-0Rootful escape and nested-on-stop pass even before the fix (
made=false, because the move and the mkdir are refused), as J172 found.Whole proxy crate at head: rootless delegated 121 lib / 2 integration, root in
Delegate=yes121 / 2. Cleanup: noproxy-*leaves, no marker sleeps, no proxy-account processes.infraternal-proxyremoved, and/root/j164holds logs only.verify quick(dev-02, head): fmt PASS, clippy PASS. Unit tests failed only ininfraternal-agent --lib, on fourremote_clienttests (server_path_capability…,unavailable_udp…,winning_quic…,durable_state::acknowledged_network_access…). The orchestrator identified these as J168's dev regression. I'll rebase onto dev and runverify fastonce J168 merges.dev-02 rootless log (red, green, scope, crate, quick)
compute01 root log
J168 finish: head
cb05abcaf1d142e69f5ec64a71358b55a729d72fon branchproxy-start-io-j168(base226a569ac, pushed)6774296f5opening_custody_on_fresh_state_writes_nothing(open + cleanup + get + delete on a fresh state dir leave the state dir empty; timing-independent) andfirst_secret_creates_the_private_store_lazily.752e57d0aFileDnsTokenCustody::openonly validates an existingproxy-secrets;putcreates root and provider dir (same parent+self fsyncs) on first write. Cleanup/delete on a never-written store are no-ops,getis NotFound. Durability for stored secrets is unchanged. Rejected: a background-thread fsync (IO stays on a path with no reason to run, races the firstput).cb05abcaf752e57d0aalone passes fmt only with this commit on top.Red/green (compute01): at
6774296f5opening_custody_on_fresh_state_writes_nothing ... FAILED(7 passed, 1 failed). At752e57d0aall 111 proxy lib tests pass.Proof (the brief):
cargo test --workspace --locked -j4 --lib -- remote_client:: login_binary::at752e57d0a(same code as the head apart from test formatting): 20 of 20 pass, load 13 to 44 during the runs (logs:~claude/j168-evidence/loop-*.logon compute01).scripts/verify fastis NOT green at the head, twice, with a different unrelated test each time:752e57d0a): fmt FAIL (mine, fixed bycb05abcaf), test FAIL:status_restart::service_states_are_truthful_in_text_and_json, "service manager query timed out".cb05abcaf): fmt PASS, clippy PASS, static builds PASS, test FAIL:agent_daemon::real_binary_rootless_cleanup_survives_sender_disconnect_before_readiness,terminal.login.revoke->resource_unavailable.cb05abcafand 6 of 6 at base226a569ac(cargo test -p infraternal --test agent_daemon --test status_restart, load 3 to 8). I did NOT reproduce either failure on unmodified base, so I cannot say they also fail there; they look like load flakes in the gate (the gate ran at high load). Logs:~claude/j168-evidence/fast-run{1,2}.log.Static-size delta (report only): aarch64 +611536 bytes vs baseline in run 1 (this change adds no code beyond a few lines; the baseline predates other dev merges).
J177 verdict on
proxy-cgroup-j164@bc6e5880ad3dc4f09ce4d3295bb35a8bf490d5a6: APPROVEScope: only the diff
31fabf153..bc6e5880a. Each of the six J172 findings (77452) is closed, and I found no new hole. One P3 note below, which does not block.Code review, finding by finding
stop()now always callschild.kill()after the best-effortcgroup.kill, andwait()stays the authoritative reap. The kill iskill(pid), not a pidfd, but the PID can't be reused before it: std has not reaped the child yet, so the PID stays a zombie and can't be handed out again. That only fails if other code reaps it first. I checked: the proxy crate and the infraternal agent have nowaitpid(-1)and noSIGCHLD=SIG_IGN. The onlywaitpidcalls (serverembedded.rs, and the shim, which is a separate process) name a specific PID. A pidfd isn't needed. If std's pidfd spawn becomes stable, it would make this robust against future reapers.read_direntries that are directories. cgroupfs has no symlinks,DirEntry::file_typedoesn't follow links anyway, and cgroup v2 refuses rename, so the walk can't leave the leaf's subtree. Reversing the breadth-first order removes every child before its parent. The cap is checked before each push, so the walk fails closed on the 4097th entry. The cost is bounded: at most 4096 paths and 4097read_dircalls.CgroupRootlesskind, chosen by the effective uid. Its text no longer says "stops everything", and it states the escape. The rootful text is unchanged.check_basecompares with/sys/fs/cgroupbefore it looks forcgroup.kill, so the reason is now right.for_reapinguses only/proc/self/cgroup(through the existingown_cgroupvalidation) plus the recorded base (valid_recorded_base). It never creates leaves. It touches only directories whose names start with this supervisor's ownproxy-<dev>-<ino>-tag, so the J172 subtree argument still holds.proxy-{dev:x}-{ino:x}-. Hex has no-, and the tag ends with-, so no tag is a prefix of another (proxy-a-b-vsproxy-a-bc-vsproxy-ab-…). The name is never parsed, only compared withstarts_with. Leaves with the old inode-only names aren't matched, but that format never shipped.Non-blocking P3: a rootless child can block its own restarts with the 4096 cap
A compromised rootless child owns its leaf. It can make more than 4096 nested cgroups, or, after it moves out (P2-1), keep running
mkdirinside the leaf while the stop runs. Either waykill_and_removereturns an error andadopt_orphansrefuses every later start until someone cleans up by hand. This is fail-closed. It only affects that user's own proxy, and it needs the compromised rootless Traefik that the design note already accepts. A possible follow-up: writecgroup.max.descendantson the leaf when it is created, so the kernel enforces the limit.Reproduction (the branch's own tests at head,
cgroup_containment,INFRATERNAL_REQUIRE_PROXY_CGROUP=1)Rootless, dev-02 (
systemd-run --user -p Delegate=yes, kernel 6.8, systemd 255):9 passed; 0 failedRoot, compute01 (
systemd-run -p Delegate=yes -p KillMode=process):9 passed; 0 failedRoot, undelegated unit (P3-4 fallback path):
leftover: containment=fallback survivors=[] leaf left=false,1 passed.I didn't repeat red: J164's red runs (77584) show each test failing against the code before the fixes.
Cleanup: 0
proxy-*cgroups on either VM, and no test processes left. I removed the scratch targets, the test binaries and the~/worktrees/j177worktrees. Logs remain at dev-02~/j177/user-run.logand compute01/root/j177/root-run.log. The branch is unchanged.Gate: I changed no code in this review, so the gate doesn't apply. J164 still owes
verify fastonce it has rebased onto dev after J168 merges (see 77584).J164 gate:
scripts/verify faston compute01, greenHead
e7030b6991d3352f1d9be93c85bbce41ffb7f8d5:proxy-cgroup-j164rebased ontoorigin/dev, which includes J168 (4fd5adf0c). The 11 commits replayed cleanly, with no conflicts. Pushed with a lease onbc6e5880a. Not merged, not deployed. J177 is re-reviewing the six fixes.Log: compute01
~/j164-fast2.log. Excerpt, verbatim:Every row passes, including
test. Theremote_clientfailures from the earlier runs are gone with J168.Size: the gate's
+979936(x86) and+815872(aarch64) are against its stale baseline, not this slice. My measurement of this slice against its base was +19,232 B on x86, at31fabf153. The J172 fixes add a few hundred lines of logic and no dependency; I didn't build dev statically again to remeasure.