Skip to content

docs: align Security Model wording with sudoers and pin the claim - #487

Open
yuee3 wants to merge 1 commit into
lacs-project:mainfrom
yuee3:docs/security-model-wording
Open

yuee3 wants to merge 1 commit into
lacs-project:mainfrom
yuee3:docs/security-model-wording

Conversation

@yuee3

@yuee3 yuee3 commented Sep 22, 2026

Copy link
Copy Markdown

What

Rewrite the Security Model preamble in SECURITY.md so it matches the trust boundary already described in packaging/sysknife-sudoers, and pin the replacement wording in scripts/check_public_claims.sh.

  • Say what the model gives: each layer gates a different stage of one request path, and the sysknife service account is root-equivalent by design.
  • Put the daemon-compromise case in the preamble, where an operator sizing blast radius will read it.
  • Add the ROOT_SHELL_UNITS example: the denylist stops unit names from the LLM/MCP path; it is not containment.
  • Retire the "every layer is independent / a bypass of one does not bypass the others" sentence.
  • Soften the Known Limitations line that leaned on independence ("the downstream enforcement layers cap their blast radius") to "the later gates on the same request path still apply".

Why

Closes #483. SECURITY.md:43-44 and packaging/sysknife-sudoers:9-12 describe the same trust boundary and disagree. All five layers run inside one process as sysknife, and that account holds useradd, systemctl, and a trailing-wildcard apt-get — each reaches root on its own. The sudoers header is accurate; the independence sentence invites a reader to treat ROOT_SHELL_UNITS as containment.

check_public_claims.sh

Yes, the replacement wording can be pinned there, and this PR pins it:

reject_pattern 'Every layer is independent|a bypass of one does not bypass the others' \
    'layers are sequential gates on one request path, not independent walls' "${claim_files[@]}"

A sentence that drifted once will drift again.

How checked

  • Read SECURITY.md against packaging/sysknife-sudoers and packaging/sysknife-daemon.service.
  • Read ROOT_SHELL_UNITS in crates/sysknife-daemon/src/actions/validate.rs (refuses debug-shell/emergency/rescue/runlevel1/single for typed actions) and kept both what it does and what it does not do in one place.
  • Scanned the rest of SECURITY.md for sentences leaning on independence; Layer 4 already carries the right register ("This boundary protects against an untrusted MCP agent, not against arbitrary malware already running as the same Linux user").
  • Documentation-only change. No code, config, or sudoers grant touched.

Note

#483 was offered to @mikevillari. If that work is already underway, please close this in favour of it — happy to rebase or drop.

@yuee3
yuee3 requested a review from vladimirrott as a code owner September 22, 2026 10:23

@vladimirrott vladimirrott left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this, and welcome. You took the part of #483 that was easiest to skip, the request to pin the wording so it cannot drift back, and you did it rather than writing a sentence saying why it was hard. The preamble rewrite is accurate: it names the sequential-gate framing, the User=sysknife line, and the three grants that reach root on their own, and it points the reader at packaging/sysknife-sudoers where the honest wording already lived.

One blocking item, and it is the guard rather than the prose.

The pin cannot fire on the file it was written for. reject_pattern runs over claim_files, which check_public_claims.sh reads from CLAIM_FILES in check_evidence_claims.py. SECURITY.md is not in that list:

$ python3 -c "
import importlib.util
spec = importlib.util.spec_from_file_location('c','scripts/check_evidence_claims.py')
m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
print('SECURITY.md screened:', 'SECURITY.md' in m.CLAIM_FILES)
print('files:', len(m.CLAIM_FILES))"
SECURITY.md screened: False
files: 16

I applied your reject_pattern line verbatim to main, put the retired sentence back into SECURITY.md, and ran the check:

$ grep -c 'Every layer is independent' SECURITY.md
1
$ bash scripts/check_public_claims.sh; echo "rc=$?"
Published figures match the evidence artifacts.
Public claims are internally consistent.
rc=0

Green, with the exact sentence the pin exists to reject sitting live in the tree. Adding SECURITY.md to CLAIM_FILES is the fix. Please check what else that turns red before you push: the file is long and the other reject_pattern calls will start seeing it for the first time, which may surface real wording to fix or may need the pattern scoped.

Whatever you land, show the mutation in the PR: the sentence restored, the check red, the sentence removed, the check green. A guard that has never been observed failing is the defect this tracker catches most, and it is the one thing #483 asked for that nothing else in the diff can demonstrate.

Two smaller notes, neither blocking:

scripts/check_public_claims.sh goes from mode 100755 to 100644 in the diff. The release rehearsal calls it as bash scripts/check_public_claims.sh so nothing breaks today, but it is almost certainly not what you meant. chmod +x and amend.

The apt-get grant you cite as trailing-wildcard is spelled env DEBIAN_FRONTEND=... NEEDRESTART_MODE=a /usr/bin/apt-get *, and v0.19.0 and v0.20.0 have since narrowed sixteen of the grant families your text describes, including systemctl and useradd. Rebase on main and the ROOT_SHELL_UNITS section will need one adjustment: systemctl now carries per-subcommand constraints, so sudo -n /usr/bin/systemctl start rescue.target is refused by the grant itself. The denylist-is-not-containment point still stands and is still worth making; the worked example needs a different verb.

Once the pin bites and the mode bit is back, this merges. Say the word if you would rather I take the CLAIM_FILES half and leave you the prose.

If you administer Linux boxes yourself, sysknife doctor and sysknife audit verify are read-only and take nothing but a checkout, and I would rather hear what they get wrong on a real host than guess.

This branch has not been deployed

No deployments
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.

SECURITY.md says the enforcement layers are independent; the sudoers file says they are not

2 participants