nixos-config/lib/fudo/system.nix

501 lines
17 KiB
Nix

{ 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.";
};
execStop = mkOption {
type = nullOr str;
default = null;
description = "Command to run to launch the service.";
};
protectSystem = mkOption {
type = enum [ "true" "false" "full" "strict" true false ];
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;
};
runtimeDirectory = mkOption {
type = nullOr str;
description =
"Directory created at runtime with perms for the service to read/write.";
default = null;
};
readWritePaths = mkOption {
type = listOf str;
description =
"A list of paths to which the service will be allowed normal access, even if ProtectSystem=strict.";
default = [ ];
};
stateDirectory = mkOption {
type = nullOr str;
description =
"State directory for the service, available via STATE_DIRECTORY.";
default = null;
};
cacheDirectory = mkOption {
type = nullOr str;
description =
"Cache directory for the service, available via CACHE_DIRECTORY.";
default = null;
};
inaccessiblePaths = mkOption {
type = listOf str;
description =
"A list of paths which should be inaccessible to the service.";
default = [ "/home" "/root" ];
};
# noExecPaths = mkOption {
# type = listOf str;
# description =
# "A list of paths where the service will not be allowed to run executables.";
# default = [ "/home" "/root" "/tmp" "/var" ];
# };
readOnlyPaths = mkOption {
type = listOf str;
description =
"A list of paths to which will be read-only for the service.";
default = [ ];
};
execPaths = mkOption {
type = listOf str;
description =
"A list of paths where the service WILL be allowed to run executables.";
default = [ ];
};
};
};
# 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 [ "~AF_INET" "~AF_INET6" ] else 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.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 = mkIf (opts.user == null) 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 =
mkIf (opts.environment-file != null) opts.environment-file;
# This is more complicated than it looks...
# CapabilityBoundingSet = restrict-capabilities opts.requiredCapabilities;
AmbientCapabilities = concatStringsSep " " opts.requiredCapabilities;
SecureBits = mkIf ((length opts.requiredCapabilities) > 0) "keep-caps";
DynamicUser = mkIf (opts.user == null) 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;
ExecStop = mkIf (opts.execStop != null) opts.execStop;
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;
RuntimeDirectory =
mkIf (opts.runtimeDirectory != null) opts.runtimeDirectory;
CacheDirectory = mkIf (opts.cacheDirectory != null) opts.cacheDirectory;
StateDirectory = mkIf (opts.stateDirectory != null) opts.stateDirectory;
ReadWritePaths = opts.readWritePaths;
ReadOnlyPaths = opts.readOnlyPaths;
InaccessiblePaths = opts.inaccessiblePaths;
# Apparently not supported yet?
# NoExecPaths = opts.noExecPaths;
ExecPaths = opts.execPaths;
};
}) config.fudo.system.services;
};
}