fix(docs): correct the MOK removal steps, drop two dead pins, fix the LSM audit trail #34

Merged
zach merged 1 commit from fix/policy-and-doc-accuracy into main 2026-07-25 21:45:59 +00:00
Owner

Five factual errors plus one process gap, ranked by user impact. No shipped-config change — both removed pins are absent from every published final.config, and an off-pin for a symbol kconfig doesn't know is a no-op through merge_config + olddefconfig.

1. uninstall.md told users to delete the wrong certificate ⚠️

sudo mokutil --list-enrolled                       # identify the project key
sudo mokutil --delete /var/lib/shim-signed/mok/MOK.der   # adjust path per setup

That path is the machine's own shim/DKMS MOK — the key Debian's dkms generates and signs local modules with. Following the section verbatim queues deletion of the user's key (breaking every DKMS module: NVIDIA, ZFS, VirtualBox) while leaving the project certificate enrolled — the exact inverse of the section's stated purpose. And the preceding line, "identify the project key", primes the reader to believe the listed path is ours, so the "adjust path per setup" hedge doesn't save it.

Rewritten around unredacted-hardened.der with a fingerprint cross-check before deleting, an explicit warning, ordering guidance (remove packages first — once the key is gone, modules signed by it won't load), and a verification command. Cross-referenced from secure-boot.md (which documented import but never removal) and dkms.md.

2. Two intent pins targeted symbols that no longer exist upstream

MODULE_SIG_KEY_TYPE_ECDSA and GCC_PLUGIN_STRUCTLEAK are absent from every final.config in releases/, across both the 7.0 and 7.1 lines. The fidelity assertion implements "off" as no SYM= line present, so absence satisfies the pin — both have been silent no-ops for the entire published history and could never fail.

The POLICY headings had to change too, not just the prose: a stale backticked assertion in POLICY keeps passing forever for the same absence-satisfies reason, so those headings were load-bearing. The ECDSA rationale is kept as history — the upstream choice now offers RSA plus the post-quantum ML-DSA variants, and only the selected member needs pinning because kconfig choice is exclusive.

3. POLICY.md claimed LoadPin and SafeSetID were active LSMs

CONFIG_LSM="landlock,lockdown,yama,integrity,bpf" — neither loadpin nor safesetid is in it, and per the kernel's own help text any LSM left off that list is ignored. So LOADPIN_ENFORCE=y reads as "kernel file loads are pinned to one verified filesystem" while nothing of the kind is in effect. integrity is inert too: the subsystem is built, but CONFIG_IMA and CONFIG_EVM are both off.

POLICY now separates active from compiled-in-but-inactive and says why the pins stay (so the symbols can't silently vanish; so activation is later a one-line edit).

Deliberately not resolved by editing CONFIG_LSM. Enabling an enforcing LSM on users' machines to fix a documentation inconsistency inverts the risk: with MODULE_SIG_FORCE=y there's no soft-fail path, and on hardware we don't control the failure mode ranges from a module refusing to load to an unbootable system. Recorded as an open decision with boot-matrix preconditions in config-architecture.md — not DECISIONS.md, which is untracked.

4. Two doc examples would rewrite the current line's seed to an old series

tools/bump-seed.sh 7.0.10.hardened1-1 (in config-architecture.md and first-build.md) defaults to the upstream-seed.toml symlink — now seeds/7.1.toml — so following either verbatim rewrites the 7.1 pin to a 7.0 config. bump-seed.sh warns on a series boundary but still performs the rewrite. Both now pass --pin-file explicitly and explain why.

5. Prose describing constraints that no longer exist

  • config-architecture.md said USERFAULTFD is "kept on with sysctl restriction". It's compiled out; intent.config and the shipped config agree.
  • intent.config and POLICY.md both said a compressed module's signature can't be stripped — "a known caveat". strip-signatures.sh decompresses .ko.xz/.gz/.zst and strips the inner stream. No caveat remains.
  • Counts: 92 → 90 setting lines after the removals (17 deltas + 73 insurance), in intent.config, configure-kernel.sh's header, and config-architecture.md.
  • Claims about what anthraxx selects today now point at tools/reconcile-vs-anthraxx.sh instead. The seed is fetched at build time and isn't in this tree, so such claims are unverifiable from here and go stale silently — replacing one unverifiable claim with another would just recreate the defect.

6. The release un-draft step appeared in no runbook

build-kernel.yml creates the release as a draft by design, and publish.yml reads draft assets via the token — so the registry publish succeeds while it's still a draft, which is precisely why the step is easy to miss. Nothing un-drafts it.

No user-facing breakage today: I checked the live server — releases show as released and an anonymous GET of unredacted-hardened.der returns HTTP 200, so this is being done by hand. The defect is that a required manual step was undocumented. Added as first-build.md step 10a, resolving the id from the LIST endpoint (the by-tag lookup filters drafts on some Forgejo versions — same reason build-kernel.yml already avoids it) plus a credential-free reachability check before announcing.

Verification

  • intent-matches-policy (extracted and run): passes. It caught a mistake mid-edit — I'd written a backticked CONFIG_INTEGRITY=y in prose, which Direction 1 reads as an assertion requiring an intent.config pin. Reworded; a sweep for any other backticked CONFIG_X=value in POLICY lacking an intent pin now comes back empty.
  • Fidelity assertion against the real shipped final.config: OK (90 intent lines, all landed).
  • Dead-pin sweep across intent.config off-pins vs the newest final.config: clean (was 2).
  • shellcheck, py_compile, yamllint -d relaxed all clean.

Note for review

tools/check-hardening-drift.sh:10's "~59 symbols" is left alone here because #30 already rewrites that line (to 92). After both land it should read 90 — one-word follow-up, flagged rather than creating a conflict between the two branches.

🤖 Generated with Claude Code

Five factual errors plus one process gap, ranked by user impact. **No shipped-config change** — both removed pins are absent from every published `final.config`, and an off-pin for a symbol kconfig doesn't know is a no-op through `merge_config` + `olddefconfig`. ## 1. `uninstall.md` told users to delete the wrong certificate ⚠️ ``` sudo mokutil --list-enrolled # identify the project key sudo mokutil --delete /var/lib/shim-signed/mok/MOK.der # adjust path per setup ``` That path is the machine's **own** shim/DKMS MOK — the key Debian's `dkms` generates and signs local modules with. Following the section verbatim queues deletion of the *user's* key (breaking every DKMS module: NVIDIA, ZFS, VirtualBox) while **leaving the project certificate enrolled** — the exact inverse of the section's stated purpose. And the preceding line, "identify the project key", primes the reader to believe the listed path *is* ours, so the "adjust path per setup" hedge doesn't save it. Rewritten around `unredacted-hardened.der` with a fingerprint cross-check before deleting, an explicit warning, ordering guidance (remove packages first — once the key is gone, modules signed by it won't load), and a verification command. Cross-referenced from `secure-boot.md` (which documented import but never removal) and `dkms.md`. ## 2. Two intent pins targeted symbols that no longer exist upstream `MODULE_SIG_KEY_TYPE_ECDSA` and `GCC_PLUGIN_STRUCTLEAK` are absent from **every** `final.config` in `releases/`, across both the 7.0 and 7.1 lines. The fidelity assertion implements "off" as *no `SYM=` line present*, so absence **satisfies** the pin — both have been silent no-ops for the entire published history and could never fail. The POLICY headings had to change too, not just the prose: a stale backticked assertion in POLICY keeps passing forever for the same absence-satisfies reason, so those headings were load-bearing. The ECDSA rationale is kept as *history* — the upstream choice now offers RSA plus the post-quantum ML-DSA variants, and only the selected member needs pinning because kconfig `choice` is exclusive. ## 3. POLICY.md claimed LoadPin and SafeSetID were active LSMs `CONFIG_LSM="landlock,lockdown,yama,integrity,bpf"` — neither `loadpin` nor `safesetid` is in it, and per the kernel's own help text any LSM left off that list is *ignored*. So `LOADPIN_ENFORCE=y` reads as "kernel file loads are pinned to one verified filesystem" while nothing of the kind is in effect. `integrity` is inert too: the subsystem is built, but `CONFIG_IMA` and `CONFIG_EVM` are both off. POLICY now separates **active** from **compiled-in-but-inactive** and says why the pins stay (so the symbols can't silently vanish; so activation is later a one-line edit). **Deliberately not resolved by editing `CONFIG_LSM`.** Enabling an enforcing LSM on users' machines to fix a documentation inconsistency inverts the risk: with `MODULE_SIG_FORCE=y` there's no soft-fail path, and on hardware we don't control the failure mode ranges from a module refusing to load to an unbootable system. Recorded as an open decision with boot-matrix preconditions in `config-architecture.md` — not `DECISIONS.md`, which is untracked. ## 4. Two doc examples would rewrite the current line's seed to an old series `tools/bump-seed.sh 7.0.10.hardened1-1` (in `config-architecture.md` and `first-build.md`) defaults to the `upstream-seed.toml` symlink — now `seeds/7.1.toml` — so following either verbatim rewrites the **7.1** pin to a **7.0** config. `bump-seed.sh` warns on a series boundary but still performs the rewrite. Both now pass `--pin-file` explicitly and explain why. ## 5. Prose describing constraints that no longer exist - `config-architecture.md` said `USERFAULTFD` is "kept on with sysctl restriction". It's compiled out; `intent.config` and the shipped config agree. - `intent.config` and `POLICY.md` both said a compressed module's signature can't be stripped — "a known caveat". `strip-signatures.sh` decompresses `.ko.xz`/`.gz`/`.zst` and strips the inner stream. No caveat remains. - Counts: 92 → **90** setting lines after the removals (17 deltas + 73 insurance), in `intent.config`, `configure-kernel.sh`'s header, and `config-architecture.md`. - Claims about what anthraxx selects *today* now point at `tools/reconcile-vs-anthraxx.sh` instead. The seed is fetched at build time and isn't in this tree, so such claims are unverifiable from here and go stale silently — replacing one unverifiable claim with another would just recreate the defect. ## 6. The release un-draft step appeared in no runbook `build-kernel.yml` creates the release as a draft by design, and `publish.yml` reads draft assets via the token — so the registry publish succeeds while it's still a draft, which is precisely why the step is easy to miss. Nothing un-drafts it. **No user-facing breakage today:** I checked the live server — releases show as `released` and an anonymous GET of `unredacted-hardened.der` returns HTTP 200, so this is being done by hand. The defect is that a required manual step was undocumented. Added as `first-build.md` step 10a, resolving the id from the LIST endpoint (the by-tag lookup filters drafts on some Forgejo versions — same reason `build-kernel.yml` already avoids it) plus a credential-free reachability check before announcing. ## Verification - `intent-matches-policy` (extracted and run): **passes**. It caught a mistake mid-edit — I'd written a backticked `CONFIG_INTEGRITY=y` in prose, which Direction 1 reads as an assertion requiring an `intent.config` pin. Reworded; a sweep for any other backticked `CONFIG_X=value` in POLICY lacking an intent pin now comes back empty. - Fidelity assertion against the real shipped `final.config`: `OK (90 intent lines, all landed)`. - Dead-pin sweep across `intent.config` off-pins vs the newest `final.config`: **clean** (was 2). - `shellcheck`, `py_compile`, `yamllint -d relaxed` all clean. ## Note for review `tools/check-hardening-drift.sh:10`'s "~59 symbols" is left alone here because #30 already rewrites that line (to 92). After both land it should read **90** — one-word follow-up, flagged rather than creating a conflict between the two branches. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(docs): correct the MOK removal steps, drop two dead pins, fix the LSM audit trail
All checks were successful
validate / shellcheck (pull_request) Successful in 11s
validate / yamllint (pull_request) Successful in 16s
validate / workflow-expressions (pull_request) Successful in 4s
validate / pycompile (pull_request) Successful in 4s
validate / intent-matches-policy (pull_request) Successful in 5s
validate / dep-list-parity (pull_request) Successful in 4s
validate / no-placeholder-digests (pull_request) Has been skipped
ddf9ebbd30
Five factual errors, ranked by user impact.

1. docs/users/uninstall.md told users to delete the WRONG certificate.

  sudo mokutil --list-enrolled                       # identify the project key
  sudo mokutil --delete /var/lib/shim-signed/mok/MOK.der   # adjust path per setup

That path is the machine's OWN shim/DKMS MOK — the key Debian's dkms generates
and signs local modules with. Following the section verbatim queues deletion of
the user's key (breaking every DKMS module: NVIDIA, ZFS, VirtualBox) while
LEAVING the project certificate enrolled: the exact inverse of the section's
purpose. The line above it ("identify the project key") primes the reader to
believe the listed path IS ours, so the "adjust path per setup" hedge does not
help. Rewritten around unredacted-hardened.der with a fingerprint cross-check
before deleting, an explicit warning, ordering guidance (remove packages first),
and a verification command. Cross-referenced from secure-boot.md (which
documented import but never removal) and dkms.md.

2. Two intent pins targeted symbols that no longer exist upstream.

MODULE_SIG_KEY_TYPE_ECDSA and GCC_PLUGIN_STRUCTLEAK are absent from every
final.config in releases/, across both the 7.0 and 7.1 lines. The fidelity
assertion implements "off" as "no SYM= line present", so absence SATISFIES the
pin: both have been silent no-ops for the entire published history and could
never fail. Removed, with the POLICY headings rewritten too — a stale backticked
assertion in POLICY keeps passing forever for the same reason, so the headings
were load-bearing, not cosmetic. The ECDSA rationale is kept as history: the
upstream choice now offers RSA plus the post-quantum ML-DSA variants, and only
the selected member needs pinning because kconfig `choice` is exclusive.

3. POLICY.md claimed LoadPin and SafeSetID were active LSMs. They are not.

CONFIG_LSM="landlock,lockdown,yama,integrity,bpf" — neither loadpin nor
safesetid is in it, and the kernel ignores any LSM left off that list. So
LOADPIN_ENFORCE=y reads as "kernel file loads are pinned to one verified
filesystem" while nothing of the kind is in effect. `integrity` is inert too
(subsystem built, IMA and EVM both off). POLICY now separates active from
compiled-in-but-inactive and says why the pins stay.

Deliberately NOT resolved by editing CONFIG_LSM: enabling an enforcing LSM on
users' machines to fix a documentation inconsistency inverts the risk. With
MODULE_SIG_FORCE=y there is no soft-fail path, and the failure mode on hardware
we don't control ranges from a module refusing to load to an unbootable system.
Recorded as an open decision with boot-matrix preconditions in
config-architecture.md (not DECISIONS.md, which is untracked).

4. Two doc examples would rewrite the CURRENT line's seed to an OLD series.

`tools/bump-seed.sh 7.0.10.hardened1-1` in config-architecture.md and
first-build.md defaults to the upstream-seed.toml symlink — now seeds/7.1.toml —
so following either verbatim rewrites the 7.1 pin to a 7.0 config. bump-seed.sh
warns on a series boundary but still performs the rewrite. Both now pass
--pin-file explicitly and say why.

5. Stale prose describing constraints that no longer exist.

  - config-architecture.md said USERFAULTFD is "kept on with sysctl
    restriction"; it is compiled out (intent.config and the shipped config
    agree).
  - intent.config and POLICY.md both said a compressed module's signature
    cannot be stripped, "a known caveat". strip-signatures.sh decompresses
    .ko.xz/.gz/.zst and strips the inner stream — no caveat remains.
  - Counts: 92 -> 90 setting lines after the two removals (17 deltas + 73
    insurance), in intent.config, configure-kernel.sh's header, and
    config-architecture.md.
  - Claims about what anthraxx selects "today" are replaced with pointers to
    tools/reconcile-vs-anthraxx.sh: the seed is fetched at build time and is
    not in this tree, so such claims are unverifiable from here and go stale
    silently. Replacing one unverifiable claim with another would just
    recreate the defect.

6. The release un-draft step appears in no runbook.

build-kernel.yml creates the release as a draft by design; publish.yml reads
draft assets via the token, so the registry publish succeeds while it is still a
draft — which is why the step is easy to miss. Nothing un-drafts it. Live
releases DO show as published and the .der returns HTTP 200, so there is no
user-facing breakage today; the gap is that a required manual step was
undocumented. Added as first-build.md step 10a, with the id resolved from the
LIST endpoint (the by-tag lookup filters drafts on some Forgejo versions) and a
credential-free reachability check before announcing.

No shipped-config change: both removed pins are absent from every published
final.config, and an off-pin for a symbol kconfig does not know is a no-op
through merge_config + olddefconfig.

Note for review: tools/check-hardening-drift.sh:10's "~59 symbols" is left
alone here because #30 already rewrites that line (to 92). After both land it
should read 90.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
zach merged commit aa8836ffc9 into main 2026-07-25 21:45:59 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
unredacted/linux-hardened-unredacted!34
No description provided.