diff --git a/modules/fs/default.nix b/modules/fs/default.nix index c7ef272ec..dd2cbbdfb 100644 --- a/modules/fs/default.nix +++ b/modules/fs/default.nix @@ -68,10 +68,6 @@ let if this unit fails, it will not block the targets in this list. ''; }; - unit = mkOption { - type = types.str; - description = "name of the systemd unit which ensures this entry"; - }; }; config = let default-acl = { @@ -80,10 +76,6 @@ let mode = lib.mkDefault (parent-acl.mode or "0755"); }; in { - # we put this here instead of as a `default` to ensure that users who specify additional - # dependencies still get a dep on the parent (unless they assign with `mkForce`). - generated.depends = if has-parent then [ parent-cfg.unit ] else []; - # populate generated items from `dir` or `symlink` shorthands generated.acl = lib.mkMerge [ default-acl @@ -102,21 +94,8 @@ let (lib.mkIf (config.symlink != null) (lib.escapeShellArgs [ "${ensure-symlink}/bin/ensure-symlink" name config.symlink.target ])) ]; - # make the unit file which generates the underlying thing available so that `mount` can use it. - generated.unit = (serviceNameFor name) + ".service"; - # if we were asked to mount, make sure we create the dir that we mount over dir = lib.mkIf (config.mount != null) {}; - - # if defaulted, this module is responsible for finalizing the entry. - # the user could override this if, say, they finalize some aspect of the entry - # with a custom service. - unit = lib.mkDefault ( - if config.mount != null then - config.mount.unit - else - config.generated.unit - ); }; }); @@ -207,10 +186,6 @@ let or a string, in which case it's passed onto the CLI unescaped. ''; }; - unit = mkOption { - type = types.str; - description = "name of the systemd unit which ensures this directory"; - }; }; }; @@ -229,11 +204,6 @@ let ''; default = []; }; - unit = mkOption { - type = types.str; - description = "name of the systemd unit which mounts this path"; - default = mountNameFor path; - }; mountConfig = mkOption { type = types.attrs; description = '' @@ -284,16 +254,13 @@ let # given a mountEntry definition, evaluate its toplevel `config` output. mkMountConfig = path: opt: let fsEntry = config.fileSystems."${path}"; - underlying = cfg."${fsEntry.device}"; isBind = opt.mount.bind != null; ifBind = lib.mkIf isBind; # before mounting: # - create the target directory # - prepare the source directory -- assuming it's not an external device # - satisfy any user-specified prerequisites ("depends") - requires = [ opt.generated.unit ] - ++ (if lib.hasPrefix "/dev/disk/" fsEntry.device || lib.hasPrefix "fuse" (fsEntry.fsType or "unknown") then [] else [ underlying.unit ]) - ++ opt.mount.depends; + requires = opt.mount.depends; in { fileSystems."${path}" = { device = ifBind opt.mount.bind; diff --git a/modules/persist/default.nix b/modules/persist/default.nix index 849870777..97863c2f5 100644 --- a/modules/persist/default.nix +++ b/modules/persist/default.nix @@ -231,7 +231,6 @@ in (lib.optionalAttrs (opt.type == "dir") { # create the backing path as a dir sane.fs."${fsPathToBackingPath fspath}" = { - wantedBeforeBy = [ config.sane.fs."${fspath}".unit ]; dir.acl = config.sane.fs."${fspath}".generated.acl; }; }) @@ -240,7 +239,6 @@ in # the old way was to create the parent directory and leave the file empty, expecting the program to create it. # that doesn't work well with sandboxing, where the fs handles we want to give the program have to exist before launch. sane.fs."${fsPathToBackingPath fspath}" = { - wantedBeforeBy = [ config.sane.fs."${fspath}".unit ]; file.acl = config.sane.fs."${fspath}".generated.acl; file.text = lib.mkDefault ""; }; diff --git a/modules/persist/stores/ephemeral/default.nix b/modules/persist/stores/ephemeral/default.nix index cdcd1e317..5794e5f03 100644 --- a/modules/persist/stores/ephemeral/default.nix +++ b/modules/persist/stores/ephemeral/default.nix @@ -4,6 +4,17 @@ let persist-base = "/nix/persist"; origin = config.sane.persist.stores."ephemeral".origin; backing = sane-lib.path.concat [ persist-base "ephemeral" ]; + + # fileSystems.* options + device = "gocryptfs-ephemeral#${backing}"; + fsType = "fuse3.sane"; + options = [ + "nodev" # only works via mount.fuse; gocryptfs requires this be passed as `-ko nodev` + "nosuid" # only works via mount.fuse; gocryptfs requires this be passed as `-ko nosuid` (also, nosuid is default) + "allow_other" # root ends up being the user that mounts this, so need to make it visible to other users. + # "defaults" # "unknown flag: --defaults. Try 'gocryptfs -help'" + "pass_fuse_fd" + ]; in lib.mkIf config.sane.persist.enable { @@ -44,55 +55,53 @@ lib.mkIf config.sane.persist.enable }; fileSystems."${origin}" = { - device = "gocryptfs-ephemeral#${backing}"; - fsType = "fuse3.sane"; - options = [ - "nodev" # only works via mount.fuse; gocryptfs requires this be passed as `-ko nodev` - "nosuid" # only works via mount.fuse; gocryptfs requires this be passed as `-ko nosuid` (also, nosuid is default) - "allow_other" # root ends up being the user that mounts this, so need to make it visible to other users. - # "defaults" # "unknown flag: --defaults. Try 'gocryptfs -help'" - "pass_fuse_fd" - ]; + inherit device fsType options; noCheck = true; }; - # let sane.fs know about our fileSystem and automatically add the appropriate dependencies - sane.fs."${origin}" = { - wantedBeforeBy = [ "local-fs.target" ]; - mount.depends = [ - config.sane.fs."${backing}".unit - ]; + # tell systemd about the mount so that i can sandbox it + systemd.mounts = [{ + where = origin; + what = device; + type = fsType; + options = lib.concatStringsSep "," options; + wantedBy = [ "local-fs.target" ]; + before = [ "local-fs.target" ]; + unitConfig.RequiresMountsFor = [ backing ]; # hardening (systemd-analyze security mnt-persist-ephemeral.mount) - mount.mountConfig.AmbientCapabilities = "CAP_SYS_ADMIN CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_CHOWN CAP_MKNOD CAP_LEASE CAP_SETGID CAP_SETUID CAP_FOWNER"; + mountConfig.AmbientCapabilities = "CAP_SYS_ADMIN CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_CHOWN CAP_MKNOD CAP_LEASE CAP_SETGID CAP_SETUID CAP_FOWNER"; # CAP_LEASE is probably not necessary -- does any fs user use leases? - mount.mountConfig.CapabilityBoundingSet = "CAP_SYS_ADMIN CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_CHOWN CAP_MKNOD CAP_LEASE CAP_SETGID CAP_SETUID CAP_FOWNER"; - mount.mountConfig.LockPersonality = true; - mount.mountConfig.MemoryDenyWriteExecute = true; - mount.mountConfig.NoNewPrivileges = true; - mount.mountConfig.ProtectClock = true; - mount.mountConfig.ProtectHostname = true; - mount.mountConfig.RemoveIPC = true; - mount.mountConfig.RestrictAddressFamilies = "AF_UNIX"; # "none" works, but then it can't connect to the logger + mountConfig.CapabilityBoundingSet = "CAP_SYS_ADMIN CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_CHOWN CAP_MKNOD CAP_LEASE CAP_SETGID CAP_SETUID CAP_FOWNER"; + mountConfig.LockPersonality = true; + mountConfig.MemoryDenyWriteExecute = true; + mountConfig.NoNewPrivileges = true; + mountConfig.ProtectClock = true; + mountConfig.ProtectHostname = true; + mountConfig.RemoveIPC = true; + mountConfig.RestrictAddressFamilies = "AF_UNIX"; # "none" works, but then it can't connect to the logger #VVV this includes anything it reads from, e.g. /bin/sh; /nix/store/... # see `systemd-analyze filesystems` for a full list - mount.mountConfig.RestrictFileSystems = "@common-block @basic-api fuse pipefs"; - # mount.mountConfig.RestrictNamespaces = true; - mount.mountConfig.RestrictNetworkInterfaces = ""; - mount.mountConfig.RestrictRealtime = true; - mount.mountConfig.RestrictSUIDSGID = true; - mount.mountConfig.SystemCallArchitectures = "native"; - mount.mountConfig.SystemCallFilter = [ + mountConfig.RestrictFileSystems = "@common-block @basic-api fuse pipefs"; + # mountConfig.RestrictNamespaces = true; + mountConfig.RestrictNetworkInterfaces = ""; + mountConfig.RestrictRealtime = true; + mountConfig.RestrictSUIDSGID = true; + mountConfig.SystemCallArchitectures = "native"; + mountConfig.SystemCallFilter = [ # unfortunately, i need to keep @network-io (accept, bind, connect, listen, recv, send, socket, ...). not sure why (daemon control socket?). "@system-service" "@mount" "@sandbox" "~@cpu-emulation" "~@keyring" ]; - mount.mountConfig.IPAddressDeny = "any"; - mount.mountConfig.DevicePolicy = "closed"; # only allow /dev/{null,zero,full,random,urandom} - mount.mountConfig.DeviceAllow = "/dev/fuse"; - mount.mountConfig.SocketBindDeny = "any"; + mountConfig.IPAddressDeny = "any"; + mountConfig.DevicePolicy = "closed"; # only allow /dev/{null,zero,full,random,urandom} + mountConfig.DeviceAllow = "/dev/fuse"; + mountConfig.SocketBindDeny = "any"; # note that anything which requires mount namespaces (ProtectHome, ReadWritePaths, ...) does NOT work. # it's in theory possible, via mount propagation, but systemd provides no way for that. - # PrivateNetwork = true BREAKS the mount action; i think systemd or udev needs that internally to communicate with the service manager? - }; + # mountConfig.PrivateNetwork = true BREAKS the mount action; i think systemd or udev needs that internally to communicate with the service manager? + }]; + + # let sane.fs know about our fileSystem and automatically add the appropriate dependencies + sane.fs."${origin}".dir = {}; sane.fs."${backing}".dir = {}; sane.programs.gocryptfs-ephemeral.enableFor.system = true; diff --git a/modules/persist/stores/private/default.nix b/modules/persist/stores/private/default.nix index b631c1ee2..832765d7b 100644 --- a/modules/persist/stores/private/default.nix +++ b/modules/persist/stores/private/default.nix @@ -4,6 +4,27 @@ let persist-base = "/nix/persist"; origin = config.sane.persist.stores."private".origin; backing = sane-lib.path.concat [ persist-base "private" ]; + + # fileSystems.* options + device = "gocryptfs-private#${backing}"; + fsType = "fuse3.sane"; + options = [ + # "auto" + "nofail" + # "noexec" # handful of scripts in ~/knowledge that are executable + "nodev" # only works via mount.fuse; gocryptfs requires this be passed as `-ko nodev` + "nosuid" # only works via mount.fuse; gocryptfs requires this be passed as `-ko nosuid` (also, nosuid is default) + "allow_other" # root ends up being the user that mounts this, so need to make it visible to other users. + # "quiet" + # "defaults" # "unknown flag: --defaults. Try 'gocryptfs -help'" + # "passfile=/run/gocryptfs/private.key" + # options so that we can block for the password file *without* systemd killing us. + # see: + # "x-systemd.mount-timeout=infinity" + # "retry=10000" + # "fg" + "pass_fuse_fd" + ]; in lib.mkIf config.sane.persist.enable { @@ -56,76 +77,55 @@ lib.mkIf config.sane.persist.enable can be auto-unlocked at login. ''; origin = lib.mkDefault "/mnt/persist/private"; - defaultOrdering = let - private-unit = config.sane.fs."${origin}".unit; - in { - # auto create only after the store is mounted - wantedBy = [ private-unit ]; - # we can't create things in private before local-fs.target - wantedBeforeBy = [ ]; - }; defaultMethod = "symlink"; }; fileSystems."${origin}" = { - device = "gocryptfs-private#${backing}"; - fsType = "fuse3.sane"; - options = [ - # "auto" - "nofail" - # "noexec" # handful of scripts in ~/knowledge that are executable - "nodev" # only works via mount.fuse; gocryptfs requires this be passed as `-ko nodev` - "nosuid" # only works via mount.fuse; gocryptfs requires this be passed as `-ko nosuid` (also, nosuid is default) - "allow_other" # root ends up being the user that mounts this, so need to make it visible to other users. - # "quiet" - # "defaults" # "unknown flag: --defaults. Try 'gocryptfs -help'" - # "passfile=/run/gocryptfs/private.key" - # options so that we can block for the password file *without* systemd killing us. - # see: - # "x-systemd.mount-timeout=infinity" - # "retry=10000" - # "fg" - "pass_fuse_fd" - ]; + inherit device fsType options; noCheck = true; }; - # let sane.fs know about the mount - sane.fs."${origin}" = { - wantedBy = [ "local-fs.target" ]; - mount.depends = [ - config.sane.fs."${backing}".unit - "gocryptfs-private-key.service" - ]; + # tell systemd about the mount so that i can sandbox it + systemd.mounts = [{ + where = origin; + what = device; + type = fsType; + options = lib.concatStringsSep "," options; + after = [ "gocryptfs-private-key.service" ]; + wants = [ "gocryptfs-private-key.service" ]; + + unitConfig.RequiresMountsFor = [ backing ]; # unitConfig.DefaultDependencies = "no"; - # mount.mountConfig.TimeoutSec = "infinity"; + # mountConfig.TimeoutSec = "infinity"; # hardening (systemd-analyze security mnt-persist-private.mount) - mount.mountConfig.AmbientCapabilities = "CAP_SYS_ADMIN CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_CHOWN CAP_MKNOD CAP_LEASE CAP_SETGID CAP_SETUID CAP_FOWNER"; + mountConfig.AmbientCapabilities = "CAP_SYS_ADMIN CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_CHOWN CAP_MKNOD CAP_LEASE CAP_SETGID CAP_SETUID CAP_FOWNER"; # CAP_LEASE is probably not necessary -- does any fs user use leases? - mount.mountConfig.CapabilityBoundingSet = "CAP_SYS_ADMIN CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_CHOWN CAP_MKNOD CAP_LEASE CAP_SETGID CAP_SETUID CAP_FOWNER"; - mount.mountConfig.LockPersonality = true; - mount.mountConfig.MemoryDenyWriteExecute = true; - mount.mountConfig.NoNewPrivileges = true; - mount.mountConfig.ProtectClock = true; - mount.mountConfig.ProtectHostname = true; - mount.mountConfig.RemoveIPC = true; - mount.mountConfig.RestrictAddressFamilies = "AF_UNIX"; # "none" works, but then it can't connect to the logger - mount.mountConfig.RestrictFileSystems = "@common-block @basic-api fuse pipefs"; - # mount.mountConfig.RestrictNamespaces = true; - mount.mountConfig.RestrictNetworkInterfaces = ""; - mount.mountConfig.RestrictRealtime = true; - mount.mountConfig.RestrictSUIDSGID = true; - mount.mountConfig.SystemCallArchitectures = "native"; - mount.mountConfig.SystemCallFilter = [ + mountConfig.CapabilityBoundingSet = "CAP_SYS_ADMIN CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_CHOWN CAP_MKNOD CAP_LEASE CAP_SETGID CAP_SETUID CAP_FOWNER"; + mountConfig.LockPersonality = true; + mountConfig.MemoryDenyWriteExecute = true; + mountConfig.NoNewPrivileges = true; + mountConfig.ProtectClock = true; + mountConfig.ProtectHostname = true; + mountConfig.RemoveIPC = true; + mountConfig.RestrictAddressFamilies = "AF_UNIX"; # "none" works, but then it can't connect to the logger + mountConfig.RestrictFileSystems = "@common-block @basic-api fuse pipefs"; + # mountConfig.RestrictNamespaces = true; + mountConfig.RestrictNetworkInterfaces = ""; + mountConfig.RestrictRealtime = true; + mountConfig.RestrictSUIDSGID = true; + mountConfig.SystemCallArchitectures = "native"; + mountConfig.SystemCallFilter = [ # unfortunately, i need to keep @network-io (accept, bind, connect, listen, recv, send, socket, ...). not sure why (daemon control socket?). "@system-service" "@mount" "@sandbox" "~@cpu-emulation" "~@keyring" ]; - mount.mountConfig.IPAddressDeny = "any"; - mount.mountConfig.DevicePolicy = "closed"; # only allow /dev/{null,zero,full,random,urandom} - mount.mountConfig.DeviceAllow = "/dev/fuse"; - mount.mountConfig.SocketBindDeny = "any"; - }; + mountConfig.IPAddressDeny = "any"; + mountConfig.DevicePolicy = "closed"; # only allow /dev/{null,zero,full,random,urandom} + mountConfig.DeviceAllow = "/dev/fuse"; + mountConfig.SocketBindDeny = "any"; + }]; + # let sane.fs know about the mount + sane.fs."${origin}" = { }; # it also needs to know that the underlying device is an ordinary folder sane.fs."${backing}".dir = {};