commit 75405396d2d7dec4b06bbeee7741b760693aee9d Author: Dennis Wuitz Date: Sat Dec 23 06:49:01 2023 +0100 base configuration diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..eb4a986 --- /dev/null +++ b/flake.nix @@ -0,0 +1,43 @@ +{ + description = "NixOS configuration for Wavelens Servers"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + + sops-nix = { + url = "github:Mic92/sops-nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + nix-index-database = { + url = "github:Mic92/nix-index-database"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { nixpkgs, nix-index-database, sops-nix, ... }: { + src = builtins.filterSource (path: type: type == "directory" || lib.hasSuffix ".nix" (baseNameOf path)) ./.; + ls = dir: lib.attrNames (builtins.readDir (src + "/${dir}")); + fileList = dir: map (file: ./. + "/${dir}/${file}") (ls dir); + nixosConfigurations = let + constructSystem = { + hostname, + system ? "x86_64-linux", + modules ? [], + }: nixpkgs.lib.nixosSystem { + inherit system hostname; + modules = [ + sops-nix.nixosModules.sops + nix-index-database.nixosModules.nix-index + ./system/programs.nix + ./system/configuration.nix + ./system/${hostname}/configuration.nix + ] ++ fileList "modules" ++ modules; + }; + in { + photon = constructSystem { + hostname = "photon" + }; + }; + }; +} diff --git a/lib/ldap.nix b/lib/ldap.nix new file mode 100644 index 0000000..c954574 --- /dev/null +++ b/lib/ldap.nix @@ -0,0 +1,9 @@ +{ lib, ... }: + +{ + mkUserGroupOption = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = lib.mdDoc "Restrict logins to users in this group"; + }; +} diff --git a/lib/modules.nix b/lib/modules.nix new file mode 100644 index 0000000..b74fbcc --- /dev/null +++ b/lib/modules.nix @@ -0,0 +1,11 @@ +{ config, lib, ... }: + +{ + mkOpinionatedOption = text: lib.mkOption { + type = lib.types.bool; + default = config.opinionatedDefaults; + description = lib.mdDoc "Whether to ${text}."; + }; + + mkRecursiveDefault = lib.mapAttrsRecursive (_: lib.mkDefault); +} diff --git a/lib/nix.nix b/lib/nix.nix new file mode 100644 index 0000000..c7f3455 --- /dev/null +++ b/lib/nix.nix @@ -0,0 +1,8 @@ +{ lib, ... }: + +{ + # taken from https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/services/misc/nix-daemon.nix#L828-L832 + # a builder can run code for `gcc.arch` and inferior architectures + gcc-system-features = arch: [ "gccarch-${arch}" ] + ++ map (x: "gccarch-${x}") lib.systems.architectures.inferiors.${arch}; +} diff --git a/lib/ssh.nix b/lib/ssh.nix new file mode 100644 index 0000000..e13b5a2 --- /dev/null +++ b/lib/ssh.nix @@ -0,0 +1,10 @@ +_: + +{ + mkPubKey = name: type: publicKey: { + "${name}-${type}" = { + extraHostNames = [ name ]; + publicKey = "${type} ${publicKey}"; + }; + }; +} diff --git a/modules/acme.nix b/modules/acme.nix new file mode 100644 index 0000000..28cdbc2 --- /dev/null +++ b/modules/acme.nix @@ -0,0 +1,21 @@ +{ config, lib, ... }: + +let + cfg = config.security.acme; +in +{ + options.security.acme.staging = lib.mkOption { + type = lib.types.bool; + default = false; + description = lib.mdDoc '' + If set to true, use Let's Encrypt's staging environment instead of the production one. + The staging environment has much higher rate limits but does not generate fully signed certificates. + This is great for testing when the normla rate limit is hit fast and impacts other people on the same IP. + See https://letsencrypt.org/docs/staging-environment for more detail. + ''; + }; + + config = lib.mkIf cfg.staging { + security.acme.server = "https://acme-staging-v02.api.letsencrypt.org/directory"; + }; +} diff --git a/modules/boot.nix b/modules/boot.nix new file mode 100644 index 0000000..0b3d873 --- /dev/null +++ b/modules/boot.nix @@ -0,0 +1,41 @@ +{ config, lib, ... }: + +let + cfg = config.boot; +in +{ + options = { + boot = { + default = lib.mkOpinionatedOption "enable the boot builder"; + }; + }; + + cfg = lib.mkIf cfg.default { + supportedFilesystems = [ "zfs" ]; + tmp.useTmpfs = true; + kernelPackages = config.boot.zfs.package.latestCompatibleLinuxPackages; + kernelParams = [ "kvm-amd" "nordrand" ]; + zfs = { + enableUnstable = true; + devNodes = "/dev/disk/by-id/"; + forceImportRoot = true; + }; + loader = { + efi = { + canTouchEfiVariables = false; + efiSysMountPoint = "/boot/efis/nvme-Samsung_SSD_980_PRO_1TB_S5GXNF0W178262L-part1"; + }; + generationsDir.copyKernels = true; + grub = { + enable = true; + copyKernels = true; + zfsSupport = true; + efiSupport = true; + efiInstallAsRemovable = true; + fsIdentifier = "uuid"; + device = "nodev"; + extraInstallCommands = "[ ! -e /boot/efis/nvme-Samsung_SSD_980_PRO_1TB_S5GXNF0W178262L-part1/EFI ] || cp -r /boot/efis/nvme-Samsung_SSD_980_PRO_1TB_S5GXNF0W178262L-part1/EFI/* /boot/efis/nvme-Samsung_SSD_980_PRO_1TB_S5GXNF0W178262L-part1"; + }; + }; + }; +} \ No newline at end of file diff --git a/modules/containers.nix b/modules/containers.nix new file mode 100644 index 0000000..7f0d5f4 --- /dev/null +++ b/modules/containers.nix @@ -0,0 +1,56 @@ +{ config, lib, libS, ... }: + +let + cfg = config.virtualisation; + cfgd = cfg.docker; + cfgp = cfg.podman; +in +{ + options.virtualisation = { + docker = { + aggresiveAutoPrune = libS.mkOpinionatedOption "configure aggresive auto prune which removes everything unreferenced by running containers. This includes named volumes and mounts should be used instead"; + + recommendedDefaults = libS.mkOpinionatedOption "set recommended and maintenance reducing default settings"; + }; + + podman.recommendedDefaults = libS.mkOpinionatedOption "set recommended and maintenance reducing default settings"; + }; + + config = { + virtualisation = { + containers.registries.search = lib.mkIf cfgp.recommendedDefaults [ + "docker.io" + "quay.io" + "ghcr.io" + "gcr.io" + ]; + + docker = { + daemon.settings = let + useIPTables = !config.networking.nftables.enable; + in lib.mkIf cfgd.recommendedDefaults { + fixed-cidr-v6 = "fd00::/80"; # TODO: is this a good idea for all networks? + iptables = useIPTables; + ip6tables = useIPTables; + ipv6 = true; + # userland proxy is slow, does not give back ports and if iptables/nftables is avaible just worsefgd.aggresiveAutoPrune + userland-proxy = false; + }; + autoPrune = lib.mkIf cfgd.aggresiveAutoPrune { + enable = true; + flags = [ + "--all" + "--external" + "--force" + "--volumes" + ]; + }; + }; + + podman = { + autoPrune.enable = lib.mkIf cfgp.recommendedDefaults true; + defaultNetwork.settings.dns_enabled = lib.mkIf cfgp.recommendedDefaults true; + }; + }; + }; +} diff --git a/modules/default.nix b/modules/default.nix new file mode 100644 index 0000000..f86fad7 --- /dev/null +++ b/modules/default.nix @@ -0,0 +1,5 @@ +{ lib, ... }: + +{ + options.opinionatedDefaults = lib.mkEnableOption "opinionated defaults"; +} diff --git a/modules/gitea.nix b/modules/gitea.nix new file mode 100644 index 0000000..1f3722c --- /dev/null +++ b/modules/gitea.nix @@ -0,0 +1,147 @@ +{ config, lib, libS, ... }: + +let + cfg = config.services.gitea; + cfgl = cfg.ldap; + inherit (config.security) ldap; +in +{ + options = { + services.gitea = { + # based on https://github.com/majewsky/nixos-modules/blob/master/gitea.nix + ldap = { + enable = lib.mkEnableOption (lib.mdDoc "login via ldap"); + + adminGroup = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + example = "gitea-admins"; + description = lib.mdDoc "Name of the ldap group that grants admin access in gitea."; + }; + + bindPasswordFile = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + example = "/var/lib/secrets/bind-password"; + description = lib.mdDoc "Path to a file containing the bind password."; + }; + + userGroup = libS.ldap.mkUserGroupOption; + + options = + let + mkOptStr = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + }; + in + { + id = lib.mkOption { + type = lib.types.ints.unsigned; + default = 1; + }; + name = mkOptStr; + security-protocol = mkOptStr; + host = mkOptStr; + port = lib.mkOption { + type = with lib.types; nullOr port; + default = null; + }; + bind-dn = mkOptStr; + bind-password = mkOptStr; + user-search-base = mkOptStr; + user-filter = mkOptStr; + admin-filter = mkOptStr; + username-attribute = mkOptStr; + firstname-attribute = mkOptStr; + surname-attribute = mkOptStr; + email-attribute = mkOptStr; + public-ssh-key-attribute = mkOptStr; + }; + }; + recommendedDefaults = libS.mkOpinionatedOption "set recommended, secure default settings"; + }; + }; + + config.services.gitea = lib.mkIf (cfg.enable && cfgl.enable) { + ldap.options = { + name = "ldap"; + security-protocol = "LDAPS"; + host = ldap.domainName; + inherit (ldap) port; + bind-dn = ldap.bindDN; + bind-password = "$(cat ${cfgl.bindPasswordFile})"; + user-search-base = ldap.userBaseDN; + user-filter = ldap.searchFilterWithGroupFilter cfgl.userGroup (ldap.userFilter "%[1]s"); + admin-filter = ldap.groupFilter cfgl.adminGroup; + username-attribute = ldap.userField; + firstname-attribute = ldap.givenNameField; + surname-attribute = ldap.surnameField; + email-attribute = ldap.mailField; + public-ssh-key-attribute = ldap.sshPublicKeyField; + }; + settings = lib.mkIf cfg.recommendedDefaults (libS.modules.mkRecursiveDefault { + cors = { + ALLOW_DOMAIN = cfg.settings.server.DOMAIN; + ENABLED = true; + SCHEME = "https"; + }; + cron.ENABLED = true; + "cron.resync_all_sshkeys".ENABLED = true; + "cron.resync_all_hooks".ENABLED = true; + other.SHOW_FOOTER_VERSION = false; + repository.ACCESS_CONTROL_ALLOW_ORIGIN = cfg.settings.server.DOMAIN; + "repository.signing".DEFAULT_TRUST_MODEL = "committer"; + security.DISABLE_GIT_HOOKS = true; + server = { + ENABLE_GZIP = true; + ROOT_URL = "https://${cfg.settings.server.DOMAIN}/"; + SSH_SERVER_CIPHERS = "chacha20-poly1305@openssh.com, aes256-gcm@openssh.com, aes128-gcm@openssh.com"; + SSH_SERVER_KEY_EXCHANGES = "curve25519-sha256@libssh.org, ecdh-sha2-nistp521, ecdh-sha2-nistp384, ecdh-sha2-nistp256, diffie-hellman-group14-sha1"; + SSH_SERVER_MACS = "hmac-sha2-256-etm@openssh.com, hmac-sha2-256, hmac-sha1"; + }; + session = { + COOKIE_SECURE = true; + PROVIDER = "db"; + SAME_SITE = "strict"; + SESSION_LIFE_TIME = 28 * 86400; # 28 days + }; + "ssh.minimum_key_sizes" = { + ECDSA = -1; + RSA = 4095; + }; + time.DEFAULT_UI_LOCATION = config.time.timeZone; + }); + }; + + config.systemd.services = lib.mkIf (cfg.enable && cfgl.enable) { + gitea.preStart = + let + exe = lib.getExe cfg.package; + # allow executing shell after the --bind-password argument to e.g. cat a password file + formatOption = key: value: "--${key} ${if key == "bind-password" then value else lib.escapeShellArg value}"; + ldapOptionsStr = opt: lib.concatStringsSep " " (lib.mapAttrsToList formatOption opt); + commonArgs = "--attributes-in-bind --synchronize-users"; + in + lib.mkAfter '' + if ${exe} admin auth list | grep -q ${cfgl.options.name}; then + ${exe} admin auth update-ldap ${commonArgs} ${ldapOptionsStr cfgl.options} + else + ${exe} admin auth add-ldap ${commonArgs} ${ldapOptionsStr (lib.filterAttrs (name: _: name != "id") cfgl.options)} + fi + ''; + }; + + config.services.portunus.seedSettings.groups = [ + (lib.mkIf (cfgl.adminGroup != null) { + long_name = "Gitea Administrators"; + name = cfgl.adminGroup; + permissions = { }; + }) + (lib.mkIf (cfgl.userGroup != null) { + long_name = "Gitea Users"; + name = cfgl.userGroup; + permissions = { }; + }) + ]; +} diff --git a/modules/grafana.nix b/modules/grafana.nix new file mode 100644 index 0000000..6a22126 --- /dev/null +++ b/modules/grafana.nix @@ -0,0 +1,130 @@ +{ config, lib, libS, ... }: + +let + cfg = config.services.grafana; +in +{ + options = { + services.grafana = { + configureNginx = lib.mkOption { + type = lib.types.bool; + default = false; + description = lib.mdDoc "Wether to configure Nginx."; + }; + + oauth = { + enable = lib.mkEnableOption (lib.mdDoc ''login only via OAuth2''); + enableViewerRole = lib.mkOption { + type = lib.types.bool; + default = false; + description = lib.mdDoc "Wether to enable the fallback Viewer role when users do not have the user- or adminGroup."; + }; + adminGroup = libS.ldap.mkUserGroupOption; + userGroup = libS.ldap.mkUserGroupOption; + }; + + recommendedDefaults = libS.mkOpinionatedOption "set recommended and secure default settings"; + }; + }; + + config = { + # the default values are hardcoded instead of using options. because I couldn't figure out how to extract them from the freeform type + assertions = lib.mkIf cfg.enable [ + { + assertion = cfg.oauth.enable -> cfg.settings."auth.generic_oauth".client_secret != null; + message = '' + Setting services.grafana.oauth.enable to true requires to set services.grafana.settings."auth.generic_oauth".client_secret. + Use this `$__file{/path/to/some/secret}` syntax to reference secrets securely. + ''; + } + { + assertion = cfg.settings.security.secret_key != "SW2YcwTIb9zpOOhoPsMm"; + message = "services.grafana.settings.security.secret_key must be changed from it's insecure, default value!"; + } + { + assertion = cfg.settings.security.admin_password != "admin"; + message = "services.grafana.settings.security.admin_password must be changed from it's insecure, default value!"; + } + ]; + + services.grafana.settings = lib.mkMerge [ + (lib.mkIf (cfg.enable && cfg.recommendedDefaults) (libS.modules.mkRecursiveDefault { + # no analytics, sorry, not sorry + analytics = { + # TODO: drop after https://github.com/NixOS/nixpkgs/pull/240323 is merged + check_for_updates = false; + feedback_links_enabled = false; + reporting_enabled = false; + }; + log.level = "warn"; + security = { + cookie_secure = true; + content_security_policy = true; + strict_transport_security = true; + }; + server = { + enable_gzip = true; + root_url = "https://${cfg.settings.server.domain}"; + }; + })) + + (lib.mkIf (cfg.enable && cfg.oauth.enable) { + "auth.generic_oauth" = let + inherit (config.services.dex.settings) issuer; + in { + enabled = true; + allow_assign_grafana_admin = true; # required for grafana-admins + allow_sign_up = true; # otherwise no new users can be created + api_url = "${issuer}/userinfo"; + auth_url = "${issuer}/auth"; + client_id = "grafana"; + disable_login_form = true; # only allow OAuth + icon = "signin"; + name = config.services.portunus.domain; + oauth_allow_insecure_email_lookup = true; # otherwise updating the mail in ldap will break login + oauth_auto_login = true; # redirect automatically to the only oauth provider + use_refresh_token = true; + role_attribute_path = "contains(groups[*], 'grafana-admins') && 'Admin' || contains(info.roles[*], 'grafana-user') && 'Editor'" + + lib.optionalString cfg.oauth.enableViewerRole "|| 'Viewer'"; + role_attribute_strict = true; + # https://dexidp.io/docs/custom-scopes-claims-clients/ + scopes = "openid email groups profile offline_access"; + token_url = "${issuer}/token"; + }; + server.protocol = "socket"; + }) + ]; + }; + + config.services.nginx = lib.mkIf (cfg.enable && cfg.configureNginx) { + upstreams.grafana.servers."unix:${cfg.settings.server.socket}" = {}; + virtualHosts = { + "${cfg.settings.server.domain}".locations = { + "/".proxyPass = "http://grafana"; + "/api/live/ws" = { + proxyPass = "http://grafana"; + proxyWebsockets = true; + }; + }; + }; + }; + + config.services.portunus = { + dex = lib.mkIf cfg.oauth.enable { + enable = true; + oidcClients = [{ + callbackURL = "https://${cfg.settings.server.domain}/login/generic_oauth"; + id = "grafana"; + }]; + }; + seedSettings.groups = lib.optional (cfg.oauth.adminGroup != null) { + long_name = "Grafana Administrators"; + name = cfg.oauth.adminGroup; + permissions = { }; + } ++ lib.optional (cfg.oauth.userGroup != null) { + long_name = "Grafana Users"; + name = cfg.oauth.userGroup; + permissions = { }; + }; + }; +} diff --git a/modules/hedgedoc.nix b/modules/hedgedoc.nix new file mode 100644 index 0000000..512d35b --- /dev/null +++ b/modules/hedgedoc.nix @@ -0,0 +1,34 @@ +{ config, lib, libS, ... }: + +let + cfg = config.services.hedgedoc.ldap; + inherit (config.security) ldap; +in +{ + options = { + services.hedgedoc.ldap = { + enable = lib.mkEnableOption (lib.mdDoc '' + login only via LDAP. + Use `service.hedgedoc.environmentFile` in format `bindCredentials=password` to set the credentials used by the search user + ''); + + userGroup = libS.ldap.mkUserGroupOption; + }; + }; + + config.services.hedgedoc.settings.ldap = lib.mkIf cfg.enable { + url = "ldaps://${ldap.domainName}:${toString ldap.port}"; + bindDn = ldap.bindDN; + bindCredentials = "$bindCredentials"; + searchBase = ldap.userBaseDN; + searchFilter = ldap.searchFilterWithGroupFilter cfg.userGroup (ldap.userFilter "{{username}}"); + tlsca = "/etc/ssl/certs/ca-certificates.crt"; + useridField = ldap.userField; + }; + + config.services.portunus.seedSettings.groups = lib.optional (cfg.userGroup != null) { + long_name = "Hedgedoc Users"; + name = cfg.userGroup; + permissions = {}; + }; +} diff --git a/modules/home-assistant.nix b/modules/home-assistant.nix new file mode 100644 index 0000000..7946d61 --- /dev/null +++ b/modules/home-assistant.nix @@ -0,0 +1,103 @@ +{ config, lib, libS, pkgs, ... }: + +let + cfg = config.services.home-assistant; + inherit (config.security) ldap; +in +{ + options = { + services.home-assistant = { + ldap = { + enable = lib.mkEnableOption (lib.mdDoc ''login only via LDAP + + ::: {.note} + Only enable this after completing the onboarding! + ::: + ''); + userGroup = libS.ldap.mkUserGroupOption; + }; + + recommendedDefaults = libS.mkOpinionatedOption "set recommended default settings"; + }; + }; + + config.services.home-assistant = lib.mkMerge [ + (lib.mkIf (cfg.enable && cfg.recommendedDefaults) { + config = { + automation = "!include automations.yaml"; + default_config = { }; # yes, this is required... + homeassistant = { + auth_providers = lib.mkIf (!cfg.ldap.enable) [ + { type = "homeassistant"; } + ]; + temperature_unit = "C"; + time_zone = config.time.timeZone; + unit_system = "metric"; + }; + }; + }) + + (lib.mkIf (cfg.enable && cfg.ldap.enable) { + config.homeassistant.auth_providers = [{ + type = "command_line"; + # the script is not inheriting PATH from home-assistant + command = pkgs.resholve.mkDerivation { + pname = "ldap-auth-sh"; + version = "unstable-2019-02-23"; + + src = pkgs.fetchFromGitHub { + owner = "bob1de"; + repo = "ldap-auth-sh"; + rev = "819f9233116e68b5af5a5f45167bcbb4ed412ed4"; + hash = "sha256-+QjRP5SKUojaCv3lZX2Kv3wkaNvpWFd97phwsRlhroY="; + }; + + installPhase = '' + install -Dm755 ldap-auth.sh -t $out/bin + ''; + + solutions.default = { + fake.external = [ "on_auth_failure" "on_auth_success" ]; + inputs = with pkgs; [ coreutils curl gnugrep gnused openldap ]; + interpreter = "${pkgs.bash}/bin/bash"; + keep."source:$CONFIG_FILE" = true; + scripts = [ "bin/ldap-auth.sh" ]; + }; + }+ "/bin/ldap-auth.sh"; + args = [ + # https://github.com/bob1de/ldap-auth-sh/blob/master/examples/home-assistant.cfg + (pkgs.writeText "config.cfg" /* shell */ '' + ATTRS="${ldap.userField}" + CLIENT="ldapsearch" + DEBUG=0 + FILTER="${ldap.groupFilter "home-assistant-users"}" + NAME_ATTR="${ldap.userField}" + SCOPE="base" + SERVER="ldaps://${ldap.domainName}" + USERDN="uid=$(ldap_dn_escape "$username"),${ldap.userBaseDN}" + BASEDN="$USERDN" + + on_auth_success() { + # print the meta entries for use in HA + if [ ! -z "$NAME_ATTR" ]; then + name=$(echo "$output" | ${lib.getExe pkgs.gnused} -nr "s/^\s*$NAME_ATTR:\s*(.+)\s*\$/\1/Ip") + [ -z "$name" ] || echo "name=$name" + fi + } + '') + ]; + meta = true; + }]; + }) + ]; + + config.services.portunus.seedSettings.groups = lib.optional (cfg.ldap.userGroup != null) { + long_name = "Home-Assistant Users"; + name = cfg.ldap.userGroup; + permissions = { }; + }; + + config.systemd.tmpfiles.rules = lib.mkIf (cfg.enable && cfg.recommendedDefaults) [ + "f ${cfg.configDir}/automations.yaml 0444 hass hass" + ]; +} diff --git a/modules/hydra.nix b/modules/hydra.nix new file mode 100644 index 0000000..9e2ddb5 --- /dev/null +++ b/modules/hydra.nix @@ -0,0 +1,91 @@ +{ config, lib, libS, ... }: + +let + cfg = config.services.hydra.ldap; + inherit (config.security) ldap; +in +{ + options = { + services.hydra.ldap = { + enable = lib.mkEnableOption (lib.mdDoc '' + login only via LDAP. + The bind user password must be placed at `/var/lib/hydra/ldap-password.conf` in the format `bindpw = "PASSWORD" + It is recommended to use a password without special characters because the perl config parser has weird escaping rule like that comment characters `#` must be escape with backslash + ''); + + roleMappings = lib.mkOption { + type = with lib.types; listOf (attrsOf str); + example = [{ hydra-admins = "admins"; }]; + default = [ ]; + description = lib.mdDoc "Map LDAP groups to hydra permissions. See upstream doc, especially role_mapping."; + }; + + userGroup = libS.ldap.mkUserGroupOption; + }; + }; + + config.services.hydra.extraConfig = lib.mkIf cfg.enable /* xml */ '' + # https://hydra.nixos.org/build/196107287/download/1/hydra/configuration.html#using-ldap-as-authentication-backend-optional + + + + class = Password + password_field = password + password_type = self_check + + + class = LDAP + ldap_server = "${ldap.domainName}" + + scheme = ldaps + timeout = 10 + + binddn = "${ldap.bindDN}" + include ldap-password.conf + start_tls = 0 + + ciphers = TLS_AES_256_GCM_SHA384 + sslversion = tlsv1_3 + + user_basedn = "${ldap.userBaseDN}" + user_filter = "${ldap.searchFilterWithGroupFilter cfg.userGroup (ldap.userFilter "%s")}" + user_scope = one + user_field = ${ldap.userField} + + deref = always + + # Important for role mappings to work: + use_roles = 1 + role_basedn = "${ldap.roleBaseDN}" + role_filter = "${ldap.roleFilter}" + role_scope = one + role_field = ${ldap.roleField} + role_value = ${ldap.roleValue} + + deref = always + + + + + # Make all users in the hydra-admin group Hydra admins + # hydra-admins = admin + # Allow all users in the dev group to restart jobs and cancel builds + # dev = restart-jobs + # dev = cancel-build + ${lib.concatStringsSep "\n" (lib.concatMap (lib.mapAttrsToList (name: value: "${name} = ${value}")) cfg.roleMappings)} + + + ''; + + config.services.portunus.seedSettings.groups = [ + (lib.mkIf (cfg.userGroup != null) { + long_name = "Hydra Users"; + name = cfg.userGroup; + permissions = { }; + }) + ] ++ lib.flatten (map lib.attrValues (map (lib.mapAttrs (ldapGroup: _: { + long_name = "Hydra Role ${ldapGroup}"; + name = ldapGroup; + permissions = { }; + })) cfg.roleMappings)); +} diff --git a/modules/intel.nix b/modules/intel.nix new file mode 100644 index 0000000..f5c61a8 --- /dev/null +++ b/modules/intel.nix @@ -0,0 +1,23 @@ +{ config, lib, pkgs, ... }: + +{ + options.hardware = { + intelGPU = lib.mkEnableOption "" // { description = "Whether to add drivers for intel hardware acceleration."; }; + }; + + config = { + hardware.opengl = { + extraPackages = with pkgs; lib.mkIf config.hardware.intelGPU [ + intel-compute-runtime # OpenCL library for iGPU + # video encoding/decoding hardware acceleration + intel-media-driver # broadwell or newer + intel-vaapi-driver # older hardware like haswell + ]; + extraPackages32 = with pkgs.pkgsi686Linux; lib.mkIf config.hardware.intelGPU [ + # video encoding/decoding hardware acceleration + intel-media-driver # broadwell or newer + intel-vaapi-driver # older hardware like haswell + ]; + }; + }; +} diff --git a/modules/ldap.nix b/modules/ldap.nix new file mode 100644 index 0000000..117e776 --- /dev/null +++ b/modules/ldap.nix @@ -0,0 +1,146 @@ +{ config, lib, ... }: + +let + cfg = config.security.ldap; +in +{ + options.security.ldap = lib.mkOption { + type = lib.types.submodule { + options = { + bindDN = lib.mkOption { + type = lib.types.str; + example = "uid=search"; + default = "uid=${cfg.searchUID}"; + apply = s: s + "," + cfg.userBaseDN; + description = lib.mdDoc '' + The DN of the service user used by services. + The user base dn will be automatically appended. + ''; + }; + + domainComponent = lib.mkOption { + type = with lib.types; listOf str; + example = [ "example" "com" ]; + apply = dc: lib.removeSuffix "," (lib.concatMapStrings (x: "dc=${x},") dc); + description = lib.mdDoc '' + Domain component(s) (dc) represented as a list of strings. + + Each entry will be prefixed with `dc=` and all are concatinated with `,`, except the last one. + The example would be concatinated to `dc=example,dc=com` + ''; + }; + + domainName = lib.mkOption { + type = lib.types.str; + example = "auth.example.com"; + description = lib.mdDoc "The domain name to connect to the ldap server."; + }; + + givenNameField = lib.mkOption { + type = lib.types.str; + example = "givenName"; + description = lib.mdDoc "The attribute of the user object where to find its given name."; + }; + + groupFilter = lib.mkOption { + type = with lib.types; functionTo str; + example = lib.literalExpression ''group: "(&(objectclass=person)(isMemberOf=cn=''${group},''${config.security.ldap.roleBaseDN}"''; + description = lib.mdDoc "A function that returns a group filter that matches the first argument against the names of the groups the user is part of."; + }; + + mailField = lib.mkOption { + type = lib.types.str; + example = "mail"; + description = lib.mdDoc "The attribute of the user object where to find its email."; + }; + + port = lib.mkOption { + type = lib.types.port; + example = "636"; + description = lib.mdDoc "The port the ldap server listens on. Usually this is 389 for ldap and 636 for ldaps."; + }; + + roleBaseDN = lib.mkOption { + type = lib.types.str; + example = "ou=groups"; + apply = s: s + "," + cfg.domainComponent; + description = lib.mdDoc '' + The directory path where applications should search for users. + Domain component will be automatically appended. + ''; + }; + + roleField = lib.mkOption { + type = lib.types.str; + example = "cn"; + description = lib.mdDoc "The attribute where the user account is listed in a group."; + }; + + roleFilter = lib.mkOption { + type = lib.types.str; + example = "(&(objectclass=groupOfNames)(member=%s))"; + description = lib.mdDoc "Filter to get the groups of an user object."; + }; + + roleValue = lib.mkOption { + type = lib.types.str; + example = "dn"; + description = lib.mdDoc "The attribute of the user object where to find its distinguished name."; + }; + + searchUID = lib.mkOption { + type = lib.types.str; + example = "search"; + description = lib.mdDoc "The uid of the service user used by services, often referred as search user."; + }; + + searchFilterWithGroupFilter = lib.mkOption { + type = with lib.types; functionTo (functionTo str); + example = lib.literalExpression ''userFilterGroup: userFilter: if (userFilterGroup != null) then "(&''${config.security.ldap.groupFilter userFilterGroup})" else userFilter''; + description = lib.mdDoc '' + A function that returns a search filter that may include a group filter. + The first argument may be the group that is filtered upon or null. + If set to null no additional filtering is done. If set the supplied filter is combined with the user filter. + The second argument must be the user filter including the applications placeholders or ideally the userFilter option. + ''; + }; + + sshPublicKeyField = lib.mkOption { + type = lib.types.str; + example = "sshPublicKey"; + description = lib.mdDoc "The attribute of the user object where to find its ssh public key."; + }; + + surnameField = lib.mkOption { + type = lib.types.str; + example = "sn"; + description = lib.mdDoc "The attribute of the user object where to find its surname."; + }; + + userBaseDN = lib.mkOption { + type = lib.types.str; + example = "ou=users"; + apply = s: s + "," + cfg.domainComponent; + description = lib.mdDoc '' + The directory path where applications should search for users. + Domain component will be automatically appended. + ''; + }; + + userField = lib.mkOption { + type = lib.types.str; + example = "uid"; + description = lib.mdDoc "The attribute of the user object where to find its username."; + }; + + userFilter = lib.mkOption { + type = with lib.types; functionTo str; + example = ''param: "(&(objectclass=person)(|(uid=''${param})(mail=''${param})))"''; + description = lib.mdDoc "A function that returns a user search filter that uses the first argument as the placeholder."; + }; + }; + }; + default = { }; + description = "LDAP options used in other services."; + }; +} diff --git a/modules/mastodon.nix b/modules/mastodon.nix new file mode 100644 index 0000000..58496b6 --- /dev/null +++ b/modules/mastodon.nix @@ -0,0 +1,75 @@ +{ config, lib, libS, pkgs, ... }: + +let + cfg = config.services.mastodon; + cfgl = cfg.ldap; + inherit (config.security) ldap; +in +{ + options.services.mastodon = { + ldap = { + enable = lib.mkEnableOption (lib.mdDoc "login only via LDAP"); + + userGroup = libS.ldap.mkUserGroupOption; + }; + + enableBirdUITheme = lib.mkEnableOption (lib.mdDoc "Bird UI Theme"); + }; + + config.services.mastodon = { + package = lib.mkIf cfg.enableBirdUITheme (pkgs.mastodon.overrideAttrs (_: with pkgs; let + src = pkgs.applyPatches { + src = fetchFromGitHub { + owner = "mstdn"; + repo = "Bird-UI-Theme-Admins"; + rev = "2f9921db746593f393c13f9b79e5b4c2e19b03bd"; + hash = "sha256-+7FUm5GNXRWyS9Oiow6kwX+pWh11wO3stm5iOTY3sYY="; + }; + + patches = [ + # fix compose box background + (fetchpatch { + url = "https://github.com/mstdn/Bird-UI-Theme-Admins/commit/d5a07d653680fba0ad8dd941405e2d0272ff9cd1.patch"; + hash = "sha256-1gnQNCSSuTE/pkPCf49lJQbmeLAbaiPD9u/q8KiFvlU="; + }) + ]; + }; + in { + mastodonModules = mastodon.mastodonModules.overrideAttrs (oldAttrs: { + pname = "mastodon-birdui-theme"; + + nativeBuildInputs = oldAttrs.nativeBuildInputs ++ [ + rsync + xorg.lndir + ]; + + postPatch = '' + rsync -r ${src}/mastodon/ . + ''; + }); + + postBuild = '' + cp ${src}/mastodon/config/themes.yml config/themes.yml + ''; + })); + + extraConfig = lib.mkIf cfgl.enable { + LDAP_ENABLED = "true"; + LDAP_BASE = ldap.userBaseDN; + LDAP_BIND_DN = ldap.bindDN; + LDAP_HOST = ldap.domainName; + LDAP_METHOD = "simple_tls"; + LDAP_PORT = toString ldap.port; + LDAP_UID = ldap.userField; + # convert .,- (space) in LDAP usernames to underscore, otherwise those users cannot log in + LDAP_UID_CONVERSION_ENABLED = "true"; + LDAP_SEARCH_FILTER = ldap.searchFilterWithGroupFilter cfgl.userGroup "(|(%{uid}=%{email})(%{mail}=%{email}))"; + }; + }; + + config.services.portunus.seedSettings.groups = lib.optional (cfgl.userGroup != null) { + long_name = "Mastodon Users"; + name = cfgl.userGroup; + permissions = { }; + }; +} diff --git a/modules/matrix.nix b/modules/matrix.nix new file mode 100644 index 0000000..d487888 --- /dev/null +++ b/modules/matrix.nix @@ -0,0 +1,158 @@ +{ config, lib, libS, pkgs, ... }: + +let + cfg = config.services.matrix-synapse; + cfge = cfg.element-web; + inherit (config.security) ldap; +in +{ + options = { + services.matrix-synapse = { + addAdditionalOembedProvider = libS.mkOpinionatedOption "add additional oembed providers from oembed.com"; + + element-web = { + enable = lib.mkEnableOption (lib.mdDoc "the element-web client"); + + domain = lib.mkOption { + type = lib.types.str; + example = "element.example.org"; + description = lib.mdDoc "The domain that element-web will use."; + }; + + package = lib.mkPackageOptionMD pkgs "Element-Web" { + default = [ "element-web" ]; + }; + + enableConfigFeatures = libS.mkOpinionatedOption "enable most features available via config.json"; + }; + + ldap = { + enable = lib.mkEnableOption (lib.mdDoc "login via ldap"); + + userGroup = libS.ldap.mkUserGroupOption; + + bindPasswordFile = lib.mkOption { + type = lib.types.str; + example = "/var/lib/secrets/bind-password"; + description = lib.mdDoc "Path to a file containing the bind password."; + }; + }; + + recommendedDefaults = libS.mkOpinionatedOption "set recommended and secure default settings"; + }; + }; + + config.environment.etc = lib.mkIf cfg.enable { + "matrix-synapse/config.yaml".source = cfg.configFile; + }; + + config.services.nginx = lib.mkIf cfge.enable { + enable = true; + virtualHosts."${cfge.domain}" = { + forceSSL = true; + enableACME = lib.mkDefault true; + root = (cfge.package.override { + conf = with config.services.matrix-synapse.settings; { + default_server_config."m.homeserver" = { + "base_url" = public_baseurl; + "server_name" = server_name; + }; + default_theme = "dark"; + room_directory.servers = [ server_name ]; + } // lib.optionalAttrs cfge.enableConfigFeatures { + features = { + # https://github.com/matrix-org/matrix-react-sdk/blob/develop/src/settings/Settings.tsx + # https://github.com/vector-im/element-web/blob/develop/docs/labs.md + feature_ask_to_join = true; + feature_bridge_state = true; + feature_exploring_public_spaces = true; + feature_jump_to_date = true; + feature_mjolnir = true; + feature_pinning = true; + feature_presence_in_room_list = true; + feature_report_to_moderators = true; + feature_qr_signin_reciprocate_show = true; + }; + show_labs_settings = true; + }; + }).overrideAttrs ({ postInstall ? "", ... }: { + # prevent 404 spam in nginx log + postInstall = postInstall + '' + ln -rs $out/config.json $out/config.${cfge.domain}.json + ''; + }); + }; + }; + + config.services.matrix-synapse = lib.mkMerge [ + { + settings = lib.mkIf cfge.enable rec { + email.client_base_url = web_client_location; + web_client_location = "https://${cfge.domain}"; + }; + } + + (lib.mkIf cfg.ldap.enable { + plugins = with config.services.matrix-synapse.package.plugins; [ + matrix-synapse-ldap3 + ]; + + settings.modules = [{ + module = "ldap_auth_provider.LdapAuthProviderModule"; + config = { + enabled = true; + mode = "search"; + uri = "ldaps://${ldap.domainName}:${toString ldap.port}"; + base = ldap.userBaseDN; + attributes = { + uid = ldap.userField; + mail = ldap.mailField; + name = ldap.givenNameField; + }; + bind_dn = ldap.bindDN; + bind_password_file = cfg.ldap.bindPasswordFile; + tls_options.validate = true; + } // lib.optionalAttrs (cfg.ldap.userGroup != null) { + filter = ldap.groupFilter cfg.ldap.userGroup; + }; + }]; + }) + + { + settings.oembed.additional_providers = lib.mkIf cfg.addAdditionalOembedProvider [ + ( + let + providers = pkgs.fetchurl { + url = "https://oembed.com/providers.json?2023-03-23"; + sha256 = "sha256-OdgBgkLbtNMn84ixKuC1gGzpyr+X+ORiLl6TAK3lYuQ="; + }; + in + pkgs.runCommand "providers.json" + { + nativeBuildInputs = with pkgs; [ jq ]; + } '' + # filter out entries that do not contain a schemes entry + # Error in configuration at 'oembed.additional_providers...endpoints.': 'schemes' is a required property + # and have none http protocols: Unsupported oEmbed scheme (spotify) for pattern: spotify:* + jq '[ ..|objects| select(.endpoints[0]|has("schemes")) | .endpoints[0].schemes=([ .endpoints[0].schemes[]|select(.|contains("http")) ]) ]' ${providers} > $out + '' + ) + ]; + } + + (lib.mkIf cfg.recommendedDefaults { + settings = { + federation_client_minimum_tls_version = "1.2"; + suppress_key_server_warning = true; + user_directory.prefer_local_users = true; + }; + withJemalloc = true; + }) + ]; + + config.services.portunus.seedSettings.groups = lib.optional (cfg.ldap.userGroup != null) { + long_name = "Matrix Users"; + name = cfg.ldap.userGroup; + permissions = { }; + }; +} diff --git a/modules/nextcloud.nix b/modules/nextcloud.nix new file mode 100644 index 0000000..01a753c --- /dev/null +++ b/modules/nextcloud.nix @@ -0,0 +1,218 @@ +{ config, lib, libS, pkgs, ... }: + +let + cfg = config.services.nextcloud; +in +{ + options = { + services.nextcloud = { + recommendedDefaults = libS.mkOpinionatedOption "set recommended default settings"; + + configureImaginary = libS.mkOpinionatedOption "configure and use Imaginary for preview generation"; + + configureMemories = libS.mkOpinionatedOption "configure dependencies for Memories App"; + + configureMemoriesVaapi = lib.mkOption { + type = lib.types.bool; + default = config.hardware.intelGPU; + defaultText = "config.hardware.intelGPU"; + description = lib.mdDoc '' + Wether to configure Memories App to use an Intel iGPU for hardware acceleration. + ''; + }; + + configurePreviewSettings = lib.mkOption { + type = lib.types.bool; + default = cfg.configureImaginary; + defaultText = "config.services.nextcloud.configureImaginary"; + description = lib.mdDoc '' + Wether to configure the preview settings to be more optimised for real world usage. + By default this is enabled, when Imaginary is configured. + ''; + }; + + configureRecognize = libS.mkOpinionatedOption "configure dependencies for Recognize App"; + }; + }; + + config = lib.mkIf cfg.enable { + services = { + imaginary = lib.mkIf cfg.configureImaginary { + enable = true; + address = "127.0.0.1"; + settings.return-size = true; + }; + + nextcloud = { + # otherwise the Logging App does not function + logType = lib.mkIf cfg.recommendedDefaults "file"; + + phpOptions = lib.mkIf cfg.recommendedDefaults { + # https://docs.nextcloud.com/server/latest/admin_manual/installation/server_tuning.html#:~:text=opcache.jit%20%3D%201255%20opcache.jit_buffer_size%20%3D%20128m + "opcache.jit" = 1255; + "opcache.jit_buffer_size" = "128M"; + }; + + extraOptions = lib.mkMerge [ + (lib.mkIf cfg.configureImaginary { + enabledPreviewProviders = [ + # default from https://github.com/nextcloud/server/blob/master/config/config.sample.php#L1295-L1304 + ''OC\Preview\BMP'' + ''OC\Preview\GIF'' + ''OC\Preview\JPEG'' + ''OC\Preview\Krita'' + ''OC\Preview\MarkDown'' + ''OC\Preview\MP3'' + ''OC\Preview\OpenDocument'' + ''OC\Preview\PNG'' + ''OC\Preview\TXT'' + ''OC\Preview\XBitmap'' + # https://docs.nextcloud.com/server/24/admin_manual/installation/server_tuning.html#previews + ''OC\Preview\Imaginary'' + ]; + }) + + (lib.mkIf cfg.configureMemories { + enabledPreviewProviders = [ + # https://github.com/pulsejet/memories/wiki/File-Type-Support + # TODO: not sure if this should be under configurePreviewSettings instead or both + ''OC\Preview\Image'' # alias for png,jpeg,gif,bmp + ''OC\Preview\HEIC'' + ''OC\Preview\TIFF'' + ''OC\Preview\Movie'' + ]; + + "memories.exiftool" = "${pkgs.exiftool}/bin/exiftool"; + "memories.vod.vaapi" = lib.mkIf cfg.configureMemoriesVaapi true; + "memories.vod.ffmpeg" = "${pkgs.ffmpeg-headless}/bin/ffmpeg"; + "memories.vod.ffprobe" = "${pkgs.ffmpeg-headless}/bin/ffprobe"; + }) + + (lib.mkIf cfg.configurePreviewSettings { + enabledPreviewProviders = [ + # https://github.com/nextcloud/server/tree/master/lib/private/Preview + ''OC\Preview\Font'' + ''OC\Preview\PDF'' + ''OC\Preview\SVG'' + ''OC\Preview\WebP'' + ]; + + jpeg_quality = 60; + preview_max_filesize_image = 128; # MB + preview_max_memory = 512; # MB + preview_max_x = 2048; # px + preview_max_y = 2048; # px + }) + ]; + }; + + phpfpm.pools = lib.mkIf cfg.configurePreviewSettings { + # add user packages to phpfpm process PATHs, required to find ffmpeg for preview generator + # beginning taken from https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/services/web-apps/nextcloud.nix#L985 + nextcloud.phpEnv.PATH = lib.mkForce "/run/wrappers/bin:/nix/var/nix/profiles/default/bin:/run/current-system/sw/bin:/usr/bin:/bin:/etc/profiles/per-user/nextcloud/bin"; + }; + }; + + systemd = { + services = { + nextcloud-cron = lib.mkIf cfg.configureMemories { + # required for memories + # see https://github.com/pulsejet/memories/blob/master/docs/troubleshooting.md#issues-with-nixos + path = with pkgs; [ perl ]; + # fix memories app being unpacked without the x-bit on binaries + # could be done in nextcloud-update-plugins but then manually updates would be broken until the next auto update + preStart = "${pkgs.coreutils}/bin/chmod +x /var/lib/nextcloud/store-apps/memories/bin-ext/*"; + }; + + nextcloud-cron-preview-generator = lib.mkIf cfg.configurePreviewSettings { + environment.NEXTCLOUD_CONFIG_DIR = "${config.services.nextcloud.datadir}/config"; + serviceConfig = { + ExecStart = "/run/current-system/sw/bin/nextcloud-occ preview:pre-generate"; + Type = "oneshot"; + User = "nextcloud"; + }; + }; + + nextcloud-preview-generator-setup = lib.mkIf cfg.configurePreviewSettings { + wantedBy = [ "multi-user.target" ]; + requires = [ "phpfpm-nextcloud.service" ]; + after = [ "phpfpm-nextcloud.service" ]; + environment.NEXTCLOUD_CONFIG_DIR = "${config.services.nextcloud.datadir}/config"; + script = + let + occ = "/run/current-system/sw/bin/nextcloud-occ"; + in + /* bash */ '' + # check with: + # for size in squareSizes widthSizes heightSizes; do echo -n "$size: "; nextcloud-occ config:app:get previewgenerator $size; done + + # extra commands run for preview generator: + # 32 icon file list + # 64 icon file list android app, photos app + # 96 nextcloud client VFS windows file preview + # 256 file app grid view, many requests + # 512 photos app tags + ${occ} config:app:set --value="32 64 96 256 512" previewgenerator squareSizes + + # 341 hover in maps app + # 1920 files/photos app when viewing picture + ${occ} config:app:set --value="341 1920" previewgenerator widthSizes + + # 256 hover in maps app + # 1080 files/photos app when viewing picture + ${occ} config:app:set --value="256 1080" previewgenerator heightSizes + ''; + serviceConfig = { + Type = "oneshot"; + User = "nextcloud"; + }; + }; + + nextcloud-setup = lib.mkIf cfg.configureRecognize { + script = /* bash */ '' + export PATH=$PATH:/etc/profiles/per-user/nextcloud/bin:/run/current-system/sw/bin + + if [[ ! -e /var/lib/nextcloud/store-apps/recognize/node_modules/@tensorflow/tfjs-node/lib/napi-v8/tfjs_binding.node ]]; then + if [[ -d /var/lib/nextcloud/store-apps/recognize/node_modules/ ]]; then + cd /var/lib/nextcloud/store-apps/recognize/node_modules/ + npm rebuild @tensorflow/tfjs-node --build-addon-from-source + fi + fi + ''; + }; + + phpfpm-nextcloud.serviceConfig = lib.mkIf cfg.configureMemoriesVaapi { + DeviceAllow = [ "/dev/dri/renderD128 rwm" ]; + PrivateDevices = lib.mkForce false; + }; + }; + + timers.nextcloud-cron-preview-generator = lib.mkIf cfg.configurePreviewSettings { + timerConfig = { + OnUnitActiveSec = "5m"; + Unit = "nextcloud-cron-preview-generator.service"; + }; + wantedBy = [ "timers.target" ]; + }; + }; + + users.users.nextcloud = { + extraGroups = lib.mkIf cfg.configureMemoriesVaapi [ + "render" # access /dev/dri/renderD128 + ]; + packages = with pkgs; + # generate video thumbnails with preview generator + lib.optional cfg.configurePreviewSettings ffmpeg-headless + # required for memories, duplicated with nextcloud-cron to better debug + ++ lib.optional cfg.configureMemories perl + # required for recognize app + ++ lib.optionals cfg.configureRecognize [ + gnumake # installation requirement + nodejs_16 # runtime and installation requirement + nodejs_16.pkgs.node-pre-gyp # installation requirement + python3 # requirement for node-pre-gyp otherwise fails with exit code 236 + util-linux # runtime requirement for taskset + ]; + }; + }; +} diff --git a/modules/nginx.nix b/modules/nginx.nix new file mode 100644 index 0000000..49d8904 --- /dev/null +++ b/modules/nginx.nix @@ -0,0 +1,176 @@ +{ config, lib, libS, pkgs, ... }: + +let + cfg = config.services.nginx; +in +{ + options.services.nginx = { + allCompression = libS.mkOpinionatedOption "set all recommended compression options"; + + default404Server = { + enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = lib.mdDoc '' + Wether to add a default server which always responds with 404. + This is useful when using a wildcard cname with a wildcard certitificate to not return the first server entry in the config on unknown subdomains + or to do the same for an old and not fully removed domain. + ''; + }; + + acmeHost = lib.mkOption { + type = lib.types.str; + description = lib.mdDoc '' + The acme host to use for the default 404 server. + ''; + }; + }; + + generateDhparams = libS.mkOpinionatedOption "generate more secure, 2048 bits dhparams replacing the default 1024 bits"; + + openFirewall = libS.mkOpinionatedOption "open the firewall port for the http (80) and https (443) default ports"; + + quic = { + enable = lib.mkEnableOption (lib.mdDoc "quic support in nginx"); + + bpf = libS.mkOpinionatedOption "configure nginx' bpf support which routes quic packets from the same source to the same worker"; + }; + + recommendedDefaults = libS.mkOpinionatedOption "set recommended performance options not grouped into other settings"; + + resolverAddrFromNameserver = libS.mkOpinionatedOption "set resolver address to environment.nameservers"; + + rotateLogsFaster = libS.mkOpinionatedOption "keep logs only for 7 days and rotate them daily"; + + setHSTSHeader = libS.mkOpinionatedOption "add the HSTS header to all virtual hosts"; + + tcpFastOpen = libS.mkOpinionatedOption "enable tcp fast open"; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = cfg.quic.enable && cfg.quic.bpf -> !lib.versionOlder cfg.package.version "1.25.0"; + message = "Setting services.nginx.quic.bpf to true requires nginx version 1.25.0 or newer, but currently \"${cfg.package.version}\" is used!"; + } + ]; + + boot.kernel.sysctl = lib.mkIf cfg.tcpFastOpen { + # enable tcp fastopen for outgoing and incoming connections + "net.ipv4.tcp_fastopen" = 3; + }; + + networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ 80 443 ]; + + nixpkgs.overlays = lib.mkIf cfg.tcpFastOpen [ + (final: prev: + let + configureFlags = [ "-DTCP_FASTOPEN=23" ]; + in + { + nginx = prev.nginx.override { inherit configureFlags; }; + nginxQuic = prev.nginxQuic.override { inherit configureFlags; }; + nginxStable = prev.nginxStable.override { inherit configureFlags; }; + nginxMainline = prev.nginxMainline.override { inherit configureFlags; }; + }) + ]; + + services = { + logrotate.settings.nginx = lib.mkIf cfg.rotateLogsFaster { + frequency = "daily"; + rotate = 7; + }; + + # NOTE: do not use mkMerge here to prevent infinite recursions + nginx = { + appendConfig = lib.optionalString (cfg.quic.enable && cfg.quic.bpf) /* nginx */ '' + quic_bpf on; + '' + lib.optionalString cfg.recommendedDefaults /* nginx */ '' + worker_processes auto; + worker_cpu_affinity auto; + ''; + + commonHttpConfig = lib.optionalString cfg.recommendedDefaults /* nginx */ '' + error_log syslog:server=unix:/dev/log; + '' + lib.optionalString cfg.quic.enable /* nginx */'' + quic_retry on; + '' + lib.optionalString cfg.recommendedZstdSettings /* nginx */ '' + # TODO: upstream this? + zstd_types application/x-nix-archive; + ''; + + commonServerConfig = lib.mkIf cfg.setHSTSHeader /* nginx */ '' + more_set_headers "Strict-Transport-Security: max-age=63072000; includeSubDomains; preload"; + ''; + + package = lib.mkIf cfg.quic.enable pkgs.nginxQuic; # based on pkgs.nginxMainline + + recommendedBrotliSettings = lib.mkIf cfg.allCompression true; + recommendedGzipSettings = lib.mkIf cfg.allCompression true; + recommendedOptimisation = lib.mkIf cfg.allCompression true; + recommendedProxySettings = lib.mkIf cfg.allCompression true; + recommendedTlsSettings = lib.mkIf cfg.allCompression true; + recommendedZstdSettings = lib.mkIf cfg.allCompression true; + + resolver.addresses = + let + isIPv6 = addr: builtins.match ".*:.*:.*" addr != null; + escapeIPv6 = addr: + if isIPv6 addr then + "[${addr}]" + else + addr; + in + lib.optionals (cfg.resolverAddrFromNameserver && config.networking.nameservers != [ ]) (map escapeIPv6 config.networking.nameservers); + sslDhparam = lib.mkIf cfg.generateDhparams config.security.dhparams.params.nginx.path; + + # NOTE: do not use mkMerge here to prevent infinite recursions + virtualHosts = + let + extraParameters = [ + # net.core.somaxconn is set to 4096 + # see https://www.nginx.com/blog/tuning-nginx/#:~:text=to%20a%20value-,greater%20than%20512,-%2C%20change%20the%20backlog + "backlog=1024" + + "deferred" + "fastopen=256" # requires nginx to be compiled with -DTCP_FASTOPEN=23 + ]; + in + lib.mkIf (cfg.recommendedDefaults || cfg.default404Server.enable || cfg.quic.enable) { + "_" = { + kTLS = lib.mkIf cfg.recommendedDefaults true; + reuseport = lib.mkIf (cfg.recommendedDefaults || cfg.quic.enable) true; + + default = lib.mkIf cfg.default404Server.enable true; + forceSSL = lib.mkIf cfg.default404Server.enable true; + useACMEHost = lib.mkIf cfg.default404Server.enable cfg.default404Server.acmeHost; + extraConfig = lib.mkIf cfg.default404Server.enable /* nginx */ '' + return 404; + ''; + + listen = lib.mkIf cfg.tcpFastOpen (lib.mkDefault [ + { addr = "0.0.0.0"; port = 80; inherit extraParameters; } + { addr = "0.0.0.0"; port = 443; ssl = true; inherit extraParameters; } + { addr = "[::]"; port = 80; inherit extraParameters; } + { addr = "[::]"; port = 443; ssl = true; inherit extraParameters; } + ]); + + quic = lib.mkIf cfg.quic.enable true; + }; + }; + }; + }; + + security.dhparams = lib.mkIf cfg.generateDhparams { + enable = cfg.generateDhparams; + params.nginx = { }; + }; + + systemd.services.nginx.serviceConfig = lib.mkIf (cfg.quic.enable && cfg.quic.bpf) { + # NOTE: CAP_BPF is included in CAP_SYS_ADMIN but it is not enough alone + AmbientCapabilities = [ "CAP_BPF" "CAP_NET_ADMIN" "CAP_SYS_ADMIN" ]; + CapabilityBoundingSet = [ "CAP_BPF" "CAP_NET_ADMIN" "CAP_SYS_ADMIN" ]; + SystemCallFilter = [ "bpf" ]; + }; + }; +} diff --git a/modules/nix.nix b/modules/nix.nix new file mode 100644 index 0000000..9c7fc71 --- /dev/null +++ b/modules/nix.nix @@ -0,0 +1,91 @@ +{ config, lib, libS, pkgs, ... }: + +let + cfg = config.nix; +in +{ + options.nix = { + deleteChannels = lib.mkEnableOption "" // { description = "Whether to delete all channels on a system switch."; }; + + deleteUserProfiles = lib.mkEnableOption "" // { description = "Whether to delete all user profiles on a system switch."; }; + + diffSystem = libS.mkOpinionatedOption "system closure diffing on updates"; + + recommendedDefaults = libS.mkOpinionatedOption "set recommended default settings"; + + remoteBuilder = { + enable = lib.mkEnableOption "restricted nix remote builder"; + + sshPublicKeys = lib.mkOption { + description = "SSH public keys accepted by the remote build user."; + type = lib.types.listOf lib.types.str; + }; + + name = lib.mkOption { + description = "Name of the user used for remote building."; + type = lib.types.str; + readOnly = true; + default = "nix-remote-builder"; + }; + }; + }; + + config = { + # based on https://github.com/numtide/srvos/blob/main/nixos/roles/nix-remote-builder.nix + # and https://discourse.nixos.org/t/wrapper-to-restrict-builder-access-through-ssh-worth-upstreaming/25834 + nix.settings = { + builders-use-substitutes = lib.mkIf cfg.recommendedDefaults true; + connect-timeout = lib.mkIf cfg.recommendedDefaults 20; + experimental-features = lib.mkIf cfg.recommendedDefaults [ "nix-command" "flakes" ]; + trusted-users = lib.mkIf cfg.remoteBuilder.enable [ cfg.remoteBuilder.name ]; + }; + + users.users.${cfg.remoteBuilder.name} = lib.mkIf cfg.remoteBuilder.enable { + group = "nogroup"; + isNormalUser = true; + openssh.authorizedKeys.keys = map + (key: + let + wrapper-dispatch-ssh-nix = pkgs.writeShellScriptBin "wrapper-dispatch-ssh-nix" /* bash */ '' + case $SSH_ORIGINAL_COMMAND in + "nix-daemon --stdio") + exec ${config.nix.package}/bin/nix-daemon --stdio + ;; + "nix-store --serve --write") + exec ${config.nix.package}/bin/nix-store --serve --write + ;; + *) + echo "Access is only allowed for the nix remote builder" 1>&2 + exit 1 + esac + ''; + + in + "restrict,pty,command=\"${wrapper-dispatch-ssh-nix}/bin/wrapper-dispatch-ssh-nix\" ${key}" + ) + config.nix.remoteBuilder.sshPublicKeys; + }; + + system.activationScripts = { + deleteChannels = lib.mkIf cfg.deleteChannels '' + echo "Deleting all channels..." + rm -rf /root/.nix-channels /home/*/.nix-channels /nix/var/nix/profiles/per-user/*/channels* || true + ''; + + deleteUserProfiles = lib.mkIf cfg.deleteUserProfiles '' + echo "Deleting all user profiles..." + rm -rf /root/.nix-profile /home/*/.nix-profile /nix/var/nix/profiles/per-user/*/profile* || true + ''; + + diff-system = lib.mkIf cfg.diffSystem { + supportsDryActivation = true; + text = '' + if [[ -e /run/current-system && -e $systemConfig ]]; then + echo System package diff: + ${lib.getExe config.nix.package} --extra-experimental-features nix-command store diff-closures /run/current-system $systemConfig || true + fi + ''; + }; + }; + }; +} diff --git a/modules/openvpn.nix b/modules/openvpn.nix new file mode 100644 index 0000000..4f77041 --- /dev/null +++ b/modules/openvpn.nix @@ -0,0 +1,9 @@ + +{ config, lib, ... }: + +let + cfg = config.services.openvpn; +in +{ + # TODO: OpenVPN +} diff --git a/modules/portunus-remove-add-group.diff b/modules/portunus-remove-add-group.diff new file mode 100644 index 0000000..e3c210a --- /dev/null +++ b/modules/portunus-remove-add-group.diff @@ -0,0 +1,25 @@ +diff --git a/internal/frontend/core.go b/internal/frontend/core.go +index 5976377..7c67991 100644 +--- a/internal/frontend/core.go ++++ b/internal/frontend/core.go +@@ -43,8 +43,6 @@ func HTTPHandler(nexus core.Nexus, isBehindTLSProxy bool) http.Handler { + r.Methods("POST").Path(`/users/{uid}/delete`).Handler(postUserDeleteHandler(nexus)) + + r.Methods("GET").Path(`/groups`).Handler(getGroupsHandler(nexus)) +- r.Methods("GET").Path(`/groups/new`).Handler(getGroupsNewHandler(nexus)) +- r.Methods("POST").Path(`/groups/new`).Handler(postGroupsNewHandler(nexus)) + r.Methods("GET").Path(`/groups/{name}/edit`).Handler(getGroupEditHandler(nexus)) + r.Methods("POST").Path(`/groups/{name}/edit`).Handler(postGroupEditHandler(nexus)) + r.Methods("GET").Path(`/groups/{name}/delete`).Handler(getGroupDeleteHandler(nexus)) +diff --git a/internal/frontend/groups.go b/internal/frontend/groups.go +index 5ac6a75..ac59f4f 100644 +--- a/internal/frontend/groups.go ++++ b/internal/frontend/groups.go +@@ -38,7 +38,6 @@ func getGroupsHandler(n core.Nexus) http.Handler { + Members + Permissions granted + +- New group + + + diff --git a/modules/portunus.nix b/modules/portunus.nix new file mode 100644 index 0000000..dca6c5a --- /dev/null +++ b/modules/portunus.nix @@ -0,0 +1,183 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.services.portunus; + inherit (config.security) ldap; +in +{ + options.services.portunus = { + # TODO: how to automatically set this? + # maybe based on $service.ldap.enable && services.portunus.enable? + addToHosts = lib.mkOption { + type = lib.types.bool; + default = false; + description = lib.mdDoc "Whether to add a hosts entry for the portunus domain pointing to externalIp"; + }; + + configureOAuth2Proxy = lib.mkOption { + type = lib.types.bool; + default = false; + description = lib.mdDoc '' + Wether to configure OAuth2 Proxy with Portunus' Dex. + + Use `services.oauth2_proxy.nginx.virtualHosts` to configure the nginx virtual hosts that should require authentication. + ''; + }; + + internalIp4 = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = lib.mdDoc "Internal IPv4 of portunus instance. This is used in the addToHosts option."; + }; + + internalIp6 = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = lib.mdDoc "Internal IPv6 of portunus instance. This is used in the addToHosts option."; + }; + + ldapPreset = lib.mkOption { + type = lib.types.bool; + default = false; + description = lib.mdDoc "Whether to set config.security.ldap to portunus specific settings."; + }; + + removeAddGroup = lib.mkOption { + type = lib.types.bool; + default = false; + description = lib.mdDoc "When enabled, remove the function to add new Groups via the web ui, to enforce seeding usage."; + }; + + seedGroups = lib.mkOption { + type = lib.types.bool; + default = false; + description = lib.mdDoc "Wether to seed groups configured in services as not member managed groups."; + }; + + # TODO: upstream to nixos + seedSettings = lib.mkOption { + type = with lib.types; nullOr (attrsOf (listOf (attrsOf anything))); + default = null; + description = lib.mdDoc '' + Seed settings for users and grousp. + See upstream for format + ''; + }; + }; + + config = { + assertions = [ + { + assertion = cfg.configureOAuth2Proxy -> config.services.oauth2_proxy.keyFile != null; + message = '' + Setting services.portunus.configureOAuth2Proxy to true requires to set service.oauth2_proxy.keyFile + to a file that contains `OAUTH2_PROXY_CLIENT_SECRET` and `OAUTH2_PROXY_COOKIE_SECRET`. + ''; + } + ]; + + networking.hosts = lib.mkIf cfg.addToHosts { + ${cfg.internalIp4} = [ cfg.domain ]; + ${cfg.internalIp6} = [ cfg.domain ]; + }; + + nixpkgs.overlays = lib.mkIf cfg.enable [ + (final: prev: with final; { + dex-oidc = prev.dex-oidc.override { + buildGoModule = args: buildGoModule (args // { + patches = args.patches or [ ] ++ [ + # remember session + (fetchpatch { + url = "https://github.com/SuperSandro2000/dex/commit/d2fb6cdf8188e6973721ddac657a7c5d3daf6955.patch"; + hash = "sha256-PKC7jsNyFN28qFZ7SLYgnd0s09G2cb+vBeFvRzyyLGQ="; + }) + # Complain if the env set in SecretEnv cannot be found + (fetchpatch { + url = "https://github.com/dexidp/dex/commit/f25f72053c9282cfe22521cd508698a07dc5190f.patch"; + hash = "sha256-dyo+UPpceHxL3gcBQaGaDAHJqmysDJw051gMG1aeh5o="; + }) + ]; + + vendorHash = "sha256-YIi67pPIcVndIjWk94ckv6X4WLELUe/J/03e+XWIdHE="; + }); + }; + + portunus = (prev.portunus.override { buildGoModule = buildGo121Module; }).overrideAttrs ({ patches ? [ ], buildInputs ? [ ], ... }: let + version = "2.0.0-beta.2"; + in { + inherit version; + + # TODO: upstream + src = fetchFromGitHub { + owner = "majewsky"; + repo = "portunus"; + rev = "v${version}"; + hash = "sha256-1OU3bepvqriGCW1qDszPnUDJ6eqBzNTiBZ2J4KF4ynw="; + }; + + patches = patches + ++ lib.optional cfg.removeAddGroup ./portunus-remove-add-group.diff; + + # TODO: upstream + buildInputs = buildInputs ++ [ + libxcrypt-legacy + ]; + }); + }) + ]; + + services = let + callbackURL = "https://${cfg.domain}/oauth2/callback"; + clientID = "oauth2_proxy"; # - is not allowed in environment variables + in { + dex = { + enable = lib.mkIf cfg.configureOAuth2Proxy true; + # the user has no other option to accept this and all clients are internal anyway + settings.oauth2.skipApprovalScreen = true; + }; + + oauth2_proxy = lib.mkIf cfg.configureOAuth2Proxy { + enable = true; + inherit clientID; + nginx = { + inherit (config.services.portunus) domain; + }; + provider = "oidc"; + redirectURL = callbackURL; + reverseProxy = true; + upstream = "http://127.0.0.1:4181"; + extraConfig = { + oidc-issuer-url = config.services.dex.settings.issuer; + provider-display-name = "Portunus"; + }; + }; + + portunus = { + dex.oidcClients = lib.mkIf cfg.configureOAuth2Proxy [{ + inherit callbackURL; + id = clientID; + }]; + seedPath = pkgs.writeText "seed.json" (builtins.toJSON cfg.seedSettings); + }; + }; + + security.ldap = lib.mkIf cfg.ldapPreset { + domainName = cfg.domain; + givenNameField = "givenName"; + groupFilter = group: "(&(objectclass=person)(isMemberOf=cn=${group},${ldap.roleBaseDN}))"; + mailField = "mail"; + port = 636; + roleBaseDN = "ou=groups"; + roleField = "cn"; + roleFilter = "(&(objectclass=groupOfNames)(member=%s))"; + roleValue = "dn"; + searchFilterWithGroupFilter = userFilterGroup: userFilter: if (userFilterGroup != null) then "(&${ldap.groupFilter userFilterGroup}${userFilter})" else userFilter; + sshPublicKeyField = "sshPublicKey"; + searchUID = "search"; + surnameField = "sn"; + userField = "uid"; + userFilter = replaceStr: "(&(objectclass=person)(|(uid=${replaceStr})(mail=${replaceStr})))"; + userBaseDN = "ou=users"; + }; + }; +} diff --git a/modules/postgres.nix b/modules/postgres.nix new file mode 100644 index 0000000..17d860c --- /dev/null +++ b/modules/postgres.nix @@ -0,0 +1,96 @@ +{ config, lib, libS, pkgs, ... }: + +# NOTE: requires https://github.com/NixOS/nixpkgs/pull/257503 because of new usage of extraPlugins + +let + cfg = config.services.postgresql; + cfgu = config.services.postgresql.upgrade; +in +{ + options.services.postgresql = { + upgrade = { + enable = libS.mkOpinionatedOption "install the upgrade-pg-cluster script to update postgres."; + + extraArgs = lib.mkOption { + type = with lib.types; listOf str; + default = [ "--link" "--jobs=$(nproc)" ]; + description = lib.mdDoc "Extra arguments to pass to pg_upgrade. See https://www.postgresql.org/docs/current/pgupgrade.html for doc."; + }; + + newPackage = (lib.mkPackageOptionMD pkgs "postgresql" { + default = [ "postgresql_16" ]; + }) // { + description = lib.mdDoc '' + The postgres package to which should be updated. + After running upgrade-pg-cluster this must be set to services.postgresql.package to complete the update. + ''; + }; + + stopServices = lib.mkOption { + type = with lib.types; listOf str; + default = [ ]; + example = [ "hedgedoc" "hydra" "nginx" ]; + description = lib.mdDoc "Systemd services to stop when upgrade is started."; + }; + }; + + recommendedDefaults = libS.mkOpinionatedOption "set recommended default settings"; + }; + + config = lib.mkIf cfg.enable { + environment.systemPackages = lib.optional cfgu.enable ( + let + # conditions copied from nixos/modules/services/databases/postgresql.nix + newPackage = if cfg.enableJIT && !cfgu.newPackage.jitSupport then cfgu.newPackage.withJIT else cfg.newPackage; + newData = "/var/lib/postgresql/${cfgu.newPackage.psqlSchema}"; + newBin = "${if cfg.extraPlugins == [] then cfgu.newPackage else cfgu.newPackage.withPackages cfg.extraPlugins}/bin"; + + oldPackage = if cfg.enableJIT && !cfg.package.jitSupport then cfg.package.withJIT else cfg.package; + oldData = config.services.postgresql.dataDir; + oldBin = "${if cfg.extraPlugins == [] then oldPackage else oldPackage.withPackages cfg.extraPlugins}/bin"; + in + pkgs.writeScriptBin "upgrade-pg-cluster" /* bash */ '' + set -eu + + echo "Current version: ${cfg.package.version}" + echo "Update version: ${cfgu.newPackage.version}" + + if [[ ${cfgu.newPackage.version} == ${cfg.package.version} ]]; then + echo "There is no major postgres update available." + exit 2 + fi + + systemctl stop postgresql ${lib.concatStringsSep " " cfgu.stopServices} + + install -d -m 0700 -o postgres -g postgres "${newData}" + cd "${newData}" + sudo -u postgres "${newBin}/initdb" -D "${newData}" + + sudo -u postgres "${newBin}/pg_upgrade" \ + --old-datadir "${oldData}" --new-datadir "${newData}" \ + --old-bindir ${oldBin} --new-bindir ${newBin} \ + ${lib.concatStringsSep " " cfgu.extraArgs} \ + "$@" + + echo " + + + Run the following commands after setting: + services.postgresql.package = pkgs.postgresql_${lib.versions.major cfgu.newPackage.version} + sudo -u postgres vacuumdb --all --analyze-in-stages + ${newData}/delete_old_cluster.sh + " + '' + ); + + services = { + postgresql.enableJIT = lib.mkIf cfg.recommendedDefaults true; + + postgresqlBackup = lib.mkIf cfg.recommendedDefaults { + compression = "zstd"; + compressionLevel = 9; + pgdumpOptions = "--create --clean"; + }; + }; + }; +} diff --git a/modules/simd.nix b/modules/simd.nix new file mode 100644 index 0000000..cd81630 --- /dev/null +++ b/modules/simd.nix @@ -0,0 +1,27 @@ +{ config, lib, libS, ... }: + +let + cfg = config.simd; +in +{ + options.simd = { + enable = lib.mkEnableOption "optimized builds with simd instructions"; + arch = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = '' + Microarchitecture string for nixpkgs.hostPlatform.gcc.march and to generate system-features. + Can be determined with: ``nix shell nixpkgs#gcc -c gcc -march=native -Q --help=target | grep march`` + ''; + }; + }; + + config = { + nix.settings.system-features = lib.mkIf (cfg.arch != null) (libS.nix.gcc-system-features config.simd.arch); + + nixpkgs.hostPlatform = lib.mkIf cfg.enable { + gcc.arch = config.simd.arch; + inherit (config.nixpkgs) system; + }; + }; +} diff --git a/modules/slim.nix b/modules/slim.nix new file mode 100644 index 0000000..c16f949 --- /dev/null +++ b/modules/slim.nix @@ -0,0 +1,23 @@ +{ config, lib, libS, ... }: + +let + cfg = config.slim; +in +{ + options.slim = { + enable = libS.mkOpinionatedOption "disable some usual rarely used things to slim down the system"; + }; + + config = lib.mkIf cfg.enable { + documentation = { + # html docs and info are not required, man pages are enough + doc.enable = false; + info.enable = false; + }; + + environment.defaultPackages = lib.mkForce [ ]; + + # durring testing only 550K-650K of the tmpfs where used + security.wrapperDirSize = "10M"; + }; +} diff --git a/modules/ssh.nix b/modules/ssh.nix new file mode 100644 index 0000000..501eeeb --- /dev/null +++ b/modules/ssh.nix @@ -0,0 +1,66 @@ +{ config, lib, libS, ... }: + +let + cfgP = config.programs.ssh; + cfgS = config.services.openssh; +in +{ + options = { + programs.ssh = { + addPopularKnownHosts = libS.mkOpinionatedOption "add ssh public keys of popular websites to known_hosts"; + recommendedDefaults = libS.mkOpinionatedOption "set recommend and secure default settings"; + }; + + services.openssh = { + fixPermissions = libS.mkOpinionatedOption "fix host key permissions to prevent lock outs"; + }; + }; + + config = lib.mkIf cfgP.addPopularKnownHosts { + programs.ssh = { + extraConfig = lib.mkIf cfgP.recommendedDefaults '' + # hard complain about wrong knownHosts + StrictHostKeyChecking accept-new + # make automated host key rotation possible + UpdateHostKeys yes + # fetch host keys via DNS and trust them + VerifyHostKeyDNS yes + ''; + knownHosts = lib.mkMerge [ + (libS.mkPubKey "github.com" "ssh-rsa" "AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=") + (libS.mkPubKey "github.com" "ecdsa-sha2-nistp256" "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=") + (libS.mkPubKey "github.com" "ssh-ed25519" "AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl") + (libS.mkPubKey "gitlab.com" "ssh-rsa" "AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9") + (libS.mkPubKey "gitlab.com" "ecdsa-sha2-nistp256" "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=") + (libS.mkPubKey "gitlab.com" "ssh-ed25519" "AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf") + (libS.mkPubKey "git.openwrt.org" "ssh-rsa" "AAAAB3NzaC1yc2EAAAABIwAAAQEAtnM1w/A1uRZqZuYHhw4ASOe9mr3J2qKAa9K9zR8jG+B+NQVtYlIBSkmCFyP6OuydCmoRZ5Gs1I9pl/hEyi7ieEi6g9yww/JbV322cw04Tli46enIYDG1bnSxF6Qt4aXqvPhcObI3z/1Z3XR6weS1fiLDzLvzq+w1gNM77xExD4Mh27LTPkdwOWjkGa5joNx3EQUC3rzwxUqE4fhOT2Ii93h8FSAUXY9C32jkJj9x7vfaJEsCacs6YTiUKKxyzEB+TvFZdUtGtoRThX7UVICUCD2th/r3UeSp8ItWPg/KqzSg2pRfWeYszlVoD59JZ6YCupSjjRqZddghQc94Hev7oQ==") + (libS.mkPubKey "git.openwrt.org" "ecdsa-sha2-nistp256" "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBASOHg+tghASiZF0ClxYb/HEhUcqnD43I86YatRZSUsXNWLEd8yOzjOJExDHHaKtmZtQ/jfEMmoYbCjdEDOYm5g=") + (libS.mkPubKey "git.openwrt.org" "ssh-ed25519" "AAAAC3NzaC1lZDI1NTE5AAAAIJZFpKQMaLM8bG9lAPfEpTBExrzuiTKMni7PgktmDbJe") + (libS.mkPubKey "git.sr.ht" "ssh-rsa" "AAAAB3NzaC1yc2EAAAADAQABAAABAQDZ+l/lvYmaeOAPeijHL8d4794Am0MOvmXPyvHTtrqvgmvCJB8pen/qkQX2S1fgl9VkMGSNxbp7NF7HmKgs5ajTGV9mB5A5zq+161lcp5+f1qmn3Dp1MWKp/AzejWXKW+dwPBd3kkudDBA1fa3uK6g1gK5nLw3qcuv/V4emX9zv3P2ZNlq9XRvBxGY2KzaCyCXVkL48RVTTJJnYbVdRuq8/jQkDRA8lHvGvKI+jqnljmZi2aIrK9OGT2gkCtfyTw2GvNDV6aZ0bEza7nDLU/I+xmByAOO79R1Uk4EYCvSc1WXDZqhiuO2sZRmVxa0pQSBDn1DB3rpvqPYW+UvKB3SOz") + (libS.mkPubKey "git.sr.ht" "ecdsa-sha2-nistp256" "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCj6y+cJlqK3BHZRLZuM+KP2zGPrh4H66DacfliU1E2DHAd1GGwF4g1jwu3L8gOZUTIvUptqWTkmglpYhFp4Iy4=") + (libS.mkPubKey "git.sr.ht" "ssh-ed25519" "AAAAC3NzaC1lZDI1NTE5AAAAIMZvRd4EtM7R+IHVMWmDkVU3VLQTSwQDSAvW0t2Tkj60") + ]; + }; + + systemd.tmpfiles.rules = lib.mkIf cfgS.fixPermissions [ + "d /etc 0755 root root -" + "d /etc/ssh 0755 root root -" + "f /etc/ssh/ssh_host_ed25519_key 0700 root root -" + "f /etc/ssh/ssh_host_ed25519_key.pub 0744 root root -" + "f /etc/ssh/ssh_host_rsa_key 0700 root root -" + "f /etc/ssh/ssh_host_rsa_key.pub 0744 root root -" + ]; + + services.openssh.banner = '' + Welcome to another + ░██╗░░░░░░░██╗░█████╗░██╗░░░██╗███████╗██╗░░░░░███████╗███╗░░██╗░██████╗ + ░██║░░██╗░░██║██╔══██╗██║░░░██║██╔════╝██║░░░░░██╔════╝████╗░██║██╔════╝ + ░╚██╗████╗██╔╝███████║╚██╗░██╔╝█████╗░░██║░░░░░█████╗░░██╔██╗██║╚█████╗░ + ░░████╔═████║░██╔══██║░╚████╔╝░██╔══╝░░██║░░░░░██╔══╝░░██║╚████║░╚═══██╗ + ░░╚██╔╝░╚██╔╝░██║░░██║░░╚██╔╝░░███████╗███████╗███████╗██║░╚███║██████╔╝ + ░░░╚═╝░░░╚═╝░░╚═╝░░╚═╝░░░╚═╝░░░╚══════╝╚══════╝╚══════╝╚═╝░░╚══╝╚═════╝░ + Server :o + + ''; + }; +} diff --git a/modules/tmux.nix b/modules/tmux.nix new file mode 100644 index 0000000..4f5c9c1 --- /dev/null +++ b/modules/tmux.nix @@ -0,0 +1,31 @@ +{ config, lib, libS, ... }: + +let + cfg = config.programs.tmux; +in +{ + options = { + programs.tmux.recommendedDefaults = libS.mkOpinionatedOption "set recommended default settings"; + }; + + config = lib.mkIf cfg.recommendedDefaults { + programs.tmux = { + keyMode = "vi"; + shortcut = "Space"; + aggressiveResize = true; + baseIndex = 1; + clock24 = true; + escapeTime = 100; + terminal = "xterm-256color"; + extraConfig = '' + # focus events enabled for terminals that support them + set -g focus-events on + + # open new tab in PWD + bind '"' split-window -c "#{pane_current_path}" + bind % split-window -h -c "#{pane_current_path}" + bind c new-window -c "#{pane_current_path}" + ''; + }; + }; +} diff --git a/modules/vaultwarden.nix b/modules/vaultwarden.nix new file mode 100644 index 0000000..899f604 --- /dev/null +++ b/modules/vaultwarden.nix @@ -0,0 +1,97 @@ +{ config, lib, libS, ... }: + +let + cfg = config.services.vaultwarden; + usingPostgres = cfg.dbBackend == "postgresql"; +in +{ + options = { + services.vaultwarden = { + configureNginx = libS.mkOpinionatedOption "configure nginx for the configured domain"; + + domain = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = lib.mdDoc '' + The domain under which vaultwarden will be reachable. + ''; + }; + + recommendedDefaults = libS.mkOpinionatedOption "set recommended default settings"; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ { + assertion = cfg.configureNginx -> cfg.domain != null; + message = '' + Setting services.vaultwarden.configureNginx to true requires configuring services.vaultwarden.domain! + ''; + } ]; + + nixpkgs.overlays = lib.mkIf cfg.recommendedDefaults [ + (final: prev: { + vaultwarden = prev.vaultwarden.overrideAttrs ({ patches ? [], ... }: { + patches = patches ++ [ + # add eu region push support + (final.fetchpatch { + url = "https://github.com/dani-garcia/vaultwarden/pull/3752.diff"; + hash = "sha256-QWbuUotNss1TkIIW6c54Y7U7u2yLg2xHopEngtNawcc="; + }) + ]; + }); + }) + ]; + + services = { + nginx = lib.mkIf cfg.configureNginx { + upstreams.vaultwarden.servers."127.0.0.1:${toString config.services.vaultwarden.config.ROCKET_PORT}" = { }; + virtualHosts.${cfg.domain}.locations = { + "/".proxyPass = "http://vaultwarden"; + "/notifications/hub" = { + proxyPass = "http://vaultwarden"; + proxyWebsockets = true; + }; + }; + }; + + postgresql = lib.mkIf usingPostgres { + enable = true; + ensureDatabases = [ "vaultwarden" ]; + ensureUsers = [{ + name = "vaultwarden"; + ensureDBOwnership = true; + }]; + }; + + vaultwarden.config = lib.mkMerge [ + { + DATABASE_URL = lib.mkIf usingPostgres "postgresql:///vaultwarden?host=/run/postgresql"; + DOMAIN = lib.mkIf (cfg.domain != null) "https://${cfg.domain}"; + } + (lib.mkIf cfg.recommendedDefaults { + DATA_FOLDER = "/var/lib/vaultwarden"; # changes data directory + # TODO: change with 1.31.0 update + # ENABLE_WEBSOCKET = true; + LOG_LEVEL = "warn"; + PASSWORD_ITERATIONS = 600000; + ROCKET_ADDRESS = "127.0.0.1"; + ROCKET_PORT = lib.mkDefault 8222; + SIGNUPS_VERIFY = true; + TRASH_AUTO_DELETE_DAYS = 30; + WEBSOCKET_ADDRESS = "127.0.0.1"; + WEBSOCKET_ENABLED = true; + WEBSOCKET_PORT = lib.mkDefault 8223; + }) + ]; + }; + + systemd.services.vaultwarden = { + after = lib.mkIf usingPostgres [ "postgresql.service" ]; + requires = lib.mkIf usingPostgres [ "postgresql.service" ]; + serviceConfig = lib.mkIf cfg.recommendedDefaults { + StateDirectory = lib.mkForce "vaultwarden"; # modules defaults to bitwarden_rs + }; + }; + }; +} diff --git a/modules/zfs.nix b/modules/zfs.nix new file mode 100644 index 0000000..457913d --- /dev/null +++ b/modules/zfs.nix @@ -0,0 +1,34 @@ +{ config, lib, libS, options, pkgs, ... }: + +let + cfg = config.boot.zfs; +in +{ + options = { + boot.zfs = { + recommendedDefaults = libS.mkOpinionatedOption "enable recommended ZFS settings"; + latestCompatibleKernel = libS.mkOpinionatedOption "use the latest ZFS compatible kernel"; + }; + }; + + config = lib.mkIf cfg.enabled { + boot.kernelPackages = + let + ver = config.boot.zfs.package.latestCompatibleLinuxPackages.kernel.version; + in + # 6.0 has a bug in the bind syscall and does not error correct when the port is already in use + # https://lists.fedoraproject.org/archives/list/devel@lists.fedoraproject.org/thread/7VPNMC77YC3SI5LFYKUA4B5MTFPLTLVB/ + # https://lore.kernel.org/stable/CAFsF8vL4CGFzWMb38_XviiEgxoKX0GYup=JiUFXUOmagdk9CRg@mail.gmail.com/ + lib.mkIf (cfg.latestCompatibleKernel && lib.versions.majorMinor ver != "6.0") (lib.mkDefault config.boot.zfs.package.latestCompatibleLinuxPackages); + + services.zfs = lib.mkIf cfg.recommendedDefaults { + autoScrub.enable = true; + trim.enable = true; + }; + + virtualisation.containers.storage.settings = lib.mkIf cfg.recommendedDefaults (lib.recursiveUpdate options.virtualisation.containers.storage.settings.default { + # fixes: Error: 'overlay' is not supported over zfs, a mount_program is required: backing file system is unsupported for this graph driver + storage.options.mount_program = "${pkgs.fuse-overlayfs}/bin/fuse-overlayfs"; + }); + }; +} diff --git a/systems/configuration.nix b/systems/configuration.nix new file mode 100644 index 0000000..2f1c738 --- /dev/null +++ b/systems/configuration.nix @@ -0,0 +1,198 @@ +{ pkgs, lib, config, ... }: +let +in { + time.timeZone = "UTC"; + + i18n = { + defaultLocale = "en_US.utf8"; + supportedLocales = [ + "en_US.UTF-8/UTF-8" + "de_DE.UTF-8/UTF-8" + ]; + }; + + console.keyMap = "de"; + + networking.firewall.allowedTCPPorts = [ 22 ]; + + services = { + openssh = { + enable = true; + fixPermissions = true; + extraConfig = ''StreamLocalBindUnlink yes''; + authorizedKeysFiles = [ "../users/dennis/keys/yubikey.pub" ]; + settings = { + PermitRootLogin = "no"; + PasswordAuthentication = false; + }; + }; + }; + + users.users.brain = { + isNormalUser = true; + description = "Administrator"; + extraGroups = [ "networkmanager" "wheel" ]; + shell = pkgs.zsh; + }; + + nixpkgs.config.allowUnfree = true; + + programs = { + fzf.keybindings = true; + git = { + enable = true; + config = { + alias = { + p = "pull"; + r = "reset --hard"; + ci = "commit"; + co = "checkout"; + lg = "log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(bold green)(%ar)%C(reset) %C(white)%s%C(reset) %C(dim white)- %an%C(reset)%C(bold yellow)%d%C(reset)'"; + st = "status"; + undo = "reset --soft HEAD^"; + }; + interactive.singlekey = true; + pull.rebase = true; + rebase.autoStash = true; + safe.directory = "/etc/nixos"; + }; + }; + + zsh = { + enable = true; + autosuggestions = { + enable = true; + strategy = [ "completion" ]; + async = true; + }; + + syntaxHighlighting.enable = true; + zsh-autoenv.enable = true; + enableCompletion = true; + enableBashCompletion = true; + ohMyZsh = { + enable = true; + plugins = [ "git" "sudo" "docker" "kubectl" "history" "colorize" "direnv" ]; + theme = "agnoster"; + }; + + shellAliases = { + flake = "nvim flake.nix"; + garbage = "sudo nix-collect-garbage -d"; + gpw = "git pull | grep \"Already up-to-date\" > /dev/null; while [ $? -gt 1 ]; do sleep 5; git pull | grep \"Already up-to-date\" > /dev/null; done; notify-send Pull f$"; + l = "ls -lah"; + nixdir = "echo \"use flake\" > .envrc && direnv allow"; + nixeditc = "nvim ~/dotfiles/system/configuration.nix"; + nixeditpc = "nvim ~/dotfiles/system/program.nix"; + pypi = "pip install --user"; + qr = "qrencode -m 2 -t utf8 <<< \"$1\""; + update = "sudo nixos-rebuild switch --fast --flake ~/dotfiles/ -L"; + v = "nvim"; + }; + }; + + neovim = { + enable = true; + defaultEditor = true; + vimAlias = true; + viAlias = true; + withPython3 = true; + configure = { + customRC = '' + set undofile " save undo file after quit + set undolevels=1000 " number of steps to save + set undoreload=10000 " number of lines to save + + " Save Cursor Position + au BufReadPost * if line("'\"") > 1 && line("'\"") <= line("$") | exe "normal! g'\"" | endif + ''; + packages.myVimPackage = with pkgs.vimPlugins; { + start = [ + colorizer + copilot-vim + csv-vim + fugitive + fzf-vim + nerdtree + nvchad + nvchad-ui + nvim-treesitter-refactor + nvim-treesitter.withAllGrammars + unicode-vim + vim-cpp-enhanced-highlight + vim-tmux + vim-tmux-navigator + ]; + }; + }; + }; + + tmux = { + enable = true; + plugins = with pkgs.tmuxPlugins; [ + nord + vim-tmux-navigator + sensible + yank + ]; + }; + + nix-ld = { + enable = true; + libraries = with pkgs; [ + acl + attr + bzip2 + curl + glib + libglvnd + libmysqlclient + libsodium + libssh + libxml2 + openssl + stdenv.cc.cc + systemd + util-linux + xz + zlib + zstd + ]; + }; + }; + + systemd.watchdog = { + enable = true; + device = "/dev/watchdog"; + runTime = "30s"; + rebootTime = "5m"; + }; + + nix = { + settings = { + experimental-features = [ "nix-command" "flakes" ]; + keep-outputs = true; + builders-use-substitutes = true; + connect-timeout = 20; + }; + + gc = { + automatic = true; + dates = "weekly"; + options = "--delete-oder-than 14d"; + }; + + diff-system = true; + }; + + system = { + autoUpgrade = { + enable = true; + randomizedDelaySec = "1h"; + persistent = true; + system.autoUpgrade.flake = "github:RAD-Development/nix-dotfiles"; + }; + + stateVersion = "22.11"; + }; +} \ No newline at end of file diff --git a/systems/programs.nix b/systems/programs.nix new file mode 100644 index 0000000..476fee9 --- /dev/null +++ b/systems/programs.nix @@ -0,0 +1,35 @@ +{ pkgs, ... }: +{ + environment.systemPackages = with pkgs; [ + bat + btop + deadnix + direnv + fd + file + htop + jp2a + jq + lsof + neofetch + nix-init + nix-output-monitor + nix-prefetch + nix-tree + nixpkgs-fmt + nmap + pciutils + python3 + qrencode + ripgrep + speedtest-cli + tig + tokei + tree + unzip + ventoy + wget + zoxide + zsh-nix-shell + ]; +} \ No newline at end of file