clawdie-ai/docs/internal/SUDO_REPLACEMENT.md
Operator & Claude Code 0724a28d68 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
2026-05-10 08:47:41 +02:00

12 KiB

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 hostdheartbeat.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