Repository navigation
test(devenv): check secrets against another local user outside the isolation - #45
Draft
knittingcode wants to merge 9 commits into
Draft
knittingcode wants to merge 9 commits into
knittingcode wants to merge 9 commits into
Conversation
…e isolation unit.sh runs under scripts/ci/no-docker.sh, where setuid programs such as sudo do not work, so its U-16 check that the user nobody cannot read a secret only printed INFO, in CI as well. tests/devenv/cross-user.sh now owns that check. The checkout's own bin/devenv generates the secrets under no-docker.sh in a new directory below /tmp; outside the isolation the script checks file type, owner and modes as data and then runs only /bin/ls and /bin/cat as nobody on fixed paths. A control file in a 0755 directory next to the secrets directory must be readable by nobody, so a pass cannot come from an unreachable path (the earlier check ran below a 0700 temporary directory). The script refuses arguments, DEVENV, GUARD, ADAPTED, a run inside no-docker.sh and local changes to the files it runs. Without passwordless sudo it skips with INFO, except in CI, where that fails.
tests/devenv/cross-user.sh runs as its own step after the allocator unit tests, on the fresh checkout of the job, and ShellCheck covers it.
State that tests/devenv/cross-user.sh, not unit.sh, owns the check that another local user cannot read a generated secret, how it keeps code under test out of its privileged part, its control and when it refuses or skips.
The negative checks CU-03 and CU-04 passed whenever the command as nobody failed, including when the secrets were never generated (CU-01 failed) or when sudo itself failed. They now count only if CU-01 and the control CU-02 passed and the command failed with 'Permission denied'; otherwise the result is a failure. Commands as nobody run through /usr/bin/env -i, so no variable kept by sudo's env_keep reaches them and their messages are in the C locale. A failed control reports its error and the mode of every parent directory, the most likely first-run failure on a CI runner. The skip line without passwordless sudo says that CU-01 to CU-04 were not run, and CU-00 in CI includes the error of sudo.
…tory The isolated step runs as the caller and is given the temporary directory, so it can replace anything in it, for example with a symbolic link to a file under $HOME that the macOS sandbox would not let it write. Outside the isolation, the script then appended every result to the tally file in that directory (tee -a), wrote the stderr of the commands run as nobody to a file there, and read the error log of the generation from it. The error output of the generation and of the commands run as nobody is now kept in variables (the generation's last 4000 bytes), and the tally is in a new directory made after the isolated step returns, outside the temporary directory. Nothing outside the isolation writes, creates or reads a file there any more; those paths are only tested and stat'ed, and read by nobody. The control CU-02 now also lists its directory as nobody, so CU-03 cannot pass because /bin/ls cannot run as nobody. The documentation says that CU-04 is denied by the 0700 directory (the files are 0444 by design), and that processes left behind by the isolated step are not ended and could still change a result, race the tally directory or keep the step waiting.
The macOS isolation now denies writes to /tmp, except the here-document files of bash 3.2, and refuses a TMPDIR below it. The isolated step of cross-user.sh generated the secrets below /tmp/flowcrm-cross-user.*, so on macOS it would no longer be able to create them. The macOS $TMPDIR is not an alternative: it is 0700, so nobody could not reach anything below it, and the control CU-02 would fail. The throwaway directory is now made below /var/tmp, which is 1777 on Linux and macOS, and given to the isolated step as its TMPDIR: on macOS the sandbox lets it write only there and in /dev. The control still shares every parent directory with the secrets. The result tally stays in the caller's own TMPDIR, which the macOS sandbox now cannot write, so a process left behind by the isolated step can no longer race it there.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Restores the U-16 check that another local user cannot read a generated secret. It now lives in a separate script,
tests/devenv/cross-user.sh, which runs as its own step in the Dev environment job. #43's Docker isolation oftests/devenv/unit.shis unchanged.Stacked on #43 (base
test/fcrm-qa009-mutation-containment).Motivation
#43 runs
unit.shunderscripts/ci/no-docker.sh, where setuid programs such assudodo not work. Because of that, the U-16 checksudo -n -u nobody cat <secret>printed INFO in CI as well, and the unit count fell from 288 to 287. Addingsudoback to the isolated suite would give mutable code a privileged step. So the check is split off:The earlier check also ran below a
0700mktemp -ddirectory.nobodycould not reach any file below it, so the check would have passed even with an exposed secrets directory.Implementation
tests/devenv/cross-user.sh(new):DEVENV,GUARD,ADAPTEDorNO_DOCKER_ACTIVEis set;bin/devenv, the script itself,tally.sh,no-docker.shorno-docker-probe.pydiffers fromHEAD.sudo -u nobodyit prints INFO and exits 0. WithCI=trueit fails instead.bin/devenv(a fixed path, not taken from any input) runsensure_secretsunderno-docker.sh. Its runtime directory is a new0755directory below/tmp(not$TMPDIR, which is0700on macOS), and a fakedockerreports no volume.0700, files0444./bin/lson the secrets directory and/bin/caton each of the four fixed secret names, asnobody.nobodymust be able to read a0444file that the script creates in a0755directory next to the secrets directory.tally.sh.tests/devenv/unit.sh: thesudobranch is removed. U-16 prints an INFO line that namescross-user.shas the owner of the check. The unit count stays at 287, now by design.docs/dev-environment.md, "Isolation from Docker" now has a "Cross-user check of the secrets" part, the test list includes the script, and the console example shows it.Testing performed
Local, macOS 26, in a separate worktree:
tests/devenv/unit.sh:TOTAL 287 passed=287 failed=0, exit 0, inside the isolation. It printedINFO U-16 cross-user read check: tests/devenv/cross-user.sh (outside the isolation); permissions checked above.bash -n(bash 5) and/bin/bash -n(3.2.57) pass oncross-user.sh.-xreports nothing ontests/devenv/unit.shandtests/devenv/cross-user.sh..github/workflows/ci.ymlparses with PyYAML and the new step is in the Dev environment job. actionlint is not installed locally, so it was not run.Not run:
tests/devenv/cross-user.shitself, so none of its paths has been executed, including the refusals, the skip, the control and the CU checks; the guard suites;no-docker.test.sh.CI runs only for pull requests that target
main, so no CI ran for this PR. It will run once this PR is retargeted tomainafter #43 merges, and that run is the first execution ofcross-user.sh.Security considerations
git,mktemp,statand[ ]tests. Asnobodyit runs/bin/lsand/bin/cat. Every path is built from amktempdirectory and constant names.sudois used only to lower privileges tonobody. A symbolic link planted by the isolated code is followed only bynobody, so at worst it gives a false result. It never gives access beyond whatnobodyalready has.HEADcomparison.git diffruns withcore.fsmonitor=false,--no-ext-diffand--no-textconv.Known limitations
no-docker.sh, asunit.shalready does. Locally on macOS the trusted part needs passwordless sudo tonobody, which is usually not set up, so the script skips there.bin/devenvis not covered until it is added to that list.tests/app/stack.shST-34 has the samesudo -n -u nobody catcheck, and its checkout copy appears to sit below a0700mktemp -dwork directory. If so, that check passes regardless of the secrets directory mode. It is not changed here.