-
Notifications
You must be signed in to change notification settings - Fork 11
fix: refuse to run as root unless --allow-root is given #116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -88,7 +88,23 @@ ARG USER_ID=1000 | |
| ARG GROUP_ID=1000 | ||
| ARG USERNAME=agent | ||
|
|
||
| RUN if id -u ${USER_ID} >/dev/null 2>&1; then \ | ||
| RUN if [ "${USER_ID}" -eq 0 ]; then \ | ||
| # UID 0 (root host with --allow-root, or --build-uid 0): usermod cannot | ||
| # rename root while the build runs as root, so add the agent as a | ||
| # second name for UID 0 instead. | ||
|
Comment on lines
+91
to
+94
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm happy for this PR to address only the root-guard part of #106. This alone is a great UX improvement! I would suggest to move the new UID-0 image and runtime support into a separate PR: the Dockerfile and QEMU account changes, the UID-0 HOME/USER handling, and the build-identity extraction needed for that handling. Those changes introduce separate compatibility questions, including deleting existing /root content and remapping a UID-0 agent. We don't need to solve those here, as the behavior on main is broken anyway in these cases. For this PR, |
||
| if [ "${GROUP_ID}" -eq 0 ]; then \ | ||
| groupadd -o -g 0 ${USERNAME}; \ | ||
| elif getent group ${GROUP_ID} >/dev/null 2>&1; then \ | ||
| groupmod -n ${USERNAME} $(getent group ${GROUP_ID} | cut -d: -f1); \ | ||
| else \ | ||
| groupadd -g ${GROUP_ID} ${USERNAME}; \ | ||
| fi && \ | ||
| useradd -o -m -u 0 -g ${GROUP_ID} -d /home/${USERNAME} -s /bin/bash ${USERNAME} && \ | ||
| # RUN steps take HOME from the first passwd entry for their UID, which | ||
| # is root's, so /root must lead to the agent's home as well. | ||
| rm -rf /root && \ | ||
| ln -s /home/${USERNAME} /root; \ | ||
| elif id -u ${USER_ID} >/dev/null 2>&1; then \ | ||
| # UID exists (e.g. Ubuntu's "ubuntu" at 1000) - rename to our username | ||
| EXISTING_USER=$(id -nu ${USER_ID}); \ | ||
| EXISTING_HOME=$(getent passwd "$EXISTING_USER" | cut -d: -f6); \ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -98,6 +98,7 @@ The restricted network request flow has a separate | |
|
|
||
| ### Orchestration (`internal/app/`) | ||
| - [`internal/app/app.go`](../internal/app/app.go) wires parsing, defaults merging, and command dispatch. | ||
| - [`internal/app/root_guard.go`](../internal/app/root_guard.go) refuses to run as root before any state is written, unless `--allow-root` or `ENCLAVE_ALLOW_ROOT` opts in. | ||
| - [`internal/app/commands.go`](../internal/app/commands.go) routes commands to handlers (run/continue/resume/exec/shell/cleanup/tools/etc). | ||
| - [`internal/app/command_run.go`](../internal/app/command_run.go) drives the run/continue/resume/exec/shell flow and runtime creation. | ||
| - [`internal/app/build.go`](../internal/app/build.go) manages Docker image build/rebuild detection plus prebuild agent update planning and post-build stamp commits. | ||
|
|
@@ -345,13 +346,21 @@ must come from global config (`~/.config/enclave/config.json`) or explicit CLI f | |
| `--cache-from`). Some build controls are intentionally CLI-only and not read | ||
| from config files, including `--rebuild`, `--no-rebuild`, | ||
| `--force-base-image`, `--build-uid`, `--build-gid`, `--runtime-uid-remap`, and | ||
| the buildx cache flags. | ||
| the buildx cache flags. `--allow-root` is likewise never read from config; its | ||
| only alternative is the `ENCLAVE_ALLOW_ROOT` environment variable. | ||
|
|
||
| Runtime image hashes include the effective build UID/GID. Explicit | ||
| `--build-uid` / `--build-gid` values are used when provided; otherwise the host | ||
| UID/GID is included after host resolution. This prevents a loaded shared image | ||
| from being accepted as current when it was built for a different numeric user. | ||
|
|
||
| For UID 0 (a root host with `--allow-root`, or `--build-uid 0`), the Dockerfile | ||
| adds `agent` as a second name for UID 0 instead of renaming `root`, which | ||
| `usermod` refuses while the build runs as root; the QEMU bundle build does the | ||
| same. Lookups by UID return root's passwd entry, which comes first, so the | ||
| Dockerfile also replaces `/root` with a link to `/home/agent` for `RUN` steps, | ||
| and the runtime sets `HOME` and `USER` for the agent in sessions. | ||
|
Comment on lines
+357
to
+362
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would defer this to a follow-up PR and only do the guard and the Building with uid 0 is broken on main anyway and triggering the guard and an understandable error message is a much nicer UX than the current behavior. If someone then uses |
||
|
|
||
| `features` can be set from config or CLI (`--features`). In devcontainer mode, | ||
| the unset default is no enclave features; pass `--features` (or configure | ||
| `features`) to opt in. `--features none` selects zero features explicitly. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -295,6 +295,18 @@ project scope so project defaults cannot weaken a stricter global setting. | |
| linked-worktree gitdir/commondir mounts: project config may strengthen the | ||
| inherited mode (`follow < readonly < none`), but cannot weaken it. | ||
|
|
||
| Root guard note: `--allow-root` is CLI-only and has no config key, so no config | ||
| file can grant it; `ENCLAVE_ALLOW_ROOT=1` is its only alternative. | ||
| `checkRootGuard` in `internal/app/root_guard.go` runs early in `app.Run`, | ||
| before any state is written; tests pin the root check through the | ||
| `runningAsRoot` seam. A root host builds the image for UID 0: the Dockerfile | ||
| and the QEMU bundle build add `agent` as a second name for UID 0, and | ||
| `applyUIDZeroAgentEnv` in `internal/runtime` sets `HOME` and `USER` for the | ||
| agent, because lookups by UID return root's passwd entry. To test that path as | ||
| a regular user, pass `--build-uid 0 --build-gid 0` with `XDG_CONFIG_HOME`, | ||
| `XDG_STATE_HOME`, and `XDG_CACHE_HOME` pointing at a scratch directory: the UID 0 | ||
| agent leaves root-owned files behind. | ||
|
Comment on lines
+302
to
+308
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See my comment in ARCHITECTURE.md |
||
|
|
||
| Config additive note: feature additive directives (`+`/`-`) are applied against | ||
| the implicit default-enabled feature set when `features` is unset. For example, | ||
| `["-node-dev"]` removes that default feature from the implicit set. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -224,6 +224,7 @@ store that holds memory; the other `--keep` kinds do not apply there. See | |
| | `--skills-validation <strict\|agent>` | Validate shared skill frontmatter strictly (default) or leave metadata interpretation to the agent; values are case-insensitive | | ||
| | `--session-monitor` | Run the agent under the managed tmux session (enables `status` snapshots) | | ||
| | `--verbose` | Verbose logging | | ||
| | `--allow-root` | Run as root anyway (accepted by every command; CLI-only). See [Running as root](#running-as-root) | | ||
| | `--playwright-mcp` | Enable Playwright MCP server for browser automation (Claude only) | | ||
|
|
||
| ### Image & Build | ||
|
|
@@ -300,6 +301,12 @@ The menu lists every installed agent. The IDE profiles (`theia`, `theia-next`) a | |
|
|
||
| `--tool` overrides the saved choice for a single run, and a configured `tool` disables the question. Set `"tool": "auto"` to be asked again. | ||
|
|
||
| ## Running as root | ||
|
|
||
| enclave refuses to run as root, including through `sudo`. The agent's container user takes the host UID, so as root the agent runs as UID 0 (named `agent`, a second name for root), which is host root on bind-mounted directories under rootful Docker. Files enclave and the agent write, both in the project and in the stores under the config, state, and cache roots, become root-owned, and later runs as the regular user fail on them (with `sudo -E`, those roots are in the regular user's home). Run enclave as a regular user with access to the Docker socket (see the [requirements](../README.md#requirements)), or use rootless podman with `--backend podman`. | ||
|
|
||
| To run as root anyway, for example in a CI job container, pass `--allow-root` or set `ENCLAVE_ALLOW_ROOT=1`; each such run prints a warning; sessions then run with the agent as UID 0, or as the UID given with `--build-uid`. The opt-in has no config key, so neither global nor project config can grant it. Help and version output work without it. | ||
|
Comment on lines
+306
to
+308
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See my comment in ARCHITECTURE.md |
||
|
|
||
| ## Backend detection | ||
|
|
||
| The default backend is `auto`: enclave uses docker when its CLI is on `PATH`, otherwise podman. A `docker` command that is really the `podman-docker` shim counts as podman, and an explicit `docker`, from `--backend` or the `backend` key, that turns out to be the shim is driven as podman as well, with a notice. When both engines are installed, a command that uses an engine asks once which one to use and saves the answer as `"backend"` in `~/.config/enclave/config.json`. The question is only asked when stdin, stdout, and stderr are terminals and no `--json` or `--yes` was given; otherwise (scripts, captured output, JSON consumers) docker is used and a notice points at that key. Commands that never touch an engine (`tools`, `features`, `config`, `review-target`, `network print`, `network diff`, `devcontainer generate`) neither detect nor ask. When neither engine is found, the engine check reports it. An explicit `--backend` or a configured `backend` disables detection. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -57,6 +57,19 @@ func Run(args []string) int { | |
| return 0 | ||
| } | ||
|
|
||
| // The guard runs before anything writes state (the tool question, asset | ||
| // extraction, stores), so a refused root run leaves no root-owned files. | ||
| // Folding the env opt-in into the options lets validation and per-tool | ||
| // re-resolution see one value. | ||
| parsed.Options.AllowRoot = rootAllowed(parsed.Options.AllowRoot) | ||
| if err := checkRootGuard(parsed.Options.AllowRoot); err != nil { | ||
|
Comment on lines
+60
to
+65
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The guard is still reached after writing paths. Run calls |
||
| if parsed.Action == cli.ActionExtensionManage && parsed.ExtRequest != nil { | ||
| return reportExtensionResults(*parsed.ExtRequest, nil, err) | ||
| } | ||
| logx.Errorf("%v", err) | ||
| return 1 | ||
| } | ||
|
|
||
| projectDir, err := resolveProjectDir() | ||
| if err != nil { | ||
| logx.Errorf("%v", err) | ||
|
|
@@ -84,7 +97,7 @@ func Run(args []string) int { | |
| if parsed.Options.Verbose { | ||
| logx.SetLevel("debug") | ||
| } | ||
| return runUserHostCommand(*parsed.UserCommand, parsed.UserCommandArgs, projectDir, home) | ||
| return runUserHostCommand(*parsed.UserCommand, parsed.UserCommandArgs, projectDir, home, parsed.Options.AllowRoot) | ||
| case usercmd.TargetSession: | ||
| // Session commands run through the standard run pipeline as a | ||
| // shell-style execution; fall through with a rewritten action. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This updates the later installation smoke test, but the earlier build-rpm-fedora job also runs as root. Its 'Verify packaged runtime assets' step calls
scripts/verify-package-assets.sh, which invokes Enclave twice without an override. CI currently fails at that verification step, so test-fedora is skipped. Please pass--allow-rootexplicitly to both Enclave invocations in the verifier and use the flag for the allowed invocation here too. The verifier's negative case must get past the guard and still fail because embedded assets are unavailable; a root-refusal error would test the wrong behavior. Please retain the separate assertion here that running without the override is refused.An out-of scope follow-up issue would be to fix the ci job for fedora. Currently, the fedora ci job runs in a fedora:44 job container and apparently the default user is root there. The follow-up should try setting a non-privileged user in the container.