]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
Use mkosi.key/mkosi.crt for SSH authentication 2182/head
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Fri, 15 Dec 2023 09:55:23 +0000 (10:55 +0100)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Sat, 16 Dec 2023 19:49:02 +0000 (20:49 +0100)
Instead of using the user's SSH certificate and key, let's use the
X509 certificate and private key generated by 'mkosi genkey' instead.

This saves us from having to rely on ssh-agent to get the public key
or doing otherwise complicated logic to try and find the public and
private key. We also avoid always needing a separate public/private
key just for SSH by automatically converting the X509 certificate to
a SSH public key.

NEWS.md
mkosi.conf.d/20-centos-fedora/mkosi.conf
mkosi.conf.d/20-debian-ubuntu.conf
mkosi.conf.d/20-opensuse.conf
mkosi/__init__.py
mkosi/config.py
mkosi/qemu.py
mkosi/resources/mkosi.md
mkosi/run.py
tests/test_json.py

diff --git a/NEWS.md b/NEWS.md
index f287e1f1b1662fcf8fc8ae61e3290f538d8eeceb..cef198360b0cbb22732bece7fc681e2060a99eaf 100644 (file)
--- a/NEWS.md
+++ b/NEWS.md
@@ -2,6 +2,11 @@
 
 ## v20
 
+- We don't use the user's SSH public/private keypair anymore for
+  `mkosi ssh` but instead use a separate key pair which can be
+  generated by `mkosi genkey`. Users using `mkosi ssh` will have to run
+  `mkosi genkey` once to generate the necessary files to keep
+  `mkosi ssh` working.
 - We don't automatically set `--offline=no` anymore when we detect the
   `Subvolumes=` setting is used in a `systemd-repart` partition
   definition file. Instead, use the new `RepartOffline` option to
index 0c3e1cfe480cd72a009abfa7ebfe2d2910e0883c..6407998e79511b826b62aa8f148fa7a85b01ed40 100644 (file)
@@ -24,6 +24,7 @@ Packages=
         kernel-core
         mtools
         openssh-clients
+        openssh-server
         openssl
         python3-cryptography
         qemu-kvm-core
index 2aa934627a95263b50e5d571763a340013908e1f..3fa9b7c11b17bd8eaf8be6adb81f29fa90f5920f 100644 (file)
@@ -26,6 +26,7 @@ Packages=
         libtss2-dev
         mtools
         openssh-client
+        openssh-server
         openssl
         ovmf
         pacman-package-manager
index 409fc5920d5d54f812a3b47e71b09b766e05a96d..522b07b7aecdd403942e87517010225c02093a2d 100644 (file)
@@ -26,6 +26,7 @@ Packages=
         kernel-kvmsmall
         mtools
         openssh-clients
+        openssh-server
         openssl
         ovmf
         pesign
index b892c2e7423b854ba164c38d188d39f0761e55fa..fa1d86984a7e05251d9d5ffaf0373efead220087 100644 (file)
@@ -63,10 +63,11 @@ from mkosi.run import (
     fork_and_wait,
     init_mount_namespace,
     run,
+    run_openssl,
 )
 from mkosi.state import MkosiState
 from mkosi.tree import copy_tree, install_tree, move_tree, rmtree
-from mkosi.types import _FILE, CompletedProcess, PathString
+from mkosi.types import PathString
 from mkosi.util import (
     INVOKING_USER,
     chdir,
@@ -668,11 +669,6 @@ def run_finalize_scripts(state: MkosiState) -> None:
                 )
 
 
-def run_openssl(args: Sequence[PathString], stdout: _FILE = None) -> CompletedProcess:
-    with tempfile.NamedTemporaryFile(prefix="mkosi-openssl.cnf") as config:
-        return run(["openssl", *args], stdout=stdout, env=dict(OPENSSL_CONF=config.name))
-
-
 def certificate_common_name(certificate: Path) -> str:
     output = run_openssl([
         "x509",
index 073aa74a1113ffae6a7e8f53165597965fe0dde4..948d7649b31b7d5d26c5fe5021ddbd19094eaf1c 100644 (file)
@@ -31,7 +31,7 @@ from mkosi.architecture import Architecture
 from mkosi.distributions import Distribution, detect_distribution
 from mkosi.log import ARG_DEBUG, ARG_DEBUG_SHELL, Style, die
 from mkosi.pager import page
-from mkosi.run import run
+from mkosi.run import run, run_openssl
 from mkosi.types import PathString, SupportsRead
 from mkosi.util import INVOKING_USER, StrEnum, chdir, flatten, is_power_of_2
 from mkosi.versioncomp import GenericVersion
@@ -1016,6 +1016,8 @@ class MkosiConfig:
     tools_tree_packages: list[str]
     runtime_trees: list[ConfigTree]
     runtime_size: Optional[int]
+    ssh_key: Optional[Path]
+    ssh_certificate: Optional[Path]
 
     # QEMU-specific options
     qemu_gui: bool
@@ -2107,6 +2109,22 @@ SETTINGS = (
         parse=config_parse_bytes,
         help="Grow disk images to the specified size before booting them",
     ),
+    MkosiConfigSetting(
+        dest="ssh_key",
+        metavar="PATH",
+        section="Host",
+        parse=config_make_path_parser(secret=True),
+        paths=("mkosi.key",),
+        help="Private key for use with mkosi ssh in PEM format",
+    ),
+    MkosiConfigSetting(
+        dest="ssh_certificate",
+        metavar="PATH",
+        section="Host",
+        parse=config_make_path_parser(),
+        paths=("mkosi.crt",),
+        help="Certificate for use with mkosi ssh in X509 format",
+    ),
     MkosiConfigSetting(
         dest="qemu_gui",
         metavar="BOOL",
@@ -2829,19 +2847,16 @@ def load_credentials(args: argparse.Namespace) -> dict[str, str]:
     if "firstboot.locale" not in creds:
         creds["firstboot.locale"] = "C.UTF-8"
 
-    if (
-        args.ssh and
-        "ssh.authorized_keys.root" not in creds and
-        "SSH_AUTH_SOCK" in os.environ and shutil.which("ssh-add")
-    ):
-        key = run(
-            ["ssh-add", "-L"],
-            stdout=subprocess.PIPE,
-            env=os.environ,
-            check=False,
-        ).stdout.strip()
-        if key:
-            creds["ssh.authorized_keys.root"] = key
+    if "ssh.authorized_keys.root" not in creds:
+        if args.ssh_certificate:
+            pubkey = run_openssl(["x509", "-in", args.ssh_certificate, "-pubkey", "-noout"],
+                                 stdout=subprocess.PIPE).stdout.strip()
+            sshpubkey = run(["ssh-keygen", "-f", "/dev/stdin", "-i", "-m", "PKCS8"],
+                            input=pubkey, stdout=subprocess.PIPE).stdout.strip()
+            creds["ssh.authorized_keys.root"] = sshpubkey
+        elif args.ssh:
+            die("Ssh= is enabled but no SSH certificate was found",
+                hint="Run 'mkosi genkey' to automatically create one")
 
     return creds
 
@@ -3182,6 +3197,8 @@ def summary(config: MkosiConfig) -> str:
            Tools Tree Packages: {line_join_list(config.tools_tree_packages)}
                  Runtime Trees: {line_join_tree_list(config.runtime_trees)}
                   Runtime Size: {format_bytes_or_none(config.runtime_size)}
+               SSH Signing Key: {none_to_none(config.ssh_key)}
+               SSH Certificate: {none_to_none(config.ssh_certificate)}
 
                       QEMU GUI: {yes_no(config.qemu_gui)}
                 QEMU CPU Cores: {config.qemu_smp}
index c859a710839b0b01680088aa2cdb6491a877d41c..611c043451a1c05911d350965538314a3d43bedd 100644 (file)
@@ -788,13 +788,18 @@ def run_ssh(args: MkosiArgs, config: MkosiConfig) -> None:
     if config.qemu_vsock_cid == QemuVsockCID.auto:
         die("Can't use ssh verb with QemuVSockCID=auto")
 
+    if not config.ssh_key:
+        die("SshKey= must be configured to use 'mkosi ssh'",
+            hint="Use 'mkosi genkey' to generate a new SSH key and certificate")
+
     if config.qemu_vsock_cid == QemuVsockCID.hash:
         cid = hash_to_vsock_cid(hash_output(config))
     else:
         cid = config.qemu_vsock_cid
 
-    cmd = [
+    cmd: list[PathString] = [
         "ssh",
+        "-i", config.ssh_key,
         "-F", "none",
         # Silence known hosts file errors/warnings.
         "-o", "UserKnownHostsFile=/dev/null",
index ed5b7bc1bda10ee40486909d742ccf3b5dfe512f..700d6e2d197ee8424539b9c57b2eafc8a6cfb48e 100644 (file)
@@ -93,11 +93,14 @@ The following command line verbs are known:
 
 : When the image is built with the `Ssh=yes` option, this command
   connects to a booted virtual machine (`qemu`) via SSH. Make sure to
-  run `mkosi ssh` with the same config as `mkosi build` was run with so
-  that it has the necessary information available to connect to the
-  running virtual machine via SSH. Any arguments passed after the `ssh`
-  verb are passed as arguments to the `ssh` invocation. To connect to a
-  container, use `machinectl login` or `machinectl shell`.
+  run `mkosi ssh` with the same config as `mkosi build` so that it has
+  the necessary information available to connect to the running virtual
+  machine via SSH. Specifically, the SSH private key from the `SshKey=`
+  setting is used to connect to the virtual machine. Use `mkosi genkey`
+  to automatically generate a key and certificate that will be picked up
+  by mkosi. Any arguments passed after the `ssh` verb are passed as
+  arguments to the `ssh` invocation. To connect to a container, use
+  `machinectl login` or `machinectl shell`.
 
 `journalctl`
 
@@ -1186,11 +1189,10 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`,
   option and running the image using `mkosi qemu`, the `mkosi ssh`
   command can be used to connect to the container/VM via SSH. Note that
   you still have to make sure openssh is installed in the image to make
-  this option behave correctly. mkosi will automatically provision the
-  user's public SSH key into the image using the
-  `ssh.authorized_keys.root` credential if it can be retrieved from a
-  running SSH agent. To access images booted using `mkosi boot`, use
-  `machinectl`.
+  this option behave correctly. Run `mkosi genkey` to automatically
+  generate an X509 certificate and private key to be used by mkosi to
+  enable SSH access to any virtual machines via `mkosi ssh`. To access
+  images booted using `mkosi boot`, use `machinectl`.
 
 ### [Validation] Section
 
@@ -1542,6 +1544,23 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`,
   Additionally, the suffixes `K`, `M` and `G` can be used to specify a
   size in kilobytes, megabytes and gigabytes respectively.
 
+`SshKey=`, `--ssh-key=`
+
+: Path to the X509 private key in PEM format to use to connect to a
+  virtual machine started with `mkosi qemu` and built with the `Ssh=`
+  option enabled via the `mkosi ssh` command. If not configured and
+  `mkosi.key` exists in the working directory, it will automatically be
+  used for this purpose. Run `mkosi genkey` to automatically generate
+  a key in `mkosi.key`.
+
+`SshCertificate=`, `--ssh-certificate=`
+
+: Path to the X509 certificate in PEM format to provision as the SSH
+  public key in virtual machines started with `mkosi qemu`.  If not
+  configured and `mkosi.crt` exists in the working directory, it will
+  automatically be used for this purpose. Run `mkosi genkey` to
+  automatically generate a certificate in `mkosi.crt`.
+
 ## Specifiers
 
 The current value of various settings can be accessed when parsing
index ebb8242207ffb41bf92e5efa63792c69c84bae91..56533bd97caddb84a553e5b7fb49a17a64e5de1b 100644 (file)
@@ -17,6 +17,7 @@ import shutil
 import signal
 import subprocess
 import sys
+import tempfile
 import threading
 from collections.abc import Awaitable, Collection, Iterator, Mapping, Sequence
 from pathlib import Path
@@ -602,3 +603,8 @@ class MkosiAsyncioThread(threading.Thread):
                 raise self.exc.get_nowait()
             except queue.Empty:
                 pass
+
+
+def run_openssl(args: Sequence[PathString], stdout: _FILE = None) -> CompletedProcess:
+    with tempfile.NamedTemporaryFile(prefix="mkosi-openssl.cnf") as config:
+        return run(["openssl", *args], stdout=stdout, env=dict(OPENSSL_CONF=config.name))
index 41e159f9819fe743721523c8a90546be4debc71b..7f452532e786cafaccb25842c1537e10eef48ae6 100644 (file)
@@ -269,6 +269,8 @@ def test_config() -> None:
             "SourceDateEpoch": 12345,
             "SplitArtifacts": true,
             "Ssh": false,
+            "SshCertificate": "/path/to/cert",
+            "SshKey": null,
             "Timezone": null,
             "ToolsTree": null,
             "ToolsTreeDistribution": null,
@@ -391,6 +393,8 @@ def test_config() -> None:
         source_date_epoch = 12345,
         split_artifacts = True,
         ssh = False,
+        ssh_certificate = Path("/path/to/cert"),
+        ssh_key = None,
         timezone = None,
         tools_tree = None,
         tools_tree_distribution = None,