]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
ssh: optionally connect to ssh-agent
authorLuca Boccassi <luca.boccassi@microsoft.com>
Mon, 1 Nov 2021 23:38:37 +0000 (23:38 +0000)
committerLuca Boccassi <luca.boccassi@microsoft.com>
Fri, 5 Nov 2021 10:34:06 +0000 (10:34 +0000)
If a path to the ssh-agent socket (typically /run/user/1000/gnupg/S.gpg-agent.ssh) is passed,
connect to it to fetch the public key(s) with ssh-add -L and avoid passing
the key(s) manually to ssh.

mkosi.md
mkosi/__init__.py
mkosi/backend.py
tests/test_config_parser.py

index 6fc6769e433f8716449b1d99742d3ec424f41c1a..be3be71daef691671741cfa9cf8e1d438904b1bd 100644 (file)
--- a/mkosi.md
+++ b/mkosi.md
@@ -1143,6 +1143,15 @@ a boolean argument: either "1", "yes", or "true" to enable, or "0",
   the same location, suffixed with `.pub` (as done by `ssh-keygen`). If this
   option is not present, `mkosi` generates a new key pair automatically.
 
+`SshAgent=`, `--ssh-agent=`
+
+: If specified as a path, use the given socket to connect to the ssh agent when
+  building an image and when connecting via `mkosi ssh` instead of hard-coding
+  a key. If specified as `true`, `$SSH_AUTH_SOCK` will be parsed instead (hint:
+  use `sudo` with `-E`). The keys listed by `ssh-add -L` will be installed as
+  authorized keys in the built image. The `ssh` invocation done by `mkosi ssh`
+  will inherit `$SSH_AUTH_SOCK` for authentication purposes.
+
 `SshTimeout=`, `--ssh-timeout=`
 
 : When used with the `ssh` verb, `mkosi` will attempt to retry the SSH connection
index 467410faedb82fcef3179d88cb24e8fb345b5367..2678b896ac98810db37e2c5716ddd8cdc981b53f 100644 (file)
@@ -4846,6 +4846,28 @@ def parse_remove_files(value: str) -> List[str]:
     return ["/" + os.path.normpath(p).lstrip("/") for p in value.split(",") if p]
 
 
+def parse_ssh_agent(value: Optional[str]) -> Optional[Path]:
+    """Will return None or a path to a socket."""
+
+    if value is None:
+        return None
+
+    try:
+        if not parse_boolean(value):
+            return None
+    except ValueError:
+        pass
+    else:
+        value = os.getenv("SSH_AUTH_SOCK")
+        if not value:
+            die("--ssh-agent=true but $SSH_AUTH_SOCK is not set (consider running 'sudo' with '-E')")
+
+    sock = Path(value)
+    if not sock.is_socket():
+        die(f"SSH agent socket {sock} is not an AF_UNIX socket")
+    return sock
+
+
 def create_parser() -> ArgumentParserMkosi:
     parser = ArgumentParserMkosi(prog="mkosi", description="Build Bespoke OS Images", add_help=False)
 
@@ -5366,6 +5388,13 @@ def create_parser() -> ArgumentParserMkosi:
         default=0,
         help="Wait up to SECONDS seconds for the SSH connection to be available when using 'mkosi ssh'",
     )
+    group.add_argument(
+        "--ssh-agent",
+        type=parse_ssh_agent,
+        default=None,
+        metavar="PATH",
+        help="Path to the ssh agent socket, or true to use $SSH_AUTH_SOCK.",
+    )
 
     group = parser.add_argument_group("Additional Configuration")
     group.add_argument(
@@ -6156,7 +6185,7 @@ def load_args(args: argparse.Namespace) -> CommandLineArguments:
         args.output_nspawn_settings = build_auxiliary_output_path(args, ".nspawn")
 
     # We want this set even if --ssh is not specified so we can find the SSH key when verb == "ssh".
-    if args.ssh_key is None:
+    if args.ssh_key is None and args.ssh_agent is None:
         args.output_sshkey = args.output.with_name("id_rsa")
 
     if args.split_artifacts:
@@ -6432,7 +6461,7 @@ def print_summary(args: CommandLineArguments) -> None:
         f"    Output nspawn Settings: {none_to_na(args.output_nspawn_settings if args.nspawn_settings is not None else None)}"
     )
     MkosiPrinter.info(
-        f"                   SSH key: {none_to_na((args.ssh_key or args.output_sshkey) if args.ssh else None)}"
+        f"                   SSH key: {none_to_na((args.ssh_key or args.output_sshkey or args.ssh_agent) if args.ssh else None)}"
     )
 
     MkosiPrinter.info("               Incremental: " + yes_no(args.incremental))
@@ -6604,10 +6633,15 @@ def setup_ssh(
         return None
 
     authorized_keys = root_home(args, root) / ".ssh/authorized_keys"
-    f: TextIO
+    f: Optional[TextIO]
     if args.ssh_key:
         f = open(args.ssh_key, mode="r", encoding="utf-8")
         copy_file(f"{args.ssh_key}.pub", authorized_keys)
+    elif args.ssh_agent is not None:
+        env = {"SSH_AUTH_SOCK": args.ssh_agent}
+        result = run(["ssh-add", "-L"], env=env, text=True, stdout=subprocess.PIPE)
+        authorized_keys.write_text(result.stdout)
+        f = None
     else:
         assert args.output_sshkey is not None
 
@@ -7403,36 +7437,33 @@ def find_address(args: CommandLineArguments) -> Tuple[str, str]:
 
 
 def run_ssh(args: CommandLineArguments) -> None:
-    ssh_key = args.ssh_key or args.output_sshkey
-    assert ssh_key is not None
+    cmd = [
+            "ssh",
+            # Silence known hosts file errors/warnings.
+            "-o", "UserKnownHostsFile=/dev/null",
+            "-o", "StrictHostKeyChecking=no",
+            "-o", "LogLevel=ERROR",
+        ]
 
-    if not ssh_key.exists():
-        die(
-            f"SSH key not found at {ssh_key}. Are you running from the project's root directory "
-            "and did you build with the --ssh option?"
-        )
+    if args.ssh_agent is None:
+        ssh_key = args.ssh_key or args.output_sshkey
+        assert ssh_key is not None
+
+        if not ssh_key.exists():
+            die(
+                f"SSH key not found at {ssh_key}. Are you running from the project's root directory "
+                "and did you build with the --ssh option?"
+            )
+
+        cmd += ["-i", cast(str, ssh_key)]
+    else:
+        cmd += ["-o", f"IdentityAgent={args.ssh_agent}"]
 
     dev, address = find_address(args)
+    cmd += [f"root@{address}%{dev}", *args.cmdline]
 
     with suppress_stacktrace():
-        run(
-            [
-                "ssh",
-                "-i",
-                ssh_key,
-                # Silence known hosts file errors/warnings.
-                "-o",
-                "UserKnownHostsFile=/dev/null",
-                "-o",
-                "StrictHostKeyChecking=no",
-                "-o",
-                "LogLevel ERROR",
-                f"root@{address}%{dev}",
-                *args.cmdline,
-            ],
-            stdout=sys.stdout,
-            stderr=sys.stderr,
-        )
+        run(cmd, stdout=sys.stdout, stderr=sys.stderr)
 
 
 def run_serve(args: CommandLineArguments) -> None:
index 63047576874778d32dc135b5a24d0893995e0850..2400ea32e00bbf407f08a551decf06bdcbb681e2 100644 (file)
@@ -440,6 +440,7 @@ class CommandLineArguments:
     ephemeral: bool
     ssh: bool
     ssh_key: Optional[Path]
+    ssh_agent: Optional[Path]
     ssh_timeout: int
     directory: Optional[Path]
     default_path: Optional[Path]
index 3f15a1b5ede5fe12547ae0f4aa88f2c6fd2dca82..994a784f6f03da3112781e56485bd9f897870107 100644 (file)
@@ -134,6 +134,7 @@ class MkosiConfig:
             "ssh": False,
             "ssh_key": None,
             "ssh_timeout": 0,
+            "ssh_agent": None,
             "minimize": False,
             "split_artifacts": False,
             "output_split_root": None,