diff options
Diffstat (limited to 'modules')
48 files changed, 3268 insertions, 332 deletions
diff --git a/modules/by-name/at/atuin-sync/module.nix b/modules/by-name/at/atuin-sync/module.nix new file mode 100644 index 0000000..0db2e29 --- /dev/null +++ b/modules/by-name/at/atuin-sync/module.nix @@ -0,0 +1,45 @@ +{ + config, + lib, + vhackPackages, + ... +}: let + cfg = config.vhack.atuin-sync; +in { + options.vhack.atuin-sync = { + enable = lib.mkEnableOption "atuin sync server"; + + fqdn = lib.mkOption { + description = "The fully qualified domain name of this instance."; + type = lib.types.str; + example = "atuin-sync.atuin.sh"; + }; + }; + + config = lib.mkIf cfg.enable { + vhack.nginx.enable = true; + + services = { + nginx.virtualHosts."${cfg.fqdn}" = { + locations."/" = { + proxyPass = "http://127.0.0.1:${toString config.services.atuin.port}"; + recommendedProxySettings = true; + }; + + enableACME = true; + forceSSL = true; + }; + + atuin = { + enable = true; + package = vhackPackages.atuin-server-only; + host = "127.0.0.1"; + + # Nobody knows about the fqdn and even if, they can only upload encrypted blobs. + openRegistration = true; + + database.createLocally = true; + }; + }; + }; +} diff --git a/modules/by-name/ba/back/module.nix b/modules/by-name/ba/back/module.nix deleted file mode 100644 index 520acdb..0000000 --- a/modules/by-name/ba/back/module.nix +++ /dev/null @@ -1,121 +0,0 @@ -{ - config, - lib, - vhackPackages, - pkgs, - ... -}: let - cfg = config.vhack.back; - - mkConfigFile = repoPath: domain: - (pkgs.formats.json {}).generate "config.json" - { - inherit (cfg) source_code_repository_url; - repository_path = repoPath; - root_url = "https://${domain}"; - }; - - mkUnit = repoPath: port: domain: { - description = "Back service for ${repoPath}"; - wants = ["network-online.target"]; - after = ["network-online.target"]; - wantedBy = ["default.target"]; - - environment = { - ROCKET_PORT = builtins.toString port; - }; - - serviceConfig = { - ExecStart = "${lib.getExe vhackPackages.back} ${mkConfigFile repoPath domain}"; - - # Ensure that the service can read the repository - # FIXME(@bpeetz): This has the implied assumption, that all the exposed git - # repositories are readable for the git group. This should not be necessary. <2024-12-23> - User = "git"; - Group = "git"; - - DynamicUser = true; - Restart = "always"; - - # Sandboxing - ProtectSystem = "strict"; - ProtectHome = true; - PrivateTmp = true; - PrivateDevices = true; - ProtectHostname = true; - ProtectClock = true; - ProtectKernelTunables = true; - ProtectKernelModules = true; - ProtectKernelLogs = true; - ProtectControlGroups = true; - RestrictAddressFamilies = ["AF_UNIX" "AF_INET" "AF_INET6"]; - RestrictNamespaces = true; - LockPersonality = true; - MemoryDenyWriteExecute = true; - RestrictRealtime = true; - RestrictSUIDSGID = true; - RemoveIPC = true; - PrivateMounts = true; - # System Call Filtering - SystemCallArchitectures = "native"; - SystemCallFilter = ["~@cpu-emulation @debug @keyring @mount @obsolete @privileged @setuid"]; - }; - }; - - mkVirtalHost = port: { - locations."/".proxyPass = "http://127.0.0.1:${builtins.toString port}"; - - enableACME = true; - forceSSL = true; - }; - - services = - lib.mapAttrs' (gitPath: config: { - name = builtins.replaceStrings ["/"] ["_"] "back-${config.domain}"; - value = mkUnit gitPath config.port config.domain; - }) - cfg.repositories; - - virtualHosts = - lib.mapAttrs' (gitPath: config: { - name = config.domain; - value = mkVirtalHost config.port; - }) - cfg.repositories; -in { - options.vhack.back = { - enable = lib.mkEnableOption "Back issue tracker (inspired by tvix's panettone)"; - - source_code_repository_url = lib.mkOption { - description = "The url to the source code of this instance of back"; - default = "https://git.foss-syndicate.org/vhack.eu/nixos-server/tree/pkgs/by-name/ba/back"; - type = lib.types.str; - }; - - repositories = lib.mkOption { - description = "An attibute set of repos to launch `back` services for."; - type = lib.types.attrsOf (lib.types.submodule { - options = { - enable = (lib.mkEnableOption "`back` for this repository.") // {default = true;}; - domain = lib.mkOption { - type = lib.types.str; - description = "The domain to host this `back` instance on."; - }; - port = lib.mkOption { - type = lib.types.port; - - # TODO: This _should_ be an implementation detail, but I've no real approach to - # automatically generate them without encountering weird bugs. <2024-12-23> - description = "The port to use for this back instance. This must be unique."; - }; - }; - }); - default = {}; - }; - }; - - config = lib.mkIf cfg.enable { - systemd = {inherit services;}; - services.nginx = {inherit virtualHosts;}; - }; -} diff --git a/modules/by-name/co/constants/module.nix b/modules/by-name/co/constants/module.nix index b344fcd..2115a37 100644 --- a/modules/by-name/co/constants/module.nix +++ b/modules/by-name/co/constants/module.nix @@ -1,64 +1,94 @@ # This file is inspired by the `nixos/modules/misc/ids.nix` # file in nixpkgs. -{lib, ...}: { +{ + lib, + config, + ... +}: { options.vhack.constants = { ids.uids = lib.mkOption { internal = true; description = '' The user IDs used in the vhack.eu nixos config. ''; - type = lib.types.attrsOf lib.types.int; + type = lib.types.attrsOf (lib.types.ints.between 0 400); }; ids.gids = lib.mkOption { internal = true; description = '' The group IDs used in the vhack.eu nixos config. ''; - type = lib.types.attrsOf lib.types.int; + type = lib.types.attrsOf (lib.types.ints.between 0 400); }; }; config.vhack.constants = { ids.uids = { + # Keep this sorted with `!sort --numeric-sort --key=2 --field-separator="="` + systemd-coredump = 151; # GROUP + opendkim = 221; + mautrix-whatsapp = 222; + etebase-server = 223; + matrix-synapse = 224; + rspamd = 225; + knot-resolver = 226; + peertube = 231; + redis-mastodon = 232; + redis-peertube = 233; + redis-rspamd = 234; + redis-stalwart-mail = 235; + mastodon = 236; + stalwart-mail = 238; acme = 328; dhcpcd = 329; nscd = 330; sshd = 331; systemd-oom = 332; - redis-peertube = 990; - peertube = 992; # TODO Sort correctly - mastodon = 996; - redis-mastodon = 991; - matrix-synapse = 224; - mautrix-whatsapp = 225; - knot-resolver = 997; - redis-rspamd = 989; - rspamd = 225; - opendkim = 221; - virtualMail = 5000; - etebase-server = 998; + resolvconf = 333; # GROUP + nix-sync = 334; + nextcloud = 335; + redis-nextcloud = 336; + taskchampion = 337; + stalwart-mail-certificates = 338; # GROUP + sharkey = 339; + redis-sharkey = 340; # As per the NixOS file, the uids should not be greater or equal to 400; }; - ids.gids = { - acme = 328; - dhcpcd = 329; - nscd = 330; - sshd = 331; - systemd-oom = 332; - resolvconf = 333; # This group is not matched to an user? - systemd-coredump = 151; # matches systemd-coredump user - redis-peertube = 990; - peertube = 992; - mastodon = 996; - redis-mastodon = 991; - matrix-synapse = 224; - knot-resolver = 997; - redis-rspamd = 989; - rspamd = 225; - opendkim = 221; - virtualMail = 5000; - etebase-server = 998; + ids.gids = let + inherit (config.vhack.constants.ids) uids; + in { + # Please add your groups to the users and inherit them here. + # This avoids having an user/group id mismatch. + inherit + (uids) + acme + dhcpcd + etebase-server + knot-resolver + mastodon + matrix-synapse + mautrix-whatsapp + nextcloud + nix-sync + nscd + opendkim + peertube + redis-mastodon + redis-nextcloud + redis-peertube + redis-rspamd + redis-stalwart-mail + rspamd + sshd + stalwart-mail + systemd-oom + sharkey + redis-sharkey + systemd-coredump # matches systemd-coredump user + resolvconf # This group is not matched to an user? + stalwart-mail-certificates # This group is used to connect nginx and stalwart-mail + ; # The gid should match the uid. Thus should not be >= 400; }; diff --git a/modules/by-name/dn/dns/dns/default.nix b/modules/by-name/dn/dns/dns/default.nix new file mode 100644 index 0000000..4ce07d8 --- /dev/null +++ b/modules/by-name/dn/dns/dns/default.nix @@ -0,0 +1,13 @@ +# +# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +{lib}: let + util = import ./util {inherit lib;}; + types = import ./types {inherit lib util;}; +in { + inherit + types + ; +} diff --git a/modules/by-name/dn/dns/dns/types/default.nix b/modules/by-name/dn/dns/dns/types/default.nix new file mode 100644 index 0000000..ece315f --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/default.nix @@ -0,0 +1,16 @@ +# +# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +{ + lib, + util, +}: let + simple = {types = import ./simple.nix {inherit lib;};}; +in { + record = import ./record.nix {inherit lib util;}; + records = import ./records {inherit lib util simple;}; + + zone = import ./zone.nix {inherit lib util simple;}; +} diff --git a/modules/by-name/dn/dns/dns/types/record.nix b/modules/by-name/dn/dns/dns/types/record.nix new file mode 100644 index 0000000..e992bf9 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/record.nix @@ -0,0 +1,75 @@ +# +# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/> +# SPDX-FileCopyrightText: 2021 Naïm Favier <n@monade.li> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +{lib, ...}: let + inherit (lib) hasSuffix isString mkOption removeSuffix types; + + recordType = rsubt: let + submodule = types.submodule { + options = + { + class = mkOption { + type = types.enum ["IN"]; + default = "IN"; + example = "IN"; + description = "Resource record class. Only IN is supported"; + }; + ttl = mkOption { + type = types.nullOr types.ints.unsigned; # TODO: u32 + default = null; + example = 300; + description = "Record caching duration (in seconds)"; + }; + } + // rsubt.options; + }; + in + ( + if rsubt ? fromString + then types.either types.str + else lib.id + ) + submodule; + + # name == "@" : use unqualified domain name + writeRecord = name: rsubt: data: let + data' = + if isString data && rsubt ? fromString + then + # add default values for the record type + (recordType rsubt).merge [] [ + { + file = ""; + value = rsubt.fromString data; + } + ] + else data; + name' = let + fname = rsubt.nameFixup or (n: _: n) name data'; + in + if name == "@" + then name + else if (hasSuffix ".@" name) + then removeSuffix ".@" fname + else "${fname}."; + inherit (rsubt) rtype; + in + lib.concatStringsSep " " (with data'; + [ + name' + ] + ++ lib.optionals (ttl != null) [ + (toString ttl) + ] + ++ [ + class + rtype + (rsubt.dataToString data') + ]); +in { + inherit recordType; + inherit writeRecord; +} diff --git a/modules/by-name/dn/dns/dns/types/records/A.nix b/modules/by-name/dn/dns/dns/types/records/A.nix new file mode 100644 index 0000000..296943e --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/A.nix @@ -0,0 +1,19 @@ +# +# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +{lib, ...}: let + inherit (lib) mkOption types; +in { + rtype = "A"; + options = { + address = mkOption { + type = types.str; + example = "26.3.0.103"; + description = "IP address of the host"; + }; + }; + dataToString = {address, ...}: address; + fromString = address: {inherit address;}; +} diff --git a/modules/by-name/dn/dns/dns/types/records/AAAA.nix b/modules/by-name/dn/dns/dns/types/records/AAAA.nix new file mode 100644 index 0000000..4717176 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/AAAA.nix @@ -0,0 +1,19 @@ +# +# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +{lib, ...}: let + inherit (lib) mkOption types; +in { + rtype = "AAAA"; + options = { + address = mkOption { + type = types.str; + example = "4321:0:1:2:3:4:567:89ab"; + description = "IPv6 address of the host"; + }; + }; + dataToString = {address, ...}: address; + fromString = address: {inherit address;}; +} diff --git a/modules/by-name/dn/dns/dns/types/records/CAA.nix b/modules/by-name/dn/dns/dns/types/records/CAA.nix new file mode 100644 index 0000000..4b40510 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/CAA.nix @@ -0,0 +1,42 @@ +# +# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +# RFC 8659 +{lib, ...}: let + inherit (lib) mkOption types; +in { + rtype = "CAA"; + options = { + issuerCritical = mkOption { + type = types.bool; + example = true; + description = '' + If set to '1', indicates that the corresponding property tag + MUST be understood if the semantics of the CAA record are to be + correctly interpreted by an issuer + ''; + }; + tag = mkOption { + type = types.enum ["issue" "issuewild" "iodef"]; + example = "issue"; + description = "One of the defined property tags"; + }; + value = mkOption { + type = types.str; # section 4.1.1: not limited in length + example = "ca.example.net"; + description = "Value of the property"; + }; + }; + dataToString = { + issuerCritical, + tag, + value, + ... + }: ''${ + if issuerCritical + then "128" + else "0" + } ${tag} "${value}"''; +} diff --git a/modules/by-name/dn/dns/dns/types/records/CNAME.nix b/modules/by-name/dn/dns/dns/types/records/CNAME.nix new file mode 100644 index 0000000..095b078 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/CNAME.nix @@ -0,0 +1,27 @@ +# +# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +# RFC 1035, 3.3.1 +{ + lib, + simple, + ... +}: let + inherit (lib) mkOption; +in { + rtype = "CNAME"; + options = { + cname = mkOption { + type = simple.types.domain-name; + example = "www.test.com"; + description = '' + A <domain-name> which specifies the canonical or primary name + for the owner. The owner name is an alias. + ''; + }; + }; + dataToString = {cname, ...}: "${cname}"; + fromString = cname: {inherit cname;}; +} diff --git a/modules/by-name/dn/dns/dns/types/records/DKIM.nix b/modules/by-name/dn/dns/dns/types/records/DKIM.nix new file mode 100644 index 0000000..31b2f67 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/DKIM.nix @@ -0,0 +1,75 @@ +# +# SPDX-FileCopyrightText: 2020 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +# This is a “fake” record type, not actually part of DNS. +# It gets compiled down to a TXT record. +# RFC 6376 +{ + lib, + util, + ... +}: let + inherit (lib) mkOption types; +in rec { + rtype = "TXT"; + options = { + selector = mkOption { + type = types.str; + example = "mail"; + description = "DKIM selector name"; + }; + h = mkOption { + type = types.listOf types.str; + default = []; + example = ["sha1" "sha256"]; + description = "Acceptable hash algorithms. Empty means all of them"; + apply = lib.concatStringsSep ":"; + }; + k = mkOption { + type = types.nullOr types.str; + default = "rsa"; + example = "rsa"; + description = "Key type"; + }; + n = mkOption { + type = types.str; + default = ""; + example = "Just any kind of arbitrary notes."; + description = "Notes that might be of interest to a human"; + }; + p = mkOption { + type = types.str; + example = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDwIRP/UC3SBsEmGqZ9ZJW3/DkMoGeLnQg1fWn7/zYtIxN2SnFCjxOCKG9v3b4jYfcTNh5ijSsq631uBItLa7od+v/RtdC2UzJ1lWT947qR+Rcac2gbto/NMqJ0fzfVjH4OuKhitdY9tf6mcwGjaNBcWToIMmPSPDdQPNUYckcQ2QIDAQAB"; + description = "Public-key data (base64)"; + }; + s = mkOption { + type = types.listOf (types.enum ["*" "email"]); + default = ["*"]; + example = ["email"]; + description = "Service Type"; + apply = lib.concatStringsSep ":"; + }; + t = mkOption { + type = types.listOf (types.enum ["y" "s"]); + default = []; + example = ["y"]; + description = "Flags"; + apply = lib.concatStringsSep ":"; + }; + }; + dataToString = data: let + items = + ["v=DKIM1"] + ++ lib.pipe data [ + (builtins.intersectAttrs options) # remove garbage list `_module` + (lib.filterAttrs (_k: v: v != null && v != "")) + (lib.filterAttrs (k: _v: k != "selector")) + (lib.mapAttrsToList (k: v: "${k}=${v}")) + ]; + result = lib.concatStringsSep "; " items + ";"; + in + util.writeCharacterString result; + nameFixup = name: self: "${self.selector}._domainkey.${name}"; +} diff --git a/modules/by-name/dn/dns/dns/types/records/DMARC.nix b/modules/by-name/dn/dns/dns/types/records/DMARC.nix new file mode 100644 index 0000000..0f10f2c --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/DMARC.nix @@ -0,0 +1,108 @@ +# +# SPDX-FileCopyrightText: 2020 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +# This is a “fake” record type, not actually part of DNS. +# It gets compiled down to a TXT record. +# RFC 7489 +{ + lib, + util, + ... +}: let + inherit (lib) mkOption types; +in rec { + rtype = "TXT"; + options = { + adkim = mkOption { + type = types.enum ["relaxed" "strict"]; + default = "relaxed"; + example = "strict"; + description = "DKIM Identifier Alignment mode"; + apply = builtins.substring 0 1; + }; + aspf = mkOption { + type = types.enum ["relaxed" "strict"]; + default = "relaxed"; + example = "strict"; + description = "SPF Identifier Alignment mode"; + apply = builtins.substring 0 1; + }; + fo = mkOption { + type = types.listOf (types.enum ["0" "1" "d" "s"]); + default = ["0"]; + example = ["0" "1" "s"]; + description = "Failure reporting options"; + apply = lib.concatStringsSep ":"; + }; + p = mkOption { + type = types.enum ["none" "quarantine" "reject"]; + example = "quarantine"; + description = "Requested Mail Receiver policy"; + }; + pct = mkOption { + type = types.ints.between 0 100; + default = 100; + example = 30; + description = "Percentage of messages to which the DMARC policy is to be applied"; + apply = builtins.toString; + }; + rf = mkOption { + type = types.listOf (types.enum ["afrf"]); + default = ["afrf"]; + example = ["afrf"]; + description = "Format to be used for message-specific failure reports"; + apply = lib.concatStringsSep ":"; + }; + ri = mkOption { + type = types.ints.unsigned; # FIXME: u32 + default = 86400; + example = 12345; + description = "Interval requested between aggregate reports"; + apply = builtins.toString; + }; + rua = mkOption { + type = types.oneOf [types.str (types.listOf types.str)]; + default = []; + example = "mailto:dmarc+rua@example.com"; + description = "Addresses to which aggregate feedback is to be sent"; + apply = val: + # FIXME: need to encode commas in URIs + if builtins.isList val + then lib.concatStringsSep "," val + else val; + }; + ruf = mkOption { + type = types.listOf types.str; + default = []; + example = ["mailto:dmarc+ruf@example.com" "mailto:another+ruf@example.com"]; + description = "Addresses to which message-specific failure information is to be reported"; + apply = val: + # FIXME: need to encode commas in URIs + if builtins.isList val + then lib.concatStringsSep "," val + else val; + }; + sp = mkOption { + type = types.nullOr (types.enum ["none" "quarantine" "reject"]); + default = null; + example = "quarantine"; + description = "Requested Mail Receiver policy for all subdomains"; + }; + }; + dataToString = data: let + # The specification could be more clear on this, but `v` and `p` MUST + # be the first two tags in the record. + items = + ["v=DMARC1; p=${data.p}"] + ++ lib.pipe data [ + (builtins.intersectAttrs options) # remove garbage list `_module` + (lib.filterAttrs (k: v: v != null && v != "" && k != "p")) + (lib.mapAttrsToList (k: v: "${k}=${v}")) + ]; + result = lib.concatStringsSep "; " items + ";"; + in + util.writeCharacterString result; + nameFixup = name: _self: "_dmarc.${name}"; +} diff --git a/modules/by-name/dn/dns/dns/types/records/DNAME.nix b/modules/by-name/dn/dns/dns/types/records/DNAME.nix new file mode 100644 index 0000000..042ce95 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/DNAME.nix @@ -0,0 +1,15 @@ +# RFC 6672 +{lib, ...}: let + inherit (lib) dns mkOption; +in { + rtype = "DNAME"; + options = { + dname = mkOption { + type = dns.types.domain-name; + example = "www.test.com"; + description = "A <domain-name> which provides redirection from a part of the DNS name tree to another part of the DNS name tree"; + }; + }; + dataToString = {dname, ...}: "${dname}"; + fromString = dname: {inherit dname;}; +} diff --git a/modules/by-name/dn/dns/dns/types/records/DNSKEY.nix b/modules/by-name/dn/dns/dns/types/records/DNSKEY.nix new file mode 100644 index 0000000..86ce3a1 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/DNSKEY.nix @@ -0,0 +1,63 @@ +# SPDX-FileCopyrightText: 2020 Aluísio Augusto Silva Gonçalves <https://aasg.name> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# RFC 4034, 2 +{lib, ...}: let + inherit (builtins) isInt split; + inherit (lib) concatStrings flatten mkOption types; + + dnssecOptions = import ./dnssec.nix {inherit lib;}; + inherit (dnssecOptions) mkDNSSECAlgorithmOption; +in { + rtype = "DNSKEY"; + options = { + flags = mkOption { + description = "Flags pertaining to this RR."; + type = types.either types.ints.u16 (types.submodule { + options = { + zoneSigningKey = mkOption { + description = "Whether this RR holds a zone signing key (ZSK)."; + type = types.bool; + default = false; + }; + secureEntryPoint = mkOption { + type = types.bool; + description = '' + Whether this RR holds a secure entry point. + In general, this means the key is a key-signing key (KSK), as opposed to a zone-signing key. + ''; + default = false; + }; + }; + }); + apply = value: + if isInt value + then value + else + ( + if value.zoneSigningKey + then 256 + else 0 + ) + + ( + if value.secureEntryPoint + then 1 + else 0 + ); + }; + algorithm = mkDNSSECAlgorithmOption { + description = "Algorithm of the key referenced by this RR."; + }; + publicKey = mkOption { + type = types.str; + description = "Base64-encoded public key."; + apply = value: concatStrings (flatten (split "[[:space:]]" value)); + }; + }; + dataToString = { + flags, + algorithm, + publicKey, + ... + }: "${toString flags} 3 ${toString algorithm} ${publicKey}"; +} diff --git a/modules/by-name/dn/dns/dns/types/records/DS.nix b/modules/by-name/dn/dns/dns/types/records/DS.nix new file mode 100644 index 0000000..76fac9a --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/DS.nix @@ -0,0 +1,48 @@ +# SPDX-FileCopyrightText: 2020 Aluísio Augusto Silva Gonçalves <https://aasg.name> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# RFC 4034, 5 +{lib, ...}: let + inherit (lib) mkOption types; + + dnssecOptions = import ./dnssec.nix {inherit lib;}; + inherit (dnssecOptions) mkRegisteredNumberOption mkDNSSECAlgorithmOption; + + mkDSDigestTypeOption = args: + mkRegisteredNumberOption { + registryName = "Delegation Signer (DS) Resource Record (RR) Type Digest Algorithms"; + numberType = types.ints.u8; + # These mnemonics are unofficial, unlike the DNSSEC algorithm ones. + mnemonics = { + "sha-1" = 1; + "sha-256" = 2; + "gost" = 3; + "sha-384" = 4; + }; + }; +in { + rtype = "DS"; + options = { + keyTag = mkOption { + description = "Tag computed over the DNSKEY referenced by this RR to identify it."; + type = types.ints.u16; + }; + algorithm = mkDNSSECAlgorithmOption { + description = "Algorithm of the key referenced by this RR."; + }; + digestType = mkDSDigestTypeOption { + description = "Type of the digest given in the `digest` attribute."; + }; + digest = mkOption { + description = "Digest of the DNSKEY referenced by this RR."; + type = types.strMatching "[[:xdigit:]]+"; + }; + }; + dataToString = { + keyTag, + algorithm, + digestType, + digest, + ... + }: "${toString keyTag} ${toString algorithm} ${toString digestType} ${digest}"; +} diff --git a/modules/by-name/dn/dns/dns/types/records/HTTPS.nix b/modules/by-name/dn/dns/dns/types/records/HTTPS.nix new file mode 100644 index 0000000..6e2ef3d --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/HTTPS.nix @@ -0,0 +1,5 @@ +args: +import ./SVCB.nix args +// { + rtype = "HTTPS"; +} diff --git a/modules/by-name/dn/dns/dns/types/records/MTA-STS.nix b/modules/by-name/dn/dns/dns/types/records/MTA-STS.nix new file mode 100644 index 0000000..030490e --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/MTA-STS.nix @@ -0,0 +1,42 @@ +# +# SPDX-FileCopyrightText: 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +# This is a “fake” record type, not actually part of DNS. +# It gets compiled down to a TXT record. +# RFC 8461 +{ + lib, + util, + ... +}: let + inherit (lib) mkOption types; +in rec { + rtype = "TXT"; + options = { + id = mkOption { + type = types.str; + example = "20160831085700Z"; + description = '' + A short string used to track policy updates. This string MUST + uniquely identify a given instance of a policy, such that senders + can determine when the policy has been updated by comparing to the + "id" of a previously seen policy. There is no implied ordering of + "id" fields between revisions. + ''; + }; + }; + dataToString = data: let + items = + ["v=STSv1"] + ++ lib.pipe data [ + (builtins.intersectAttrs options) # remove garbage list `_module` + (lib.filterAttrs (k: v: v != null && v != "")) + (lib.mapAttrsToList (k: v: "${k}=${v}")) + ]; + result = lib.concatStringsSep "; " items + ";"; + in + util.writeCharacterString result; + nameFixup = name: _self: "_mta-sts.${name}"; +} diff --git a/modules/by-name/dn/dns/dns/types/records/MX.nix b/modules/by-name/dn/dns/dns/types/records/MX.nix new file mode 100644 index 0000000..c25b89c --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/MX.nix @@ -0,0 +1,32 @@ +# +# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +# RFC 1035, 3.3.9 +{ + lib, + simple, + ... +}: let + inherit (lib) mkOption types; +in { + rtype = "MX"; + options = { + preference = mkOption { + type = types.ints.u16; + example = 10; + description = "The preference given to this RR among others at the same owner. Lower values are preferred"; + }; + exchange = mkOption { + type = simple.types.domain-name; + example = "smtp.example.com."; + description = "A <domain-name> which specifies a host willing to act as a mail exchange for the owner name"; + }; + }; + dataToString = { + preference, + exchange, + ... + }: "${toString preference} ${exchange}"; +} diff --git a/modules/by-name/dn/dns/dns/types/records/NS.nix b/modules/by-name/dn/dns/dns/types/records/NS.nix new file mode 100644 index 0000000..ea60a91 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/NS.nix @@ -0,0 +1,24 @@ +# +# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +# RFC 1035, 3.3.11 +{ + lib, + simple, + ... +}: let + inherit (lib) mkOption; +in { + rtype = "NS"; + options = { + nsdname = mkOption { + type = simple.types.domain-name; + example = "ns2.example.com"; + description = "A <domain-name> which specifies a host which should be authoritative for the specified class and domain"; + }; + }; + dataToString = {nsdname, ...}: "${nsdname}"; + fromString = nsdname: {inherit nsdname;}; +} diff --git a/modules/by-name/dn/dns/dns/types/records/OPENPGPKEY.nix b/modules/by-name/dn/dns/dns/types/records/OPENPGPKEY.nix new file mode 100644 index 0000000..1f39cb9 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/OPENPGPKEY.nix @@ -0,0 +1,18 @@ +# RFC7929 +{ + lib, + util, + ... +}: let + inherit (lib) mkOption types; +in { + rtype = "OPENPGPKEY"; + options = { + data = mkOption { + type = types.str; + }; + }; + + dataToString = {data, ...}: util.writeCharacterString data; + fromString = data: {inherit data;}; +} diff --git a/modules/by-name/dn/dns/dns/types/records/PTR.nix b/modules/by-name/dn/dns/dns/types/records/PTR.nix new file mode 100644 index 0000000..075f82e --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/PTR.nix @@ -0,0 +1,92 @@ +# +# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +# RFC 1035, 3.3.12 +{ + lib, + simple, + ... +}: let + inherit (lib) mkOption; + + inherit (lib.strings) stringToCharacters splitString; + + reverseIpv4 = input: + builtins.concatStringsSep "." (lib.lists.reverseList (splitString "." + input)); + + reverseIpv6 = input: let + split = splitString ":" input; + elementLength = builtins.length split; + + reverseString = string: + builtins.concatStringsSep "" (lib.lists.reverseList + (stringToCharacters string)); + in + reverseString (builtins.concatStringsSep "." (stringToCharacters (builtins.concatStringsSep + "" (builtins.map ( + part: let + c = stringToCharacters part; + in + if builtins.length c == 4 + then + # valid part + part + else if builtins.length c < 4 && builtins.length c > 0 + then + # leading zeros were elided + (builtins.concatStringsSep "" ( + builtins.map builtins.toString ( + builtins.genList (_: 0) (4 - (builtins.length c)) + ) + )) + + part + else if builtins.length c == 0 + then + # Multiple full blocks were elided. Only one of these can be in an + # IPv6 address, as such we can simply add (8 - (elementLength - 1)) `0000` + # blocks. We need to substract one from `elementLength` because + # this empty part is included in the `elementLength`. + builtins.concatStringsSep "" (builtins.genList (_: "0000") (8 - (elementLength - 1))) + else builtins.throw "Impossible" + ) + split)))); +in { + rtype = "PTR"; + options = { + name = mkOption { + type = simple.types.domain-name; + example = "mail2.server.com"; + description = "The <domain-name> which is defined by the IP."; + }; + ip = { + v4 = mkOption { + type = lib.types.nullOr lib.types.str; + example = "192.168.1.4"; + description = "The IPv4 address of the host."; + default = null; + apply = v: + if v != null + then reverseIpv4 v + else v; + }; + v6 = mkOption { + type = lib.types.nullOr lib.types.str; + example = "192.168.1.4"; + description = "The IPv6 address of the host."; + default = null; + apply = v: + if v != null + then reverseIpv6 v + else v; + }; + }; + }; + dataToString = {name, ...}: "${name}."; + nameFixup = name: self: + if self.ip.v6 == null + then "${self.ip.v4}.in-addr.arpa" + else "${self.ip.v6}.ip6.arpa"; +} diff --git a/modules/by-name/dn/dns/dns/types/records/SOA.nix b/modules/by-name/dn/dns/dns/types/records/SOA.nix new file mode 100644 index 0000000..db7436e --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/SOA.nix @@ -0,0 +1,65 @@ +# +# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +# RFC 1035, 3.3.13 +{ + lib, + simple, + ... +}: let + inherit (lib) concatStringsSep removeSuffix replaceStrings; + inherit (lib) mkOption types; +in { + rtype = "SOA"; + options = { + nameServer = mkOption { + type = simple.types.domain-name; + example = "ns1.example.com"; + description = "The <domain-name> of the name server that was the original or primary source of data for this zone. Don't forget the dot at the end!"; + }; + adminEmail = mkOption { + type = simple.types.domain-name; + example = "admin@example.com"; + description = "An email address of the person responsible for this zone. (Note: in traditional zone files you are supposed to put a dot instead of `@` in your address; you can use `@` with this module and it is recommended to do so. Also don't put the dot at the end!)"; + apply = s: replaceStrings ["@"] ["."] (removeSuffix "." s); + }; + serial = mkOption { + type = types.ints.unsigned; # TODO: u32 + example = 20; + description = "Version number of the original copy of the zone"; + }; + refresh = mkOption { + type = types.ints.unsigned; # TODO: u32 + default = 24 * 60 * 60; + example = 7200; + description = "Time interval before the zone should be refreshed"; + }; + retry = mkOption { + type = types.ints.unsigned; # TODO: u32 + default = 10 * 60; + example = 600; + description = "Time interval that should elapse before a failed refresh should be retried"; + }; + expire = mkOption { + type = types.ints.unsigned; # TODO: u32 + default = 10 * 24 * 60 * 60; + example = 3600000; + description = "Time value that specifies the upper limit on the time interval that can elapse before the zone is no longer authoritative"; + }; + minimum = mkOption { + type = types.ints.unsigned; # TODO: u32 + default = 60; + example = 60; + description = "Minimum TTL field that should be exported with any RR from this zone"; + }; + }; + dataToString = data @ { + nameServer, + adminEmail, + ... + }: let + numbers = map toString (with data; [serial refresh retry expire minimum]); + in "${nameServer} ${adminEmail}. (${concatStringsSep " " numbers})"; +} diff --git a/modules/by-name/dn/dns/dns/types/records/SRV.nix b/modules/by-name/dn/dns/dns/types/records/SRV.nix new file mode 100644 index 0000000..5f558ed --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/SRV.nix @@ -0,0 +1,51 @@ +# +# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +# RFC 2782 +{ + lib, + simple, + ... +}: let + inherit (lib) mkOption types; +in { + rtype = "SRV"; + options = { + service = mkOption { + type = types.str; + example = "foobar"; + description = "The symbolic name of the desired service. Do not add the underscore!"; + }; + proto = mkOption { + type = types.str; + example = "tcp"; + description = "The symbolic name of the desired protocol. Do not add the underscore!"; + }; + priority = mkOption { + type = types.ints.u16; + default = 0; + example = 0; + description = "The priority of this target host"; + }; + weight = mkOption { + type = types.ints.u16; + default = 100; + example = 20; + description = "The weight field specifies a relative weight for entries with the same priority. Larger weights SHOULD be given a proportionately higher probability of being selected"; + }; + port = mkOption { + type = types.ints.u16; + example = 9; + description = "The port on this target host of this service"; + }; + target = mkOption { + type = simple.types.domain-name; + example = ""; + description = "The domain name of the target host"; + }; + }; + dataToString = data: with data; "${toString priority} ${toString weight} ${toString port} ${target}"; + nameFixup = name: self: "_${self.service}._${self.proto}.${name}"; +} diff --git a/modules/by-name/dn/dns/dns/types/records/SSHFP.nix b/modules/by-name/dn/dns/dns/types/records/SSHFP.nix new file mode 100644 index 0000000..1409860 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/SSHFP.nix @@ -0,0 +1,39 @@ +# RFC 4255 +{lib, ...}: let + inherit (lib) mkOption types; + inherit (builtins) attrNames; + algorithm = { + "rsa" = 1; + "dsa" = 2; + "ecdsa" = 3; # RFC 6594 + "ed25519" = 4; # RFC 7479 / RFC 8709 + "ed448" = 6; # RFC 8709 + }; + mode = { + "sha1" = 1; + "sha256" = 2; # RFC 6594 + }; +in { + rtype = "SSHFP"; + options = { + algorithm = mkOption { + example = "ed25519"; + type = types.enum (attrNames algorithm); + apply = value: algorithm.${value}; + }; + fingerprintType = mkOption { + example = "sha256"; + type = types.enum (attrNames mode); + apply = value: mode.${value}; + }; + fingerprint = mkOption { + type = types.str; + }; + }; + dataToString = { + algorithm, + fingerprintType, + fingerprint, + ... + }: "${toString algorithm} ${toString fingerprintType} ${fingerprint}"; +} diff --git a/modules/by-name/dn/dns/dns/types/records/SVCB.nix b/modules/by-name/dn/dns/dns/types/records/SVCB.nix new file mode 100644 index 0000000..62cbc3d --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/SVCB.nix @@ -0,0 +1,100 @@ +# rfc9460 +{lib, ...}: let + inherit + (lib) + concatStringsSep + filter + isInt + isList + mapAttrsToList + mkOption + types + ; + + mkSvcParams = params: + concatStringsSep " " ( + filter (s: s != "") ( + mapAttrsToList ( + name: value: + if value + then name + else if isList value + then "${name}=${concatStringsSep "," value}" + else if isInt value + then "${name}=${builtins.toString value}" + else "" + ) + params + ) + ); +in { + rtype = "SVCB"; + options = { + svcPriority = mkOption { + example = 1; + type = types.ints.u16; + }; + targetName = mkOption { + example = "."; + type = types.str; + }; + mandatory = mkOption { + example = ["ipv4hint"]; + default = null; + type = types.nullOr (types.nonEmptyListOf types.str); + }; + alpn = mkOption { + example = ["h2"]; + default = null; + type = types.nullOr (types.nonEmptyListOf types.str); + }; + no-default-alpn = mkOption { + example = true; + default = false; + type = types.bool; + }; + port = mkOption { + example = 443; + default = null; + type = types.nullOr types.port; + }; + ipv4hint = mkOption { + example = ["127.0.0.1"]; + default = null; + type = types.nullOr (types.nonEmptyListOf types.str); + }; + ipv6hint = mkOption { + example = ["::1"]; + default = null; + type = types.nullOr (types.nonEmptyListOf types.str); + }; + ech = mkOption { + type = types.nullOr types.str; + default = null; + }; + }; + dataToString = { + svcPriority, + targetName, + mandatory ? null, + alpn ? null, + no-default-alpn ? null, + port ? null, + ipv4hint ? null, + ipv6hint ? null, + ech ? null, + ... + }: "${toString svcPriority} ${targetName} ${ + mkSvcParams { + inherit + alpn + ech + ipv4hint + ipv6hint + mandatory + no-default-alpn + port + ; + } + }"; +} diff --git a/modules/by-name/dn/dns/dns/types/records/TLSA.nix b/modules/by-name/dn/dns/dns/types/records/TLSA.nix new file mode 100644 index 0000000..d92a29b --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/TLSA.nix @@ -0,0 +1,50 @@ +# RFC 6698 +{lib, ...}: let + inherit (lib) mkOption types; + inherit (builtins) attrNames; + + certUsage = { + "pkix-ta" = 0; + "pkix-ee" = 1; + "dane-ta" = 2; + "dane-ee" = 3; + }; + selectors = { + "cert" = 0; + "spki" = 1; + }; + match = { + "exact" = 0; + "sha256" = 1; + "sha512" = 2; + }; +in { + rtype = "TLSA"; + options = { + certUsage = mkOption { + example = "dane-ee"; + type = types.enum (attrNames certUsage); + apply = value: certUsage.${value}; + }; + selector = mkOption { + example = "spki"; + type = types.enum (attrNames selectors); + apply = value: selectors.${value}; + }; + matchingType = mkOption { + example = "sha256"; + type = types.enum (attrNames match); + apply = value: match.${value}; + }; + certificate = mkOption { + type = types.str; + }; + }; + dataToString = { + certUsage, + selector, + matchingType, + certificate, + ... + }: "${toString certUsage} ${toString selector} ${toString matchingType} ${certificate}"; +} diff --git a/modules/by-name/dn/dns/dns/types/records/TXT.nix b/modules/by-name/dn/dns/dns/types/records/TXT.nix new file mode 100644 index 0000000..d605ce8 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/TXT.nix @@ -0,0 +1,24 @@ +# +# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +# RFC 1035, 3.3.14 +{ + lib, + util, + ... +}: let + inherit (lib) mkOption types; +in { + rtype = "TXT"; + options = { + data = mkOption { + type = types.str; + example = "favorite drink=orange juice"; + description = "Arbitrary information"; + }; + }; + dataToString = {data, ...}: util.writeCharacterString data; + fromString = data: {inherit data;}; +} diff --git a/modules/by-name/dn/dns/dns/types/records/default.nix b/modules/by-name/dn/dns/dns/types/records/default.nix new file mode 100644 index 0000000..76a86cd --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/default.nix @@ -0,0 +1,43 @@ +# +# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +{ + lib, + util, + simple, +}: let + inherit (lib.attrsets) genAttrs; + + types = [ + "A" + "AAAA" + "CAA" + "CNAME" + "DNAME" + "MX" + "NS" + "SOA" + "SRV" + "TXT" + "PTR" + + # DNSSEC types + "DNSKEY" + "DS" + + # DANE types + "SSHFP" + "TLSA" + "OPENPGPKEY" + "SVCB" + "HTTPS" + + # Pseudo types + "DKIM" + "DMARC" + "MTA-STS" + ]; +in + genAttrs types (t: import (./. + "/${t}.nix") {inherit lib simple util;}) diff --git a/modules/by-name/dn/dns/dns/types/records/dnssec.nix b/modules/by-name/dn/dns/dns/types/records/dnssec.nix new file mode 100644 index 0000000..648f676 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/records/dnssec.nix @@ -0,0 +1,48 @@ +# SPDX-FileCopyrightText: 2020 Aluísio Augusto Silva Gonçalves <https://aasg.name> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +{lib}: let + inherit (builtins) attrNames isInt removeAttrs; + inherit (lib) mkOption types; +in rec { + mkRegisteredNumberOption = { + registryName, + numberType, + mnemonics, + } @ args: + mkOption + { + type = + types.either numberType (types.enum (attrNames mnemonics)) + // { + name = "registeredNumber"; + description = "number in IANA registry '${registryName}'"; + }; + apply = value: + if isInt value + then value + else mnemonics.${value}; + } + // removeAttrs args ["registryName" "numberType" "mnemonics"]; + + mkDNSSECAlgorithmOption = args: + mkRegisteredNumberOption { + registryName = "Domain Name System Security (DNSSEC) Algorithm Numbers"; + numberType = types.ints.u8; + mnemonics = { + "dsa" = 3; + "rsasha1" = 5; + "dsa-nsec3-sha1" = 6; + "rsasha1-nsec3-sha1" = 7; + "rsasha256" = 8; + "rsasha512" = 10; + "ecc-gost" = 12; + "ecdsap256sha256" = 13; + "ecdsap384sha384" = 14; + "ed25519" = 15; + "ed448" = 16; + "privatedns" = 253; + "privateoid" = 254; + }; + }; +} diff --git a/modules/by-name/dn/dns/dns/types/simple.nix b/modules/by-name/dn/dns/dns/types/simple.nix new file mode 100644 index 0000000..fece2c9 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/simple.nix @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2021 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +{lib}: let + inherit (builtins) stringLength; +in { + # RFC 1035, 3.1 + domain-name = lib.types.addCheck lib.types.str (s: stringLength s <= 255); +} diff --git a/modules/by-name/dn/dns/dns/types/zone.nix b/modules/by-name/dn/dns/dns/types/zone.nix new file mode 100644 index 0000000..44ccb15 --- /dev/null +++ b/modules/by-name/dn/dns/dns/types/zone.nix @@ -0,0 +1,119 @@ +# +# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/> +# SPDX-FileCopyrightText: 2021 Naïm Favier <n@monade.li> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +# +{ + lib, + util, + simple, +}: let + inherit (builtins) filter removeAttrs; + inherit + (lib) + concatMapStringsSep + concatStringsSep + mapAttrs + mapAttrsToList + optionalString + ; + inherit (lib) mkOption literalExample types; + + inherit (import ./record.nix {inherit lib;}) recordType writeRecord; + + rsubtypes = import ./records {inherit lib util simple;}; + rsubtypes' = removeAttrs rsubtypes ["SOA"]; + + subzoneOptions = + { + subdomains = mkOption { + type = types.attrsOf subzone; + default = {}; + example = { + www = { + A = [{address = "1.1.1.1";}]; + }; + staging = { + A = [{address = "1.0.0.1";}]; + }; + }; + description = "Records for subdomains of the domain"; + }; + } + // mapAttrs (n: t: + mkOption { + type = types.listOf (recordType t); + default = []; + # example = [ t.example ]; # TODO: any way to auto-generate an example for submodule? + description = "List of ${n} records for this zone/subzone"; + }) + rsubtypes'; + + subzone = types.submodule { + options = subzoneOptions; + }; + + writeSubzone = name: zone: let + groupToString = pseudo: subt: + concatMapStringsSep "\n" (writeRecord name subt) zone."${pseudo}"; + groups = mapAttrsToList groupToString rsubtypes'; + groups' = filter (s: s != "") groups; + + writeSubzone' = subname: writeSubzone "${subname}.${name}"; + sub = concatStringsSep "\n\n" (mapAttrsToList writeSubzone' zone.subdomains); + in + concatStringsSep "\n\n" groups' + + optionalString (sub != "") ("\n\n" + sub); + zone = types.submodule ({name, ...}: { + options = + { + useOrigin = mkOption { + type = types.bool; + default = false; + description = "Wether to use $ORIGIN and unqualified name or fqdn when exporting the zone."; + }; + + TTL = mkOption { + type = types.ints.unsigned; + default = 24 * 60 * 60; + example = literalExample "60 * 60"; + description = "Default record caching duration. Sets the $TTL variable"; + }; + SOA = mkOption rec { + type = recordType rsubtypes.SOA; + example = + { + ttl = 24 * 60 * 60; + } + // type.example; + description = "SOA record"; + }; + } + // subzoneOptions; + }); + renderToString = name: { + useOrigin, + TTL, + SOA, + ... + } @ zone: + if useOrigin + then '' + $ORIGIN ${name}. + $TTL ${toString TTL} + + ${writeRecord "@" rsubtypes.SOA SOA} + + ${writeSubzone "@" zone} + '' + else '' + $TTL ${toString TTL} + + ${writeRecord name rsubtypes.SOA SOA} + + ${writeSubzone name zone} + ''; +in { + inherit zone subzone renderToString; +} diff --git a/modules/by-name/dn/dns/dns/util/default.nix b/modules/by-name/dn/dns/dns/util/default.nix new file mode 100644 index 0000000..59e661d --- /dev/null +++ b/modules/by-name/dn/dns/dns/util/default.nix @@ -0,0 +1,76 @@ +# SPDX-FileCopyrightText: 2021 Kirill Elagin <https://kir.elagin.me/> +# +# SPDX-License-Identifier: MPL-2.0 or MIT +{lib}: let + inherit + (builtins) + concatStringsSep + genList + stringLength + substring + ; + inherit + (lib.strings) + concatMapStrings + concatMapStringsSep + fixedWidthString + splitString + stringToCharacters + ; + inherit (lib.lists) filter reverseList; + + /* + Split a string into byte chunks, such that each output String is less then or equal to + `n` bytes. + + # Type + + splitInGroupsOf :: Integer -> String -> [String] + + # Arguments + + n + : The number of bytes to put into each String. + + s + : The String to split. + */ + splitInGroupsOf = n: s: let + groupCount = (stringLength s - 1) / n + 1; + in + genList (i: substring (i * n) n s) groupCount; + + # : str -> str + # Prepares a Nix string to be written to a zone file as a character-string + # literal: breaks it into chunks of 255 (per RFC 1035, 3.3) and encloses + # each chunk in quotation marks. + writeCharacterString = s: + if stringLength s <= 255 + then ''"${s}"'' + else concatMapStringsSep " " (x: ''"${x}"'') (splitInGroupsOf 255 s); + + # : str -> str, with length 4 (zeros are padded to the left) + align4Bytes = fixedWidthString 4 "0"; + + # : int -> str -> str + # Expands "" to 4n zeros and aligns the rest on 4 bytes + align4BytesOrExpand = n: v: + if v == "" + then (fixedWidthString (4 * n) "0" "") + else align4Bytes v; + + # : str -> [ str ] + # Returns the record of the ipv6 as a list + mkRecordAux = v6: let + splitted = splitString ":" v6; + n = 8 - builtins.length (filter (x: x != "") splitted); + in + stringToCharacters (concatMapStrings (align4BytesOrExpand n) splitted); + + # : str -> str + # Returns the reversed record of the ipv6 + mkReverseRecord = v6: + concatStringsSep "." (reverseList (mkRecordAux v6)) + ".ip6.arpa"; +in { + inherit writeCharacterString mkReverseRecord; +} diff --git a/modules/by-name/dn/dns/module.nix b/modules/by-name/dn/dns/module.nix new file mode 100644 index 0000000..8f4ad37 --- /dev/null +++ b/modules/by-name/dn/dns/module.nix @@ -0,0 +1,86 @@ +{ + config, + lib, + ... +}: let + cfg = config.vhack.dns; + + zones = + builtins.mapAttrs (name: value: { + data = + dns.types.zone.renderToString name value; + }) + cfg.zones; + + dns = import ./dns {inherit lib;}; + + ports = let + parsePorts = listeners: let + splitAddress = addr: lib.splitString "@" addr; + + extractPort = addr: let + split = splitAddress addr; + in + lib.toInt ( + if (builtins.length split) == 2 + then builtins.elemAt split 1 + else "53" + ); + in + builtins.map extractPort listeners; + in + lib.unique (parsePorts cfg.interfaces); +in { + options.vhack.dns = { + enable = lib.mkEnableOption "custom dns server"; + + openFirewall = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Open the following ports: + TCP (${lib.concatStringsSep ", " (map toString ports)}) + UDP (${lib.concatStringsSep ", " (map toString ports)}) + ''; + }; + + interfaces = lib.mkOption { + type = lib.types.listOf lib.types.str; + description = '' + A list of the interfaces to bind to. To select the port add `@` to the end of the + interface. The default port is 53. + ''; + example = [ + "192.168.1.3" + "2001:db8:1::3" + ]; + }; + + zones = lib.mkOption { + type = lib.types.attrsOf dns.types.zone.zone; + description = "DNS zones"; + }; + }; + + config = lib.mkIf cfg.enable { + services.nsd = { + enable = true; + verbosity = 4; + inherit (cfg) interfaces; + inherit zones; + }; + + networking.firewall.allowedUDPPorts = lib.mkIf cfg.openFirewall ports; + networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall ports; + + systemd.services.nsd = { + requires = [ + "network-online.target" + ]; + after = [ + "network.target" + "network-online.target" + ]; + }; + }; +} diff --git a/modules/by-name/gi/git-back/module.nix b/modules/by-name/gi/git-back/module.nix new file mode 100644 index 0000000..96f4913 --- /dev/null +++ b/modules/by-name/gi/git-back/module.nix @@ -0,0 +1,41 @@ +{ + config, + lib, + ... +}: let + cfg = config.vhack.git-back; +in { + options.vhack.git-back = { + enable = lib.mkEnableOption "Back integration into git-server"; + + domain = lib.mkOption { + type = lib.types.str; + description = "The domain where to deploy back"; + }; + }; + + config = lib.mkIf cfg.enable { + vhack.back = { + enable = true; + + user = "git"; + group = "git"; + + settings = { + scan_path = "${config.services.gitolite.dataDir}/repositories"; + project_list = "${config.services.gitolite.dataDir}/projects.list"; + root_url = "https://${cfg.domain}"; + }; + }; + + services.nginx = { + enable = true; + virtualHosts."${cfg.domain}" = { + locations."/".proxyPass = "http://127.0.0.1:8000"; + + enableACME = true; + forceSSL = true; + }; + }; + }; +} diff --git a/modules/by-name/ma/mastodon/module.nix b/modules/by-name/ma/mastodon/module.nix index 895428d..84f3ec8 100644 --- a/modules/by-name/ma/mastodon/module.nix +++ b/modules/by-name/ma/mastodon/module.nix @@ -37,16 +37,22 @@ in { owner = "mastodon"; group = "mastodon"; }; - vhack.persist.directories = [ - { - directory = "/var/lib/mastodon"; - user = "mastodon"; - group = "mastodon"; - mode = "0700"; - } - ]; - vhack.postgresql.enable = true; + vhack = { + persist.directories = [ + { + directory = "/var/lib/mastodon"; + user = "mastodon"; + group = "mastodon"; + mode = "0700"; + } + ]; + + postgresql.enable = true; + + nginx.enable = true; + }; + services.mastodon = { enable = true; @@ -54,7 +60,7 @@ in { # Unstable Mastodon package, used if # security updates aren't backported. - #package = applyPatches pkgs-unstable.mastodon; + #package = applyPatches pkgsUnstable.mastodon; localDomain = if cfg.enableTLD @@ -75,7 +81,6 @@ in { }; }; - vhack.nginx.enable = true; services.nginx = { enable = true; recommendedProxySettings = true; # required for redirections to work diff --git a/modules/by-name/ma/matrix/module.nix b/modules/by-name/ma/matrix/module.nix index 4b730da..ae3f04e 100644 --- a/modules/by-name/ma/matrix/module.nix +++ b/modules/by-name/ma/matrix/module.nix @@ -1,6 +1,5 @@ { config, - pkgs, lib, ... }: let @@ -29,6 +28,7 @@ in { description = "The age encrypted shared secret file for synapse, passed to agenix"; }; }; + config = lib.mkIf cfg.enable { age.secrets.matrix-synapse_registration_shared_secret = { file = cfg.sharedSecretFile; @@ -38,45 +38,53 @@ in { }; networking.firewall.allowedTCPPorts = [80 443]; - vhack.persist.directories = [ - { - directory = "/var/lib/matrix"; - user = "matrix-synapse"; - group = "matrix-synapse"; - mode = "0700"; - } - { - directory = "/var/lib/mautrix-whatsapp"; - user = "mautrix-whatsapp"; - group = "matrix-synapse"; - mode = "0750"; - } - ]; - systemd.tmpfiles.rules = [ - "d /etc/matrix 0755 matrix-synapse matrix-synapse" - ]; + vhack = { + persist.directories = [ + { + directory = "/var/lib/matrix"; + user = "matrix-synapse"; + group = "matrix-synapse"; + mode = "0700"; + } + { + directory = "/var/lib/mautrix-whatsapp"; + user = "mautrix-whatsapp"; + group = "matrix-synapse"; + mode = "0750"; + } + ]; - vhack.postgresql.enable = true; - vhack.nginx.enable = true; + postgresql.enable = true; + nginx.enable = true; + }; + + systemd = { + tmpfiles.rules = [ + "d /etc/matrix 0755 matrix-synapse matrix-synapse" + ]; + services.postgresql.postStart = '' + $PSQL -tAc "ALTER ROLE \"matrix-synapse\" WITH PASSWORD 'synapse';" + $PSQL -tAc "ALTER ROLE \"mautrix-whatsapp\" WITH PASSWORD 'whatsapp';" + ''; + }; services = { postgresql = { enable = true; - initialScript = pkgs.writeText "synapse-init.sql" '' - --Matrix: - CREATE ROLE "matrix-synapse" WITH LOGIN PASSWORD 'synapse'; - CREATE DATABASE "matrix-synapse" WITH OWNER "matrix-synapse" - TEMPLATE template0 - LC_COLLATE = "C" - LC_CTYPE = "C"; - - --Whatsapp-bridge: - CREATE ROLE "mautrix-whatsapp" WITH LOGIN PASSWORD 'whatsapp'; - CREATE DATABASE "mautrix-whatsapp" WITH OWNER "mautrix-whatsapp" - TEMPLATE template0 - LC_COLLATE = "C" - LC_CTYPE = "C"; - ''; + ensureUsers = [ + { + name = "matrix-synapse"; + ensureDBOwnership = true; + } + { + name = "mautrix-whatsapp"; + ensureDBOwnership = true; + } + ]; + ensureDatabases = [ + "matrix-synapse" + "mautrix-whatsapp" + ]; }; nginx = { diff --git a/modules/by-name/ne/nextcloud/module.nix b/modules/by-name/ne/nextcloud/module.nix new file mode 100644 index 0000000..e0d7cb3 --- /dev/null +++ b/modules/by-name/ne/nextcloud/module.nix @@ -0,0 +1,82 @@ +{ + config, + pkgs, + lib, + ... +}: let + cfg = config.vhack.nextcloud; +in { + options.vhack.nextcloud = { + enable = lib.mkEnableOption "a sophisticated nextcloud setup"; + package = lib.mkOption { + type = lib.types.package; + default = pkgs.nextcloud31; + description = "The nextcloud package to use"; + }; + hostname = lib.mkOption { + type = lib.types.str; + description = "The nextcloud hostname (fqdn)"; + }; + adminpassFile = lib.mkOption { + type = lib.types.path; + description = "The age encrypted admin password file"; + }; + }; + config = lib.mkIf cfg.enable { + vhack = { + nginx.enable = true; + postgresql.enable = true; + persist.directories = [ + "/var/lib/nextcloud" + ]; + }; + age.secrets = { + adminpassFile = { + file = cfg.adminpassFile; + mode = "0700"; + owner = "nextcloud"; + group = "nextcloud"; + }; + }; + + services = { + nextcloud = { + enable = true; + extraApps = { + inherit (cfg.package.packages.apps) calendar contacts tasks; + }; + extraAppsEnable = true; + configureRedis = true; + config = { + adminuser = "admin"; + adminpassFile = config.age.secrets.adminpassFile.path; + dbname = "nextcloud"; + dbuser = "nextcloud"; + dbtype = "pgsql"; + }; + database.createLocally = true; + hostName = cfg.hostname; + https = true; + maxUploadSize = "5G"; + package = cfg.package; + settings = { + default_phone_region = "DE"; + }; + }; + nginx.virtualHosts.${cfg.hostname} = { + forceSSL = true; + enableACME = true; + }; + }; + users = { + users = { + "nextcloud".uid = config.vhack.constants.ids.uids.nextcloud; + "redis-nextcloud".uid = config.vhack.constants.ids.uids.redis-nextcloud; + }; + groups = { + "nextcloud".gid = config.vhack.constants.ids.gids.nextcloud; + "redis-nextcloud".gid = config.vhack.constants.ids.gids.redis-nextcloud; + }; + }; + }; +} diff --git a/modules/by-name/ng/nginx/module.nix b/modules/by-name/ng/nginx/module.nix index 27b0302..fa3337d 100644 --- a/modules/by-name/ng/nginx/module.nix +++ b/modules/by-name/ng/nginx/module.nix @@ -6,7 +6,7 @@ mkRedirect = _: value: { forceSSL = true; enableACME = true; - locations."/".return = "301 ${value}"; + locations."/".return = "301 ${value}$request_uri"; }; redirects = builtins.mapAttrs mkRedirect cfg.redirects; @@ -44,7 +44,10 @@ in { ]; users = { - users.acme.uid = config.vhack.constants.ids.uids.acme; + users.acme = { + uid = config.vhack.constants.ids.uids.acme; + group = "acme"; + }; groups.acme.gid = config.vhack.constants.ids.gids.acme; }; diff --git a/modules/by-name/ni/nix-sync/hosts.nix b/modules/by-name/ni/nix-sync/hosts.nix deleted file mode 100644 index 98dbbf1..0000000 --- a/modules/by-name/ni/nix-sync/hosts.nix +++ /dev/null @@ -1,48 +0,0 @@ -{...}: let - extraWkdSettings = { - locations."/.well-known/openpgpkey/hu/".extraConfig = '' - default_type application/octet-stream; - - # Came from: https://www.uriports.com/blog/setting-up-openpgp-web-key-directory/ - # No idea if it is actually necessary - # add_header Access-Control-Allow-Origin * always; - ''; - }; -in [ - { - domain = "vhack.eu"; - url = "https://codeberg.org/vhack.eu/website.git"; - } - { - domain = "b-peetz.de"; - url = "https://codeberg.org/bpeetz/b-peetz.de.git"; - } - - # Trinitrix - { - domain = "trinitrix.vhack.eu"; - url = "https://codeberg.org/trinitrix/website.git"; - } - - # WKD - { - domain = "openpgpkey.b-peetz.de"; - url = "https://codeberg.org/vhack.eu/gpg_wkd.git"; - extraSettings = extraWkdSettings; - } - { - domain = "openpgpkey.s-schoeffel.de"; - url = "https://codeberg.org/vhack.eu/gpg_wkd.git"; - extraSettings = extraWkdSettings; - } - { - domain = "openpgpkey.sils.li"; - url = "https://codeberg.org/vhack.eu/gpg_wkd.git"; - extraSettings = extraWkdSettings; - } - { - domain = "openpgpkey.vhack.eu"; - url = "https://codeberg.org/vhack.eu/gpg_wkd.git"; - extraSettings = extraWkdSettings; - } -] diff --git a/modules/by-name/ni/nix-sync/module.nix b/modules/by-name/ni/nix-sync/module.nix index de096b9..9ddd210 100644 --- a/modules/by-name/ni/nix-sync/module.nix +++ b/modules/by-name/ni/nix-sync/module.nix @@ -1,43 +1,44 @@ { config, lib, + modulesPath, + nixLib, ... }: let cfg = config.vhack.nix-sync; mkNixSyncRepository = { domain, - root ? "", - url, - extraSettings ? {}, + repositoryUrl, + extraSettings, }: { name = "${domain}"; value = { - path = "/etc/nginx/websites/${domain}/${root}"; - uri = "${url}"; + path = "/etc/nginx/websites/${domain}"; + uri = "${repositoryUrl}"; inherit extraSettings; }; }; - nixSyncRepositories = builtins.listToAttrs (builtins.map mkNixSyncRepository domains); + nixSyncRepositories = builtins.listToAttrs (builtins.map mkNixSyncRepository cfg.domains); mkVirtHost = { domain, - root ? "", - url, - extraSettings ? {}, + repositoryUrl, + extraSettings, }: { name = "${domain}"; value = - lib.recursiveUpdate { + # FIXME(@bpeetz): We cannot use something like `lib.recursiveUpdate` because the + # `extraSettings` are instantiated from the “real” nginx type. As such the + # `extaSettings` would override our values here. Therefore, the direct merge. <2025-02-07> + extraSettings + // { forceSSL = true; enableACME = true; - root = "/etc/nginx/websites/${domain}/${root}"; - } - extraSettings; + root = "/etc/nginx/websites/${domain}"; + }; }; - virtHosts = builtins.listToAttrs (builtins.map mkVirtHost domains); - - domains = import ./hosts.nix {}; + virtHosts = builtins.listToAttrs (builtins.map mkVirtHost cfg.domains); in { imports = [ ./internal_module.nix @@ -47,6 +48,38 @@ in { enable = lib.mkEnableOption '' a website git ops solution. ''; + + domains = lib.mkOption { + type = lib.types.listOf (lib.types.submodule { + options = { + domain = lib.mkOption { + type = lib.types.str; + example = "b-peetz.de"; + description = '' + The fully qualified domain to use as base of this website. + ''; + }; + repositoryUrl = lib.mkOption { + type = lib.types.str; + example = "b-peetz.de"; + description = '' + The url used for the source git repository, which is deployed at this domain. + ''; + }; + extraSettings = lib.mkOption { + type = + lib.types.submodule (import (modulesPath + "/services/web-servers/nginx/vhost-options.nix") {inherit config lib;}); + example = { + locations."/.well-known/openpgpkey/".extraConfig = "default_type application/octet-stream"; + }; + default = {}; + description = '' + Extra configuration to add to the nginx virtual host. + ''; + }; + }; + }); + }; }; config = lib.mkIf cfg.enable { @@ -66,5 +99,10 @@ in { vhack.nginx.enable = true; services.nginx.virtualHosts = virtHosts; + + users = { + users.nix-sync.uid = config.vhack.constants.ids.uids.nix-sync; + groups.nix-sync.gid = config.vhack.constants.ids.gids.nix-sync; + }; }; } diff --git a/modules/by-name/re/redlib/module.nix b/modules/by-name/re/redlib/module.nix index 2b20c66..909c9f1 100644 --- a/modules/by-name/re/redlib/module.nix +++ b/modules/by-name/re/redlib/module.nix @@ -31,14 +31,6 @@ in { enableACME = true; forceSSL = true; }; - - # TODO: Remove this at a certain point. <2024-12-19> - virtualHosts."libreddit.vhack.eu" = { - locations."/".return = "301 https://${domain}"; - - forceSSL = true; - enableACME = true; - }; }; }; } diff --git a/modules/by-name/ru/rust-motd/module.nix b/modules/by-name/ru/rust-motd/module.nix index a6998f4..8d0939a 100644 --- a/modules/by-name/ru/rust-motd/module.nix +++ b/modules/by-name/ru/rust-motd/module.nix @@ -19,6 +19,13 @@ || v.openssh.authorizedKeys.keyFiles != [] ); userList = builtins.mapAttrs (n: v: 2) (lib.filterAttrs pred config.users.users); + + bannerFile = + pkgs.runCommandNoCCLocal "banner-file" { + nativeBuildInputs = [pkgs.figlet]; + } '' + echo "${config.system.name}" | figlet -f slant > "$out" + ''; in { options.vhack.rust-motd = { enable = lib.mkEnableOption "rust-motd"; @@ -49,25 +56,22 @@ in { banner = { color = "red"; - command = "${pkgs.hostname}/bin/hostname | ${pkgs.figlet}/bin/figlet -f slant"; - # if you don't want a dependency on figlet, you can generate your - # banner however you want, put it in a file, and then use something like: - # command = "cat banner.txt" + # Avoid some runtime dependencies. + command = "cat ${bannerFile}"; + }; + + cg_stats = { + state_file = "/var/lib/rust-motd/cg_stats_state"; + threshold = 0.02; # When to start generating output for a cgroup + }; + load_avg = { + format = "Load (1, 5, 15 min.): {one:.02}, {five:.02}, {fifteen:.02}"; }; uptime = { prefix = "Uptime:"; }; - # ssl_certificates = { - # sort_method = "manual"; - # - # certs = { - # "server1.vhack.eu" = "/var/lib/acme/server1.vhack.eu/cert.pem"; - # "vhack.eu" = "/var/lib/acme/vhack.eu/cert.pem"; - # }; - # }; - filesystems = { root = "/"; persistent = "/srv"; @@ -79,7 +83,7 @@ in { swap_pos = "beside"; # or "below" or "none" }; - fail2_ban = { + fail_2_ban = { jails = ["sshd"]; #, "anotherjail"] }; diff --git a/modules/by-name/sh/sharkey/module.nix b/modules/by-name/sh/sharkey/module.nix new file mode 100644 index 0000000..2b50cf0 --- /dev/null +++ b/modules/by-name/sh/sharkey/module.nix @@ -0,0 +1,298 @@ +# Source: https://github.com/sodiboo/system/blob/b63c7b27f49043e8701b3ff5e1441cd27d5a2fff/sharkey/module.nix +{ + config, + lib, + pkgs, + vhackPackages, + ... +}: let + cfg = config.vhack.sharkey; + + createDB = cfg.database.host == "127.0.0.1" && cfg.database.createLocally; + + settingsFormat = pkgs.formats.yaml {}; + configFile = settingsFormat.generate "sharkey-config.yml" cfg.settings; +in { + options.vhack.sharkey = { + enable = lib.mkEnableOption "sharkey"; + + fqdn = lib.mkOption { + description = "The fully qualified domain name of this instance."; + type = lib.types.str; + example = "sharkey.shonk.social"; + }; + + package = lib.mkOption { + type = lib.types.package; + default = vhackPackages.sharkey; + defaultText = lib.literalExpression "vhackPackages.sharkey"; + description = "Sharkey package to use."; + }; + + dataDirectory = lib.mkOption { + type = lib.types.path; + default = "/var/lib/sharkey"; + description = "The directory where sharkey stores it's data."; + + # This is already set in the package. + readOnly = true; + }; + + database = { + createLocally = lib.mkOption { + description = "Whether to enable local db creation."; + type = lib.types.bool; + default = true; + }; + + host = lib.mkOption { + type = lib.types.str; + default = "127.0.0.1"; + description = "The database host."; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 5432; + description = "The database port."; + }; + + name = lib.mkOption { + type = lib.types.str; + default = "sharkey"; + description = "The database name in postgresql."; + }; + }; + + settings = lib.mkOption { + inherit (settingsFormat) type; + default = {}; + description = '' + Configuration for Sharkey, see + <link xlink:href="https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/.config/example.yml"/> + for supported settings. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + environment.systemPackages = [cfg.package]; + + vhack = { + nginx.enable = true; + + sharkey.settings = { + id = "aidx"; + + url = "https://${cfg.fqdn}/"; + port = 5312; + + db = { + inherit (cfg.database) host port; + db = cfg.database.name; + user = cfg.database.name; + pass = "sharkey-password"; + }; + redis = { + path = config.services.redis.servers."sharkey".unixSocket; + }; + }; + + persist.directories = [ + { + directory = "${config.services.redis.servers."sharkey".settings.dir}"; + user = "sharkey"; + group = "redis-sharey"; + mode = "0770"; + } + { + directory = "${cfg.dataDirectory}"; + user = "sharkey"; + group = "sharkey"; + mode = "0770"; + } + ]; + }; + + services = { + nginx.virtualHosts."${cfg.fqdn}" = { + locations."/" = { + proxyPass = "http://127.0.0.1:${toString cfg.settings.port}"; + proxyWebsockets = true; + }; + + # proxy_set_header Host $host; + # proxy_http_version 1.1; + # proxy_redirect off; + # + # # If it's behind another reverse proxy or CDN, remove the following. + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_set_header X-Forwarded-Proto https; + # + # # For WebSocket + # proxy_set_header Upgrade $http_upgrade; + # proxy_set_header Connection $connection_upgrade; + # + # # Cache settings + # proxy_cache cache1; + # proxy_cache_lock on; + # proxy_cache_use_stale updating; + # proxy_force_ranges on; + # add_header X-Cache $upstream_cache_status; + + enableACME = true; + forceSSL = true; + }; + + postgresql = lib.mkIf createDB { + enable = true; + settings.port = cfg.database.port; + ensureUsers = [ + { + inherit (cfg.database) name; + ensureDBOwnership = true; + } + ]; + ensureDatabases = [cfg.database.name]; + }; + + redis = { + servers."sharkey" = { + enable = true; + + user = "sharkey"; + + # Disable TCP listening. (We have a UNIX socket) + port = 0; + bind = null; + + settings = { + protected-mode = true; + enable-protected-configs = false; + enable-debug-command = false; + enable-module-command = false; + + supervised = "systemd"; + stop-writes-on-bgsave-error = true; + sanitize-dump-payload = "clients"; + }; + }; + }; + }; + + systemd.services.postgresql.postStart = '' + $PSQL -tAc "ALTER ROLE ${cfg.database.name} WITH ENCRYPTED PASSWORD 'sharkey-password';" + ''; + + systemd.services.sharkey = { + requires = + [ + "redis-sharkey.service" + "network-online.target" + ] + ++ lib.optionals createDB ["postgresql.service"]; + + after = + [ + "redis-sharkey.service" + "network-online.target" + ] + ++ lib.optionals createDB ["postgresql.service"]; + + wantedBy = ["multi-user.target"]; + + environment = { + MISSKEY_CONFIG_YML = "${configFile}"; + NODE_ENV = "production"; + }; + + serviceConfig = { + Type = "simple"; + + StateDirectory = "sharkey"; + StateDirectoryMode = "0700"; + CacheDirectory = "sharkey"; + RuntimeDirectory = "sharkey"; + RuntimeDirectoryMode = "0700"; + ExecStart = "${lib.getExe cfg.package} migrateandstart"; + + TimeoutSec = 60; + Restart = "no"; + + StandardOutput = "journal"; + StandardError = "journal"; + SyslogIdentifier = "sharkey"; + + User = "sharkey"; + Group = "sharkey"; + + # Bind standard privileged ports + AmbientCapabilities = []; + CapabilityBoundingSet = []; + + ReadWritePaths = [ + "${cfg.dataDirectory}" + ]; + + # Hardening + DeviceAllow = [""]; + LockPersonality = true; + # Probably needed for v8's JIT (crashes with it on). + MemoryDenyWriteExecute = false; + PrivateDevices = true; + PrivateUsers = true; + # Sharkey needs access to the hosts CPUs + ProcSubset = "all"; + PrivateTmp = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + RestrictAddressFamilies = [ + "AF_UNIX" # Local communication unix(7) + "AF_INET" # IPv4 Internet protocols ip(7) + "AF_INET6" # IPv6 Internet protocols ipv6(7) + # Needed for nodes `os.networkInterfaces()` function. + "AF_NETLINK" # Kernel user interface device netlink(7) + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "@chown" + "~@mount" + ]; + UMask = "0077"; + }; + }; + + users = { + groups.sharkey = { + gid = config.vhack.constants.ids.gids.sharkey; + }; + users.sharkey = { + isSystemUser = true; + group = "sharkey"; + uid = config.vhack.constants.ids.uids.sharkey; + home = cfg.package; + packages = [cfg.package]; + }; + + groups.redis-sharkey = { + gid = config.vhack.constants.ids.gids.redis-sharkey; + }; + users.redis-sharkey = { + group = "redis-sharkey"; + uid = config.vhack.constants.ids.uids.redis-sharkey; + }; + }; + }; +} diff --git a/modules/by-name/st/stalwart-mail/module.nix b/modules/by-name/st/stalwart-mail/module.nix new file mode 100644 index 0000000..4565bf4 --- /dev/null +++ b/modules/by-name/st/stalwart-mail/module.nix @@ -0,0 +1,428 @@ +{ + lib, + config, + pkgs, + vhackPackages, + ... +}: let + cfg = config.vhack.stalwart-mail; + topCfg = config.services.stalwart-mail; + + configFormat = pkgs.formats.toml {}; + configFile = configFormat.generate "stalwart-mail.toml" topCfg.settings; +in { + imports = [ + ./settings.nix + ]; + + options.vhack.stalwart-mail = { + enable = lib.mkEnableOption "starwart-mail"; + + package = lib.mkPackageOption vhackPackages "stalwart-mail-patched" {}; + + admin = lib.mkOption { + description = '' + Email address to advertise as administrator. This is the address, where dkim, spv + etc. refusal reports are sent to. + + The format should be: `mailto:<name>@<domain>` + ''; + type = lib.types.str; + example = "mailto:dmarc+rua@example.com"; + default = ""; + }; + + fqdn = lib.mkOption { + type = lib.types.str; + example = "mail.foss-syndicate.org"; + description = '' + The fully qualified domain name for this mail server. + ''; + }; + + principals = lib.mkOption { + default = null; + type = lib.types.nullOr (lib.types.listOf (lib.types.submodule { + options = { + name = lib.mkOption { + type = lib.types.str; + description = "Specifies the username of the account"; + }; + + class = lib.mkOption { + type = lib.types.enum ["individual" "admin"]; + description = "Specifies the account type"; + }; + + description = lib.mkOption { + type = lib.types.str; + description = "Provides a description or full name for the user"; + default = ""; + }; + + secret = lib.mkOption { + type = let + prefix = pre: lib.types.strMatching "^${lib.strings.escapeRegex pre}.*"; + in + lib.types.oneOf [ + (prefix "$argon2") + (prefix "$pbkdf2") + (prefix "$scrypt") + (prefix "$2") # bcrypt + (prefix "$6$") # sha-512 + (prefix "$5$") # sha-256 + (prefix "$sha1") + (prefix "$1") # md5 + (prefix "_") # BSDi crypt + (prefix "{SHA}") # base64 sha + (prefix "{SSHA}") # base64 salted sha + + # unix crypt + (prefix "{CRYPT}") + (prefix "{crypt}") + + # Plain text + (prefix "{PLAIN}") + (prefix "{plain}") + (prefix "{CLEAR}") + (prefix "{clear}") + ]; + description = '' + Sets the password for the user account. + Passwords can be stored hashed or in plain text (not recommended). + See <https://stalw.art/docs/auth/authentication/password/> for a description + of password encoding. + ''; + }; + email = lib.mkOption { + type = lib.types.listOf lib.types.str; + description = '' + A list of email addresses associated with the user. + The first address in the list is considered the primary address. + ''; + }; + }; + })); + }; + + dataDirectory = lib.mkOption { + description = '' + The directory in which to store all storage things. + ''; + default = "/var/lib/stalwart-mail"; + type = lib.types.path; + readOnly = true; + }; + + openFirewall = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether to open TCP firewall ports, which are specified in + {option}`services.stalwart-mail.settings.listener` on all interfaces. + ''; + }; + + security = lib.mkOption { + type = lib.types.nullOr (lib.types.submodule { + options = { + verificationMode = lib.mkOption { + type = lib.types.enum ["relaxed" "strict"]; + description = '' + Whether to allow invalid signatures/checks or not. + ''; + default = "relaxed"; + }; + + dkimKeys = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule { + options = { + dkimPublicKey = lib.mkOption { + type = lib.types.str; + description = '' + The base 64 encoded representation of the public dkim key. + ''; + }; + dkimPrivateKeyPath = lib.mkOption { + type = lib.types.path; + description = '' + The path to the dkim private key agenix file. + Generate it via the `./gen_key` script: + ''; + }; + keyAlgorithm = lib.mkOption { + type = lib.types.enum ["ed25519-sha256" "rsa-sha-256" "rsa-sha-1"]; + description = "The algorithm of the used key"; + }; + }; + }); + description = '' + Which key to use for which domain. The attr keys are the domains + ''; + default = {}; + }; + }; + }); + description = '' + Security options. This should only be set to `null` when testing. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = cfg.admin != ""; + message = "You need to specify an admin address."; + } + ]; + + vhack.nginx.enable = true; + services = { + stalwart-mail = { + # NOTE(@bpeetz): We do not use the NixOS service, as it comes with too much + # bothersome default configuration and not really any useful configuration. + # However, this decision could obviously be reversed in the future. <2025-02-08> + enable = false; + inherit (cfg) package; + }; + + nginx.virtualHosts."${cfg.fqdn}" = { + locations."/" = { + proxyPass = "http://${builtins.elemAt config.services.stalwart-mail.settings.server.listener.http.bind 0}"; + recommendedProxySettings = true; + }; + + useACMEHost = "${cfg.fqdn}"; + forceSSL = true; + }; + + redis = { + servers = { + "stalwart-mail" = { + enable = true; + + user = "stalwart-mail"; + + # Disable TCP listening. (We have a UNIX socket) + port = 0; + bind = null; + + settings = { + protected-mode = true; + enable-protected-configs = false; + enable-debug-command = false; + enable-module-command = false; + + supervised = "systemd"; + stop-writes-on-bgsave-error = true; + sanitize-dump-payload = "clients"; + }; + }; + }; + }; + }; + security.acme.certs = { + "${cfg.fqdn}" = { + domain = cfg.fqdn; + group = "stalwart-mail-certificates"; + }; + }; + + age.secrets = let + keys = + lib.mapAttrs' ( + keyDomain: keyConfig: + lib.nameValuePair "stalwartMail${keyDomain}" + { + file = keyConfig.dkimPrivateKeyPath; + mode = "600"; + owner = "stalwart-mail"; + group = "stalwart-mail"; + } + ) + cfg.security.dkimKeys; + in + lib.mkIf (cfg.security != null) keys; + + vhack.persist.directories = [ + { + directory = "${cfg.dataDirectory}"; + user = "stalwart-mail"; + group = "stalwart-mail"; + mode = "0700"; + } + { + directory = "${config.services.redis.servers."stalwart-mail".settings.dir}"; + user = "stalwart-mail"; + group = "redis-stalwart-mail"; + mode = "0770"; + } + ]; + + # This service stores a potentially large amount of data. + # Running it as a dynamic user would force chown to be run every time the + # service is restarted on a potentially large number of files. + # That would cause unnecessary and unwanted delays. + users = { + groups = { + stalwart-mail = { + gid = config.vhack.constants.ids.gids.stalwart-mail; + }; + stalwart-mail-certificates = { + gid = config.vhack.constants.ids.gids.stalwart-mail-certificates; + }; + redis-stalwart-mail = { + gid = config.vhack.constants.ids.gids.redis-stalwart-mail; + }; + }; + users = { + nginx = { + extraGroups = ["stalwart-mail-certificates"]; + }; + stalwart-mail = { + isSystemUser = true; + group = "stalwart-mail"; + uid = config.vhack.constants.ids.uids.stalwart-mail; + extraGroups = ["stalwart-mail-certificates"]; + }; + redis-stalwart-mail = { + group = "redis-stalwart-mail"; + uid = config.vhack.constants.ids.uids.redis-stalwart-mail; + }; + }; + }; + + systemd.tmpfiles.rules = [ + "d '${cfg.dataDirectory}' - stalwart-mail stalwart-mail - -" + ]; + + systemd = { + services.stalwart-mail = { + wantedBy = ["multi-user.target"]; + requires = + [ + "redis-stalwart-mail.service" + "network-online.target" + ] + ++ (lib.optional (cfg.security != null) "acme-${cfg.fqdn}.service"); + after = [ + "local-fs.target" + "network.target" + "network-online.target" + "redis-stalwart-mail.service" + "acme-${cfg.fqdn}.service" + ]; + conflicts = [ + "postfix.service" + "sendmail.service" + "exim4.service" + ]; + description = "Stalwart Mail Server"; + + environment = { + SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt"; + NIX_SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt"; + }; + + preStart = let + esa = lib.strings.escapeShellArg; + mkTmpFile = path: "[ -d ${esa path} ] || mkdir --parents ${esa path}"; + + # Create the directories for stalwart + storageDirectories = lib.lists.filter (v: v != null) (lib.attrsets.mapAttrsToList (_: {path ? null, ...}: + if (path != null) + then mkTmpFile path + else null) + topCfg.settings.store); + in + '' + # Stalwart actually wants to store _data_ (e.g., blocked ips) in it's own config file. + # Thus we need to make it writable. + cat ${esa configFile} >$CACHE_DIRECTORY/mutable_config_file.toml + '' + + (builtins.concatStringsSep "\n" storageDirectories); + + serviceConfig = { + ExecStart = pkgs.writers.writeDash "start-stalwart-mail" '' + ${lib.getExe cfg.package} --config="$CACHE_DIRECTORY/mutable_config_file.toml" + ''; + + Restart = "no"; + + KillMode = "process"; + KillSignal = "SIGINT"; + + Type = "simple"; + LimitNOFILE = 65536; + + StandardOutput = "journal"; + StandardError = "journal"; + + ReadWritePaths = [ + cfg.dataDirectory + ]; + CacheDirectory = "stalwart-mail"; + StateDirectory = "stalwart-mail"; + + User = "stalwart-mail"; + Group = "stalwart-mail"; + + SyslogIdentifier = "stalwart-mail"; + + # Bind standard privileged ports + AmbientCapabilities = ["CAP_NET_BIND_SERVICE"]; + CapabilityBoundingSet = ["CAP_NET_BIND_SERVICE"]; + + # Hardening + DeviceAllow = [""]; + LockPersonality = true; + MemoryDenyWriteExecute = true; + PrivateDevices = true; + PrivateUsers = false; # incompatible with CAP_NET_BIND_SERVICE + ProcSubset = "pid"; + PrivateTmp = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged" + ]; + UMask = "0077"; + }; + }; + }; + + # Make admin commands available in the shell + environment.systemPackages = [cfg.package]; + + networking.firewall = let + parsePorts = listeners: let + parseAddresses = listeners: lib.flatten (lib.mapAttrsToList (name: value: value.bind) listeners); + splitAddress = addr: lib.splitString ":" addr; + extractPort = addr: lib.toInt (builtins.foldl' (a: b: b) "" (splitAddress addr)); + in + builtins.map extractPort (parseAddresses listeners); + in + lib.mkIf (cfg.openFirewall && (builtins.hasAttr "listener" topCfg.settings.server)) + { + allowedTCPPorts = parsePorts topCfg.settings.server.listener; + }; + }; +} diff --git a/modules/by-name/st/stalwart-mail/settings.nix b/modules/by-name/st/stalwart-mail/settings.nix new file mode 100644 index 0000000..907cea9 --- /dev/null +++ b/modules/by-name/st/stalwart-mail/settings.nix @@ -0,0 +1,552 @@ +{ + config, + lib, + pkgs, + ... +}: let + cfg = config.vhack.stalwart-mail; + + signaturesByDomain = + (builtins.map ({name, ...}: { + "if" = "sender_domain = '${name}'"; + "then" = "'${name}'"; + }) + (lib.attrsToList cfg.security.dkimKeys)) + ++ [{"else" = false;}]; + + maybeVerificationMode = + if cfg.security != null + then cfg.security.verificationMode + else "disable"; + + directory = + if cfg.principals == null + then "internal" + else "in-memory"; +in { + config.services.stalwart-mail.settings = lib.mkIf cfg.enable { + # https://www.rfc-editor.org/rfc/rfc6376.html#section-3.3 + signature = let + signatures = + lib.mapAttrs (keyDomain: keyConfig: { + private-key = "%{file:${config.age.secrets."stalwartMail${keyDomain}".path}}%"; + + domain = keyDomain; + + selector = "mail"; + headers = ["From" "To" "Cc" "Date" "Subject" "Message-ID" "Organization" "MIME-Version" "Content-Type" "In-Reply-To" "References" "List-Id" "User-Agent" "Thread-Topic" "Thread-Index"]; + algorithm = keyConfig.keyAlgorithm; + + # How do we canonicalize the headers/body? + # https://www.rfc-editor.org/rfc/rfc6376.html#section-3.4 + canonicalization = "simple/simple"; + + expire = "50d"; + report = true; + }) + cfg.security.dkimKeys; + in + lib.mkIf (cfg.security != null) signatures; + + auth = let + # NOTE(@bpeetz): We disable all the checks if the `listener` is submissions, because the + # user's email client will obviously not have the right IP address to pass SPF or + # IPREV. It will also not be able to sign the message with DKIM (as we has to key). <2025-02-25> + ifNotSmpt = valueTrue: valueFalse: [ + { + "if" = "listener != 'submissions'"; + "then" = valueTrue; + } + {"else" = valueFalse;} + ]; + in { + iprev = { + verify = ifNotSmpt maybeVerificationMode "disable"; + }; + spf = { + verify = { + ehlo = ifNotSmpt maybeVerificationMode "disable"; + + mail-from = ifNotSmpt maybeVerificationMode "disable"; + }; + }; + dmarc = { + verify = ifNotSmpt maybeVerificationMode "disable"; + }; + arc = { + seal = lib.mkIf (cfg.security != null) signaturesByDomain; + verify = ifNotSmpt maybeVerificationMode "disable"; + }; + dkim = { + verify = ifNotSmpt maybeVerificationMode "disable"; + + # Ignore insecure dkim signed messages (i.e., messages containing both + # signed and appended not-signed content.) + strict = true; + + sign = + lib.mkIf (cfg.security != null) signaturesByDomain; + }; + }; + report = { + domain = "${cfg.fqdn}"; + submitter = "'${cfg.fqdn}'"; + analysis = { + addresses = ["dmarc@*" "abuse@*"]; + forward = true; + store = "30d"; + }; + tls.aggregate = { + from-name = "'TLS Report'"; + from-address = "'noreply-tls@${cfg.fqdn}'"; + org-name = "'Foss Syndicate Mail Handling'"; + contact-info = "'${cfg.admin}'"; + send = "daily"; + max-size = 26214400; # 25 MiB + sign = lib.mkIf (cfg.security != null) "'${cfg.fqdn}'"; + }; + dmarc = { + aggregate = { + from-name = "'DMARC Report'"; + from-address = "'noreply-dmarc@${cfg.fqdn}'"; + org-name = "'Foss Syndicate Mail Handling'"; + contact-info = "'${cfg.admin}'"; + send = "weekly"; + max-size = 26214400; # 25MiB + sign = lib.mkIf (cfg.security != null) "'${cfg.fqdn}'"; + }; + from-name = "'Report Subsystem'"; + from-address = "'noreply-dmarc@${cfg.fqdn}'"; + subject = "'DMARC Authentication Failure Report'"; + send = "1/1d"; + sign = lib.mkIf (cfg.security != null) signaturesByDomain; + }; + spf = { + from-name = "'Report Subsystem'"; + from-address = "'noreply-spf@${cfg.fqdn}'"; + subject = "'SPF Authentication Failure Report'"; + send = "1/1d"; + sign = lib.mkIf (cfg.security != null) signaturesByDomain; + }; + dkim = { + from-name = "'Report Subsystem'"; + from-address = "'noreply-dkim@${cfg.fqdn}'"; + subject = "'DKIM Authentication Failure Report'"; + send = "1/1d"; + sign = lib.mkIf (cfg.security != null) signaturesByDomain; + }; + dsn = { + from-name = "'Mail Delivery Subsystem'"; + from-address = "'MAILER-DAEMON@${cfg.fqdn}'"; + sign = lib.mkIf (cfg.security != null) signaturesByDomain; + }; + }; + queue = { + schedule = { + retry = "[2m, 5m, 10m, 15m, 30m, 1h, 2h]"; + notify = "[2h, 7h, 1d, 3d]"; + expire = "5d"; + }; + outbound = { + tls = { + starttls = + if maybeVerificationMode == "strict" + then "require" + else "optional"; + allow-invalid-certs = false; + ip-strategy = "ipv6_then_ipv4"; + mta-sts = + if maybeVerificationMode == "strict" + then "require" + else "optional"; + }; + }; + }; + resolver = { + type = "system"; + preserve-intermediates = true; + concurrency = 2; + timeout = "5s"; + attempts = 2; + try-tcp-on-error = true; + public-suffix = [ + "file://${pkgs.publicsuffix-list}/share/publicsuffix/public_suffix_list.dat" + ]; + }; + + spam-filter = { + enable = true; + header = { + status = { + enable = true; + name = "X-Spam-Status"; + }; + result = { + enable = true; + name = "X-Spam-Result"; + }; + }; + bayes = { + enable = true; + + # Learn from users putting mail into JUNK or taking mail out of it. + account = { + enable = true; + }; + }; + + # Fetch the newest spam-filter rules not from github, but from the nix + # package. + resource = "file://${cfg.package.passthru.spamfilter}/spam-filter.toml"; + auto-update = false; + }; + + webadmin = { + # Fetch the newest webadmin bundle not from github, but from the nix + # package. + resource = "file://${cfg.package.passthru.webadmin}/webadmin.zip"; + auto-update = false; + path = "/var/cache/stalwart-mail"; + }; + + session = { + milter = { + # TODO: Add this <2025-02-07> + # "clamav" = { + # enable = true; + # hostname = "127.0.0.1"; + # port = 15112; + # tls = false; + # allow-invalid-certs = false; + # }; + }; + ehlo = { + require = true; + }; + rcpt = { + directory = "'${directory}'"; + catch-all = true; + subaddressing = true; + }; + data = { + spam-filter = true; + add-headers = { + received = true; + received-spf = true; + auth-results = true; + message-id = true; + date = true; + return-path = true; + delivered-to = true; + }; + auth = { + mechanisms = ["LOGIN" "PLAIN"]; + directory = "'${directory}'"; + require = true; + must-match-sender = true; + errors = { + total = 3; + wait = "5s"; + }; + }; + }; + extensions = { + pipelining = true; + chunking = true; + requiretls = true; + no-soliciting = ""; + dsn = [ + { + "if" = "!is_empty(authenticated_as)"; + "then" = true; + } + {"else" = false;} + ]; + future-release = [ + { + "if" = "!is_empty(authenticated_as)"; + "then" = "7d"; + } + {"else" = false;} + ]; + deliver-by = [ + { + "if" = "!is_empty(authenticated_as)"; + "then" = "15d"; + } + {"else" = false;} + ]; + mt-priority = [ + { + "if" = "!is_empty(authenticated_as)"; + "then" = "mixer"; + } + {"else" = false;} + ]; + vrfy = [ + { + "if" = "!is_empty(authenticated_as)"; + "then" = true; + } + {"else" = false;} + ]; + expn = [ + { + "if" = "!is_empty(authenticated_as)"; + "then" = true; + } + {"else" = false;} + ]; + }; + }; + + jmap = { + account = { + purge.frequency = "0 0 *"; + }; + protocol = { + changes.max-history = "14d"; + }; + email = { + # NOTE(@bpeetz): We probably want to enable the auto-deletion of emails in + # the "Junk" and "Deleted" items mail folders, but this should be + # communicated to the users. <2025-02-07> + auto-expunge = false; + }; + mailbox = { + max-depth = 50; + max-name-length = 255; + }; + folders = let + mkFolder = name: { + inherit name; + create = true; + subscribe = true; + }; + in { + inbox = mkFolder "INBOX"; + drafts = mkFolder "DRAFTS"; + sent = mkFolder "SENT"; + trash = mkFolder "TRASH"; + archive = mkFolder "ARCHIVE"; + junk = mkFolder "JUNK"; + shared = {name = "SHARED";}; + }; + }; + imap = { + auth = { + # Allow password login over non tls connection + allow-plain-text = false; + }; + }; + + server = { + hostname = cfg.fqdn; + + listener = { + # HTTP (used for jmap) + "http" = { + bind = ["127.0.0.1:8112"]; + protocol = "http"; + # handled by ngnix + tls.implicit = false; + }; + + # IMAP + "imap" = { + bind = ["[::]:993"]; + protocol = "imap"; + tls.implicit = true; + }; + + # SMTP + "submissions" = { + bind = ["[::]:465"]; + protocol = "smtp"; + tls.implicit = true; + }; + "input" = { + bind = ["[::]:25"]; + protocol = "smtp"; + tls = { + enable = true; + # Require an explicit `STARTTLS` + implicit = false; + }; + }; + + # # POP3 (should be disabled, unless there is a real reason to use it) + # "pop3" = { + # bind = ["[::]:995"]; + # protocol = "pop3"; + # tls.implicit = true; + # }; + + # # LMTP + # "lmtp" = { + # bind = ["[::]:24"]; + # protocol = "lmtp"; + # }; + + # ManageSieve + "managesieve" = { + bind = ["[::]:4190"]; + protocol = "managesieve"; + tls.implicit = true; + }; + }; + + tls = { + enable = true; + + # Expect the client connection to be encrypted from the start (i.e., + # without STARTTLS) + implicit = true; + + certificate = "default"; + }; + + http = { + url = "protocol + '://' + config_get('server.hostname') + ':' + local_port"; + + # We are behind a nginx proxy, and can thus trust this header. + use-x-forwarded = true; + }; + + auto-ban = { + # Ban if the same IP fails to login 10 times in a day + rate = "10/1d"; + + # Ban the login for an user account, if different IP-Addresses tried and + # failed to login 100 times in single day + auth.rate = "100/1d"; + + abuse.rate = "35/1d"; + + loiter.rate = "150/1d"; + + scan.rate = "150/1d"; + }; + + cache = let + MiB = 1024 * 1024; + in { + access-token.size = 10 * MiB; + http-auth.size = 1 * MiB; + permission.size = 5 * MiB; + account.size = 10 * MiB; + mailbox.size = 10 * MiB; + thread.size = 10 * MiB; + bayes.size = 10 * MiB; + dns = { + txt.size = 5 * MiB; + mx.size = 5 * MiB; + ptr.size = 1 * MiB; + ipv4.size = 5 * MiB; + ipv6.size = 5 * MiB; + tlsa.size = 1 * MiB; + mta-sts.size = 1 * MiB; + rbl.size = 5 * MiB; + }; + }; + }; + + tracer = { + # NOTE(@bpeetz): + # We are using the console logger, because that has nice color output. + # Simply using the console should be fine, as systemd pipes that to the journal + # either way. <2025-02-08> + console = { + enable = true; + ansi = true; + level = "info"; + type = "console"; + }; + }; + + store = { + "rocksdb-data" = { + type = "rocksdb"; + path = "${cfg.dataDirectory}/storage/data"; + compression = "lz4"; + + # Perform “maintenance” every day at 3 am local time. + purge.frequency = "0 3 *"; + }; + "rocksdb-directory" = lib.mkIf (cfg.principals == null) { + type = "rocksdb"; + path = "${cfg.dataDirectory}/storage/directory"; + compression = "lz4"; + + # Perform “maintenance” every day at 1 am local time. + purge.frequency = "0 1 *"; + }; + "rocksdb-full-text-search" = { + type = "rocksdb"; + path = "${cfg.dataDirectory}/storage/full-text-search"; + compression = "lz4"; + + # Perform “maintenance” every day at 2 am local time. + purge.frequency = "0 2 *"; + }; + "file-system" = { + type = "fs"; + path = "${cfg.dataDirectory}/storage/blobs"; + depth = 2; + compression = "lz4"; + + # Perform “maintenance” every day at 5:30 am local time. + purge.frequency = "30 5 *"; + }; + "redis" = { + type = "redis"; + redis-type = "single"; + urls = "unix://${config.services.redis.servers."stalwart-mail".unixSocket}"; + timeout = "10s"; + + # Perform “maintenance” every day at 2:30 am local time. + purge.frequency = "30 2 *"; + }; + }; + storage = { + # PostgreSQL is an option, but this is recommended for single node + # configurations. + data = "rocksdb-data"; + + # We could also re-use the data storage backend for that. + blob = "file-system"; + + full-text.default-language = "en"; + fts = "rocksdb-full-text-search"; + + directory = "${directory}"; + + lookup = "redis"; + + # NOTE(@bpeetz): This will encrypt all emails with the users pgp key (if it + # can be determined.) This is a wonderful feature, but quite tiresome, if + # the user intends to read their email without a their pgp key present (for + # example via their smartphone.) <2025-02-07> + encryption.enable = false; + }; + + directory = { + "in-memory" = lib.mkIf (cfg.principals != null) { + type = "memory"; + inherit (cfg) principals; + }; + "internal" = lib.mkIf (cfg.principals == null) { + type = "internal"; + store = "rocksdb-directory"; + }; + }; + + certificate = { + "default" = { + cert = "%{file:${config.security.acme.certs.${cfg.fqdn}.directory}/fullchain.pem}%"; + private-key = "%{file:${config.security.acme.certs.${cfg.fqdn}.directory}/key.pem}%"; + default = true; + }; + }; + }; +} diff --git a/modules/by-name/sy/system-info/module.nix b/modules/by-name/sy/system-info/module.nix new file mode 100644 index 0000000..8136ae5 --- /dev/null +++ b/modules/by-name/sy/system-info/module.nix @@ -0,0 +1,79 @@ +{ + lib, + config, + pkgs, + ... +}: let + mkVirtualHostDisplay = name: value: let + aliases = + if value.serverAliases != [] + then + ": " + + builtins.concatStringsSep " " value.serverAliases + else ""; + in '' + ${name}${aliases} + ''; + vHosts = builtins.concatStringsSep "" (builtins.attrValues (builtins.mapAttrs mkVirtualHostDisplay config.services.nginx.virtualHosts)); + + mkOpenPortDisplay = mode: port: let + checkEnabled = service: name: + if config.vhack.${service}.enable + then name + else "<port is '${name}' but service 'vhack.${service}' is not enabled.>"; + mappings = { + "22" = checkEnabled "openssh" "ssh"; + "80" = checkEnabled "nginx" "http"; + "443" = checkEnabled "nginx" "https"; + + "53" = checkEnabled "dns" "dns"; + + "24" = checkEnabled "mail" "mail-lmtp"; + "465" = checkEnabled "mail" "mail-smtp-tls"; + "25" = checkEnabled "mail" "mail-smtp"; + "993" = checkEnabled "mail" "mail-imap-tls"; + "995" = checkEnabled "mail" "mail-pop3-tls"; + + "10222" = checkEnabled "taskchampion-sync" "taskchampion-sync"; + + # TODO(@bpeetz): Check which service opens these ports: <2025-01-28> + "64738" = "???"; + "4190" = "???"; + }; + in '' + ${mode} ${builtins.toString port}: ${ + if (builtins.hasAttr "${builtins.toString port}" mappings) + then mappings.${builtins.toString port} + else + builtins.throw + "'${builtins.toString port}' is still missing from the system info port -> name map. Maybe add it?" + } + ''; + + # TODO(@bpeetz): This should probably also include the allowed TCP/UDP port ranges. <2025-01-28> + openTCPPorts = builtins.concatStringsSep "" (builtins.map (mkOpenPortDisplay "TCP") config.networking.firewall.allowedTCPPorts); + openUDPPorts = builtins.concatStringsSep "" (builtins.map (mkOpenPortDisplay "UDP") config.networking.firewall.allowedUDPPorts); + + markdown = pkgs.writeText "${config.networking.hostName}-system-info.md" '' + ## Virtual Hosts + ${vHosts} + ## Open ports + ${openTCPPorts} + ${openUDPPorts} + ''; +in { + options.vhack.system-info = { + markdown = lib.mkOption { + type = lib.types.package; + description = '' + A derivation, that builds a markdown file, showing relevant system + information for this host. + ''; + readOnly = true; + }; + }; + + config.vhack.system-info = { + inherit markdown; + }; +} diff --git a/modules/by-name/ta/taskchampion-sync/module.nix b/modules/by-name/ta/taskchampion-sync/module.nix new file mode 100644 index 0000000..a722883 --- /dev/null +++ b/modules/by-name/ta/taskchampion-sync/module.nix @@ -0,0 +1,53 @@ +{ + config, + lib, + ... +}: let + cfg = config.vhack.taskchampion-sync; + dataDirectory = "/var/lib/taskchampion-sync-server"; +in { + options.vhack.taskchampion-sync = { + enable = lib.mkEnableOption "taskchampion-sync"; + + fqdn = lib.mkOption { + description = "The fully qualified domain name of this instance."; + type = lib.types.str; + example = "task-sync.tw.online"; + }; + }; + + config = lib.mkIf cfg.enable { + users = { + users.taskchampion.uid = config.vhack.constants.ids.uids.taskchampion; + groups.taskchampion.gid = config.vhack.constants.ids.uids.taskchampion; + }; + + vhack = { + persist.directories = [ + { + directory = dataDirectory; + user = "taskchampion"; + group = "taskchampion"; + mode = "0700"; + } + ]; + nginx.enable = true; + }; + + services = { + taskchampion-sync-server = { + enable = true; + dataDir = dataDirectory; + }; + + nginx.virtualHosts."${cfg.fqdn}" = { + locations."/" = { + proxyPass = "http://127.0.0.1:${toString config.services.taskchampion-sync-server.port}"; + recommendedProxySettings = true; + }; + enableACME = true; + forceSSL = true; + }; + }; + }; +} diff --git a/modules/by-name/us/users/module.nix b/modules/by-name/us/users/module.nix index a197b13..4be3f41 100644 --- a/modules/by-name/us/users/module.nix +++ b/modules/by-name/us/users/module.nix @@ -3,45 +3,51 @@ lib, pkgs, ... -}: let +}: +let cfg = config.vhack.users; - mkUser = { - name, - password, - uid, - sshKey, - }: { - inherit name; - value = { - inherit name uid; - isNormalUser = true; - home = "/home/${name}"; - hashedPassword = password; - extraGroups = [ - "wheel" - ]; - openssh.authorizedKeys.keys = [ - sshKey - ]; - }; - }; - - extraUsers = lib.listToAttrs (builtins.map mkUser [ + mkUser = { - name = "soispha"; - password = "$y$jFT$3.8XmUyukZvpExMUxDZkI.$IVrJgm8ysNDF/0vDD2kF6w73ozXgr1LMVRNN4Bq7pv1"; - sshKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIME4ZVa+IoZf6T3U08JG93i6QIAJ4amm7mkBzO14JSkz cardno:000F_18F83532"; - uid = 1000; - } + name, + password, + uid, + sshKey, + }: { - name = "sils"; - password = "$y$jFT$KpFnahVCE9JbE.5P3us8o.$ZzSxCusWqe3sL7b6DLgOXNNUf114tiiptM6T8lDxtKC"; - sshKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAe4o1PM6VasT3KZNl5NYvgkkBrPOg36dqsywd10FztS openpgp:0x21D20D6A"; - uid = 1001; - } - ]); -in { + inherit name; + value = { + inherit name uid; + isNormalUser = true; + home = "/home/${name}"; + hashedPassword = password; + extraGroups = [ + "wheel" + ]; + openssh.authorizedKeys.keys = [ + sshKey + ]; + }; + }; + + extraUsers = lib.listToAttrs ( + builtins.map mkUser [ + { + name = "soispha"; + password = "$y$jFT$3.8XmUyukZvpExMUxDZkI.$IVrJgm8ysNDF/0vDD2kF6w73ozXgr1LMVRNN4Bq7pv1"; + sshKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIME4ZVa+IoZf6T3U08JG93i6QIAJ4amm7mkBzO14JSkz cardno:000F_18F83532"; + uid = 1000; + } + { + name = "sils"; + password = "$y$jFT$KpFnahVCE9JbE.5P3us8o.$ZzSxCusWqe3sL7b6DLgOXNNUf114tiiptM6T8lDxtKC"; + sshKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILn7Oumr5IYtTTIKRFvDnofGXXiDLBQE9jVF+7UE+4G5 vhack.eu"; + uid = 1001; + } + ] + ); +in +{ options.vhack.users = { enable = lib.mkEnableOption "user setup"; }; @@ -51,14 +57,12 @@ in { mutableUsers = false; defaultUserShell = pkgs.bashInteractive; - users = - { - root = { - hashedPassword = lib.mkForce null; # to lock root - openssh.authorizedKeys.keys = lib.mkForce []; - }; - } - // extraUsers; + users = { + root = { + hashedPassword = lib.mkForce null; # to lock root + openssh.authorizedKeys.keys = lib.mkForce [ ]; + }; + } // extraUsers; # TODO(@bpeetz): Is this still relevant? # If it is, it should be moved to a separate module. <2024-12-24> |