Skip to content

fix: check the ports the stack publishes in the rootless guard - #7313

Merged
otavio merged 1 commit into
masterfrom
fix/rootless-guard-published-ports
Oct 3, 2026
Merged

otavio merged 1 commit into
masterfrom
fix/rootless-guard-published-ports

Conversation

@otavio

@otavio otavio commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

Summary

Under rootless Docker, the guard from #7309 aborted every wrapper command in a worktree instance with "rootless Docker cannot publish port 80". The instance publishes nothing on port 80. compose.instance.yml resets the gateway's ports because Traefik serves it, and only SSH is published, on 2200+N. The guard took the lowest of SHELLHUB_HTTP_PORT, which defaults to 80, and SHELLHUB_SSH_PORT, without asking Compose what it actually publishes.

 lowest_published_port
-  min(SHELLHUB_HTTP_PORT or 80, SHELLHUB_SSH_PORT or 22)
+  lowest "published:" in `docker compose config`, with the files, profiles and overrides the wrapper selected
+  nothing published, so nothing to check

The check moves below export COMPOSE_FILE, because it needs the files the wrapper selected. The socket detection stays where it was.

On NixOS, rootlesskit lives inside the docker package and is not on PATH. So getcap "" failed, the guard read the failure as a missing capability, and it printed a setcap command for an empty path. A missing rootlesskit now warns that the guard cannot tell, as a missing getcap already did. The lookup goes through ROOTLESSKIT when it is set; an empty value means missing.

Evidence

  • Before: on a NixOS host with rootless Docker, in a T3 worktree instance, DOCKER_HOST=unix:///run/user/1000/docker.sock ./bin/docker-compose config --services exited with 🚫 ERROR: rootless Docker cannot publish port 80. docker compose config shows the instance publishes only 2210.
    After: the same command lists the services. With feat: run the testcontainers and e2e suites in containers #7306 on top, on the same rootless daemon, through the wrapper:
    • run test go test -run '^TestLoginWithoutANamespace$' in tests/ passes
    • ui/scripts/e2e.sh test auth.spec.ts passes all 8 specs
  • bats tests/compose/ reports 46 ok, including with a rootlesskit in /usr/bin. The stub's docker compose config prints the ports each case names in STUB_PUBLISHED, shaped as Compose writes them, with a bind address on its own host_ip: line. Four cases cover the changed behaviour:
    • an unpublished privileged port is ignored
    • a stack that publishes nothing passes
    • a missing rootlesskit warns instead of aborting
    • the advice names SHELLHUB_HTTP_PORT, SHELLHUB_HTTPS_PORT and SHELLHUB_SSH_PORT

Merge Danger

Door: two-way

Blast Radius: rootless dev stacks

The guard now runs docker compose config once per wrapper call, but only in development and only on a rootless daemon. A stack that does publish a privileged port still aborts with the same setcap advice.

@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Claude finished @otavio's task in 3m 38s —— View job


Code Review Complete

Reviewed 3 files. 3 inline issues posted. Standards: 0 violations, 1 judgement call. Spec: 0. Correctness: 2.

Standards

Judgement call (Duplicated Code): the test stub for compose config (tests/compose/helpers.bash:34-38) repeats the port logic that was removed from the wrapper. As a result, the bind-address case tests the stub instead of the wrapper.

Correctness

Worst: the new "a rootlesskit off PATH warns instead of aborting" case fails on any host with a real rootlesskit on PATH, which is exactly a rootless-Docker developer's machine. Removing the stub binary still leaves the host's PATH behind it (tests/compose/helpers.bash:60). Separately, under auto-SSL the abort advice at bin/docker-compose:134 now leaves out SHELLHUB_HTTPS_PORT.

Spec

The Spec axis did not run because the PR references no issue to review against.

Additional notes

No cap dropped any findings. Two low-value judgement calls were filtered out:

  • exit status 127 now covers both "no getcap" and "no rootlesskit"
  • the "publishes nothing" case asserts only the exit status

To request another review round, comment /review.

Comment thread tests/compose/helpers.bash Outdated
Comment thread bin/docker-compose Outdated
Comment thread tests/compose/helpers.bash Outdated
@otavio
otavio force-pushed the fix/rootless-guard-published-ports branch 2 times, most recently from 3c7bc23 to 19c9069 Compare October 3, 2026 00:01
@otavio

otavio commented Oct 3, 2026

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Claude finished @otavio's task in 6m 30s —— View job


Code Review Complete

Reviewed 3 files. 2 inline issues posted. Standards: 0 violations, 1 judgement call. Spec: 0. Correctness: 1.

Standards

Judgement call (Mysterious Name): "the ssh port counts too" (tests/compose/wrapper.bats:138) and "the https port counts too" (:152) now pin which port is lowest, not anything about SSH or HTTPS.

Correctness

Last round's STUB_NO_ROOTLESSKIT fix (tests/compose/helpers.bash:64-70) drops every PATH directory that holds a rootlesskit. With the Debian/Ubuntu package that directory is /usr/bin, and on a usrmerged system /bin too. The wrapper then fails at dirname before it reaches the guard, so the "rootlesskit off PATH" case still fails on a packaged rootless-Docker host. Fix this →

Spec

The Spec axis did not run because the PR references no issue to review against.

Additional notes

No cap dropped any findings. Four low-value judgement calls were filtered out:

  • splitting the guard into a function
  • advice wording when an override file publishes the low port
  • the "unpublished privileged port" case passing SHELLHUB_HTTP_PORT=80
  • a stale PR description, which still says the stub renders from env files and lists "three new cases" (there are now four)

To request another review round, comment /review.

Comment thread tests/compose/helpers.bash Outdated
Comment thread tests/compose/wrapper.bats Outdated
@otavio
otavio force-pushed the fix/rootless-guard-published-ports branch from 19c9069 to f5e9723 Compare October 3, 2026 00:18
@otavio

otavio commented Oct 3, 2026

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Claude finished @otavio's task in 3m 32s —— View job


Code Review Complete

Reviewed 3 files. 1 inline issue posted. Standards: 0 violations, 1 judgement call. Spec: 0. Correctness: 0.

Standards

Judgement call (test coverage, tdd): the stub's compose config branch (tests/compose/helpers.bash:29-33) prints its ports whatever COMPOSE_FILE holds. So nothing pins the guard below export COMPOSE_FILE, and moving it back above the exports would leave every case passing.

Spec

The Spec axis did not run because the PR references no issue to review against.

Additional notes

The last round's fixes hold up. ROOTLESSKIT keeps PATH intact, the stub writes host_ip: on its own line, and the test names describe what each case pins. Every variable the compose files interpolate is exported before the guard runs.

No cap dropped any findings. These were filtered out:

  • Two judgement calls:
    • A failed docker compose config lets the guard pass silently. The exec that follows fails on the same config anyway.
    • The guard runs a config render on every rootless dev call. The PR description already accepts that cost.
  • Two correctness candidates below the bar:
    • Global -f/--env-file flags passed through "$@" don't reach the guard's config call.
    • A host port range ("80-90:80") doesn't match the regex. No compose file in the repo uses one.
  • A repeat: a note on the bind-address case, which was rewritten the way the previous round suggested.

To request another review round, comment /review.

Comment thread tests/compose/helpers.bash
The guard took the lowest of SHELLHUB_HTTP_PORT, default 80, and SHELLHUB_SSH_PORT, whether the
stack publishes them or not. A worktree instance resets the gateway's ports in compose.instance.yml,
since Traefik serves it, so it publishes only SSH on 2200+N; under rootless every wrapper command
there still aborted with "cannot publish port 80", including runs that publish nothing at all. The
guard now reads the lowest published port from docker compose config, with the files, profiles and
overrides the wrapper selected, and passes when nothing below the unprivileged start is published.
That needs COMPOSE_FILE, so the check moves below its export; the socket stays where it was. The
rendered config also carries the auto-SSL overlay's HTTPS port, so the advice names
SHELLHUB_HTTPS_PORT next to the other two.

On NixOS rootlesskit is not on PATH, it sits inside the docker package, so getcap "" failed, the
guard read the failure as a missing capability and printed a setcap command for an empty path. A
missing rootlesskit now warns that the guard cannot tell, as a missing getcap already did. The
lookup goes through ROOTLESSKIT when it is set, an empty value meaning missing, so a test can hide
the binary without hiding the directory around it.

The test stub's compose config now prints the ports each case names in STUB_PUBLISHED, shaped as
Compose writes them, a bind address on its own host_ip line, instead of rebuilding them from the
env files the way the old guard did. The rootlesskit-off-PATH case sets ROOTLESSKIT empty, so a
rootless developer's own /usr/bin/rootlesskit cannot satisfy the lookup, and PATH keeps coreutils.
The stub's config also fails until the wrapper has exported COMPOSE_FILE, so moving the guard back
above that export turns the guard cases red.
@otavio
otavio force-pushed the fix/rootless-guard-published-ports branch from f5e9723 to 2d3a61b Compare October 3, 2026 00:30
@otavio
otavio merged commit ca30fbf into master Oct 3, 2026
30 checks passed
@otavio
otavio deleted the fix/rootless-guard-published-ports branch October 3, 2026 00:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant