Add remote ssh LUKS device unlocking#320
Conversation
c84c112 to
9b43f1a
Compare
Adds a SSH server that enables unlocking LUKS devices with a passphrase.
The SSH server is enabled only when the `network:` configuration is
defined, and when either `ssh_pass:` or `ssh_authorized_keys:` is defined
under `network:`.
Example configuration:
```yaml
network:
dhcp: true
ssh_addr: ":2222"
ssh_server_keys: |-
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQQDMfxplAcF1tRvQgpIXz3cUJ1G9L70
PJLmDx3IL1CwMWu5r1d1/XxsHA8Tau9CRGVliQvyKTBhlRrs3ViM8glbAAAAqEhXJrpIVy
a6AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBAMx/GmUBwXW1G9C
CkhfPdxQnUb0vvQ8kuYPHcgvULAxa7mvV3X9fGwcDxNq70JEZWWJC/IpMGGVGuzdWIzyCV
sAAAAhAPghE5yL0ITueX8r8vhYA+aG6F3UMGlwANugAK2poytVAAAAD2tlbkBrZW4tZGVz
a3RvcA==
-----END OPENSSH PRIVATE KEY-----
ssh_user: my_user
ssh_pass: "$1$FecCMLMd$R10.c/UKY4IaKwrLo4NaT0"
ssh_authorized_keys: |-
ssh-ed25519 AAAACFFFFFFFFBBBBTTTTFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF+ user@localhost
```
When enabled as above, the Booster `init` process will start a SSH
server at boot listening oen all interfaces on port 2222, using the
specified server private key, and only allowing logins from `my_user`
(default user is `root` when not specified).
Additionally, password authentication will be available (the password
above is hash of "password"), as well as public-key authentication for
the provided authorized key. If `ssh_pass:` is not provided, then
password authentication will not be available.
Similarly, if `ssh_authorized_keys:` is not provided, then public-key
authentication will not be available.
When both `ssh_pass:` and `ssh_authorized_keys:` are not provided, the
SSH server will not be started.
An example remote unlock session:
```sh
$ ssh -p 2222 my_user@example.com
Enter passphrase:
Attempting unlock...
device /dev/md1 slot #0 matches password
device /dev/md1 unlocked successfully
Connection to localhost closed.
```
The `ssh_pass:` in the example configuration above was generated as
follows:
```sh
echo "password" | mkpasswd -H md5 -s
```
Any standard crypt-style prefixed password hashes, as well as plaintext
passwords are supported for `ssh_pass:`.
See the [`github.com/go-crypt/crypt`](https://github.com/go-crypt/crypt)
package for more information.
If `ssh_server_keys:` is not provided, a random ECDSA key will be
generated and used instead.
Implements anatol#191.
9b43f1a to
cf80785
Compare
anatol
left a comment
There was a problem hiding this comment.
One more thought. If we enable SSH server during the boot, it means that it is a remote machine. In this case we should disable keyboard and plymouth input then. What do you think?
|
|
||
| // sshSessionHandler handles ssh sessions. | ||
| func sshSessionHandler(sess ssh.Session) { | ||
| warning("ssh: session %q [%s]: opened", sess.User(), sess.RemoteAddr()) |
There was a problem hiding this comment.
it should be either debug or info level
| github.com/anatol/tang.go v0.0.0-20250920193351-e505ad2c76db | ||
| github.com/anatol/vmtest v0.0.0-20250627153117-302402d269a6 | ||
| github.com/cavaliergopher/cpio v1.0.1 | ||
| github.com/gliderlabs/ssh v0.3.8 |
There was a problem hiding this comment.
I feel more comfortable with going with the basic golang.org/x/crypto/ssh as I trust golang.org/x/ it more.
Does github.com/gliderlabs/ssh really save that much for us?
There was a problem hiding this comment.
It provides much of the functionality to actually do/launch a SSH server. It would basically be recopying everything from that package. The vast majority of the actual implementation is within the golang.org/x package. Considering that the gliderlabs package is used elsewhere in other well known apps, I thought it prudent to use here.
There was a problem hiding this comment.
I would suggest reading through the gliderlabs ssh package. It has essentially zero imports, other than github.com/anmitsu/go-shlex, which is used only in one place: when executing a remote command. Note that functionality is not available through the implementation I wrote. I would prefer if the gliderlabs package didn't have this dependency, but, at the end of the day, it's not my package.
| SshServerKeys string `yaml:"ssh_server_keys,omitempty"` | ||
| SshUser string `yaml:"ssh_user,omitempty"` | ||
| SshPass string `yaml:"ssh_pass,omitempty"` | ||
| SshAuthorizedKeys string `yaml:"ssh_authorized_keys,omitempty"` |
There was a problem hiding this comment.
you use both Password and AuthorizedKeys. I propose to leave only AuthorizedKeys as a more secure approach.
There was a problem hiding this comment.
Ultimately, I'll take your lead here; it does reduce the complexity, but I would just suggest that it be kept in because it's kind of basic, expected SSH functionality, so implemented this as it was quick and fast and to head off future maintenance. I agree with your point, but trying to be somewhat general in purpose.
| // ensure secure key | ||
| if config.Network.SshServerKeys == "" { | ||
| var err error | ||
| if config.Network.SshServerKeys, err = sshGenEcdsaKey(rand.Reader); err != nil { |
There was a problem hiding this comment.
Generating server authentication keys at the boot time does not sound right to me. I think the key needs to be provided by the user during the build time, and then the user adds the public part to ~/.ssh/known_hosts.
There was a problem hiding this comment.
I agree with you, but I imagine most people would want to use this with the least amount of possible configuration entries. As with the password functionality, my thinking was that users would be able do:
network:
ssh_password: "password"
And it would work.
Technically, the SSH server could be launched without any credentials (not that the implementation I submitted is like that), but I thought at least a password or SSH key would be the most prudent options and head off future maintenance requests.
| } | ||
|
|
||
| // check if mapped device | ||
| mapping := matchLuksMapping(blk) |
There was a problem hiding this comment.
You reimplement unlock LUKS logic. See if you can piggy-back on requestKeyboardPassword() instead. Similar to how recent Plymouth integration done here a766edb#diff-fd49727bba9fd7c9b76f07c573dc61e84b82d2b8ba9c1cbe3fe48a3789c601e9
There was a problem hiding this comment.
I had originally looked at consolidating these code paths, but didn't want to waste too much time/effort in case the feature would be rejected. I'll rework the code here and make it more generalized.
|
I travel often and sometimes ssh into my home desktop. But Plymouth is an opt-in config anyway. We can just leave it up to the user if they want both. I generally like the idea and I may have some time later this week to build some tests if you need. |
Replaces console.go's readPasswordLine / readPasswordLocked / readPassword (cooked-termios bufio over stdin) with a new console_input.go raw-mode reader that honors ctx cancellation. Headline behaviour fix: when a non-interactive token (TPM2 PCR-only, touchless FIDO2, or clevis) wins the unlock race while the keyboard prompt is showing, the cooked reader stays blocked in read(2) — the prompt dangles, inputMutex/keyboardMu stay held, and any subsequent volumes that need keyboard entry block indefinitely until the user manually presses Enter on the now-pointless prompt. After this change the reader checks ctx.Done() between Poll(100ms) cycles, returns ctx.Err() within ~100ms of cancellation, restores termios via defer, ends the prompt line cleanly, and releases the locks. Scope is the keyboard-passphrase path only: - askPasswordWithFallback / readPassword / readPasswordLocked take ctx context.Context (threaded from requestKeyboardPassword, which is already ctx-aware after the prior PR in this series). - FIDO2-PIN and TPM2-PIN call sites pass context.Background() and continue to dangle on autounlock; a follow-up PR threads ctx into recoverFido2Password / recoverSystemdFido2Password / recoverSystemdTPM2Password to close that gap. Scanner design: - Per-byte FSM consuming ANSI CSI / SS3 / OSC / DCS sequences and bracketed-paste markers so they never enter the password buffer. - UTF-8 multi-byte reassembly across read boundaries with overlong / surrogate / >U+10FFFF rejection. - Editing keys: Backspace, Delete, Ctrl+W (word kill), Ctrl+U (line kill), Tab (mask toggle), Enter (CR/LF/Ctrl+D). - Inspired by Plymouth's on_key_event in src/libply-splash-core/ply-keyboard.c with intentional divergences (kept ISIG so Ctrl+C still cancels; accept BS and DEL; proper word-kill; OSC/DCS/bracketed-paste handling; bounded CSI parameter accumulation; UTF-8 validation). UTF-8 helpers are direct ports of ply_utf8_*; the scanner itself is reimplemented as a per-byte FSM rather than a slice-scan. Bracketed-paste support has a specific forward-compat value for the in-flight remote SSH unlock work (PR anatol#320): SSH users typically paste passphrases from password managers, and modern terminal emulators bracket the pasted content with \x1b[200~ ... \x1b[201~ markers. A cooked-mode reader would treat those markers as literal password bytes and corrupt the entry. K's scanner consumes the markers and preserves the pasted content verbatim. Termios: ECHO/ICANON/IEXTEN/IXON/IXOFF/BRKINT/INPCK/ISTRIP/INLCR/ IGNCR cleared in raw mode; ISIG kept on. VMIN=1, VTIME=0. Every return path restores the original termios via defer. Notably IXON off means Ctrl+S no longer freezes the prompt. No new dependency: the scanner is ~440 LOC of stdlib Go. Surveyed alternatives (golang.org/x/term, charmbracelet/x/input, peterh/liner, eiannone/keyboard, mattn/go-tty) either don't support cancellable raw reads or pull substantial transitive deps inappropriate for an initramfs.
Replaces console.go's readPasswordLine / readPasswordLocked / readPassword (cooked-termios bufio over stdin) with a new console_input.go raw-mode reader that honors ctx cancellation. Headline behaviour fix: when a non-interactive token (TPM2 PCR-only, touchless FIDO2, or clevis) wins the unlock race while the keyboard prompt is showing, the cooked reader stays blocked in read(2) — the prompt dangles, inputMutex/keyboardMu stay held, and any subsequent volumes that need keyboard entry block indefinitely until the user manually presses Enter on the now-pointless prompt. After this change the reader checks ctx.Done() between Poll(100ms) cycles, returns ctx.Err() within ~100ms of cancellation, restores termios via defer, ends the prompt line cleanly, and releases the locks. Scope is the keyboard-passphrase path only: - askPasswordWithFallback / readPassword / readPasswordLocked take ctx context.Context (threaded from requestKeyboardPassword, which is already ctx-aware after the prior PR in this series). - FIDO2-PIN and TPM2-PIN call sites pass context.Background() and continue to dangle on autounlock; a follow-up PR threads ctx into recoverFido2Password / recoverSystemdFido2Password / recoverSystemdTPM2Password to close that gap. Scanner design: - Per-byte FSM consuming ANSI CSI / SS3 / OSC / DCS sequences and bracketed-paste markers so they never enter the password buffer. - UTF-8 multi-byte reassembly across read boundaries with overlong / surrogate / >U+10FFFF rejection. - Editing keys: Backspace, Delete, Ctrl+W (word kill), Ctrl+U (line kill), Tab (mask toggle), Enter (CR/LF/Ctrl+D). - Inspired by Plymouth's on_key_event in src/libply-splash-core/ply-keyboard.c with intentional divergences (kept ISIG so Ctrl+C still cancels; accept BS and DEL; proper word-kill; OSC/DCS/bracketed-paste handling; bounded CSI parameter accumulation; UTF-8 validation). UTF-8 helpers are direct ports of ply_utf8_*; the scanner itself is reimplemented as a per-byte FSM rather than a slice-scan. Bracketed-paste support has a specific forward-compat value for the in-flight remote SSH unlock work (PR #320): SSH users typically paste passphrases from password managers, and modern terminal emulators bracket the pasted content with \x1b[200~ ... \x1b[201~ markers. A cooked-mode reader would treat those markers as literal password bytes and corrupt the entry. K's scanner consumes the markers and preserves the pasted content verbatim. Termios: ECHO/ICANON/IEXTEN/IXON/IXOFF/BRKINT/INPCK/ISTRIP/INLCR/ IGNCR cleared in raw mode; ISIG kept on. VMIN=1, VTIME=0. Every return path restores the original termios via defer. Notably IXON off means Ctrl+S no longer freezes the prompt. No new dependency: the scanner is ~440 LOC of stdlib Go. Surveyed alternatives (golang.org/x/term, charmbracelet/x/input, peterh/liner, eiannone/keyboard, mattn/go-tty) either don't support cancellable raw reads or pull substantial transitive deps inappropriate for an initramfs.
Replaces console.go's readPasswordLine / readPasswordLocked / readPassword (cooked-termios bufio over stdin) with a new console_input.go raw-mode reader that honors ctx cancellation. Headline behaviour fix: when a non-interactive token (TPM2 PCR-only, touchless FIDO2, or clevis) wins the unlock race while the keyboard prompt is showing, the cooked reader stays blocked in read(2) — the prompt dangles, inputMutex/keyboardMu stay held, and any subsequent volumes that need keyboard entry block indefinitely until the user manually presses Enter on the now-pointless prompt. After this change the reader checks ctx.Done() between Poll(100ms) cycles, returns ctx.Err() within ~100ms of cancellation, restores termios via defer, ends the prompt line cleanly, and releases the locks. Scope is the keyboard-passphrase path only: - askPasswordWithFallback / readPassword / readPasswordLocked take ctx context.Context (threaded from requestKeyboardPassword, which is already ctx-aware after the prior PR in this series). - FIDO2-PIN and TPM2-PIN call sites pass context.Background() and continue to dangle on autounlock; a follow-up PR threads ctx into recoverFido2Password / recoverSystemdFido2Password / recoverSystemdTPM2Password to close that gap. Scanner design: - Per-byte FSM consuming ANSI CSI / SS3 / OSC / DCS sequences and bracketed-paste markers so they never enter the password buffer. - UTF-8 multi-byte reassembly across read boundaries with overlong / surrogate / >U+10FFFF rejection. - Editing keys: Backspace, Delete, Ctrl+W (word kill), Ctrl+U (line kill), Tab (mask toggle), Enter (CR/LF/Ctrl+D). - Inspired by Plymouth's on_key_event in src/libply-splash-core/ply-keyboard.c with intentional divergences (kept ISIG so Ctrl+C still cancels; accept BS and DEL; proper word-kill; OSC/DCS/bracketed-paste handling; bounded CSI parameter accumulation; UTF-8 validation). UTF-8 helpers are direct ports of ply_utf8_*; the scanner itself is reimplemented as a per-byte FSM rather than a slice-scan. Bracketed-paste support has a specific forward-compat value for the in-flight remote SSH unlock work (PR anatol#320): SSH users typically paste passphrases from password managers, and modern terminal emulators bracket the pasted content with \x1b[200~ ... \x1b[201~ markers. A cooked-mode reader would treat those markers as literal password bytes and corrupt the entry. K's scanner consumes the markers and preserves the pasted content verbatim. Termios: ECHO/ICANON/IEXTEN/IXON/IXOFF/BRKINT/INPCK/ISTRIP/INLCR/ IGNCR cleared in raw mode; ISIG kept on. VMIN=1, VTIME=0. Every return path restores the original termios via defer. Notably IXON off means Ctrl+S no longer freezes the prompt. No new dependency: the scanner is ~440 LOC of stdlib Go. Surveyed alternatives (golang.org/x/term, charmbracelet/x/input, peterh/liner, eiannone/keyboard, mattn/go-tty) either don't support cancellable raw reads or pull substantial transitive deps inappropriate for an initramfs.
|
Hey @kenshaw — with 0.13 close, @anatol asked about updates here. Want to give you first dibs before doing more. I've been deep in the LUKS + Plymouth paths the review feedback points to (PRs #331, #347, #362, etc.), and I sketched a rework on a fresh branch — stdlib If you have time to drive #320 through, awesome — happy to share notes or stand down. If not, happy to open a PR with you as co-author on the import commit and address the review. Whichever you prefer — let me know either way. Quick context on what "existing prompt machinery" means — since March the keyboard path picked up Plymouth integration, a |
|
Hi @kenshaw Do you have any objections if @pilotstew look at this feature request? This is the last big feature and it worth resolving it before the upcoming release. |
|
@anatol / @pilotstew Yes, please continue -- I'm not sure if I'll be able to get this done as I had originally suggested. Love the project, using the |
Adds a SSH server that enables unlocking LUKS devices with a passphrase. The SSH server is enabled only when the
network:configuration is defined, and when eitherssh_pass:orssh_authorized_keys:is defined undernetwork:.Example configuration:
When enabled as above, the Booster
initprocess will start a SSH server at boot listening oen all interfaces on port 2222, using the specified server private key, and only allowing logins frommy_user(default user isrootwhen not specified).Additionally, password authentication will be available (the password above is hash of "password"), as well as public-key authentication for the provided authorized key. If
ssh_pass:is not provided, then password authentication will not be available.Similarly, if
ssh_authorized_keys:is not provided, then public-key authentication will not be available.When both
ssh_pass:andssh_authorized_keys:are not provided, the SSH server will not be started.An example remote unlock session:
$ ssh -p 2222 my_user@example.com Enter passphrase: Attempting unlock... device /dev/md1 slot #0 matches password device /dev/md1 unlocked successfully Connection to localhost closed.The
ssh_pass:in the example configuration above was generated as follows:Any standard crypt-style prefixed password hashes, as well as plaintext passwords are supported for
ssh_pass:.See the
github.com/go-crypt/cryptpackage for more information.If
ssh_server_keys:is not provided, a random ECDSA key will be generated and used instead.Implements #191.