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

feat: add cachet on terebithia

dunkirk.sh 6d1e4908 dce1beb8

verified
+210 -2
+7 -1
machines/prattle/default.nix
··· 181 181 tls { 182 182 dns cloudflare {env.CLOUDFLARE_API_TOKEN} 183 183 } 184 - reverse_proxy localhost:3001 184 + header { 185 + Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" 186 + } 187 + reverse_proxy localhost:3001 { 188 + header_up X-Forwarded-Proto {scheme} 189 + header_up X-Forwarded-For {remote} 190 + } 185 191 ''; 186 192 }; 187 193 };
+34 -1
machines/terebithia/default.nix
··· 72 72 gnupg 73 73 # dev_langs 74 74 nodejs_22 75 + unstable.bun 75 76 python3 76 77 go 77 78 gopls ··· 80 81 gcc 81 82 # misc 82 83 neofetch 84 + git 83 85 ]; 84 86 85 87 programs.nh = { ··· 99 101 path = "/home/kierank/.wakatime.cfg"; 100 102 owner = "kierank"; 101 103 }; 104 + cachet = { 105 + file = ../../secrets/cachet.age; 106 + owner = "cachet"; 107 + }; 108 + cloudflare = { 109 + file = ../../secrets/cloudflare.age; 110 + owner = "caddy"; 111 + }; 102 112 }; 103 113 104 114 environment.sessionVariables = { ··· 134 144 extraGroups = [ 135 145 "wheel" 136 146 "networkmanager" 147 + "services" 137 148 ]; 138 149 }; 139 150 root.openssh.authorizedKeys.keys = [ ··· 152 163 153 164 networking.firewall = { 154 165 enable = true; 155 - allowedTCPPorts = [ 22 ]; 166 + allowedTCPPorts = [ 22 80 443 ]; 156 167 logRefusedConnections = false; 157 168 rejectPackets = true; 158 169 }; ··· 160 171 services.tailscale = { 161 172 enable = true; 162 173 useRoutingFeatures = "client"; 174 + }; 175 + 176 + services.caddy = { 177 + enable = true; 178 + package = pkgs.caddy.withPlugins { 179 + plugins = [ "github.com/caddy-dns/cloudflare@v0.2.2" ]; 180 + hash = "sha256-Z8nPh4OI3/R1nn667ZC5VgE+Q9vDenaQ3QPKxmqPNkc="; 181 + }; 182 + email = "me@dunkirk.sh"; 183 + globalConfig = '' 184 + acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN} 185 + ''; 186 + }; 187 + 188 + systemd.services.caddy.serviceConfig = { 189 + EnvironmentFile = config.age.secrets.cloudflare.path; 190 + }; 191 + 192 + atelier.services.cachet = { 193 + enable = true; 194 + domain = "cachet.dunkirk.sh"; 195 + secretsFile = config.age.secrets.cachet.path; 163 196 }; 164 197 165 198 boot.loader.systemd-boot.enable = true;
+166
modules/nixos/services/cachet.nix
··· 1 + { 2 + config, 3 + lib, 4 + pkgs, 5 + ... 6 + }: 7 + let 8 + cfg = config.atelier.services.cachet; 9 + in 10 + { 11 + options.atelier.services.cachet = { 12 + enable = lib.mkEnableOption "Cachet Slack emoji/profile cache"; 13 + 14 + domain = lib.mkOption { 15 + type = lib.types.str; 16 + description = "Domain to serve cachet on"; 17 + }; 18 + 19 + port = lib.mkOption { 20 + type = lib.types.port; 21 + default = 3000; 22 + description = "Port to run cachet on"; 23 + }; 24 + 25 + dataDir = lib.mkOption { 26 + type = lib.types.path; 27 + default = "/var/lib/cachet"; 28 + description = "Directory to store cachet data"; 29 + }; 30 + 31 + secretsFile = lib.mkOption { 32 + type = lib.types.path; 33 + description = "Path to secrets file containing SLACK_TOKEN, SLACK_SIGNING_SECRET, BEARER_TOKEN"; 34 + }; 35 + 36 + repository = lib.mkOption { 37 + type = lib.types.str; 38 + default = "https://github.com/taciturnaxolotl/cachet.git"; 39 + description = "Git repository URL (optional, for auto-deployment)"; 40 + }; 41 + 42 + autoUpdate = lib.mkEnableOption "Automatically git pull on service restart"; 43 + 44 + webhook = { 45 + enable = lib.mkEnableOption "Enable webhook endpoint for triggering service restart"; 46 + 47 + path = lib.mkOption { 48 + type = lib.types.str; 49 + default = "/webhook/restart"; 50 + description = "URL path for the webhook endpoint"; 51 + }; 52 + 53 + secretFile = lib.mkOption { 54 + type = lib.types.nullOr lib.types.path; 55 + default = null; 56 + description = "Path to file containing webhook secret token"; 57 + }; 58 + }; 59 + }; 60 + 61 + config = lib.mkIf cfg.enable { 62 + users.groups.services = { }; 63 + 64 + users.users.cachet = { 65 + isSystemUser = true; 66 + group = "cachet"; 67 + extraGroups = [ "services" ]; 68 + home = cfg.dataDir; 69 + createHome = true; 70 + }; 71 + 72 + users.groups.cachet = { }; 73 + 74 + systemd.services.cachet-webhook = lib.mkIf cfg.webhook.enable { 75 + description = "Cachet webhook listener"; 76 + wantedBy = [ "multi-user.target" ]; 77 + after = [ "network.target" ]; 78 + 79 + script = let 80 + webhookScript = pkgs.writeShellScript "cachet-webhook" '' 81 + SECRET="" 82 + ${lib.optionalString (cfg.webhook.secretFile != null) '' 83 + SECRET=$(cat "${cfg.webhook.secretFile}") 84 + ''} 85 + 86 + while IFS= read -r line; do 87 + # Parse the request line 88 + if [[ "$line" =~ ^GET.*token=([^\ \&]+) ]]; then 89 + TOKEN="''${BASH_REMATCH[1]}" 90 + if [ "$TOKEN" = "$SECRET" ]; then 91 + echo -e "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nRestarting cachet service..." 92 + ${pkgs.systemd}/bin/systemctl restart cachet & 93 + else 94 + echo -e "HTTP/1.1 403 Forbidden\r\nContent-Type: text/plain\r\n\r\nInvalid token" 95 + fi 96 + else 97 + echo -e "HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain\r\n\r\nBad request" 98 + fi 99 + break 100 + done 101 + ''; 102 + in '' 103 + while true; do 104 + ${pkgs.netcat}/bin/nc -l -p 9000 -c "${webhookScript}" 105 + done 106 + ''; 107 + 108 + serviceConfig = { 109 + Type = "simple"; 110 + Restart = "always"; 111 + RestartSec = "5s"; 112 + }; 113 + }; 114 + 115 + systemd.services.cachet = { 116 + description = "Cachet Slack emoji/profile cache"; 117 + wantedBy = [ "multi-user.target" ]; 118 + after = [ "network.target" ]; 119 + 120 + preStart = '' 121 + mkdir -p ${cfg.dataDir}/data 122 + mkdir -p ${cfg.dataDir}/app 123 + chown -R cachet:services ${cfg.dataDir} 124 + chmod -R g+rwX ${cfg.dataDir} 125 + '' + lib.optionalString cfg.autoUpdate '' 126 + if [ ! -d ${cfg.dataDir}/app/.git ]; then 127 + ${pkgs.git}/bin/git clone ${cfg.repository} ${cfg.dataDir}/app 128 + else 129 + cd ${cfg.dataDir}/app && ${pkgs.git}/bin/git pull 130 + fi 131 + ''; 132 + 133 + serviceConfig = { 134 + Type = "simple"; 135 + User = "cachet"; 136 + Group = "cachet"; 137 + WorkingDirectory = "${cfg.dataDir}/app"; 138 + EnvironmentFile = cfg.secretsFile; 139 + Environment = [ 140 + "NODE_ENV=production" 141 + "PORT=${toString cfg.port}" 142 + "DATABASE_PATH=${cfg.dataDir}/data/cachet.db" 143 + ]; 144 + ExecStart = "${pkgs.unstable.bun}/bin/bun run src/index.ts"; 145 + Restart = "always"; 146 + RestartSec = "10s"; 147 + }; 148 + }; 149 + 150 + services.caddy.virtualHosts.${cfg.domain} = { 151 + extraConfig = '' 152 + tls { 153 + dns cloudflare {env.CLOUDFLARE_API_TOKEN} 154 + } 155 + 156 + ${lib.optionalString cfg.webhook.enable '' 157 + handle ${cfg.webhook.path} { 158 + reverse_proxy localhost:9000 159 + } 160 + ''} 161 + 162 + reverse_proxy localhost:${toString cfg.port} 163 + ''; 164 + }; 165 + }; 166 + }
secrets/cachet.age

This is a binary file and will not be displayed.

+3
secrets/secrets.nix
··· 26 26 "cloudflare.age".publicKeys = [ 27 27 kierank 28 28 ]; 29 + "cachet.age".publicKeys = [ 30 + kierank 31 + ]; 29 32 }