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

feat: nix module #16

merged opened by lewis.moe targeting main from feat/nix-module

Added a module and a test file to test it out! Tested locally on qemu

Labels

None yet.

assignee

None yet.

Participants 2
Referenced by
AT URI
at://did:plc:3fwecdnvtcscjnrx2p4n7alz/sh.tangled.repo.pull/3me4wjqzoiv22
+214 -689
Interdiff #2 โ†’ #3
docs/install-debian.md

This file has not been changed.

+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; 11 + inherit (lib) types mkOption; 12 12 13 - filterNulls = lib.filterAttrs (_: v: v != null); 13 + backendUrl = "http://127.0.0.1:${toString cfg.settings.SERVER_PORT}"; 14 14 15 - boolToStr = b: 16 - if b 17 - then "true" 18 - else "false"; 19 - 20 - backendUrl = "http://127.0.0.1:${toString cfg.settings.server.port}"; 21 - 22 15 useACME = cfg.nginx.enableACME && cfg.nginx.useACMEHost == null; 23 16 hasSSL = useACME || cfg.nginx.useACMEHost != null; 17 + in 18 + { 19 + _class = "nixos"; 24 20 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; 110 - 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 - }; 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 + ); 212 112 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 - }; 523 - 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"; 120 + SERVER_PORT = mkOption { 121 + type = types.int; 122 + default = 3000; 123 + description = "Port for tranquil-pds to listen on"; 564 124 }; 565 - }; 566 125 567 - discord = { 568 - enable = lib.mkOption { 569 - type = lib.types.bool; 570 - default = false; 571 - description = "Enable Discord SSO"; 572 - }; 573 - 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 - }; 177 + description = '' 178 + Environment variables to set for the service. Secrets should be 179 + specified using {option}`environmentFile`. 647 180 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 - }; 659 - 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 - }; 203 + systemd.services.tranquil-pds = { 204 + requires = [ "postgresql.service" ]; 205 + after = [ "postgresql.service" ]; 206 + }; 207 + }) 692 208 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 - }) 701 - 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 - } 808 - 809 - (lib.mkIf cfg.nginx.openFirewall { 810 - networking.firewall.allowedTCPPorts = [80 443]; 811 309 }) 812 - ])) 813 310 814 - { 815 - users.users.${cfg.user} = { 816 - isSystemUser = true; 817 - inherit (cfg) group; 818 - home = cfg.dataDir; 819 - }; 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 + ); 820 316 821 - users.groups.${cfg.group} = {}; 317 + SIGNAL_CLI_PATH = lib.mkDefault ( 318 + if cfg.settings.SIGNAL_SENDER_NUMBER != null then (lib.getExe pkgs.signal-cli) else null 319 + ); 320 + }; 822 321 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 - ]; 322 + users.users.${cfg.user} = { 323 + isSystemUser = true; 324 + inherit (cfg) group; 325 + home = cfg.dataDir; 326 + }; 828 327 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"]; 328 + users.groups.${cfg.group} = { }; 834 329 835 - environment = settingsToEnv cfg.settings; 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 + }); 836 343 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; 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" ]; 844 349 845 - WorkingDirectory = cfg.dataDir; 846 - StateDirectory = "tranquil-pds"; 350 + serviceConfig = { 351 + User = cfg.user; 352 + Group = cfg.group; 353 + ExecStart = lib.getExe cfg.package; 354 + Restart = "on-failure"; 355 + RestartSec = 5; 847 356 848 - EnvironmentFile = lib.mkIf (cfg.secretsFile != null) cfg.secretsFile; 357 + WorkingDirectory = cfg.dataDir; 358 + StateDirectory = "tranquil-pds"; 849 359 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; 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 + ); 865 364 866 - ReadWritePaths = [ 867 - cfg.settings.storage.blobPath 868 - cfg.settings.backup.path 869 - ]; 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; 384 + 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 };
+1
default.nix
··· 36 36 37 37 meta = { 38 38 license = lib.licenses.agpl3Plus; 39 + mainProgram = "tranquil-pds"; 39 40 }; 40 41 }

History

8 rounds 13 comments
sign up or login to add to the discussion
6 commits
expand
feat: nix module
fix: better defaults, add in pg & nginx
feat: actual good config from Isabel
fix: set env vars of paths for sendmail & signal from nix pkg not manual
refactor(nix): toml conf
fix: minor format cleanup and description fixing in the module
expand 0 comments
pull request successfully merged
6 commits
expand
feat: nix module
fix: better defaults, add in pg & nginx
feat: actual good config from Isabel
fix: set env vars of paths for sendmail & signal from nix pkg not manual
refactor(nix): toml conf
fix: minor format cleanup and description fixing in the module
expand 0 comments
4 commits
expand
feat: nix module
fix: better defaults, add in pg & nginx
feat: actual good config from Isabel
fix: set env vars of paths for sendmail & signal from nix pkg not manual
expand 0 comments
3 commits
expand
feat: nix module
fix: better defaults, add in pg & nginx
feat: actual good config from Isabel
expand 0 comments
lewis.moe submitted #3
4 commits
expand
feat: nix module
fix: better defaults, add in pg & nginx
fix: bulk type safety improvements, added a couple of tests
feat: actual good config from Isabel
expand 0 comments
2 commits
expand
feat: nix module
fix: better defaults, add in pg & nginx
expand 5 comments

Generally much better! I'll go through with a more fine-toothed comb at some point, but one immediate comment is that you should probably be using lib.mkEnableOption rather than lib.mkOption for your enable options - it's more idiomatic.

I'd also be interested in seeing your default paths for sendmail and signal moved up (and the options can be made no longer nullOr in that case too) - remember that in Nix nothing is evaluated unless you actually look at it, so as long as you check that email/signal are enabled when you place those options into the environment file (e.g. with mkIf) then this still needn't cause a dependency when it's not desirable...

https://search.nixos.org/packages?channel=25.11&show=system-sendmail&query=sendmail

^ You should probably also consider using system-sendmail which looks in a few places rather than your own best-guess sendmail path

The starred alias in nginx should really be tied to availableUserDomains rather than the hostname

I'm not a huge fan of some of the nginx stuff you're doing - recommendedProxySettings, etc. are good settings but you're leaking config outside the module which might change stuff for other modules. It might be better to enable recommendedProxySettings per-location and then setup whatever settings for the other categories you feel work best for tranquil in a lower scope...

...on a similar note, openFirewall tends to default to false to avoid unknowingly opening ports in firewalls that you didn't mean to

openFirewall tends to default to false

(I think there's one place in nixpkgs this isn't true - and it's with SSH to avoid making stuff unreachable unexpectedly)

7 commits
expand
feat: nix module
feat: fun handle resolution during migration
feat: initial in-house cache distribution
feat: cache locks less
fix: smaller docker img
fix: removed some bs
fix: better defaults, add in pg & nginx
expand 1 comment

Sorry, just updating to main

lewis.moe submitted #0
1 commit
expand
feat: nix module
expand 7 comments

Is there any reason the package doesn't seem to default to packages.tranquil-pds from this flake? I think it might be kinda nice if it did... (though I guess it would mean you have to have a reference to the flake in your module - that shouldn't really be much of a problem though)

What's the default for settings.storage.blobPath - is it actually null? what happens if I leave the blob backend as filesystem and then this as null? Could we default it in terms of dataDir (as I suspect it must really be) so that it's easier to introspect or could we put what this'll be in the description of the option

Why is backup.backend set in the test file - given it has the same value as the default and backups aren't enabled by default (or are they? they're set to null?) ... is this a mistake or something else? blobBackend is also set to its default but this is a little more understandable incase the default were to change/etc.

I also wonder if backup.enabled should be called backup.enable and should be a mkEnableOption rather than what you are doing here...... it seems quite unidiomatic

You're using optional types in a bunch of places. I'm going to assume that they are being set as overrides to some other config that is secretly elsewhere (e.g. plc.directoryUrl)... it would be much more idiomatic to have these required and have the true defaults in nix

Can you default sendmailPath (and signal CLI path) to whatever it should be from nixpkgs? I think that'd be nice :) - you need to make sure these options don't get evaluated unless mail or signal is enabled respectively

Thanks for all the comments! I am no nix connoisseur and i'll read through carefully and try to fix