Bring SUDO_REPLACEMENT analysis into main as architecture guardrail (Sam & Claude)
Cherry-pick of the mac_do/mdo feasibility analysis that lived on origin/multitenant-zai. Lands as documented context, not as an execution path: the doc's own conclusion is "do not replace sudo with mac_do now, finish the hostd migration instead." Why land it now: the queue-fix sequence test exposed that the chat agent hits the "needs root for pkg/freebsd-update audit" wall. The natural temptation when seeing that wall is to reach for a sudo replacement. This doc is the guardrail that says no — the gap is missing hostd operations, not a wrong privilege model. Concrete forward direction the doc supports: - Don't add mac_do/mdo to runtime paths. - Treat "can't inspect updates without root" as a missing-hostd-op signal, not a sudo problem. - Future hostd work should add operations for host package updates, pkg audit, jail package update checks, and freebsd-update fetch status — same pattern as the existing service-status/start/stop authorization flow. mac_do remains interesting for narrow read-only audit operators in a later slice, per section 5.3 of the doc. No embeddings refresh. Skills artifact stays where it is. --- Build: FAIL | Tests: FAIL — 16 failed
This commit is contained in:
parent
8654c4e145
commit
0724a28d68
1 changed files with 183 additions and 0 deletions
183
docs/internal/SUDO_REPLACEMENT.md
Normal file
183
docs/internal/SUDO_REPLACEMENT.md
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
# Sudo Replacement: mac_do / mdo Feasibility Analysis
|
||||
|
||||
**Date:** 01.maj.2026
|
||||
**Author:** Linux Claude agent (research, no runtime validation)
|
||||
**Status:** Analysis complete — recommendation: do not replace, finish hostd migration instead
|
||||
|
||||
---
|
||||
|
||||
## 1. What mac_do Is
|
||||
|
||||
`mac_do` is a FreeBSD Mandatory Access Control (MAC) policy module (base system, FreeBSD 14.2+) that allows unprivileged users to change process credentials according to kernel-enforced rules. The companion userland tool is `mdo(1)`.
|
||||
|
||||
Key properties:
|
||||
|
||||
- **Kernel-enforced**: rules live in sysctl/jail parameters, not in a userland config file parsed by a setuid binary
|
||||
- **No password**: purely role-based, no PAM, no authentication prompt
|
||||
- **Atomic credential change**: uses `setcred(2)` to set all UIDs/GIDs/supplementary groups at once
|
||||
- **Per-jail configuration**: `mac.do.rules` jail parameter for jail-scoped rules
|
||||
- **In base system**: no external package dependency
|
||||
|
||||
### Syntax Overview
|
||||
|
||||
Rules are strings of the form `uid=X>uid=Y` governing which credential transitions are allowed:
|
||||
|
||||
```
|
||||
# Allow UID 1001 to become root
|
||||
uid=1001>uid=0
|
||||
|
||||
# Allow UID 1001 to become UID 1002, keeping current supplementary groups
|
||||
uid=1001>uid=1002,gid=1002,+gid=.
|
||||
|
||||
# Allow members of group 10001 to become root
|
||||
gid=10001>uid=0
|
||||
```
|
||||
|
||||
Loaded via sysctl (`security.mac.do.rules`) or jail parameter (`mac.do.rules`).
|
||||
|
||||
### mdo(1) Usage
|
||||
|
||||
```
|
||||
mdo -u root bastille cmd dbjail "pkg install -y postgresql16-server"
|
||||
mdo -u postgres psql -d clawdie_brain -c "SELECT 1"
|
||||
mdo -k -s +wheel /usr/bin/id
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Current sudo Usage Audit
|
||||
|
||||
Full codebase audit found sudo referenced in **~10 runtime categories** across **~100+ files**.
|
||||
|
||||
### 2.1 Runtime Code (Programmatic Execution)
|
||||
|
||||
These files actually execute `sudo` at runtime:
|
||||
|
||||
| Category | Command Pattern | Files |
|
||||
| ---------------------- | ----------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
||||
| Bastille jail mgmt | `spawnSync('sudo', ['bastille', 'cmd', ...])` | `src/jail-exec-runner.ts:162,239` |
|
||||
| Bastille listing | `shellExec('sudo bastille list all')` | `src/startup-report.ts:801` |
|
||||
| Package queries | `shellExec('sudo pkg version ...')` | `src/startup-report.ts:803`, `src/telegram-commands.ts:1264`, `src/channels/telegram.ts:255` |
|
||||
| ZFS snapshots | `execFileSync('sudo', ['zfs', 'snapshot', ...])` | `setup/install.ts:305` |
|
||||
| PostgreSQL user switch | `execFileSync('sudo', ['-n', '-u', 'postgres', ...])` | `setup/skills-memory.ts:233` |
|
||||
| Service reload | `sudo service nginx reload` | `scripts/docs-sync.cron.sh:272` |
|
||||
| Jail destroy | `sudo bastille stop/destroy` | `scripts/destroy-jails.sh:47,59` |
|
||||
| Heartbeat | `sudo timeout 10 bastille list` | `scripts/heartbeat.sh:48` |
|
||||
|
||||
### 2.2 Setup / Install Time
|
||||
|
||||
| File | Usage |
|
||||
| ----------------------------- | ---------------------------------------------------------------------- |
|
||||
| `setup/environment.ts` | Checks `commandExists('sudo')`, constructs `sudo pkg install` commands |
|
||||
| `setup/install.ts` | `useSudo` flag, `sudo zfs snapshot` for non-root installs |
|
||||
| `setup.sh` | Checks `command -v sudo`, sets `missing_no_sudo` status |
|
||||
| `setup/bastille-helpers.ts` | Creates `/usr/local/etc/sudoers.d/<user>-bastille` NOPASSWD entry |
|
||||
| `setup/service.ts` | Reads `SUDO_USER`/`SUDO_UID`/`SUDO_GID` env vars |
|
||||
| `setup/hostd.ts` | Reads `SUDO_USER`/`SUDO_UID`/`SUDO_GID` for file ownership |
|
||||
| `setup/profile.ts` | Reads `SUDO_UID`/`SUDO_GID` for file ownership |
|
||||
| `setup/verify-agent-jails.ts` | Verifies sudoers file exists |
|
||||
|
||||
### 2.3 Sudoers Configuration
|
||||
|
||||
- `setup/bastille-helpers.ts:156-181` — Creates `/usr/local/etc/sudoers.d/<agentUser>-bastille` with NOPASSWD entry for bastille, validates with `visudo -cf`
|
||||
- `setup/verify-agent-jails.ts:211-214` — Checks sudoers drop-in exists
|
||||
- AGENTS.md documents the sudoers policy (read `/usr/local/etc/sudoers`, confirm `@includedir`, validate with `visudo -c`)
|
||||
|
||||
### 2.4 Documentation / Instruction References
|
||||
|
||||
~40+ files across skills, public docs, internal docs, CMS content, and HTML pages reference `sudo` in operator-facing instructions. Major concentrations:
|
||||
|
||||
- **Skill files**: debug, nginx, strapi, zfs-scrub, setup, freebsd-admin, postgres-memory, docs-deployment, update, agent-setup, docs-localization-pipeline
|
||||
- **Public docs**: install guide, fresh-install checklist, controlplane install, security, db disaster recovery
|
||||
- **Internal docs**: STRAPI-FREEBSD-SETUP, LOCAL-LLM, POSTGRES-MEMORY, CLEAN-RESET-PI-TUI, AGENT-HARNESS-V2
|
||||
- **Bootstrap CMS**: ~15+ content markdown files in English and Slovenian
|
||||
- **HTML**: docs site, changelog, landing page guides
|
||||
|
||||
### 2.5 Safety Harness
|
||||
|
||||
`.agent/harness/safety.yaml` defines a `confirm-sudo` rule requiring confirmation for sudo commands — this would need rewriting for `mdo`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Critical Architectural Context: hostd
|
||||
|
||||
The codebase **already has** a root-privileged daemon (`hostd`) at `src/hostd/`. It runs as root and the non-root agent communicates via Unix socket. The CHANGELOG explicitly states:
|
||||
|
||||
> "Agent user calls `hostd(op, params)` from `src/hostd/client.ts`; never needs sudo at runtime."
|
||||
|
||||
hostd already calls `bastille`, `zfs`, `zpool`, `pfctl`, `service`, `pkg`, `sysrc`, `sanoid`, `shutdown` directly via `spawnSync` — no sudo needed.
|
||||
|
||||
The **only significant runtime sudo holdout** is `jail-exec-runner.ts` which still does `spawnSync('sudo', ['bastille', 'cmd', ...])` for agent jail execution.
|
||||
|
||||
---
|
||||
|
||||
## 4. Comparative Analysis
|
||||
|
||||
### 4.1 Where mac_do is Stronger Than sudo
|
||||
|
||||
| Aspect | sudo | mac_do |
|
||||
| ------------------- | --------------------------------------- | ----------------------------------------------- |
|
||||
| Trust model | Trusts setuid binary + sudoers parser | Kernel-enforced, no setuid binary to compromise |
|
||||
| Credential change | Piecewise (setuid → setgid → setgroups) | Atomic via `setcred(2)` |
|
||||
| Config location | `/usr/local/etc/sudoers` (file) | sysctl / jail param (kernel state) |
|
||||
| Password dependency | Optional but infrastructure exists | None — purely role-based |
|
||||
| External dependency | Ports package (`security/sudo`) | Base system |
|
||||
| Per-jail scoping | Not native | `mac.do.rules` jail parameter |
|
||||
|
||||
### 4.2 Where mac_do Falls Short
|
||||
|
||||
| Concern | Severity | Detail |
|
||||
| ----------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **No audit logging** | **HIGH** | sudo logs every invocation to `/var/log/auth.log` (timestamp, user, command, exit code). mac_do has zero logging. An agent system that executes privileged commands autonomously needs traceability. |
|
||||
| **No per-command granularity** | **HIGH** | mac_do rules allow a UID/GID transition, not a specific command. `uid=1001>uid=0` lets the agent become root for anything. sudoers lets you restrict: `agent ALL=(root) NOPASSWD: /usr/local/bin/bastille`. |
|
||||
| **Loss of sudo env vars** | **MEDIUM** | The codebase reads `SUDO_USER`, `SUDO_UID`, `SUDO_GID` in `setup/service.ts`, `setup/hostd.ts`, `setup/profile.ts`, `setup/install.ts`. mdo does not set these. |
|
||||
| **FreeBSD 14.x incomplete** | **MEDIUM** | Basic mdo exists in 14.2 but group control (`-g`, `-G`, `-s`, fine-grained UID/GID) is 15.0+ only. ISO currently targets 14.x. |
|
||||
| **Kernel module requirement** | **LOW** | Must load `mac_do` at boot via `loader.conf`. Adds a system requirement. |
|
||||
| **Hardcoded path** | **LOW** | Only `/usr/bin/mdo` works currently. Not configurable. |
|
||||
| **Bastille ecosystem** | **LOW** | Bastille documentation expects sudo. Every skill and doc in this repo says `sudo`. |
|
||||
| **No `sudo -u` equivalent clarity** | **LOW** | `sudo -u postgres psql` is a well-understood idiom. `mdo -u postgres psql` works but the transition rules must be set up for both directions (agent→root AND root→postgres). |
|
||||
|
||||
---
|
||||
|
||||
## 5. Recommendations
|
||||
|
||||
### 5.1 Do Not Replace sudo With mac_do Now
|
||||
|
||||
The security gain is marginal and the trade-offs are net negative:
|
||||
|
||||
- You lose per-command restriction (sudoers: "only bastille" vs. mac_do: "become root")
|
||||
- You lose audit logging in an autonomous agent context
|
||||
- You lose sudo env vars that setup code depends on
|
||||
- 14.x support is incomplete
|
||||
- Massive documentation churn (~100+ files) for no operator-visible benefit
|
||||
|
||||
### 5.2 Do Finish the hostd Migration
|
||||
|
||||
The correct path is to eliminate the need for runtime sudo entirely:
|
||||
|
||||
1. **Move `jail-exec-runner.ts`'s `sudo bastille cmd` to hostd** — hostd already runs as root with a Unix socket API. This is the last significant runtime sudo use.
|
||||
2. **Move `sudo pkg version` to hostd** — startup report and Telegram commands already have hostd access.
|
||||
3. **Move shell script sudo calls to hostd** — `heartbeat.sh`, `destroy-jails.sh`, `docs-sync.cron.sh` can call hostd client instead.
|
||||
|
||||
Result: zero runtime sudo, no kernel module needed, no doc churn, no lost audit trail.
|
||||
|
||||
### 5.3 Where mac_do Could Be Useful Later
|
||||
|
||||
mac_do becomes interesting for a **different** problem than "replace host sudo":
|
||||
|
||||
- **Per-jail privilege isolation for multi-tenant**: each jail gets its own `mac.do.rules` allowing restricted credential transitions inside the jail without sudo installed in the jail at all
|
||||
- **Removing sudo from the base install entirely**: if hostd absorbs all privileged operations, you could uninstall sudo and use mac_do as a minimal fallback for interactive operator access
|
||||
- **Defense in depth**: even with hostd, mac_do could enforce that only specific UID transitions are possible at the kernel level, limiting blast radius of any hostd vulnerability
|
||||
|
||||
These are future considerations, not current priorities.
|
||||
|
||||
---
|
||||
|
||||
## 6. References
|
||||
|
||||
- `mac_do(4)`: https://man.freebsd.org/cgi/man.cgi?query=mac_do
|
||||
- `mdo(1)`: https://man.freebsd.org/cgi/man.cgi?query=mdo
|
||||
- `setcred(2)`: https://man.freebsd.org/cgi/man.cgi?query=setcred
|
||||
- `mac(4)`: https://man.freebsd.org/cgi/man.cgi?query=mac
|
||||
- Commit introducing mdo: FreeBSD 14.2 (basic), FreeBSD 15.0 (full group/fine-grained control)
|
||||
- Authors: Olivier Certner `<olce@FreeBSD.org>`, Baptiste Daroussin `<bapt@FreeBSD.org>`
|
||||
Loading…
Add table
Reference in a new issue