Run the app as an unprivileged user and sign file links #8

Merged
jacensekai merged 1 commit from hardening into master 2026-10-13 22:02:47 -04:00
Owner

The app container ran everything as root, and workspace download links
put the access token in the URL, where it could end up in logs, browser
history and Referer headers.

A new entrypoint (scripts/docker-entrypoint.sh) starts as root only to
hand /data to the app user (uid 1000, or LUMINA_UID/LUMINA_GID), then
setprivs to it before starting tini, so nothing in the container stays
root. Compose adds no-new-privileges. dind's client key is 0644, so the
app can still reach the daemon.

Root never walks the workspace on a normal start: a sandbox could swap
a directory for a symlink mid-walk and redirect the chown. It does so
once, on the first start after an upgrade. Shell commands now run as
the app's uid so the file tools can edit what they write; a new
as_root flag covers package installs, and root-owned files in
/workspace are handed back from inside the sandbox (where symlinks only
reach the sandbox's own files) when a sandbox is created and after each
as_root command. When the app itself runs as root, nothing changes.

The API no longer accepts the token as a query parameter. Images and
links use URLs from POST /api/workspace/links: HMAC-signed for one path,
expiring after 60-70 minutes (rounded so the browser can cache them),
with a key derived from the token so changing it revokes every link.
The UI's useFileUrl() batches, caches and refreshes them.

The smoke test's MCP check had hung since tool approvals made MCP tools
ask by default; it now allows its test server. It also checks the app
runs as uid 1000, that as_root files stay editable, and that a
root-owned volume is handed over on restart.

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

The app container ran everything as root, and workspace download links put the access token in the URL, where it could end up in logs, browser history and Referer headers. A new entrypoint (scripts/docker-entrypoint.sh) starts as root only to hand /data to the app user (uid 1000, or LUMINA_UID/LUMINA_GID), then setprivs to it before starting tini, so nothing in the container stays root. Compose adds no-new-privileges. dind's client key is 0644, so the app can still reach the daemon. Root never walks the workspace on a normal start: a sandbox could swap a directory for a symlink mid-walk and redirect the chown. It does so once, on the first start after an upgrade. Shell commands now run as the app's uid so the file tools can edit what they write; a new `as_root` flag covers package installs, and root-owned files in /workspace are handed back from inside the sandbox (where symlinks only reach the sandbox's own files) when a sandbox is created and after each as_root command. When the app itself runs as root, nothing changes. The API no longer accepts the token as a query parameter. Images and links use URLs from POST /api/workspace/links: HMAC-signed for one path, expiring after 60-70 minutes (rounded so the browser can cache them), with a key derived from the token so changing it revokes every link. The UI's useFileUrl() batches, caches and refreshes them. The smoke test's MCP check had hung since tool approvals made MCP tools ask by default; it now allows its test server. It also checks the app runs as uid 1000, that as_root files stay editable, and that a root-owned volume is handed over on restart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run the app as an unprivileged user and sign file links
Some checks failed
CI / Typecheck, build and test (pull_request) Successful in 20s
CI / End-to-end (Playwright) (pull_request) Successful in 1m3s
CI / Docker images and sandbox (pull_request) Failing after 11s
CI / Compose stack smoke test (pull_request) Failing after 6s
ae9ab41917
The app container ran everything as root, and workspace download links
put the access token in the URL, where it could end up in logs, browser
history and Referer headers.

A new entrypoint (scripts/docker-entrypoint.sh) starts as root only to
hand /data to the app user (uid 1000, or LUMINA_UID/LUMINA_GID), then
setprivs to it before starting tini, so nothing in the container stays
root. Compose adds no-new-privileges. dind's client key is 0644, so the
app can still reach the daemon.

Root never walks the workspace on a normal start: a sandbox could swap
a directory for a symlink mid-walk and redirect the chown. It does so
once, on the first start after an upgrade. Shell commands now run as
the app's uid so the file tools can edit what they write; a new
`as_root` flag covers package installs, and root-owned files in
/workspace are handed back from inside the sandbox (where symlinks only
reach the sandbox's own files) when a sandbox is created and after each
as_root command. When the app itself runs as root, nothing changes.

The API no longer accepts the token as a query parameter. Images and
links use URLs from POST /api/workspace/links: HMAC-signed for one path,
expiring after 60-70 minutes (rounded so the browser can cache them),
with a key derived from the token so changing it revokes every link.
The UI's useFileUrl() batches, caches and refreshes them.

The smoke test's MCP check had hung since tool approvals made MCP tools
ask by default; it now allows its test server. It also checks the app
runs as uid 1000, that as_root files stay editable, and that a
root-owned volume is handed over on restart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
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
jacensekai/lumina!8
No description provided.