zaphyra's git: nixfiles

zaphyra and void's nixfiles

commit a1d895c20eeba48f818e312cba6696f883897f8f
parent 1de3fc7316426d1fee00bf583da083b2634f5bc2
Author: Katja (zaphyra) <git@ctu.cx>
Date: Fri, 23 May 2025 11:31:20 +0200

config/nixos/modules/websites: add `git.zaphyra.eu` (and enable on host `morio`)
6 files changed, 343 insertions(+), 4 deletions(-)
M
config/home/katja/programs/ssh.nix
|
4
++++
A
config/nixos/modules/websites/git.zaphyra.eu.nix
|
284
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
M
flake.lock
|
23
++++++++++++++++++++++-
M
flake.nix
|
4
++++
M
hosts/morio/default.nix
|
26
+++++++++++++++++++++++++-
M
secrets/morio.yaml
|
6
++++--
diff --git a/config/home/katja/programs/ssh.nix b/config/home/katja/programs/ssh.nix
@@ -14,6 +14,10 @@
         user = "git";
         hostname = "git.katja.wtf";
       };
+      "zaphyra-git" = {
+        user = "git";
+        hostname = "git.zaphyra.eu";
+      };
     };
   };
 
diff --git a/config/nixos/modules/websites/git.zaphyra.eu.nix b/config/nixos/modules/websites/git.zaphyra.eu.nix
@@ -0,0 +1,284 @@
+{
+  povSelf,
+  hostConfig,
+  config,
+  lib,
+  pkgs,
+  dnsNix,
+  ...
+}:
+
+let
+  inherit (lib) types;
+  cfg = lib.getAttrFromPath povSelf config;
+
+in
+{
+
+  options = {
+    enable = {
+      type = types.bool;
+      default = false;
+    };
+    domain = {
+      type = types.str;
+      default = "zaphyra.eu";
+    };
+    subdomain = {
+      type = types.str;
+      default = "git";
+    };
+    title = {
+      type = types.str;
+      default = "zaphyra's git";
+    };
+    mail = {
+      type = types.str;
+      default = "git@zaphyra.eu";
+    };
+    categories = {
+      type = with types; listOf str;
+      default = [
+        "nix"
+        "etc"
+      ];
+    };
+    adminPubkey = {
+      type = types.str;
+      default = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDIHFm/bePR+HT5MAuMslUHt68nTrEhlqcKIS+9Rfi9FzRKAia/DdLbwpfC1iXuM+iQd8fIMp4Ir+kBMqoZaVzyCtqKH6QHbhwBiWIdMA7FndMbfDcO9BzUcqCAVt8HxcGd1Z4bE9ZgZZuIsJiPbqJ+QbK1rkY8uJLDVR2MXI5jpU/m+9RJzrwVZ6JxwjdY4cNaIYwoOW6ZxL+ukLRwy+spBWmWdcHeq6zeLsl/OjUV6WIh2pM9O0o9nsiDekhOBf2MJLlM+e8rWICwYsfLqGAeRAuDe03BFBXsbDg/lqTYB5G8XSaT2R8ty2RyeEBySS32pUyErdKVXnyHNBEvxC6+cJiZtL8rhkpU1qRg/MIUjprMVUWisMlYnai2K0VpNpc5w09YXQl7aXSge8L/5+IzugPj17+FK4FVwRptXxynnYeKiwWEsOiiFe3IVaQ6vyRN66fbMjx/d0JSfadbwV7L++aT85bsb05zhNDpaqK1I5sGs3uV3CglkmhxBmky67Eq/qkMlJZMVtgE7i88H8+XzTiofJaKYTeyq+XQnK6a6OVGyca2dEorBFmBTEtz70nQnSrhPqQrS4zgr4OTSFtUtdFVDzgHaxRC+y4/SP5zCA8Xfwp0q1M0jVE9XpVpGydXtGGV08uXOsDPv5E4euxq6qgv8d2azDeBHXp+kEHm4w== cardno:6445184";
+    };
+  };
+
+  config = lib.mkIf cfg.enable (
+    let
+      stagitFunctions = pkgs.writeShellScript "stagitFunctions" ''
+        is_public_and_listed() {
+          if [ ! -f "$1/git-daemon-export-ok" ]; then
+            return 1
+          fi
+          return 0
+        }
+
+        is_forced_update() {
+          test "$oldrev" = "0000000000000000000000000000000000000000" && return 1
+          test "$newrev" = "0000000000000000000000000000000000000000" && return 1
+
+          hasrevs="$(${pkgs.git}/bin/git rev-list "$oldrev" "^$newrev" | ${pkgs.gnused}/bin/sed 1q)"
+          if test -n "$hasrevs"; then
+            return 0
+          fi
+          return 1
+        }
+
+        build_stagit_repo() {
+          reponame="$(basename "$1" ".git")"
+          printf "[%s] Generate stagit HTML pages... " "$reponame"
+
+          mkdir -p "${config.modules.services.gitolite.dataDir}/stagit-cache"
+          mkdir -p "/var/lib/stagit/$reponame"
+
+          cd "/var/lib/stagit/$reponame" || return 1
+
+          # build repo pages
+          ${pkgs.stagit}/bin/stagit -c "${config.modules.services.gitolite.dataDir}/stagit-cache/$reponame" -n "${cfg.title}" -h 'https://git.${cfg.domain}/' -s 'git@${config.networking.fqdn}:' "$1"
+
+          # set correct permissions
+          chown git:git -R /var/lib/stagit/$reponame;
+          chmod 755 -R /var/lib/stagit/$reponame;
+
+          echo "done"
+        }
+
+        build_stagit_index() {
+          printf "Generating stagit index... "
+
+          # set assets if not already there
+          ln -sf "${pkgs.stagit}/share/doc/stagit/highlight.min.js" "/var/lib/stagit/highlight.min.js" 2> /dev/null
+          ln -sf "${pkgs.stagit}/share/doc/stagit/style.css"        "/var/lib/stagit/style.css" 2> /dev/null
+
+          # generate index arguments
+          args="-n \"${cfg.title}\" -e '${cfg.mail}'"
+
+          for category in ${lib.escapeShellArgs cfg.categories}; do
+            args="$args -c '$category'"
+            for repo in "$HOME/repositories/"*.git/; do
+              repo="''${repo%/}"
+              is_public_and_listed "$repo" || continue
+
+              [ "$(${pkgs.gawk}/bin/awk -F '=' '/category/ {print $2}' $repo/config | ${pkgs.gnused}/bin/sed -e 's/^[[:space:]]*//')" = "$category" ] && args="$args $repo"
+            done
+          done
+
+          # build index
+          echo "$args" | xargs ${pkgs.stagit}/bin/stagit-index > /var/lib/stagit/index.html
+
+          # set correct permissions
+          chown git:git /var/lib/stagit/index.html;
+          chmod 755 /var/lib/stagit/index.html;
+
+          echo "done"
+        }
+
+
+        update_stagit_repo() {
+          repo="$(pwd)"
+          reponame="$(basename "$repo" ".git")"
+
+          cd "$repo" || return 1
+          is_public_and_listed "$repo" || return 0
+
+          # if forced update, remove directory and cache file
+          is_forced_update && printf "[%s] Forced update, trigger complete regeneration of stagit-pages... \n" "$reponame" && rm -rf "/var/lib/stagit/$reponame" "/var/lib/gitolite/stagit-cache/$reponame"
+
+          build_stagit_repo "$repo"
+          build_stagit_index
+        }
+
+      '';
+
+      rebuildWebdir = ''
+        source ${stagitFunctions}
+
+        # clear webdir
+        rm -rf /var/lib/stagit/*
+
+        # clear cache
+        rm -rf ${config.modules.services.gitolite.dataDir}/stagit-cache/*
+
+        # generate pages per repo
+        for repo in "$HOME/repositories/"*.git/; do
+          repo="''${repo%/}"
+          is_public_and_listed "$repo" || continue
+
+          build_stagit_repo "$repo"
+        done
+
+        # generate index page
+        build_stagit_index
+      '';
+
+    in
+    {
+      dns.zones."${cfg.domain}".subdomains."${cfg.subdomain}".CNAME = [ "${config.networking.fqdn}." ];
+
+      sops.secrets."resticPasswords/gitolite" = {
+        owner = "git";
+      };
+
+      systemd.tmpfiles.settings.stagit = {
+        "/var/lib/stagit".d = {
+          group = "git";
+          user = "git";
+          mode = "775";
+          age = "-";
+        };
+      };
+
+      modules.services = {
+        resticBackup.paths = {
+          gitolite = {
+            enable = true;
+            user = "git";
+            passwordFile = config.sops.secrets."resticPasswords/gitolite".path;
+            paths = [ config.modules.services.gitolite.dataDir ];
+          };
+        };
+        gitolite = {
+          enable = true;
+          user = "git";
+          group = "git";
+          adminPubkey = cfg.adminPubkey;
+
+          extraGitoliteRc = ''
+            $RC{GIT_CONFIG_KEYS} = ".*";
+            $RC{UMASK}           = 0027;
+
+            push(@{$RC{ENABLE}}, 'cgit');
+            push(@{$RC{ENABLE}}, 'symbolic-ref');
+            push(@{$RC{ENABLE}}, 'rebuild-webdir');
+            push(@{$RC{ENABLE}}, 'rebuild-webdir');
+
+            $RC{NON_CORE} = "rebuild-webdir-trigger POST_COMPILE rebuild-stagit";
+          '';
+
+          triggers.rebuild-webdir = rebuildWebdir;
+          commands.rebuild-webdir = rebuildWebdir;
+          commonHooks.post-receive = ''
+            # update stagit pages
+            source ${stagitFunctions}
+            update_stagit_repo "$1"
+          '';
+        };
+      };
+
+      services = {
+        fcgiwrap = {
+          instances.git = {
+            process.user = "git";
+            process.group = "git";
+            socket.user = "nginx";
+            socket.group = "nginx";
+          };
+        };
+
+        nginx = {
+          enable = true;
+          virtualHosts."${cfg.subdomain}.${cfg.domain}" = {
+            useACMEHost = "${config.networking.fqdn}";
+            forceSSL = true;
+            kTLS = true;
+            root = "/var/lib/stagit";
+            locations = {
+              "@redir".return = "307 ../log.html";
+              "~ '^/([a-zA-Z0-9_.]+)/commit/.*$'".extraConfig = "error_page 404 = @redir;";
+
+              "~* \.html$".extraConfig = ''
+                add_header Last-Modified $date_gmt;
+                add_header Cache-Control 'private no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
+                if_modified_since off;
+                expires off;
+                etag off;
+              '';
+
+              "~ '^/[a-zA-Z0-9._-]+/raw'".extraConfig = ''
+                types {
+                  application/json                                 json;
+
+                  application/wasm                                 wasm;
+                  font/woff                                        woff;
+                  font/woff2                                       woff2;
+
+                  application/pdf                                  pdf;
+
+                  image/gif                                        gif;
+                  image/jpeg                                       jpeg jpg;
+                  image/png                                        png;
+                  image/svg+xml                                    svg svgz;
+                  image/webp                                       webp;
+                  image/x-icon                                     ico;
+                }
+
+                default_type   text/plain;
+                try_files $uri =404;
+              '';
+
+              "~ '^/[a-zA-Z0-9._-]+/(git-(receive|upload)-pack|HEAD|info/refs|objects/(info/(http-)?alternates|packs)|[0-9a-f]{2}/[0-9a-f]{38}|pack/pack-[0-9a-f]{40}\.(pack|idx))$'".extraConfig =
+                ''
+                  if ($query_string = service=git-receive-pack) {
+                    return 403;
+                  }
+
+                  include "${pkgs.nginx}/conf/fastcgi_params";
+                  fastcgi_param SCRIPT_FILENAME  "${pkgs.git}/libexec/git-core/git-http-backend";
+                  fastcgi_param GIT_PROJECT_ROOT /var/lib/gitolite/repositories;
+                  fastcgi_param PATH_INFO        $uri;
+                  fastcgi_pass  unix:${config.services.fcgiwrap.instances.git.socket.address};
+                '';
+            };
+          };
+        };
+      };
+    }
+  );
+
+}
diff --git a/flake.lock b/flake.lock
@@ -499,7 +499,8 @@
         "nixSystemsDefault": "nixSystemsDefault",
         "nixpkgs": "nixpkgs",
         "nixpkgsUnstable": "nixpkgsUnstable",
-        "sopsNix": "sopsNix"
+        "sopsNix": "sopsNix",
+        "stagit": "stagit"
       }
     },
     "rust-overlay": {

@@ -546,6 +547,26 @@
         "repo": "sops-nix",
         "type": "github"
       }
+    },
+    "stagit": {
+      "inputs": {
+        "nixpkgs": [
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1742749207,
+        "narHash": "sha256-3gGCusgJ9qEPMdevaYCU2UM54hhu1sAm5++mC7bNdy8=",
+        "ref": "refs/heads/main",
+        "rev": "a4b05b204f1854c98e7ae68960fc1582493de890",
+        "revCount": 445,
+        "type": "git",
+        "url": "https://git.katja.wtf/stagit"
+      },
+      "original": {
+        "type": "git",
+        "url": "https://git.katja.wtf/stagit"
+      }
     }
   },
   "root": "root",
diff --git a/flake.nix b/flake.nix
@@ -79,6 +79,7 @@
                 inputs.self.overlays.packages
                 inputs.self.overlays.nixpkgsUnstable
                 inputs.ctucxWebsite.overlays.default
+                inputs.stagit.overlays.default
               ];
             }
 

@@ -183,6 +184,9 @@
     ctucxWebsite.url = "git+https://git.katja.wtf/website";
     ctucxWebsite.inputs.nixpkgs.follows = "nixpkgs";
 
+    stagit.url = "git+https://git.katja.wtf/stagit";
+    stagit.inputs.nixpkgs.follows = "nixpkgs";
+
     firefoxGnomeTheme.flake = false;
     firefoxGnomeTheme.url = "github:rafaelmardojai/firefox-gnome-theme/v137";
   };
diff --git a/hosts/morio/default.nix b/hosts/morio/default.nix
@@ -25,12 +25,23 @@
   };
 
   configuration =
-    { config, pkgs, ... }:
+    {
+      inputs,
+      config,
+      pkgs,
+      ...
+    }:
     {
 
       boot.initrd.systemd.emergencyAccess = true;
       boot.kernelPackages = pkgs.linuxPackages_latest;
 
+      sops.secrets = {
+        "resticEnv/novus" = {
+          sopsFile = inputs.self.sopsSecrets.common;
+        };
+      };
+
       modules = {
         filesystem.rootDisk = {
           enable = true;

@@ -53,6 +64,19 @@
           };
         };
 
+        services = {
+          resticBackup.targets = {
+            novus = {
+              repository = "rest:https://restic.novus.infra.zaphyra.eu";
+              environmentFile = config.sops.secrets."resticEnv/novus".path;
+            };
+          };
+        };
+
+        websites = {
+          "git.zaphyra.eu".enable = true;
+        };
+
         users.katja.enable = true;
       };
 
diff --git a/secrets/morio.yaml b/secrets/morio.yaml
@@ -1,4 +1,6 @@
 acmeTSIGKey: ENC[AES256_GCM,data:XbTSbHisL5ZszYY4hvKplyWG98eK4DUeiSpA24Am/QPjEw8ofHWzU2WmV9hzj8Jd29Z0Yf0u/m5T/FESS2Gt9w==,iv:liySg99CmJ9RePJ84pD2+2mNsvZ4SbEXt3d58kDsHgI=,tag:zNwYe1ZfhFGmfP2s+OLj3Q==,type:str]
+resticPasswords:
+    gitolite: ENC[AES256_GCM,data:g28//NtKEYL+Dh0+Ws73ZKySl1L0avxqNXVn5lKaj1U=,iv:mGQ7pYjeMEGTCS1l6H/h043M2oAhgMOAlUHkgDir03E=,tag:E/ps0EZmlMEm+ziWzXzQPQ==,type:str]
 knotKeys: ENC[AES256_GCM,data:rlTFDvonfEQFST1eSHHcaG3e1CSt5paDUTvfoYmInBV7mjqe7PwT5dtg01W2ANZJYl+SN/cdI3eEvAdJvwYR6FK+7g1LPwn6G1coE68a/XwzsWM5WpSemmDfTykoUiguEUfRCZ0Q3M7YqV0/jDWrKMaH0iKqKqvlv7nEy6VXB5SZBX+aN18KvPVygw5FixQ/kD3XFI2HTTST4vqlMma3CTsjnK6Uwf1421JOIe3JR32qd0V7IfhFvL0mErMIRhLnITO9uJ//t1HJoeaOV7FEY4K6Ohacng1c68fkUjVX5wYBTd6X657nFqevvLiMRDiQnASOJrAJUAeq4Kwf5R7C/I/MeVh+1Hq/U+z4ZQKh/DViEE8+TkJwDMBAWarzlyOz7xDF8O+fj4iH5jTX8H3FmJLU/TVU0QXqnwjcAAVs/YNARNVt0wGdWTb9iyvD7vEIZE57wIp+TIGE8XFjOO11/DRC/0kC8HFkvoXke9IRrTIj1pCP1VIrv31v7aIyphWa5hBuBHfVb0f8g5eaqyKumM03Rge+Fo+jtM/NP7H2gao6uaZM/K4a625nVx+M1lUpW+1c0sIAME2SlDjSyuhTkMknOPGAAYXMwVQGazoOJna6sEBl6jYcgn31w3dHtJXGKyAB0eqELxjt5b0tzcBfJ4pXi7HO7w2yhKrqyL7GuE5LtLp5mkguC5eZbiX+VlGTLX6V2z1kDRUdYDDZMMh3cYGBIrGVoJhWx8xLWrLGm7TrvifiwYJq5Mq13tt2hS7HpY8T8YGBD2x3IdPAHtikZUYgv5cQxs7drSJi8zFQAwDUKofxhJQUvqrnmvNf+eiGkfgI7lQ0//NLg9o4t+5g+T3mV8IUkW4nbJsP46k6azQGBt3udYAVhgrFy/jTE++KrA==,iv:+5NBUUC1QhPjN+6E8nWhzd2SNuH9mLbhsFwDTm8Hy+U=,tag:RtSO5Rmb0wNR9ovtpwJIIg==,type:str]
 sops:
     kms: []

@@ -15,8 +17,8 @@ sops:
             bDRhUEtDdmlZa0ZENFhSVnNqVjFCR1UKEIkSg3tKFkwlnNXFFqCBtdZBGz1bEmWl
             wghkTtqTl++759zZAAmjdnFFQWs/AoCZ5g/GUidz6HHcFdxMpGVmiA==
             -----END AGE ENCRYPTED FILE-----
-    lastmodified: "2025-05-21T10:53:45Z"
-    mac: ENC[AES256_GCM,data:N7NTYDFRqb57D/sxbTGvOI1HqAJ3GmGCzwq7+Yi6refzpi8Ch3hh/gs5aqWmGJN1kMCR7P1kijnnCgMzpKNZ4hZ9VWtIwGmzkfAOuA8D8tE1uCS1D2eYuaiStKWgpDj4m//6nqaiUO7KN7snKE4M68ZPlh5k430dhBLvBRpF7sY=,iv:OcCo/c4P8zcAZWWXdQecZbUr1eLUq8wBJaCoXDqU1Dc=,tag:AVAdT5bC6lOsyhJehJ1qYA==,type:str]
+    lastmodified: "2025-05-22T22:59:42Z"
+    mac: ENC[AES256_GCM,data:5XIqoKdnnoHhX3Kkkq83X9cFu6Mm5OMDE9ZsjPBQ73fwgfl++XARaUhVVqKllvaCw4AHFQakS6VLgMfJ9/NrHw46fFUnixl91Som51T3+73JDi6ebCi69txNe5EYWRR5i3kWylus8dnnIWzTOouguFE6VT/fHPVZgndaiNScLqM=,iv:+Er27YY1//YQhvqnxVqO5hhwyiMCNFgo7ZRjTOtQiPY=,tag:bIuKUKHpdnjdcGq2Fj2xFg==,type:str]
     pgp:
         - created_at: "2025-05-21T08:09:28Z"
           enc: |-