Our Personal Data Server from scratch! tranquil.farm
oauth atproto pds rust postgresql objectstorage fun

feat: actual good config from Isabel

authored by lewis.moe and committed by nel.pet 320065ca d62bc1b5

+214 -689
+1
default.nix
··· 36 36 37 37 meta = { 38 38 license = lib.licenses.agpl3Plus; 39 + mainProgram = "tranquil-pds"; 39 40 }; 40 41 }
+7 -1
flake.nix
··· 30 30 default = pkgs.callPackage ./shell.nix {}; 31 31 }); 32 32 33 - nixosModules.default = import ./module.nix self; 33 + nixosModules = { 34 + default = self.nixosModules.tranquil-pds; 35 + tranquil-pds = { 36 + _file = "${self.outPath}/flake.nix#nixosModules.tranquil-pds"; 37 + imports = [(import ./module.nix self)]; 38 + }; 39 + }; 34 40 35 41 checks.x86_64-linux.integration = import ./test.nix { 36 42 pkgs = nixpkgs.legacyPackages.x86_64-linux;
+198 -678
module.nix
··· 1 - self: { 2 - config, 1 + self: 2 + { 3 3 lib, 4 4 pkgs, 5 + config, 5 6 ... 6 - }: let 7 + }: 8 + let 7 9 cfg = config.services.tranquil-pds; 8 10 9 - optionalStr = lib.types.nullOr lib.types.str; 10 - optionalInt = lib.types.nullOr lib.types.int; 11 - optionalPath = lib.types.nullOr lib.types.str; 12 - 13 - filterNulls = lib.filterAttrs (_: v: v != null); 14 - 15 - boolToStr = b: 16 - if b 17 - then "true" 18 - else "false"; 11 + inherit (lib) types mkOption; 19 12 20 - backendUrl = "http://127.0.0.1:${toString cfg.settings.server.port}"; 13 + backendUrl = "http://127.0.0.1:${toString cfg.settings.SERVER_PORT}"; 21 14 22 15 useACME = cfg.nginx.enableACME && cfg.nginx.useACMEHost == null; 23 16 hasSSL = useACME || cfg.nginx.useACMEHost != null; 24 - 25 - settingsToEnv = settings: let 26 - raw = { 27 - SERVER_HOST = settings.server.host; 28 - SERVER_PORT = settings.server.port; 29 - PDS_HOSTNAME = settings.server.pdsHostname; 30 - 31 - DATABASE_URL = settings.database.url; 32 - DATABASE_MAX_CONNECTIONS = settings.database.maxConnections; 33 - DATABASE_MIN_CONNECTIONS = settings.database.minConnections; 34 - DATABASE_ACQUIRE_TIMEOUT_SECS = settings.database.acquireTimeoutSecs; 35 - 36 - BLOB_STORAGE_BACKEND = settings.storage.blobBackend; 37 - BLOB_STORAGE_PATH = settings.storage.blobPath; 38 - S3_ENDPOINT = settings.storage.s3Endpoint; 39 - AWS_REGION = settings.storage.awsRegion; 40 - S3_BUCKET = settings.storage.s3Bucket; 41 - 42 - BACKUP_ENABLED = boolToStr settings.backup.enable; 43 - BACKUP_STORAGE_BACKEND = settings.backup.backend; 44 - BACKUP_STORAGE_PATH = settings.backup.path; 45 - BACKUP_S3_BUCKET = settings.backup.s3Bucket; 46 - BACKUP_RETENTION_COUNT = settings.backup.retentionCount; 47 - BACKUP_INTERVAL_SECS = settings.backup.intervalSecs; 48 - 49 - VALKEY_URL = settings.cache.valkeyUrl; 50 - 51 - TRANQUIL_PDS_ALLOW_INSECURE_SECRETS = boolToStr settings.security.allowInsecureSecrets; 52 - 53 - PLC_DIRECTORY_URL = settings.plc.directoryUrl; 54 - PLC_TIMEOUT_SECS = settings.plc.timeoutSecs; 55 - PLC_CONNECT_TIMEOUT_SECS = settings.plc.connectTimeoutSecs; 56 - PLC_ROTATION_KEY = settings.plc.rotationKey; 57 - 58 - DID_CACHE_TTL_SECS = settings.did.cacheTtlSecs; 59 - 60 - CRAWLERS = settings.relay.crawlers; 61 - 62 - FIREHOSE_BUFFER_SIZE = settings.firehose.bufferSize; 63 - FIREHOSE_MAX_LAG = settings.firehose.maxLag; 64 - 65 - NOTIFICATION_BATCH_SIZE = settings.notifications.batchSize; 66 - NOTIFICATION_POLL_INTERVAL_MS = settings.notifications.pollIntervalMs; 67 - MAIL_FROM_ADDRESS = settings.notifications.mailFromAddress; 68 - MAIL_FROM_NAME = settings.notifications.mailFromName; 69 - SENDMAIL_PATH = settings.notifications.sendmailPath; 70 - SIGNAL_CLI_PATH = settings.notifications.signalCliPath; 71 - SIGNAL_SENDER_NUMBER = settings.notifications.signalSenderNumber; 72 - 73 - MAX_BLOB_SIZE = settings.limits.maxBlobSize; 74 - 75 - ACCEPTING_REPO_IMPORTS = boolToStr settings.import.accepting; 76 - MAX_IMPORT_SIZE = settings.import.maxSize; 77 - MAX_IMPORT_BLOCKS = settings.import.maxBlocks; 78 - SKIP_IMPORT_VERIFICATION = boolToStr settings.import.skipVerification; 79 - 80 - INVITE_CODE_REQUIRED = boolToStr settings.registration.inviteCodeRequired; 81 - AVAILABLE_USER_DOMAINS = settings.registration.availableUserDomains; 82 - ENABLE_SELF_HOSTED_DID_WEB = boolToStr settings.registration.enableSelfHostedDidWeb; 83 - 84 - PRIVACY_POLICY_URL = settings.metadata.privacyPolicyUrl; 85 - TERMS_OF_SERVICE_URL = settings.metadata.termsOfServiceUrl; 86 - CONTACT_EMAIL = settings.metadata.contactEmail; 87 - 88 - DISABLE_RATE_LIMITING = boolToStr settings.rateLimiting.disable; 89 - 90 - SCHEDULED_DELETE_CHECK_INTERVAL_SECS = settings.scheduling.deleteCheckIntervalSecs; 91 - 92 - REPORT_SERVICE_URL = settings.moderation.reportServiceUrl; 93 - REPORT_SERVICE_DID = settings.moderation.reportServiceDid; 94 - 95 - PDS_AGE_ASSURANCE_OVERRIDE = boolToStr settings.misc.ageAssuranceOverride; 96 - ALLOW_HTTP_PROXY = boolToStr settings.misc.allowHttpProxy; 97 - 98 - SSO_GITHUB_ENABLED = boolToStr settings.sso.github.enable; 99 - SSO_GITHUB_CLIENT_ID = settings.sso.github.clientId; 100 - 101 - SSO_DISCORD_ENABLED = boolToStr settings.sso.discord.enable; 102 - SSO_DISCORD_CLIENT_ID = settings.sso.discord.clientId; 103 - 104 - SSO_GOOGLE_ENABLED = boolToStr settings.sso.google.enable; 105 - SSO_GOOGLE_CLIENT_ID = settings.sso.google.clientId; 106 - 107 - SSO_GITLAB_ENABLED = boolToStr settings.sso.gitlab.enable; 108 - SSO_GITLAB_CLIENT_ID = settings.sso.gitlab.clientId; 109 - SSO_GITLAB_ISSUER = settings.sso.gitlab.issuer; 17 + in 18 + { 19 + _class = "nixos"; 110 20 111 - SSO_OIDC_ENABLED = boolToStr settings.sso.oidc.enable; 112 - SSO_OIDC_CLIENT_ID = settings.sso.oidc.clientId; 113 - SSO_OIDC_ISSUER = settings.sso.oidc.issuer; 114 - SSO_OIDC_NAME = settings.sso.oidc.name; 115 - 116 - SSO_APPLE_ENABLED = boolToStr settings.sso.apple.enable; 117 - SSO_APPLE_CLIENT_ID = settings.sso.apple.clientId; 118 - SSO_APPLE_TEAM_ID = settings.sso.apple.teamId; 119 - SSO_APPLE_KEY_ID = settings.sso.apple.keyId; 120 - }; 121 - in 122 - lib.mapAttrs (_: v: toString v) (filterNulls raw); 123 - in { 124 21 options.services.tranquil-pds = { 125 22 enable = lib.mkEnableOption "tranquil-pds AT Protocol personal data server"; 126 23 127 - package = lib.mkOption { 128 - type = lib.types.package; 24 + package = mkOption { 25 + type = types.package; 129 26 default = self.packages.${pkgs.stdenv.hostPlatform.system}.tranquil-pds; 130 27 defaultText = lib.literalExpression "self.packages.\${pkgs.stdenv.hostPlatform.system}.tranquil-pds"; 131 28 description = "The tranquil-pds package to use"; 132 29 }; 133 30 134 - user = lib.mkOption { 135 - type = lib.types.str; 31 + user = mkOption { 32 + type = types.str; 136 33 default = "tranquil-pds"; 137 34 description = "User under which tranquil-pds runs"; 138 35 }; 139 36 140 - group = lib.mkOption { 141 - type = lib.types.str; 37 + group = mkOption { 38 + type = types.str; 142 39 default = "tranquil-pds"; 143 40 description = "Group under which tranquil-pds runs"; 144 41 }; 145 42 146 - dataDir = lib.mkOption { 147 - type = lib.types.str; 43 + dataDir = mkOption { 44 + type = types.str; 148 45 default = "/var/lib/tranquil-pds"; 149 46 description = "Directory for tranquil-pds data (blobs, backups)"; 150 47 }; 151 48 152 - secretsFile = lib.mkOption { 153 - type = lib.types.nullOr lib.types.path; 154 - default = null; 49 + environmentFiles = mkOption { 50 + type = types.listOf types.path; 51 + default = [ ]; 155 52 description = '' 156 - Path to a file containing secrets in EnvironmentFile format. 157 - Should contain: JWT_SECRET, DPOP_SECRET, MASTER_KEY 158 - May also contain: DISCORD_BOT_TOKEN, TELEGRAM_BOT_TOKEN, 159 - TELEGRAM_WEBHOOK_SECRET, SSO_*_CLIENT_SECRET, SSO_APPLE_PRIVATE_KEY, 160 - AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY 53 + File to load environment variables from. Loaded variables override 54 + values set in {option}`environment`. 55 + 56 + Use it to set values of `JWT_SECRET`, `DPOP_SECRET` and `MASTER_KEY`. 57 + 58 + Generate these with: 59 + ``` 60 + openssl rand --hex 32 61 + ``` 161 62 ''; 162 63 }; 163 64 164 - database.createLocally = lib.mkOption { 165 - type = lib.types.bool; 65 + database.createLocally = mkOption { 66 + type = types.bool; 166 67 default = false; 167 68 description = '' 168 69 Create the postgres database and user on the local host. 169 70 ''; 170 71 }; 171 72 172 - frontend.package = lib.mkOption { 173 - type = lib.types.nullOr lib.types.package; 73 + frontend.package = mkOption { 74 + type = types.nullOr types.package; 174 75 default = self.packages.${pkgs.stdenv.hostPlatform.system}.tranquil-frontend; 175 76 defaultText = lib.literalExpression "self.packages.\${pkgs.stdenv.hostPlatform.system}.tranquil-frontend"; 176 77 description = "Frontend package to serve via nginx (set null to disable frontend)"; ··· 179 80 nginx = { 180 81 enable = lib.mkEnableOption "nginx reverse proxy for tranquil-pds"; 181 82 182 - enableACME = lib.mkOption { 183 - type = lib.types.bool; 83 + enableACME = mkOption { 84 + type = types.bool; 184 85 default = true; 185 86 description = "Enable ACME for the pds domain"; 186 87 }; 187 88 188 - useACMEHost = lib.mkOption { 189 - type = lib.types.nullOr lib.types.str; 89 + useACMEHost = mkOption { 90 + type = types.nullOr types.str; 190 91 default = null; 191 92 description = '' 192 93 Use a pre-configured ACME certificate instead of generating one. 193 94 Set this to the cert name from security.acme.certs for wildcard setups. 95 + 194 96 REMEMBER: Handle subdomains (*.pds.example.com) require a wildcard cert via DNS-01. 195 97 ''; 196 98 }; 197 - 198 - openFirewall = lib.mkOption { 199 - type = lib.types.bool; 200 - default = true; 201 - description = "Open ports 80 and 443 in the firewall"; 202 - }; 203 99 }; 204 100 205 - settings = { 206 - server = { 207 - host = lib.mkOption { 208 - type = lib.types.str; 209 - default = "127.0.0.1"; 210 - description = "Address to bind the server to"; 211 - }; 212 - 213 - port = lib.mkOption { 214 - type = lib.types.port; 215 - default = 3000; 216 - description = "Port to bind the server to"; 217 - }; 218 - 219 - pdsHostname = lib.mkOption { 220 - type = lib.types.str; 221 - description = "Public-facing hostname of the PDS (used in DID documents, JWTs, etc)"; 222 - }; 223 - }; 224 - 225 - database = { 226 - url = lib.mkOption { 227 - type = lib.types.str; 228 - description = "PostgreSQL connection string"; 229 - }; 230 - 231 - maxConnections = lib.mkOption { 232 - type = lib.types.int; 233 - default = 100; 234 - description = "Maximum database connections"; 235 - }; 236 - 237 - minConnections = lib.mkOption { 238 - type = lib.types.int; 239 - default = 10; 240 - description = "Minimum database connections"; 241 - }; 242 - 243 - acquireTimeoutSecs = lib.mkOption { 244 - type = lib.types.int; 245 - default = 10; 246 - description = "Connection acquire timeout in seconds"; 247 - }; 248 - }; 249 - 250 - storage = { 251 - blobBackend = lib.mkOption { 252 - type = lib.types.enum ["filesystem" "s3"]; 253 - default = "filesystem"; 254 - description = "Backend for blob storage"; 255 - }; 256 - 257 - blobPath = lib.mkOption { 258 - type = lib.types.str; 259 - default = "${cfg.dataDir}/blobs"; 260 - defaultText = lib.literalExpression ''"''${cfg.dataDir}/blobs"''; 261 - description = "Path for filesystem blob storage"; 262 - }; 263 - 264 - s3Endpoint = lib.mkOption { 265 - type = optionalStr; 266 - default = null; 267 - description = "S3 endpoint URL (for object storage)"; 268 - }; 269 - 270 - awsRegion = lib.mkOption { 271 - type = optionalStr; 272 - default = null; 273 - description = "Region for objsto"; 274 - }; 275 - 276 - s3Bucket = lib.mkOption { 277 - type = optionalStr; 278 - default = null; 279 - description = "Bucket name for objsto"; 280 - }; 281 - }; 282 - 283 - backup = { 284 - enable = lib.mkEnableOption "automatic repo backups"; 285 - 286 - backend = lib.mkOption { 287 - type = lib.types.enum ["filesystem" "s3"]; 288 - default = "filesystem"; 289 - description = "Backend for backup storage"; 290 - }; 291 - 292 - path = lib.mkOption { 293 - type = lib.types.str; 294 - default = "${cfg.dataDir}/backups"; 295 - defaultText = lib.literalExpression ''"''${cfg.dataDir}/backups"''; 296 - description = "Path for filesystem backup storage"; 297 - }; 298 - 299 - s3Bucket = lib.mkOption { 300 - type = optionalStr; 301 - default = null; 302 - description = "Object storage bucket name for backups"; 303 - }; 304 - 305 - retentionCount = lib.mkOption { 306 - type = lib.types.int; 307 - default = 7; 308 - description = "Number of backups to retain"; 309 - }; 310 - 311 - intervalSecs = lib.mkOption { 312 - type = lib.types.int; 313 - default = 86400; 314 - description = "Backup interval in seconds"; 315 - }; 316 - }; 317 - 318 - cache = { 319 - valkeyUrl = lib.mkOption { 320 - type = optionalStr; 321 - default = null; 322 - description = "Valkey URL for caching"; 323 - }; 324 - }; 325 - 326 - security = { 327 - allowInsecureSecrets = lib.mkOption { 328 - type = lib.types.bool; 329 - default = false; 330 - description = "Allow default/weak secrets (development only, NEVER in production ofc)"; 331 - }; 332 - }; 333 - 334 - plc = { 335 - directoryUrl = lib.mkOption { 336 - type = lib.types.str; 337 - default = "https://plc.directory"; 338 - description = "PLC directory URL"; 339 - }; 340 - 341 - timeoutSecs = lib.mkOption { 342 - type = lib.types.int; 343 - default = 10; 344 - description = "PLC request timeout in seconds"; 345 - }; 346 - 347 - connectTimeoutSecs = lib.mkOption { 348 - type = lib.types.int; 349 - default = 5; 350 - description = "PLC connection timeout in seconds"; 351 - }; 352 - 353 - rotationKey = lib.mkOption { 354 - type = optionalStr; 355 - default = null; 356 - description = "Rotation key for PLC operations (did:key:xyz)"; 357 - }; 358 - }; 359 - 360 - did = { 361 - cacheTtlSecs = lib.mkOption { 362 - type = lib.types.int; 363 - default = 300; 364 - description = "DID document cache TTL in seconds"; 365 - }; 366 - }; 367 - 368 - relay = { 369 - crawlers = lib.mkOption { 370 - type = optionalStr; 371 - default = null; 372 - description = "Comma-separated list of relay URLs to notify via requestCrawl"; 373 - }; 374 - }; 375 - 376 - firehose = { 377 - bufferSize = lib.mkOption { 378 - type = lib.types.int; 379 - default = 10000; 380 - description = "Firehose broadcast channel buffer size"; 381 - }; 382 - 383 - maxLag = lib.mkOption { 384 - type = optionalInt; 385 - default = null; 386 - description = "Disconnect slow consumers after this many events of lag"; 387 - }; 388 - }; 389 - 390 - notifications = { 391 - batchSize = lib.mkOption { 392 - type = lib.types.int; 393 - default = 100; 394 - description = "Notification queue batch size"; 395 - }; 396 - 397 - pollIntervalMs = lib.mkOption { 398 - type = lib.types.int; 399 - default = 1000; 400 - description = "Notification queue poll interval in ms"; 401 - }; 402 - 403 - mailFromAddress = lib.mkOption { 404 - type = optionalStr; 405 - default = null; 406 - description = "Email from address for notifications"; 407 - }; 408 - 409 - mailFromName = lib.mkOption { 410 - type = optionalStr; 411 - default = null; 412 - description = "Email from name for notifications"; 413 - }; 414 - 415 - sendmailPath = lib.mkOption { 416 - type = optionalPath; 417 - default = null; 418 - description = "Path to sendmail binary"; 419 - }; 420 - 421 - signalCliPath = lib.mkOption { 422 - type = optionalPath; 423 - default = null; 424 - description = "Path to signal-cli binary"; 425 - }; 426 - 427 - signalSenderNumber = lib.mkOption { 428 - type = optionalStr; 429 - default = null; 430 - description = "Signal sender phone number"; 431 - }; 432 - }; 433 - 434 - limits = { 435 - maxBlobSize = lib.mkOption { 436 - type = lib.types.int; 437 - default = 10737418240; 438 - description = "Maximum blob size in bytes"; 439 - }; 440 - }; 441 - 442 - import = { 443 - accepting = lib.mkOption { 444 - type = lib.types.bool; 445 - default = true; 446 - description = "Accept repository imports"; 447 - }; 448 - 449 - maxSize = lib.mkOption { 450 - type = lib.types.int; 451 - default = 1073741824; 452 - description = "Maximum import size in bytes"; 453 - }; 454 - 455 - maxBlocks = lib.mkOption { 456 - type = lib.types.int; 457 - default = 500000; 458 - description = "Maximum blocks per import"; 459 - }; 460 - 461 - skipVerification = lib.mkOption { 462 - type = lib.types.bool; 463 - default = false; 464 - description = "Skip verification during import (testing only)"; 465 - }; 466 - }; 467 - 468 - registration = { 469 - inviteCodeRequired = lib.mkOption { 470 - type = lib.types.bool; 471 - default = false; 472 - description = "Require invite codes for registration"; 473 - }; 474 - 475 - availableUserDomains = lib.mkOption { 476 - type = optionalStr; 477 - default = null; 478 - description = "Comma-separated list of available user domains"; 479 - }; 480 - 481 - enableSelfHostedDidWeb = lib.mkOption { 482 - type = lib.types.bool; 483 - default = true; 484 - description = "Enable self-hosted did:web identities"; 485 - }; 486 - }; 487 - 488 - metadata = { 489 - privacyPolicyUrl = lib.mkOption { 490 - type = optionalStr; 491 - default = null; 492 - description = "Privacy policy URL"; 493 - }; 494 - 495 - termsOfServiceUrl = lib.mkOption { 496 - type = optionalStr; 497 - default = null; 498 - description = "Terms of service URL"; 499 - }; 500 - 501 - contactEmail = lib.mkOption { 502 - type = optionalStr; 503 - default = null; 504 - description = "Contact email address"; 505 - }; 506 - }; 507 - 508 - rateLimiting = { 509 - disable = lib.mkOption { 510 - type = lib.types.bool; 511 - default = false; 512 - description = "Disable rate limiting (testing only, NEVER in production you naughty!)"; 513 - }; 514 - }; 515 - 516 - scheduling = { 517 - deleteCheckIntervalSecs = lib.mkOption { 518 - type = lib.types.int; 519 - default = 3600; 520 - description = "Scheduled deletion check interval in seconds"; 521 - }; 522 - }; 101 + settings = mkOption { 102 + type = types.submodule { 103 + freeformType = types.attrsOf ( 104 + types.nullOr ( 105 + types.oneOf [ 106 + types.str 107 + types.path 108 + types.int 109 + ] 110 + ) 111 + ); 523 112 524 - moderation = { 525 - reportServiceUrl = lib.mkOption { 526 - type = optionalStr; 527 - default = null; 528 - description = "Moderation report service URL (like ozone)"; 529 - }; 530 - 531 - reportServiceDid = lib.mkOption { 532 - type = optionalStr; 533 - default = null; 534 - description = "Moderation report service DID"; 535 - }; 536 - }; 537 - 538 - misc = { 539 - ageAssuranceOverride = lib.mkOption { 540 - type = lib.types.bool; 541 - default = false; 542 - description = "Override age assurance checks"; 543 - }; 544 - 545 - allowHttpProxy = lib.mkOption { 546 - type = lib.types.bool; 547 - default = false; 548 - description = "Allow HTTP for proxy requests (development only)"; 549 - }; 550 - }; 551 - 552 - sso = { 553 - github = { 554 - enable = lib.mkOption { 555 - type = lib.types.bool; 556 - default = false; 557 - description = "Enable GitHub SSO"; 113 + options = { 114 + SERVER_HOST = mkOption { 115 + type = types.str; 116 + default = "127.0.0.1"; 117 + description = "Host for tranquil-pds to listen on"; 558 118 }; 559 119 560 - clientId = lib.mkOption { 561 - type = optionalStr; 562 - default = null; 563 - description = "GitHub OAuth client ID"; 564 - }; 565 - }; 566 - 567 - discord = { 568 - enable = lib.mkOption { 569 - type = lib.types.bool; 570 - default = false; 571 - description = "Enable Discord SSO"; 120 + SERVER_PORT = mkOption { 121 + type = types.int; 122 + default = 3000; 123 + description = "Port for tranquil-pds to listen on"; 572 124 }; 573 125 574 - clientId = lib.mkOption { 575 - type = optionalStr; 126 + PDS_HOSTNAME = mkOption { 127 + type = types.nullOr types.str; 576 128 default = null; 577 - description = "Discord OAuth client ID"; 129 + example = "pds.example.com"; 130 + description = "The public-facing hostname of the PDS"; 578 131 }; 579 - }; 580 132 581 - google = { 582 - enable = lib.mkOption { 583 - type = lib.types.bool; 584 - default = false; 585 - description = "Enable Google SSO"; 133 + BLOB_STORAGE_PATH = mkOption { 134 + type = types.path; 135 + default = "/var/lib/tranquil-pds/blobs"; 136 + description = "Directory for storing blobs"; 586 137 }; 587 138 588 - clientId = lib.mkOption { 589 - type = optionalStr; 590 - default = null; 591 - description = "Google OAuth client ID"; 139 + BACKUP_STORAGE_PATH = mkOption { 140 + type = types.path; 141 + default = "/var/lib/tranquil-pds/backups"; 142 + description = "Directory for storing backups"; 592 143 }; 593 - }; 594 144 595 - gitlab = { 596 - enable = lib.mkOption { 597 - type = lib.types.bool; 598 - default = false; 599 - description = "Enable GitLab SSO"; 600 - }; 601 - 602 - clientId = lib.mkOption { 603 - type = optionalStr; 145 + MAIL_FROM_ADDRESS = mkOption { 146 + type = types.nullOr types.str; 604 147 default = null; 605 - description = "GitLab OAuth client ID"; 148 + description = "Email address to use in the From header when sending emails."; 606 149 }; 607 150 608 - issuer = lib.mkOption { 609 - type = optionalStr; 151 + SENDMAIL_PATH = mkOption { 152 + type = types.nullOr types.path; 610 153 default = null; 611 - description = "GitLab issuer URL"; 154 + description = "Path to the sendmail executable to use for sending emails."; 612 155 }; 613 - }; 614 156 615 - oidc = { 616 - enable = lib.mkOption { 617 - type = lib.types.bool; 618 - default = false; 619 - description = "Enable generic OIDC SSO"; 620 - }; 621 - 622 - clientId = lib.mkOption { 623 - type = optionalStr; 157 + SIGNAL_SENDER_NUMBER = mkOption { 158 + type = types.nullOr types.str; 624 159 default = null; 625 - description = "OIDC client ID"; 160 + description = "Phone number (in international format) to use for sending Signal notifications."; 626 161 }; 627 162 628 - issuer = lib.mkOption { 629 - type = optionalStr; 163 + SIGNAL_CLI_PATH = mkOption { 164 + type = types.nullOr types.path; 630 165 default = null; 631 - description = "OIDC issuer URL"; 166 + description = "Path to the signal-cli executable to use for sending Signal notifications."; 632 167 }; 633 168 634 - name = lib.mkOption { 635 - type = optionalStr; 636 - default = null; 637 - description = "OIDC provider display name"; 169 + MAX_BLOB_SIZE = mkOption { 170 + type = types.int; 171 + default = 10737418240; # 10 GiB 172 + description = "Maximum allowed blob size in bytes."; 638 173 }; 639 174 }; 175 + }; 640 176 641 - apple = { 642 - enable = lib.mkOption { 643 - type = lib.types.bool; 644 - default = false; 645 - description = "Enable Apple Sign-in"; 646 - }; 647 - 648 - clientId = lib.mkOption { 649 - type = optionalStr; 650 - default = null; 651 - description = "Apple Services ID"; 652 - }; 653 - 654 - teamId = lib.mkOption { 655 - type = optionalStr; 656 - default = null; 657 - description = "Apple Team ID"; 658 - }; 177 + description = '' 178 + Environment variables to set for the service. Secrets should be 179 + specified using {option}`environmentFile`. 659 180 660 - keyId = lib.mkOption { 661 - type = optionalStr; 662 - default = null; 663 - description = "Apple Key ID"; 664 - }; 665 - }; 666 - }; 181 + Refer to <https://tangled.org/tranquil.farm/tranquil-pds/blob/main/.env.example> 182 + available environment variables. 183 + ''; 667 184 }; 668 185 }; 669 186 670 - config = lib.mkIf cfg.enable (lib.mkMerge [ 671 - (lib.mkIf (cfg.settings.notifications.mailFromAddress != null) { 672 - services.tranquil-pds.settings.notifications.sendmailPath = 673 - lib.mkDefault "/run/wrappers/bin/sendmail"; 674 - }) 187 + config = lib.mkIf cfg.enable ( 188 + lib.mkMerge [ 189 + (lib.mkIf cfg.database.createLocally { 190 + services.postgresql = { 191 + enable = true; 192 + ensureDatabases = [ cfg.user ]; 193 + ensureUsers = [ 194 + { 195 + name = cfg.user; 196 + ensureDBOwnership = true; 197 + } 198 + ]; 199 + }; 675 200 676 - (lib.mkIf (cfg.settings.notifications.signalSenderNumber != null) { 677 - services.tranquil-pds.settings.notifications.signalCliPath = 678 - lib.mkDefault (lib.getExe pkgs.signal-cli); 679 - }) 201 + services.tranquil-pds.settings.DATABASE_URL = lib.mkDefault "postgresql:///${cfg.user}?host=/run/postgresql"; 680 202 681 - (lib.mkIf cfg.database.createLocally { 682 - services.postgresql = { 683 - enable = true; 684 - ensureDatabases = [cfg.user]; 685 - ensureUsers = [ 686 - { 687 - name = cfg.user; 688 - ensureDBOwnership = true; 689 - } 690 - ]; 691 - }; 692 - 693 - services.tranquil-pds.settings.database.url = 694 - lib.mkDefault "postgresql:///${cfg.user}?host=/run/postgresql"; 695 - 696 - systemd.services.tranquil-pds = { 697 - requires = ["postgresql.service"]; 698 - after = ["postgresql.service"]; 699 - }; 700 - }) 203 + systemd.services.tranquil-pds = { 204 + requires = [ "postgresql.service" ]; 205 + after = [ "postgresql.service" ]; 206 + }; 207 + }) 701 208 702 - (lib.mkIf cfg.nginx.enable (lib.mkMerge [ 703 - { 209 + (lib.mkIf cfg.nginx.enable { 704 210 services.nginx = { 705 211 enable = true; 706 - recommendedProxySettings = lib.mkDefault true; 707 - recommendedTlsSettings = lib.mkDefault true; 708 - recommendedGzipSettings = lib.mkDefault true; 709 - recommendedOptimisation = lib.mkDefault true; 710 212 711 - virtualHosts.${cfg.settings.server.pdsHostname} = { 712 - serverAliases = ["*.${cfg.settings.server.pdsHostname}"]; 213 + virtualHosts.${cfg.settings.PDS_HOSTNAME} = { 214 + serverAliases = [ "*.${cfg.settings.PDS_HOSTNAME}" ]; 713 215 forceSSL = hasSSL; 714 216 enableACME = useACME; 715 217 useACMEHost = cfg.nginx.useACMEHost; 716 218 717 - root = lib.mkIf (cfg.frontend.package != null) "${cfg.frontend.package}"; 219 + root = lib.mkIf (cfg.frontend.package != null) cfg.frontend.package; 718 220 719 - extraConfig = "client_max_body_size ${toString cfg.settings.limits.maxBlobSize};"; 221 + extraConfig = "client_max_body_size ${toString cfg.settings.MAX_BLOB_SIZE};"; 720 222 721 - locations = let 722 - proxyLocations = { 223 + locations = lib.mkMerge [ 224 + { 723 225 "/xrpc/" = { 724 226 proxyPass = backendUrl; 725 227 proxyWebsockets = true; ··· 766 268 "~ ^/u/[^/]+/did\\.json$" = { 767 269 proxyPass = backendUrl; 768 270 }; 769 - }; 271 + } 770 272 771 - frontendLocations = lib.optionalAttrs (cfg.frontend.package != null) { 273 + (lib.optionalAttrs (cfg.frontend.package != null) { 772 274 "= /oauth/client-metadata.json" = { 773 275 root = "${cfg.frontend.package}"; 774 276 extraConfig = '' ··· 780 282 }; 781 283 782 284 "/assets/" = { 285 + # TODO: use `add_header_inherit` when nixpkgs updates to nginx 1.29.3+ 783 286 extraConfig = '' 784 287 expires 1y; 785 288 add_header Cache-Control "public, immutable"; ··· 799 302 tryFiles = "$uri $uri/ /index.html"; 800 303 priority = 9999; 801 304 }; 802 - }; 803 - in 804 - proxyLocations // frontendLocations; 305 + }) 306 + ]; 805 307 }; 806 308 }; 807 - } 309 + }) 808 310 809 - (lib.mkIf cfg.nginx.openFirewall { 810 - networking.firewall.allowedTCPPorts = [80 443]; 811 - }) 812 - ])) 311 + { 312 + services.tranquil-pds.settings = { 313 + SENDMAIL_PATH = lib.mkDefault ( 314 + if cfg.settings.MAIL_FROM_ADDRESS != null then (lib.getExe pkgs.system-sendmail) else null 315 + ); 813 316 814 - { 815 - users.users.${cfg.user} = { 816 - isSystemUser = true; 817 - inherit (cfg) group; 818 - home = cfg.dataDir; 819 - }; 317 + SIGNAL_CLI_PATH = lib.mkDefault ( 318 + if cfg.settings.SIGNAL_SENDER_NUMBER != null then (lib.getExe pkgs.signal-cli) else null 319 + ); 320 + }; 820 321 821 - users.groups.${cfg.group} = {}; 322 + users.users.${cfg.user} = { 323 + isSystemUser = true; 324 + inherit (cfg) group; 325 + home = cfg.dataDir; 326 + }; 822 327 823 - systemd.tmpfiles.rules = [ 824 - "d ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} -" 825 - "d ${cfg.settings.storage.blobPath} 0750 ${cfg.user} ${cfg.group} -" 826 - "d ${cfg.settings.backup.path} 0750 ${cfg.user} ${cfg.group} -" 827 - ]; 328 + users.groups.${cfg.group} = { }; 828 329 829 - systemd.services.tranquil-pds = { 830 - description = "Tranquil PDS - AT Protocol Personal Data Server"; 831 - after = ["network.target" "postgresql.service"]; 832 - wants = ["network.target"]; 833 - wantedBy = ["multi-user.target"]; 330 + systemd.tmpfiles.settings."tranquil-pds" = 331 + lib.genAttrs 332 + [ 333 + cfg.dataDir 334 + cfg.settings.BLOB_STORAGE_PATH 335 + cfg.settings.BACKUP_STORAGE_PATH 336 + ] 337 + (_: { 338 + d = { 339 + mode = "0750"; 340 + inherit (cfg) user group; 341 + }; 342 + }); 834 343 835 - environment = settingsToEnv cfg.settings; 344 + systemd.services.tranquil-pds = { 345 + description = "Tranquil PDS - AT Protocol Personal Data Server"; 346 + after = [ "network-online.target" ]; 347 + wants = [ "network-online.target" ]; 348 + wantedBy = [ "multi-user.target" ]; 836 349 837 - serviceConfig = { 838 - Type = "exec"; 839 - User = cfg.user; 840 - Group = cfg.group; 841 - ExecStart = "${cfg.package}/bin/tranquil-pds"; 842 - Restart = "on-failure"; 843 - RestartSec = 5; 350 + serviceConfig = { 351 + User = cfg.user; 352 + Group = cfg.group; 353 + ExecStart = lib.getExe cfg.package; 354 + Restart = "on-failure"; 355 + RestartSec = 5; 844 356 845 - WorkingDirectory = cfg.dataDir; 846 - StateDirectory = "tranquil-pds"; 357 + WorkingDirectory = cfg.dataDir; 358 + StateDirectory = "tranquil-pds"; 847 359 848 - EnvironmentFile = lib.mkIf (cfg.secretsFile != null) cfg.secretsFile; 360 + EnvironmentFile = cfg.environmentFiles; 361 + Environment = lib.mapAttrsToList (k: v: "${k}=${if builtins.isInt v then toString v else v}") ( 362 + lib.filterAttrs (_: v: v != null) cfg.settings 363 + ); 849 364 850 - NoNewPrivileges = true; 851 - ProtectSystem = "strict"; 852 - ProtectHome = true; 853 - PrivateTmp = true; 854 - PrivateDevices = true; 855 - ProtectKernelTunables = true; 856 - ProtectKernelModules = true; 857 - ProtectControlGroups = true; 858 - RestrictAddressFamilies = ["AF_INET" "AF_INET6" "AF_UNIX"]; 859 - RestrictNamespaces = true; 860 - LockPersonality = true; 861 - MemoryDenyWriteExecute = true; 862 - RestrictRealtime = true; 863 - RestrictSUIDSGID = true; 864 - RemoveIPC = true; 365 + NoNewPrivileges = true; 366 + ProtectSystem = "strict"; 367 + ProtectHome = true; 368 + PrivateTmp = true; 369 + PrivateDevices = true; 370 + ProtectKernelTunables = true; 371 + ProtectKernelModules = true; 372 + ProtectControlGroups = true; 373 + RestrictAddressFamilies = [ 374 + "AF_INET" 375 + "AF_INET6" 376 + "AF_UNIX" 377 + ]; 378 + RestrictNamespaces = true; 379 + LockPersonality = true; 380 + MemoryDenyWriteExecute = true; 381 + RestrictRealtime = true; 382 + RestrictSUIDSGID = true; 383 + RemoveIPC = true; 865 384 866 - ReadWritePaths = [ 867 - cfg.settings.storage.blobPath 868 - cfg.settings.backup.path 869 - ]; 385 + ReadWritePaths = [ 386 + cfg.settings.BLOB_STORAGE_PATH 387 + cfg.settings.BACKUP_STORAGE_PATH 388 + ]; 389 + }; 870 390 }; 871 - }; 872 - } 873 - ]); 391 + } 392 + ] 393 + ); 874 394 }
+8 -10
test.nix
··· 16 16 services.tranquil-pds = { 17 17 enable = true; 18 18 database.createLocally = true; 19 - secretsFile = pkgs.writeText "tranquil-secrets" '' 20 - JWT_SECRET=test-jwt-secret-must-be-32-chars-long 21 - DPOP_SECRET=test-dpop-secret-must-be-32-chars-long 22 - MASTER_KEY=test-master-key-must-be-32-chars-long 23 - ''; 24 19 25 20 nginx = { 26 21 enable = true; ··· 28 23 }; 29 24 30 25 settings = { 31 - server.pdsHostname = "test.local"; 32 - server.host = "0.0.0.0"; 26 + PDS_HOSTNAME = "test.local"; 27 + SERVER_HOST = "0.0.0.0"; 33 28 34 - storage.blobBackend = "filesystem"; 35 - rateLimiting.disable = true; 36 - security.allowInsecureSecrets = true; 29 + DISABLE_RATE_LIMITING = 1; 30 + TRANQUIL_PDS_ALLOW_INSECURE_SECRETS = 1; 31 + 32 + JWT_SECRET="test-jwt-secret-must-be-32-chars-long"; 33 + DPOP_SECRET="test-dpop-secret-must-be-32-chars-long"; 34 + MASTER_KEY="test-master-key-must-be-32-chars-long"; 37 35 }; 38 36 }; 39 37 };