Release 0.10.0 — an owner-only app can keep one door open for its own API - #417
Conversation
…how (#414) The per-app Logs tab was empty on every real box, for every app, since it shipped: it opened the stream and waited forever. host-agent-real tails an app by running `journalctl CONTAINER_NAME=<container> -f`, and CONTAINER_NAME is a field only Docker's journald log driver sets. LOGGING.md calls that driver switch "the single biggest configuration decision", but no image ever wrote /etc/docker/daemon.json, so both real profiles ran Docker's json-file default and the match returned nothing. Two blind spots hid it, both the same mistake: the inner loop's fake host-agent reads `docker logs -f`, and the cloud boot proof greps the brain through `docker logs`. Both work on any driver, so nothing exercised the query the feature depends on. Ship the driver on both images, disable journald's per-unit rate limit for dockerd (every container now shares docker.service's one bucket, so a chatty container would silently starve the rest), and assert in the boot proof that `journalctl CONTAINER_NAME=malmo-brain` returns lines -- not via docker logs, since reading it that way is why this shipped. The journal is still volatile; persistence is the next entry.
* Let an owner-only app keep one door open for its own API (#415) Hosted exposure was whole-app and binary, so an app that pairs a token-authed API with a session-authed UI had to go fully public for its SDK to work — which dropped the box login in front of the UI too. A manifest may now declare `access.public_paths`. A restricted app's route becomes one Caddy subroute: the declared paths proxy straight to the app, everything else stays gated. One route and one @id, so the insert-at-0 ordering against the catch-all is unchanged, and the proxy handler is built once and used on both branches so the per-cookie strip cannot land on one and not the other. The identity headers are now scrubbed on every hosted app route, not only where the gate runs. The gate does not run on a public path, so nothing there would overwrite a caller-supplied X-Malmo-User; the same hole already existed for a fully public app. The dashboard names the open paths instead of claiming a bare "Only me", reading them from the instance's own manifest copy so the label cannot drift from the route. The hosted lane's access boot carries the proof through real Caddy, including a bypass table that must stay gated. * Carry the declared paths on the exposure echo too, not just the detail GET The access label is exposure plus declared public paths together, so the toggle's echo carrying only the exposure would render "Only me" for an app with open paths until the next refetch. Both responses now go through one withPublicPaths helper. * Fix the bypass table's wrong expectation, and the glob that hid its failure Two things the first cloud run found. //v1/ never reaches the gate: Caddy collapses the duplicate slash and 301s to the normalized path before matching, so the app is not reached and the expectation was wrong, not the probe. The table now asserts the claim that matters — an undeclared path never reaches the app upstream anonymously — and allows the merge-redirect shape for that entry. Worse, that failure was reported green. run-cloud-tests.sh matched the guest verdict with a *PASS* glob and the reason read 'PATH GATE BYPASS', which contains PASS, so a red access boot printed 'boot access OK'. The verdict is now matched exactly. Any failure text containing bypass or password would have done this to any boot in this lane.
Confidence Score: 3/5The PR should not merge until failed exposure updates can no longer leave a supposedly restricted hosted app publicly reachable. The exposure update stores the new owner-only policy before a newly introduced manifest read, so that read can fail before Caddy receives the route containing the forward-auth gate. Files Needing Attention: internal/lifecycle/lifecycle.go
|
| Filename | Overview |
|---|---|
| internal/lifecycle/lifecycle.go | Integrates manifest public paths and identity-header scrubbing into every hosted route, but exposure updates can commit before a new manifest-read failure prevents route reconstruction. |
| internal/caddy/caddy.go | Builds a scrub-first subroute with direct public-path proxying and a gated fallback while retaining forward-auth cookie stripping. |
| internal/manifest/manifest.go | Adds conservative validation for exact and trailing-wildcard public paths, rejecting ambiguous normalization and whole-app patterns. |
| internal/api/api.go | Adds best-effort instance-manifest enrichment for public paths on detail responses. |
| internal/api/appexposure.go | Returns public-path enrichment with exposure mutation responses, while relying on lifecycle to keep route policy synchronized. |
| web-ui/src/views/settings/InstalledAppDetailSection.vue | Discloses manifest-declared anonymous paths in the restricted-access summary. |
| dev/cloud/mkosi.extra/etc/docker/daemon.json | Switches Docker’s default container logging driver to journald. |
| dev/cloud/mkosi.extra/etc/systemd/system/docker.service.d/10-malmo-logging.conf | Disables dockerd’s shared journald rate limit to prevent one container from silently starving other container logs. |
Comments Outside Diff (1)
-
internal/lifecycle/lifecycle.go, line 1113-1115 (link)Exposure state diverges from route
When a running hosted app’s instance manifest is unreadable during a public-to-restricted change,
SetExposurepersistsrestrictedbefore returning from the failed manifest load, soAddRoutenever reinstalls the forward-auth gate and the app remains anonymously reachable while the API reports it as owner-only.How this was verified: The changed path commits the exposure at lines 1105–1108, while the fallible manifest read at lines 1113–1115 precedes the only subsequent
AddRoutecall.Knowledge Base Used:
Reviews (1): Last reviewed commit: "Merge main into release/0.10.0, keeping ..." | Re-trigger Greptile
Two changes since 0.9.0.
Path-scoped app exposure (#415, PR #416)
Hosted exposure was whole-app and binary, which left no room for the app that pairs a token-authed API with a session-authed UI: letting an external SDK reach the API meant making the whole app public, which dropped the box login in front of the UI too. Langfuse ships with exactly that limitation; Laminar is worse, because its self-hosted UI signs in any email with no password.
A manifest can now declare
access.public_paths(APP_MANIFEST.md# E2). Arestrictedapp's route becomes one Caddysubroute— the declared paths proxy straight to the app, everything else stays gated — so the app keeps the box login on its UI forever while its API answers anonymously.Two things went wider than the feature itself:
X-Malmo-Userwhile owner-only kept trusting it the moment the owner flipped the toggle to Public. That hole is closed for every hosted app, not just the ones using the new field.Also fixed in the same PR: the hosted boot lane matched its guest verdict with a
*PASS*glob, so any assertion failing with bypass, password or passphrase in its message was read as a pass. A genuinely red access boot printedboot access OKand exited 0. The verdict is now matched exactly — this was latent across every boot in that lane, not just the new assertions.Container logs reach journald (#414)
The dashboard's per-app Logs tab was empty on every real box, for every app, since the tail shipped — no image ever wrote
/etc/docker/daemon.json, so both profiles ran Docker'sjson-filedefault while host-agent queriedjournalctl CONTAINER_NAME=…, a field only the journald driver sets.Verification
The hosted boot proof is green on all five boots (
unseeded seeded bios access update) at the merged tree, with theaccessboot now also proving the new carve-out through real Caddy: declared paths answer with no session, a forgedX-Malmo-Usernever reaches the app on either branch while the vouched one still does,malmo_forward_authis stripped on both branches, and an undeclared-path table (prefix footgun, traversal, encoded traversal, double slash, case variant) never reaches the app upstream.Note for whoever reads VERSION on dev
devstill carries0.6.0— release bumps land onmainvia the release branch and have never been merged back, so dev builds stamp a version three releases old. Pre-existing drift, not introduced here, but it makes a dev build'smalmo 0.6.0stamp misleading.