diff --git a/nixos/doc/manual/release-notes/rl-2611.section.md b/nixos/doc/manual/release-notes/rl-2611.section.md index f9ff54192bba..71ec7b1b98ec 100644 --- a/nixos/doc/manual/release-notes/rl-2611.section.md +++ b/nixos/doc/manual/release-notes/rl-2611.section.md @@ -409,6 +409,8 @@ The above example can be migrated via the following structured code block: ``` ::: +- The new {option}`programs.ssh.hosts` option can be used to add `Host` blocks to the client {file}`ssh_config`, keyed by `Host` pattern, instead of writing them by hand via {option}`programs.ssh.extraConfig`. Each block declares an {option}`programs.ssh.hosts..order`, since `ssh_config` applies the first obtained value for each keyword. + - NixOS VM tests now prefer to express durations and timeouts as `datetime.timedelta` values instead of bare numbers. Methods such as `machine.wait_until_succeeds`, `machine.sleep`, `retry`, and `polling_condition` now accept a `timedelta` (e.g., `machine.wait_for_unit("sshd.service", timeout=datetime.timedelta(minutes=1))`). Passing an `int`/`float` as seconds still works but now emits a deprecation warning. Argument names that explicitly defined units were preserved but have had `timedelta` equivalents introduced (`timeout_seconds` → `timeout`, `secs` → `duration`, `seconds_interval` → `interval`). - `darwin.linux-builder-vz` has been added: a variant of `darwin.linux-builder` that runs the builder guest on Apple's Virtualization.framework via the new `vzvm` package, translating `x86_64-linux` builds with Rosetta instead of emulating them. Apple silicon hosts only. As part of this, the `nixos/modules/profiles/nix-builder-vm.nix` profile has been split into the backend-neutral `nixos/modules/profiles/nix-builder.nix` and a QEMU-specific part. Existing imports of `nix-builder-vm.nix` keep working unchanged. diff --git a/nixos/modules/programs/ssh.nix b/nixos/modules/programs/ssh.nix index f883abb2b62c..8fa7fc473066 100644 --- a/nixos/modules/programs/ssh.nix +++ b/nixos/modules/programs/ssh.nix @@ -35,6 +35,40 @@ let ] ++ map pkgs.copyPathToStore cfg.knownHostsFiles; + sshConfigValue = v: if builtins.isBool v then lib.boolToYesNo v else toString v; + + hostSettingsText = lib.generators.toKeyValue { + mkKeyValue = lib.generators.mkKeyValueDefault { + mkValueString = sshConfigValue; + } " "; + indent = " "; + }; + + hostsText = lib.pipe cfg.hosts [ + (lib.mapAttrsToList ( + pattern: hostCfg: + # `order` is forced here so that it is required for a lone block too, + # which `sort` would otherwise never compare against anything. + lib.seq hostCfg.order { + inherit pattern; + inherit (hostCfg) order; + settings = removeAttrs hostCfg [ "order" ]; + } + )) + # `ssh_config(5)` applies the first obtained value for each keyword, so the + # blocks are written in ascending `order`, and sharing an `order` value is + # an error rather than an arbitrary choice between the two blocks. + (lib.sort ( + a: b: + if a.order != b.order then + a.order < b.order + else + throw "programs.ssh.hosts: blocks '${a.pattern}' and '${b.pattern}' cannot have the same order value (${toString a.order})" + )) + (map (host: "Host ${host.pattern}\n" + hostSettingsText host.settings)) + (lib.concatStringsSep "\n") + ]; + in { ###### interface @@ -124,6 +158,76 @@ in ''; }; + hosts = lib.mkOption { + default = { }; + type = lib.types.attrsOf ( + lib.types.submodule { + freeformType = lib.types.attrsOf ( + lib.types.oneOf [ + lib.types.bool + lib.types.int + lib.types.str + ] + ); + options.order = lib.mkOption { + type = lib.types.int; + example = 100; + description = '' + Position of this block in {file}`ssh_config`. Blocks are + written in ascending order of this value, and two blocks + may not share a value. + + Since {manpage}`ssh_config(5)` applies the *first* obtained + value for each keyword, a block with a lower value takes + precedence over the blocks that follow it. In particular, a + catch-all pattern such as `"*"` should have the highest + value, so that it does not shadow the more specific blocks. + + To place a block relative to one that another module + declares, derive this value from that block's one instead of + hardcoding it, e.g. + `config.programs.ssh.hosts."*".order - 100`. + ''; + }; + } + ); + description = '' + Per-host (or host-pattern) client configuration, added as `Host` + blocks to {file}`ssh_config`. + + Attribute names are used as the `Host` pattern, as accepted by + {manpage}`ssh_config(5)`. Several space-separated patterns can be + given to apply one block to all of them, e.g. + `"foo bar *.baz.org"`. + + Every attribute of a block besides {option}`order` becomes a + `Key Value` line within that block; boolean values are rendered as + `yes`/`no`. + ''; + example = lib.literalExpression '' + { + jumphost = { + order = 100; + User = "deploy"; + Port = 2222; + IdentityFile = "/root/.ssh/id_deploy"; + IdentitiesOnly = true; + }; + "*.example.org" = { + order = 200; + ControlMaster = "auto"; + ControlPath = "/root/.ssh/master-%r@%n:%p"; + ControlPersist = "5m"; + }; + # Written last, so that it does not shadow the blocks above. + "*" = { + order = 300; + ServerAliveInterval = 60; + }; + } + ''; + }; + startAgent = lib.mkOption { type = lib.types.bool; default = false; @@ -342,6 +446,7 @@ in environment.etc."ssh/ssh_config".text = lib.concatStringsSep "\n" ( # Custom options from `extraConfig`, to override generated options lib.optional (cfg.extraConfig != "") cfg.extraConfig + ++ lib.optional (cfg.hosts != { }) hostsText ++ [ '' # Generated options from other settings