summaryrefslogtreecommitdiffstats
path: root/3modules/krebs
diff options
context:
space:
mode:
authorlassulus <lass@aidsballs.de>2015-07-27 10:09:13 +0200
committerlassulus <lass@aidsballs.de>2015-07-27 10:09:13 +0200
commit54a01c0c74bdd4233962d62c4e6631f7f8b50f77 (patch)
tree705a3f8307b49e52bd95ecac8cd6d6ca828921a4 /3modules/krebs
parent6476abd6ac7e000d0759569a1e2754acb2f518ca (diff)
parent3197897292f0fc8f38d30ad6ddc9742be4a7cc1d (diff)
Merge branch 'tv' into master
Diffstat (limited to '3modules/krebs')
-rw-r--r--3modules/krebs/default.nix308
-rw-r--r--3modules/krebs/git.nix490
-rw-r--r--3modules/krebs/github-hosts-sync.nix83
-rw-r--r--3modules/krebs/nginx.nix72
-rw-r--r--3modules/krebs/retiolum.nix226
-rw-r--r--3modules/krebs/urlwatch.nix136
6 files changed, 1315 insertions, 0 deletions
diff --git a/3modules/krebs/default.nix b/3modules/krebs/default.nix
new file mode 100644
index 000000000..33c108811
--- /dev/null
+++ b/3modules/krebs/default.nix
@@ -0,0 +1,308 @@
+{ config, lib, ... }:
+
+with import ../../4lib/krebs { inherit lib; };
+let
+ cfg = config.krebs;
+
+ out = {
+ imports = [
+ ./github-hosts-sync.nix
+ ./git.nix
+ ./nginx.nix
+ ./retiolum.nix
+ ./urlwatch.nix
+ ];
+ options.krebs = api;
+ config = mkIf cfg.enable imp;
+ };
+
+ api = {
+ enable = mkEnableOption "krebs";
+
+ build = mkOption {
+ type = types.submodule {
+ options = {
+ host = mkOption {
+ type = types.host;
+ };
+ user = mkOption {
+ type = types.user;
+ };
+ };
+ };
+ # Define defaul value, so unset values of the submodule get reported.
+ default = {};
+ };
+
+ hosts = mkOption {
+ type = with types; attrsOf host;
+ };
+
+ users = mkOption {
+ type = with types; attrsOf user;
+ };
+
+ # XXX is there a better place to define search-domain?
+ # TODO search-domains :: listOf hostname
+ search-domain = mkOption {
+ type = types.hostname;
+ default = "";
+ example = "retiolum";
+ };
+ };
+
+ imp = mkMerge [
+ { krebs = lass-imp; }
+ { krebs = makefu-imp; }
+ { krebs = tv-imp; }
+ {
+ # XXX This overlaps with krebs.retiolum
+ networking.extraHosts =
+ let
+ # TODO move domain name providers to a dedicated module
+ # providers : tree label providername
+ providers = {
+ internet = "hosts";
+ retiolum = "hosts";
+ de.viljetic = "regfish";
+ de.krebsco = "ovh";
+ };
+
+ # splitByProvider : [alias] -> listset providername alias
+ splitByProvider = foldl (acc: alias: listset-insert (providerOf alias) alias acc) {};
+
+ # providerOf : alias -> providername
+ providerOf = alias:
+ tree-get (splitString "." alias) providers;
+ in
+ concatStringsSep "\n" (flatten (
+ # TODO deepMap ["hosts" "nets"] (hostname: host: netname: net:
+ mapAttrsToList (hostname: host:
+ mapAttrsToList (netname: net:
+ let
+ aliases = toString (unique (longs ++ shorts));
+ longs = (splitByProvider net.aliases).hosts;
+ shorts = map (removeSuffix ".${cfg.search-domain}") longs;
+ in
+ map (addr: "${addr} ${aliases}") net.addrs
+ ) host.nets
+ ) config.krebs.hosts
+ ));
+ }
+ ];
+
+ lass-imp = {
+ hosts = addNames {
+ };
+ users = addNames {
+ lass = {
+ pubkey = readFile ../../Zpubkeys/lass.ssh.pub;
+ };
+ uriel = {
+ pubkey = readFile ../../Zpubkeys/uriel.ssh.pub;
+ };
+ };
+ };
+
+ makefu-imp = {
+ hosts = addNames {
+ pnp = {
+ cores = 1;
+ dc = "makefu"; #vm on 'omo'
+ nets = {
+ retiolum = {
+ addrs4 = ["10.243.0.210"];
+ addrs6 = ["42:f9f1:0000:0000:0000:0000:0000:0001"];
+ aliases = [
+ "pnp.retiolum"
+ "cgit.pnp.retiolum"
+ ];
+ tinc.pubkey = ''
+ -----BEGIN RSA PUBLIC KEY-----
+ MIIBCgKCAQEAugkgEK4iy2C5+VZHwhjj/q3IOhhazE3TYHuipz37KxHWX8ZbjH+g
+ Ewtm79dVysujAOX8ZqV8nD8JgDAvkIZDp8FCIK0/rgckhpTsy1HVlHxa7ECrOS8V
+ pGz4xOxgcPFRbv5H2coHtbnfQc4GdA5fcNedQ3BP3T2Tn7n/dbbVs30bOP5V0EMR
+ SqZwNmtqaDQxOvjpPg9EoHvAYTevrpbbIst9UzCyvmNli9R+SsiDrzEPgB7zOc4T
+ TG12MT+XQr6JUu4jPpzdhb6H/36V6ADCIkBjzWh0iSfWGiFDQFinD+YSWbA1NOTr
+ Qtd1I3Ov+He7uc2Z719mb0Og2kCGnCnPIwIDAQAB
+ -----END RSA PUBLIC KEY-----
+ '';
+ };
+ };
+ };
+ };
+ users = addNames {
+ makefu = {
+ pubkey = readFile ../../Zpubkeys/makefu_arch.ssh.pub;
+ };
+ };
+ };
+
+ tv-imp = {
+ hosts = addNames {
+ cd = {
+ cores = 2;
+ dc = "tv"; #dc = "cac";
+ nets = rec {
+ internet = {
+ addrs4 = ["162.219.7.216"];
+ aliases = [
+ "cd.internet"
+ "cd.viljetic.de"
+ "cgit.cd.viljetic.de"
+ "cd.krebsco.de"
+ ];
+ };
+ retiolum = {
+ via = internet;
+ addrs4 = ["10.243.113.222"];
+ addrs6 = ["42:4522:25f8:36bb:8ccb:0150:231a:2af3"];
+ aliases = [
+ "cd.retiolum"
+ "cgit.cd.retiolum"
+ ];
+ tinc.pubkey = ''
+ -----BEGIN RSA PUBLIC KEY-----
+ MIICCgKCAgEAvmCBVNKT/Su4v9nl/Nm3STPo5QxWPg7xEkzIs3Oh39BS8+r6/7UQ
+ rebib7mczb+ebZd+Rg2yFoGrWO8cmM0VcLy5bYRMK7in8XroLEjWecNNM4TRfNR4
+ e53+LhcPdkxo0A3/D+yiut+A2Mkqe+4VXDm/JhAiAYkZTn7jUtj00Atrc7CWW1gN
+ sP3jIgv4+CGftdSYOB4dm699B7OD9XDLci2kOaFqFl4cjDYUok03G0AduUlRx10v
+ CKbKOTIdm8C36A902/3ms+Hyzkruu+VagGIZuPSwqXHJPCu7Ju+jarKQstMmpQi0
+ PubweWDL0o/Dfz2qT3DuL4xDecIvGE6kv3m41hHJYiK+2/azTSehyPFbsVbL7w0V
+ LgKN3usnZNcpTsBWxRGT7nMFSnX2FLDu7d9OfCuaXYxHVFLZaNrpccOq8NF/7Hbk
+ DDW81W7CvLyJDlp0WLnAawSOGTUTPoYv/2wAapJ89i8QGCueGvEc6o2EcnBVMFEW
+ ejWTQzyD816f4RsplnrRqLVlIMbr9Q/n5TvlgjjhX7IMEfMy4+7qLGRQkNbFzgwK
+ jxNG2fFSCjOEQitm0gAtx7QRIyvYr6c7/xiHz4AwxYzBmvQsL/OK57NO4+Krwgj5
+ Vk8TQ2jGO7J4bB38zaxK+Lrtfl8i1AK1171JqFMhOc34JSJ7T4LWDMECAwEAAQ==
+ -----END RSA PUBLIC KEY-----
+ '';
+ };
+ };
+ };
+ mkdir = {
+ cores = 1;
+ dc = "tv"; #dc = "cac";
+ nets = rec {
+ internet = {
+ addrs4 = ["162.248.167.241"];
+ aliases = [
+ "mkdir.internet"
+ ];
+ };
+ retiolum = {
+ via = internet;
+ addrs4 = ["10.243.113.223"];
+ addrs6 = ["42:4522:25f8:36bb:8ccb:0150:231a:2af4"];
+ aliases = [
+ "mkdir.retiolum"
+ "cgit.mkdir.retiolum"
+ ];
+ tinc.pubkey = ''
+ -----BEGIN RSA PUBLIC KEY-----
+ MIIBCgKCAQEAuyfM+3od75zOYXqnqRMAt+yp/4z/vC3vSWdjUvEmCuM23c5BOBw+
+ dKqbWoSPTzOuaQ0szdL7a6YxT+poSUXd/i3pPz59KgCl192rd1pZoJKgvoluITev
+ voYSP9rFQOUrustfDb9qKW/ZY95cwdCvypo7Vf4ghxwDCnlmyCGz7qXTJMLydNKF
+ 2PH9KiY4suv15sCg/zisu+q0ZYQXUc1TcgpoIYBOftDunOJoNdbti+XjwWdjGmJZ
+ Bn4GelsrrpwJFvfDmouHUe8GsD7nTgbZFtiJbKfCEiK16N0Q0d0ZFHhAV2nPjsk2
+ 3JhG4n9vxATBkO82f7RLrcrhkx9cbLfN3wIDAQAB
+ -----END RSA PUBLIC KEY-----
+ '';
+ };
+ };
+ };
+ nomic = {
+ cores = 2;
+ dc = "tv"; #dc = "gg23";
+ nets = rec {
+ retiolum = {
+ addrs4 = ["10.243.0.110"];
+ addrs6 = ["42:02d5:733f:d6da:c0f5:2bb7:2b18:09ec"];
+ aliases = [
+ "nomic.retiolum"
+ "cgit.nomic.retiolum"
+ ];
+ tinc.pubkey = ''
+ -----BEGIN RSA PUBLIC KEY-----
+ MIIBCgKCAQEAwb8Yk/YRc17g2J9n960p6j4W/l559OPyuMPdGJ4DmCm3WNQtxoa+
+ qTFUiDiI85BcmfqnSeddLG8zTC2XnSlIvCRMJ9oKzppFM4PX4OTAaJZVE5WyCQhw
+ Kd4tHVdoQgJW5yFepmT9IUmHqkxXJ0R2W93l2eSZNOcnFvFn0ooiAlRi4zAiHClu
+ 5Mz80Sc2rvez+n9wtC2D06aYjP23pHYld2xighHR9SUqX1dFzgSXNSoWWCcgNp2a
+ OKcM8LzxLV7MTMZFOJCJndZ77e4LsUvxhQFP6nyKZWg30PC0zufZsuN5o2xsWSlA
+ Wi9sMB1AUR6mZrxgcgTFpUjbjbLQf+36CwIDAQAB
+ -----END RSA PUBLIC KEY-----
+ '';
+ };
+ };
+ secure = true;
+ };
+ rmdir = {
+ cores = 1;
+ dc = "tv"; #dc = "cac";
+ nets = rec {
+ internet = {
+ addrs4 = ["167.88.44.94"];
+ aliases = [
+ "rmdir.internet"
+ ];
+ };
+ retiolum = {
+ via = internet;
+ addrs4 = ["10.243.113.224"];
+ addrs6 = ["42:4522:25f8:36bb:8ccb:0150:231a:2af5"];
+ aliases = [
+ "rmdir.retiolum"
+ "cgit.rmdir.retiolum"
+ ];
+ tinc.pubkey = ''
+ -----BEGIN RSA PUBLIC KEY-----
+ MIIBCgKCAQEA+twy4obSbJdmZLfBoe9YYeyoDnXkO/WPa2D6Eh6jXrWk5fbhBjRf
+ i3EAQfLiXXFJX3E8V8YvJyazXklI19jJtCLDiu/F5kgJJfyAkWHH+a/hcg7qllDM
+ Xx2CvS/nCbs+p48/VLO6zLC7b1oHu3K/ob5M5bwPK6j9NEDIL5qYiM5PQzV6zryz
+ hS9E/+l8Z+UUpYcfS3bRovXJAerB4txc/gD3Xmptq1zk53yn1kJFYfVlwyyz+NEF
+ 59JZj2PDrvWoG0kx/QjiNurs6XfdnyHe/gP3rmSTrihKFVuA3cZM62sDR4FcaeWH
+ SnKSp02pqjBOjC/dOK97nXpKLJgNH046owIDAQAB
+ -----END RSA PUBLIC KEY-----
+ '';
+ };
+ };
+ };
+ wu = {
+ cores = 4;
+ # TODO wu is mobile, so dc means "home data center"
+ dc = "tv"; #dc = "gg23";
+ nets = {
+ retiolum = {
+ addrs4 = ["10.243.13.37"];
+ addrs6 = ["42:0:0:0:0:0:0:1337"];
+ aliases = [
+ "wu.retiolum"
+ ];
+ tinc.pubkey = ''
+ -----BEGIN RSA PUBLIC KEY-----
+ MIIBCgKCAQEArDvU0cuBsVqTjCX2TlWL4XHSy4qSjUhjrDvUPZSKTVN7x6OENCUn
+ M27g9H7j4/Jw/8IHoJLiKnXHavOoc9UJM+P9Fla/4TTVADr69UDSnLgH+wGiHcEg
+ GxPkb2jt0Z8zcpD6Fusj1ATs3sssaLHTHvg1D0LylEWA3cI4WPP13v23PkyUENQT
+ KpSWfR+obqDl38Q7LuFi6dH9ruyvqK+4syddrBwjPXrcNxcGL9QbDn7+foRNiWw4
+ 4CE5z25oGG2iWMShI7fe3ji/fMUAl7DSOOrHVVG9eMtpzy+uI8veOHrdTax4oKik
+ AFGCrMIov3F0GIeu3nDlrTIZPZDTodbFKQIDAQAB
+ -----END RSA PUBLIC KEY-----
+ '';
+ };
+ };
+ secure = true;
+ };
+ };
+ users = addNames {
+ mv = {
+ mail = "mv@cd.retiolum";
+ pubkey = readFile ../../Zpubkeys/mv_vod.ssh.pub;
+ };
+ tv = {
+ mail = "tv@wu.retiolum";
+ pubkey = readFile ../../Zpubkeys/tv_wu.ssh.pub;
+ };
+ };
+ };
+
+in
+out
diff --git a/3modules/krebs/git.nix b/3modules/krebs/git.nix
new file mode 100644
index 000000000..604645189
--- /dev/null
+++ b/3modules/krebs/git.nix
@@ -0,0 +1,490 @@
+{ 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 import ../../4lib/krebs { inherit 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.
+ Cgit is an attempt to create a fast web interface for the git version
+ control system, using a built in cache to decrease pressure on the
+ git server.
+ cgit in this module is being served via fastcgi nginx.This module
+ deploys a http://cgit.<hostname> nginx configuration and enables nginx
+ if not yet enabled.
+ '';
+ };
+ 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;
+ default = {};
+ 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;
+ };
+ };
+
+ 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)
+ config.krebs.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 000000000..c3b56ef94
--- /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 000000000..702e8a7f6
--- /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/retiolum.nix b/3modules/krebs/retiolum.nix
new file mode 100644
index 000000000..481d6565c
--- /dev/null
+++ b/3modules/krebs/retiolum.nix
@@ -0,0 +1,226 @@
+{ config, pkgs, lib, ... }:
+
+with builtins;
+with lib;
+let
+ cfg = config.krebs.retiolum;
+
+ out = {
+ options.krebs.retiolum = api;
+ config = mkIf cfg.enable imp;
+ };
+
+ api = {
+ enable = mkEnableOption "krebs.retiolum";
+
+ name = mkOption {
+ type = types.str;
+ default = config.networking.hostName;
+ # Description stolen from tinc.conf(5).
+ description = ''
+ This is the name which identifies this tinc daemon. It must
+ be unique for the virtual private network this daemon will
+ connect to. The Name may only consist of alphanumeric and
+ underscore characters. If Name starts with a $, then the
+ contents of the environment variable that follows will be
+ used. In that case, invalid characters will be converted to
+ underscores. If Name is $HOST, but no such environment
+ variable exist, the hostname will be read using the
+ gethostnname() system call This is the name which identifies
+ the this tinc daemon.
+ '';
+ };
+
+ generateEtcHosts = mkOption {
+ type = types.str;
+ default = "both";
+ description = ''
+ If set to <literal>short</literal>, <literal>long</literal>, or <literal>both</literal>,
+ then generate entries in <filename>/etc/hosts</filename> from subnets.
+ '';
+ };
+
+ network = mkOption {
+ type = types.str;
+ default = "retiolum";
+ description = ''
+ The tinc n