Skip to content

Stage the firmware's first-login marker with the authorized key - #51

Open
abcd-ca wants to merge 4 commits into
BandarLabs:mainfrom
abcd-ca:first-login-marker
Open

Stage the firmware's first-login marker with the authorized key#51
abcd-ca wants to merge 4 commits into
BandarLabs:mainfrom
abcd-ca:first-login-marker

Conversation

@abcd-ca

@abcd-ca abcd-ca commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Fixes the first-contact failure I hit on a fresh Libra Colour (full diagnosis and correction on #49): the firmware's sshd_config forces root through a setup script that runs an interactive passwd until /.login_pass_set exists. Key authentication succeeds, and then the very first non-interactive kobo deploy/doctor after setup --enable-ssh pipes its script into that passwd — the transfer lines are eaten as password input, and the leftovers run as shell commands once the wrapper reaches exec /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-ssh already 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 own touch ${PASS_SET} would have created after a successful interactive password change. Root already carries a password hash in /etc/passwd and access is by key, so the marker gates only the interactive reset, not authentication — the wrapper goes straight to exec /bin/sh on every login, including the first.

  • authorize.rs: the marker joins entries() and STAGED_MEMBERS; the module contract ("ships nothing but this machine's public key") is updated to name both things it ships and why.
  • setup.rs: the KEY_SCOPE and PLUGIN_AND_KEY_SCOPE reports name the marker and explain it.
  • main.rs: the dry-run plan's "nothing extracted as root but…" lines include it.
  • Tests: the nothing-but-the-key boundary test now admits exactly the marker; a new test pins its path, emptiness, and mode; the plan-string tests updated.

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.sh doesn'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.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

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.
@abhishek-anand

Copy link
Copy Markdown
Contributor

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.

@abcd-ca

abcd-ca commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

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 passwd step was never an authentication gate. It runs as a ForceCommand after SSH authentication has already succeeded. On stock firmware, anyone who can authenticate with the factory credential gets the passwd prompt and then a shell; the wrapper invites them to set the password themselves. So the surface with the marker staged is identical to stock. What the marker removes is the interactive trap that breaks piped, non-interactive sessions (the bug this PR fixes).

That said, the underlying posture is worth spelling out. The effective sshd configuration for root on this firmware (sshd -T -C user=root,...) is:

permitrootlogin yes
passwordauthentication yes
permitemptypasswords yes
forcecommand /etc/ssh/initial_ssh_setup.sh

(The firmware's trailing PermitRootLogin yes / PermitEmptyPasswords yes lines sit after the Match User root line, so they apply inside that block.) There is no /etc/shadow; root's credential is a legacy DES crypt hash in world-readable /etc/passwd. That's the stock reality with or without this PR.

I validated a fix on hardware that closes it properly. Appending a sentinel-guarded block scoped to sshd:

# cobalt-hardening: key-only SSH for root.
Match User root
PasswordAuthentication no

validated with sshd -t, then reloaded. Results on the device:

  • effective root context now reports passwordauthentication no
  • a fresh key-authenticated login succeeds
  • a password-only attempt is refused with Permission denied (publickey) (the method is no longer offered)
  • key-authenticated read-only doctor succeeds end to end: result: write ready

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), sshd -t gated, with a timed auto-revert that a fresh key login cancels, idempotent, and removed by setup --undo. Would you want that inside this PR, or as a follow-up so this one stays a one-file fix? Happy either way.

@abhishek-anand

Copy link
Copy Markdown
Contributor

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.
@abcd-ca

abcd-ca commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

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 kobo setup --enable-ssh already ends with the first key-authenticated connection to the restarted reader, so that connection now carries the hardening. It appends the sentinel-marked key-only block to sshd_config, gated on sshd -t and on sshd -T actually reporting passwordauthentication no (which catches an append to a file the server doesn't read). The saved pre-hardening 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 confirming login is made by the same command with connection multiplexing disabled, so its success genuinely proves keys work against the hardened server. If it never arrives, the reader puts itself back and stays reachable. --no-key skips the hardening (no key, no proof to stand on), setup --undo removes the block over Wi-Fi since USB can't reach the root filesystem, and both dry runs disclose all of it.

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 sshd -T reports yes again.

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 # cobalt-hardening: marker prefix rather than the exact line, so a rerun won't append a second block, and removal refuses a block that has no end marker instead of running a sed range to the end of the file. The ordering that carries the safety argument is pinned by tests, and each script is syntax-checked through sh -n.

@abhishek-anand

Copy link
Copy Markdown
Contributor

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.

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.

2 participants