depot/ops/nixos/lib/bsky-pds.nix

249 lines
8.2 KiB
Nix

{ depot, config, pkgs, lib, ... }:
let
cfg = config.my.services.bsky-pds;
marshalValue = key: value: if builtins.isString value then value
else if builtins.isInt value then toString value
else if builtins.isBool value then (if value then "true" else "false")
else throw "unknown value type for key ${key}";
cfgFile = pkgs.writeTextFile {
name = "bsky-pds.env";
text = lib.concatStringsSep "\n" (lib.mapAttrsToList (attrName: attrVal: lib.optionalString (attrVal != null) ''
${lib.toUpper attrName}=${marshalValue attrName attrVal}
'') cfg.settings);
};
inSecretsDir = v: builtins.isString v && (builtins.match "/var/lib/bsky-pds/secrets/.*" (toString v)) != null;
testAndGenerate = how: path: ''
test -f "${path}" || {
${how} >> "${path}"
}
'';
testAndGenerateHex = testAndGenerate "${pkgs.openssl}/bin/openssl rand --hex 16";
testAndGenerateK256 = testAndGenerate "${pkgs.openssl}/bin/openssl ecparam --name secp256k1 --genkey --noout --outform DER";
in
{
options.my.services.bsky-pds = {
enable = lib.mkEnableOption "Bluesky Personal Data Server";
package = lib.mkOption {
type = lib.types.package;
default = depot.nix.pkgs.bsky-pds;
description = ''
Package containing the PDS code.
'';
};
configureCaddy = (lib.mkEnableOption "configure Caddy to serve the PDS") // {
default = true;
};
settings = lib.mkOption {
type = with lib.types; submodule {
freeformType = attrsOf (nullOr (oneOf [
str
bool
int
]));
options.log_enabled = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether or not to output any log messages.
'';
};
options.port = lib.mkOption {
type = lib.types.port;
default = 3000;
description = ''
Port number that the PDS will listen on.
'';
};
options.pds_hostname = lib.mkOption {
type = lib.types.str;
description = ''
Hostname under which the PDS will run.
'';
};
options.pds_admin_email = lib.mkOption {
type = lib.types.str;
description = ''
Email address of the PDS operator.
'';
};
options.pds_data_directory = lib.mkOption {
type = lib.types.path;
default = "/var/lib/bsky-pds/data";
description = ''
Path to data directory.
'';
};
options.pds_blobstore_disk_location = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = "/var/lib/bsky-pds/blobs";
description = ''
Path to location for blobstore storage, if using on-disk storage.
'';
};
options.pds_blob_upload_limit = lib.mkOption {
type = lib.types.int;
default = 52428800;
description = ''
Maximum allowable blob size for upload.
'';
};
options.pds_did_plc_url = lib.mkOption {
type = lib.types.str;
default = "https://plc.directory";
description = ''
URL of the PLC directory.
'';
};
options.pds_bsky_app_view_url = lib.mkOption {
type = lib.types.str;
default = "https://api.bsky.app";
description = ''
URL of the Bluesky AppView.
'';
};
options.pds_bsky_app_view_did = lib.mkOption {
type = lib.types.str;
default = "did:web:api.bsky.app";
description = ''
DID of the Bluesky AppView.
'';
};
options.pds_report_service_url = lib.mkOption {
type = lib.types.str;
default = "https://mod.bsky.app";
description = ''
URL of the Bluesky moderation service.
'';
};
options.pds_report_service_did = lib.mkOption {
type = lib.types.str;
default = "did:plc:ar7c4by46qjdydhdevvrndac";
description = ''
DID of the Bluesky moderation service.
'';
};
options.crawlers = lib.mkOption {
type = lib.types.commas;
default = "https://bsky.network";
description = ''
URLs of hosts to notify of new data.
'';
};
};
default = {};
description = ''
Configuration for Bluesky PDS.
'';
};
generateSecrets = lib.mkEnableOption "automatically generate required PDS secrets";
secrets = lib.mkOption {
type = with lib.types; submodule {
freeformType = attrsOf (nullOr path);
options.pds_jwt_secret = lib.mkOption {
type = nullOr path;
default = null;
description = ''
Path to a file containing a 16 character hex secret used for JWT secrets.
'';
};
options.pds_admin_password = lib.mkOption {
type = nullOr path;
default = null;
description = ''
Path to a file containing the PDS admin password.
'';
};
options.pds_plc_rotation_key_k256_private_key = lib.mkOption {
type = nullOr path;
default = null;
description = ''
Path to a file containing a secp256k1 private key in DER format.
'';
};
};
default = {};
description = ''
Path to files containing secrets for Bluesky PDS.
'';
};
};
config = lib.mkMerge [(lib.mkIf cfg.generateSecrets {
my.services.bsky-pds.secrets = {
pds_jwt_secret = lib.mkDefault "/var/lib/bsky-pds/secrets/pds_jwt_secret";
pds_admin_password = lib.mkDefault "/var/lib/bsky-pds/secrets/pds_admin_password";
pds_plc_rotation_key_k256_private_key = lib.mkDefault "/var/lib/bsky-pds/secrets/pds_plc_rotation_key_k256_private_key";
};
}) (lib.mkIf cfg.enable {
systemd.services.bsky-pds = {
wantedBy = [ "multi-user.target" ];
unitConfig = {
Description = "Bluesky PDS Service";
Documentation = "https://github.com/bluesky-social/pds";
};
script = ''
mkdir -p /var/lib/bsky-pds "$PDS_DATA_DIRECTORY"
if [[ ! -z "$PDS_BLOBSTORE_DISK_LOCATION" ]]; then
mkdir -p "$PDS_BLOBSTORE_DISK_LOCATION"
fi
old_umask=$(umask)
umask 077
mkdir -p /var/lib/bsky-pds/secrets
${lib.optionalString (inSecretsDir cfg.secrets.pds_jwt_secret) (testAndGenerateHex cfg.secrets.pds_jwt_secret)}
${lib.optionalString (inSecretsDir cfg.secrets.pds_admin_password) (testAndGenerateHex cfg.secrets.pds_admin_password)}
${lib.optionalString (inSecretsDir cfg.secrets.pds_plc_rotation_key_k256_private_key) (testAndGenerateK256 cfg.secrets.pds_plc_rotation_key_k256_private_key)}
umask "$old_umask"
${lib.concatStringsSep "\n" (lib.mapAttrsToList (k: v: lib.optionalString (v != null) ''
export ${lib.toUpper k}="$(cat "${v}")"
'') cfg.secrets)}
${lib.optionalString (cfg.secrets.pds_plc_rotation_key_k256_private_key != null) ''
if [[ -z "$PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX ]]; then
export PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX="$(cat "${cfg.secrets.pds_plc_rotation_key_k256_private_key}" | ${pkgs.coreutils}/bin/tail --bytes=+8 | ${pkgs.coreutils}/bin/head --bytes=32 | ${pkgs.xxd}/bin/xxd --plain --cols 32)"
fi
''}
exec ${cfg.package}/bin/bsky-pds
'';
serviceConfig = {
User = "bsky-pds";
DynamicUser = true;
StateDirectory = "bsky-pds";
EnvironmentFile = cfgFile;
};
};
}) (lib.mkIf (cfg.enable && cfg.configureCaddy) {
services.caddy = {
enable = lib.mkDefault true;
globalConfig = ''
email ${cfg.settings.pds_admin_email}
on_demand_tls {
ask http://localhost:${toString cfg.settings.port}/tls-check
}
'';
virtualHosts."${cfg.settings.pds_hostname}" = {
serverAliases = [ "*.${cfg.settings.pds_hostname}" ];
extraConfig = ''
tls {
on_demand
}
reverse_proxy http://localhost:${toString cfg.settings.port}
'';
};
};
})];
}