From: Daan De Meyer Date: Fri, 15 Dec 2023 09:55:23 +0000 (+0100) Subject: Use mkosi.key/mkosi.crt for SSH authentication X-Git-Tag: v20~57^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=refs%2Fpull%2F2182%2Fhead;p=thirdparty%2Fmkosi.git Use mkosi.key/mkosi.crt for SSH authentication 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. --- diff --git a/NEWS.md b/NEWS.md index f287e1f1b..cef198360 100644 --- 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 diff --git a/mkosi.conf.d/20-centos-fedora/mkosi.conf b/mkosi.conf.d/20-centos-fedora/mkosi.conf index 0c3e1cfe4..6407998e7 100644 --- a/mkosi.conf.d/20-centos-fedora/mkosi.conf +++ b/mkosi.conf.d/20-centos-fedora/mkosi.conf @@ -24,6 +24,7 @@ Packages= kernel-core mtools openssh-clients + openssh-server openssl python3-cryptography qemu-kvm-core diff --git a/mkosi.conf.d/20-debian-ubuntu.conf b/mkosi.conf.d/20-debian-ubuntu.conf index 2aa934627..3fa9b7c11 100644 --- a/mkosi.conf.d/20-debian-ubuntu.conf +++ b/mkosi.conf.d/20-debian-ubuntu.conf @@ -26,6 +26,7 @@ Packages= libtss2-dev mtools openssh-client + openssh-server openssl ovmf pacman-package-manager diff --git a/mkosi.conf.d/20-opensuse.conf b/mkosi.conf.d/20-opensuse.conf index 409fc5920..522b07b7a 100644 --- a/mkosi.conf.d/20-opensuse.conf +++ b/mkosi.conf.d/20-opensuse.conf @@ -26,6 +26,7 @@ Packages= kernel-kvmsmall mtools openssh-clients + openssh-server openssl ovmf pesign diff --git a/mkosi/__init__.py b/mkosi/__init__.py index b892c2e74..fa1d86984 100644 --- a/mkosi/__init__.py +++ b/mkosi/__init__.py @@ -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", diff --git a/mkosi/config.py b/mkosi/config.py index 073aa74a1..948d7649b 100644 --- a/mkosi/config.py +++ b/mkosi/config.py @@ -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} diff --git a/mkosi/qemu.py b/mkosi/qemu.py index c859a7108..611c04345 100644 --- a/mkosi/qemu.py +++ b/mkosi/qemu.py @@ -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", diff --git a/mkosi/resources/mkosi.md b/mkosi/resources/mkosi.md index ed5b7bc1b..700d6e2d1 100644 --- a/mkosi/resources/mkosi.md +++ b/mkosi/resources/mkosi.md @@ -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 diff --git a/mkosi/run.py b/mkosi/run.py index ebb824220..56533bd97 100644 --- a/mkosi/run.py +++ b/mkosi/run.py @@ -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)) diff --git a/tests/test_json.py b/tests/test_json.py index 41e159f98..7f452532e 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -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,