From 09bffb616bf39a8bbc69bf3880266c93223144a9 Mon Sep 17 00:00:00 2001 From: r-vdp Date: Mon, 6 Jul 2026 16:13:18 +0200 Subject: [PATCH 1/2] nixos-rebuild-ng: allow overriding default ssh opts via environment nixos-rebuild-ng unconditionally appends ControlMaster/ControlPath/ ControlPersist options pointing at a private master under its tmpdir and closes that master at exit. Users who configure their own connection sharing (to reuse an interactive master and the port forwards attached to it, e.g. an agent socket forwarded with RemoteForward) get a second connection per deploy. That connection re-binds any RemoteForward from ssh_config and unlinks the socket again when the deploy exits, orphaning the forward held by the interactive session. Add NIXOS_REBUILD_SSH_DEFAULT_OPTS to replace the built-in defaults, with the empty string falling back to whatever connection sharing ssh_config sets up. --- .../ni/nixos-rebuild-ng/nixos-rebuild.8.scd | 5 ++ .../nixos-rebuild-ng/src/nixos_rebuild/nix.py | 4 +- .../src/nixos_rebuild/process.py | 19 +++++++- .../ni/nixos-rebuild-ng/src/tests/test_nix.py | 11 +++++ .../src/tests/test_process.py | 46 +++++++++++++++++++ 5 files changed, 81 insertions(+), 4 deletions(-) diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/nixos-rebuild.8.scd b/pkgs/by-name/ni/nixos-rebuild-ng/nixos-rebuild.8.scd index 218eaa8d395f..305c39a2c2ac 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/nixos-rebuild.8.scd +++ b/pkgs/by-name/ni/nixos-rebuild-ng/nixos-rebuild.8.scd @@ -379,6 +379,11 @@ NIX_PATH NIX_SSHOPTS Additional options to be passed to ssh on the command line. +NIXOS_REBUILD_SSH_DEFAULT_OPTS + Replaces the built-in default ssh options (connection sharing via a + private _ControlMaster_ that is closed on exit). If empty, no + default options are added. + NIX_SUDOOPTS Additional options to be passed to sudo on the command line. diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py index 4fbe7fbe2b9d..5a4be4d1a557 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py @@ -26,7 +26,7 @@ from .models import ( Profile, Remote, ) -from .process import SSH_DEFAULT_OPTS, run_wrapper +from .process import run_wrapper, ssh_default_opts from .utils import Args, dict_to_flags FLAKE_FLAGS: Final = ["--extra-experimental-features", "nix-command flakes"] @@ -193,7 +193,7 @@ def copy_closure( Also supports copying a closure from a remote to another remote.""" sshopts = os.getenv("NIX_SSHOPTS", "") - env = {"NIX_SSHOPTS": " ".join(filter(lambda x: x, [sshopts, *SSH_DEFAULT_OPTS]))} + env = {"NIX_SSHOPTS": " ".join(filter(lambda x: x, [sshopts, *ssh_default_opts()]))} def nix_copy_closure(host: Remote, to: bool) -> None: run_wrapper( diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/process.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/process.py index 7ba809d4cb9c..63b001a4af49 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/process.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/process.py @@ -31,6 +31,21 @@ SSH_DEFAULT_OPTS: Final = [ ] +def ssh_default_opts() -> list[str]: + """Default ssh options appended after NIX_SSHOPTS. + + NIXOS_REBUILD_SSH_DEFAULT_OPTS replaces the built-in connection + sharing defaults, e.g. to reuse an externally managed ControlMaster + and the port forwards attached to it. Set it to the empty string to + fall back to the connection sharing configured in ssh_config, if + any. + """ + env = os.getenv("NIXOS_REBUILD_SSH_DEFAULT_OPTS") + if env is None: + return SSH_DEFAULT_OPTS + return shlex.split(env) + + @dataclass(frozen=True) class Remote: host: str @@ -133,7 +148,7 @@ def run_wrapper( ssh_args: list[Arg] = [ "ssh", *remote.opts, - *SSH_DEFAULT_OPTS, + *ssh_default_opts(), remote.ssh_host(), "--", *[_quote_remote_arg(a) for a in remote_run_args], @@ -282,7 +297,7 @@ def _kill_long_running_ssh_process(args: Args, remote: Remote) -> None: [ "ssh", *remote.opts, - *SSH_DEFAULT_OPTS, + *ssh_default_opts(), remote.ssh_host(), "--", "pkill", diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py index 1cdccc9a874e..848a09bb742b 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py @@ -275,6 +275,17 @@ def test_copy_closure(monkeypatch: MonkeyPatch) -> None: }, ) + # NIXOS_REBUILD_SSH_DEFAULT_OPTS replaces the ControlMaster defaults + monkeypatch.setenv("NIX_SSHOPTS", "-oControlPath=/run/user/1000/%C") + monkeypatch.setenv("NIXOS_REBUILD_SSH_DEFAULT_OPTS", "") + with patch(get_qualified_name(n.run_wrapper, n), autospec=True) as mock_run: + n.copy_closure(closure, target_host) + mock_run.assert_called_with( + ["nix-copy-closure", "--to", "user@target.host", closure], + append_local_env={"NIX_SSHOPTS": "-oControlPath=/run/user/1000/%C"}, + ) + monkeypatch.delenv("NIXOS_REBUILD_SSH_DEFAULT_OPTS") + monkeypatch.setenv("NIX_SSHOPTS", "--ssh build-target-opt") env = {"NIX_SSHOPTS": " ".join(["--ssh build-target-opt", *p.SSH_DEFAULT_OPTS])} with patch(get_qualified_name(n.run_wrapper, n), autospec=True) as mock_run: diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_process.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_process.py index 2abdd26ebe07..c8f1ec9126e9 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_process.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_process.py @@ -234,6 +234,52 @@ def test_remote_from_name(monkeypatch: MonkeyPatch) -> None: ) +def test_ssh_default_opts(monkeypatch: MonkeyPatch) -> None: + monkeypatch.delenv("NIXOS_REBUILD_SSH_DEFAULT_OPTS", raising=False) + assert p.ssh_default_opts() == p.SSH_DEFAULT_OPTS + + monkeypatch.setenv( + "NIXOS_REBUILD_SSH_DEFAULT_OPTS", "-o ControlPath=/run/user/1000/%C" + ) + assert p.ssh_default_opts() == ["-o", "ControlPath=/run/user/1000/%C"] + + monkeypatch.setenv("NIXOS_REBUILD_SSH_DEFAULT_OPTS", "") + assert p.ssh_default_opts() == [] + + +@patch.dict( + p.os.environ, + {"PATH": "/path/to/bin", "NIXOS_REBUILD_SSH_DEFAULT_OPTS": ""}, + clear=True, +) +@patch("subprocess.run", autospec=True) +def test_run_wrapper_ssh_default_opts_override(mock_run: Any) -> None: + p.run_wrapper( + ["test"], + check=True, + remote=m.Remote("user@localhost", ["-p", "2222"], "ssh"), + ) + mock_run.assert_called_with( + [ + "ssh", + "-p", + "2222", + "user@localhost", + "--", + "/bin/sh", + "-c", + """'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""", + "sh", + "test", + ], + check=True, + text=True, + errors="surrogateescape", + env=None, + input=None, + ) + + def test_ssh_host() -> None: ssh_remotes = { "user@[fe80::1%25eth0]": "user@fe80::1%eth0", From 8de14381defab90bc1892551507585f0add4b0c4 Mon Sep 17 00:00:00 2001 From: Ramses Date: Tue, 7 Jul 2026 13:42:50 +0200 Subject: [PATCH 2/2] nixos-rebuild-ng: reword docstring Co-authored-by: Thiago Kenji Okada --- .../ni/nixos-rebuild-ng/src/nixos_rebuild/process.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/process.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/process.py index 63b001a4af49..3ae6afa9c68c 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/process.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/process.py @@ -32,14 +32,7 @@ SSH_DEFAULT_OPTS: Final = [ def ssh_default_opts() -> list[str]: - """Default ssh options appended after NIX_SSHOPTS. - - NIXOS_REBUILD_SSH_DEFAULT_OPTS replaces the built-in connection - sharing defaults, e.g. to reuse an externally managed ControlMaster - and the port forwards attached to it. Set it to the empty string to - fall back to the connection sharing configured in ssh_config, if - any. - """ + "Default ssh options appended after NIX_SSHOPTS." env = os.getenv("NIXOS_REBUILD_SSH_DEFAULT_OPTS") if env is None: return SSH_DEFAULT_OPTS