diff --git a/modules/list.nix b/modules/list.nix index 9c60b6a..7d7df3a 100644 --- a/modules/list.nix +++ b/modules/list.nix @@ -50,4 +50,6 @@ ./services/dnsmasq.nix ./services/attic.nix ./services/ntfy-sh.nix + + ./nixos ] diff --git a/modules/nixos/assertions.nix b/modules/nixos/assertions.nix new file mode 100644 index 0000000..f757b67 --- /dev/null +++ b/modules/nixos/assertions.nix @@ -0,0 +1,6 @@ +{ config, lib, ... }: +{ + options.nixos = lib.mkOption { type = lib.types.submodule { imports = [ ../assertions.nix ]; }; }; + + config.assertions = config.nixos.assertions; +} diff --git a/modules/nixos/default.nix b/modules/nixos/default.nix new file mode 100644 index 0000000..bcf8ed0 --- /dev/null +++ b/modules/nixos/default.nix @@ -0,0 +1,24 @@ +{ lib, pkgs, ... }: +{ + imports = [ + ./systemd.nix + ./nginx.nix + ./users.nix + ./postgresql.nix + ./assertions.nix + ./oauth2-proxy.nix + ./nix.nix + ./meta.nix + ./networking.nix + ./buildbot.nix + ]; + + options.nixos = lib.mkOption { + type = lib.types.submodule { + _module.args = { + inherit pkgs; + }; + }; + default = { }; + }; +} diff --git a/modules/nixos/meta.nix b/modules/nixos/meta.nix new file mode 100644 index 0000000..603a78d --- /dev/null +++ b/modules/nixos/meta.nix @@ -0,0 +1,6 @@ +{ lib, ... }: +{ + options = { + nixos.meta = lib.mkOption { type = lib.types.unspecified; }; + }; +} diff --git a/modules/nixos/networking.nix b/modules/nixos/networking.nix new file mode 100644 index 0000000..d80432d --- /dev/null +++ b/modules/nixos/networking.nix @@ -0,0 +1,10 @@ +{ lib, config, ... }: +{ + options = { + nixos.networking.hostName = lib.mkOption { type = lib.types.str; }; + }; + + config = { + nixos.networking.hostName = "buildbot"; + }; +} diff --git a/modules/nixos/nginx.nix b/modules/nixos/nginx.nix new file mode 100644 index 0000000..5cb16f3 --- /dev/null +++ b/modules/nixos/nginx.nix @@ -0,0 +1,196 @@ +{ + lib, + config, + pkgs, + options, + ... +}: +let + cfg = config.nixos.services.nginx; + + nixosOptions = options.nixos.type.getSubOptions ["nixos"]; + + evalSubmoduleOption = path: options: + let + option = lib.getAttrFromPath path options; + in + lib.evalModules { + modules = option.type.getSubModules ++ option.definitions; + inherit + (option.type.functor.payload) + class + specialArgs + ; + }; + + extractWithPriority = options: submodulePath: optionPath: + let + submoduleOptions = evalSubmoduleOption submodulePath options; + option = lib.getAttrFromPath optionPath submoduleOptions.options; + in + lib.mkOverride (option.highestPrio) (option.value); + + recommendedProxyConfig = { + proxy_set_header = [ + ["Host" "$$host"] + ["X-Real-IP" "$$remote_addr"] + ["X-Forwarded-For" "$$proxy_add_x_forwarded_for"] + ["X-Forwarded-Proto" "$$scheme"] + ["X-Forwarded-Host" "$$host"] + ["X-Forwarded-Server" "$$host"] + ]; + }; +in +{ + options = { + nixos.services.nginx = { + enable = lib.mkOption { + type = lib.types.bool; + default = false; + }; + + recommendedProxySettings = lib.mkOption { + type = lib.types.bool; + default = true; + }; + + proxyTimeout = lib.mkOption { + type = lib.types.str; + default = "60s"; + example = "20s"; + description = '' + Change the proxy related timeouts in recommendedProxySettings. + ''; + }; + + virtualHosts = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule { + options = { + locations = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule { + options.proxyPass = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + }; + + options.proxyWebsockets = lib.mkOption { + type = lib.types.bool; + default = false; + }; + + options.extraConfig = lib.mkOption { + type = lib.types.lines; + default = ""; + }; + } + ); + default = { }; + }; + + forceSSL = lib.mkOption { + type = lib.types.bool; + default = false; + }; + + addSSL = lib.mkOption { + type = lib.types.bool; + default = false; + }; + + useHTTPS = lib.mkOption { + type = lib.types.bool; + default = false; + }; + }; + } + ); + default = { }; + }; + }; + }; + + config = { + services.nginx = { + enable = extractWithPriority options [ "nixos" ] [ "services" "nginx" "enable" ]; + envsubst = extractWithPriority options [ "nixos" ] [ "services" "nginx" "enable" ]; + configuration = lib.mkIf config.nixos.services.nginx.enable (lib.singleton { + daemon = "off"; + worker_processes = 8; + user = "nginx"; + + events."" = { + use = "epoll"; + worker_connections = 512; + }; + + error_log = [ + "/dev/stderr" + "warn" + ]; + + pid = "/nginx.pid"; + + http."" = + [ + { + server_tokens = "off"; + include = [ [ "${pkgs.nginx}/conf/mime.types" ] ]; + charset = "utf-8"; + access_log = [ + "/dev/stdout" + "combined" + ]; + + # $connection_upgrade is used for websocket proxying + map."$$http_upgrade $$connection_upgrade" = { + default = "upgrade"; + "''" = "close"; + }; + } + ] + ++ (lib.optionals cfg.recommendedProxySettings [ + { + proxy_redirect = "off"; + proxy_connect_timeout = cfg.proxyTimeout; + proxy_send_timeout = cfg.proxyTimeout; + proxy_read_timeout = cfg.proxyTimeout; + proxy_http_version = "1.1"; + # don't let clients close the keep-alive connection to upstream. See the nginx blog for details: + # https://www.nginx.com/blog/avoiding-top-10-nginx-configuration-mistakes/#no-keepalives + proxy_set_header = ["Connection" "''"]; + } + recommendedProxyConfig + ]) + ++ (lib.flip lib.mapAttrsToList cfg.virtualHosts ( + server_name: server: { + server."" = { + listen = [ + "80" + "http2" + ]; + inherit server_name; + + location = lib.flip lib.mapAttrs server.locations ( + location: settings: [ + (lib.optionalAttrs (settings.proxyPass != null && cfg.recommendedProxySettings) + recommendedProxyConfig) + (lib.optionalAttrs settings.proxyWebsockets { + proxy_http_version = "1.1"; + proxy_set_header = [ + [ "Upgrade""$$http_upgrade" ] + [ "Connection" "$$connection_upgrade" ] + ]; + }) + settings.extraConfig + (lib.optionalAttrs (settings.proxyPass != null) { proxy_pass = settings.proxyPass; }) + ] + ); + }; + } + )); + }); + }; + }; +} diff --git a/modules/nixos/nix.nix b/modules/nixos/nix.nix new file mode 100644 index 0000000..be10a05 --- /dev/null +++ b/modules/nixos/nix.nix @@ -0,0 +1,15 @@ +{ lib, config, ... }: +{ + options = { + nixos.nix = { + settings = lib.mkOption { + type = lib.types.unspecified; + default = { }; + }; + }; + }; + + config = { + nix.config = config.nixos.nix.settings; + }; +} diff --git a/modules/nixos/oauth2-proxy.nix b/modules/nixos/oauth2-proxy.nix new file mode 100644 index 0000000..708c27f --- /dev/null +++ b/modules/nixos/oauth2-proxy.nix @@ -0,0 +1,9 @@ +{ lib, ... }: +{ + options = { + nixos.services.oauth2-proxy = lib.mkOption { + type = lib.types.unspecified; + default = { }; + }; + }; +} diff --git a/modules/nixos/postgresql.nix b/modules/nixos/postgresql.nix new file mode 100644 index 0000000..db9ae65 --- /dev/null +++ b/modules/nixos/postgresql.nix @@ -0,0 +1,13 @@ +{ lib, config, ... }: +{ + options = { + nixos.services.postgresql = lib.mkOption { + type = lib.types.unspecified; + default = { }; + }; + }; + + config = { + services.postgresql = config.nixos.services.postgresql; + }; +} diff --git a/modules/nixos/systemd.nix b/modules/nixos/systemd.nix new file mode 100644 index 0000000..9aadc1d --- /dev/null +++ b/modules/nixos/systemd.nix @@ -0,0 +1,196 @@ +{ + lib, + nglib, + config, + pkgs, + ... +}: +{ + options = { + nixos.systemd.services = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule ( + { name, ... }: + { + options = { + description = lib.mkOption { + type = lib.types.str; + default = name; + }; + + after = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + }; + + wants = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + }; + + wantedBy = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + }; + + requires = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + }; + + requiredBy = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + }; + + environment = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + }; + + path = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = { }; + }; + + preStart = lib.mkOption { + type = lib.types.lines; + default = ""; + }; + + reloadIfChanged = lib.mkOption { type = lib.types.bool; }; + + serviceConfig = lib.mkOption { + type = lib.types.submodule { + options = { + Type = lib.mkOption { type = lib.types.str; }; + + ExecStart = lib.mkOption { + type = lib.types.either (lib.types.listOf lib.types.str) lib.types.str; + apply = x: lib.toList x; + }; + + ExecReload = lib.mkOption { + type = lib.types.either (lib.types.listOf lib.types.str) lib.types.str; + apply = x: lib.toList x; + }; + + User = lib.mkOption { type = lib.types.str; }; + + Group = lib.mkOption { type = lib.types.str; }; + + WorkingDirectory = lib.mkOption { type = lib.types.path; }; + + LoadCredential = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + }; + + RuntimeDirectory = lib.mkOption { type = lib.types.str; }; + + Environment = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + }; + + OOMPolicy = lib.mkOption { + type = lib.types.str; + default = ""; + }; + }; + }; + }; + }; + } + ) + ); + default = { }; + }; + + nixos.systemd.tmpfiles = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + }; + }; + + config = { + init.services = ( + lib.flip lib.mapAttrs config.nixos.systemd.services ( + n: v: + let + combinedDeps = lib.unique (v.after ++ v.wants ++ v.requires); + withCredentials = v.serviceConfig.LoadCredential != [ ]; + credentialsDirectory = "/run/credentials/${n}"; + in + { + dependencies = lib.flip lib.map combinedDeps ( + e: + lib.pipe e [ + (lib.strings.removeSuffix ".service") + (lib.strings.removeSuffix ".target") + ] + ); + enabled = lib.elem "multi-user.target" (v.wantedBy ++ v.requiredBy); + environment = lib.mkMerge [ + v.environment + { + PATH = lib.makeBinPath v.path; + CREDENTIALS_DIRECTORY = lib.mkIf withCredentials credentialsDirectory; + } + (lib.listToAttrs ( + lib.map ( + env: + let + parts = lib.splitString "=" env; + envKey = lib.elemAt parts 0; + envValue = lib.replaceStrings [ "%d" ] [ credentialsDirectory ] (lib.elemAt parts 1); + in + assert (lib.assertMsg (lib.length parts == 2) "\"${env}\" is not of the format \"KEY=VALUE\""); + lib.nameValuePair envKey envValue + ) v.serviceConfig.Environment + )) + ]; + execStartPre = pkgs.writeShellScript "${n}-start-pre" '' + ${nglib.mergeShellFragmentsIsolated ( + lib.map (x: { + name = x; + data = '' + umask 077 + set -euo pipefail + _source="$(cut -f 2 -d ':'<<<"${x}")" + _dest="${credentialsDirectory}/$(cut -f 1 -d ':'<<<"${x}")" + + if ! [ -e "$_source" ] ; then + printf "Credential $_source for service ${n} not found.\n" + exit 1 + fi + + cp "$_source" "$_dest" + chown ${v.serviceConfig.User}:${v.serviceConfig.Group} "$_dest" + ''; + }) v.serviceConfig.LoadCredential + )} + + if ! [ "$_status" = "0" ] ; then + exit "$_status" + fi + + ${v.preStart} + ''; + execStart = pkgs.writeShellScript "${n}-start" ( + lib.concatStringsSep "\n" v.serviceConfig.ExecStart + ); + group = v.serviceConfig.Group; + user = v.serviceConfig.User; + workingDirectory = v.serviceConfig.WorkingDirectory; + tmpfiles = + with nglib.nottmpfiles.dsl; + lib.optionals withCredentials [ + (d credentialsDirectory "0700" config.init.services.${n}.user config.init.services.${n}.group _ _) + (R credentialsDirectory "0700" config.init.services.${n}.user config.init.services.${n}.group _ _) + ]; + } + ) + ); + }; +} diff --git a/modules/nixos/users.nix b/modules/nixos/users.nix new file mode 100644 index 0000000..adebc0c --- /dev/null +++ b/modules/nixos/users.nix @@ -0,0 +1,50 @@ +{ lib, config, ... }: +{ + options = { + nixos.users.users = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule ({name, ...}: { + options = { + description = lib.mkOption { type = lib.types.str; }; + createHome = lib.mkOption { type = lib.types.bool; }; + home = lib.mkOption { type = lib.types.path; }; + group = lib.mkOption { type = lib.types.str; }; + extraGroups = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + }; + useDefaultShell = lib.mkOption { type = lib.types.bool; }; + isSystemUser = lib.mkOption { + type = lib.types.bool; + default = true; + }; + isNormalUser = lib.mkOption { + type = lib.types.bool; + default = false; + }; + name = lib.mkOption { + type = lib.types.str; + default = name; + readOnly = true; + }; + }; + }) + ); + default = { }; + }; + + nixos.users.groups = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule { }); + default = { }; + }; + + }; + + config = { + users.users = + (lib.mapAttrs ( + _: v: lib.filterAttrs (n: _: !lib.elem n [ "name" "isSystemUser" ]) v + ) config.nixos.users.users); + users.groups = config.nixos.users.groups; + }; +} diff --git a/modules/services/postgresql.nix b/modules/services/postgresql.nix index abd2a76..4e48ffa 100644 --- a/modules/services/postgresql.nix +++ b/modules/services/postgresql.nix @@ -105,7 +105,10 @@ in initdbArgs = lib.mkOption { type = with lib.types; listOf str; - default = [ ]; + default = [ + "-E" + "UTF8" + ]; example = [ "--data-checksums" "--allow-group-access"