Skip to content

feat(grants): route every privilege grant through one validated path - #602

Merged
chubes4 merged 1 commit into
mainfrom
feat/grant-lifecycle
Sep 17, 2026
Merged

chubes4 merged 1 commit into
mainfrom
feat/grant-lifecycle

Conversation

@chubes4

@chubes4 chubes4 commented Sep 17, 2026

Copy link
Copy Markdown
Member

Implements #601.

The bug this removes

lib/systems-capabilities.sh contained a correct sudoers installer — validate a temp file, mv into place only once visudo accepts it — with zero call sites, orphaned when its only consumer was retired. The next component that needed a grant didn't find it and wrote its own (bridges/kimaki.sh:537), which installed at the live path and validated afterwards:

printf '%s\n' "$sudoers_content" > "$sudoers_file"   # live: /etc/sudoers.d/
chown root:root "$sudoers_file"
chmod 0440 "$sudoers_file"
if command -v visudo >/dev/null 2>&1; then
  visudo -cf "$sudoers_file" >/dev/null                # too late
fi

setup.sh and upgrade.sh both run under set -e, so a rejected policy aborts the run and stays on disk. An invalid file in /etc/sudoers.d makes sudo refuse to run for every user on the host — including the operator's recovery path. Two implementations of one thing, and the one in service was the one that could lock you out of the box.

What replaces it

lib/grants.sh — the single path a grant reaches a host. Declare, validate in isolation, install atomically.

  • grant_declare records without touching the host, so a declaration is readable before anything is installed. Re-declaring replaces rather than appends.
  • grant_validate runs visudo against a temp file that sudo never reads.
  • grant_install validates first, then replaces atomically. The live path only ever holds a policy visudo already accepted.
  • grant_content_matches and grant_ownership_ok are separate, so drift reports which invariant moved. A grant with the right words and the wrong mode is not a correct grant — sudo ignores a group-writable policy file.
  • grant_undeclared_files is the census.

The kimaki bridge routes through it; its private writer is deleted. systems_capabilities_install_sudoers is deleted — this is its logic, extracted to where a second caller could find it.

Library functions warn and return rather than calling error, which exits. Whether a refused grant is fatal belongs to the caller — the kimaki bridge treats it as fatal, a census or dry run does not — and a library that exits takes that choice away. (Found by writing the test: the first version called error and killed the test run mid-suite.)

verify.sh gains the seam nobody owned

verify.sh never read /etc/sudoers.d, so a grant that was hand-placed, widened, or left group-writable looked exactly like one installed correctly. It now asserts root:root 0440 and valid policy syntax on every grant, and reports which ones this harness does not install.

Run against the production agent host, unmodified:

privilege grant agreement
  ok    all 5 privilege grants are root:root 440
  ok    all 5 privilege grants parse as valid sudoers policy
  note  grants this harness does not install: 90-cloud-init-users homeboy-edge-apply homeboy-upgrade opencode-kimaki
        these do not survive a host rebuild and nothing detects changes to them

Four of five, named — including homeboy-edge-apply, which I placed by hand last night and which has exactly the defect #601 describes. The census is deliberately a note, not a failure: an operator-placed grant is not wrong, and failing every existing host on day one would get this check disabled rather than acted on. Declaring those grants is follow-up work.

Tests

tests/privilege-grants.sh, 22 assertions, wired into CI. The ones that matter are negative:

ok   installing invalid policy fails
ok   no file is left at the live path
ok   no temp file is left behind
ok   an existing valid grant survives a rejected update
ok   surviving grant is still 0440
ok   content drift is detected
ok   permission drift is detected
ok   dry-run installs nothing

"An existing valid grant survives a rejected update" is the regression that matters: a host with a working grant, and an upgrade that computes a broken one.

Full affected suite on Linux — 14 suites, all passing, including service-identity-adoption which asserts the generated sudoers content byte-for-byte, so the refactor is byte-compatible:

privilege-grants PASS            kimaki-agent-fallback PASS
kimaki-dispatch-helper-health PASS   kimaki-install-existing PASS
service-identity-adoption PASS       kimaki-credential-seeding PASS
bridge-render PASS                   post-upgrade-restore PASS
kimaki-multi-instance PASS           ai-gateway PASS
kimaki-no-default-channel PASS       opencode-local-plugin-path PASS
cli-channel-binary-path PASS         agent-state-ownership PASS

Also fixes a hardcoded GNU stat -c in tests/agent-state-ownership.sh that broke that suite on macOS, using file_mode — the portable helper this repo already has.

Not in scope

Declaring the three hand-placed grants, which is the other half of #601 and a behaviour change to what gets installed on existing hosts. This PR makes that possible and measurable; it does not grant anything new.


AI assistance: implemented by Claude Opus 4.5 via Claude Code, driven by Chris. The model traced the dead call site through git history, found the install-before-validate ordering, wrote the library, tests, and verify seam, and ran the affected suites on a Linux host plus the new verify section against the production agent host.

A sudoers grant is the most consequential file this harness writes, and it was
written two different ways.

lib/systems-capabilities.sh had a correct installer -- validate a temp file,
move it into place only once visudo accepts it -- but its only consumer was
retired and it was left with zero call sites. The next component that needed a
grant did not find it and wrote its own, which installed at the live path and
validated afterwards:

    printf '%s\n' "$content" > /etc/sudoers.d/name
    visudo -cf /etc/sudoers.d/name

Under set -e a rejected policy aborts the run and stays on disk, and an invalid
file in /etc/sudoers.d makes sudo refuse to run for every user on the host,
including the operator's recovery path. The duplicate was not just redundant,
it was the dangerous one.

lib/grants.sh is now the single path: declare, validate in isolation, install
atomically, compare content and ownership separately so drift reports which one
moved. The kimaki bridge routes through it and its private writer is deleted.

verify.sh gains the seam nobody owned. It never read /etc/sudoers.d, so a grant
that was hand-placed, widened, or left group-writable looked exactly like one
installed correctly. It now asserts root:root 0440 and valid policy syntax on
every grant, and reports the ones this harness does not install -- which on the
first host checked was four of five.

Also fixes a hardcoded GNU stat -c in tests/agent-state-ownership.sh that broke
the suite on macOS, using the portable helper the repo already has.

Refs #601
@chubes4
chubes4 merged commit 64620ab into main Sep 17, 2026
71 checks passed
@chubes4
chubes4 deleted the feat/grant-lifecycle branch September 17, 2026 00:21
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