greenhouse

kore

my public infrastructure monorepo

git clone [email protected]:kore.git

kore / nixos / roles / git.nix193 lines · 6301B · raw

1{ inputs, config, lib, pkgs, ... }:
2
3let
4  inherit (config.kore) identities repos;
5  inherit (config.networking) domain;
6  root = "/srv/git";
7
8  petal = pkgs.stdenv.mkDerivation {
9    pname = "petal";
10    version = "0-${inputs.greenhouse.shortRev or "dirty"}";
11
12    src = "${inputs.greenhouse}/petal";
13
14    buildInputs = [ pkgs.libgit2 ];
15
16    makeFlags = [
17      "PREFIX=$(out)"
18      "CC=${pkgs.stdenv.cc.targetPrefix}cc"
19      "GIT2INC=${lib.getDev pkgs.libgit2}/include"
20      "GIT2LIB=${lib.getLib pkgs.libgit2}/lib"
21    ];
22  };
23
24  petalHome = pkgs.writeTextDir ".gitconfig" (lib.generators.toGitINI {
25    safe.directory = map (repo: "${root}/${repo}.git") (lib.attrNames repos);
26  });
27
28  mirrorKey = "${root}/.ssh/mirror";
29  mirrors = lib.any (access: access.mirror != null) (lib.attrValues repos);
30
31  mirrorHook = pkgs.writeShellScript "kore-git-mirror" ''
32    cat > /dev/null
33    url=$(${pkgs.git}/bin/git config --get kore.mirror) || exit 0
34    export GIT_SSH_COMMAND="${pkgs.openssh}/bin/ssh -i ${mirrorKey} -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=yes"
35    if ${pkgs.git}/bin/git push --mirror --quiet "$url"; then
36      echo "kore-git: mirrored to $url"
37    else
38      echo "kore-git: mirror to $url failed" >&2
39    fi
40  '';
41
42  fastcgi = ''
43    fastcgi_param GATEWAY_INTERFACE CGI/1.1;
44    fastcgi_param SERVER_PROTOCOL $server_protocol;
45    fastcgi_param SERVER_NAME $server_name;
46    fastcgi_param SERVER_PORT $server_port;
47    fastcgi_param REQUEST_METHOD $request_method;
48    fastcgi_param REQUEST_URI $request_uri;
49    fastcgi_param QUERY_STRING $query_string;
50    fastcgi_param CONTENT_TYPE $content_type;
51    fastcgi_param CONTENT_LENGTH $content_length;
52    fastcgi_param REMOTE_ADDR $remote_addr;
53    fastcgi_param HTTPS $https if_not_empty;
54    fastcgi_param HOME ${petalHome};
55    fastcgi_pass unix:${config.services.fcgiwrap.instances.petal.socket.address};
56  '';
57
58  owner = access: if access.owner != null then access.owner else lib.findFirst (_: true) "" access.write;
59
60  acl = pkgs.writeText "kore-git-acl" (lib.concatLines (lib.concatLists (lib.mapAttrsToList (repo: access:
61    map (id: "${repo} write ${id}") access.write
62    ++ map (id: "${repo} read ${id}") access.read
63  ) repos)));
64
65  kore-git = pkgs.writeShellApplication {
66    name = "kore-git";
67    runtimeInputs = [ pkgs.git pkgs.gnugrep ];
68    text = ''
69      id="$1"
70      cmd="''${SSH_ORIGINAL_COMMAND:-}"
71
72      deny() {
73        echo "kore-git: access denied" >&2
74        exit 1
75      }
76
77      verb="''${cmd%% *}"
78      case "$verb" in
79        git-upload-pack | git-upload-archive) perm="read" ;;
80        git-receive-pack) perm="write" ;;
81        *)
82          echo "kore-git: hi $id, shell access is not provided" >&2
83          exit 1
84          ;;
85      esac
86
87      path="''${cmd#"$verb" }"
88      path="''${path#\'}"
89      path="''${path%\'}"
90      path="''${path#/}"
91      path="''${path%/}"
92      path="''${path%.git}"
93
94      [[ "$path" =~ ^[A-Za-z0-9_][A-Za-z0-9._-]*(/[A-Za-z0-9_][A-Za-z0-9._-]*)*$ ]] || deny
95
96      grep -qFx "$path write $id" ${acl} \
97        || { [ "$perm" = read ] && grep -qFx "$path read $id" ${acl}; } \
98        || deny
99
100      exec git-shell -c "$verb '${root}/$path.git'"
101    '';
102  };
103
104  members = lib.unique (lib.concatLists (lib.mapAttrsToList (_: access: access.write ++ access.read) repos));
105in {
106  assertions = [{
107    assertion = lib.elem "web" config.kore.currentHost.roles;
108    message = "kore: the git role needs the web role for petal";
109  }];
110
111  users.groups.git = {};
112  users.users.git = {
113    isSystemUser = true;
114    group = "git";
115    home = root;
116    homeMode = "750";
117    createHome = true;
118    shell = pkgs.bash;
119
120    openssh.authorizedKeys.keys = lib.concatMap (id:
121      map (key: ''command="${kore-git}/bin/kore-git ${id}",restrict ${key}'') (identities.${id}.sshKeys or [])
122    ) members;
123  };
124
125  systemd.services.kore-git-repos = {
126    wantedBy = [ "multi-user.target" ];
127    after = [ "local-fs.target" ];
128    serviceConfig = {
129      Type = "oneshot";
130      User = "git";
131      Group = "git";
132    };
133    path = [ pkgs.git pkgs.coreutils pkgs.openssh ];
134    script = lib.optionalString mirrors ''
135      if [ ! -f '${mirrorKey}' ]; then
136        mkdir -p -m 700 '${root}/.ssh'
137        ssh-keygen -q -t ed25519 -N "" -C "git@${config.networking.hostName} mirror" -f '${mirrorKey}'
138      fi
139    '' + lib.concatStrings (lib.mapAttrsToList (repo: access: ''
140      if [ ! -d '${root}/${repo}.git' ]; then
141        mkdir -p "$(dirname '${root}/${repo}.git')"
142        git init --quiet --bare --initial-branch=main '${root}/${repo}.git'
143      fi
144      printf '%s\n' ${lib.escapeShellArg access.description} > '${root}/${repo}.git/description'
145      printf '%s\n' ${lib.escapeShellArg (owner access)} > '${root}/${repo}.git/owner'
146      ln -sfn ${mirrorHook} '${root}/${repo}.git/hooks/post-receive'
147      ${if access.mirror != null
148        then "git -C '${root}/${repo}.git' config kore.mirror ${lib.escapeShellArg access.mirror}"
149        else "git -C '${root}/${repo}.git' config --unset kore.mirror || true"}
150    '') repos);
151  };
152
153  programs.ssh.knownHosts."github.com".publicKey =
154    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl";
155
156  systemd.tmpfiles.rules = [ "z ${root} 0750 git git -" ];
157
158  users.users.petal = {
159    isSystemUser = true;
160    group = "git";
161  };
162
163  services.fcgiwrap.instances.petal = {
164    process.user = "petal";
165    process.group = "git";
166    socket.user = config.services.nginx.user;
167    socket.group = config.services.nginx.group;
168  };
169
170  services.nginx.virtualHosts."git.${domain}" = {
171    enableACME = true;
172    forceSSL = true;
173
174    locations."= /style.css".alias = "${petal}/share/petal/style.css";
175
176    locations."/".extraConfig = fastcgi + ''
177      fastcgi_param SCRIPT_FILENAME ${petal}/bin/petal;
178      fastcgi_param SCRIPT_NAME "";
179      fastcgi_param PATH_INFO $uri;
180    '';
181
182    locations."~ ^/(.+\\.git/(?:info/refs|git-upload-pack))$".extraConfig = ''
183      if ($arg_service = git-receive-pack) {
184        return 403;
185      }
186    '' + fastcgi + ''
187      fastcgi_param SCRIPT_FILENAME ${pkgs.git}/libexec/git-core/git-http-backend;
188      fastcgi_param PATH_INFO /$1;
189      fastcgi_param GIT_PROJECT_ROOT ${root};
190      fastcgi_param GIT_HTTP_EXPORT_ALL 1;
191    '';
192  };
193}