kittybox: { config, pkgs, lib, ... }: with lib; let cfg = config.services.kittybox; in { options = { services.kittybox = { enable = mkOption { type = types.bool; default = false; description = '' Whether to enable Kittybox, the IndieWeb blogging solution. ''; }; package = mkOption { type = types.package; default = kittybox.packages.${pkgs.stdenv.hostPlatform.system}.kittybox; defaultText = ""; description = "Which Kittybox derivation to use."; }; bind = mkOption { type = types.nullOr types.str; default = "[::1]"; description = "The host for Kittybox to bind to."; example = "192.168.1.100"; }; port = mkOption { type = types.int; default = 8080; description = "The port for Kittybox to listen at."; example = 16420; }; logLevel = mkOption { type = types.str; default = "warn"; example = "info"; description = "Specify the server verbosity level. Uses RUST_LOG environment variable internally."; }; backendUri = mkOption { type = types.str; default = "file:///var/lib/kittybox/data"; example = "redis://192.168.1.200:6379"; description = lib.mdDoc '' Set the backend used for storing data. Available backends are: - `postgres://` - PostgreSQL backend (recommended) - `file://` - static folder backend - `redis://` - Redis backend (currently unavailable) Make sure that if you are using the file backend, the state directory is accessible by Kittybox. By default, the unit config uses DynamicUser=true and heavy sandboxing options which prevent the unit from accessing data outside of its directory. It is recommended to reconfigure the sandboxing or use a bind-mount to `/var/lib/private/kittybox` if you require the state directory to reside elsewhere. ''; }; blobstoreUri = mkOption { type = types.nullOr types.str; default = "file:///var/lib/kittybox/media"; description = lib.mdDoc '' Set the backend used for the media endpoint storage. Available options are: - `file://` - content-addressed storage using flat files (recommended) When using the file backend, check notes in the `backendUri` option too. ''; }; authstoreUri = mkOption { type = types.nullOr types.str; default = "file:///var/lib/kittybox/auth"; description = lib.mdDoc '' Set the backend used for persisting authentication data. Available options are: - `file://` - flat files. Codes are stored globally, tokens and credentials are stored per-site. ''; }; jobQueueUri = mkOption { type = types.nullOr types.str; default = "postgres://localhost/kittybox?host=/run/postgresql"; description = lib.mdDoc '' Set the job queue backend. Available options are: - `postgres://` - PostgreSQL based job queue. It shares the schema with the Kittybox PostgreSQL backend, so Kittybox can reuse the same database for both. ''; }; microsubServer = mkOption { type = types.nullOr types.str; default = null; example = "https://aperture.p3k.io/microsub/69420"; description = '' The URL of your Microsub server, which saves feeds for you and allows you to browse Web content from one place. Try https://aperture.p3k.io/ if you don't have one yet! ''; }; internalTokenFile = mkOption { type = types.nullOr types.str; default = null; example = "/run/secrets/kittybox-shared-secret"; description = "A shared secret that will, when passed, allow unlimited editing access to database. Keep it safe."; }; cookieSecretFile = mkOption { type = types.str; default = "/var/lib/kittybox/cookie_secret_key"; example = "/run/secrets/kittybox-cookie-secret"; description = "A secret file to encrypt cookies with the contents of. Should be at least 32 bytes in length. A random persistent file will be generated if it doesn't exist or is invalid."; }; }; }; config = lib.mkIf cfg.enable { assertions = [ { assertion = (lib.strings.hasPrefix cfg.backendUri "postgres://" || lib.strings.hasPrefix cfg.jobQueueUri "postgres://") -> cfg.package.hasPostgres; message = "To use the Postgres backend, Kittybox has to be compiled with Postgres support enabled."; } ]; systemd.sockets.kittybox = { description = config.systemd.services.kittybox.description; wantedBy = [ "sockets.target" ]; restartTriggers = [ cfg.bind cfg.port ]; listenStreams = lib.mkMerge [ [ (lib.mkIf (cfg.bind == null) (builtins.toString cfg.port)) ] [ (lib.mkIf (cfg.bind != null) "${cfg.bind}:${builtins.toString cfg.port}") ] ]; socketConfig = { BindIPv6Only = true; }; }; systemd.services.kittybox = { description = "An IndieWeb-enabled blog engine"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; restartTriggers = [ cfg.package cfg.backendUri cfg.blobstoreUri cfg.authstoreUri cfg.jobQueueUri cfg.internalTokenFile cfg.bind cfg.port cfg.cookieSecretFile ]; environment = { MICROSUB_ENDPOINT = cfg.microsubServer; BACKEND_URI = cfg.backendUri; BLOBSTORE_URI = cfg.blobstoreUri; AUTH_STORE_URI = cfg.authstoreUri; JOB_QUEUE_URI = cfg.jobQueueUri; RUST_LOG = "${cfg.logLevel}"; # TODO: consider hardening by using systemd credentials COOKIE_KEY_FILE = "${cfg.cookieSecretFile}"; }; script = '' ${lib.optionalString (cfg.internalTokenFile != null) '' if [[ -f ${cfg.internalTokenFile} ]]; then export KITTYBOX_INTERNAL_TOKEN=$(${pkgs.coreutils}/bin/cat ${cfg.internalTokenFile}) fi ''} if [[ ! -e "$COOKIE_KEY_FILE" ]]; then dd if=/dev/urandom bs=64 count=1 | base64 > "$COOKIE_KEY_FILE" fi export COOKIE_KEY="$(cat "$COOKIE_KEY_FILE")" exec ${cfg.package}/bin/kittybox ''; serviceConfig = { DynamicUser = true; StateDirectory = "kittybox"; # Hardening NoNewPrivileges = true; CapabilityBoundingSet = ""; ProtectSystem = "strict"; ProtectHome = true; ProtectHostname = true; ProtectClock = true; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectKernelLogs = true; ProtectControlGroups = true; RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; RestrictNamespaces = true; LockPersonality = true; MemoryDenyWriteExecute = true; RestrictRealtime = true; RestrictSUIDSGID = true; RemoveIPC = true; ProtectProc = "invisible"; SystemCallArchitectures = "native"; SystemCallFilter = [ "@aio" "@basic-io" "@file-system" "@io-event" "@network-io" "@sync" "@system-service" "~@resources" "~@privileged" ]; PrivateDevices = true; DeviceAllow = [ "" ]; UMask = "0077"; IPAddressDeny = [ "link-local" "multicast" ]; }; }; }; }