{ lib, config, options, pkgs, ... }: let cfg = config.services.homebox; inherit (lib) literalExpression mkEnableOption mkPackageOption mkDefault mkOption types mkIf ; defaultUser = "homebox"; defaultGroup = "homebox"; pepperDefault = (cfg.secrets ? HBOX_AUTH_API_KEY_PEPPER) && (cfg.secrets.HBOX_AUTH_API_KEY_PEPPER == "/var/lib/homebox/api-pepper-secret"); opts = options.services.homebox; in { options.services.homebox = { enable = mkEnableOption "homebox"; package = mkPackageOption pkgs "homebox" { }; user = mkOption { type = types.str; default = defaultUser; description = "User account under which Homebox runs."; }; group = mkOption { type = types.str; default = defaultGroup; description = "Group under which Homebox runs."; }; settings = mkOption { type = types.submodule { freeformType = types.attrsOf (types.nullOr types.str); }; defaultText = lib.literalExpression '' { HBOX_STORAGE_CONN_STRING = "file:///var/lib/homebox"; HBOX_STORAGE_PREFIX_PATH = "data"; HBOX_DATABASE_DRIVER = "sqlite3"; HBOX_DATABASE_SQLITE_PATH = "/var/lib/homebox/data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1"; HBOX_OPTIONS_ALLOW_REGISTRATION = "false"; HBOX_OPTIONS_GITHUB_RELEASE_CHECK = "false"; HBOX_MODE = "production"; HOME = "/var/lib/homebox"; TMPDIR = "/var/lib/homebox/tmp"; } ''; description = '' The homebox configuration as environment variables. For definitions and available options see the upstream [documentation](https://homebox.software/en/configure/#configure-homebox). ''; }; database = { createLocally = mkOption { type = lib.types.bool; default = false; description = '' Configure local PostgreSQL database server for Homebox. ''; }; }; secrets = mkOption { type = types.submodule { options = { HBOX_AUTH_API_KEY_PEPPER = mkOption { type = types.externalPath; default = "/var/lib/homebox/api-pepper-secret"; description = '' Path to the API key pepper secret file (required for homebox to start). ''; example = "/run/secrets/homebox-api-pepper"; }; }; freeformType = types.attrsOf types.externalPath; }; default = { }; description = '' This follows the same structure as {option}`${opts.settings}` but the value of each key is a path. The specified secret path is then read by systemd via [`LoadCredential=`] and templated into {option}`${opts.settings}` for you. [`LoadCredential=`]: https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#Credentials ''; example = literalExpression '' { HBOX_AUTH_API_KEY_PEPPER = "/run/secrets/homebox-api-pepper"; } ''; }; }; config = mkIf cfg.enable { assertions = [ { assertion = !(cfg.settings ? HBOX_STORAGE_DATA); message = '' `services.homebox.settings.HBOX_STORAGE_DATA` has been deprecated. Please use `services.homebox.settings.HBOX_STORAGE_CONN_STRING` and `services.homebox.settings.HBOX_STORAGE_PREFIX_PATH` instead. ''; } ]; users = { users = mkIf (cfg.user == defaultUser) { ${defaultUser} = { description = "homebox service user"; inherit (cfg) group; isSystemUser = true; }; }; groups = mkIf (cfg.group == defaultGroup) { ${defaultGroup} = { }; }; }; services.homebox.settings = lib.mkMerge [ (lib.mapAttrs (_: mkDefault) { HBOX_STORAGE_CONN_STRING = "file:///var/lib/homebox"; HBOX_STORAGE_PREFIX_PATH = "data"; HBOX_DATABASE_DRIVER = "sqlite3"; HBOX_DATABASE_SQLITE_PATH = "/var/lib/homebox/data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1"; HBOX_OPTIONS_ALLOW_REGISTRATION = "false"; HBOX_OPTIONS_GITHUB_RELEASE_CHECK = "false"; HBOX_MODE = "production"; # Fix this startup issue: # failed to create modcache index dir: mkdir /var/empty/.cache: read-only file system HOME = "/var/lib/homebox"; # Fix uploading/saving attachments/images: # [...] rename /tmp/ced4804c80b1ed1f6e88060f6d829db421e6dbf3a189715265900b5d6b0243ed.1889b3d16ab36e22.tmp /var/lib/homebox/data/5f42f81b-e9ad-4495-b6a6-9e9f704db30e/documents/ced4804c80b1ed1f6e88060f6d829db421e6dbf3a189715265900b5d6b0243ed: invalid cross-device link" [...] TMPDIR = "/var/lib/homebox/tmp"; }) (mkIf cfg.database.createLocally { HBOX_DATABASE_DRIVER = "postgres"; HBOX_DATABASE_HOST = "/run/postgresql"; HBOX_DATABASE_USERNAME = "homebox"; HBOX_DATABASE_DATABASE = "homebox"; HBOX_DATABASE_PORT = toString config.services.postgresql.settings.port; }) ]; services.postgresql = mkIf cfg.database.createLocally { enable = true; ensureDatabases = [ "homebox" ]; ensureUsers = [ { name = "homebox"; ensureDBOwnership = true; } ]; }; systemd.services.homebox-setup = mkIf pepperDefault { script = '' if [ ! -r "$STATE_DIRECTORY"/api-pepper-secret ]; then umask 0277 openssl rand -base64 48 > "$STATE_DIRECTORY"/api-pepper-secret fi ''; path = [ pkgs.openssl ]; serviceConfig = { Type = "oneshot"; User = cfg.user; Group = cfg.group; StateDirectory = "homebox"; }; }; systemd.services.homebox = let deps = lib.optionals pepperDefault [ "homebox-setup.service" ] ++ lib.optionals cfg.database.createLocally [ "postgresql.target" ]; in { requires = deps; after = deps; environment = lib.filterAttrs (_: v: v != null) cfg.settings; preStart = '' "${pkgs.coreutils}/bin/rm" -rf /var/lib/homebox/tmp "${pkgs.coreutils}/bin/mkdir" -p /var/lib/homebox/tmp ''; script = '' ${lib.strings.concatLines ( lib.mapAttrsToList (name: _: "export ${name}=$(<\"$CREDENTIALS_DIRECTORY\"/${name})") cfg.secrets )} exec ${lib.getExe cfg.package} ''; serviceConfig = { User = cfg.user; Group = cfg.group; LoadCredential = (lib.mapAttrsToList (name: path: "${name}:${path}") cfg.secrets); LimitNOFILE = "1048576"; PrivateTmp = true; PrivateDevices = true; Restart = "always"; StateDirectory = "homebox"; # Hardening CapabilityBoundingSet = ""; LockPersonality = true; MemoryDenyWriteExecute = true; PrivateUsers = true; ProtectClock = true; ProtectControlGroups = true; ProtectHome = true; ProtectHostname = true; ProtectKernelLogs = true; ProtectKernelModules = true; ProtectKernelTunables = true; ProtectProc = "invisible"; ProcSubset = "pid"; ProtectSystem = "strict"; RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" "AF_NETLINK" ]; RestrictNamespaces = true; RestrictRealtime = true; SystemCallArchitectures = "native"; SystemCallFilter = [ "@system-service" "@pkey" ]; RestrictSUIDSGID = true; PrivateMounts = true; UMask = "0077"; }; wantedBy = [ "multi-user.target" ]; }; }; meta.maintainers = with lib.maintainers; [ patrickdag swarsel ]; }