{ pkgs, lib, config, ... }: with lib; let cfg = config.fudo.system; mkDisableOption = description: mkOption { type = types.bool; default = true; description = description; }; isEmpty = lst: 0 == (length lst); serviceOpts = { name, ... }: with types; { options = { after = mkOption { type = listOf str; description = "List of services to start before this one."; default = [ ]; }; script = mkOption { type = nullOr str; description = "Simple shell script for the service to run."; default = null; }; reloadScript = mkOption { type = nullOr str; description = "Script to run whenever the service is restarted."; default = null; }; before = mkOption { type = listOf str; description = "List of services before which this service should be started."; default = [ ]; }; requires = mkOption { type = listOf str; description = "List of services on which this service depends. If they fail to start, this service won't start."; default = [ ]; }; preStart = mkOption { type = nullOr str; description = "Script to run prior to starting this service."; default = null; }; postStart = mkOption { type = nullOr str; description = "Script to run after starting this service."; default = null; }; preStop = mkOption { type = nullOr str; description = "Script to run prior to stopping this service."; default = null; }; postStop = mkOption { type = nullOr str; description = "Script to run after stopping this service."; default = null; }; requiredBy = mkOption { type = listOf str; description = "List of services which require this service, and should fail without it."; default = [ ]; }; wantedBy = mkOption { type = listOf str; default = [ ]; description = "List of services before which this service should be started."; }; environment = mkOption { type = attrsOf str; description = "Environment variables supplied to this service."; default = { }; }; environment-file = mkOption { type = nullOr str; description = "File containing environment variables supplied to this service."; default = null; }; description = mkOption { type = str; description = "Description of the service."; }; path = mkOption { type = listOf package; description = "A list of packages which should be in the service PATH."; default = [ ]; }; restartIfChanged = mkDisableOption "Restart the service if the definition changes."; dynamicUser = mkDisableOption "Create a new user for this service."; privateNetwork = mkDisableOption "Only allow access to localhost."; privateUsers = mkDisableOption "Don't allow access to system user list."; privateDevices = mkDisableOption "Restrict access to system devices other than basics."; privateTmp = mkDisableOption "Limit service to a private tmp dir."; protectControlGroups = mkDisableOption "Don't allow service to modify control groups."; protectClock = mkDisableOption "Don't allow service to modify system clock."; restrictSuidSgid = mkDisableOption "Don't allow service to suid or sgid binaries."; protectKernelTunables = mkDisableOption "Don't allow service to modify kernel tunables."; privateMounts = mkDisableOption "Don't allow service to access mounted devices."; protectKernelModules = mkDisableOption "Don't allow service to load or evict kernel modules."; protectHome = mkDisableOption "Limit access to home directories."; protectHostname = mkDisableOption "Don't allow service to modify hostname."; protectKernelLogs = mkDisableOption "Don't allow access to kernel logs."; lockPersonality = mkDisableOption "Lock service 'personality'."; restrictRealtime = mkDisableOption "Restrict service from using realtime functionality."; restrictNamespaces = mkDisableOption "Restrict service from using namespaces."; memoryDenyWriteExecute = mkDisableOption "Restrict process from executing from writable memory."; keyringMode = mkOption { type = str; default = "private"; description = "Sharing state of process keyring."; }; requiredCapabilities = mkOption { type = listOf (enum capabilities); default = [ ]; description = "List of capabilities granted to the service."; }; restartWhen = mkOption { type = str; default = "on-failure"; description = "Conditions under which process should be restarted."; }; restartSec = mkOption { type = int; default = 10; description = "Number of seconds to wait before restarting service."; }; execStart = mkOption { type = nullOr str; default = null; description = "Command to run to launch the service."; }; protectSystem = mkOption { type = enum [ "true" "false" "full" "strict" ]; default = "full"; description = "Level of protection to apply to the system for this service."; }; addressFamilies = mkOption { type = listOf (enum address-families); default = [ ]; description = "List of address families which the service can use."; }; workingDirectory = mkOption { type = nullOr path; default = null; description = "Directory in which to launch the service."; }; user = mkOption { type = nullOr str; default = null; description = "User as which to launch this service."; }; group = mkOption { type = nullOr str; default = null; description = "Primary group as which to launch this service."; }; type = mkOption { type = enum [ "simple" "exec" "forking" "oneshot" "dbus" "notify" "idle" ]; default = "simple"; description = "Systemd service type of this service."; }; partOf = mkOption { type = listOf str; default = [ ]; description = "List of targets to which this service belongs (and with which it should be restarted)."; }; standardOutput = mkOption { type = str; default = "journal"; description = "Destination of standard output for this service."; }; standardError = mkOption { type = str; default = "journal"; description = "Destination of standard error for this service."; }; pidFile = mkOption { type = nullOr str; default = null; description = "Service PID file."; }; networkWhitelist = mkOption { type = nullOr (listOf str); default = null; description = "A list of networks with which this process may communicate."; }; allowedSyscalls = mkOption { type = listOf (enum syscalls); default = [ ]; description = "System calls which the service is permitted to make."; }; maximumUmask = mkOption { type = str; default = "0077"; description = "Umask to apply to files created by the service."; }; startOnlyPerms = mkDisableOption "Disable perms after startup."; onCalendar = mkOption { type = nullOr str; description = "Schedule on which the job should be invoked. See: man systemd.time(7)."; default = null; }; }; }; # See: man capabilities(7) capabilities = [ "CAP_AUDIT_CONTROL" "CAP_AUDIT_READ" "CAP_AUDIT_WRITE" "CAP_BLOCK_SUSPEND" "CAP_BPF" "CAP_CHECKPOINT_RESTORE" "CAP_CHOWN" "CAP_DAC_OVERRIDE" "CAP_DAC_READ_SEARCH" "CAP_FOWNER" "CAP_FSETID" "CAP_IPC_LOCK" "CAP_IPC_OWNER" "CAP_KILL" "CAP_LEASE" "CAP_LINUX_IMMUTABLE" "CAP_MAC_ADMIN" "CAP_MAC_OVERRIDE" "CAP_MKNOD" "CAP_NET_ADMIN" "CAP_NET_BIND_SERVICE" "CAP_NET_BROADCAST" "CAP_NET_RAW" "CAP_PERFMON" "CAP_SETGID" "CAP_SETFCAP" "CAP_SETPCAP" "CAP_SETUID" "CAP_SYS_ADMIN" "CAP_SYS_BOOT" "CAP_SYS_CHROOT" "CAP_SYS_MODULE" "CAP_SYS_NICE" "CAP_SYS_PACCT" "CAP_SYS_PTRACE" "CAP_SYS_RAWIO" "CAP_SYS_RESOURCE" "CAP_SYS_TIME" "CAP_SYS_TTY_CONFIG" "CAP_SYSLOG" "CAP_WAKE_ALARM" ]; syscalls = [ "@clock" "@debug" "@module" "@mount" "@raw-io" "@reboot" "@swap" "@privileged" "@resources" "@cpu-emulation" "@obsolete" ]; address-families = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; restrict-capabilities = allowed: if (allowed == [ ]) then "~${concatStringsSep " " capabilities}" else concatStringsSep " " allowed; restrict-syscalls = allowed: if (allowed == [ ]) then "~${concatStringsSep " " syscalls}" else concatStringsSep " " allowed; restrict-address-families = allowed: if (allowed == [ ]) then "~${concatStringsSep " " address-families}" else concatStringsSep " " allowed; dirOpts = { path, ... }: { options = with types; { user = mkOption { type = str; description = "User by whom the directory will be owned."; default = "nobody"; }; group = mkOption { type = str; description = "Group by which the directory will be owned."; default = "nogroup"; }; perms = mkOption { type = str; description = "Permission bits to apply to the directory."; default = "0770"; }; }; }; in { options.fudo.system = with types; { services = mkOption { type = attrsOf (submodule serviceOpts); description = "Fudo system service definitions, with secure defaults."; default = { }; }; tmpOnTmpfs = mkOption { type = bool; description = "Put tmp filesystem on tmpfs (needs enough RAM)."; default = true; }; ensure-directories = mkOption { type = attrsOf (submodule dirOpts); description = "A map of required directories to directory properties."; default = { }; }; }; config = { # systemd.slices = mapAttrs (name: opts: { # sliceConfig = { # IpAddressAllow = opts.networkWhitelist; # IpAddressDeny = "any"; # }; # }) (filterAttrs (name: opts: opts.networkWhitelist != null) cfg.services); systemd.timers = mapAttrs (name: opts: { enable = true; description = opts.description; partOf = [ "${name}.timer" ]; wantedBy = [ "timers.target" ]; timerConfig = { OnCalendar = opts.onCalendar; }; }) (filterAttrs (name: opts: opts.onCalendar != null) cfg.services); systemd.tmpfiles.rules = mapAttrsToList (path: opts: "d ${path} ${opts.perms} ${opts.user} ${opts.group} - -") cfg.ensure-directories; systemd.targets.fudo-init = { wantedBy = [ "multi-user.target" ]; }; systemd.services = mapAttrs (name: opts: { enable = true; script = mkIf (opts.script != null) opts.script; reload = mkIf (opts.reloadScript != null) opts.reloadScript; after = opts.after ++ [ "fudo-init.target" ]; before = opts.before; requires = opts.requires; wantedBy = opts.wantedBy; preStart = mkIf (opts.preStart != null) opts.preStart; postStart = mkIf (opts.postStart != null) opts.postStart; postStop = mkIf (opts.postStop != null) opts.postStop; preStop = mkIf (opts.preStop != null) opts.preStop; partOf = opts.partOf; requiredBy = opts.requiredBy; environment = opts.environment; description = opts.description; restartIfChanged = opts.restartIfChanged; path = opts.path; serviceConfig = { PrivateNetwork = opts.privateNetwork; PrivateUsers = opts.privateUsers; PrivateDevices = opts.privateDevices; PrivateTmp = opts.privateTmp; PrivateMounts = opts.privateMounts; ProtectControlGroups = opts.protectControlGroups; ProtectKernelTunables = opts.protectKernelTunables; ProtectKernelModules = opts.protectKernelModules; ProtectSystem = opts.protectSystem; ProtectHostname = opts.protectHostname; ProtectHome = opts.protectHome; ProtectClock = opts.protectClock; ProtectKernelLogs = opts.protectKernelLogs; KeyringMode = opts.keyringMode; EnvironmentFile = opts.environment-file; # This is more complicated than it looks... CapabilityBoundingSet = restrict-capabilities opts.requiredCapabilities; DynamicUser = opts.dynamicUser; Restart = opts.restartWhen; WorkingDirectory = mkIf (opts.workingDirectory != null) opts.workingDirectory; RestrictAddressFamilies = restrict-address-families opts.addressFamilies; RestrictNamespaces = opts.restrictNamespaces; User = mkIf (opts.user != null) opts.user; Group = mkIf (opts.group != null) opts.group; Type = opts.type; StandardOutput = opts.standardOutput; PIDFile = mkIf (opts.pidFile != null) opts.pidFile; LockPersonality = opts.lockPersonality; RestrictRealtime = opts.restrictRealtime; ExecStart = mkIf (opts.execStart != null) opts.execStart; MemoryDenyWriteExecute = opts.memoryDenyWriteExecute; SystemCallFilter = restrict-syscalls opts.allowedSyscalls; UMask = opts.maximumUmask; IpAddressAllow = mkIf (opts.networkWhitelist != null) opts.networkWhitelist; IpAddressDeny = mkIf (opts.networkWhitelist != null) "any"; LimitNOFILE = "49152"; PermissionsStartOnly = opts.startOnlyPerms; }; }) config.fudo.system.services; }; }