feat(nixos/roles): add git server
4 files changed, -4/+127
home/features/cli/ssh.nix
@@ -24,6 +24,11 @@ HostName = "github.com"; User = "git"; };++ ld = {+ HostName = "git.lostduk.com";+ User = "git";+ }; }; }; }
kore.json
@@ -16,9 +16,13 @@ "iris": { "system": "x86_64-linux", "stateVersion": "26.11",- "roles": ["web"],+ "roles": ["web", "git"], "users": ["lostduk"],+ "aliases": ["git"], "ipv4": "82.153.138.53" }+ },+ "repos": {+ "kore": { "write": ["lostduk"] } } }
nixos/features/kore.nix
@@ -50,11 +50,30 @@ let type = types.listOf types.str; default = []; };+ aliases = mkOption {+ type = types.listOf types.str;+ default = [];+ }; ipv4 = mkOption { type = types.nullOr types.str; default = null; }; };++ repoOptions = with lib; {+ write = mkOption {+ type = types.listOf types.str;+ default = [];+ };+ read = mkOption {+ type = types.listOf types.str;+ default = [];+ };+ };++ hostNames = name: host:+ let names = [ name ] ++ host.aliases;+ in map (n: "${n}.${config.networking.domain}") names ++ names; in { options.kore = with lib; { identities = mkOption {@@ -68,6 +87,10 @@ in { currentHost = mkOption { type = types.submodule [{ options = hostOptions; }]; };+ repos = mkOption {+ type = types.attrsOf (types.submodule [{ options = repoOptions; }]);+ default = {};+ }; impermanence.enable = mkOption { type = types.bool;@@ -80,12 +103,21 @@ in { identities = kore.identities or {}; hosts = kore.hosts; currentHost = kore.hosts.${hostname};+ repos = kore.repos or {}; }; assertions = map (user: { assertion = cfg.identities ? ${user}; message = "kore: ${hostname} has user ${user}, but kore.json has no identity for it";- }) cfg.currentHost.users;+ }) cfg.currentHost.users+ ++ lib.concatLists (lib.mapAttrsToList (repo: acl: [{+ assertion = builtins.match "[A-Za-z0-9_][A-Za-z0-9._-]*(/[A-Za-z0-9_][A-Za-z0-9._-]*)*" repo != null+ && !(lib.hasSuffix ".git" repo);+ message = "kore: repo name ${repo} is invalid";+ }] ++ map (id: {+ assertion = cfg.identities ? ${id};+ message = "kore: repo ${repo} lists ${id}, but kore.json has no identity for it";+ }) (acl.write ++ acl.read)) cfg.repos); nixpkgs.hostPlatform = cfg.currentHost.system; system.stateVersion = cfg.currentHost.stateVersion;@@ -107,11 +139,11 @@ in { # /etc/hosts so machines resolve each other by name. networking.hosts = lib.mkMerge (lib.mapAttrsToList (name: host: lib.optionalAttrs (host.ipv4 != null) {- "${host.ipv4}" = [ "${name}.${config.networking.domain}" name ];+ "${host.ipv4}" = hostNames name host; }) cfg.hosts); programs.ssh.knownHosts = lib.mapAttrs (name: host: {- hostNames = [ name "${name}.${config.networking.domain}" ]+ hostNames = hostNames name host ++ lib.optional (host.ipv4 != null) host.ipv4; publicKeyFile = hostKey name; }) (lib.filterAttrs (name: _: builtins.pathExists (hostKey name)) cfg.hosts);
nixos/roles/git.nix
@@ -0,0 +1,82 @@+{ config, lib, pkgs, ... }:++let+ inherit (config.kore) identities repos;+ root = "/srv/git";++ acl = pkgs.writeText "kore-git-acl" (lib.concatLines (lib.concatLists (lib.mapAttrsToList (repo: access:+ map (id: "${repo} write ${id}") access.write+ ++ map (id: "${repo} read ${id}") access.read+ ) repos)));++ kore-git = pkgs.writeShellApplication {+ name = "kore-git";+ runtimeInputs = [ pkgs.git pkgs.gnugrep ];+ text = ''+ id="$1"+ cmd="''${SSH_ORIGINAL_COMMAND:-}"++ deny() {+ echo "kore-git: access denied" >&2+ exit 1+ }++ verb="''${cmd%% *}"+ case "$verb" in+ git-upload-pack | git-upload-archive) perm="read" ;;+ git-receive-pack) perm="write" ;;+ *)+ echo "kore-git: hi $id, shell access is not provided" >&2+ exit 1+ ;;+ esac++ path="''${cmd#"$verb" }"+ path="''${path#\'}"+ path="''${path%\'}"+ path="''${path#/}"+ path="''${path%/}"+ path="''${path%.git}"++ [[ "$path" =~ ^[A-Za-z0-9_][A-Za-z0-9._-]*(/[A-Za-z0-9_][A-Za-z0-9._-]*)*$ ]] || deny++ grep -qFx "$path write $id" ${acl} \+ || { [ "$perm" = read ] && grep -qFx "$path read $id" ${acl}; } \+ || deny++ exec git-shell -c "$verb '${root}/$path.git'"+ '';+ };++ members = lib.unique (lib.concatLists (lib.mapAttrsToList (_: access: access.write ++ access.read) repos));+in {+ users.groups.git = {};+ users.users.git = {+ isSystemUser = true;+ group = "git";+ home = root;+ createHome = true;+ shell = pkgs.bash;++ openssh.authorizedKeys.keys = lib.concatMap (id:+ map (key: ''command="${kore-git}/bin/kore-git ${id}",restrict ${key}'') (identities.${id}.sshKeys or [])+ ) members;+ };++ systemd.services.kore-git-repos = {+ wantedBy = [ "multi-user.target" ];+ after = [ "local-fs.target" ];+ serviceConfig = {+ Type = "oneshot";+ User = "git";+ Group = "git";+ };+ path = [ pkgs.git pkgs.coreutils ];+ script = lib.concatMapStrings (repo: ''+ if [ ! -d '${root}/${repo}.git' ]; then+ mkdir -p "$(dirname '${root}/${repo}.git')"+ git init --quiet --bare --initial-branch=main '${root}/${repo}.git'+ fi+ '') (lib.attrNames repos);+ };+}