diff --git a/nixos/doc/manual/configuration/file-systems.chapter.md b/nixos/doc/manual/configuration/file-systems.chapter.md index aca978be064d..3dfdd20ac33e 100644 --- a/nixos/doc/manual/configuration/file-systems.chapter.md +++ b/nixos/doc/manual/configuration/file-systems.chapter.md @@ -39,4 +39,5 @@ and non-critical by adding `options = [ "nofail" ];`. ```{=include=} sections luks-file-systems.section.md sshfs-file-systems.section.md +overlayfs.section.md ``` diff --git a/nixos/doc/manual/configuration/overlayfs.section.md b/nixos/doc/manual/configuration/overlayfs.section.md new file mode 100644 index 000000000000..592fb7c2e6f7 --- /dev/null +++ b/nixos/doc/manual/configuration/overlayfs.section.md @@ -0,0 +1,27 @@ +# Overlayfs {#sec-overlayfs} + +NixOS offers a convenient abstraction to create both read-only as well writable +overlays. + +```nix +fileSystems = { + "/writable-overlay" = { + overlay = { + lowerdir = [ writableOverlayLowerdir ]; + upperdir = "/.rw-writable-overlay/upper"; + workdir = "/.rw-writable-overlay/work"; + }; + # Mount the writable overlay in the initrd. + neededForBoot = true; + }; + "/readonly-overlay".overlay.lowerdir = [ + writableOverlayLowerdir + writableOverlayLowerdir2 + ]; +}; +``` + +If `upperdir` and `workdir` are not null, they will be created before the +overlay is mounted. + +To mount an overlay as read-only, you need to provide at least two `lowerdir`s. diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md index bad1fd449bbb..d3efc1789cdc 100644 --- a/nixos/doc/manual/release-notes/rl-2405.section.md +++ b/nixos/doc/manual/release-notes/rl-2405.section.md @@ -272,6 +272,11 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m - The option [`services.nextcloud.config.dbport`] of the Nextcloud module was removed to match upstream. The port can be specified in [`services.nextcloud.config.dbhost`](#opt-services.nextcloud.config.dbhost). +- A new abstraction to create both read-only as well as writable overlay file + systems was added. Available via + [fileSystems.overlay](#opt-fileSystems._name_.overlay.lowerdir). See also the + [NixOS docs](#sec-overlayfs). + - `stdenv`: The `--replace` flag in `substitute`, `substituteInPlace`, `substituteAll`, `substituteAllStream`, and `substituteStream` is now deprecated if favor of the new `--replace-fail`, `--replace-warn` and `--replace-quiet`. The deprecated `--replace` equates to `--replace-warn`. - New options were added to the dnsdist module to enable and configure a DNSCrypt endpoint (see `services.dnsdist.dnscrypt.enable`, etc.). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 1384c7c6dfa2..72b6d42591da 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1527,6 +1527,7 @@ ./tasks/filesystems/jfs.nix ./tasks/filesystems/nfs.nix ./tasks/filesystems/ntfs.nix + ./tasks/filesystems/overlayfs.nix ./tasks/filesystems/reiserfs.nix ./tasks/filesystems/sshfs.nix ./tasks/filesystems/squashfs.nix diff --git a/nixos/modules/tasks/filesystems/overlayfs.nix b/nixos/modules/tasks/filesystems/overlayfs.nix new file mode 100644 index 000000000000..e71ef9ba62e9 --- /dev/null +++ b/nixos/modules/tasks/filesystems/overlayfs.nix @@ -0,0 +1,144 @@ +{ config, lib, pkgs, utils, ... }: + +let + # The scripted initrd contains some magic to add the prefix to the + # paths just in time, so we don't add it here. + sysrootPrefix = fs: + if config.boot.initrd.systemd.enable && (utils.fsNeededForBoot fs) then + "/sysroot" + else + ""; + + # Returns a service that creates the required directories before the mount is + # created. + preMountService = _name: fs: + let + prefix = sysrootPrefix fs; + + escapedMountpoint = utils.escapeSystemdPath (prefix + fs.mountPoint); + mountUnit = "${escapedMountpoint}.mount"; + + upperdir = prefix + fs.overlay.upperdir; + workdir = prefix + fs.overlay.workdir; + in + lib.mkIf (fs.overlay.upperdir != null) + { + "rw-${escapedMountpoint}" = { + requiredBy = [ mountUnit ]; + before = [ mountUnit ]; + unitConfig = { + DefaultDependencies = false; + RequiresMountsFor = "${upperdir} ${workdir}"; + }; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${pkgs.coreutils}/bin/mkdir -p -m 0755 ${upperdir} ${workdir}"; + }; + }; + }; + + overlayOpts = { config, ... }: { + + options.overlay = { + + lowerdir = lib.mkOption { + type = with lib.types; nullOr (nonEmptyListOf (either str pathInStore)); + default = null; + description = lib.mdDoc '' + The list of path(s) to the lowerdir(s). + + To create a writable overlay, you MUST provide an upperdir and a + workdir. + + You can create a read-only overlay when you provide multiple (at + least 2!) lowerdirs and neither an upperdir nor a workdir. + ''; + }; + + upperdir = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = lib.mdDoc '' + The path to the upperdir. + + If this is null, a read-only overlay is created using the lowerdir. + + If you set this to some value you MUST also set `workdir`. + ''; + }; + + workdir = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = lib.mdDoc '' + The path to the workdir. + + This MUST be set if you set `upperdir`. + ''; + }; + + }; + + config = lib.mkIf (config.overlay.lowerdir != null) { + fsType = "overlay"; + device = lib.mkDefault "overlay"; + + options = + let + prefix = sysrootPrefix config; + + lowerdir = map (s: prefix + s) config.overlay.lowerdir; + upperdir = prefix + config.overlay.upperdir; + workdir = prefix + config.overlay.workdir; + in + [ + "lowerdir=${lib.concatStringsSep ":" lowerdir}" + ] ++ lib.optionals (config.overlay.upperdir != null) [ + "upperdir=${upperdir}" + "workdir=${workdir}" + ] ++ (map (s: "x-systemd.requires-mounts-for=${s}") lowerdir); + }; + + }; +in + +{ + + options = { + + # Merge the overlay options into the fileSystems option. + fileSystems = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule [ overlayOpts ]); + }; + + }; + + config = + let + overlayFileSystems = lib.filterAttrs (_name: fs: (fs.overlay.lowerdir != null)) config.fileSystems; + initrdFileSystems = lib.filterAttrs (_name: utils.fsNeededForBoot) overlayFileSystems; + userspaceFileSystems = lib.filterAttrs (_name: fs: (!utils.fsNeededForBoot fs)) overlayFileSystems; + in + { + + boot.initrd.availableKernelModules = lib.mkIf (initrdFileSystems != { }) [ "overlay" ]; + + assertions = lib.concatLists (lib.mapAttrsToList + (_name: fs: [ + { + assertion = (fs.overlay.upperdir == null) == (fs.overlay.workdir == null); + message = "You cannot define a `lowerdir` without a `workdir` and vice versa for mount point: ${fs.mountPoint}"; + } + { + assertion = (fs.overlay.lowerdir != null && fs.overlay.upperdir == null) -> (lib.length fs.overlay.lowerdir) >= 2; + message = "A read-only overlay (without an `upperdir`) requires at least 2 `lowerdir`s: ${fs.mountPoint}"; + } + ]) + config.fileSystems); + + boot.initrd.systemd.services = lib.mkMerge (lib.mapAttrsToList preMountService initrdFileSystems); + systemd.services = lib.mkMerge (lib.mapAttrsToList preMountService userspaceFileSystems); + + }; + +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index c2114098fe05..31af6ec64214 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -301,6 +301,7 @@ in { fenics = handleTest ./fenics.nix {}; ferm = handleTest ./ferm.nix {}; ferretdb = handleTest ./ferretdb.nix {}; + filesystems-overlayfs = runTest ./filesystems-overlayfs.nix; firefox = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox; }; firefox-beta = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox-beta; }; firefox-devedition = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox-devedition; }; diff --git a/nixos/tests/filesystems-overlayfs.nix b/nixos/tests/filesystems-overlayfs.nix new file mode 100644 index 000000000000..d7cbf640abe4 --- /dev/null +++ b/nixos/tests/filesystems-overlayfs.nix @@ -0,0 +1,89 @@ +{ lib, pkgs, ... }: + +let + initrdLowerdir = pkgs.runCommand "initrd-lowerdir" { } '' + mkdir -p $out + echo "initrd" > $out/initrd.txt + ''; + initrdLowerdir2 = pkgs.runCommand "initrd-lowerdir-2" { } '' + mkdir -p $out + echo "initrd2" > $out/initrd2.txt + ''; + userspaceLowerdir = pkgs.runCommand "userspace-lowerdir" { } '' + mkdir -p $out + echo "userspace" > $out/userspace.txt + ''; + userspaceLowerdir2 = pkgs.runCommand "userspace-lowerdir-2" { } '' + mkdir -p $out + echo "userspace2" > $out/userspace2.txt + ''; +in +{ + + name = "writable-overlays"; + + meta.maintainers = with lib.maintainers; [ nikstur ]; + + nodes.machine = { config, pkgs, ... }: { + boot.initrd.systemd.enable = true; + boot.initrd.availableKernelModules = [ "overlay" ]; + + virtualisation.fileSystems = { + "/initrd-overlay" = { + overlay = { + lowerdir = [ initrdLowerdir ]; + upperdir = "/.rw-initrd-overlay/upper"; + workdir = "/.rw-initrd-overlay/work"; + }; + neededForBoot = true; + }; + "/userspace-overlay" = { + overlay = { + lowerdir = [ userspaceLowerdir ]; + upperdir = "/.rw-userspace-overlay/upper"; + workdir = "/.rw-userspace-overlay/work"; + }; + }; + "/ro-initrd-overlay" = { + overlay.lowerdir = [ + initrdLowerdir + initrdLowerdir2 + ]; + neededForBoot = true; + }; + "/ro-userspace-overlay" = { + overlay.lowerdir = [ + userspaceLowerdir + userspaceLowerdir2 + ]; + }; + }; + }; + + testScript = '' + machine.wait_for_unit("default.target") + + with subtest("Initrd overlay"): + machine.wait_for_file("/initrd-overlay/initrd.txt", 5) + machine.succeed("touch /initrd-overlay/writable.txt") + machine.succeed("findmnt --kernel --types overlay /initrd-overlay") + + with subtest("Userspace overlay"): + machine.wait_for_file("/userspace-overlay/userspace.txt", 5) + machine.succeed("touch /userspace-overlay/writable.txt") + machine.succeed("findmnt --kernel --types overlay /userspace-overlay") + + with subtest("Read only initrd overlay"): + machine.wait_for_file("/ro-initrd-overlay/initrd.txt", 5) + machine.wait_for_file("/ro-initrd-overlay/initrd2.txt", 5) + machine.fail("touch /ro-initrd-overlay/not-writable.txt") + machine.succeed("findmnt --kernel --types overlay /ro-initrd-overlay") + + with subtest("Read only userspace overlay"): + machine.wait_for_file("/ro-userspace-overlay/userspace.txt", 5) + machine.wait_for_file("/ro-userspace-overlay/userspace2.txt", 5) + machine.fail("touch /ro-userspace-overlay/not-writable.txt") + machine.succeed("findmnt --kernel --types overlay /ro-userspace-overlay") + ''; + +}