Skip to content

Add remote ssh LUKS device unlocking#320

Open
kenshaw wants to merge 1 commit intoanatol:masterfrom
kenshaw:add-ssh-remote-unlocking
Open

Add remote ssh LUKS device unlocking#320
kenshaw wants to merge 1 commit intoanatol:masterfrom
kenshaw:add-ssh-remote-unlocking

Conversation

@kenshaw
Copy link
Copy Markdown

@kenshaw kenshaw commented Mar 18, 2026

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:

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:

$ 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:

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 package for more information.

If ssh_server_keys: is not provided, a random ECDSA key will be generated and used instead.

Implements #191.

@kenshaw kenshaw mentioned this pull request Mar 18, 2026
@kenshaw kenshaw force-pushed the add-ssh-remote-unlocking branch 2 times, most recently from c84c112 to 9b43f1a Compare March 19, 2026 01:21
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.
@kenshaw kenshaw force-pushed the add-ssh-remote-unlocking branch from 9b43f1a to cf80785 Compare March 19, 2026 01:24
Copy link
Copy Markdown
Owner

@anatol anatol left a comment

Choose a reason for hiding this comment

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

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?

Comment thread init/ssh.go

// sshSessionHandler handles ssh sessions.
func sshSessionHandler(sess ssh.Session) {
warning("ssh: session %q [%s]: opened", sess.User(), sess.RemoteAddr())
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

it should be either debug or info level

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

OK.

Comment thread go.mod
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
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

Comment thread generator/config.go
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"`
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

you use both Password and AuthorizedKeys. I propose to leave only AuthorizedKeys as a more secure approach.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

Comment thread init/main.go
// ensure secure key
if config.Network.SshServerKeys == "" {
var err error
if config.Network.SshServerKeys, err = sshGenEcdsaKey(rand.Reader); err != nil {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

Comment thread init/ssh.go
}

// check if mapped device
mapping := matchLuksMapping(blk)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

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