blob: be24ec050c4572badf6cc561cc98554ec62fd2e3 (
plain) (
tree)
|
|
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 = "<kittybox package from the upstream flake>";
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" ];
};
};
};
}
|