From: Luca Boccassi Date: Mon, 1 Nov 2021 23:38:37 +0000 (+0000) Subject: ssh: optionally connect to ssh-agent X-Git-Tag: v11~9^2~2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=ea06842a1e537292069aaabd441984a24845e2e9;p=thirdparty%2Fmkosi.git ssh: optionally connect to ssh-agent 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. --- diff --git a/mkosi.md b/mkosi.md index 6fc6769e4..be3be71da 100644 --- 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 diff --git a/mkosi/__init__.py b/mkosi/__init__.py index 467410fae..2678b896a 100644 --- a/mkosi/__init__.py +++ b/mkosi/__init__.py @@ -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: diff --git a/mkosi/backend.py b/mkosi/backend.py index 630475768..2400ea32e 100644 --- a/mkosi/backend.py +++ b/mkosi/backend.py @@ -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] diff --git a/tests/test_config_parser.py b/tests/test_config_parser.py index 3f15a1b5e..994a784f6 100644 --- a/tests/test_config_parser.py +++ b/tests/test_config_parser.py @@ -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,