diff --git a/ops/nixos/lib/bsky-pds.nix b/ops/nixos/lib/bsky-pds.nix new file mode 100644 index 0000000000..deb8ea314a --- /dev/null +++ b/ops/nixos/lib/bsky-pds.nix @@ -0,0 +1,249 @@ +{ 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} + ''; + }; + }; + })]; +} diff --git a/ops/nixos/rexxar/bsky-pds.nix b/ops/nixos/rexxar/bsky-pds.nix new file mode 100644 index 0000000000..8f339a18ef --- /dev/null +++ b/ops/nixos/rexxar/bsky-pds.nix @@ -0,0 +1,42 @@ +{ config, lib, ... }: + +let + bskySecretsFromVault = [ + "PDS_ADMIN_PASSWORD" + "PDS_BLOBSTORE_S3_ACCESS_KEY_ID" + "PDS_BLOBSTORE_S3_SECRET_ACCESS_KEY" + "PDS_JWT_SECRET" + "PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX" + ]; +in { + imports = [ ../lib/bsky-pds.nix ]; + + my.services.bsky-pds = { + enable = true; + settings = { + pds_hostname = "pds.lukegb.com"; + pds_admin_email = "bskypds@lukegb.com"; + pds_blobstore_disk_location = null; + pds_blobstore_s3_bucket = "bsky-pds"; + pds_blobstore_s3_region = "anywhere"; + pds_blobstore_s3_endpoint = "https://objdump.zxcvbnm.ninja"; + pds_blobstore_s3_force_path_style = false; + pds_blobstore_s3_upload_timeout_ms = 10000; + }; + generateSecrets = false; + secrets = lib.listToAttrs (map (k: lib.nameValuePair (lib.toLower k) config.my.vault.secrets."bsky_${lib.toLower k}".path) bskySecretsFromVault); + }; + + my.vault.secrets = let + bskySecret = key: { + group = "bsky-pds"; + template = '' + {{- with secret "kv/apps/bsky-pds" -}} + {{- .Data.data.${key} -}} + {{- end -}} + ''; + }; + in lib.listToAttrs (map (k: lib.nameValuePair "bsky_${lib.toLower k}" (bskySecret k)) bskySecretsFromVault); + users.groups.bsky-pds = {}; + users.users.bsky-pds = { isSystemUser = true; group = "bsky-pds"; }; +} diff --git a/ops/nixos/rexxar/default.nix b/ops/nixos/rexxar/default.nix index 6541597d40..c406c52ac5 100644 --- a/ops/nixos/rexxar/default.nix +++ b/ops/nixos/rexxar/default.nix @@ -14,6 +14,7 @@ ../lib/emfminiserv.nix ../lib/seaweedfs.nix ../lib/fup.nix + ./bsky-pds.nix ]; # Otherwise _this_ machine won't enumerate things properly. diff --git a/ops/vault/cfg/config.nix b/ops/vault/cfg/config.nix index dcb0b987a2..f5213f859c 100644 --- a/ops/vault/cfg/config.nix +++ b/ops/vault/cfg/config.nix @@ -75,6 +75,7 @@ my.apps.hacky-vouchproxy = {}; my.apps.hackyplayer = {}; my.apps.emfminiserv = {}; + my.apps.bsky-pds = {}; my.servers.etheroute-lon01.apps = [ "pomerium" ]; my.servers.howl.apps = [ "nixbuild" ]; @@ -90,5 +91,5 @@ my.servers.bvm-heptapod.apps = [ "gitlab-runner" ]; my.servers.bvm-nixosmgmt.apps = [ "plex-pass" ]; my.servers.bvm-netbox.apps = [ "netbox" ]; - my.servers.rexxar.apps = [ "deluge" "gitlab-runner" "nixbuild" "hacky-vouchproxy" "hackyplayer" "emfminiserv" "fup" ]; + my.servers.rexxar.apps = [ "deluge" "gitlab-runner" "nixbuild" "hacky-vouchproxy" "hackyplayer" "emfminiserv" "fup" "bsky-pds" ]; }