Prototype FreeBSD build user isolation
This commit is contained in:
@@ -896,3 +896,79 @@ Next recommended step:
|
||||
2. then establish a `/frx/store`-based store-management prototype covering permissions, package readability, and garbage-collection behavior
|
||||
3. continue carrying the separate Guix checkout runtime blocker:
|
||||
- investigate the `leave-on-EPIPE` failure in `./pre-inst-env guix --version`
|
||||
|
||||
## 2026-04-01 — Phase 2.2 completed: privilege dropping and concurrent build-user isolation validated
|
||||
|
||||
Completed work:
|
||||
|
||||
- added a C helper implementing the core daemon-side privilege drop mechanics:
|
||||
- `tests/daemon/freebsd-build-user-helper.c`
|
||||
- added a harness that combines that helper with the new jail model and runs two jobs concurrently:
|
||||
- `tests/daemon/run-freebsd-privilege-drop-prototype.sh`
|
||||
- wrote the Phase 2.2 report:
|
||||
- `docs/reports/phase2-freebsd-privilege-drop.md`
|
||||
- ran the concurrent build-user prototype successfully and captured metadata under:
|
||||
- `/tmp/freebsd-privdrop-metadata.txt`
|
||||
|
||||
Important findings:
|
||||
|
||||
- a root-launched FreeBSD helper can successfully perform the expected daemon-side transition:
|
||||
- `setgroups`
|
||||
- `setgid`
|
||||
- `setuid`
|
||||
into a dedicated build identity
|
||||
- once dropped, the helper cannot regain root with `setuid(0)`:
|
||||
- `Operation not permitted`
|
||||
- each build job can create files in its own writable directory and those files end up owned by the dropped build UID/GID rather than by root
|
||||
- two concurrent jobs using distinct numeric build identities succeeded with:
|
||||
- job 1 UID/GID `35001:35001`
|
||||
- job 2 UID/GID `35002:35002`
|
||||
- host-side result files were observed with the matching ownership and restrictive permissions:
|
||||
- `0600`
|
||||
- the two jobs were deliberately held for two seconds each and the measured wall-clock elapsed time was also about two seconds, demonstrating actual concurrent execution rather than serialized execution
|
||||
- two complementary denial modes were validated at the same time:
|
||||
- peer build files mounted but blocked by permissions: `Permission denied`
|
||||
- host path not mounted into the jail at all: `No such file or directory`
|
||||
- the dropped build user also could not:
|
||||
- create files in a protected root-owned directory
|
||||
- `chown` its own output back to root
|
||||
|
||||
Current assessment:
|
||||
|
||||
- Phase 2.2 is now satisfied on the current FreeBSD prototype track
|
||||
- the combined jail + build-user model now has practical validation for the most important security properties required by a future FreeBSD Guix daemon:
|
||||
- root-controlled setup
|
||||
- permanent drop to build credentials
|
||||
- per-build writable areas
|
||||
- cross-build isolation
|
||||
- concurrent execution under distinct identities
|
||||
- the remaining Phase 2 work is now centered on the store itself: permissions, readability, content-addressed layout, and garbage-collection behavior under `/frx/store`
|
||||
|
||||
Recent commits:
|
||||
|
||||
- `e380e88` — `Add FreeBSD Guile verification harness`
|
||||
- `cd721b1` — `Update progress after Guile verification`
|
||||
- `27916cb` — `Diagnose Guile subprocess crash on FreeBSD`
|
||||
- `02f7a7f` — `Validate local Guile fix on FreeBSD`
|
||||
- `4aebea4` — `Add native GNU Hello FreeBSD build harness`
|
||||
- `c944cdb` — `Validate Guix builder phases on FreeBSD`
|
||||
- `0a2e48e` — `Validate GNU which builder phases on FreeBSD`
|
||||
- `245a47d` — `Document gaps to real Guix FreeBSD builds`
|
||||
- `d62e9b0` — `Investigate Guix derivation generation on FreeBSD`
|
||||
- `c0a85ed` — `Build local Guile-GnuTLS on FreeBSD`
|
||||
- `15b9037` — `Build local Guile-Git on FreeBSD`
|
||||
- `47d31e8` — `Build local Guile-JSON on FreeBSD`
|
||||
- `d82195b` — `Advance Guix checkout on FreeBSD`
|
||||
- `9bf3d30` — `Document FreeBSD syscall mapping`
|
||||
- `7621798` — `Prototype FreeBSD jail build isolation`
|
||||
|
||||
Next recommended step:
|
||||
|
||||
1. complete Phase 2.3 by establishing a `/frx/store`-based store prototype with:
|
||||
- correct root/daemon write restrictions
|
||||
- unprivileged read access
|
||||
- content-addressed path naming
|
||||
- garbage-collection behavior
|
||||
2. if possible, use outputs or dependency relationships realistic enough to model how a future FreeBSD Guix daemon would retain referenced store items
|
||||
3. continue carrying the separate Guix checkout runtime blocker:
|
||||
- investigate the `leave-on-EPIPE` failure in `./pre-inst-env guix --version`
|
||||
|
||||
159
docs/reports/phase2-freebsd-privilege-drop.md
Normal file
159
docs/reports/phase2-freebsd-privilege-drop.md
Normal file
@@ -0,0 +1,159 @@
|
||||
# Phase 2.2: FreeBSD privilege dropping and build-user isolation prototype
|
||||
|
||||
Date: 2026-04-01
|
||||
|
||||
## Summary
|
||||
|
||||
This step adds a concrete FreeBSD privilege-dropping prototype that complements the Phase 2.1 jail-isolation work.
|
||||
|
||||
Added files:
|
||||
|
||||
- `tests/daemon/freebsd-build-user-helper.c`
|
||||
- `tests/daemon/run-freebsd-privilege-drop-prototype.sh`
|
||||
|
||||
The prototype demonstrates that a root-launched daemon-side process can:
|
||||
|
||||
- enter a FreeBSD jail build environment
|
||||
- drop permanently to a designated numeric build UID/GID
|
||||
- lose the ability to regain root with `setuid(0)`
|
||||
- write only to its own build directory
|
||||
- fail to read a peer build user's files
|
||||
- fail to access a host path that is not mounted into the jail
|
||||
- fail to create files in a protected root-owned directory
|
||||
- run two builds concurrently under different build identities
|
||||
|
||||
## Prototype design
|
||||
|
||||
### Build-user model
|
||||
|
||||
For this prototype, build users are represented by dedicated numeric IDs rather than pre-created named accounts:
|
||||
|
||||
- job 1: UID/GID `35001`
|
||||
- job 2: UID/GID `35002`
|
||||
|
||||
This is enough to validate the essential daemon-side mechanics:
|
||||
|
||||
- root can allocate a build identity
|
||||
- root can prepare directories owned by that identity
|
||||
- the build process can drop into that identity before executing untrusted work
|
||||
|
||||
For a real FreeBSD Guix daemon, these numeric identities would likely come from a reserved build-user pool, whether or not they are backed by visible passwd entries.
|
||||
|
||||
### Isolation structure
|
||||
|
||||
Each job gets its own jail and its own writable work directory. Inside each jail, the helper sees:
|
||||
|
||||
- `/lib`
|
||||
- `/libexec`
|
||||
- `/tools/freebsd-build-user-helper`
|
||||
- `/work` (owned by that job's UID/GID)
|
||||
- `/peer` (mounted from the other job's work directory, but inaccessible because of permissions)
|
||||
- `/protected` (root-owned and not writable)
|
||||
|
||||
A host-side sentinel file is left outside the jail entirely and is referenced as `/outside-sentinel`; the helper confirms it is not visible.
|
||||
|
||||
### Concurrency check
|
||||
|
||||
Both jailed helper processes are started in parallel and intentionally sleep for two seconds after their checks. The observed elapsed wall-clock time was also two seconds, not four, showing that the prototype was running the two build-user jobs concurrently rather than serially.
|
||||
|
||||
## Run command
|
||||
|
||||
```sh
|
||||
METADATA_OUT=/tmp/freebsd-privdrop-metadata.txt \
|
||||
./tests/daemon/run-freebsd-privilege-drop-prototype.sh
|
||||
```
|
||||
|
||||
## Observed results
|
||||
|
||||
Observed helper output for job 1:
|
||||
|
||||
```text
|
||||
job=job1
|
||||
target.uid=35001
|
||||
target.gid=35001
|
||||
post_drop.euid=35001
|
||||
post_drop.egid=35001
|
||||
drop.regain_root=Operation not permitted
|
||||
access.allowed_read=job1-allowed
|
||||
access.own_output_uid=35001
|
||||
access.own_output_gid=35001
|
||||
access.peer_file=Permission denied
|
||||
access.hidden_file=No such file or directory
|
||||
access.protected_create=Permission denied
|
||||
drop.chown_root=Operation not permitted
|
||||
```
|
||||
|
||||
Observed helper output for job 2 was identical in structure, with UID/GID `35002`.
|
||||
|
||||
Observed host-side result file ownership:
|
||||
|
||||
- job 1 result file: UID/GID `35001:35001`, mode `0600`
|
||||
- job 2 result file: UID/GID `35002:35002`, mode `0600`
|
||||
|
||||
Observed concurrency metadata:
|
||||
|
||||
- configured hold time per job: `2` seconds
|
||||
- measured elapsed wall-clock time: `2` seconds
|
||||
|
||||
Metadata was captured in:
|
||||
|
||||
- `/tmp/freebsd-privdrop-metadata.txt`
|
||||
|
||||
## What this demonstrates for the FreeBSD port
|
||||
|
||||
### 1. Permanent privilege dropping works in the required direction
|
||||
|
||||
The helper was started as root by the harness, then used:
|
||||
|
||||
- `setgroups`
|
||||
- `setgid`
|
||||
- `setuid`
|
||||
|
||||
After the drop, `setuid(0)` failed with `Operation not permitted`, which is the basic property Guix needs from a build-worker launch path.
|
||||
|
||||
### 2. Build-user ownership boundaries behave as expected
|
||||
|
||||
Each job could write its own result file, and the file ended up owned by the dropped build UID/GID rather than root. Each job then failed to read the peer job's file due to permission checks.
|
||||
|
||||
### 3. Jail filesystem containment and credential isolation compose correctly
|
||||
|
||||
Two distinct denial modes were observed:
|
||||
|
||||
- peer work directory mounted but inaccessible: `Permission denied`
|
||||
- host path not mounted into the jail: `No such file or directory`
|
||||
|
||||
This is exactly the combined model the FreeBSD daemon design needs:
|
||||
|
||||
- jail layout restricts visibility
|
||||
- build-user credentials restrict access where visibility alone is not enough
|
||||
|
||||
### 4. Protected daemon-owned paths remain protected after the drop
|
||||
|
||||
The dropped build user could not create files under the root-owned `/protected` directory and could not `chown` its output back to root. This is the essential privilege-separation behavior needed to keep store/daemon-owned paths outside build control.
|
||||
|
||||
## Mapping to Guix-daemon needs
|
||||
|
||||
| Guix-daemon requirement | Prototype result |
|
||||
|---|---|
|
||||
| launch build as root-controlled setup step | yes |
|
||||
| drop to dedicated build UID/GID before untrusted work | yes |
|
||||
| prevent build from regaining root | yes |
|
||||
| keep per-build writable areas separate | yes |
|
||||
| prevent one build from reading another build's files | yes |
|
||||
| combine credential isolation with jail filesystem containment | yes |
|
||||
| support concurrent builds under distinct identities | yes |
|
||||
|
||||
## Conclusion
|
||||
|
||||
Phase 2.2 is satisfied on the current prototype track:
|
||||
|
||||
- a concrete FreeBSD privilege-dropping implementation now exists in C
|
||||
- it integrates cleanly with the jail-first model established in Phase 2.1
|
||||
- it demonstrates the core properties required for a future FreeBSD Guix daemon:
|
||||
- root-controlled setup
|
||||
- permanent drop to build credentials
|
||||
- cross-build isolation
|
||||
- blocked access to protected daemon-owned paths
|
||||
- concurrent build execution under separate identities
|
||||
|
||||
This reduces the remaining Phase 2 uncertainty mainly to store management and lifecycle, not to whether FreeBSD can enforce the basic build-user security model.
|
||||
Reference in New Issue
Block a user