Stage the firmware's first-login marker with the authorized key - #51
Stage the firmware's first-login marker with the authorized key#51abcd-ca wants to merge 4 commits into
Conversation
The firmware forces root's SSH sessions through a setup script that runs
an interactive passwd until /.login_pass_set exists, and only then hands
over the shell. A key authenticates fine, and then the very first
non-interactive kobo deploy or doctor after setup --enable-ssh feeds its
piped script into that passwd: the transfer lines are consumed as
password input, and whatever is left over runs as shell commands once
the wrapper reaches exec /bin/sh. Observed on a Libra Colour on firmware
4.45.23697 (see Cobalt#49); it presents as the base64 body executing
line by line, which is confusing enough that it was first blamed on the
shell.
The archive setup already stages for the firmware to extract as root now
carries the marker too: /.login_pass_set, empty, mode 644, which is byte
for byte what the firmware's own `touch ${PASS_SET}` would have created.
Root already carries a password hash and access is by key, so the marker
gates only the interactive reset, not authentication.
The reports and the dry-run plan say so, and the archive tests pin the
marker's presence, emptiness, and mode alongside the existing
nothing-but-the-key boundary.
|
Before merging, could you confirm the password-authentication posture after the marker is staged? Because this bypasses the firmware’s forced passwd step, we should verify that it does not leave root password login enabled with a factory/default credential. Evidence from the effective sshd configuration, plus confirmation that key-authenticated doctor / deploy succeeds while password authentication is disabled or otherwise safe, would close the remaining concern. |
|
Good thoughts. I dug into it on hardware (Libra Colour, N428, firmware 4.45.23697) and here's what I came up with: The marker doesn't change the posture, because the forced That said, the underlying posture is worth spelling out. The effective sshd configuration for root on this firmware ( (The firmware's trailing I validated a fix on hardware that closes it properly. Appending a sentinel-guarded block scoped to sshd: validated with
Because it's scoped to sshd, serial console login is untouched, and a reverting KoboRoot.tgz over USB remains the recovery path of last resort. I'd be glad to implement this in the CLI: applied over the first key-authenticated connection (proof by construction that keys work before the password door closes), |
|
Thanks for validating this on hardware. The key-only hardening looks good and addresses the concern. Could you please include it in this PR rather than defer it? |
The firmware's SSH server accepts root by password: sshd_config ends with PermitRootLogin yes and PermitEmptyPasswords yes applying inside Match User root, the credential is a legacy DES hash in world-readable /etc/passwd, and there is no /etc/shadow. The first-login marker this branch stages changes none of that either way (the passwd wrapper runs after authentication, so it never was a gate), but a setup that has just installed a key can do better than stock, and upstream review of Cobalt#51 asked for exactly that in this PR. So the wait at the end of 'kobo setup --enable-ssh' now ends by closing the door it no longer needs, ordered so the one bad outcome, locking an owner out of a reader whose key was never accepted, is impossible by construction: - The hardening script travels over the first key-authenticated connection; a key that does not work means nothing is touched. - The change is a sentinel-marked block appended to sshd_config (Match User root / PasswordAuthentication no, the exact block validated on a Libra Colour on 4.45.23697), gated on sshd -t and on sshd -T actually reporting passwordauthentication no; either gate failing puts the saved copy back before the server ever reloads. - The saved copy doubles as a deadline: a watchdog on the reader restores it after five minutes unless a second, fresh key login removes it first. That login is made by the same command, opts out of connection multiplexing so it really authenticates, and its success is what commits the change. 'kobo setup --undo' takes the block back out the way it went in, over SSH, since USB cannot reach the root filesystem: after ejecting it watches for the reader to rejoin Wi-Fi and deletes exactly the sentinel range, gated on sshd -t -f against a scratch copy. A reader it cannot find keeps the block, said plainly, and inert once the same undo has switched the server off. --no-key skips the hardening, since without this machine's key there is no proof to stand on, and the dry run disclosed all of it before anything agreed. The scripts are pinned by tests on the ordering that carries the safety argument, and each is syntax-checked through sh -n, the same POSIX grammar the reader's BusyBox ash reads.
A hand-applied hardening, which is exactly what the review comment on Cobalt#51 showed, has the begin sentinel and no end. The removal's sed range would then run to the end of the file, and sshd -t on a truncated config passes more often than not. Refused with an explanation instead.
The block on the reader this was validated against carries a note on
its marker line ('Delete through end of file to undo.'), so an exact
line match failed to see it and would have appended a second block.
Detection now greps the '# cobalt-hardening:' prefix; the sed range
that removes still requires the exact begin and end lines this tool
writes, and the end-marker guard keeps refusing what it cannot bound.
|
Done, it's in this PR now. The last three commits add the hardening to the CLI. How it works: the wait at the end of Validated on the same hardware as before (Libra Colour N428, 4.45.23697), using the byte-exact scripts the CLI sends: apply/commit works and password authentication is no longer offered afterwards; deliberately withholding the confirming login had the watchdog restore the config byte-identical to the original after five minutes, with keys working throughout; removal takes out exactly the sentinel range and Testing on hardware also caught two edge cases around a block applied by hand before this existed (like the one from my earlier comment): detection now matches the |
|
Thanks for adding the hardening. Before merging, could you make it fail closed when sshd -T returns no effective PasswordAuthentication value, and build/validate the complete config in a temporary file before replacing the live one? An interrupted append can currently leave a partial block that a retry treats as already applied. Please also avoid re-enabling password login during setup --undo before SSH is actually disabled. |
Fixes the first-contact failure I hit on a fresh Libra Colour (full diagnosis and correction on #49): the firmware's
sshd_configforces root through a setup script that runs an interactivepasswduntil/.login_pass_setexists. Key authentication succeeds, and then the very first non-interactivekobo deploy/doctoraftersetup --enable-sshpipes its script into thatpasswd— the transfer lines are eaten as password input, and the leftovers run as shell commands once the wrapper reachesexec /bin/sh. It presents as the base64 body executing line by line (/bin/sh: f0VMRg…: not found), which I initially misattributed to the shell.The change
kobo setup --enable-sshalready stages one archive for the firmware to extract as root, carrying this machine's public key. That archive now also carries/.login_pass_set: empty, mode 644, which is byte for byte what the firmware's owntouch ${PASS_SET}would have created after a successful interactive password change. Root already carries a password hash in/etc/passwdand access is by key, so the marker gates only the interactive reset, not authentication — the wrapper goes straight toexec /bin/shon every login, including the first.authorize.rs: the marker joinsentries()andSTAGED_MEMBERS; the module contract ("ships nothing but this machine's public key") is updated to name both things it ships and why.setup.rs: theKEY_SCOPEandPLUGIN_AND_KEY_SCOPEreports name the marker and explain it.main.rs: the dry-run plan's "nothing extracted as root but…" lines include it.cargo test -p kobo-cli: 203 passed. Clippy (pedantic) and rustfmt clean.Scope note
This helps every model whose firmware ships the wrapper (the Clara BW on 4.45.23697 has the same script). On firmwares whose
/etc/ssh/initial_ssh_setup.shdoesn't exist the marker is a single inert empty file at/. I considered gating it on the wrapper's presence, but USB setup cannot read the root filesystem to check, and an inert empty file seemed the better trade than a first login that fails interactively — happy to change the approach if you'd rather.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.