Kieran's opinionated (and probably slightly dumb) nix config

feat: add control endpoint

dunkirk.sh b9ed6794 c3c2bc68

verified
+176 -5
+1
.envrc
··· 1 +
+53 -3
machines/terebithia/default.nix
··· 137 137 file = ../../secrets/l4.age; 138 138 owner = "l4"; 139 139 }; 140 + control = { 141 + file = ../../secrets/control.age; 142 + owner = "control"; 143 + }; 140 144 "restic/env".file = ../../secrets/restic/env.age; 141 145 "restic/repo".file = ../../secrets/restic/repo.age; 142 146 "restic/password".file = ../../secrets/restic/password.age; ··· 310 314 header { 311 315 Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" 312 316 } 313 - reverse_proxy localhost:8084 { 314 - header_up X-Forwarded-Proto {scheme} 315 - header_up X-Forwarded-For {remote} 317 + 318 + # Kill-check for protected endpoints via control panel 319 + @protected path /sse /sse/* /tiles/*/markers/pl3xmap_players.json 320 + handle @protected { 321 + reverse_proxy localhost:3010 { 322 + rewrite /kill-check 323 + header_up X-Orig-Host {host} 324 + header_up X-Orig-Path {path} 325 + 326 + @allowed status 200 327 + handle_response @allowed { 328 + reverse_proxy localhost:8084 329 + } 330 + handle_response { 331 + respond "Temporarily disabled" 503 332 + } 333 + } 316 334 } 335 + 336 + # Proxy settings.json through control to conditionally redact fields 337 + handle /tiles/settings.json { 338 + reverse_proxy localhost:3010 { 339 + rewrite /proxy/settings.json 340 + header_up X-Orig-Host {host} 341 + header_up X-Orig-Path {path} 342 + header_up X-Backend-Url http://localhost:8084{path} 343 + } 344 + } 345 + 346 + reverse_proxy localhost:8084 317 347 ''; 318 348 }; 319 349 extraConfig = '' ··· 420 450 port = 3004; 421 451 deploy.autoUpdate = false; 422 452 secretsFile = config.age.secrets.l4.path; 453 + }; 454 + 455 + atelier.services.control = { 456 + enable = true; 457 + domain = "control.dunkirk.sh"; 458 + deploy.repository = "https://tangled.org/dunkirk.sh/control"; 459 + deploy.autoUpdate = true; 460 + secretsFile = config.age.secrets.control.path; 461 + 462 + flags."map.dunkirk.sh" = { 463 + name = "Map"; 464 + flags = { 465 + "block-tracking" = { 466 + name = "Block Player Tracking"; 467 + description = "Disable real-time player location updates"; 468 + paths = [ "/sse" "/sse/*" "/tiles/*/markers/pl3xmap_players.json" ]; 469 + redact."/tiles/settings.json" = [ "players" ]; 470 + }; 471 + }; 472 + }; 423 473 }; 424 474 425 475 services.n8n = {
+10 -2
modules/lib/mkService.nix
··· 196 196 inherit description; 197 197 wantedBy = [ "multi-user.target" ]; 198 198 after = [ "network.target" ]; 199 - path = [ pkgs.git ]; 199 + path = [ pkgs.git pkgs.openssh ]; 200 200 201 201 preStart = lib.optionalString (cfg.deploy.enable && cfg.deploy.repository != null) '' 202 202 # Clone repository if not present ··· 245 245 }; 246 246 247 247 serviceConfig.ExecStartPre = [ 248 - "+${pkgs.writeShellScript "${name}-setup" '' 248 + # Run before preStart, creates directories so WorkingDirectory exists 249 + "!${pkgs.writeShellScript "${name}-setup" '' 249 250 mkdir -p ${cfg.dataDir}/app/data 250 251 mkdir -p ${cfg.dataDir}/data 251 252 chown -R ${name}:services ${cfg.dataDir} ··· 253 254 ''}" 254 255 ]; 255 256 }; 257 + 258 + # Ensure working directory exists before service starts 259 + systemd.tmpfiles.rules = [ 260 + "d ${cfg.dataDir} 0755 ${name} services -" 261 + "d ${cfg.dataDir}/app 0755 ${name} services -" 262 + "d ${cfg.dataDir}/data 0755 ${name} services -" 263 + ]; 256 264 257 265 # Caddy reverse proxy 258 266 services.caddy.virtualHosts.${cfg.domain} = lib.mkIf cfg.caddy.enable {
+109
modules/nixos/services/control.nix
··· 1 + # Control Panel - Admin dashboard for Caddy toggles 2 + # 3 + # Protected by Indiko OAuth, allows toggling feature flags 4 + # that control Caddy behavior. Uses SQLite for flag storage 5 + # and exposes a /kill-check endpoint for Caddy to query. 6 + 7 + { config, lib, pkgs, ... }: 8 + 9 + let 10 + mkService = import ../../lib/mkService.nix; 11 + cfg = config.atelier.services.control; 12 + 13 + # Generate flags.json from Nix config 14 + flagsJson = pkgs.writeText "flags.json" (builtins.toJSON { 15 + services = lib.mapAttrs (serviceId: serviceCfg: { 16 + name = serviceCfg.name; 17 + flags = lib.mapAttrs (flagId: flagCfg: { 18 + name = flagCfg.name; 19 + description = flagCfg.description; 20 + paths = flagCfg.paths; 21 + redact = flagCfg.redact; 22 + }) serviceCfg.flags; 23 + }) cfg.flags; 24 + }); 25 + 26 + baseModule = mkService { 27 + name = "control"; 28 + description = "Control Panel - Admin dashboard for Caddy toggles"; 29 + defaultPort = 3010; 30 + runtime = "bun"; 31 + entryPoint = "src/index.ts"; 32 + 33 + extraOptions = { 34 + flags = lib.mkOption { 35 + type = lib.types.attrsOf (lib.types.submodule { 36 + options = { 37 + name = lib.mkOption { 38 + type = lib.types.str; 39 + description = "Display name for this service"; 40 + }; 41 + flags = lib.mkOption { 42 + type = lib.types.attrsOf (lib.types.submodule { 43 + options = { 44 + name = lib.mkOption { 45 + type = lib.types.str; 46 + description = "Display name for this flag"; 47 + }; 48 + description = lib.mkOption { 49 + type = lib.types.str; 50 + description = "Description of what this flag does"; 51 + }; 52 + paths = lib.mkOption { 53 + type = lib.types.listOf lib.types.str; 54 + default = []; 55 + description = "URL paths this flag fully blocks"; 56 + }; 57 + redact = lib.mkOption { 58 + type = lib.types.attrsOf (lib.types.listOf lib.types.str); 59 + default = {}; 60 + description = "Map of path -> fields to redact from JSON response"; 61 + }; 62 + }; 63 + }); 64 + default = {}; 65 + description = "Flags for this service"; 66 + }; 67 + }; 68 + }); 69 + default = {}; 70 + description = "Services and their flags"; 71 + example = lib.literalExpression '' 72 + { 73 + "map.dunkirk.sh" = { 74 + name = "Map"; 75 + flags = { 76 + "block-tracking" = { 77 + name = "Block Player Tracking"; 78 + description = "Disable real-time player location updates"; 79 + paths = [ "/sse" "/tiles/world/markers/pl3xmap_players.json" ]; 80 + redact = { 81 + "/tiles/settings.json" = [ "players" ]; 82 + }; 83 + }; 84 + }; 85 + }; 86 + } 87 + ''; 88 + }; 89 + }; 90 + 91 + extraConfig = innerCfg: { 92 + atelier.services.control.environment = { 93 + INDIKO_URL = "https://indiko.dunkirk.sh"; 94 + CLIENT_ID = "https://${innerCfg.domain}/"; 95 + REDIRECT_URI = "https://${innerCfg.domain}/auth/callback"; 96 + DATABASE_PATH = "${innerCfg.dataDir}/data/control.db"; 97 + FLAGS_CONFIG = toString flagsJson; 98 + }; 99 + 100 + # Data declarations for backup (SQLite database) 101 + atelier.services.control.data = { 102 + sqlite = "${innerCfg.dataDir}/data/control.db"; 103 + }; 104 + }; 105 + }; 106 + in 107 + { 108 + imports = [ baseModule ]; 109 + }
secrets/control.age

This is a binary file and will not be displayed.

+3
secrets/secrets.nix
··· 47 47 "l4.age".publicKeys = [ 48 48 kierank 49 49 ]; 50 + "control.age".publicKeys = [ 51 + kierank 52 + ]; 50 53 "restic/env.age".publicKeys = [ 51 54 kierank 52 55 ];