aboutsummaryrefslogtreecommitdiffstats
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/by-name/an/anubis/module.nix94
-rw-r--r--modules/by-name/at/atuin-sync/module.nix37
-rw-r--r--modules/by-name/ba/back/module.nix121
-rw-r--r--modules/by-name/co/constants/module.nix106
-rw-r--r--modules/by-name/dn/dns/dns/default.nix13
-rw-r--r--modules/by-name/dn/dns/dns/types/default.nix16
-rw-r--r--modules/by-name/dn/dns/dns/types/record.nix75
-rw-r--r--modules/by-name/dn/dns/dns/types/records/A.nix19
-rw-r--r--modules/by-name/dn/dns/dns/types/records/AAAA.nix19
-rw-r--r--modules/by-name/dn/dns/dns/types/records/CAA.nix42
-rw-r--r--modules/by-name/dn/dns/dns/types/records/CNAME.nix27
-rw-r--r--modules/by-name/dn/dns/dns/types/records/DKIM.nix75
-rw-r--r--modules/by-name/dn/dns/dns/types/records/DMARC.nix108
-rw-r--r--modules/by-name/dn/dns/dns/types/records/DNAME.nix15
-rw-r--r--modules/by-name/dn/dns/dns/types/records/DNSKEY.nix63
-rw-r--r--modules/by-name/dn/dns/dns/types/records/DS.nix48
-rw-r--r--modules/by-name/dn/dns/dns/types/records/HTTPS.nix5
-rw-r--r--modules/by-name/dn/dns/dns/types/records/MTA-STS.nix42
-rw-r--r--modules/by-name/dn/dns/dns/types/records/MX.nix32
-rw-r--r--modules/by-name/dn/dns/dns/types/records/NS.nix24
-rw-r--r--modules/by-name/dn/dns/dns/types/records/OPENPGPKEY.nix18
-rw-r--r--modules/by-name/dn/dns/dns/types/records/PTR.nix92
-rw-r--r--modules/by-name/dn/dns/dns/types/records/SOA.nix65
-rw-r--r--modules/by-name/dn/dns/dns/types/records/SRV.nix51
-rw-r--r--modules/by-name/dn/dns/dns/types/records/SSHFP.nix39
-rw-r--r--modules/by-name/dn/dns/dns/types/records/SVCB.nix100
-rw-r--r--modules/by-name/dn/dns/dns/types/records/TLSA.nix50
-rw-r--r--modules/by-name/dn/dns/dns/types/records/TXT.nix24
-rw-r--r--modules/by-name/dn/dns/dns/types/records/default.nix43
-rw-r--r--modules/by-name/dn/dns/dns/types/records/dnssec.nix48
-rw-r--r--modules/by-name/dn/dns/dns/types/simple.nix9
-rw-r--r--modules/by-name/dn/dns/dns/types/zone.nix119
-rw-r--r--modules/by-name/dn/dns/dns/util/default.nix76
-rw-r--r--modules/by-name/dn/dns/module.nix86
-rw-r--r--modules/by-name/et/etesync/module.nix14
-rw-r--r--modules/by-name/gi/git-back/module.nix33
-rw-r--r--modules/by-name/gi/git-server/module.nix11
-rw-r--r--modules/by-name/gr/grocy/module.nix51
-rw-r--r--modules/by-name/in/invidious-router/module.nix4
-rw-r--r--modules/by-name/ma/mail/module.nix30
-rw-r--r--modules/by-name/ma/mastodon/module.nix27
-rw-r--r--modules/by-name/ma/matrix/module.nix80
-rw-r--r--modules/by-name/mu/murmur/module.nix7
-rw-r--r--modules/by-name/ne/nextcloud/module.nix87
-rw-r--r--modules/by-name/ng/nginx/module.nix7
-rw-r--r--modules/by-name/ni/nix-sync/hosts.nix48
-rw-r--r--modules/by-name/ni/nix-sync/module.nix70
-rw-r--r--modules/by-name/re/redlib/module.nix18
-rw-r--r--modules/by-name/ro/rocie/module.nix59
-rw-r--r--modules/by-name/ru/rust-motd/module.nix32
-rw-r--r--modules/by-name/sh/sharkey/module.nix135
-rw-r--r--modules/by-name/st/stalwart-mail/module.nix428
-rw-r--r--modules/by-name/st/stalwart-mail/settings.nix552
-rw-r--r--modules/by-name/sy/system-info/module.nix81
-rw-r--r--modules/by-name/ta/taskchampion-sync/module.nix66
-rw-r--r--modules/by-name/us/users/module.nix30
56 files changed, 3325 insertions, 346 deletions
diff --git a/modules/by-name/an/anubis/module.nix b/modules/by-name/an/anubis/module.nix
new file mode 100644
index 0000000..4e70e4f
--- /dev/null
+++ b/modules/by-name/an/anubis/module.nix
@@ -0,0 +1,94 @@
+{
+ config,
+ lib,
+ ...
+}: let
+ cfg = config.vhack.anubis;
+
+ anubisInstances =
+ lib.mapAttrs (domain: conf: {
+ settings = {
+ TARGET = conf.target;
+ BIND = "/run/anubis/anubis-${domain}/anubis.sock";
+ METRICS_BIND = "/run/anubis/anubis-${domain}/anubis-metrics.sock";
+ };
+ })
+ cfg.instances;
+
+ nginxVirtualHosts = lib.mapAttrs' (domain: conf:
+ lib.nameValuePair domain {
+ locations."/" = {
+ proxyPass = "http://unix:${config.services.anubis.instances."${domain}".settings.BIND}";
+
+ recommendedProxySettings = true;
+ proxyWebsockets = true;
+ };
+
+ enableACME = true;
+ forceSSL = true;
+ })
+ cfg.instances;
+in {
+ options.vhack.anubis.instances = lib.mkOption {
+ description = ''
+ Protect this reverse proxy with anubis.
+
+ The attr key is the subdomain, the value the config.
+ '';
+
+ type = lib.types.attrsOf (lib.types.submodule {
+ options = {
+ target = lib.mkOption {
+ description = "nginx `proxyPass` target";
+ type = lib.types.str;
+ example = "http://127.0.0.1:8080";
+ };
+ };
+ config = {};
+ });
+
+ default = {};
+
+ example = lib.literalExample ''
+ {
+ target = "http://127.0.0.1:$${toString config.servies.<name>.port}";
+ }
+ '';
+ };
+
+ config = {
+ users = {
+ users.nginx.extraGroups = [
+ config.services.anubis.defaultOptions.group
+ ];
+
+ users.anubis = {
+ uid = config.vhack.constants.ids.uids.anubis;
+ group = "anubis";
+ };
+ groups.anubis.gid = config.vhack.constants.ids.gids.anubis;
+ };
+
+ vhack.nginx = lib.mkIf (cfg.instances != {}) {
+ enable = true;
+ };
+
+ services = {
+ anubis = {
+ defaultOptions.settings.COOKIE_DYNAMIC_DOMAIN = true;
+ instances = anubisInstances;
+ };
+
+ nginx = {
+ enable = true;
+
+ recommendedTlsSettings = true;
+ recommendedOptimisation = true;
+ recommendedGzipSettings = true;
+ recommendedProxySettings = true;
+
+ virtualHosts = nginxVirtualHosts;
+ };
+ };
+ };
+}
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..e0d75bb
--- /dev/null
+++ b/modules/by-name/at/atuin-sync/module.nix
@@ -0,0 +1,37 @@
+{
+ 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;
+
+ vhack.anubis.instances."${cfg.fqdn}".target = "http://127.0.0.1:${toString config.services.atuin.port}";
+
+ services = {
+ 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..b94020b 100644
--- a/modules/by-name/co/constants/module.nix
+++ b/modules/by-name/co/constants/module.nix
@@ -1,64 +1,102 @@
# 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;
+ grocy = 341;
+ anubis = 342;
+ postfix-tlspol = 343;
+ rocie = 344;
# 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
+ anubis
+ dhcpcd
+ etebase-server
+ knot-resolver
+ mastodon
+ matrix-synapse
+ mautrix-whatsapp
+ nextcloud
+ nix-sync
+ nscd
+ opendkim
+ peertube
+ postfix-tlspol
+ redis-mastodon
+ redis-nextcloud
+ redis-peertube
+ redis-rspamd
+ redis-stalwart-mail
+ rspamd
+ sshd
+ stalwart-mail
+ systemd-oom
+ sharkey
+ redis-sharkey
+ grocy
+ 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
+ rocie
+ ;
# 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/et/etesync/module.nix b/modules/by-name/et/etesync/module.nix
index bcabc8a..4dc8575 100644
--- a/modules/by-name/et/etesync/module.nix
+++ b/modules/by-name/et/etesync/module.nix
@@ -45,26 +45,14 @@ in {
];
services.nginx = {
- enable = true;
- recommendedTlsSettings = true;
- recommendedOptimisation = true;
- recommendedGzipSettings = true;
- recommendedProxySettings = true;
-
virtualHosts = {
"etebase.vhack.eu" = {
- enableACME = true;
- forceSSL = true;
-
locations = {
# TODO: Maybe fix permissions to use pregenerated static files which would
# improve performance.
#"/static" = {
# root = config.services.etebase-server.settings.global.static_root;
#};
- "/" = {
- proxyPass = "http://127.0.0.1:${builtins.toString config.services.etebase-server.port}";
- };
};
serverAliases = [
"dav.vhack.eu"
@@ -72,6 +60,8 @@ in {
};
};
};
+ vhack.anubis.instances."etebase.vhack.eu".target = "http://127.0.0.1:${builtins.toString config.services.etebase-server.port}";
+
users = {
users.etebase-server.uid = config.vhack.constants.ids.uids.etebase-server;
groups.etebase-server.gid = config.vhack.constants.ids.gids.etebase-server;
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..7df1bac
--- /dev/null
+++ b/modules/by-name/gi/git-back/module.nix
@@ -0,0 +1,33 @@
+{
+ 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}";
+ };
+ };
+
+ vhack.anubis.instances."${cfg.domain}".target = "http://127.0.0.1:8000";
+ };
+}
diff --git a/modules/by-name/gi/git-server/module.nix b/modules/by-name/gi/git-server/module.nix
index db35897..3e2c848 100644
--- a/modules/by-name/gi/git-server/module.nix
+++ b/modules/by-name/gi/git-server/module.nix
@@ -8,8 +8,7 @@
cgitCss = import ./css.nix {
inherit pkgs;
- cgitPkg =
- config.services.cgit."${cfg.domain}".package;
+ cgitPkg = config.services.cgit."${cfg.domain}".package;
};
in {
options.vhack.git-server = {
@@ -84,10 +83,16 @@ in {
cgit."${cfg.domain}" = {
enable = true;
- package = pkgs.cgit-pink;
+ package = pkgs.cgit;
scanPath = "${config.services.gitolite.dataDir}/repositories";
user = "git";
group = "git";
+
+ # Don't bypass `cgit` when performing a http only clone.
+ # This is slightly slower, but we don't need to worry about the access
+ # restrictions also being by-passed.
+ gitHttpBackend.enable = false;
+
settings = {
branch-sort = "age";
diff --git a/modules/by-name/gr/grocy/module.nix b/modules/by-name/gr/grocy/module.nix
new file mode 100644
index 0000000..28107f2
--- /dev/null
+++ b/modules/by-name/gr/grocy/module.nix
@@ -0,0 +1,51 @@
+{
+ config,
+ lib,
+ ...
+}: let
+ cfg = config.vhack.grocy;
+ data = "/var/lib/grocy";
+in {
+ options.vhack.grocy = {
+ enable = lib.mkEnableOption "grocy";
+
+ domain = lib.mkOption {
+ type = lib.types.str;
+ description = "FQDN for the grocy instance.";
+ };
+ };
+
+ config = lib.mkIf cfg.enable {
+ services.grocy = {
+ enable = true;
+
+ hostName = cfg.domain;
+ dataDir = data;
+
+ settings = {
+ currency = "EUR";
+ culture = "sv_SE";
+ calendar.firstDayOfWeek = 1;
+ };
+ };
+
+ vhack.persist.directories = [
+ {
+ directory = data;
+ user = "grocy";
+ group = "grocy";
+ mode = "0700";
+ }
+ ];
+
+ users = {
+ groups.grocy = {
+ gid = config.vhack.constants.ids.gids.grocy;
+ };
+ users.grocy = {
+ extraGroups = ["grocy"];
+ uid = config.vhack.constants.ids.uids.grocy;
+ };
+ };
+ };
+}
diff --git a/modules/by-name/in/invidious-router/module.nix b/modules/by-name/in/invidious-router/module.nix
index f85a06c..750f852 100644
--- a/modules/by-name/in/invidious-router/module.nix
+++ b/modules/by-name/in/invidious-router/module.nix
@@ -1,7 +1,7 @@
{
config,
lib,
- pkgsUnstable,
+ pkgs,
...
}: let
cfg = config.vhack.invidious-router;
@@ -21,7 +21,7 @@ in {
config = lib.mkIf cfg.enable {
services.invidious-router = {
enable = true;
- package = pkgsUnstable.invidious-router;
+ package = pkgs.invidious-router;
settings = {
app = {
listen = "127.0.0.1:8050";
diff --git a/modules/by-name/ma/mail/module.nix b/modules/by-name/ma/mail/module.nix
index 55f2fb8..768eb33 100644
--- a/modules/by-name/ma/mail/module.nix
+++ b/modules/by-name/ma/mail/module.nix
@@ -27,12 +27,6 @@ in {
mode = "0700";
}
{
- directory = "/var/lib/mail/sieve";
- user = "virtualMail";
- group = "virtualMail";
- mode = "0700";
- }
- {
directory = "/var/lib/mail/vmail";
user = "virtualMail";
group = "virtualMail";
@@ -63,27 +57,32 @@ in {
mode = "0700";
}
];
+
vhack.nginx.enable = true;
security.acme.certs = {
"${cfg.fqdn}" = {
domain = cfg.fqdn;
};
};
+
mailserver = {
enable = true;
inherit (cfg) fqdn;
- useFsLayout = true;
+ stateVersion = 5;
- extraVirtualAliases = {
+ aliases = {
"abuse@vhack.eu" = all_admins;
"postmaster@vhack.eu" = all_admins;
"admin@vhack.eu" = all_admins;
};
- mailDirectory = "/var/lib/mail/vmail";
- dkimKeyDirectory = "/var/lib/mail/dkim";
- sieveDirectory = "/var/lib/mail/sieve";
+ storage = {
+ directoryLayout = "fs";
+ path = "/var/lib/mail/vmail";
+ };
+
+ dkim.keyDirectory = "/var/lib/mail/dkim";
backup.snapshotRoot = "/var/lib/mail/backup";
enableImap = false;
@@ -95,9 +94,8 @@ in {
enableSubmissionSsl = true;
openFirewall = true;
- keyFile = "/var/lib/acme/${cfg.fqdn}/key.pem";
- certificateScheme = "acme";
- certificateFile = "/var/lib/acme/${cfg.fqdn}/fullchain.pem";
+ # Reference the existing ACME configuration created by nginx
+ x509.useACMEHost = cfg.fqdn;
domains = [
"vhack.eu"
@@ -110,7 +108,7 @@ in {
"sils.sils.li"
];
- loginAccounts = {
+ accounts = {
"sils@vhack.eu" = {
hashedPassword = "$2b$05$RW/Svgk7iGxvP5W7ZwUZ1e.a3fj4fteevb2MtfFYYD0d1DQ17y9Fm";
};
@@ -156,11 +154,13 @@ in {
knot-resolver.uid = config.vhack.constants.ids.uids.knot-resolver;
redis-rspamd.uid = config.vhack.constants.ids.uids.redis-rspamd;
rspamd.uid = config.vhack.constants.ids.uids.rspamd;
+ postfix-tlspol.uid = config.vhack.constants.ids.uids.postfix-tlspol;
};
groups = {
knot-resolver.gid = lib.mkForce config.vhack.constants.ids.gids.knot-resolver;
redis-rspamd.gid = config.vhack.constants.ids.gids.redis-rspamd;
rspamd.gid = config.vhack.constants.ids.gids.rspamd;
+ postfix-tlspol.gid = config.vhack.constants.ids.gids.postfix-tlspol;
};
};
};
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..39631ef 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,55 @@ 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"
+ ];
+ # TODO: Do we still need this? <2025-12-18>
+ # The `$PSQL` env var seemed to go away between the 25.05 -> 25.11 update
+ # 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/mu/murmur/module.nix b/modules/by-name/mu/murmur/module.nix
index 5cc6f7d..061e236 100644
--- a/modules/by-name/mu/murmur/module.nix
+++ b/modules/by-name/mu/murmur/module.nix
@@ -47,8 +47,11 @@ in {
The entire team of [name of the company] is thrilled to welcome you on board. We hope you’ll do some amazing work here!
'';
- sslKey = "${cfg.murmurStore}/key.pem";
- sslCert = "${cfg.murmurStore}/fullchain.pem";
+
+ tls = {
+ keyPath = "${cfg.murmurStore}/key.pem";
+ certPath = "${cfg.murmurStore}/fullchain.pem";
+ };
registerUrl = cfg.url;
registerName = cfg.name;
diff --git a/modules/by-name/ne/nextcloud/module.nix b/modules/by-name/ne/nextcloud/module.nix
new file mode 100644
index 0000000..f91ddae
--- /dev/null
+++ b/modules/by-name/ne/nextcloud/module.nix
@@ -0,0 +1,87 @@
+{
+ 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.nextcloud32;
+ 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;
+ inherit (cfg) package;
+
+ 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";
+ 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;
+ group = "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..4d3c600 100644
--- a/modules/by-name/re/redlib/module.nix
+++ b/modules/by-name/re/redlib/module.nix
@@ -23,22 +23,6 @@ in {
openFirewall = false;
};
- services.nginx = {
- enable = true;
- virtualHosts.${domain} = {
- locations."/".proxyPass = "http://127.0.0.1:${toString config.services.redlib.port}";
-
- 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;
- };
- };
+ vhack.anubis.instances."${domain}".target = "http://127.0.0.1:${toString config.services.redlib.port}";
};
}
diff --git a/modules/by-name/ro/rocie/module.nix b/modules/by-name/ro/rocie/module.nix
new file mode 100644
index 0000000..1e419b8
--- /dev/null
+++ b/modules/by-name/ro/rocie/module.nix
@@ -0,0 +1,59 @@
+{
+ config,
+ lib,
+ ...
+}: let
+ cfg = config.vhack.rocie;
+ data = "/var/lib/rocie";
+in {
+ options.vhack.rocie = {
+ enable = lib.mkEnableOption "Rocie integration into vhack.eu";
+
+ domain = lib.mkOption {
+ type = lib.types.str;
+ description = "The domain where to deploy rocie";
+ };
+
+ loginSecret = lib.mkOption {
+ type = lib.types.path;
+ description = "The age encrypted secret file for rocie, passed to agenix";
+ };
+ };
+
+ config = lib.mkIf cfg.enable {
+ rocie = {
+ enable = true;
+ inherit (cfg) domain;
+
+ dbPath = "${data}/database.db";
+
+ secretKeyFile = config.age.secrets.rocie_secret.path;
+ };
+
+ vhack.persist.directories = [
+ {
+ directory = data;
+ user = "rocie";
+ group = "rocie";
+ mode = "0700";
+ }
+ ];
+
+ users = {
+ groups.rocie = {
+ gid = config.vhack.constants.ids.gids.rocie;
+ };
+ users.rocie = {
+ group = "rocie";
+ uid = config.vhack.constants.ids.uids.rocie;
+ };
+ };
+
+ age.secrets.rocie_secret = {
+ file = cfg.loginSecret;
+ mode = "700";
+ owner = "rocie";
+ group = "rocie";
+ };
+ };
+}
diff --git a/modules/by-name/ru/rust-motd/module.nix b/modules/by-name/ru/rust-motd/module.nix
index a6998f4..bf23843 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.runCommandLocal "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..186fed2
--- /dev/null
+++ b/modules/by-name/sh/sharkey/module.nix
@@ -0,0 +1,135 @@
+{
+ config,
+ lib,
+ pkgs,
+ ...
+}: let
+ cfg = config.vhack.sharkey;
+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 = pkgs.sharkey;
+ defaultText = lib.literalExpression "vhackPackages.sharkey";
+ description = "Sharkey package to use.";
+ };
+
+ mediaDirectory = lib.mkOption {
+ type = lib.types.path;
+ default = "/var/lib/sharkey";
+ description = "The directory where sharkey stores it's data.";
+ };
+
+ settings = lib.mkOption {
+ inherit (pkgs.formats.yaml {}) type;
+ default = {};
+ description = ''
+ Extra Configuration for Sharkey, see
+ <link xlink:href="https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/.config/example.yml"/>
+ for supported settings.
+
+ Note, that this is applied on-top of the neccessary config.
+ '';
+ };
+ };
+ };
+
+ config = lib.mkIf cfg.enable {
+ services = {
+ sharkey = {
+ enable = true;
+
+ inherit (cfg) package;
+ openFirewall = false;
+ setupRedis = true;
+ setupPostgresql = true;
+
+ settings =
+ cfg.settings
+ // {
+ url = "https://${cfg.fqdn}/";
+ port = 5312;
+
+ inherit (cfg) mediaDirectory;
+ fulltextSearch.provider = "sqlLike";
+ };
+ };
+
+ nginx.virtualHosts."${cfg.fqdn}" = {
+ locations."/" = {
+ proxyPass = "http://127.0.0.1:${toString config.services.sharkey.settings.port}";
+ proxyWebsockets = true;
+ };
+
+ enableACME = true;
+ forceSSL = true;
+ };
+ };
+
+ systemd.services.sharkey = {
+ after = [
+ "redis-sharkey.service"
+ ];
+ bindsTo = [
+ "redis-sharkey.service"
+ ];
+
+ serviceConfig = {
+ # The upstream service uses DynamicUsers, which currently poses issues to our
+ # directory persisting strategy.
+ User = "sharkey";
+ Group = "sharkey";
+ DynamicUser = lib.mkForce false;
+ };
+ };
+
+ vhack = {
+ nginx.enable = true;
+
+ persist.directories = [
+ {
+ directory = "${config.services.redis.servers."sharkey".settings.dir}";
+ user = "sharkey";
+ group = "redis-sharey";
+ mode = "0770";
+ }
+ {
+ directory = "${cfg.mediaDirectory}";
+ user = "sharkey";
+ group = "sharkey";
+ mode = "0700";
+ }
+ ];
+ };
+
+ 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..f7e44d8
--- /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 pkgs "stalwart-mail" {};
+
+ 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..dfaf63d
--- /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.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.spam-filter}/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..e25e88c
--- /dev/null
+++ b/modules/by-name/sy/system-info/module.nix
@@ -0,0 +1,81 @@
+{
+ 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";
+
+ "64738" = checkEnabled "murmur" "murmur";
+
+ # TODO(@bpeetz): Check which service opens these ports: <2025-01-28>
+ "4190" = "???";
+ "8112" = "???";
+ };
+ 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..a7cba30
--- /dev/null
+++ b/modules/by-name/ta/taskchampion-sync/module.nix
@@ -0,0 +1,66 @@
+{
+ 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;
+ group = "taskchampion";
+ };
+ groups.taskchampion.gid = config.vhack.constants.ids.uids.taskchampion;
+ };
+
+ vhack = {
+ persist.directories = [
+ {
+ directory = dataDirectory;
+ user = "taskchampion";
+ group = "taskchampion";
+ mode = "0700";
+ }
+ ];
+ nginx.enable = true;
+ };
+
+ systemd.services.taskchampion-sync-server = {
+ serviceConfig = {
+ # The upstream service uses DynamicUsers, which currently poses issues to our
+ # directory persisting strategy.
+ User = "taskchampion";
+ Group = "taskchampion";
+ DynamicUser = lib.mkForce false;
+ };
+ };
+
+ 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..6011204 100644
--- a/modules/by-name/us/users/module.nix
+++ b/modules/by-name/us/users/module.nix
@@ -27,20 +27,22 @@
};
};
- 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 AAAAC3NzaC1lZDI1NTE5AAAAIAe4o1PM6VasT3KZNl5NYvgkkBrPOg36dqsywd10FztS openpgp:0x21D20D6A";
- uid = 1001;
- }
- ]);
+ 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";