summary refs log tree commit diff stats
path: root/modules
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-08-02 22:39:02 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-08-02 23:13:29 +0200
commit30e649a6d43c4ef2473a1820930cbe7d43e28432 (patch)
treef34df66d41344a9289628d9c8f9e002614f97c16 /modules
parentbuild(flake): Update (diff)
downloadnixos-server-30e649a6d43c4ef2473a1820930cbe7d43e28432.tar.gz
nixos-server-30e649a6d43c4ef2473a1820930cbe7d43e28432.zip
refactor(nixos/{nginx, nix-sync}): Migrate from `system/services`
Nix-sync was sort-of mixed into the nginx configuration, thus separating
it completely seemed reasonable.
Diffstat (limited to 'modules')
-rw-r--r--modules/nixos/vhack/default.nix2
-rw-r--r--modules/nixos/vhack/nginx/default.nix68
-rw-r--r--modules/nixos/vhack/nginx/redirects.nix6
-rw-r--r--modules/nixos/vhack/nix-sync/default.nix61
-rw-r--r--modules/nixos/vhack/nix-sync/hosts.nix48
-rw-r--r--modules/nixos/vhack/nix-sync/module.nix299
6 files changed, 484 insertions, 0 deletions
diff --git a/modules/nixos/vhack/default.nix b/modules/nixos/vhack/default.nix
index 1c98f58..cb0131f 100644
--- a/modules/nixos/vhack/default.nix
+++ b/modules/nixos/vhack/default.nix
@@ -2,6 +2,8 @@
   imports = [
     ./etesync
     ./git-server
+    ./nginx
+    ./nix-sync
     ./peertube
   ];
 }
diff --git a/modules/nixos/vhack/nginx/default.nix b/modules/nixos/vhack/nginx/default.nix
new file mode 100644
index 0000000..6a82147
--- /dev/null
+++ b/modules/nixos/vhack/nginx/default.nix
@@ -0,0 +1,68 @@
+{
+  lib,
+  config,
+  ...
+}: let
+  importedRedirects = import ./redirects.nix {};
+  mkRedirect = {
+    key,
+    value,
+  }: {
+    name = key;
+    value = {
+      forceSSL = true;
+      enableACME = true;
+      locations."/".return = "301 ${value}";
+    };
+  };
+
+  redirects = builtins.listToAttrs (builtins.map mkRedirect importedRedirects);
+
+  cfg = config.vhack.nginx;
+in {
+  options.vhack.nginx = {
+    enable = lib.mkEnableOption ''
+      a default nginx config.
+    '';
+
+    selfsign = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = ''
+        Whether to selfsign the acme certificates. This should only
+        really be useful for tests.
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    security.acme = {
+      acceptTerms = true;
+      defaults = {
+        email = "admin@vhack.eu";
+        webroot = "/var/lib/acme/acme-challenge";
+
+        # Avoid spamming the acme server, if we run in a test, and only really want self-signed
+        # certificates
+        server = lib.mkIf cfg.selfsign "https://127.0.0.1";
+      };
+    };
+
+    networking.firewall = {
+      allowedTCPPorts = [80 443];
+    };
+    services.nginx = {
+      enable = true;
+      # The merge here is fine, as no domain should be specified twice
+      virtualHosts =
+        {
+          "gallery.s-schoeffel.de" = {
+            forceSSL = true;
+            enableACME = true;
+            root = "/srv/gallery.s-schoeffel.de";
+          };
+        }
+        // redirects;
+    };
+  };
+}
diff --git a/modules/nixos/vhack/nginx/redirects.nix b/modules/nixos/vhack/nginx/redirects.nix
new file mode 100644
index 0000000..a021e72
--- /dev/null
+++ b/modules/nixos/vhack/nginx/redirects.nix
@@ -0,0 +1,6 @@
+{...}: [
+  {
+    key = "source.vhack.eu";
+    value = "https://codeberg.org/vhack.eu/nixos-server";
+  }
+]
diff --git a/modules/nixos/vhack/nix-sync/default.nix b/modules/nixos/vhack/nix-sync/default.nix
new file mode 100644
index 0000000..a624e0e
--- /dev/null
+++ b/modules/nixos/vhack/nix-sync/default.nix
@@ -0,0 +1,61 @@
+{
+  config,
+  lib,
+  ...
+}: let
+  cfg = config.vhack.nix-sync;
+
+  mkNixSyncRepository = {
+    domain,
+    root ? "",
+    url,
+    extraSettings ? {},
+  }: {
+    name = "${domain}";
+    value = {
+      path = "/etc/nginx/websites/${domain}/${root}";
+      uri = "${url}";
+      inherit extraSettings;
+    };
+  };
+  nixSyncRepositories = builtins.listToAttrs (builtins.map mkNixSyncRepository domains);
+
+  mkVirtHost = {
+    domain,
+    root ? "",
+    url,
+    extraSettings ? {},
+  }: {
+    name = "${domain}";
+    value =
+      lib.recursiveUpdate {
+        forceSSL = true;
+        enableACME = true;
+        root = "/etc/nginx/websites/${domain}/${root}";
+      }
+      extraSettings;
+  };
+  virtHosts = builtins.listToAttrs (builtins.map mkVirtHost domains);
+
+  domains = import ./hosts.nix {};
+in {
+  imports = [
+    ./module.nix
+  ];
+
+  options.vhack.nix-sync = {
+    enable = lib.mkEnableOption ''
+      a website git ops solution.
+    '';
+  };
+
+  config = lib.mkIf cfg.enable {
+    services.nix-sync = {
+      enable = true;
+      repositories = nixSyncRepositories;
+    };
+
+    vhack.nginx.enable = true;
+    services.nginx.virtualHosts = virtHosts;
+  };
+}
diff --git a/modules/nixos/vhack/nix-sync/hosts.nix b/modules/nixos/vhack/nix-sync/hosts.nix
new file mode 100644
index 0000000..98dbbf1
--- /dev/null
+++ b/modules/nixos/vhack/nix-sync/hosts.nix
@@ -0,0 +1,48 @@
+{...}: let
+  extraWkdSettings = {
+    locations."/.well-known/openpgpkey/hu/".extraConfig = ''
+      default_type application/octet-stream;
+
+      # Came from: https://www.uriports.com/blog/setting-up-openpgp-web-key-directory/
+      # No idea if it is actually necessary
+      # add_header Access-Control-Allow-Origin * always;
+    '';
+  };
+in [
+  {
+    domain = "vhack.eu";
+    url = "https://codeberg.org/vhack.eu/website.git";
+  }
+  {
+    domain = "b-peetz.de";
+    url = "https://codeberg.org/bpeetz/b-peetz.de.git";
+  }
+
+  # Trinitrix
+  {
+    domain = "trinitrix.vhack.eu";
+    url = "https://codeberg.org/trinitrix/website.git";
+  }
+
+  # WKD
+  {
+    domain = "openpgpkey.b-peetz.de";
+    url = "https://codeberg.org/vhack.eu/gpg_wkd.git";
+    extraSettings = extraWkdSettings;
+  }
+  {
+    domain = "openpgpkey.s-schoeffel.de";
+    url = "https://codeberg.org/vhack.eu/gpg_wkd.git";
+    extraSettings = extraWkdSettings;
+  }
+  {
+    domain = "openpgpkey.sils.li";
+    url = "https://codeberg.org/vhack.eu/gpg_wkd.git";
+    extraSettings = extraWkdSettings;
+  }
+  {
+    domain = "openpgpkey.vhack.eu";
+    url = "https://codeberg.org/vhack.eu/gpg_wkd.git";
+    extraSettings = extraWkdSettings;
+  }
+]
diff --git a/modules/nixos/vhack/nix-sync/module.nix b/modules/nixos/vhack/nix-sync/module.nix
new file mode 100644
index 0000000..a3ab0af
--- /dev/null
+++ b/modules/nixos/vhack/nix-sync/module.nix
@@ -0,0 +1,299 @@
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}: let
+  cfg = config.services.nix-sync;
+  esa = lib.strings.escapeShellArg;
+
+  mkTimer = name: repo: {
+    description = "Nix sync ${name} timer";
+    wantedBy = ["timers.target"];
+    timerConfig = {
+      OnUnitActiveSec = repo.interval;
+    };
+    wants = ["network-online.target"];
+    after = ["network-online.target"];
+  };
+
+  parents = path: let
+    split_path = builtins.split "/" path;
+    filename = builtins.elemAt split_path (builtins.length split_path - 1);
+    path_build =
+      lib.strings.removeSuffix "/" (builtins.replaceStrings [filename] [""] path);
+    final_path =
+      if filename == ""
+      then parents path_build
+      else path_build;
+  in
+    final_path;
+
+  mkUnit = name: repo: let
+    optionalPathSeparator =
+      if lib.strings.hasPrefix "/" repo.path
+      then ""
+      else "/";
+    /*
+    * `ln` tries to create a symlink in the directory, if the target ends with a '/',
+    * thus remove it.
+    */
+    repoPath = lib.strings.removeSuffix "/" repo.path;
+
+    repoCachePath = cfg.cachePath + optionalPathSeparator + repo.path;
+    execStartScript = pkgs.writeScript "nix-sync-exec" ''
+      #! /usr/bin/env dash
+      export XDG_CACHE_HOME="$CACHE_DIRECTORY";
+      cd ${esa repoCachePath};
+
+      git fetch
+      origin="$(git rev-parse @{u})";
+      branch="$(git rev-parse @)";
+
+      if ! [ "$origin" = "$branch" ]; then
+        git pull --rebase;
+
+        out_paths=$(mktemp);
+        nix build . --print-out-paths --experimental-features 'nix-command flakes' > "$out_paths";
+        [ "$(wc -l < "$out_paths")" -gt 1 ] && (echo "To many out-paths"; exit 1)
+        out_path="$(cat "$out_paths")";
+        rm ${esa repoPath};
+        ln -s "$out_path" ${esa repoPath};
+        rm "$out_paths";
+      fi
+    '';
+    execStartPreScript = ''
+      export XDG_CACHE_HOME="$CACHE_DIRECTORY";
+
+      if ! [ -d ${esa repoCachePath}/.git ]; then
+          mkdir --parents ${esa repoCachePath};
+          git clone ${esa repo.uri} ${esa repoCachePath};
+
+          out_paths=$(mktemp);
+          nix build ${esa repoCachePath} --print-out-paths --experimental-features 'nix-command flakes' > "$out_paths";
+          [ "$(wc -l < "$out_paths")" -gt 1 ] && (echo "To many out-paths"; exit 1)
+          out_path="$(cat "$out_paths")";
+          ln -s "$out_path" ${esa repoPath};
+          rm "$out_paths";
+      fi
+
+      if ! [ -L ${esa repoPath} ]; then
+        cd ${esa repoCachePath};
+
+        git pull --rebase;
+
+        out_paths=$(mktemp);
+        nix build . --print-out-paths --experimental-features 'nix-command flakes' > "$out_paths";
+        [ "$(wc -l < "$out_paths")" -gt 1 ] && { echo "To many out-paths"; exit 1; }
+        out_path="$(cat "$out_paths")";
+
+        if [ -d ${esa repoPath} ]; then
+          rm -d ${esa repoPath};
+        else
+          mkdir --parents "$(dirname ${esa repoPath})";
+        fi
+        [ -e ${esa repoPath} ] && rm ${esa repoPath};
+
+        ln -s "$out_path" ${esa repoPath};
+        rm "$out_paths";
+      fi
+    '';
+  in {
+    description = "Nix Sync ${name}";
+    wantedBy = ["default.target"];
+    after = ["network.target"];
+    path = with pkgs; [openssh git nix mktemp coreutils dash];
+    preStart = execStartPreScript;
+
+    serviceConfig = {
+      TimeoutSec = 0;
+      ExecStart = execStartScript;
+      Restart = "on-abort";
+      # User and group
+      User = cfg.user;
+      Group = cfg.group;
+      # Runtime directory and mode
+      RuntimeDirectory = "nix-sync";
+      RuntimeDirectoryMode = "0750";
+      # Cache directory and mode
+      CacheDirectory = "nix-sync";
+      CacheDirectoryMode = "0750";
+      # Logs directory and mode
+      LogsDirectory = "nix-sync";
+      LogsDirectoryMode = "0750";
+      # Proc filesystem
+      ProcSubset = "all";
+      ProtectProc = "invisible";
+      # New file permissions
+      UMask = "0027"; # 0640 / 0750
+      # Capabilities
+      AmbientCapabilities = ["CAP_CHOWN"];
+      CapabilityBoundingSet = ["CAP_CHOWN"];
+      # Security
+      NoNewPrivileges = true;
+      # Sandboxing (sorted by occurrence in https://www.freedesktop.org/software/systemd/man/systemd.exec.html)
+      ReadWritePaths = ["${esa (parents repo.path)}" "-${esa (parents repoCachePath)}" "-${esa cfg.cachePath}"];
+      ReadOnlyPaths = ["/nix"]; # TODO: Should be irrelevant, as we have ProtectSystem=Strict <2024-06-01>
+      ProtectSystem = "strict";
+      ProtectHome = true;
+      PrivateTmp = true;
+      PrivateDevices = true;
+      ProtectHostname = true;
+      ProtectClock = true;
+      ProtectKernelTunables = true;
+      ProtectKernelModules = true;
+      ProtectKernelLogs = true;
+      ProtectControlGroups = true;
+      RestrictAddressFamilies = ["AF_UNIX" "AF_INET" "AF_INET6"];
+      RestrictNamespaces = true;
+      LockPersonality = true;
+      MemoryDenyWriteExecute = true;
+      RestrictRealtime = true;
+      RestrictSUIDSGID = true;
+      RemoveIPC = true;
+      PrivateMounts = true;
+      # System Call Filtering
+      SystemCallArchitectures = "native";
+      SystemCallFilter = ["~@cpu-emulation @debug @keyring @mount @obsolete @privileged @setuid"];
+    };
+  };
+
+  services =
+    lib.mapAttrs' (name: repo: {
+      name = "nix-sync-${name}";
+      value = mkUnit name repo;
+    })
+    cfg.repositories;
+  timers =
+    lib.mapAttrs' (name: repo: {
+      name = "nix-sync-${name}";
+      value = mkTimer name repo;
+    })
+    cfg.repositories;
+
+  # generate the websites directory, so systemd can mount it read write
+  generatedDirectories =
+    lib.mapAttrsToList (
+      _: repo: "d ${esa (parents repo.path)} 0755 ${cfg.user} ${cfg.group}"
+    )
+    cfg.repositories;
+
+  repositoryType = lib.types.submodule ({name, ...}: {
+    options = {
+      name = lib.mkOption {
+        internal = true;
+        default = name;
+        type = lib.types.str;
+        description = "The name that should be given to this unit.";
+      };
+
+      path = lib.mkOption {
+        type = lib.types.str;
+        description = "The path at which to sync the repository";
+      };
+
+      uri = lib.mkOption {
+        type = lib.types.str;
+        example = "ssh://user@example.com:/~[user]/path/to/repo.git";
+        description = ''
+          The URI of the remote to be synchronized. This is only used in the
+          event that the directory does not already exist. See
+          <link xlink:href="https://git-scm.com/docs/git-clone#_git_urls"/>
+          for the supported URIs.
+        '';
+      };
+
+      extraSettings = lib.mkOption {
+        type = lib.types.attrsOf lib.types.anything;
+        example = lib.literalExpression ''
+          {
+            locations."/.well-known/openpgpkey/hu/" = {
+              extraConfig = \'\'
+                  default_type application/octet-stream;
+
+                  add_header Access-Control-Allow-Origin * always;
+                \'\';
+            };
+          }
+        '';
+        description = ''
+          Extra config to add the the nginx virtual host.
+        '';
+      };
+
+      interval = lib.mkOption {
+        type = lib.types.int;
+        default = 500;
+        description = ''
+          The interval, specified in seconds, at which the synchronization will
+          be triggered.
+        '';
+      };
+    };
+  });
+in {
+  options = {
+    services.nix-sync = {
+      enable = lib.mkEnableOption "nix-sync services";
+
+      user = lib.mkOption {
+        type = lib.types.str;
+        default = "nix-sync";
+        description = lib.mdDoc "User account under which nix-sync units runs.";
+      };
+
+      group = lib.mkOption {
+        type = lib.types.str;
+        default = "nix-sync";
+        description = lib.mdDoc "Group account under which nix-sync units runs.";
+      };
+
+      cachePath = lib.mkOption {
+        type = lib.types.str;
+        default = "/var/lib/nix-sync";
+        description = lib.mdDoc ''
+          Where to cache git directories. Should not end with a slash ("/")
+        '';
+      };
+
+      repositories = lib.mkOption {
+        type = with lib.types; attrsOf repositoryType;
+        description = ''
+          The repositories that should be synchronized.
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = !lib.strings.hasSuffix "/" cfg.cachePath;
+        message = "Your cachePath ('${cfg.cachePath}') ends with a slash ('/'), please use: '${lib.strings.removeSuffix "/" cfg.cachePath}'.";
+      }
+    ];
+    systemd = {
+      tmpfiles.rules =
+        generatedDirectories;
+
+      inherit services timers;
+    };
+    users.users =
+      if cfg.user == "nix-sync"
+      then {
+        nix-sync = {
+          group = "${cfg.group}";
+          isSystemUser = true;
+        };
+      }
+      else lib.warnIf (cfg.user != "nix-sync") "The user (${cfg.user}) is not \"nix-sync\", thus you are responible for generating it.";
+    users.groups =
+      if cfg.group == "nix-sync"
+      then {
+        nix-sync = {
+          members = ["${cfg.user}"];
+        };
+      }
+      else lib.warnIf (cfg.group != "nix-sync") "The group (${cfg.group}) is not \"nix-sync\", thus you are responible for generating it.";
+  };
+}