diff options
Diffstat (limited to '3modules/krebs')
-rw-r--r-- | 3modules/krebs/git.nix | 484 | ||||
-rw-r--r-- | 3modules/krebs/github-hosts-sync.nix | 83 | ||||
-rw-r--r-- | 3modules/krebs/nginx.nix | 72 | ||||
-rw-r--r-- | 3modules/krebs/urlwatch.nix | 136 |
4 files changed, 775 insertions, 0 deletions
diff --git a/3modules/krebs/git.nix b/3modules/krebs/git.nix new file mode 100644 index 0000000..3c3e934 --- /dev/null +++ b/3modules/krebs/git.nix @@ -0,0 +1,484 @@ +{ config, pkgs, lib, ... }: + +# TODO unify logging of shell scripts to user and journal +# TODO move all scripts to ${etcDir}, so ControlMaster connections +# immediately pick up new authenticators +# TODO when authorized_keys changes, then restart ssh +# (or kill already connected users somehow) + +with builtins; +with lib; +let + cfg = config.krebs.git; + + out = { + # TODO don't import krebs.nginx here + imports = [ + ../../3modules/krebs/nginx.nix + ]; + options.krebs.git = api; + config = mkIf cfg.enable (mkMerge [ + (mkIf cfg.cgit cgit-imp) + git-imp + ]); + }; + + api = { + enable = mkEnableOption "krebs.git"; + + cgit = mkOption { + type = types.bool; + default = true; + description = "Enable cgit."; # TODO better desc; talk about nginx + }; + dataDir = mkOption { + type = types.str; + default = "/var/lib/git"; + description = "Directory used to store repositories."; + }; + etcDir = mkOption { + type = types.str; + default = "/etc/git"; + }; + repos = mkOption { + type = types.attrsOf (types.submodule ({ + options = { + desc = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Repository description. + ''; + }; + section = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Repository section. + ''; + }; + name = mkOption { + type = types.str; + description = '' + Repository name. + ''; + }; + hooks = mkOption { + type = types.attrsOf types.str; + description = '' + Repository-specific hooks. + ''; + }; + public = mkOption { + type = types.bool; + default = false; + description = '' + Allow everybody to read the repository via HTTP if cgit enabled. + ''; + # TODO allow every configured user to fetch the repository via SSH. + }; + }; + })); + + default = {}; + + example = literalExample '' + { + testing = { + name = "testing"; + hooks.post-update = ''' + #! /bin/sh + set -euf + echo post-update hook: $* >&2 + '''; + }; + testing2 = { name = "testing2"; }; + } + ''; + + description = '' + Repositories. + ''; + }; + root-desc = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Text printed below the heading on the repository index page. + Default value: "a fast webinterface for the git dscm". + ''; + }; + root-title = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Text printed as heading on the repository index page. + Default value: "Git Repository Browser". + ''; + }; + rules = mkOption { + type = types.unspecified; + }; + users = mkOption { + type = types.unspecified; + }; + }; + + git-imp = { + system.activationScripts.git-init = "${init-script}"; + + # TODO maybe put all scripts here and then use PATH? + environment.etc."${etc-base}".source = + scriptFarm "git-ssh-authorizers" { + authorize-command = makeAuthorizeScript (map ({ repo, user, perm }: [ + (map getName (ensureList user)) + (map getName (ensureList repo)) + (map getName perm.allow-commands) + ]) cfg.rules); + + authorize-push = makeAuthorizeScript (map ({ repo, user, perm }: [ + (map getName (ensureList user)) + (map getName (ensureList repo)) + (ensureList perm.allow-receive-ref) + (map getName perm.allow-receive-modes) + ]) (filter (x: hasAttr "allow-receive-ref" x.perm) cfg.rules)); + }; + + users.extraUsers = singleton { + description = "Git repository hosting user"; + name = "git"; + shell = "/bin/sh"; + openssh.authorizedKeys.keys = + mapAttrsToList (_: makeAuthorizedKey git-ssh-command) cfg.users; + uid = 129318403; # genid git + }; + }; + + cgit-imp = { + users.extraUsers = lib.singleton { + inherit (fcgitwrap-user) group name uid; + home = toString (pkgs.runCommand "empty" {} "mkdir -p $out"); + }; + + users.extraGroups = lib.singleton { + inherit (fcgitwrap-group) gid name; + }; + + services.fcgiwrap = { + enable = true; + user = fcgitwrap-user.name; + group = fcgitwrap-user.group; + # socketAddress = "/run/fcgiwrap.sock" (default) + # socketType = "unix" (default) + }; + + environment.etc."cgitrc".text = '' + css=/static/cgit.css + logo=/static/cgit.png + + # if you do not want that webcrawler (like google) index your site + robots=noindex, nofollow + + virtual-root=/ + + # TODO make this nicer (and/or somewhere else) + cache-root=/tmp/cgit + + cache-size=1000 + enable-commit-graph=1 + enable-index-links=1 + enable-index-owner=0 + enable-log-filecount=1 + enable-log-linecount=1 + enable-remote-branches=1 + + ${optionalString (cfg.root-title != null) "root-title=${cfg.root-title}"} + ${optionalString (cfg.root-desc != null) "root-desc=${cfg.root-desc}"} + + snapshots=0 + max-stats=year + + ${concatMapStringsSep "\n" (repo: '' + repo.url=${repo.name} + repo.path=${cfg.dataDir}/${repo.name} + ${optionalString (repo.section != null) "repo.section=${repo.section}"} + ${optionalString (repo.desc != null) "repo.desc=${repo.desc}"} + '') (filter isPublicRepo (attrValues cfg.repos))} + ''; + + system.activationScripts.cgit = '' + mkdir -m 0700 -p /tmp/cgit + chown ${toString fcgitwrap-user.uid}:${toString fcgitwrap-group.gid} /tmp/cgit + ''; + + krebs.nginx = { + enable = true; + servers.cgit = { + server-names = [ + "cgit.${config.networking.hostName}" + "cgit.${config.networking.hostName}.retiolum" + ]; + locations = [ + (nameValuePair "/" '' + include ${pkgs.nginx}/conf/fastcgi_params; + fastcgi_param SCRIPT_FILENAME ${pkgs.cgit}/cgit/cgit.cgi; + fastcgi_param PATH_INFO $uri; + fastcgi_param QUERY_STRING $args; + fastcgi_param HTTP_HOST $server_name; + fastcgi_pass unix:${config.services.fcgiwrap.socketAddress}; + '') + (nameValuePair "/static/" '' + root ${pkgs.cgit}/cgit; + rewrite ^/static(/.*)$ $1 break; + '') + ]; + }; + }; + }; + + fcgitwrap-user = { + name = "fcgiwrap"; + uid = 2867890860; # genid fcgiwrap + group = "fcgiwrap"; + }; + + fcgitwrap-group = { + name = fcgitwrap-user.name; + gid = fcgitwrap-user.uid; + }; + + + ensureList = x: + if typeOf x == "list" then x else [x]; + + getName = x: x.name; + + isPublicRepo = getAttr "public"; # TODO this is also in ./cgit.nix + + makeAuthorizedKey = git-ssh-command: user@{ name, pubkey }: + # TODO assert name + # TODO assert pubkey + let + options = concatStringsSep "," [ + ''command="exec ${git-ssh-command} ${name}"'' + "no-agent-forwarding" + "no-port-forwarding" + "no-pty" + "no-X11-forwarding" + ]; + in + "${options} ${pubkey}"; + + # [case-pattern] -> shell-script + # Create a shell script that succeeds (exit 0) when all its arguments + # match the case patterns (in the given order). + makeAuthorizeScript = + let + # TODO escape + to-pattern = x: concatStringsSep "|" (ensureList x); + go = i: ps: + if ps == [] + then "exit 0" + else '' + case ''$${toString i} in ${to-pattern (head ps)}) + ${go (i + 1) (tail ps)} + esac''; + in + patterns: '' + #! /bin/sh + set -euf + ${concatStringsSep "\n" (map (go 1) patterns)} + exit -1 + ''; + + reponames = rules: sort lessThan (unique (map (x: x.repo.name) rules)); + + # TODO makeGitHooks that uses runCommand instead of scriptFarm? + scriptFarm = + farm-name: scripts: + let + makeScript = script-name: script-string: { + name = script-name; + path = pkgs.writeScript "${farm-name}_${script-name}" script-string; + }; + in + pkgs.linkFarm farm-name (mapAttrsToList makeScript scripts); + + + git-ssh-command = pkgs.writeScript "git-ssh-command" '' + #! /bin/sh + set -euf + + PATH=${makeSearchPath "bin" (with pkgs; [ + coreutils + git + gnugrep + gnused + systemd + ])} + + abort() { + echo "error: $1" >&2 + systemd-cat -p err -t git echo "error: $1" + exit -1 + } + + GIT_SSH_USER=$1 + + systemd-cat -p info -t git echo \ + "authorizing $GIT_SSH_USER $SSH_CONNECTION $SSH_ORIGINAL_COMMAND" + + # References: The Base Definitions volume of + # POSIX.1‐2013, Section 3.278, Portable Filename Character Set + portable_filename_bre="^[A-Za-z0-9._-]\\+$" + + command=$(echo "$SSH_ORIGINAL_COMMAND" \ + | sed -n 's/^\([^ ]*\) '"'"'\(.*\)'"'"'/\1/p' \ + | grep "$portable_filename_bre" \ + || abort 'cannot read command') + + GIT_SSH_REPO=$(echo "$SSH_ORIGINAL_COMMAND" \ + | sed -n 's/^\([^ ]*\) '"'"'\(.*\)'"'"'/\2/p' \ + | grep "$portable_filename_bre" \ + || abort 'cannot read reponame') + + ${cfg.etcDir}/authorize-command \ + "$GIT_SSH_USER" "$GIT_SSH_REPO" "$command" \ + || abort 'access denied' + + repodir=${escapeShellArg cfg.dataDir}/$GIT_SSH_REPO + + systemd-cat -p info -t git \ + echo "authorized exec $command $repodir" + + export GIT_SSH_USER + export GIT_SSH_REPO + exec "$command" "$repodir" + ''; + + init-script = pkgs.writeScript "git-init" '' + #! /bin/sh + set -euf + + PATH=${makeSearchPath "bin" (with pkgs; [ + coreutils + findutils + gawk + git + gnugrep + gnused + ])} + + dataDir=${escapeShellArg cfg.dataDir} + mkdir -p "$dataDir" + + # Notice how the presence of hooks symlinks determine whether + # we manage a repositry or not. + + # Make sure that no existing repository has hooks. We can delete + # symlinks because we assume we created them. + find "$dataDir" -mindepth 2 -maxdepth 2 -name hooks -type l -delete + bad_hooks=$(find "$dataDir" -mindepth 2 -maxdepth 2 -name hooks) + if echo "$bad_hooks" | grep -q .; then + printf 'error: unknown hooks:\n%s\n' \ + "$(echo "$bad_hooks" | sed 's/^/ /')" \ + >&2 + exit -1 + fi + + # Initialize repositories. + ${concatMapStringsSep "\n" (repo: + let + hooks = scriptFarm "git-hooks" (makeHooks repo); + in + '' + reponame=${escapeShellArg repo.name} + repodir=$dataDir/$reponame + mode=${toString (if isPublicRepo repo then 0711 else 0700)} + if ! test -d "$repodir"; then + mkdir -m "$mode" "$repodir" + git init --bare --template=/var/empty "$repodir" + chown -R git:nogroup "$repodir" + fi + ln -s ${hooks} "$repodir/hooks" + '' + ) (attrValues cfg.repos)} + + # Warn about repositories that exist but aren't mentioned in the + # current configuration (and thus didn't receive a hooks symlink). + unknown_repos=$(find "$dataDir" -mindepth 1 -maxdepth 1 \ + -type d \! -exec test -e '{}/hooks' \; -print) + if echo "$unknown_repos" | grep -q .; then + printf 'warning: stale repositories:\n%s\n' \ + "$(echo "$unknown_repos" | sed 's/^/ /')" \ + >&2 + fi + ''; + + makeHooks = repo: removeAttrs repo.hooks [ "pre-receive" ] // { + pre-receive = '' + #! /bin/sh + set -euf + + PATH=${makeSearchPath "bin" (with pkgs; [ + coreutils # env + git + systemd + ])} + + accept() { + #systemd-cat -p info -t git echo "authorized $1" + accept_string="''${accept_string+$accept_string + }authorized $1" + } + reject() { + #systemd-cat -p err -t git echo "denied $1" + #echo 'access denied' >&2 + #exit_code=-1 + reject_string="''${reject_string+$reject_string + }access denied: $1" + } + + empty=0000000000000000000000000000000000000000 + + accept_string= + reject_string= + while read oldrev newrev ref; do + + if [ $oldrev = $empty ]; then + receive_mode=create + elif [ $newrev = $empty ]; then + receive_mode=delete + elif [ "$(git merge-base $oldrev $newrev)" = $oldrev ]; then + receive_mode=fast-forward + else + receive_mode=non-fast-forward + fi + + if ${cfg.etcDir}/authorize-push \ + "$GIT_SSH_USER" "$GIT_SSH_REPO" "$ref" "$receive_mode"; then + accept "$receive_mode $ref" + else + reject "$receive_mode $ref" + fi + done + + if [ -n "$reject_string" ]; then + systemd-cat -p err -t git echo "$reject_string" + exit -1 + fi + + systemd-cat -p info -t git echo "$accept_string" + + ${optionalString (hasAttr "post-receive" repo.hooks) '' + # custom post-receive hook + ${repo.hooks.post-receive}''} + ''; + }; + + etc-base = + assert (hasPrefix "/etc/" cfg.etcDir); + removePrefix "/etc/" cfg.etcDir; + +in +out diff --git a/3modules/krebs/github-hosts-sync.nix b/3modules/krebs/github-hosts-sync.nix new file mode 100644 index 0000000..c3b56ef --- /dev/null +++ b/3modules/krebs/github-hosts-sync.nix @@ -0,0 +1,83 @@ +{ config, lib, pkgs, ... }: + +with builtins; +with lib; +let + cfg = config.krebs.github-hosts-sync; + + out = { + options.krebs.github-hosts-sync = api; + config = mkIf cfg.enable imp; + }; + + api = { + enable = mkEnableOption "krebs.github-hosts-sync"; + port = mkOption { + type = types.int; # TODO port type + default = 1028; + }; + dataDir = mkOption { + type = types.str; # TODO path (but not just into store) + default = "/var/lib/github-hosts-sync"; + }; + ssh-identity-file = mkOption { + type = types.str; # TODO must be named *.ssh.{id_rsa,id_ed25519} + default = "/root/src/secrets/github-hosts-sync.ssh.id_rsa"; + }; + }; + + imp = { + systemd.services.github-hosts-sync = { + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + environment = { + port = toString cfg.port; + }; + serviceConfig = { + PermissionsStartOnly = "true"; + SyslogIdentifier = "github-hosts-sync"; + User = user.name; + Restart = "always"; + ExecStartPre = pkgs.writeScript "github-hosts-sync-init" '' + #! /bin/sh + set -euf + + ssh_identity_file_target=$( + case ${cfg.ssh-identity-file} in + *.ssh.id_rsa|*.ssh.id_ed25519) echo ${cfg.dataDir}/.ssh/id_rsa;; + *.ssh.id_ed25519) echo ${cfg.dataDir}/.ssh/id_ed25519;; + *) + echo "bad identity file name: ${cfg.ssh-identity-file}" >&2 + exit 1 + esac + ) + + mkdir -p ${cfg.dataDir} + chown ${user.name}: ${cfg.dataDir} + + install \ + -o ${user.name} \ + -m 0400 \ + ${cfg.ssh-identity-file} \ + "$ssh_identity_file_target" + + ln -snf ${Zpkgs.github-known_hosts} ${cfg.dataDir}/.ssh/known_hosts + ''; + ExecStart = "${Zpkgs.github-hosts-sync}/bin/github-hosts-sync"; + }; + }; + + users.extraUsers = singleton { + inherit (user) name uid; + home = cfg.dataDir; + }; + }; + + user = { + name = "github-hosts-sync"; + uid = 3220554646; # genid github-hosts-sync + }; + + Zpkgs = import ../../Zpkgs/krebs { inherit pkgs; }; +in +out diff --git a/3modules/krebs/nginx.nix b/3modules/krebs/nginx.nix new file mode 100644 index 0000000..702e8a7 --- /dev/null +++ b/3modules/krebs/nginx.nix @@ -0,0 +1,72 @@ +{ config, pkgs, lib, ... }: + +with builtins; +with lib; +let + cfg = config.krebs.nginx; + + out = { + options.krebs.nginx = api; + config = mkIf cfg.enable imp; + }; + + api = { + enable = mkEnableOption "krebs.nginx"; + + servers = mkOption { + type = with types; attrsOf optionSet; + options = singleton { + server-names = mkOption { + type = with types; listOf str; + # TODO use identity + default = [ + "${config.networking.hostName}" + "${config.networking.hostName}.retiolum" + ]; + }; + locations = mkOption { + type = with types; listOf (attrsOf str); + }; + }; + default = {}; + }; + }; + + imp = { + services.nginx = { + enable = true; + httpConfig = '' + include ${pkgs.nginx}/conf/mime.types; + default_type application/octet-stream; + sendfile on; + keepalive_timeout 65; + gzip on; + server { + listen 80 default_server; + server_name _; + return 404; + } + ${concatStrings (mapAttrsToList (_: to-server) cfg.servers)} + ''; + }; + }; + + + indent = replaceChars ["\n"] ["\n "]; + + to-location = { name, value }: '' + location ${name} { + ${indent value} + } + ''; + + to-server = { server-names, locations, ... }: '' + server { + listen 80; + server_name ${toString server-names}; + ${indent (concatStrings (map to-location locations))} + } + ''; + +in +out diff --git a/3modules/krebs/urlwatch.nix b/3modules/krebs/urlwatch.nix new file mode 100644 index 0000000..58de72f --- /dev/null +++ b/3modules/krebs/urlwatch.nix @@ -0,0 +1,136 @@ +{ config, lib, pkgs, ... }: + +# TODO multiple users +# TODO inform about unused caches +# cache = url: "${cfg.dataDir}/.urlwatch/cache/${hashString "sha1" url}" +# TODO hooks.py + +with builtins; +with lib; +let + cfg = config.krebs.urlwatch; + + # TODO assert sendmail's existence + out = { + options.krebs.urlwatch = api; + config = mkIf cfg.enable imp; + }; + + api = { + enable = mkEnableOption "krebs.urlwatch"; + + dataDir = mkOption { + type = types.str; + default = "/var/lib/urlwatch"; + description = '' + Directory where the urlwatch service should store its state. + ''; + }; + from = mkOption { + type = types.str; + default = "${user.name}@${config.networking.hostName}.retiolum"; + description = '' + Content of the From: header of the generated mails. + ''; + }; + mailto = mkOption { + type = types.str; + description = '' + Content of the To: header of the generated mails. [AKA recipient :)] + ''; + }; + onCalendar = mkOption { + type = types.str; + description = '' + Run urlwatch at this interval. + The format is described in systemd.time(7), CALENDAR EVENTS. + ''; + example = "04:23"; + }; + urls = mkOption { + type = with types; listOf str; + description = "URL to watch."; + example = [ + https://nixos.org/channels/nixos-unstable/git-revision + ]; + }; + }; + + urlsFile = toFile "urls" (concatStringsSep "\n" cfg.urls); + + imp = { + systemd.timers.urlwatch = { + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = cfg.onCalendar; + Persistent = "true"; + }; + }; + systemd.services.urlwatch = { + path = with pkgs; [ + coreutils + gnused + urlwatch + ]; + environment = { + HOME = cfg.dataDir; + LC_ALL = "en_US.UTF-8"; + LOCALE_ARCHIVE = "${pkgs.glibcLocales}/lib/locale/locale-archive"; + SSL_CERT_FILE = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"; + }; + serviceConfig = { + User = user.name; + PermissionsStartOnly = "true"; + PrivateTmp = "true"; + Type = "oneshot"; + ExecStartPre = + pkgs.writeScript "urlwatch-prestart" '' + #! /bin/sh + set -euf + + dataDir=$HOME + + if ! test -e "$dataDir"; then + mkdir -m 0700 -p "$dataDir" + chown ${user.name}: "$dataDir" + fi + ''; + ExecStart = pkgs.writeScript "urlwatch" '' + #! /bin/sh + set -euf + + from=${escapeShellArg cfg.from} + mailto=${escapeShellArg cfg.mailto} + urlsFile=${escapeShellArg urlsFile} + + cd /tmp + + urlwatch -e --urls="$urlsFile" > changes 2>&1 || : + + if test -s changes; then + date=$(date -R) + subject=$(sed -n 's/^\(CHANGED\|ERROR\|NEW\): //p' changes \ + | tr \\n \ ) + { + echo "Date: $date" + echo "From: $from" + echo "Subject: $subject" + echo "To: $mailto" + echo + cat changes + } | /var/setuid-wrappers/sendmail -t + fi + ''; + }; + }; + users.extraUsers = singleton { + inherit (user) name uid; + }; + }; + + user = { + name = "urlwatch"; + uid = 3467631196; # genid urlwatch + }; +in +out |