Files
brianmcgee 206bb1fe1f {pkg, nix}: create plugin directories at startup
Replace the NixOS tmpfiles-based approach for pre-creating plugin
subdirectories with runtime directory creation in the data-mesher
process itself. This ensures plugin directories exist under every
configured network's state directory, not just the home network.

- Rename NixOS option fileDirectories to pluginDirectories
- Add plugin_directories as a top-level dm.toml config key
- Create plugin subdirectories in NewFiles() on startup
- Remove tmpfiles rules and related assertions from the NixOS module
- Add test covering multi-network plugin directory creation
2026-04-07 11:50:56 +01:00

174 lines
5.7 KiB
Nix

{
lib,
pkgs,
config,
...
}:
let
cfg = config.services.data-mesher;
settingsFormat = pkgs.formats.toml { };
in
{
options.services.data-mesher = {
enable = lib.mkEnableOption "Data Mesher, data syncing daemon";
package = lib.mkOption {
type = lib.types.package;
description = "The Data Mesher package to use.";
default = pkgs.callPackage ../../packages/data-mesher/package.nix { };
};
user = lib.mkOption {
type = lib.types.str;
default = "data-mesher";
description = ''
User account under which data-mesher runs.
'';
};
group = lib.mkOption {
type = lib.types.str;
default = "data-mesher";
description = ''
User group under which data-mesher runs.
'';
};
openFirewall = lib.mkEnableOption "Open ports in firewall";
pluginDirectories = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"dns"
"nss"
];
description = ''
List of subdirectories to create under each network's files directory on startup.
These are intended for plugins that need to BindReadOnlyPath into the file tree.
Plugin services should use After=data-mesher.service to ensure directories exist.
'';
};
settings = lib.mkOption {
default = { };
description = "Data Mesher settings, which correspond to the contents of the dm.toml file";
type = lib.types.submodule {
freeformType = settingsFormat.type;
imports = [
./settings.nix
];
};
};
};
config = lib.mkIf cfg.enable (
let
configFile =
settingsFormat.generate "dm.toml"
# filter any null values as the toml generator doesn't like it
(lib.filterAttrsRecursive (_n: v: v != null) cfg.settings);
in
{
environment.systemPackages = [
cfg.package
];
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [
cfg.settings.cluster.port
cfg.settings.http.port
];
};
services.data-mesher.settings.plugin_directories = lib.mkDefault cfg.pluginDirectories;
environment.etc."${cfg.user}/dm.toml".source = configFile;
users.users.${cfg.user} = {
isSystemUser = true;
home = "/var/lib/${cfg.user}";
inherit (cfg) group;
};
users.groups.${cfg.group} = { };
systemd.services.data-mesher = {
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
description = "data mesher daemon";
# restart if the config changes
restartTriggers = [ configFile ];
serviceConfig = {
Type = "notify";
User = cfg.user;
Group = cfg.group;
StateDirectory = cfg.user;
ConfigurationDirectory = cfg.user;
Restart = "always";
RestartSec = "10";
ExecStart = "${cfg.package}/bin/data-mesher server";
# Basic protection (similar to DynamicUser)
RemoveIPC = true; # Remove SysV IPC objects when service stops
PrivateTmp = true; # Private /tmp and /var/tmp
NoNewPrivileges = true; # Prevent privilege escalation via setuid/setgid
ProtectSystem = "strict"; # Mount /, /usr, /boot, /efi read-only; /etc read-only
ProtectHome = "read-only"; # Make /home, /root, /run/user read-only
# Filesystem isolation
ProtectProc = "invisible"; # Hide processes owned by other users in /proc
ProcSubset = "pid"; # Only expose /proc/self and /proc/pid/*, hide /proc/sys, /proc/fs, etc.
PrivateDevices = true; # Private /dev with only pseudo-devices (null, zero, random), no physical hardware
ProtectKernelTunables = true; # Make /proc/sys and /sys read-only, prevent tweaking kernel parameters
ProtectKernelModules = true; # Deny module loading/unloading, block access to /usr/lib/modules
ProtectKernelLogs = true; # Block access to kernel log buffer (/dev/kmsg, dmesg)
ProtectControlGroups = true; # Make /sys/fs/cgroup read-only, prevent cgroup manipulation
ProtectClock = true; # Prevent setting system/hardware clock
ProtectHostname = true; # Prevent changing hostname/domainname
# Only allow IPv4, IPv6, and Unix sockets; block netlink, bluetooth, packet sockets, etc.
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
"AF_NETLINK" # Required for querying network interfaces
];
# Drop all capabilities since ports > 1024 don't need CAP_NET_BIND_SERVICE
CapabilityBoundingSet = "";
AmbientCapabilities = "";
# System calls
SystemCallArchitectures = "native"; # Only allow native arch syscalls, block 32-bit compat (common exploit vector)
# Allowlist syscalls typical for services, minus privileged ops (reboot, mount) and resource control
SystemCallFilter = [
"@system-service"
"~@privileged"
"~@resources"
];
SystemCallErrorNumber = "EPERM"; # Return "Permission denied" for blocked syscalls instead of killing process
# Other restrictions
LockPersonality = true; # Prevent changing execution domain (used for foreign binaries)
RestrictNamespaces = true; # Prevent creating new namespaces; blocks container-escape techniques
RestrictRealtime = true; # Prevent realtime scheduling; stops potential DoS via CPU monopolization
RestrictSUIDSGID = true; # Prevent creating/running SUID/SGID files
UMask = "0027"; # Files created are 0640 (owner rw, group r), directories 0750
};
};
}
);
}