Our Personal Data Server from scratch! tranquil.farm
oauth atproto pds rust postgresql objectstorage fun
at feat/nix-module 375 lines 11 kB view raw
1self: { 2 lib, 3 pkgs, 4 config, 5 ... 6}: let 7 cfg = config.services.tranquil-pds; 8 9 inherit (lib) types mkOption; 10 11 settingsFormat = pkgs.formats.toml { }; 12 13 backendUrl = "http://127.0.0.1:${toString cfg.settings.server.port}"; 14 15 useACME = cfg.nginx.enableACME && cfg.nginx.useACMEHost == null; 16 hasSSL = useACME || cfg.nginx.useACMEHost != null; 17in { 18 _class = "nixos"; 19 20 options.services.tranquil-pds = { 21 enable = lib.mkEnableOption "tranquil-pds AT Protocol personal data server"; 22 23 package = mkOption { 24 type = types.package; 25 default = self.packages.${pkgs.stdenv.hostPlatform.system}.tranquil-pds; 26 defaultText = lib.literalExpression "self.packages.\${pkgs.stdenv.hostPlatform.system}.tranquil-pds"; 27 description = "The tranquil-pds package to use"; 28 }; 29 30 user = mkOption { 31 type = types.str; 32 default = "tranquil-pds"; 33 description = "User under which tranquil-pds runs"; 34 }; 35 36 group = mkOption { 37 type = types.str; 38 default = "tranquil-pds"; 39 description = "Group under which tranquil-pds runs"; 40 }; 41 42 dataDir = mkOption { 43 type = types.str; 44 default = "/var/lib/tranquil-pds"; 45 description = "Directory for tranquil-pds data (blobs, backups)"; 46 }; 47 48 environmentFiles = mkOption { 49 type = types.listOf types.path; 50 default = [ ]; 51 description = '' 52 File to load environment variables from. Loaded variables override 53 values set in {option}`environment`. 54 55 Use it to set values of `JWT_SECRET`, `DPOP_SECRET` and `MASTER_KEY`. 56 57 Generate these with: 58 ``` 59 openssl rand --hex 32 60 ``` 61 ''; 62 }; 63 64 database.createLocally = mkOption { 65 type = types.bool; 66 default = false; 67 description = '' 68 Create the postgres database and user on the local host. 69 ''; 70 }; 71 72 frontend.package = mkOption { 73 type = types.nullOr types.package; 74 default = self.packages.${pkgs.stdenv.hostPlatform.system}.tranquil-frontend; 75 defaultText = lib.literalExpression "self.packages.\${pkgs.stdenv.hostPlatform.system}.tranquil-frontend"; 76 description = "Frontend package to serve via nginx (set null to disable frontend)"; 77 }; 78 79 nginx = { 80 enable = lib.mkEnableOption "nginx reverse proxy for tranquil-pds"; 81 82 enableACME = mkOption { 83 type = types.bool; 84 default = true; 85 description = "Enable ACME for the pds domain"; 86 }; 87 88 useACMEHost = mkOption { 89 type = types.nullOr types.str; 90 default = null; 91 description = '' 92 Use a pre-configured ACME certificate instead of generating one. 93 Set this to the cert name from security.acme.certs for wildcard setups. 94 95 REMEMBER: Handle subdomains (*.pds.example.com) require a wildcard cert via DNS-01. 96 ''; 97 }; 98 }; 99 100 settings = mkOption { 101 type = types.submodule { 102 freeformType = settingsFormat.type; 103 104 options = { 105 server = { 106 host = mkOption { 107 type = types.str; 108 default = "127.0.0.1"; 109 description = "Host for tranquil-pds to listen on"; 110 }; 111 112 port = mkOption { 113 type = types.int; 114 default = 3000; 115 description = "Port for tranquil-pds to listen on"; 116 }; 117 118 hostname = mkOption { 119 type = types.str; 120 default = ""; 121 example = "pds.example.com"; 122 description = "The public-facing hostname of the PDS"; 123 }; 124 125 max_blob_size = mkOption { 126 type = types.int; 127 default = 10737418240; # 10 GiB 128 description = "Maximum allowed blob size in bytes."; 129 }; 130 }; 131 132 storage = { 133 path = mkOption { 134 type = types.path; 135 default = "/var/lib/tranquil-pds/blobs"; 136 description = "Directory for storing blobs"; 137 }; 138 }; 139 140 backup = { 141 path = mkOption { 142 type = types.path; 143 default = "/var/lib/tranquil-pds/backups"; 144 description = "Directory for storing backups"; 145 }; 146 }; 147 148 email = { 149 sendmail_path = mkOption { 150 type = types.path; 151 default = lib.getExe pkgs.system-sendmail; 152 description = "Path to the sendmail executable to use for sending emails."; 153 }; 154 }; 155 156 signal = { 157 cli_path = mkOption { 158 type = types.path; 159 default = lib.getExe pkgs.signal-cli; 160 description = "Path to the signal-cli executable to use for sending Signal notifications."; 161 }; 162 }; 163 }; 164 }; 165 166 description = '' 167 Configuration options to set for the service. Secrets should be 168 specified using {option}`environmentFile`. 169 170 Refer to <https://tangled.org/tranquil.farm/tranquil-pds/blob/main/example.toml> 171 for available configuration options. 172 ''; 173 }; 174 }; 175 176 config = lib.mkIf cfg.enable ( 177 lib.mkMerge [ 178 (lib.mkIf cfg.database.createLocally { 179 services.postgresql = { 180 enable = true; 181 ensureDatabases = [ cfg.user ]; 182 ensureUsers = [ 183 { 184 name = cfg.user; 185 ensureDBOwnership = true; 186 } 187 ]; 188 }; 189 190 services.tranquil-pds.settings.database.url = 191 lib.mkDefault "postgresql:///${cfg.user}?host=/run/postgresql"; 192 193 systemd.services.tranquil-pds = { 194 requires = [ "postgresql.service" ]; 195 after = [ "postgresql.service" ]; 196 }; 197 }) 198 199 (lib.mkIf cfg.nginx.enable { 200 services.nginx = { 201 enable = true; 202 203 virtualHosts.${cfg.settings.server.hostname} = { 204 serverAliases = [ "*.${cfg.settings.server.hostname}" ]; 205 forceSSL = hasSSL; 206 enableACME = useACME; 207 useACMEHost = cfg.nginx.useACMEHost; 208 209 root = lib.mkIf (cfg.frontend.package != null) cfg.frontend.package; 210 211 extraConfig = "client_max_body_size ${toString cfg.settings.server.max_blob_size};"; 212 213 locations = lib.mkMerge [ 214 { 215 "/xrpc/" = { 216 proxyPass = backendUrl; 217 proxyWebsockets = true; 218 extraConfig = '' 219 proxy_read_timeout 86400; 220 proxy_send_timeout 86400; 221 proxy_buffering off; 222 proxy_request_buffering off; 223 ''; 224 }; 225 226 "/oauth/" = { 227 proxyPass = backendUrl; 228 extraConfig = '' 229 proxy_read_timeout 300; 230 proxy_send_timeout 300; 231 ''; 232 }; 233 234 "/.well-known/" = { 235 proxyPass = backendUrl; 236 }; 237 238 "/webhook/" = { 239 proxyPass = backendUrl; 240 }; 241 242 "= /metrics" = { 243 proxyPass = backendUrl; 244 }; 245 246 "= /health" = { 247 proxyPass = backendUrl; 248 }; 249 250 "= /robots.txt" = { 251 proxyPass = backendUrl; 252 }; 253 254 "= /logo" = { 255 proxyPass = backendUrl; 256 }; 257 258 "~ ^/u/[^/]+/did\\.json$" = { 259 proxyPass = backendUrl; 260 }; 261 } 262 263 (lib.optionalAttrs (cfg.frontend.package != null) { 264 "= /oauth/client-metadata.json" = { 265 root = "${cfg.frontend.package}"; 266 extraConfig = '' 267 default_type application/json; 268 sub_filter_once off; 269 sub_filter_types application/json; 270 sub_filter '__PDS_HOSTNAME__' $host; 271 ''; 272 }; 273 274 "/assets/" = { 275 # TODO: use `add_header_inherit` when nixpkgs updates to nginx 1.29.3+ 276 extraConfig = '' 277 expires 1y; 278 add_header Cache-Control "public, immutable"; 279 ''; 280 tryFiles = "$uri =404"; 281 }; 282 283 "/app/" = { 284 tryFiles = "$uri $uri/ /index.html"; 285 }; 286 287 "= /" = { 288 tryFiles = "/homepage.html /index.html"; 289 }; 290 291 "/" = { 292 tryFiles = "$uri $uri/ /index.html"; 293 priority = 9999; 294 }; 295 }) 296 ]; 297 }; 298 }; 299 }) 300 301 { 302 users.users.${cfg.user} = { 303 isSystemUser = true; 304 inherit (cfg) group; 305 home = cfg.dataDir; 306 }; 307 308 users.groups.${cfg.group} = { }; 309 310 systemd.tmpfiles.settings."tranquil-pds" = 311 lib.genAttrs 312 [ 313 cfg.dataDir 314 cfg.settings.storage.path 315 cfg.settings.backup.path 316 ] 317 (_: { 318 d = { 319 mode = "0750"; 320 inherit (cfg) user group; 321 }; 322 }); 323 324 environment.etc = { 325 "tranquil-pds/config.toml".source = settingsFormat.generate "tranquil-pds.toml" cfg.settings; 326 }; 327 328 systemd.services.tranquil-pds = { 329 description = "Tranquil PDS - AT Protocol Personal Data Server"; 330 after = [ "network-online.target" ]; 331 wants = [ "network-online.target" ]; 332 wantedBy = [ "multi-user.target" ]; 333 334 serviceConfig = { 335 User = cfg.user; 336 Group = cfg.group; 337 ExecStart = lib.getExe cfg.package; 338 Restart = "on-failure"; 339 RestartSec = 5; 340 341 WorkingDirectory = cfg.dataDir; 342 StateDirectory = "tranquil-pds"; 343 344 EnvironmentFile = cfg.environmentFiles; 345 346 NoNewPrivileges = true; 347 ProtectSystem = "strict"; 348 ProtectHome = true; 349 PrivateTmp = true; 350 PrivateDevices = true; 351 ProtectKernelTunables = true; 352 ProtectKernelModules = true; 353 ProtectControlGroups = true; 354 RestrictAddressFamilies = [ 355 "AF_INET" 356 "AF_INET6" 357 "AF_UNIX" 358 ]; 359 RestrictNamespaces = true; 360 LockPersonality = true; 361 MemoryDenyWriteExecute = true; 362 RestrictRealtime = true; 363 RestrictSUIDSGID = true; 364 RemoveIPC = true; 365 366 ReadWritePaths = [ 367 cfg.settings.storage.path 368 cfg.settings.backup.path 369 ]; 370 }; 371 }; 372 } 373 ] 374 ); 375}