diff --git a/nixos/lib/test-driver/default.nix b/nixos/lib/test-driver/default.nix index 77638397d11c..03cfcb6915b3 100644 --- a/nixos/lib/test-driver/default.nix +++ b/nixos/lib/test-driver/default.nix @@ -18,8 +18,6 @@ netpbm, vhost-device-vsock, nixosTests, - qemu_pkg ? qemu_test, - qemu_test, setuptools, socat, systemd, @@ -56,7 +54,6 @@ buildPythonApplication { propagatedBuildInputs = [ coreutils netpbm - qemu_pkg socat util-linux vde2 diff --git a/nixos/lib/test-driver/src/test_driver/machine/__init__.py b/nixos/lib/test-driver/src/test_driver/machine/__init__.py index 00876b0e7d1b..7a572b44367f 100644 --- a/nixos/lib/test-driver/src/test_driver/machine/__init__.py +++ b/nixos/lib/test-driver/src/test_driver/machine/__init__.py @@ -1688,6 +1688,7 @@ class NspawnMachine(BaseMachine): self.process = subprocess.Popen( [self.start_command], + cwd=self.state_dir, env={ "RUN_NSPAWN_ROOT_DIR": str(self.state_dir), "RUN_NSPAWN_SHARED_DIR": str(self.shared_dir), diff --git a/nixos/lib/testing/driver.nix b/nixos/lib/testing/driver.nix index 7a4c7e31a0e1..c15578f4fbfc 100644 --- a/nixos/lib/testing/driver.nix +++ b/nixos/lib/testing/driver.nix @@ -20,7 +20,6 @@ let # the respective qemu version and with or without ocr support testDriver = config.pythonTestDriverPackage.override { inherit (config) enableOCR extraPythonPackages; - qemu_pkg = config.qemu.package; enableNspawn = config.containers != { }; }; diff --git a/nixos/modules/services/hardware/udev.nix b/nixos/modules/services/hardware/udev.nix index 1299a5115a5f..1f28286b059d 100644 --- a/nixos/modules/services/hardware/udev.nix +++ b/nixos/modules/services/hardware/udev.nix @@ -461,10 +461,17 @@ in "${config.boot.initrd.systemd.package}/lib/systemd/systemd-udevd" "${config.boot.initrd.systemd.package}/lib/udev/ata_id" "${config.boot.initrd.systemd.package}/lib/udev/cdrom_id" - "${config.boot.initrd.systemd.package}/lib/udev/dmi_memory_id" "${config.boot.initrd.systemd.package}/lib/udev/scsi_id" "${config.boot.initrd.systemd.package}/lib/udev/rules.d" ] + ++ lib.optional ( + # https://github.com/systemd/systemd/blob/v259/meson.build#L1529-L1530 + pkgs.stdenv.hostPlatform.isx86 + || pkgs.stdenv.hostPlatform.isAarch + || pkgs.stdenv.hostPlatform.isLoongArch64 + || pkgs.stdenv.hostPlatform.isMips + || pkgs.stdenv.hostPlatform.isRiscV64 + ) "${config.boot.initrd.systemd.package}/lib/udev/dmi_memory_id" ++ map (x: "${x}/bin") config.boot.initrd.services.udev.binPackages; # Generate the udev rules for the initrd diff --git a/nixos/modules/system/boot/networkd.nix b/nixos/modules/system/boot/networkd.nix index 2b14caac86fa..4a22d6585eb9 100644 --- a/nixos/modules/system/boot/networkd.nix +++ b/nixos/modules/system/boot/networkd.nix @@ -4231,8 +4231,10 @@ let "systemd-networkd-wait-online@.service" "systemd-networkd.service" "systemd-networkd.socket" - "systemd-networkd-persistent-storage.service" + "systemd-networkd-resolve-hook.socket" "systemd-networkd-varlink-metrics.socket" + "systemd-networkd-varlink.socket" + "systemd-networkd-persistent-storage.service" ]; systemd.sockets.systemd-networkd-varlink-metrics.wantedBy = [ "sockets.target" ]; @@ -4314,6 +4316,9 @@ let systemd.additionalUpstreamUnits = [ "systemd-networkd-wait-online.service" "systemd-networkd.service" + "systemd-networkd-resolve-hook.socket" + "systemd-networkd-varlink-metrics.socket" + "systemd-networkd-varlink.socket" "systemd-networkd.socket" "systemd-network-generator.service" "network-online.target" diff --git a/nixos/modules/system/boot/resolved.nix b/nixos/modules/system/boot/resolved.nix index da2b57285a76..6760dab044e9 100644 --- a/nixos/modules/system/boot/resolved.nix +++ b/nixos/modules/system/boot/resolved.nix @@ -185,7 +185,11 @@ in # added with order 501 to allow modules to go before with mkBefore system.nssDatabases.hosts = (mkOrder 501 [ "resolve [!UNAVAIL=return]" ]); - systemd.additionalUpstreamSystemUnits = [ "systemd-resolved.service" ]; + systemd.additionalUpstreamSystemUnits = [ + "systemd-resolved.service" + "systemd-resolved-monitor.socket" + "systemd-resolved-varlink.socket" + ]; systemd.services.systemd-resolved = { wantedBy = [ "sysinit.target" ]; @@ -248,7 +252,12 @@ in tmpfiles.settings.systemd-resolved-stub."/etc/resolv.conf".L.argument = "/run/systemd/resolve/stub-resolv.conf"; - additionalUpstreamUnits = [ "systemd-resolved.service" ]; + additionalUpstreamUnits = [ + "systemd-resolved.service" + "systemd-resolved-monitor.socket" + "systemd-resolved-varlink.socket" + ]; + users.systemd-resolve = { }; groups.systemd-resolve = { }; storePaths = [ "${config.boot.initrd.systemd.package}/lib/systemd/systemd-resolved" ]; diff --git a/nixos/modules/system/boot/systemd.nix b/nixos/modules/system/boot/systemd.nix index 75b4143d6810..acfb213b98e0 100644 --- a/nixos/modules/system/boot/systemd.nix +++ b/nixos/modules/system/boot/systemd.nix @@ -65,6 +65,7 @@ let # Udev. "systemd-udevd-control.socket" "systemd-udevd-kernel.socket" + "systemd-udevd-varlink.socket" "systemd-udevd.service" ] ++ (optional (!config.boot.isContainer) "systemd-udev-trigger.service") @@ -157,6 +158,8 @@ let "systemd-ask-password-wall.service" # Varlink APIs + "systemd-ask-password@.service" + "systemd-ask-password.socket" ] ++ lib.optionals cfg.package.withBootloader [ "systemd-bootctl@.service" @@ -178,11 +181,13 @@ let ] ++ optionals cfg.package.withImportd [ "systemd-importd.service" + "systemd-importd.socket" ] ++ optionals cfg.package.withMachined [ "machine.slice" "machines.target" "systemd-machined.service" + "systemd-machined.socket" ] ++ optionals cfg.package.withNspawn [ "systemd-nspawn@.service" @@ -191,6 +196,9 @@ let # Misc. "systemd-sysctl.service" "systemd-machine-id-commit.service" + + "systemd-mute-console@.service" + "systemd-mute-console.socket" ] ++ optionals cfg.package.withTimedated [ "dbus-org.freedesktop.timedate1.service" @@ -209,6 +217,11 @@ let "dbus-org.freedesktop.portable1.service" "systemd-portabled.service" ] + ++ optionals cfg.package.withRepart [ + # Varlink APIs + "systemd-repart@.service" + "systemd-repart.socket" + ] ++ [ "systemd-exit.service" "systemd-update-done.service" @@ -221,6 +234,8 @@ let "factory-reset.target" "systemd-factory-reset-request.service" "systemd-factory-reset-reboot.service" + "systemd-factory-reset@.service" + "systemd-factory-reset.socket" ] ++ cfg.additionalUpstreamSystemUnits; diff --git a/nixos/modules/system/boot/systemd/repart.nix b/nixos/modules/system/boot/systemd/repart.nix index fce8e417991d..2126464c5250 100644 --- a/nixos/modules/system/boot/systemd/repart.nix +++ b/nixos/modules/system/boot/systemd/repart.nix @@ -157,6 +157,12 @@ in boot.initrd.systemd = lib.mkIf initrdCfg.enable { additionalUpstreamUnits = [ "systemd-repart.service" + # Varlink APIs + # NOTE: compared to stage 2 where the IPC is enabled in the global location, initrd + # might be optimized to keep away the repart binary. + # As a result, we enable repart IPC in the initrd only if repart is enabled in the initrd. + "systemd-repart.socket" + "systemd-repart@.service" ]; storePaths = [ diff --git a/nixos/modules/system/boot/systemd/tmpfiles.nix b/nixos/modules/system/boot/systemd/tmpfiles.nix index c606ff1ac50b..631a02645d9b 100644 --- a/nixos/modules/system/boot/systemd/tmpfiles.nix +++ b/nixos/modules/system/boot/systemd/tmpfiles.nix @@ -317,6 +317,7 @@ in mkdir -p $out/lib/tmpfiles.d cd $out/lib/tmpfiles.d + ln -s "${systemd}/example/tmpfiles.d/credstore.conf" ln -s "${systemd}/example/tmpfiles.d/home.conf" ln -s "${systemd}/example/tmpfiles.d/journal-nocow.conf" ln -s "${systemd}/example/tmpfiles.d/portables.conf" diff --git a/nixos/modules/system/boot/systemd/tpm2.nix b/nixos/modules/system/boot/systemd/tpm2.nix index 2703d60428f1..de0f72e5a543 100644 --- a/nixos/modules/system/boot/systemd/tpm2.nix +++ b/nixos/modules/system/boot/systemd/tpm2.nix @@ -43,6 +43,8 @@ "tpm2.target" "systemd-tpm2-setup-early.service" "systemd-tpm2-setup.service" + "systemd-pcrextend.socket" + "systemd-pcrextend@.service" ]; } ) @@ -69,6 +71,8 @@ boot.initrd.systemd.additionalUpstreamUnits = [ "tpm2.target" "systemd-tpm2-setup-early.service" + "systemd-pcrextend.socket" + "systemd-pcrextend@.service" ]; boot.initrd.availableKernelModules = [ @@ -81,6 +85,7 @@ pkgs.tpm2-tss "${cfg.package}/lib/systemd/systemd-tpm2-setup" "${cfg.package}/lib/systemd/system-generators/systemd-tpm2-generator" + "${cfg.package}/lib/systemd/systemd-pcrextend" ]; } ) @@ -89,7 +94,9 @@ cfg = config.boot.initrd.systemd; in lib.mkIf (cfg.enable && cfg.tpm2.enable && cfg.tpm2.pcrphases.enable) { - boot.initrd.systemd.additionalUpstreamUnits = [ "systemd-pcrphase-initrd.service" ]; + boot.initrd.systemd.additionalUpstreamUnits = [ + "systemd-pcrphase-initrd.service" + ]; boot.initrd.systemd.services.systemd-pcrphase-initrd.wantedBy = [ "initrd.target" ]; boot.initrd.systemd.storePaths = [ "${cfg.package}/lib/systemd/systemd-pcrextend" ]; } diff --git a/nixos/modules/virtualisation/credentials-options.nix b/nixos/modules/virtualisation/credentials-options.nix new file mode 100644 index 000000000000..3345f6933784 --- /dev/null +++ b/nixos/modules/virtualisation/credentials-options.nix @@ -0,0 +1,63 @@ +{ lib, pkgs, ... }: +{ + options.virtualisation.credentials = lib.mkOption { + description = '' + Credentials to pass to the VM or container using systemd's credential system. + + See {manpage}`systemd.exec(5)`, {manpage}`systemd-creds(1)` and https://systemd.io/CREDENTIALS/ for more + information about systemd credentials. + ''; + default = { }; + example = lib.literalExpression '' + { + database-password = { + text = "my-secret-password"; + }; + ssl-cert = { + source = "./cert.pem"; + }; + binary-key = { + source = "./private.der"; + }; + } + ''; + type = lib.types.attrsOf ( + lib.types.submodule ( + { + name, + options, + config, + ... + }: + { + options = { + source = lib.mkOption { + type = lib.types.nullOr (lib.types.pathWith { }); + default = null; + description = '' + Source file on the host containing the credential data. + ''; + }; + text = lib.mkOption { + default = null; + type = lib.types.nullOr lib.types.str; + description = '' + Text content of the credential. + + For binary data or when the credential content should come from + an existing file, use `source` instead. + + ::: {.warning} + The text here is stored in the host's nix store as a file. + ::: + ''; + }; + }; + config.source = lib.mkIf (config.text != null) ( + lib.mkDerivedConfig options.text (pkgs.writeText name) + ); + } + ) + ); + }; +} diff --git a/nixos/modules/virtualisation/nspawn-container/default.nix b/nixos/modules/virtualisation/nspawn-container/default.nix index 9100b580768b..82a06929e925 100644 --- a/nixos/modules/virtualisation/nspawn-container/default.nix +++ b/nixos/modules/virtualisation/nspawn-container/default.nix @@ -21,6 +21,10 @@ let cfg = config.virtualisation; in { + imports = [ + ../credentials-options.nix + ]; + options = { virtualisation.cmdline = lib.mkOption { @@ -131,7 +135,8 @@ in # Send a READY=1 notification to a socket when the container is fully booted. "--notify-ready=yes" - ]; + ] + ++ lib.mapAttrsToList (name: cred: "--load-credential=${name}:${cred.source}") cfg.credentials; system.build.nspawn = let diff --git a/nixos/modules/virtualisation/qemu-vm.nix b/nixos/modules/virtualisation/qemu-vm.nix index 8d2817c23bf0..8230da280b50 100644 --- a/nixos/modules/virtualisation/qemu-vm.nix +++ b/nixos/modules/virtualisation/qemu-vm.nix @@ -399,6 +399,7 @@ in imports = [ ../profiles/qemu-guest.nix ./disk-size-option.nix + ./credentials-options.nix (mkRenamedOptionModule [ "virtualisation" @@ -1126,81 +1127,20 @@ in }; virtualisation.credentials = mkOption { - description = '' - Credentials to pass to the VM using systemd's credential system. - - See {manpage}`systemd.exec(5)` , {manpage}`systemd-creds(1)` and https://systemd.io/CREDENTIALS/ for more - information about systemd credentials. - ''; - default = { }; - example = { - database-password = { - text = "my-secret-password"; - }; - ssl-cert = { - source = "./cert.pem"; - }; - binary-key = { - mechanism = "fw_cfg"; - source = "./private.der"; - }; - config-file = { - mechanism = "smbios"; - text = '' - [database] - host=localhost - port=5432 - ''; - }; - }; type = types.attrsOf ( - lib.types.submodule ( - { - name, - options, - config, - ... - }: - { - options = { - mechanism = lib.mkOption { - type = lib.types.enum [ - "fw_cfg" - "smbios" - ]; - default = if pkgs.stdenv.hostPlatform.isx86 then "smbios" else "fw_cfg"; - defaultText = lib.literalExpression ''if pkgs.stdenv.hostPlatform.isx86 then "smbios" else "fw_cfg"''; - description = '' - The mechanism used to pass the credential to the VM. - ''; - }; - source = lib.mkOption { - type = lib.types.nullOr (lib.types.pathWith { }); - default = null; - description = '' - Source file on the host containing the credential data. - ''; - }; - text = lib.mkOption { - default = null; - type = lib.types.nullOr lib.types.str; - description = '' - Text content of the credential. - - For binary data or when the credential content should come from - an existing file, use `source` instead. - - ::: {.warning} - The text here is stored in the host's nix store as a file. - ::: - ''; - }; - }; - config.source = lib.mkIf (config.text != null) ( - lib.mkDerivedConfig options.text (pkgs.writeText name) - ); - } - ) + lib.types.submodule { + options.mechanism = lib.mkOption { + type = lib.types.enum [ + "fw_cfg" + "smbios" + ]; + default = if pkgs.stdenv.hostPlatform.isx86 then "smbios" else "fw_cfg"; + defaultText = lib.literalExpression ''if pkgs.stdenv.hostPlatform.isx86 then "smbios" else "fw_cfg"''; + description = '' + The mechanism used to pass the credential to the VM. + ''; + }; + } ); }; diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index ab8e486ca8cc..6a1c6f8e1065 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -446,6 +446,14 @@ in }; coturn = runTest ./coturn.nix; couchdb = runTest ./couchdb.nix; + credentials-fwcfg = runTest { + imports = [ ./credentials.nix ]; + _module.args.mechanism = "fw_cfg"; + }; + credentials-smbios = runTestOn [ "x86_64-linux" ] { + imports = [ ./credentials.nix ]; + _module.args.mechanism = "smbios"; + }; cri-o = runTestOn [ "aarch64-linux" "x86_64-linux" ] ./cri-o.nix; croc = runTest ./croc.nix; cross-seed = runTest ./cross-seed.nix; @@ -1423,14 +1431,6 @@ in pykms = runTest ./pykms.nix; qbittorrent = runTest ./qbittorrent.nix; qboot = runTestOn [ "x86_64-linux" "i686-linux" ] ./qboot.nix; - qemu-vm-credentials-fwcfg = runTest { - imports = [ ./qemu-vm-credentials.nix ]; - _module.args.mechanism = "fw_cfg"; - }; - qemu-vm-credentials-smbios = runTestOn [ "x86_64-linux" ] { - imports = [ ./qemu-vm-credentials.nix ]; - _module.args.mechanism = "smbios"; - }; qemu-vm-external-disk-image = runTest ./qemu-vm-external-disk-image.nix; qemu-vm-restrictnetwork = handleTest ./qemu-vm-restrictnetwork.nix { }; qemu-vm-store = runTest ./qemu-vm-store.nix; @@ -1713,6 +1713,7 @@ in systemd-user-settings = runTest ./systemd-user-settings.nix; systemd-user-tmpfiles-rules = runTest ./systemd-user-tmpfiles-rules.nix; systemd-userdbd = runTest ./systemd-userdbd.nix; + systemd-varlink = runTest ./systemd-varlink.nix; systemtap = handleTest ./systemtap.nix { }; szurubooru = handleTest ./szurubooru.nix { }; taler = handleTest ./taler { }; diff --git a/nixos/tests/boot.nix b/nixos/tests/boot.nix index d1ac1a8769fe..a59f94b94edf 100644 --- a/nixos/tests/boot.nix +++ b/nixos/tests/boot.nix @@ -207,7 +207,7 @@ in import os # Create a mutable linked image backed by the read-only SD image - if os.system("qemu-img create -f qcow2 -F raw -b ${sdImage} ${mutableImage}") != 0: + if os.system("${pkgs.qemu}/bin/qemu-img create -f qcow2 -F raw -b ${sdImage} ${mutableImage}") != 0: raise RuntimeError("Could not create mutable linked image") machine = create_machine("${startCommand}") diff --git a/nixos/tests/common/ec2.nix b/nixos/tests/common/ec2.nix index 864afc63cb8b..f28e094d455b 100644 --- a/nixos/tests/common/ec2.nix +++ b/nixos/tests/common/ec2.nix @@ -51,9 +51,11 @@ in ) os.makedirs(image_dir, mode=0o700, exist_ok=True) disk_image = os.path.join(image_dir, "machine.qcow2") + QEMU_BIN = "${pkgs.qemu}" + QEMU_IMG = f"{QEMU_BIN}/bin/qemu-img" subprocess.check_call( [ - "qemu-img", + QEMU_IMG, "create", "-f", "qcow2", @@ -64,7 +66,7 @@ in disk_image, ] ) - subprocess.check_call(["qemu-img", "resize", disk_image, "10G"]) + subprocess.check_call([QEMU_IMG, "resize", disk_image, "10G"]) # Note: we use net=169.0.0.0/8 rather than # net=169.254.0.0/16 to prevent dhcpcd from getting horribly @@ -74,7 +76,7 @@ in # turn off the DHCP server, but qemu does not have an option # to do that. start_command = ( - "qemu-kvm -m 1024" + f"{QEMU_BIN}/bin/qemu-kvm -m 1024" + " -device virtio-net-pci,netdev=vlan0" + " -netdev 'user,id=vlan0,net=169.0.0.0/8,guestfwd=tcp:169.254.169.254:80-cmd:${getExe imdsServer} ${metaData}'" + f" -drive file={disk_image},if=virtio,werror=report" diff --git a/nixos/tests/credentials.nix b/nixos/tests/credentials.nix new file mode 100644 index 000000000000..7984ffd03593 --- /dev/null +++ b/nixos/tests/credentials.nix @@ -0,0 +1,106 @@ +{ + lib, + pkgs, + mechanism, + ... +}: + +let + secret = '' + foo + bar + baz + ''; + secret-file = "bar"; + + common-credentials = { + secret-default-mechanism = { + text = "default-mechanism"; + }; + secret-file-nix-store = { + source = pkgs.writeText "secret-file-nix-store" secret-file; + }; + secret-file-host = { + source = "./secret-file-host"; + }; + secret-file-host-binary = { + source = "./secret-file-host-binary"; + }; + }; +in + +{ + name = "credentials-${mechanism}"; + + meta.maintainers = with lib.maintainers; [ arianvp ]; + + # No VM<->container traffic in this test; credentials are static. + requiredFeatures.devnet = lib.mkForce false; + + nodes.vm = { + virtualisation.credentials = common-credentials // { + secret = { + inherit mechanism; + text = secret; + }; + }; + }; + + containers.container = { + virtualisation.credentials = common-credentials // { + secret = { + text = secret; + }; + }; + }; + + testScript = '' + import base64 + + secret_file_host = "baz" + # Binary data with null bytes, high bytes, and other problematic characters. + secret_file_host_binary = bytes([ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0xDE, 0xAD, 0xBE, 0xEF, + 0xFF, 0xFE, 0xFD, 0xFC, + 0x00, 0x00, 0x00, 0x00, + 0x80, 0x81, 0x82, 0x83, + ]) + + def assert_credentials(m): + with open(m.state_dir / "secret-file-host", "w") as f: + f.write(secret_file_host) + with open(m.state_dir / "secret-file-host-binary", "wb") as f2: + f2.write(secret_file_host_binary) + + t.assertEqual( + m.succeed("systemd-creds --system cat secret").strip(), + "foo\nbar\nbaz", + ) + t.assertEqual( + m.succeed("systemd-creds --system cat secret-default-mechanism").strip(), + "default-mechanism", + ) + t.assertEqual( + m.succeed("systemd-creds --system cat secret-file-nix-store").strip(), + "${secret-file}", + ) + t.assertEqual( + m.succeed("systemd-creds --system cat secret-file-host").strip(), + secret_file_host, + ) + result = m.succeed( + "systemd-creds --system cat secret-file-host-binary --transcode=base64" + ).strip() + expected = base64.b64encode(secret_file_host_binary).decode("ascii") + t.assertEqual( + result, + expected, + f"Binary credential mismatch: got {result}, expected {expected}", + ) + + assert_credentials(vm) + assert_credentials(container) + ''; +} diff --git a/nixos/tests/ec2-image.nix b/nixos/tests/ec2-image.nix index b54b153bd52e..df07d14b8ba7 100644 --- a/nixos/tests/ec2-image.nix +++ b/nixos/tests/ec2-image.nix @@ -88,294 +88,298 @@ in }; }; - testScript = '' - import os - import re - import subprocess - import tempfile + testScript = + { nodes, ... }: + '' + import os + import re + import subprocess + import tempfile - # Instance Metadata Service (IMDSv2 with 1.0 metadata version) - # TODO: Use 'latest' metadata version instead of '1.0' - # TODO: [Test matrix] also test providing the host key through IMDS - # - i.e. a test module argument to select between writing or reading the host key - def create_ec2_metadata_dir(temp_dir, client_pubkey): - """Create fake EC2 metadata directory structure with mock data""" - metadata_dir = os.path.join(temp_dir.name, "ec2-metadata") + # Instance Metadata Service (IMDSv2 with 1.0 metadata version) + # TODO: Use 'latest' metadata version instead of '1.0' + # TODO: [Test matrix] also test providing the host key through IMDS + # - i.e. a test module argument to select between writing or reading the host key + def create_ec2_metadata_dir(temp_dir, client_pubkey): + """Create fake EC2 metadata directory structure with mock data""" + metadata_dir = os.path.join(temp_dir.name, "ec2-metadata") - # Create directory structure - os.makedirs(os.path.join(metadata_dir, "1.0", "meta-data", "public-keys", "0"), exist_ok=True) - os.makedirs(os.path.join(metadata_dir, "latest", "api"), exist_ok=True) + # Create directory structure + os.makedirs(os.path.join(metadata_dir, "1.0", "meta-data", "public-keys", "0"), exist_ok=True) + os.makedirs(os.path.join(metadata_dir, "latest", "api"), exist_ok=True) - # Metadata version 1.0 endpoints (what fetch-ec2-metadata.sh actually fetches) - with open(os.path.join(metadata_dir, "1.0", "meta-data", "hostname"), "w") as f: - f.write("test-instance") - with open(os.path.join(metadata_dir, "1.0", "meta-data", "ami-manifest-path"), "w") as f: - f.write("(test)") - with open(os.path.join(metadata_dir, "1.0", "meta-data", "instance-id"), "w") as f: - f.write("i-1234567890abcdef0") - with open(os.path.join(metadata_dir, "1.0", "user-data"), "w") as f: - f.write("") - with open(os.path.join(metadata_dir, "1.0", "meta-data", "public-keys", "0", "openssh-key"), "w") as f: - f.write(client_pubkey) + # Metadata version 1.0 endpoints (what fetch-ec2-metadata.sh actually fetches) + with open(os.path.join(metadata_dir, "1.0", "meta-data", "hostname"), "w") as f: + f.write("test-instance") + with open(os.path.join(metadata_dir, "1.0", "meta-data", "ami-manifest-path"), "w") as f: + f.write("(test)") + with open(os.path.join(metadata_dir, "1.0", "meta-data", "instance-id"), "w") as f: + f.write("i-1234567890abcdef0") + with open(os.path.join(metadata_dir, "1.0", "user-data"), "w") as f: + f.write("") + with open(os.path.join(metadata_dir, "1.0", "meta-data", "public-keys", "0", "openssh-key"), "w") as f: + f.write(client_pubkey) - # IMDSv2 token endpoint - return a fake token - with open(os.path.join(metadata_dir, "latest", "api", "token"), "w") as f: - f.write("test-token-12345") + # IMDSv2 token endpoint - return a fake token + with open(os.path.join(metadata_dir, "latest", "api", "token"), "w") as f: + f.write("test-token-12345") - return metadata_dir + return metadata_dir - def generate_client_ssh_key(): - """Generate SSH key pair on VM host for client authentication""" - # Use temporary directory for key generation - import tempfile - with tempfile.TemporaryDirectory() as key_dir: - private_key = os.path.join(key_dir, "id_ed25519") - public_key = os.path.join(key_dir, "id_ed25519.pub") + def generate_client_ssh_key(): + """Generate SSH key pair on VM host for client authentication""" + # Use temporary directory for key generation + import tempfile + with tempfile.TemporaryDirectory() as key_dir: + private_key = os.path.join(key_dir, "id_ed25519") + public_key = os.path.join(key_dir, "id_ed25519.pub") - # Generate key pair using host SSH tools - ret = os.system(f"${hostPkgs.openssh}/bin/ssh-keygen -t ed25519 -f {private_key} -N \"\"") - if ret != 0: - raise Exception("Failed to generate SSH key pair") + # Generate key pair using host SSH tools + ret = os.system(f"${hostPkgs.openssh}/bin/ssh-keygen -t ed25519 -f {private_key} -N \"\"") + if ret != 0: + raise Exception("Failed to generate SSH key pair") - # Read the generated public key - with open(public_key, "r") as f: - client_pubkey = f.read().strip() + # Read the generated public key + with open(public_key, "r") as f: + client_pubkey = f.read().strip() - # Read the private key - with open(private_key, "r") as f: - client_private_key = f.read() + # Read the private key + with open(private_key, "r") as f: + client_private_key = f.read() - return client_pubkey, client_private_key + return client_pubkey, client_private_key - def setup_client_ssh_key(client, client_private_key): - """Install the pre-generated SSH private key on client""" - client.succeed("mkdir -p /root/.ssh") - client.succeed(f"cat > /root/.ssh/id_ed25519 << 'EOF'\n{client_private_key}\nEOF") - client.succeed("chmod 600 /root/.ssh/id_ed25519") + def setup_client_ssh_key(client, client_private_key): + """Install the pre-generated SSH private key on client""" + client.succeed("mkdir -p /root/.ssh") + client.succeed(f"cat > /root/.ssh/id_ed25519 << 'EOF'\n{client_private_key}\nEOF") + client.succeed("chmod 600 /root/.ssh/id_ed25519") - def setup_machine(temp_dir, client_pubkey): - """Initialize EC2 machine with disk image, metadata server, and networking""" - # Set up disk image - image_dir = os.path.join( - os.environ.get("TMPDIR", tempfile.gettempdir()), "tmp", "vm-state-machine" - ) - os.makedirs(image_dir, mode=0o700, exist_ok=True) - disk_image = os.path.join(image_dir, "machine.qcow2") - subprocess.check_call([ - "qemu-img", "create", "-f", "qcow2", "-F", "qcow2", - "-o", "backing_file=${image}", disk_image - ]) - subprocess.check_call(["qemu-img", "resize", disk_image, "10G"]) + def setup_machine(temp_dir, client_pubkey): + """Initialize EC2 machine with disk image, metadata server, and networking""" + # Set up disk image + image_dir = os.path.join( + os.environ.get("TMPDIR", tempfile.gettempdir()), "tmp", "vm-state-machine" + ) + os.makedirs(image_dir, mode=0o700, exist_ok=True) + disk_image = os.path.join(image_dir, "machine.qcow2") + QEMU = "${nodes.machine.virtualisation.qemu.package}" + QEMU_IMG = f"{QEMU}/bin/qemu-img" + subprocess.check_call([ + QEMU_IMG, "create", "-f", "qcow2", "-F", "qcow2", + "-o", "backing_file=${image}", disk_image + ]) + subprocess.check_call([QEMU_IMG, "resize", disk_image, "10G"]) - # Create fake EC2 metadata in temporary directory with client's public key - metadata_dir = create_ec2_metadata_dir(temp_dir, client_pubkey) + # Create fake EC2 metadata in temporary directory with client's public key + metadata_dir = create_ec2_metadata_dir(temp_dir, client_pubkey) - # Add both VLAN networking (matching test framework) and EC2 metadata server - vlan_net = ( - " -device virtio-net-pci,netdev=vlan1,mac=52:54:00:12:01:02" - + ' -netdev vde,id=vlan1,sock="$QEMU_VDE_SOCKET_1"' - ) - metadata_net = ( - " -device virtio-net-pci,netdev=ec2meta" - + f" -netdev 'user,id=ec2meta,net=169.0.0.0/8,guestfwd=tcp:169.254.169.254:80-cmd:${lib.getExe imdsServer} {metadata_dir}'" - ) + # Add both VLAN networking (matching test framework) and EC2 metadata server + vlan_net = ( + " -device virtio-net-pci,netdev=vlan1,mac=52:54:00:12:01:02" + + ' -netdev vde,id=vlan1,sock="$QEMU_VDE_SOCKET_1"' + ) + metadata_net = ( + " -device virtio-net-pci,netdev=ec2meta" + + f" -netdev 'user,id=ec2meta,net=169.0.0.0/8,guestfwd=tcp:169.254.169.254:80-cmd:${lib.getExe imdsServer} {metadata_dir}'" + ) - start_command = ( - "qemu-kvm -m 1024" - + f" -drive file={disk_image},if=virtio,werror=report" - + vlan_net - + metadata_net - + " $QEMU_OPTS" - ) + start_command = ( + f"{QEMU}/bin/qemu-kvm -m 1024" + + f" -drive file={disk_image},if=virtio,werror=report" + + vlan_net + + metadata_net + + " $QEMU_OPTS" + ) - return create_machine(start_command), metadata_dir + return create_machine(start_command), metadata_dir - def test_userdata_decompression(machine, user_data_path, compressed_data, format_name): - """Test that compressed user-data is decompressed by fetch-ec2-metadata""" - test_marker = f"{format_name}-decompression-test" - with open(user_data_path, "wb") as f: - f.write(compressed_data) - machine.succeed("systemctl reset-failed fetch-ec2-metadata; systemctl restart fetch-ec2-metadata") - result = machine.succeed("cat /etc/ec2-metadata/user-data") - assert test_marker in result, f"Expected '{test_marker}' in decompressed {format_name} content, got: {result}" - journal = machine.succeed("journalctl -u fetch-ec2-metadata --no-pager -b") - assert "decompressing:" in journal, f"Expected decompression log in journal for {format_name}" + def test_userdata_decompression(machine, user_data_path, compressed_data, format_name): + """Test that compressed user-data is decompressed by fetch-ec2-metadata""" + test_marker = f"{format_name}-decompression-test" + with open(user_data_path, "wb") as f: + f.write(compressed_data) + machine.succeed("systemctl reset-failed fetch-ec2-metadata; systemctl restart fetch-ec2-metadata") + result = machine.succeed("cat /etc/ec2-metadata/user-data") + assert test_marker in result, f"Expected '{test_marker}' in decompressed {format_name} content, got: {result}" + journal = machine.succeed("journalctl -u fetch-ec2-metadata --no-pager -b") + assert "decompressing:" in journal, f"Expected decompression log in journal for {format_name}" - # Create temporary directory for metadata (scoped for cleanup) - temp_dir = tempfile.TemporaryDirectory() + # Create temporary directory for metadata (scoped for cleanup) + temp_dir = tempfile.TemporaryDirectory() - # Start client first (but don't wait for it to boot) - client.start() + # Start client first (but don't wait for it to boot) + client.start() - # Generate SSH key pair on VM host before starting machine - client_pubkey, client_private_key = generate_client_ssh_key() + # Generate SSH key pair on VM host before starting machine + client_pubkey, client_private_key = generate_client_ssh_key() - # Set up machine with client's public key in metadata service - machine, metadata_dir = setup_machine(temp_dir, client_pubkey) - user_data_path = os.path.join(metadata_dir, "1.0", "user-data") + # Set up machine with client's public key in metadata service + machine, metadata_dir = setup_machine(temp_dir, client_pubkey) + user_data_path = os.path.join(metadata_dir, "1.0", "user-data") - try: - machine.start() + try: + machine.start() - # Wait for services to be ready - machine.wait_for_unit("sshd.service") - machine.wait_for_unit("print-host-key.service") - machine.wait_for_unit("apply-ec2-data.service") + # Wait for services to be ready + machine.wait_for_unit("sshd.service") + machine.wait_for_unit("print-host-key.service") + machine.wait_for_unit("apply-ec2-data.service") - # Extract shared variables outside subtests - machine_ip = "${config.nodes.machine.networking.primaryIPAddress}" + # Extract shared variables outside subtests + machine_ip = "${config.nodes.machine.networking.primaryIPAddress}" - with subtest("EC2 metadata service connectivity"): - # Obtain an IMDSv2 token, then use it to fetch metadata - imds_token = machine.succeed( - "curl -sf -X PUT -H 'X-aws-ec2-metadata-token-ttl-seconds: 600'" - " http://169.254.169.254/latest/api/token" - ).strip() - assert imds_token, "Failed to obtain IMDSv2 token" - hostname_response = machine.succeed( - f"curl -sf -H 'X-aws-ec2-metadata-token: {imds_token}'" - " http://169.254.169.254/1.0/meta-data/hostname" - ) - assert "test-instance" in hostname_response, f"Expected 'test-instance', got: {hostname_response}" + with subtest("EC2 metadata service connectivity"): + # Obtain an IMDSv2 token, then use it to fetch metadata + imds_token = machine.succeed( + "curl -sf -X PUT -H 'X-aws-ec2-metadata-token-ttl-seconds: 600'" + " http://169.254.169.254/latest/api/token" + ).strip() + assert imds_token, "Failed to obtain IMDSv2 token" + hostname_response = machine.succeed( + f"curl -sf -H 'X-aws-ec2-metadata-token: {imds_token}'" + " http://169.254.169.254/1.0/meta-data/hostname" + ) + assert "test-instance" in hostname_response, f"Expected 'test-instance', got: {hostname_response}" - with subtest("SSH host key extraction from console"): - console_log = machine.get_console_log() - assert "-----BEGIN SSH HOST KEY FINGERPRINTS-----" in console_log - assert "-----END SSH HOST KEY FINGERPRINTS-----" in console_log - assert "-----BEGIN SSH HOST KEY KEYS-----" in console_log - assert "-----END SSH HOST KEY KEYS-----" in console_log + with subtest("SSH host key extraction from console"): + console_log = machine.get_console_log() + assert "-----BEGIN SSH HOST KEY FINGERPRINTS-----" in console_log + assert "-----END SSH HOST KEY FINGERPRINTS-----" in console_log + assert "-----BEGIN SSH HOST KEY KEYS-----" in console_log + assert "-----END SSH HOST KEY KEYS-----" in console_log - keys_pattern = r"-----BEGIN SSH HOST KEY KEYS-----(.*?)-----END SSH HOST KEY KEYS-----" - keys_match = re.search(keys_pattern, console_log, re.DOTALL) - assert keys_match, "Could not find SSH host keys section" - keys_content = keys_match.group(1).strip() - assert "ssh-" in keys_content, "SSH keys should contain ssh- prefix" + keys_pattern = r"-----BEGIN SSH HOST KEY KEYS-----(.*?)-----END SSH HOST KEY KEYS-----" + keys_match = re.search(keys_pattern, console_log, re.DOTALL) + assert keys_match, "Could not find SSH host keys section" + keys_content = keys_match.group(1).strip() + assert "ssh-" in keys_content, "SSH keys should contain ssh- prefix" - with subtest("Network connectivity"): - client.succeed(f"ping -c 1 {machine_ip}") + with subtest("Network connectivity"): + client.succeed(f"ping -c 1 {machine_ip}") - with subtest("SSH connectivity with strict host key checking"): - # Install the pre-generated private key on client - setup_client_ssh_key(client, client_private_key) + with subtest("SSH connectivity with strict host key checking"): + # Install the pre-generated private key on client + setup_client_ssh_key(client, client_private_key) - # Get console log and extract host keys - console_log = machine.get_console_log() - keys_pattern = r"-----BEGIN SSH HOST KEY KEYS-----(.*?)-----END SSH HOST KEY KEYS-----" - keys_match = re.search(keys_pattern, console_log, re.DOTALL) - assert keys_match, "Could not find SSH host keys section" + # Get console log and extract host keys + console_log = machine.get_console_log() + keys_pattern = r"-----BEGIN SSH HOST KEY KEYS-----(.*?)-----END SSH HOST KEY KEYS-----" + keys_match = re.search(keys_pattern, console_log, re.DOTALL) + assert keys_match, "Could not find SSH host keys section" - # Create known_hosts file from console-extracted host keys - keys_content = keys_match.group(1).strip() - known_hosts_entries = [] - for line in keys_content.split('\n'): - if line.strip() and line.startswith('ssh-'): - known_hosts_entries.append(f"{machine_ip} {line.strip()}") + # Create known_hosts file from console-extracted host keys + keys_content = keys_match.group(1).strip() + known_hosts_entries = [] + for line in keys_content.split('\n'): + if line.strip() and line.startswith('ssh-'): + known_hosts_entries.append(f"{machine_ip} {line.strip()}") - assert known_hosts_entries, "No SSH host keys found for known_hosts generation" + assert known_hosts_entries, "No SSH host keys found for known_hosts generation" - known_hosts_content = '\n'.join(known_hosts_entries) - client.succeed(f"cat > /root/.ssh/known_hosts << 'EOF'\n{known_hosts_content}\nEOF") + known_hosts_content = '\n'.join(known_hosts_entries) + client.succeed(f"cat > /root/.ssh/known_hosts << 'EOF'\n{known_hosts_content}\nEOF") - # Test SSH connectivity with strict host key checking - ssh_result = client.succeed(f"ssh -o ConnectTimeout=60 -o BatchMode=yes -i /root/.ssh/id_ed25519 root@{machine_ip} 'echo Hello from $(hostname)'") - assert "Hello from test-instance" in ssh_result, f"Unexpected SSH result: {ssh_result}" + # Test SSH connectivity with strict host key checking + ssh_result = client.succeed(f"ssh -o ConnectTimeout=60 -o BatchMode=yes -i /root/.ssh/id_ed25519 root@{machine_ip} 'echo Hello from $(hostname)'") + assert "Hello from test-instance" in ssh_result, f"Unexpected SSH result: {ssh_result}" - with subtest("Basic EC2 functionality"): - machine.succeed("findmnt / -o SIZE -n | grep -E '[0-9]+G'") + with subtest("Basic EC2 functionality"): + machine.succeed("findmnt / -o SIZE -n | grep -E '[0-9]+G'") - with subtest("Decompression of gzip-compressed user-data"): - import gzip as gzip_mod - test_data = b"#!/bin/bash\necho gzip-decompression-test\n" - test_userdata_decompression(machine, user_data_path, gzip_mod.compress(test_data), "gzip") + with subtest("Decompression of gzip-compressed user-data"): + import gzip as gzip_mod + test_data = b"#!/bin/bash\necho gzip-decompression-test\n" + test_userdata_decompression(machine, user_data_path, gzip_mod.compress(test_data), "gzip") - with subtest("Decompression of bzip2-compressed user-data"): - import bz2 - test_data = b"#!/bin/bash\necho bzip2-decompression-test\n" - test_userdata_decompression(machine, user_data_path, bz2.compress(test_data), "bzip2") + with subtest("Decompression of bzip2-compressed user-data"): + import bz2 + test_data = b"#!/bin/bash\necho bzip2-decompression-test\n" + test_userdata_decompression(machine, user_data_path, bz2.compress(test_data), "bzip2") - with subtest("Decompression of xz-compressed user-data"): - import lzma - test_data = b"#!/bin/bash\necho xz-decompression-test\n" - test_userdata_decompression(machine, user_data_path, lzma.compress(test_data), "xz") + with subtest("Decompression of xz-compressed user-data"): + import lzma + test_data = b"#!/bin/bash\necho xz-decompression-test\n" + test_userdata_decompression(machine, user_data_path, lzma.compress(test_data), "xz") - with subtest("Decompression of zstd-compressed user-data"): - test_data = b"#!/bin/bash\necho zstd-decompression-test\n" - proc = subprocess.run( - ["${hostPkgs.zstd}/bin/zstd", "-c"], - input=test_data, capture_output=True, check=True, - ) - test_userdata_decompression(machine, user_data_path, proc.stdout, "zstd") + with subtest("Decompression of zstd-compressed user-data"): + test_data = b"#!/bin/bash\necho zstd-decompression-test\n" + proc = subprocess.run( + ["${hostPkgs.zstd}/bin/zstd", "-c"], + input=test_data, capture_output=True, check=True, + ) + test_userdata_decompression(machine, user_data_path, proc.stdout, "zstd") - with subtest("Decompression of lzip-compressed user-data"): - test_data = b"#!/bin/bash\necho lzip-decompression-test\n" - proc = subprocess.run( - ["${hostPkgs.lzip}/bin/lzip", "-c"], - input=test_data, capture_output=True, check=True, - ) - test_userdata_decompression(machine, user_data_path, proc.stdout, "lzip") + with subtest("Decompression of lzip-compressed user-data"): + test_data = b"#!/bin/bash\necho lzip-decompression-test\n" + proc = subprocess.run( + ["${hostPkgs.lzip}/bin/lzip", "-c"], + input=test_data, capture_output=True, check=True, + ) + test_userdata_decompression(machine, user_data_path, proc.stdout, "lzip") - with subtest("IPv6 IMDS fallback"): - # Save hostname fetched via IPv4 for later comparison - original_hostname = machine.succeed("cat /etc/ec2-metadata/hostname").strip() + with subtest("IPv6 IMDS fallback"): + # Save hostname fetched via IPv4 for later comparison + original_hostname = machine.succeed("cat /etc/ec2-metadata/hostname").strip() - # Assign the EC2 IPv6 IMDS address to loopback - machine.succeed("ip -6 addr add fd00:ec2::254/128 dev lo") + # Assign the EC2 IPv6 IMDS address to loopback + machine.succeed("ip -6 addr add fd00:ec2::254/128 dev lo") - # Create metadata directory structure for the IPv6 endpoint - machine.succeed( - "mkdir -p /tmp/ipv6-metadata/1.0/meta-data/public-keys/0" - " && mkdir -p /tmp/ipv6-metadata/latest/api" - " && cp /etc/ec2-metadata/hostname /tmp/ipv6-metadata/1.0/meta-data/hostname" - " && cp /etc/ec2-metadata/ami-manifest-path /tmp/ipv6-metadata/1.0/meta-data/ami-manifest-path" - " && echo i-1234567890abcdef0 > /tmp/ipv6-metadata/1.0/meta-data/instance-id" - " && echo ipv6-test-token > /tmp/ipv6-metadata/latest/api/token" - " && touch /tmp/ipv6-metadata/1.0/user-data" - ) - machine.execute( - "test -f /etc/ec2-metadata/public-keys-0-openssh-key" - " && cp /etc/ec2-metadata/public-keys-0-openssh-key" - " /tmp/ipv6-metadata/1.0/meta-data/public-keys/0/openssh-key" - ) + # Create metadata directory structure for the IPv6 endpoint + machine.succeed( + "mkdir -p /tmp/ipv6-metadata/1.0/meta-data/public-keys/0" + " && mkdir -p /tmp/ipv6-metadata/latest/api" + " && cp /etc/ec2-metadata/hostname /tmp/ipv6-metadata/1.0/meta-data/hostname" + " && cp /etc/ec2-metadata/ami-manifest-path /tmp/ipv6-metadata/1.0/meta-data/ami-manifest-path" + " && echo i-1234567890abcdef0 > /tmp/ipv6-metadata/1.0/meta-data/instance-id" + " && echo ipv6-test-token > /tmp/ipv6-metadata/latest/api/token" + " && touch /tmp/ipv6-metadata/1.0/user-data" + ) + machine.execute( + "test -f /etc/ec2-metadata/public-keys-0-openssh-key" + " && cp /etc/ec2-metadata/public-keys-0-openssh-key" + " /tmp/ipv6-metadata/1.0/meta-data/public-keys/0/openssh-key" + ) - # Serve metadata on the IPv6 IMDS address via socat + imds-server (inetd-style) - machine.succeed( - "systemd-run --unit=ipv6-imds --" - " socat TCP6-LISTEN:80,bind=[fd00:ec2::254],fork,reuseaddr" - " SYSTEM:'${lib.getExe imdsServer} /tmp/ipv6-metadata'" - ) + # Serve metadata on the IPv6 IMDS address via socat + imds-server (inetd-style) + machine.succeed( + "systemd-run --unit=ipv6-imds --" + " socat TCP6-LISTEN:80,bind=[fd00:ec2::254],fork,reuseaddr" + " SYSTEM:'${lib.getExe imdsServer} /tmp/ipv6-metadata'" + ) - # Wait for IPv6 IMDS to become reachable (token endpoint doesn't require auth) - machine.wait_until_succeeds( - "curl -sf -X PUT -H 'X-aws-ec2-metadata-token-ttl-seconds: 600'" - " http://[fd00:ec2::254]/latest/api/token" - ) + # Wait for IPv6 IMDS to become reachable (token endpoint doesn't require auth) + machine.wait_until_succeeds( + "curl -sf -X PUT -H 'X-aws-ec2-metadata-token-ttl-seconds: 600'" + " http://[fd00:ec2::254]/latest/api/token" + ) - # Block IPv4 IMDS to force fallback to IPv6 - machine.succeed( - "iptables -I OUTPUT -d 169.254.169.254 -p tcp --dport 80 -j REJECT" - ) + # Block IPv4 IMDS to force fallback to IPv6 + machine.succeed( + "iptables -I OUTPUT -d 169.254.169.254 -p tcp --dport 80 -j REJECT" + ) - # Verify IPv4 IMDS is now unreachable - machine.fail( - "curl -sf --connect-timeout 2 http://169.254.169.254/1.0/meta-data/hostname" - ) + # Verify IPv4 IMDS is now unreachable + machine.fail( + "curl -sf --connect-timeout 2 http://169.254.169.254/1.0/meta-data/hostname" + ) - # Clear fetched metadata and re-run the fetcher - machine.succeed("rm -f /etc/ec2-metadata/*") - machine.succeed("systemctl restart fetch-ec2-metadata") + # Clear fetched metadata and re-run the fetcher + machine.succeed("rm -f /etc/ec2-metadata/*") + machine.succeed("systemctl restart fetch-ec2-metadata") - # Verify metadata was successfully re-fetched via IPv6 - hostname = machine.succeed("cat /etc/ec2-metadata/hostname").strip() - assert hostname == original_hostname, f"Expected '{original_hostname}', got '{hostname}'" + # Verify metadata was successfully re-fetched via IPv6 + hostname = machine.succeed("cat /etc/ec2-metadata/hostname").strip() + assert hostname == original_hostname, f"Expected '{original_hostname}', got '{hostname}'" - # Clean up: restore IPv4 IMDS access - machine.succeed( - "iptables -D OUTPUT -d 169.254.169.254 -p tcp --dport 80 -j REJECT" - ) - machine.succeed("systemctl stop ipv6-imds") + # Clean up: restore IPv4 IMDS access + machine.succeed( + "iptables -D OUTPUT -d 169.254.169.254 -p tcp --dport 80 -j REJECT" + ) + machine.succeed("systemctl stop ipv6-imds") - finally: - machine.shutdown() - temp_dir.cleanup() - ''; + finally: + machine.shutdown() + temp_dir.cleanup() + ''; } diff --git a/nixos/tests/qemu-vm-credentials.nix b/nixos/tests/qemu-vm-credentials.nix deleted file mode 100644 index 9ee2d270d8f0..000000000000 --- a/nixos/tests/qemu-vm-credentials.nix +++ /dev/null @@ -1,83 +0,0 @@ -{ - lib, - pkgs, - mechanism, - ... -}: - -let - secret = '' - foo - bar - baz - ''; - secret-file = "bar"; -in - -{ - name = "qemu-vm-credentials-${mechanism}"; - - meta.maintainers = with lib.maintainers; [ arianvp ]; - - nodes = { - machine = { - virtualisation.credentials = { - secret = { - inherit mechanism; - text = secret; - }; - secret-default-mechanism = { - text = "default-mechanism"; - }; - secret-file-nix-store = { - inherit mechanism; - source = pkgs.writeText "secret-file-nix-store" secret-file; - }; - secret-file-host = { - inherit mechanism; - source = "./secret-file-host"; - }; - secret-file-host-binary = { - inherit mechanism; - source = "./secret-file-host-binary"; - }; - }; - }; - }; - - testScript = '' - import base64 - secret_file_host = "baz" - # Binary data with null bytes, high bytes, and all sorts of problematic characters - secret_file_host_binary = bytes([ - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, # null and control chars - 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, - 0xDE, 0xAD, 0xBE, 0xEF, # classic binary pattern - 0xFF, 0xFE, 0xFD, 0xFC, # high bytes - 0x00, 0x00, 0x00, 0x00, # multiple nulls - 0x80, 0x81, 0x82, 0x83, # more high bytes - ]) - - with open(machine.state_dir / "secret-file-host", "w") as f: - f.write(secret_file_host) - with open(machine.state_dir / "secret-file-host-binary", "wb") as f2: - f2.write(secret_file_host_binary) - - - # Test text credential - t.assertEqual(machine.succeed("systemd-creds --system cat secret").strip(), "foo\nbar\nbaz") - - t.assertEqual(machine.succeed("systemd-creds --system cat secret-default-mechanism").strip(), "default-mechanism") - - # Test credential from nix store - t.assertEqual(machine.succeed("systemd-creds --system cat secret-file-nix-store").strip(), "${secret-file}") - - # Test credential from host file - t.assertEqual(machine.succeed("systemd-creds --system cat secret-file-host").strip(), secret_file_host) - - # Test binary credential - verify exact binary content - result = machine.succeed("systemd-creds --system cat secret-file-host-binary --transcode=base64").strip() - expected = base64.b64encode(secret_file_host_binary).decode('ascii') - t.assertEqual(result, expected, f"Binary credential mismatch: got {result}, expected {expected}") - ''; -} diff --git a/nixos/tests/systemd-initrd-simple.nix b/nixos/tests/systemd-initrd-simple.nix index 191a23abb2e4..b02616e1b772 100644 --- a/nixos/tests/systemd-initrd-simple.nix +++ b/nixos/tests/systemd-initrd-simple.nix @@ -1,15 +1,15 @@ +{ pkgs, ... }: { name = "systemd-initrd-simple"; - nodes.machine = - { pkgs, ... }: - { - testing.initrdBackdoor = true; - boot.initrd.systemd.enable = true; - virtualisation.fileSystems."/".autoResize = true; - }; + nodes.machine = { + testing.initrdBackdoor = true; + boot.initrd.systemd.enable = true; + virtualisation.fileSystems."/".autoResize = true; + }; testScript = + { nodes, ... }: # python '' import subprocess @@ -43,7 +43,7 @@ oldAvail = machine.succeed("df --output=avail / | sed 1d") machine.shutdown() - subprocess.check_call(["qemu-img", "resize", "vm-state-machine/machine.qcow2", "+1G"]) + subprocess.check_call(["${nodes.machine.virtualisation.qemu.package}/bin/qemu-img", "resize", "vm-state-machine/machine.qcow2", "+1G"]) machine.start() machine.switch_root() diff --git a/nixos/tests/systemd-varlink.nix b/nixos/tests/systemd-varlink.nix new file mode 100644 index 000000000000..df5d7cbb73a3 --- /dev/null +++ b/nixos/tests/systemd-varlink.nix @@ -0,0 +1,46 @@ +{ lib, ... }: +{ + name = "systemd-varlink"; + meta.maintainers = [ lib.maintainers.raitobezarius ]; + nodes.machine = + { config, pkgs, ... }: + { + networking.useNetworkd = true; + services.resolved.enable = true; + systemd.network.enable = true; + }; + testScript = '' + def list_interfaces(intf_path: str) -> list[str]: + return machine.succeed(f"varlinkctl list-interfaces {intf_path}").split('\n') + + expected_reg_sd_interfaces = [ + ("BootControl", "bootctl"), + ("Credentials", "creds"), + ("Hostname", "hostnamed"), + ("JournalAccess", "journald"), + ("Import", "importd"), + ("Machine", "machined"), + ("Resolve", "resolved-varlink"), + ("Resolve.Monitor", "resolved-monitor"), + ("Udev", "udevd-varlink"), + ("MuteConsole", "mute-console"), + ("FactoryReset", "factory-reset"), + ("AskPassword", "ask-password"), + ("Network", "networkd-varlink"), + ("Repart", "repart"), + ] + expected_priv_sd_interfaces = [ + ("Login", None), # systemd-logind-varlink.socket exist but is not necessary. + ] + expected_interfaces = [ + (f"io.systemd.{intf}", f"systemd-{socket_name}", f"/run/varlink/registry/io.systemd.{intf}") for intf, socket_name in expected_reg_sd_interfaces + ] + [ + (f"io.systemd.{intf}", f"systemd-{socket_name}" if socket_name is not None else None, f"/run/systemd/io.systemd.{intf}") for intf, socket_name in expected_priv_sd_interfaces + ] + + for intf, socket_name, intf_path in expected_interfaces: + if socket_name is not None: + machine.wait_for_unit(f"{socket_name}.socket") + assert intf in list_interfaces(intf_path), f"Interface '{intf}' not found in the Varlink registry" + ''; +} diff --git a/nixos/tests/systemd.nix b/nixos/tests/systemd.nix index 8cca68465802..b04b288e8939 100644 --- a/nixos/tests/systemd.nix +++ b/nixos/tests/systemd.nix @@ -38,21 +38,23 @@ services.journald.extraConfig = "Storage=volatile"; test-support.displayManager.auto.user = "alice"; - systemd.shutdown.test = pkgs.writeScript "test.shutdown" '' - #!${pkgs.runtimeShell} - PATH=${ - lib.makeBinPath ( - with pkgs; - [ - util-linux - coreutils - ] - ) - } - mount -t 9p shared -o trans=virtio,version=9p2000.L /tmp/shared - touch /tmp/shared/shutdown-test - umount /tmp/shared - ''; + systemd.shutdownRamfs.contents."/etc/systemd/system-shutdown/test".source = + pkgs.writeShellScript "test.shutdown" '' + PATH=${ + lib.makeBinPath ( + with pkgs; + [ + util-linux + coreutils + ] + ) + } + mkdir -p /tmp/shared + mount -t 9p shared -o trans=virtio,version=9p2000.L /tmp/shared + touch /tmp/shared/shutdown-test + umount /tmp/shared + ''; + systemd.shutdownRamfs.storePaths = [ "${pkgs.util-linux}/bin" ]; systemd.services.oncalendar-test = { description = "calendar test"; @@ -147,7 +149,7 @@ subprocess.check_call( [ - "qemu-img", + "${nodes.machine.virtualisation.qemu.package}/bin/qemu-img", "convert", "-O", "raw", diff --git a/pkgs/by-name/na/nano/package.nix b/pkgs/by-name/na/nano/package.nix index e4a2d0b1ea96..2e68584027ee 100644 --- a/pkgs/by-name/na/nano/package.nix +++ b/pkgs/by-name/na/nano/package.nix @@ -31,11 +31,11 @@ let in stdenv.mkDerivation rec { pname = "nano"; - version = "9.0"; + version = "9.1"; src = fetchurl { - url = "mirror://gnu/nano/${pname}-${version}.tar.xz"; - hash = "sha256-nzhDdLSWEQoltzrVpf67OEeDxuMYizcGP2d6yQgBP94="; + url = "mirror://gnu/nano/nano-${version}.tar.xz"; + hash = "sha256-X0d2QnTLdTI0nOCqIOwQ8ejoUabp+j62aBLEPRltsEI="; }; nativeBuildInputs = [ texinfo ] ++ lib.optional enableNls gettext; diff --git a/pkgs/by-name/ru/ruff/package.nix b/pkgs/by-name/ru/ruff/package.nix index ca5d02475508..56982ab4db18 100644 --- a/pkgs/by-name/ru/ruff/package.nix +++ b/pkgs/by-name/ru/ruff/package.nix @@ -16,7 +16,7 @@ rustPlatform.buildRustPackage (finalAttrs: { pname = "ruff"; - version = "0.15.18"; + version = "0.15.20"; __structuredAttrs = true; @@ -24,12 +24,12 @@ rustPlatform.buildRustPackage (finalAttrs: { owner = "astral-sh"; repo = "ruff"; tag = finalAttrs.version; - hash = "sha256-DH00tENXdCdNcGPXPGzZsU3RVYQ0VBe1QLvbgEg/G6k="; + hash = "sha256-8PFMGKG15kWBpG4YXg37940WtSe/e5pQDqIe3iJRh5A="; }; cargoBuildFlags = [ "--package=ruff" ]; - cargoHash = "sha256-RmK14NMPbhoRVLwIF5GXdGQg2AnMH20JGx7XF4HO+wY="; + cargoHash = "sha256-Bf6nsUnNMYapP0YN0SBkTPoP1czmj35tPwN1awyKhUw="; nativeBuildInputs = [ installShellFiles ]; diff --git a/pkgs/by-name/ty/ty/package.nix b/pkgs/by-name/ty/ty/package.nix index 48a4dbae05f2..187daf164c61 100644 --- a/pkgs/by-name/ty/ty/package.nix +++ b/pkgs/by-name/ty/ty/package.nix @@ -17,7 +17,7 @@ rustPlatform.buildRustPackage (finalAttrs: { pname = "ty"; - version = "0.0.52"; + version = "0.0.54"; __structuredAttrs = true; src = fetchFromGitHub { @@ -25,7 +25,7 @@ rustPlatform.buildRustPackage (finalAttrs: { repo = "ty"; tag = finalAttrs.version; fetchSubmodules = true; - hash = "sha256-p9fTzQ4DYvnwrtLdpSTekBV4ZbR1KETR8dfsbp8CDpo="; + hash = "sha256-hbVH0dCUHkWKD9IG/CYhYI4TfLgpk++tPOkCD36eVSg="; }; # For Darwin platforms, remove the integration test for file notifications, @@ -39,7 +39,7 @@ rustPlatform.buildRustPackage (finalAttrs: { cargoBuildFlags = [ "--package=ty" ]; - cargoHash = "sha256-NUIdYOeyRsR/ZQueEXshYdWTnSeQiRjZRRi2ag8Dm48="; + cargoHash = "sha256-Bf6nsUnNMYapP0YN0SBkTPoP1czmj35tPwN1awyKhUw="; nativeBuildInputs = [ installShellFiles ]; buildInputs = [ rust-jemalloc-sys ]; diff --git a/pkgs/os-specific/linux/busybox/default.nix b/pkgs/os-specific/linux/busybox/default.nix index d155c1064676..20a76523092a 100644 --- a/pkgs/os-specific/linux/busybox/default.nix +++ b/pkgs/os-specific/linux/busybox/default.nix @@ -99,6 +99,13 @@ stdenv.mkDerivation (finalAttrs: { excludes = [ "networking/httpd_ratelimit_cgi.c" ]; # New since release. hash = "sha256-Msm9sDZrVx7ofunnvnTS73SPKUUpR3Tv5xZ/wBd+rts="; }) + # tar: only strip unsafe components from hardlinks, not symlinks + # fix issue introduced by the previous patch (CVE-2026-26157_CVE-2026-26158.patch) + (fetchpatch { + name = "CVE-2026-26157_CVE-2026-26158-2.patch"; + url = "https://github.com/vda-linux/busybox_mirror/commit/599f5dd8fac390c18b79cba4c14c334957605dae.patch"; + hash = "sha256-go/KHSsuMSm21nC0yvKEtAQs8Jnjjqdcs5i8RWBGwT4="; + }) # syslogd: fix writing to local log file # https://lists.busybox.net/pipermail/busybox/2024-October/090969.html (fetchpatch { @@ -263,12 +270,6 @@ stdenv.mkDerivation (finalAttrs: { # Relies on suid/guid bits skip-testcase cpio.tests "cpio restores suid/sgid bits" - # Weird failures, looks related to our sandbox - skip-testcase tar.tests "tar does not extract into symlinks" - skip-testcase tar.tests "tar -k does not extract into symlinks" - skip-testcase tar.tests "tar Symlink attack: create symlink and then write through it" - skip-testcase tar.tests "tar Symlinks and hardlinks coexist" - popd ''; }); diff --git a/pkgs/os-specific/linux/kernel/kernels-org.json b/pkgs/os-specific/linux/kernel/kernels-org.json index 54ff5434cfe3..1850e03a312b 100644 --- a/pkgs/os-specific/linux/kernel/kernels-org.json +++ b/pkgs/os-specific/linux/kernel/kernels-org.json @@ -30,18 +30,18 @@ "lts": true }, "6.18": { - "version": "6.18.36", - "hash": "sha256:0kn4r43lnd5nb5c298b30030qyaxv05s7k40n9si1j3iyk4qdazv", + "version": "6.18.37", + "hash": "sha256:0maj2ap1m09bxl6a3g9wc65h9sdr6y8rwc5qcqlbavb4wq0d4g58", "lts": true }, "7.0": { - "version": "7.0.13", - "hash": "sha256:04wrz38ldls7pv1yxa1m7p2hqn1731l93xnz93fs7b0nyz8fv09w", + "version": "7.0.14", + "hash": "sha256:160ggaq9rh50a39gz02fpia8maq85bwxhqlwsc03yafjhjvrk6fy", "lts": false }, "7.1": { - "version": "7.1.1", - "hash": "sha256:0z8x6wafxzc5vkim9jh8wpycdkk9y5bpxgsirmdpyznw84szl5aj", + "version": "7.1.2", + "hash": "sha256:0gw8nnq6nix9xk2dhb1jwmhnqjayrn3bn2akzg4lgqkvfa9qq69p", "lts": false } } diff --git a/pkgs/os-specific/linux/systemd/default.nix b/pkgs/os-specific/linux/systemd/default.nix index 4d91ed2e72a5..09e87bf7be83 100644 --- a/pkgs/os-specific/linux/systemd/default.nix +++ b/pkgs/os-specific/linux/systemd/default.nix @@ -703,6 +703,7 @@ stdenv.mkDerivation (finalAttrs: { withMachined withNetworkd withNspawn + withRepart withPortabled withSysupdate withTimedated diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index 4bb26534613f..8fc736912ef5 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -196,7 +196,6 @@ with pkgs; inherit (nix-update) nix-update-script; nixos-test-driver = pkgs.python3Packages.callPackage ../../nixos/lib/test-driver { - qemu_pkg = pkgs.qemu; imagemagick_light = pkgs.imagemagick_light.override { inherit (pkgs) libtiff; }; tesseract4 = pkgs.tesseract4.override { enableLanguages = [ "eng" ]; }; # We want `pkgs.systemd`, *not* `python3Packages.system`.