From ce06f43476863da90dc60dcee606d2b6c5a89a8e Mon Sep 17 00:00:00 2001 From: sefidel Date: Wed, 29 Mar 2023 20:54:19 +0900 Subject: project: initial commit --- .gitignore | 3 + LICENSE | 17 ++ README.md | 11 + TODO.md | 16 ++ default.nix | 10 + flake.lock | 253 ++++++++++++++++++++ flake.nix | 44 ++++ lib/README.md | 13 + lib/attrs.nix | 26 ++ lib/default.nix | 18 ++ lib/misc.nix | 9 + lib/modules.nix | 54 +++++ lib/system.nix | 22 ++ modules/README.md | 9 + modules/cachix/caches/nix-community.nix | 12 + modules/cachix/default.nix | 13 + modules/flakes.nix | 36 +++ modules/nix.nix | 34 +++ modules/security.nix | 59 +++++ modules/services/acme.nix | 52 ++++ modules/services/akkoma/blocklist.toml | 163 +++++++++++++ modules/services/akkoma/default.nix | 95 ++++++++ modules/services/akkoma/favicon-withbg.png | Bin 0 -> 17246 bytes modules/services/akkoma/favicon.png | Bin 0 -> 16693 bytes modules/services/akkoma/logo.png | Bin 0 -> 1304 bytes modules/services/akkoma/logo.svg | 71 ++++++ modules/services/akkoma/robots.txt | 2 + modules/services/akkoma/terms-of-service.html | 26 ++ modules/services/cgit.nix | 121 ++++++++++ modules/services/coredns/_corefile.nix | 3 + modules/services/coredns/default.nix | 18 ++ modules/services/coturn.nix | 64 +++++ modules/services/dendrite.nix | 230 ++++++++++++++++++ modules/services/dovecot.nix | 18 ++ modules/services/element-web.nix | 47 ++++ modules/services/fail2ban.nix | 17 ++ modules/services/git-daemon.nix | 29 +++ modules/services/gitolite/default.nix | 108 +++++++++ modules/services/gitolite/fix-refs | 9 + modules/services/gitolite/rename | 63 +++++ modules/services/jitsi.nix | 38 +++ modules/services/ldap.nix | 76 ++++++ modules/services/matrix-bridge.nix | 200 ++++++++++++++++ modules/services/matrix-moderation.nix | 52 ++++ modules/services/metrics.nix | 165 +++++++++++++ modules/services/misskey/config/default.yml | 156 ++++++++++++ modules/services/misskey/default.nix | 88 +++++++ modules/services/nginx.nix | 37 +++ modules/services/nixos-mailserver.nix | 106 +++++++++ modules/services/postgresql.nix | 34 +++ modules/services/pubnix.nix | 20 ++ modules/services/sefidel-web.nix | 26 ++ modules/services/soju.nix | 48 ++++ modules/services/userweb.nix | 36 +++ modules/services/vikunja.nix | 50 ++++ modules/sops.nix | 21 ++ overlays/README.md | 4 + overlays/default.nix | 4 + overlays/git-daemon-module.nix | 137 +++++++++++ overlays/mautrix-signal-module.nix | 196 +++++++++++++++ overlays/mautrix-whatsapp-module.nix | 192 +++++++++++++++ overlays/mjolnir-module/default.nix | 242 +++++++++++++++++++ overlays/mjolnir-module/mjolnir.md | 110 +++++++++ overlays/mjolnir-module/pantalaimon-options.nix | 70 ++++++ overlays/mjolnir-package/default.nix | 80 +++++++ overlays/mjolnir-package/package.json | 69 ++++++ overlays/mjolnir-package/pin.json | 5 + overlays/mjolnir-package/update.sh | 36 +++ overlays/sliding-sync-module.nix | 87 +++++++ overlays/sliding-sync.nix | 22 ++ overlays/soju-module.nix | 132 +++++++++++ scripts/README.md | 5 + scripts/manage-user | 79 ++++++ systems/.sops.yaml | 10 + systems/cobalt/default.nix | 303 ++++++++++++++++++++++++ systems/cobalt/hardware-configuration.nix | 65 +++++ systems/cobalt/secrets/secrets.yaml | 39 +++ 77 files changed, 4835 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 TODO.md create mode 100644 default.nix create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 lib/README.md create mode 100644 lib/attrs.nix create mode 100644 lib/default.nix create mode 100644 lib/misc.nix create mode 100644 lib/modules.nix create mode 100644 lib/system.nix create mode 100644 modules/README.md create mode 100644 modules/cachix/caches/nix-community.nix create mode 100644 modules/cachix/default.nix create mode 100644 modules/flakes.nix create mode 100644 modules/nix.nix create mode 100644 modules/security.nix create mode 100644 modules/services/acme.nix create mode 100644 modules/services/akkoma/blocklist.toml create mode 100644 modules/services/akkoma/default.nix create mode 100644 modules/services/akkoma/favicon-withbg.png create mode 100644 modules/services/akkoma/favicon.png create mode 100644 modules/services/akkoma/logo.png create mode 100644 modules/services/akkoma/logo.svg create mode 100644 modules/services/akkoma/robots.txt create mode 100644 modules/services/akkoma/terms-of-service.html create mode 100644 modules/services/cgit.nix create mode 100644 modules/services/coredns/_corefile.nix create mode 100644 modules/services/coredns/default.nix create mode 100644 modules/services/coturn.nix create mode 100644 modules/services/dendrite.nix create mode 100644 modules/services/dovecot.nix create mode 100644 modules/services/element-web.nix create mode 100644 modules/services/fail2ban.nix create mode 100644 modules/services/git-daemon.nix create mode 100644 modules/services/gitolite/default.nix create mode 100644 modules/services/gitolite/fix-refs create mode 100644 modules/services/gitolite/rename create mode 100644 modules/services/jitsi.nix create mode 100644 modules/services/ldap.nix create mode 100644 modules/services/matrix-bridge.nix create mode 100644 modules/services/matrix-moderation.nix create mode 100644 modules/services/metrics.nix create mode 100644 modules/services/misskey/config/default.yml create mode 100644 modules/services/misskey/default.nix create mode 100644 modules/services/nginx.nix create mode 100644 modules/services/nixos-mailserver.nix create mode 100644 modules/services/postgresql.nix create mode 100644 modules/services/pubnix.nix create mode 100644 modules/services/sefidel-web.nix create mode 100644 modules/services/soju.nix create mode 100644 modules/services/userweb.nix create mode 100644 modules/services/vikunja.nix create mode 100644 modules/sops.nix create mode 100644 overlays/README.md create mode 100644 overlays/default.nix create mode 100644 overlays/git-daemon-module.nix create mode 100644 overlays/mautrix-signal-module.nix create mode 100644 overlays/mautrix-whatsapp-module.nix create mode 100644 overlays/mjolnir-module/default.nix create mode 100644 overlays/mjolnir-module/mjolnir.md create mode 100644 overlays/mjolnir-module/pantalaimon-options.nix create mode 100644 overlays/mjolnir-package/default.nix create mode 100644 overlays/mjolnir-package/package.json create mode 100644 overlays/mjolnir-package/pin.json create mode 100755 overlays/mjolnir-package/update.sh create mode 100644 overlays/sliding-sync-module.nix create mode 100644 overlays/sliding-sync.nix create mode 100644 overlays/soju-module.nix create mode 100644 scripts/README.md create mode 100755 scripts/manage-user create mode 100644 systems/.sops.yaml create mode 100644 systems/cobalt/default.nix create mode 100644 systems/cobalt/hardware-configuration.nix create mode 100644 systems/cobalt/secrets/secrets.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f3790b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +result +*.qcow2 +**/**/.DS_Store diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6a528e8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,17 @@ +ISC License +=================================== + +Copyright (c) 2023, sefidel + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..ae1411b --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +exotic->infra +============= + +Infrastructure configurations for exotic.sh + +This is still WIP: [TODO](TODO.md) + +Systems +------- + +`cobalt` - Main server diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..b08a964 --- /dev/null +++ b/TODO.md @@ -0,0 +1,16 @@ +# infra->todo + +* BEFORE-RELEASE: send mjolnir patch upstream + +* move dendrite and matrix-\* modules to modules/matrix/[..] +* Ensure functionality on polylith setup + 1. Mirror userdb (mirror ldap, or migrate to pgsql+libnss-pgsql) +* Add a 'ctl' command to let users control their info +* move scripts to ./script +* use same option name for 'hostName', 'domain', etc (perhaps use `fqdn`?) +* don't blindly enable TLS, make `tls.enable, tls.acmeHost` for all modules +* set `hostName` default to `config.networking.hostName` +* DNS server should also be hosted here +* ^ subdomains should be replaced to `*` + +* prometheus dashboard diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..afcdc08 --- /dev/null +++ b/default.nix @@ -0,0 +1,10 @@ +{ inputs, lib, ... }: +with lib; +with lib.my; +{ + imports = [ + inputs.impermanence.nixosModules.impermanence + ] ++ mapModulesRec' (toString ./modules) import; + + networking.useDHCP = mkDefault false; +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..6b86b8b --- /dev/null +++ b/flake.lock @@ -0,0 +1,253 @@ +{ + "nodes": { + "blobs": { + "flake": false, + "locked": { + "lastModified": 1604995301, + "narHash": "sha256-wcLzgLec6SGJA8fx1OEN1yV/Py5b+U5iyYpksUY/yLw=", + "owner": "simple-nixos-mailserver", + "repo": "blobs", + "rev": "2cccdf1ca48316f2cfd1c9a0017e8de5a7156265", + "type": "gitlab" + }, + "original": { + "owner": "simple-nixos-mailserver", + "repo": "blobs", + "type": "gitlab" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1668681692, + "narHash": "sha256-Ht91NGdewz8IQLtWZ9LCeNXMSXHUss+9COoqu6JLmXU=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "009399224d5e398d03b22badca40a37ac85412a1", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-utils": { + "locked": { + "lastModified": 1667395993, + "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "impermanence": { + "locked": { + "lastModified": 1675359654, + "narHash": "sha256-FPxzuvJkcO49g4zkWLSeuZkln54bLoTtrggZDJBH90I=", + "owner": "nix-community", + "repo": "impermanence", + "rev": "6138eb8e737bffabd4c8fc78ae015d4fd6a7e2fd", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "impermanence", + "type": "github" + } + }, + "nixos-mailserver": { + "inputs": { + "blobs": "blobs", + "flake-compat": "flake-compat", + "nixpkgs": [ + "unstable" + ], + "nixpkgs-22_11": "nixpkgs-22_11", + "utils": "utils" + }, + "locked": { + "lastModified": 1671738303, + "narHash": "sha256-PRgqtaWf2kMSYqVmcnmhTh+UsC0RmvXRTr+EOw5VZUA=", + "owner": "simple-nixos-mailserver", + "repo": "nixos-mailserver", + "rev": "6d0d9fb966cc565a3df74d3b686f924c7615118c", + "type": "gitlab" + }, + "original": { + "owner": "simple-nixos-mailserver", + "repo": "nixos-mailserver", + "type": "gitlab" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1673606088, + "narHash": "sha256-wdYD41UwNwPhTdMaG0AIe7fE1bAdyHe6bB4HLUqUvck=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "37b97ae3dd714de9a17923d004a2c5b5543dfa6d", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-2111": { + "locked": { + "lastModified": 1659446231, + "narHash": "sha256-hekabNdTdgR/iLsgce5TGWmfIDZ86qjPhxDg/8TlzhE=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "eabc38219184cc3e04a974fe31857d8e0eac098d", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-21.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-22_11": { + "locked": { + "lastModified": 1669558522, + "narHash": "sha256-yqxn+wOiPqe6cxzOo4leeJOp1bXE/fjPEi/3F/bBHv8=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "ce5fe99df1f15a09a91a86be9738d68fadfbad82", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-22.11", + "type": "indirect" + } + }, + "nixpkgs-stable": { + "locked": { + "lastModified": 1679748960, + "narHash": "sha256-BP8XcYHyj1NxQi04RpyNW8e7KiXSoI+Fy1tXIK2GfdA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "da26ae9f6ce2c9ab380c0f394488892616fc5a6a", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "release-22.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "impermanence": "impermanence", + "nixos-mailserver": "nixos-mailserver", + "nixpkgs-2111": "nixpkgs-2111", + "sefidel-web": "sefidel-web", + "sops-nix": "sops-nix", + "unstable": "unstable", + "unstable-small": "unstable-small" + } + }, + "sefidel-web": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1680013210, + "narHash": "sha256-892xzeELa85wJxOro40fVK4zmqbj7ZWJW54cECW7IYg=", + "ref": "refs/heads/main", + "rev": "ac0028999700416577f9a353530a89bd896cbc0d", + "revCount": 3, + "type": "git", + "url": "https://git.exotic.sh/pub/sefidel/sefidel-web" + }, + "original": { + "type": "git", + "url": "https://git.exotic.sh/pub/sefidel/sefidel-web" + } + }, + "sops-nix": { + "inputs": { + "nixpkgs": [ + "unstable" + ], + "nixpkgs-stable": "nixpkgs-stable" + }, + "locked": { + "lastModified": 1679993313, + "narHash": "sha256-pfZ/BxJDTifnQBMXg60OhwpJvg96LHvEXGtpHeGcWLM=", + "owner": "Mic92", + "repo": "sops-nix", + "rev": "5b26523e28989a7f56953b695184070c06335814", + "type": "github" + }, + "original": { + "owner": "Mic92", + "repo": "sops-nix", + "type": "github" + } + }, + "unstable": { + "locked": { + "lastModified": 1679944645, + "narHash": "sha256-e5Qyoe11UZjVfgRfwNoSU57ZeKuEmjYb77B9IVW7L/M=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "4bb072f0a8b267613c127684e099a70e1f6ff106", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "unstable-small": { + "locked": { + "lastModified": 1680024716, + "narHash": "sha256-f9824KWmxVBI4WLI7o6tDFfj+dW+qj6uQKo0ZRsbaZQ=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "49079a134fd3d3ac25d5ae1f5516f37770f19138", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable-small", + "repo": "nixpkgs", + "type": "github" + } + }, + "utils": { + "locked": { + "lastModified": 1605370193, + "narHash": "sha256-YyMTf3URDL/otKdKgtoMChu4vfVL3vCMkRqpGifhUn0=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "5021eac20303a61fafe17224c087f5519baed54d", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..40bd39d --- /dev/null +++ b/flake.nix @@ -0,0 +1,44 @@ +{ + description = "exotic.sh system flake"; + + inputs = { + unstable.url = "github:nixos/nixpkgs/nixos-unstable"; + unstable-small.url = "github:nixos/nixpkgs/nixos-unstable-small"; + nixpkgs-2111.url = "github:nixos/nixpkgs/nixos-21.11"; + + impermanence.url = "github:nix-community/impermanence"; + + sops-nix.url = "github:Mic92/sops-nix"; + sops-nix.inputs.nixpkgs.follows = "unstable"; + + nixos-mailserver.url = "gitlab:simple-nixos-mailserver/nixos-mailserver"; + nixos-mailserver.inputs.nixpkgs.follows = "unstable"; + + sefidel-web.url = "git+https://git.exotic.sh/pub/sefidel/sefidel-web"; + }; + + outputs = { self, unstable, ... } @ inputs: + let + inherit (lib.my) mapModulesRec mapSystems; + + system = "x86_64-linux"; + + pkgs = import unstable { inherit system; }; + + lib = unstable.lib.extend + (self: super: { my = import ./lib { inherit pkgs inputs; lib = self; }; }); + in + { + lib = lib.my; + + nixosModules = mapModulesRec ./modules import; + + colmena = { + meta = { + nixpkgs = import unstable { inherit system; overlays = [ (import ./overlays) ]; }; + specialArgs = { inherit lib inputs system; }; + }; + } + // mapSystems ./systems { }; + }; +} diff --git a/lib/README.md b/lib/README.md new file mode 100644 index 0000000..21ca023 --- /dev/null +++ b/lib/README.md @@ -0,0 +1,13 @@ +infra->lib +========== + +Attribution +----------- + +Most of the 'moving parts' of this is largely copied/modified from: + +- [hlissner/dotfiles][hlissner]: 2023-Feb, MIT (C) Henrik Lissner +- [NobbZ/nixos-config][nobbz]: 2022, MIT (C) Nobert Melzer + +[hlissner]: https://github.com/hlissner/dotfiles +[nobbz]: https://github.com/NobbZ/nixos-config diff --git a/lib/attrs.nix b/lib/attrs.nix new file mode 100644 index 0000000..0f8ebd1 --- /dev/null +++ b/lib/attrs.nix @@ -0,0 +1,26 @@ +{ lib, ... }: + +with builtins; +with lib; +rec { + # attrsToList + attrsToList = attrs: + mapAttrsToList (name: value: { inherit name value; }) attrs; + + # mapFilterAttrs :: + # (name -> value -> bool) + # (name -> value -> { name = any; value = any; }) + # attrs + mapFilterAttrs = pred: f: attrs: filterAttrs pred (mapAttrs' f attrs); + + # Generate an attribute set by mapping a function over a list of values. + genAttrs' = values: f: listToAttrs (map f values); + + # anyAttrs :: (name -> value -> bool) attrs + anyAttrs = pred: attrs: + any (attr: pred attr.name attr.value) (attrsToList attrs); + + # countAttrs :: (name -> value -> bool) attrs + countAttrs = pred: attrs: + count (attr: pred attr.name attr.value) (attrsToList attrs); +} diff --git a/lib/default.nix b/lib/default.nix new file mode 100644 index 0000000..a275c09 --- /dev/null +++ b/lib/default.nix @@ -0,0 +1,18 @@ +{ inputs, lib, pkgs, ... }: + +let + inherit (lib) makeExtensible attrValues foldr; + inherit (modules) mapModules; + + modules = import ./modules.nix { + inherit lib; + self.attrs = import ./attrs.nix { inherit lib; self = { }; }; + }; + + mylib = makeExtensible (self: + with self; mapModules ./. + (file: import file { inherit self lib pkgs inputs; })); +in +mylib.extend + (self: super: + foldr (a: b: a // b) { } (attrValues super)) diff --git a/lib/misc.nix b/lib/misc.nix new file mode 100644 index 0000000..484d0d5 --- /dev/null +++ b/lib/misc.nix @@ -0,0 +1,9 @@ +{ pkgs, ... }: + +rec { + # ifd3f/infra + wrapFile = name: path: + (pkgs.runCommand name { inherit path; } '' + cp -r "$path" "$out" + ''); +} diff --git a/lib/modules.nix b/lib/modules.nix new file mode 100644 index 0000000..ef7c289 --- /dev/null +++ b/lib/modules.nix @@ -0,0 +1,54 @@ +{ self, lib, ... }: + +let + inherit (builtins) attrValues readDir pathExists concatLists; + inherit (lib) id mapAttrsToList filterAttrs hasPrefix hasSuffix nameValuePair removeSuffix; + inherit (self.attrs) mapFilterAttrs; +in +rec { + mapModules = dir: fn: + mapFilterAttrs + (n: v: + v != null && + !(hasPrefix "_" n)) + (n: v: + let path = "${toString dir}/${n}"; in + if v == "directory" && pathExists "${path}/default.nix" + then nameValuePair n (fn path) + else if v == "regular" && + n != "default.nix" && + hasSuffix ".nix" n + then nameValuePair (removeSuffix ".nix" n) (fn path) + else nameValuePair "" null) + (readDir dir); + + mapModules' = dir: fn: + attrValues (mapModules dir fn); + + mapModulesRec = dir: fn: + mapFilterAttrs + (n: v: + v != null && + !(hasPrefix "_" n)) + (n: v: + let path = "${toString dir}/${n}"; in + if v == "directory" + then nameValuePair n (mapModulesRec path fn) + else if v == "regular" && n != "default.nix" && hasSuffix ".nix" n + then nameValuePair (removeSuffix ".nix" n) (fn path) + else nameValuePair "" null) + (readDir dir); + + mapModulesRec' = dir: fn: + let + dirs = + mapAttrsToList + (k: _: "${dir}/${k}") + (filterAttrs + (n: v: v == "directory" && !(hasPrefix "_" n)) + (readDir dir)); + files = attrValues (mapModules dir id); + paths = files ++ concatLists (map (d: mapModulesRec' d id) dirs); + in + map fn paths; +} diff --git a/lib/system.nix b/lib/system.nix new file mode 100644 index 0000000..8fc4dce --- /dev/null +++ b/lib/system.nix @@ -0,0 +1,22 @@ +{ self, inputs, lib, pkgs, ... }: + +with lib; +with lib.my; +{ + mkSystem = path: attrs @ { ... }: { + imports = [ + { + networking.hostName = mkDefault + (removeSuffix ".nix" (baseNameOf path)); + system.configurationRevision = self.rev or "dirty"; + } + ../. # /default.nix + (import path) + ]; + }; + + mapSystems = dir: attrs @ { system ? system, ... }: + mapModules dir + (hostPath: mkSystem hostPath attrs); +} + diff --git a/modules/README.md b/modules/README.md new file mode 100644 index 0000000..25031dc --- /dev/null +++ b/modules/README.md @@ -0,0 +1,9 @@ +infra->modules +============== + +This is all the modules used to configure our systems based on its purposes. + +As of now, it doesn't have any clear "naming convention", but generally it's +`` for a module configuring one thing, `` otherwise. + +e.g) `dendrite` -> `dendrite`, `prometheus`, `grafana` -> `metrics` diff --git a/modules/cachix/caches/nix-community.nix b/modules/cachix/caches/nix-community.nix new file mode 100644 index 0000000..d323939 --- /dev/null +++ b/modules/cachix/caches/nix-community.nix @@ -0,0 +1,12 @@ +{ config, lib, ... }: + +{ + nix.settings = { + substituters = [ + "https://nix-community.cachix.org" + ]; + trusted-public-keys = [ + "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=" + ]; + }; +} diff --git a/modules/cachix/default.nix b/modules/cachix/default.nix new file mode 100644 index 0000000..9dd55b5 --- /dev/null +++ b/modules/cachix/default.nix @@ -0,0 +1,13 @@ +{ config, pkgs, lib, ... }: +let + folder = ./caches; + toImport = name: value: folder + ("/" + name); + filterCaches = key: value: value == "regular" && lib.hasSuffix ".nix" key; + imports = lib.mapAttrsToList toImport (lib.filterAttrs filterCaches (builtins.readDir folder)); +in +{ + inherit imports; + nix.settings.substituters = [ "https://cache.nixos.org/" ]; + + environment.systemPackages = [ pkgs.cachix ]; +} diff --git a/modules/flakes.nix b/modules/flakes.nix new file mode 100644 index 0000000..df86369 --- /dev/null +++ b/modules/flakes.nix @@ -0,0 +1,36 @@ +{ config, pkgs, unstable, unstable-small, nixpkgs-2111, lib, ... }: + +with lib; +let + base = "/etc/nixpkgs/channels"; + nixpkgsPath = "${base}/nixpkgs"; + nixpkgsSmallPath = "${base}/nixpkgsSmall"; + nixpkgs2111Path = "${base}/nixpkgs2111"; +in +{ + options.nix.flakes.enable = mkEnableOption "nix flakes"; + + config = lib.mkIf config.nix.flakes.enable { + nix = { + package = pkgs.nixUnstable; + experimentalFeatures = "nix-command flakes"; + + registry.nixpkgs.flake = unstable; + registry.nixpkgsSmall.flake = unstable-small; + registry.nixpkgs2111.flake = nixpkgs-2111; + + nixPath = [ + "nixpkgs=${nixpkgsPath}" + "nixpkgsSmall=${nixpkgsSmallPath}" + "nixpkgs2111=${nixpkgs2111Path}" + "/nix/var/nix/profiles/per-user/root/channels" + ]; + }; + + systemd.tmpfiles.rules = [ + "L+ ${nixpkgsPath} - - - - ${unstable}" + "L+ ${nixpkgsSmallPath} - - - - ${unstable-small}" + "L+ ${nixpkgs2111Path} - - - - ${nixpkgs-2111}" + ]; + }; +} diff --git a/modules/nix.nix b/modules/nix.nix new file mode 100644 index 0000000..1f61d45 --- /dev/null +++ b/modules/nix.nix @@ -0,0 +1,34 @@ +{ config, lib, ... }: + +let + allowed = config.nix.allowedUnfree; +in +{ + options.nix = { + experimentalFeatures = lib.mkOption { + type = lib.types.separatedString " "; + default = ""; + description = '' + Enables experimental features + ''; + }; + + allowedUnfree = lib.mkOption { + type = lib.types.listOf lib.types.string; + default = [ ]; + description = '' + Allows for unfree packages by their name. + ''; + }; + }; + + config = lib.mkMerge [ + (lib.mkIf (config.nix.experimentalFeatures != "") { nix.extraOptions = "experimental-features = ${config.nix.experimentalFeatures}"; }) + (lib.mkIf (allowed != [ ]) { nixpkgs.config.allowUnfreePredicate = (pkg: __elem (lib.getName pkg) allowed); }) + { nix.settings.auto-optimise-store = lib.mkDefault true; } + { + nix.gc.automatic = lib.mkDefault true; + nix.gc.options = lib.mkDefault "--delete-older-than 10d"; + } + ]; +} diff --git a/modules/security.nix b/modules/security.nix new file mode 100644 index 0000000..d845393 --- /dev/null +++ b/modules/security.nix @@ -0,0 +1,59 @@ +{ config, lib, ... }: + +{ + # Security-related system tweaks + + # Prevent replacing the running kernel without reboot. + security.protectKernelImage = true; + + # mount /tmp in ram. This makes temp file management faster + # on ssd systems, and volatile! Because it's wiped on reboot. + boot.tmpOnTmpfs = false; + boot.tmpOnTmpfsSize = "80%"; + + # Purge /tmp on boot. (fallback option) + boot.cleanTmpDir = lib.mkDefault (!config.boot.tmpOnTmpfs); + + boot.kernel.sysctl = { + # The Magic SysRq key is a key combo that allows users connected to the + # system console of a Linux kernel to perform some low-level commands. + # Disable it, since we don't need it, and is a potential security concern. + "kernel.sysrq" = 0; + + ## TCP hardening + # Prevent bogus ICMP errors from filling up logs. + "net.ipv4.icmp_ignore_bogus_error_responses" = 1; + # Reverse path filtering causes the kernel to do source validation of + # packets received from all interfaces. This can mitigate IP spoofing. + "net.ipv4.conf.default.rp_filter" = 1; + "net.ipv4.conf.all.rp_filter" = 1; + # Do not accept IP source route packets (we're not a router) + "net.ipv4.conf.all.accept_source_route" = 0; + "net.ipv6.conf.all.accept_source_route" = 0; + # Don't send ICMP redirects (again, we're on a router) + "net.ipv4.conf.all.send_redirects" = 0; + "net.ipv4.conf.default.send_redirects" = 0; + # Refuse ICMP redirects (MITM mitigations) + "net.ipv4.conf.all.accept_redirects" = 0; + "net.ipv4.conf.default.accept_redirects" = 0; + "net.ipv4.conf.all.secure_redirects" = 0; + "net.ipv4.conf.default.secure_redirects" = 0; + "net.ipv6.conf.all.accept_redirects" = 0; + "net.ipv6.conf.default.accept_redirects" = 0; + # Protects against SYN flood attacks + "net.ipv4.tcp_syncookies" = 1; + # Incomplete protection again TIME-WAIT assassination + "net.ipv4.tcp_rfc1337" = 1; + + ## TCP optimization + # TCP Fast Open is a TCP extension that reduces network latency by packing + # data in the sender’s initial TCP SYN. Setting 3 = enable TCP Fast Open for + # both incoming and outgoing connections: + "net.ipv4.tcp_fastopen" = 3; + # Bufferbloat mitigations + slight improvement in throughput & latency + "net.ipv4.tcp_congestion_control" = "bbr"; + "net.core.default_qdisc" = "cake"; + }; + + boot.kernelModules = [ "tcp_bbr" ]; +} diff --git a/modules/services/acme.nix b/modules/services/acme.nix new file mode 100644 index 0000000..6f6e33e --- /dev/null +++ b/modules/services/acme.nix @@ -0,0 +1,52 @@ +{ config, lib, ... }: + +with lib; +let + cfg = config.modules.services.acme; +in +{ + options.modules.services.acme = { + enable = mkEnableOption "ACME certificate manager"; + email = mkOption { + type = types.str; + description = mdDoc '' + The postmaster email address to use. + ''; + }; + certs = mkOption { + type = types.attrsOf + (types.submodule { + options = { + domain = mkOption { + type = types.nullOr types.str; + default = null; + }; + subDomains = mkOption { type = types.listOf types.str; }; + }; + }); + }; + secrets.acme-credentials = mkOption { type = types.str; description = "path to the acme environment file"; }; + }; + + config = mkIf cfg.enable { + security.acme = { + acceptTerms = true; + defaults.email = cfg.email; + certs = mapAttrs + (name: { domain, subDomains }: { + extraDomainNames = lists.forEach subDomains (elem: elem + ".${name}"); + } // { + dnsProvider = "hetzner"; + dnsPropagationCheck = true; + credentialsFile = cfg.secrets.acme-credentials; + } // optionalAttrs (domain != null) { + domain = domain; + }) + cfg.certs; + }; + + environment.persistence."/persist".directories = [ + "/var/lib/acme" + ]; + }; +} diff --git a/modules/services/akkoma/blocklist.toml b/modules/services/akkoma/blocklist.toml new file mode 100644 index 0000000..e5eac7a --- /dev/null +++ b/modules/services/akkoma/blocklist.toml @@ -0,0 +1,163 @@ +[followers_only] + +[media_nsfw] + +[reject] +"*.tk" = "Free TLD" +"*.ml" = "Free TLD" +"*.ga" = "Free TLD" +"*.cf" = "Free TLD" +"*.gq" = "Free TLD" +# Reject list from chaos.social at 2023-02-06 +"activitypub-proxy.cf" = "Only exists to evade instance blocks, details" +"activitypub-troll.cf" = "Spam" +"aethy.com" = "Lolicon" +"bae.st" = "Discrimination, racism, “free speech zone”" +"baraag.net" = "Lolicon" +"banepo.st" = "Homophobia" +"beefyboys.club" = "Discrimination, racism, “free speech zone”" +"beefyboys.win" = "Discrimination, racism, “free speech zone”" +"beta.birdsite.live" = "Twitter crossposter" +"birb.elfenban.de" = "Twitter crossposter" +"bird.evilcyberhacker.net" = "Twitter crossposter" +"bird.froth.zone" = "Twitter crossposter" +"bird.geiger.ee" = "Twitter crossposter" +"bird.im-in.space" = "Twitter crossposter" +"bird.istheguy.com" = "Twitter crossposter" +"bird.karatek.net" = "Twitter crossposter" +"bird.makeup" = "Twitter crossposter" +"bird.nzbr.de" = "Twitter crossposter" +"bird.r669.live" = "Twitter crossposter" +"bird.seafoam.space" = "Twitter crossposter" +"birdbots.leptonics.com" = "Twitter crossposter" +"birdsite.b93.dece.space" = "Twitter crossposter" +"birdsite.blazelights.dev" = "Twitter crossposter" +"birdsite.frog.fashion" = "Twitter crossposter" +"birdsite.gabeappleton.me" = "Twitter crossposter" +"birdsite.james.moody.name" = "Twitter crossposter" +"birdsite.koyu.space" = "Twitter crossposter" +"birdsite.lakedrops.com" = "Twitter crossposter" +"birdsite.link" = "Twitter crossposter" +"birdsite.monster" = "Twitter crossposter" +"birdsite.oliviaappleton.com" = "Twitter crossposter" +"birdsite.platypush.tech" = "Twitter crossposter" +"birdsite.slashdev.space" = "Twitter crossposter" +"birdsite.tcjc.uk" = "Twitter crossposter" +"birdsite.thorlaksson.com" = "Twitter crossposter" +"birdsite.toot.si" = "Twitter crossposter" +"birdsite.wilde.cloud" = "Twitter crossposter" +"birdsitelive.ffvo.dev" = "Twitter crossposter" +"birdsitelive.kevinyank.com" = "Twitter crossposter" +"birdsitelive.peanutlasko.com" = "Twitter crossposter" +"birdsitelive.treffler.cloud" = "Twitter crossposter" +"bridge.birb.space" = "Twitter crossposter" +"brighteon.social" = "“free speech zone”" +"cawfee.club" = "Discrimination, racism, “free speech zone”" +"childpawn.shop" = "Pedophilia" +"chudbuds.lol" = "Discrimination, racism, “free speech zone”" +"club.darknight-coffee.eu" = "“free speech zone”" +"clubcyberia.co" = "Homophobia" +"clube.social" = "Harassment" +"comfyboy.club" = "Discrimination, racism" +"cum.camp" = "Harassment" +"cum.salon" = "Misogynic, pedophilia" +"daishouri.moe" = "Fascism, openly advertises with swastika" +"detroitriotcity.com" = "Discrimination, racism, “free speech zone”" +"eientei.org" = "Racism, antisemitism" +"eveningzoo.club" = "Discrimination, racism, “free speech zone”" +"f.haeder.net" = "Discrimination" +"freak.university" = "Pedophilia" +"freeatlantis.com" = "Conspiracy theory instance" +"freecumextremist.com" = "Discrimination, racism, “free speech zone”" +"freefedifollowers.ga" = "Follower spam" +"freespeechextremist.com" = "Discrimination, racism, “free speech zone”" +"frennet.link" = "Discrimination, racism, “free speech zone”" +"froth.zone" = "Calls freespeechextremist their local bubble" +"gab.com/.ai, develop.gab.com" = "Discrimination, racism, “free speech zone”" +"gameliberty.club" = "“free speech zone”" +"gegenstimme.tv" = "“free speech zone”" +"genderheretics.xyz" = "Tagline “Now With 41% More Misgendering!”" +"gitmo.life" = "“free speech zone”" +"gleasonator.com" = "Transphobia, TERFs" +"glindr.org" = "Discrimination" +"glowers.club" = "Discrimination, racism, “free speech zone”" +"honkwerx.tech" = "Racism" +"iamterminally.online" = "Discrimination, racism, “free speech zone”" +"iddqd.social" = "Discrimination, racism, “free speech zone”" +"itmslaves.com" = "“free speech zone”, noagenda affiliated" +"jaeger.website" = "Discrimination, racism, “free speech zone”" +"kenfm.quadplay.tv" = "Conspiracy videos" +"kiwifarms.cc" = "Discrimination" +"lgbtfree.zone" = "Racism, transphobia, all that" +"liberdon.com" = "Conspiracy theories, transphobia, racism" +"libre.tube" = "Promotion of violence and murder, multiple other violations of our rules" +"lolicon.rocks" = "Lolicon" +"lolison.top" = "Lolicon, paedophilia" +"mastinator.com" = "Block evasion, unwanted profile mirroring, and more" +"mastodon.network" = "Instance went down, now porn spam" +"mastodon.popps.org" = "Homophobia" +"mastodong.lol" = "Admin maintains and runs activitypub-proxy.cf" +"meta-tube.de" = "Conspiracy, CoVid19 denier videos https://fediblock.org/blocklist/#meta-tube.de" +"midnightride.rs" = "Discrimination" +"misskey-forkbomb.cf" = "Spam" +"morale.ch" = "Antisemitism and more" +"mstdn.foxfam.club" = "Right wing twitter mirror" +"natehiggers.online" = "Racism" +"newjack.city" = "Exclusive to unwanted follow bots" +"nicecrew.digital" = "Discrimination, racism, “free speech zone”" +"noagendasocial.com" = "“free speech zone”, harassment" +"noagendasocial.nl" = "“free speech zone”, harassment" +"noagendatube.com" = "“free speech zone”, harassment" +"ns.auction" = "Racism etc" +"ohai.su" = "Offline" +"pawoo.net" = "Untagged nfsw content, unwanted follow bots, lolicon" +"paypig.org" = "Racism" +"pieville.net" = "Racism, antisemitism" +"pl.serialmay.link" = "Racism, transphobia" +"pl.tkammer.de" = "Transphobia" +"play.xmr.101010.pl" = "Cryptomining" +"pleroma.kitsunemimi.club" = "Discrimination" +"pleroma.narrativerry.xyz" = "Discrimination, racism, “free speech zone”" +"pleroma.nobodyhasthe.biz" = "Doxxing and discrimination" +"pleroma.rareome.ga" = "Doesn’t respect blocks or status privacy, lolicons" +"poa.st" = "Discrimination" +"podcastindex.social" = "noagenda affiliated" +"poster.place" = "Discrimination, racism, “free speech zone”, harassment in response to blocks" +"qoto.org" = "“free speech zone”, harassment" +"rapemeat.solutions" = "Lolicon and also, like, the domain name" +"rdrama.cc" = "Discrimination, “free speech zone”, racism" +"repl.co" = "Spam" +"rojogato.com" = "Harassment, “free speech zone”" +"ryona.agency" = "Alt-right trolls, harassment" +"seal.cafe" = "Discrimination, racism, “free speech zone”" +"shitpost.cloud" = "“Free speech zone”, antisemitism" +"shitposter.club" = "“Free speech zone”" +"shortstackran.ch" = "Racism, homophobia, “free speech zone”" +"shota.house" = "Lolicon" +"skippers-bin.com" = "Same admin as neckbeard.xyz, same behaviour" +"sleepy.cafe" = "Racism, harassment" +"sneak.berlin" = "privacy violation" +"sneed.social" = "Discrimination, racism, “free speech zone”, nationalism, hate speech, completely unmoderated" +"soc.ua-fediland.de" = "Spam" +"social.ancreport.com" = "Discrimination, racism, “free speech zone”" +"social.lovingexpressions.net" = "Transphobia" +"social.teci.world" = "Discrimination, racism, “free speech zone”" +"social.urspringer.de" = "Conspiracy, CoVid19 denier" +"socnet.supes.com" = "Right wing “free speech zone”" +"solagg.com" = "Scammers" +"spinster.xyz" = "Discrimination, TERFs" +"tastingtraffic.net" = "Homophobia" +"truthsocial.co.in" = "Alt-right trolls" +"tube.kenfm.de" = "Right-wing conspiracy videos" +"tube.querdenken-711.de" = "Right-wing onspiracy videos" +"tweet.pasture.moe" = "Twitter crossposter" +"tweetbridge.kogasa.de" = "Twitter crossposter" +"tweets.icu" = "Twitter crossposter" +"twitter.activitypub.actor" = "Twitter crossposter" +"twitter.doesnotexist.club" = "Twitter crossposter" +"twitterbridge.jannis.rocks" = "Twitter crossposter" +"twtr.plus" = "Twitter crossposter" +"varishangout.net" = "Transphobia and racism go unmoderated, aggressive trolling, lolicon permitted in rules" +"wiki-tube.de" = "Right-wing conspiracy videos (initial video welcomes Querdenken and KenFM)" +"wolfgirl.bar" = "Discrimination, homophobia, unmoderated trolling" +"yggdrasil.social" = "Instance rules: “No LGBTQ. Period. No homosexuality. No men who think they’re women or women who think they’re men. No made up genders.”" diff --git a/modules/services/akkoma/default.nix b/modules/services/akkoma/default.nix new file mode 100644 index 0000000..a0cd42c --- /dev/null +++ b/modules/services/akkoma/default.nix @@ -0,0 +1,95 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + cfg = config.modules.services.akkoma; + + poorObfuscation = y: x: "${x}@${y}"; + federation-blocklist = lib.importTOML ./blocklist.toml; + + inherit (lib.my) wrapFile; +in +{ + options.modules.services.akkoma = { + enable = mkEnableOption "Akkoma instance"; + domain = mkOption { type = types.str; }; + realHost = mkOption { type = types.str; }; + instanceName = mkOption { type = types.str; default = "Akkoma on ${cfg.domain}"; }; + }; + + config = mkIf cfg.enable { + modules.services.postgresql.enable = true; + + services.akkoma = { + enable = true; + initDb.enable = true; + + extraStatic = { + "static/terms-of-service.html" = wrapFile "terms-of-service.html" ./terms-of-service.html; + "static/logo.svg" = wrapFile "logo.svg" ./logo.svg; + "static/logo.png" = wrapFile "logo.png" ./logo.png; + "static/logo-512.png" = wrapFile "logo-512.png" ./favicon-withbg.png; # Intentional, for PWA favicon. + "static/icon.png" = wrapFile "icon.png" ./favicon.png; + "favicon.png" = wrapFile "favicon.png" ./favicon.png; + }; + config = + let inherit ((pkgs.formats.elixirConf { }).lib) mkRaw mkMap; + in { + ":pleroma"."Pleroma.Web.Endpoint".url.host = cfg.realHost; + ":pleroma"."Pleroma.Web.WebFinger".domain = cfg.domain; + ":pleroma".":media_proxy".enabled = false; + ":pleroma".":instance" = { + name = cfg.instanceName; + + description = "Private akkoma instance"; + email = poorObfuscation cfg.domain "postmaster"; + notify_email = poorObfuscation cfg.domain "postmaster"; + + registrations_open = false; + invites_enabled = true; + + limit = 5000; + }; + ":pleroma".":frontend_configurations" = { + pleroma_fe = mkMap { + logo = "/static/logo.png"; + }; + }; + ":pleroma".":mrf" = { + policies = map mkRaw [ "Pleroma.Web.ActivityPub.MRF.SimplePolicy" ]; + }; + ":pleroma".":mrf_simple" = { + followers_only = mkMap federation-blocklist.followers_only; + media_nsfw = mkMap federation-blocklist.media_nsfw; + reject = mkMap federation-blocklist.reject; + }; + }; + + nginx = { + forceSSL = true; + useACMEHost = cfg.domain; + + locations."~ \\.(js|css|woff|woff2?|png|jpe?g|svg)$" = { + extraConfig = '' + add_header Cache-Control "public, max-age=14400, must-revalidate"; + ''; + + proxyPass = "http://unix:${config.services.akkoma.config.":pleroma"."Pleroma.Web.Endpoint".http.ip}"; + proxyWebsockets = true; + recommendedProxySettings = true; + }; + }; + }; + + services.nginx.virtualHosts.${cfg.domain} = { + forceSSL = true; + useACMEHost = cfg.domain; + + locations."/.well-known/host-meta" = { + extraConfig = '' + return 301 https://${cfg.realHost}$request_uri; + ''; + }; + }; + }; + } diff --git a/modules/services/akkoma/favicon-withbg.png b/modules/services/akkoma/favicon-withbg.png new file mode 100644 index 0000000..7d15954 Binary files /dev/null and b/modules/services/akkoma/favicon-withbg.png differ diff --git a/modules/services/akkoma/favicon.png b/modules/services/akkoma/favicon.png new file mode 100644 index 0000000..d8cbce3 Binary files /dev/null and b/modules/services/akkoma/favicon.png differ diff --git a/modules/services/akkoma/logo.png b/modules/services/akkoma/logo.png new file mode 100644 index 0000000..7744b1a Binary files /dev/null and b/modules/services/akkoma/logo.png differ diff --git a/modules/services/akkoma/logo.svg b/modules/services/akkoma/logo.svg new file mode 100644 index 0000000..68e647e --- /dev/null +++ b/modules/services/akkoma/logo.svg @@ -0,0 +1,71 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/modules/services/akkoma/robots.txt b/modules/services/akkoma/robots.txt new file mode 100644 index 0000000..1f53798 --- /dev/null +++ b/modules/services/akkoma/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/modules/services/akkoma/terms-of-service.html b/modules/services/akkoma/terms-of-service.html new file mode 100644 index 0000000..b954760 --- /dev/null +++ b/modules/services/akkoma/terms-of-service.html @@ -0,0 +1,26 @@ + + + + +

Terms of Service

+

This is a personal instance with only one user. Therefore, I'll write rules that I'll abide:

+
    +
  1. +

    No discrimination based on race, gender, sexual orientation, disabilities, or any other characteristic.

    +
  2. +
  3. +

    No harassment or doxxing towards others.

    +
  4. +
  5. +

    No promotion of violence.

    +
  6. +
  7. +

    No content that is illegal in United Kingdom, Japan, Finland, Germany, and South Korea.

    +
  8. +
  9. +

    Use content warnings for explicit or controversial content.

    +
  10. +
+

Since I'm the only user here, I try to moderate myself best as I can. But I might sometimes fail to do so. If that ever happens, please do let me know. I'll make sure it never happens again!

+ + diff --git a/modules/services/cgit.nix b/modules/services/cgit.nix new file mode 100644 index 0000000..418312b --- /dev/null +++ b/modules/services/cgit.nix @@ -0,0 +1,121 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + cfg = config.modules.services.cgit; +in +{ + options.modules.services.cgit = { + enable = mkEnableOption "cgit with uwsgi"; + + domain = mkOption { type = types.str; }; + realHost = mkOption { type = types.str; }; + # TODO: use generators & submodules + settings = { + title = mkOption { type = types.str; default = "${cfg.domain} git"; }; + description = mkOption { type = types.str; default = "cgit, hyperfast web frontend for Git"; }; + }; + }; + config = mkIf cfg.enable { + + modules.services.nginx.enable = true; + + services.uwsgi = { + enable = true; + user = "nginx"; + group = "nginx"; + plugins = [ "cgi" ]; + + instance = { + type = "emperor"; + vassals = { + cgit = { + type = "normal"; + master = true; + socket = "/run/uwsgi/cgit.sock"; + procname-master = "uwsgi cgit"; + plugins = [ "cgi" ]; + cgi = "${pkgs.cgit-pink}/cgit/cgit.cgi"; + }; + }; + }; + }; + + users.extraUsers.nginx.extraGroups = [ "git" ]; + + services.nginx.virtualHosts.${cfg.realHost} = { + forceSSL = true; + useACMEHost = cfg.domain; + root = "${pkgs.cgit-pink}/cgit"; + locations = { + "/" = { + extraConfig = '' + try_files $uri @cgit; + ''; + }; + "@cgit" = { + extraConfig = '' + uwsgi_pass unix:/run/uwsgi/cgit.sock; + include ${pkgs.nginx}/conf/uwsgi_params; + uwsgi_modifier1 9; + ''; + }; + }; + }; + + networking.firewall.allowedTCPPorts = [ 80 443 ]; + + systemd.services.create-cgit-cache = { + description = "Create cache directory for cgit"; + enable = true; + + script = '' + mkdir -p /run/cgit + chown -R nginx:nginx /run/cgit + ''; + + wantedBy = [ "uwsgi.service" ]; + serviceConfig = { + Type = "oneshot"; + }; + }; + + environment.etc."cgitrc".text = '' + virtual-root=/ + + cache-size=1000 + cache-root=/run/cgit + + root-title=${cfg.domain} git + root-desc=Exotic place. + + snapshots=tar.gz zip + + enable-git-config=1 + remove-suffix=1 + + enable-git-clone=1 + enable-index-links=1 + enable-commit-graph=1 + enable-log-filecount=1 + enable-log-linecount=1 + + branch-sort=age + + readme=:README + readme=:readme + readme=:README.md + readme=:readme.md + readme=:README.org + readme=:readme.org + + source-filter=${pkgs.cgit-pink}/lib/cgit/filters/syntax-highlighting.py + about-filter=${pkgs.cgit-pink}/lib/cgit/filters/about-formatting.sh + + section-from-path=2 + + project-list=${config.services.gitolite.dataDir}/projects.list + scan-path=${config.services.gitolite.dataDir}/repositories + ''; + }; +} diff --git a/modules/services/coredns/_corefile.nix b/modules/services/coredns/_corefile.nix new file mode 100644 index 0000000..8d0ec66 --- /dev/null +++ b/modules/services/coredns/_corefile.nix @@ -0,0 +1,3 @@ +'' +Add content here +'' diff --git a/modules/services/coredns/default.nix b/modules/services/coredns/default.nix new file mode 100644 index 0000000..52d8570 --- /dev/null +++ b/modules/services/coredns/default.nix @@ -0,0 +1,18 @@ +{ config, lib, ... }: + +with lib; +let + cfg = config.modules.services.coredns; +in +{ + options.modules.services.coredns = { + enable = mkEnableOption "coredns"; + }; + + config = mkIf cfg.enable { + services.coredns = { + enable = true; + config = import ./_corefile.nix; + }; + }; +} diff --git a/modules/services/coturn.nix b/modules/services/coturn.nix new file mode 100644 index 0000000..967ba60 --- /dev/null +++ b/modules/services/coturn.nix @@ -0,0 +1,64 @@ +{ config, lib, ... }: + +with lib; +let + turnRange = with config.services.coturn; [{ + from = min-port; + to = max-port; + }]; + + cfg = config.modules.services.coturn; +in +{ + options.modules.services.coturn = { + enable = mkEnableOption "coturn"; + domain = mkOption { type = types.str; default = config.networking.hostName; }; + shared_secret = mkOption { type = types.str; }; + tls.acmeHost = mkOption { type = types.str; default = cfg.domain; }; + }; + + config = mkIf cfg.enable { + services.coturn = { + enable = true; + use-auth-secret = true; + static-auth-secret = cfg.shared_secret; + realm = cfg.domain; + cert = "${config.security.acme.certs.${cfg.tls.acmeHost}.directory}/fullchain.pem"; + pkey = "${config.security.acme.certs.${cfg.tls.acmeHost}.directory}/key.pem"; + + no-tcp-relay = true; + no-cli = true; + + extraConfig = '' + user-quota=12 + total-quota=1200 + + no-loopback-peers + no-multicast-peers + denied-peer-ip=0.0.0.0-0.255.255.255 + denied-peer-ip=10.0.0.0-10.255.255.255 + denied-peer-ip=100.64.0.0-100.127.255.255 + denied-peer-ip=127.0.0.0-127.255.255.255 + denied-peer-ip=169.254.0.0-169.254.255.255 + denied-peer-ip=172.16.0.0-172.31.255.255 + denied-peer-ip=192.0.0.0-192.0.0.255 + denied-peer-ip=192.0.2.0-192.0.2.255 + denied-peer-ip=192.88.99.0-192.88.99.255 + denied-peer-ip=192.168.0.0-192.168.255.255 + denied-peer-ip=198.18.0.0-198.19.255.255 + denied-peer-ip=198.51.100.0-198.51.100.255 + denied-peer-ip=203.0.113.0-203.0.113.255 + denied-peer-ip=240.0.0.0-255.255.255.255 + ''; + }; + + systemd.services.coturn = { + serviceConfig.SupplementaryGroups = [ "acme" ]; + }; + + networking.firewall.allowedUDPPortRanges = turnRange; + networking.firewall.allowedTCPPortRanges = turnRange; + networking.firewall.allowedTCPPorts = [ 3478 3479 5349 5350 ]; + networking.firewall.allowedUDPPorts = [ 3478 3479 5349 5350 ]; + }; +} diff --git a/modules/services/dendrite.nix b/modules/services/dendrite.nix new file mode 100644 index 0000000..70f9db8 --- /dev/null +++ b/modules/services/dendrite.nix @@ -0,0 +1,230 @@ +{ config, lib, ... }: + +with lib; +let + cfg = config.modules.services.dendrite; + + database = { + connection_string = "postgres:///dendrite?host=/run/postgresql"; + max_open_conns = 100; + max_idle_conns = 5; + conn_max_lifetime = -1; + }; +in +{ + imports = [ + ../../overlays/sliding-sync-module.nix + ]; + + options.modules.services.dendrite = { + enable = mkEnableOption "dendrite instance"; + domain = mkOption { type = types.str; }; + realHost = mkOption { type = types.str; default = "matrix.${cfg.domain}"; }; + slidingSyncHost = mkOption { type = types.str; default = "slidingsync.${cfg.domain}"; }; + turn = { + enable = mkEnableOption "VOIP suing TURN"; + domain = mkOption { type = types.str; default = "turn.${cfg.domain}"; }; + shared_secret = mkOption { type = types.str; }; + }; + secrets = { + matrix-server-key = mkOption { type = types.str; description = "path to the server key"; }; + dendrite-envs = mkOption { type = types.nullOr types.str; description = "path for the environment file to source"; }; + sliding-sync-secret = mkOption { type = types.nullOr types.str; description = "path to the sliding sync secret"; }; + }; + }; + + config = mkIf cfg.enable { + # Adapted from Mic92/dotfiles, (C) 2021 Jörg Thalheim (MIT) + services.dendrite = { + enable = true; + settings = { + global = { + server_name = cfg.domain; + # `private_key` has the type `path` + # prefix a `/` to make `path` happy + private_key = "/$CREDENTIALS_DIRECTORY/matrix-server-key"; + jetstream.storage_path = "/var/lib/dendrite/jetstream"; + trusted_third_party_id_servers = [ + "matrix.org" + "vector.im" + ]; + metrics.enabled = true; + }; + logging = [ + { + type = "std"; + level = "info"; # "warn" on public release + } + ]; + app_service_api = { + inherit database; + config_files = [ ]; + }; + client_api = { + registration_disabled = true; + rate_limiting.enabled = false; + rate_limiting.exempt_user_ids = [ + "@abuse:${cfg.domain}" + ]; + # registration_shared_secret = ""; # Initially set this option to configure the admin user. + } // optionalAttrs cfg.turn.enable { + turn = { + turn_user_lifetime = "24h"; + turn_uris = [ + "turns:${cfg.turn.domain}?transport=udp" + "turns:${cfg.turn.domain}?transport=tcp" + "turn:${cfg.turn.domain}?transport=udp" + "turn:${cfg.turn.domain}?transport=tcp" + ]; + turn_shared_secret = cfg.turn.shared_secret; + }; + }; + media_api = { + inherit database; + dynamic_thumbnails = true; + }; + room_server = { + inherit database; + }; + push_server = { + inherit database; + }; + mscs = { + inherit database; + mscs = [ "msc2836" "msc2946" ]; + }; + sync_api = { + inherit database; + real_ip_header = "X-Real-IP"; + # The NixOS option is 'enable', which doesn't exist in Dendrite. + search.enabled = true; + }; + key_server = { + inherit database; + }; + federation_api = { + inherit database; + key_perspectives = [ + { + server_name = "matrix.org"; + keys = [ + { + key_id = "ed25519:auto"; + public_key = "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw"; + } + { + key_id = "ed25519:a_RXGa"; + public_key = "l8Hft5qXKn1vfHrg3p4+W8gELQVo8N13JkluMfmn2sQ"; + } + ]; + } + ]; + prefer_direct_fetch = false; + }; + user_api = { + account_database = database; + device_database = database; + }; + }; + loadCredential = [ "matrix-server-key:${cfg.secrets.matrix-server-key}" ]; + } // optionalAttrs (cfg.secrets.dendrite-envs != null) { + environmentFile = cfg.secrets.dendrite-envs; + }; + + services.prometheus.scrapeConfigs = [ + { + job_name = "dendrite"; + static_configs = [{ + targets = [ "127.0.0.1:${toString config.services.dendrite.httpPort}" ]; + }]; + } + ]; + + systemd.services.dendrite = { + after = [ "postgresql.service" ]; + }; + + environment.persistence."/persist".directories = [ + "/var/lib/private/dendrite" + ]; + + services.sliding-sync = { + enable = true; + server = "https://${cfg.realHost}"; + bindAddr = "[::1]:8009"; + db = "postgres:///syncv3?host=/run/postgresql"; + secret = cfg.secrets.sliding-sync-secret; + }; + + services.postgresql.enable = true; + services.postgresql.ensureDatabases = [ "dendrite" "syncv3" ]; + services.postgresql.ensureUsers = [ + { + name = "dendrite"; + ensurePermissions."DATABASE dendrite" = "ALL PRIVILEGES"; + } + { + name = "sliding-sync"; + ensurePermissions."DATABASE syncv3" = "ALL PRIVILEGES"; + } + ]; + + services.nginx.virtualHosts.${cfg.realHost} = { + forceSSL = true; + useACMEHost = cfg.domain; + listen = [ + { addr = "0.0.0.0"; port = 443; ssl = true; } + { addr = "[::]"; port = 443; ssl = true; } + { addr = "0.0.0.0"; port = 8448; ssl = true; } + { addr = "[::]"; port = 8448; ssl = true; } + + ]; + extraConfig = '' + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_read_timeout 600; + client_max_body_size 50M; + ''; + locations."/_matrix".proxyPass = "http://[::1]:${toString config.services.dendrite.httpPort}"; + locations."/_dendrite".proxyPass = "http://[::1]:${toString config.services.dendrite.httpPort}"; + locations."/_synapse".proxyPass = "http://[::1]:${toString config.services.dendrite.httpPort}"; + }; + + services.nginx.virtualHosts.${cfg.domain} = + let + server-hello = { "m.server" = "${cfg.realHost}:443"; }; + client-hello = { + "m.homeserver"."base_url" = "https://${cfg.realHost}"; + "m.identity_server"."base_url" = "https://vector.im"; + "org.matrix.msc3575.proxy"."url" = "https://${cfg.slidingSyncHost}"; + }; + in + { + forceSSL = true; + useACMEHost = cfg.domain; + locations = { + "/.well-known/matrix/server" = { + extraConfig = '' + add_header Content-Type application/json; + return 200 '${builtins.toJSON server-hello}'; + ''; + }; + "/.well-known/matrix/client" = { + extraConfig = '' + add_header Content-Type application/json; + add_header Access-Control-Allow-Origin *; + return 200 '${builtins.toJSON client-hello}'; + ''; + }; + }; + }; + + services.nginx.virtualHosts.${cfg.slidingSyncHost} = { + forceSSL = true; + useACMEHost = cfg.domain; + locations."/".proxyPass = "http://${config.services.sliding-sync.bindAddr}"; + }; + + networking.firewall.allowedTCPPorts = [ 443 8448 ]; + }; +} diff --git a/modules/services/dovecot.nix b/modules/services/dovecot.nix new file mode 100644 index 0000000..a33b0d1 --- /dev/null +++ b/modules/services/dovecot.nix @@ -0,0 +1,18 @@ +{ config, lib, ... }: + +with lib; +let + cfg = config.modules.services.dovecot; +in +{ + options.modules.services.dovecot = { + enable = mkEnableOption "dovecot"; + }; + + config = mkIf cfg.enable { + services.dovecot2 = { + enable = true; + }; + networking.firewall.allowedTCPPorts = [ 587 465 ]; + }; +} diff --git a/modules/services/element-web.nix b/modules/services/element-web.nix new file mode 100644 index 0000000..2b200bd --- /dev/null +++ b/modules/services/element-web.nix @@ -0,0 +1,47 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + cfg = config.modules.services.element-web; +in +{ + options.modules.services.element-web = { + enable = mkEnableOption "element-web"; + package = mkOption { type = types.package; default = pkgs.element-web; }; + hostName = mkOption { type = types.str; default = config.networking.hostName; }; + matrix = { + baseUrl = mkOption { type = types.str; default = "https://matrix.${config.networking.hostName}"; }; + serverName = mkOption { type = types.str; default = config.networking.hostName; }; + }; + tls.acmeHost = mkOption { type = types.str; default = cfg.hostName; }; + jitsi.domain = mkOption { type = types.str; default = "jitsi.${cfg.hostName}"; }; + }; + + config = mkIf cfg.enable { + services.nginx.virtualHosts.${cfg.hostName} = { + useACMEHost = cfg.tls.acmeHost; + forceSSL = true; + + root = cfg.package.override { + conf = { + default_server_config = { + "m.homeserver" = { + "base_url" = cfg.matrix.baseUrl; + "server_name" = cfg.matrix.serverName; + }; + "m.identity_server" = { + "base_url" = "https://vector.im"; + }; + }; + showLabsSettings = true; + } // optionalAttrs (cfg.jitsi.domain != null) { + jitsi.preferredDomain = cfg.jitsi.domain; + }; + }; + + locations."~ \\.(js|css|woff|woff2?|png|jpe?g|svg)$".extraConfig = '' + add_header Cache-Control "public, max-age=14400, must-revalidate"; + ''; + }; + }; +} diff --git a/modules/services/fail2ban.nix b/modules/services/fail2ban.nix new file mode 100644 index 0000000..99351b1 --- /dev/null +++ b/modules/services/fail2ban.nix @@ -0,0 +1,17 @@ +{ config, lib, ... }: + +with lib; +let + cfg = config.modules.services.fail2ban; +in +{ + options.modules.services.fail2ban = { + enable = mkEnableOption "fail2ban"; + }; + + config = mkIf cfg.enable { + services.fail2ban = { + enable = true; + }; + }; +} diff --git a/modules/services/git-daemon.nix b/modules/services/git-daemon.nix new file mode 100644 index 0000000..5d027de --- /dev/null +++ b/modules/services/git-daemon.nix @@ -0,0 +1,29 @@ +{ config, lib, ... }: + +with lib; +let + cfg = config.modules.services.gitDaemon; +in +{ + disabledModules = [ + "services/networking/git-daemon.nix" + ]; + + imports = [ + ../../overlays/git-daemon-module.nix + ]; + + options.modules.services.gitDaemon = { + enable = mkEnableOption "git daemon"; + }; + + config = mkIf cfg.enable { + services.gitDaemon = { + enable = true; + createUserAndGroup = false; + basePath = "/var/lib/gitolite/repositories"; + }; + + networking.firewall.allowedTCPPorts = [ 9418 ]; + }; +} diff --git a/modules/services/gitolite/default.nix b/modules/services/gitolite/default.nix new file mode 100644 index 0000000..c2eb975 --- /dev/null +++ b/modules/services/gitolite/default.nix @@ -0,0 +1,108 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + cfg = config.modules.services.gitolite; +in +{ + options.modules.services.gitolite = { + enable = mkEnableOption "gitolite server"; + adminPubkey = mkOption { type = types.str; }; + }; + config = mkIf cfg.enable { + services.openssh.enable = true; + + services.gitolite = { + enable = true; + user = "git"; + group = "git"; + adminPubkey = cfg.adminPubkey; + extraGitoliteRc = '' + $RC{UMASK} = 0027; + $RC{GIT_CONFIG_KEYS} = '.*'; + $RC{ROLES}{OWNERS} = 1; + $RC{OWNER_ROLENAME} = 'OWNERS'; + # For some unknown reason, $ENV{HOME} doesn't get resolved to the correct + # directory. + # $RC{LOCAL_CODE} = '$ENV{HOME}/local'; + $RC{LOCAL_CODE} = '/var/lib/gitolite/local'; + push(@{$RC{ENABLE}}, 'D'); + push(@{$RC{ENABLE}}, 'symbolic-ref'); + push(@{$RC{ENABLE}}, 'rename'); + push(@{$RC{POST_GIT}}, 'fix-refs'); + # push(@{$RC{ENABLE}}, 'set-default-roles'); + # push(@{$RC{ENABLE}}, 'create'); + # push(@{$RC{ENABLE}}, 'fork'); + + ''; + }; + + environment.persistence."/persist".directories = [ + "/var/lib/gitolite" + ]; + + system.activationScripts.gitolite-create-local = '' + mkdir -p /var/lib/gitolite/local/triggers + mkdir -p /var/lib/gitolite/local/commands + chown -R git:git /var/lib/gitolite/local + ''; + + systemd.tmpfiles.rules = [ + # https://groups.google.com/g/gitolite/c/NwZ1-hq9-9E/m/mDbiKyAvDwAJ + "C /var/lib/gitolite/local/triggers/fix-refs 755 - - - ${./fix-refs}" + "C /var/lib/gitolite/local/commands/rename 755 - - - ${./rename}" + ]; + + + systemd.timers."gitolite-trash-cleanup" = { + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "*-*-* 00:00:00"; + Unit = "gitolite-trash-cleanup.service"; + }; + }; + + systemd.services."gitolite-trash-cleanup" = { + script = '' + set -euo pipefail + if [ ! -d "Trash" ] ; then + echo Trash directory is nonexistent! + echo No operations to perform. Exiting. + exit 0 + fi + + match=$(find Trash -type d -regextype posix-extended -regex ".*/[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}:[0-9]{2}:[0-9]{2}$") + processed_entry=0 + removed_entry=0 + + for dir in $match + do + system_timestamp=$(date +%s) + trash_timestamp=$(basename $dir | sed -e "s/_/ /g" | date -f - +%s) + age=$(( $system_timestamp - $trash_timestamp )) + # Wipe trashes older than 2w + if [[ age -gt 1209600 ]] ; then + echo "Removing '$dir' (age $age)" + rm -rf $dir + ((removed_entry+=1)) + fi + ((processed_entry+=1)) + done + + echo "Directories that needs cleanup:" + find Trash -type d -empty -print -delete + echo "Cleaned empty directories." + + echo "Done! Removed $removed_entry/$processed_entry" + ''; + + path = with pkgs; [ bash util-linux coreutils ]; + + serviceConfig = { + Type = "oneshot"; + User = "git"; + WorkingDirectory = "/var/lib/gitolite/repositories"; + }; + }; + }; +} diff --git a/modules/services/gitolite/fix-refs b/modules/services/gitolite/fix-refs new file mode 100644 index 0000000..8ffec9e --- /dev/null +++ b/modules/services/gitolite/fix-refs @@ -0,0 +1,9 @@ +[[ $4 == W ]] || exit 0 + +cd $GL_REPO_BASE/$2.git + +head=`git symbolic-ref HEAD` +[[ -f $head ]] || { + set -- refs/heads/* + git symbolic-ref HEAD $1 +} diff --git a/modules/services/gitolite/rename b/modules/services/gitolite/rename new file mode 100644 index 0000000..2b00c7a --- /dev/null +++ b/modules/services/gitolite/rename @@ -0,0 +1,63 @@ + +# Usage: ssh git@host rename [-c] +# +# Renames repo1 to repo2. You must be the creator of repo1, and have +# create ("C") permissions for repo2, which of course must not exist. +# Alternatively you must be an account admin, that is, you must have +# write access to the gitolite-admin repository. If you have "C" +# permissions for repo2 then you can use the -c option to take over +# as creator of the repository. + +die() { echo "$@" >&2; exit 1; } +usage() { perl -lne 'print substr($_, 2) if /^# Usage/../^$/' < $0; exit 1; } +[ -z "$1" ] && usage +[ "$1" = "-h" ] && usage +[ -z "$GL_USER" ] && die GL_USER not set + +# ---------------------------------------------------------------------- + +if [ "$1" = "-c" ] +then shift + takeover=true +else takeover=false +fi + +from="$1"; shift +to="$1"; shift +[ -z "$to" ] && usage + +topath=$GL_REPO_BASE/$to.git + +checkto() { + gitolite access -q "$to" $GL_USER ^C any || + die "'$to' already exists or you are not allowed to create it" +} + +if gitolite access -q gitolite-admin $GL_USER +then + # the user is an admin so we can avoid most permission checks + if $takeover + then checkto + elif [ -e $topath ] + then die "'$to' already exists" + fi +else + # the user isn't an admin, so do all the checks + checkto + gitolite creator "$from" $GL_USER || + die "'$from' does not exist or you are not allowed to delete it" +fi + +# ---------------------------------------------------------------------- + +mv $GL_REPO_BASE/$from.git $topath +[ $? -ne 0 ] && exit 1 + +$takeover && echo $GL_USER > $topath/gl-creator + +# Rebuild projects.list +gitolite trigger POST_COMPILE + +echo "$from renamed to $to" >&2 + +exit diff --git a/modules/services/jitsi.nix b/modules/services/jitsi.nix new file mode 100644 index 0000000..ab02bb4 --- /dev/null +++ b/modules/services/jitsi.nix @@ -0,0 +1,38 @@ +{ config, lib, ... }: + +with lib; +let + cfg = config.modules.services.jitsi; +in +{ + options.modules.services.jitsi = { + enable = mkEnableOption "jitsi"; + hostName = mkOption { type = types.str; default = config.networking.hostName; }; + tls.acmeHost = mkOption { type = types.str; default = cfg.hostName; }; + }; + + config = mkIf cfg.enable { + services.jitsi-meet = { + enable = true; + hostName = cfg.hostName; + + config = { + prejoinPageEnabled = true; + }; + + interfaceConfig = { + SHOW_JITSI_WATERMARK = false; + }; + }; + + services.jitsi-videobridge.openFirewall = true; + + services.nginx.virtualHosts.${cfg.hostName} = { + enableACME = mkForce false; + useACMEHost = cfg.tls.acmeHost; + forceSSL = true; + }; + + networking.firewall.allowedTCPPorts = [ 80 443 ]; + }; +} diff --git a/modules/services/ldap.nix b/modules/services/ldap.nix new file mode 100644 index 0000000..ba19761 --- /dev/null +++ b/modules/services/ldap.nix @@ -0,0 +1,76 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + cfg = config.modules.services.ldap; +in +{ + options.modules.services.ldap = { + enable = mkEnableOption "OpenLDAP server"; + package = mkOption { type = types.package; default = pkgs.openldap; }; + dc = mkOption { type = types.str; }; + tld = mkOption { type = types.str; }; + tls.acmeHost = mkOption { type = types.str; default = "${cfg.dc}.${cfg.tld}"; }; + secrets.rootPass = mkOption { type = types.str; description = "path to the root password file"; }; + }; + + config = mkIf cfg.enable { + services.openldap = { + enable = true; + + urlList = [ "ldap:///" "ldaps:///" ]; + + settings = { + attrs = { + olcLogLevel = "conns config"; + + olcTLSCACertificateFile = "${config.security.acme.certs.${cfg.tls.acmeHost}.directory}/full.pem"; + olcTLSCertificateFile = "${config.security.acme.certs.${cfg.tls.acmeHost}.directory}/cert.pem"; + olcTLSCertificateKeyFile = "${config.security.acme.certs.${cfg.tls.acmeHost}.directory}/key.pem"; + olcTLSCipherSuite = "HIGH:MEDIUM:+3DES:+RC4:+aNULL"; + olcTLSCRLCheck = "none"; + olcTLSVerifyClient = "never"; + olcTLSProtocolMin = "3.1"; + }; + + children = { + "cn=schema".includes = [ + "${cfg.package}/etc/schema/core.ldif" + "${cfg.package}/etc/schema/cosine.ldif" + "${cfg.package}/etc/schema/inetorgperson.ldif" + ]; + + "olcDatabase={1}mdb".attrs = { + objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ]; + + olcDatabase = "{1}mdb"; + olcDbDirectory = "/var/lib/openldap/data"; + + olcSuffix = "dc=${cfg.dc},dc=${cfg.tld}"; + + olcRootDN = "cn=admin,dc=${cfg.dc},dc=${cfg.tld}"; + olcRootPW.path = cfg.secrets.rootPass; + + olcAccess = [ + # ''{0}to + # by '' + + ''{0}to * + by * none'' # Should be changed to {1} + ]; + }; + }; + }; + }; + + systemd.services.openldap = { + after = [ "acme-finished-${cfg.tls.acmeHost}.target" ]; + }; + + users.groups.acme.members = [ "openldap" ]; + + environment.persistence."/persist".directories = [ + "/var/lib/openldap" + ]; + }; +} diff --git a/modules/services/matrix-bridge.nix b/modules/services/matrix-bridge.nix new file mode 100644 index 0000000..65d8187 --- /dev/null +++ b/modules/services/matrix-bridge.nix @@ -0,0 +1,200 @@ +{ config, lib, ... }: + +with lib; +let + cfg = config.modules.services.matrix-bridge; +in +{ + imports = [ + ../../overlays/mautrix-signal-module.nix + ../../overlays/mautrix-whatsapp-module.nix + ]; + + options.modules.services.matrix-bridge = { + enable = mkEnableOption "matrix-bridge"; + domain = mkOption { type = types.str; }; + realHost = mkOption { type = types.str; default = "matrix.${cfg.domain}"; }; + secrets.mautrix-envs = mkOption { type = types.str; description = "path to the mautrix-* environment file"; }; + }; + + config = mkIf cfg.enable { + services.mautrix-telegram = { + enable = true; + environmentFile = cfg.secrets.mautrix-envs; + serviceDependencies = [ "dendrite.service" ]; + + settings = { + homeserver.address = "https://${cfg.realHost}"; + homeserver.domain = cfg.domain; + homeserver.verify_ssl = true; + appservice = { + address = "http://localhost:29317"; + port = 29317; + database = "postgres:///mautrix-telegram?host=/run/postgresql"; + bot_avatar = "mxc://maunium.net/tJCRmUyJDsgRNgqhOgoiHWbX"; + id = "telegram"; + max_body_size = 1; + provisioning.enabled = false; + }; + bridge = { + alias_template = "tg_{groupname}"; + username_templace = "tg_{userid}"; + allow_matrix_login = true; + bot_messages_as_notices = true; + catch_up = true; + plaintext_highlights = true; + startup_sync = true; + animated_stickers = { + target = "webp"; + convert_from_webm = true; + }; + permissions = { + "@sef:exotic.sh" = "admin"; + "exotic.sh" = "full"; + }; + }; + }; + }; + + services.mautrix-signal = { + enable = true; + environmentFile = cfg.secrets.mautrix-envs; + serviceDependencies = [ "dendrite.service" ]; + + settings = { + homeserver.address = "https://${cfg.realHost}"; + homeserver.domain = cfg.domain; + homeserver.verify_ssl = true; + appservice = { + address = "http://localhost:29318"; + port = 29318; + database = "postgres:///mautrix-signal?host=/run/postgresql"; + bot_avatar = "mxc://maunium.net/wPJgTQbZOtpBFmDNkiNEMDUp"; + id = "signal"; + max_body_size = 1; + provisioning.enabled = false; + }; + + signal = { + avatar_dir = "/var/lib/signald/avatars"; + data_dir = "/var/lib/signald/data"; + }; + + bridge = { + alias_template = "sig_{groupname}"; + username_templace = "sig_{userid}"; + allow_matrix_login = true; + catch_up = true; + plaintext_highlights = true; + startup_sync = true; + animated_stickers = { + target = "webp"; + convert_from_webm = true; + }; + permissions = { + "@sef:exotic.sh" = "admin"; + "exotic.sh" = "full"; + }; + }; + }; + }; + + services.mautrix-whatsapp = { + enable = true; + environmentFile = cfg.secrets.mautrix-envs; + serviceDependencies = [ "dendrite.service" ]; + + settings = { + homeserver.address = "https://${cfg.realHost}"; + homeserver.domain = cfg.domain; + homeserver.verify_ssl = true; + appservice = { + address = "http://localhost:29319"; + port = 29319; + database = { + type = "postgres"; + uri = "postgres://mautrix-whatsapp:@/mautrix-whatsapp?host=/run/postgresql"; + }; + bot_avatar = "mxc://maunium.net/NeXNQarUbrlYBiPCpprYsRqr"; + id = "whatsapp"; + max_body_size = 1; + provisioning.enabled = false; + }; + + bridge = { + alias_template = "wa_{groupname}"; + username_templace = "wa_{userid}"; + personal_filtering_spaces = true; + delivery_receipts = true; + identity_change_notices = true; + hystory_sync = { + backfill = false; # MSC2716 + request_full_sync = true; + }; + send_presence_on_typing = true; + double_puppet_server_map = { }; + login_shared_secret_map = { }; + private_chat_portal_meta = true; + mute_bridging = true; + pinned_tag = "m.favourite"; + archive_tag = "m.lowpriority"; + allow_user_invite = true; + disappearing_messages_in_groups = true; + url_previews = true; + # TODO: https://github.com/matrix-org/dendrite/issues/2723 + # encryption = { + # allow = true; + # default = true; + # require = true; + # allow_key_sharing = true; + # }; + sync_manual_marked_unread = true; + force_active_delivery_receipts = true; + parallel_member_sync = true; + extev_polls = true; + send_whatsapp_edits = true; + permissions = { + "@sef:exotic.sh" = "admin"; + "exotic.sh" = "full"; + }; + }; + }; + }; + + + environment.persistence."/persist".directories = [ + "/var/lib/private/mautrix-telegram" + "/var/lib/private/mautrix-signal" + "/var/lib/private/mautrix-whatsapp" + "/var/lib/signald" + ]; + + modules.services.postgresql.enable = true; + services.postgresql.ensureDatabases = [ "mautrix-telegram" "mautrix-signal" "mautrix-whatsapp" ]; + services.postgresql.ensureUsers = [ + { + name = "mautrix-telegram"; + ensurePermissions."DATABASE \"mautrix-telegram\"" = "ALL PRIVILEGES"; + } + { + name = "mautrix-signal"; + ensurePermissions."DATABASE \"mautrix-signal\"" = "ALL PRIVILEGES"; + } + { + name = "mautrix-whatsapp"; + ensurePermissions."DATABASE \"mautrix-whatsapp\"" = "ALL PRIVILEGES"; + } + ]; + + systemd.services.dendrite = { + serviceConfig.SupplementaryGroups = [ "mautrix-telegram" "mautrix-signal" "mautrix-whatsapp" ]; + }; + + services.dendrite.settings.app_service_api.config_files = [ + # Symlinks doesn't seem to work. Provide the actual path. + "/persist/var/lib/private/mautrix-telegram/telegram-registration.yaml" + "/persist/var/lib/private/mautrix-signal/signal-registration.yaml" + "/persist/var/lib/private/mautrix-whatsapp/whatsapp-registration.yaml" + ]; + }; +} diff --git a/modules/services/matrix-moderation.nix b/modules/services/matrix-moderation.nix new file mode 100644 index 0000000..c8f0702 --- /dev/null +++ b/modules/services/matrix-moderation.nix @@ -0,0 +1,52 @@ +{ config, lib, ... }: + +# TODO: rename + +with lib; +let + cfg = config.modules.services.matrix-moderation; +in +{ + disabledModules = [ + "services/matrix/mjolnir.nix" + ]; + + imports = [ + ../../overlays/mjolnir-module + ]; + + options.modules.services.matrix-moderation = { + enable = mkEnableOption "matrix-moderation"; + domain = mkOption { type = types.str; }; + realHost = mkOption { type = types.str; default = "matrix.${cfg.domain}"; }; + secrets.userPassword = mkOption { type = types.str; description = "path to the mjolnir password"; }; + }; + + config = mkIf cfg.enable { + + services.mjolnir = { + enable = true; + homeserverUrl = "https://${cfg.realHost}"; + pantalaimon.enable = true; + pantalaimon.username = "abuse"; + pantalaimon.passwordFile = cfg.secrets.userPassword; + managementRoom = "#moderation:${cfg.domain}"; + + settings = { + homeserverUrl = "http://127.0.0.1:8009"; + automaticallyRedactForReasons = [ + "spam" + "advertising" + "unwanted" + ]; + }; + }; + + systemd.services.mjolnir.after = [ "dendrite.service" ]; + + environment.persistence."/persist".directories = [ + "/var/lib/private/pantalaimon-mjolnir" + "/var/lib/mjolnir" + ]; + }; +} diff --git a/modules/services/metrics.nix b/modules/services/metrics.nix new file mode 100644 index 0000000..74f7e9a --- /dev/null +++ b/modules/services/metrics.nix @@ -0,0 +1,165 @@ +{ config, lib, ... }: + +with lib; +let + cfg = config.modules.services.metrics; +in +{ + options.modules.services.metrics = { + enable = mkEnableOption "metrics"; + domain = mkOption { type = types.str; }; + tls.acmeHost = mkOption { type = types.str; default = cfg.domain; }; + }; + + config = mkIf cfg.enable { + services.prometheus = { + enable = true; + port = 9001; + + exporters = { + node = { + enable = true; + enabledCollectors = [ "systemd" ]; + port = 9002; + }; + }; + + scrapeConfigs = [ + { + job_name = "node"; + static_configs = [{ + targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.node.port}" ]; + }]; + } + ]; + }; + + services.loki = { + enable = true; + configuration = { + auth_enabled = false; + server.http_listen_port = 3100; + + ingester = { + lifecycler = { + address = "127.0.0.1"; + ring.kvstore.store = "inmemory"; + ring.replication_factor = 1; + final_sleep = "0s"; + }; + chunk_idle_period = "1h"; + max_chunk_age = "1h"; + chunk_target_size = 1048576; # 1.5M + chunk_retain_period = "30s"; + max_transfer_retries = 0; + }; + + schema_config.configs = [ + { + from = "2023-02-24"; + store = "boltdb-shipper"; + object_store = "filesystem"; + schema = "v11"; + index = { + prefix = "index_"; + period = "24h"; + }; + } + ]; + + storage_config = { + boltdb_shipper = { + active_index_directory = "/var/lib/loki/boltdb-shipper-active"; + cache_location = "/var/lib/loki/boltdb-shipper-cache"; + cache_ttl = "24h"; + shared_store = "filesystem"; + }; + + filesystem.directory = "/var/lib/loki/chunks"; + }; + + limits_config = { + reject_old_samples = true; + reject_old_samples_max_age = "168h"; + }; + + chunk_store_config = { + max_look_back_period = "0s"; + }; + + table_manager = { + retention_deletes_enabled = false; + retention_period = "0s"; + }; + + compactor = { + working_directory = "/var/lib/loki"; + shared_store = "filesystem"; + compactor_ring.kvstore.store = "inmemory"; + }; + }; + }; + + services.promtail = { + enable = true; + configuration = { + server = { + http_listen_port = 3031; + grpc_listen_port = 0; + }; + positions.filename = "/tmp/positions.yaml"; + clients = [ + { url = "http://127.0.0.1:${toString config.services.loki.configuration.server.http_listen_port}/loki/api/v1/push"; } + ]; + scrape_configs = [ + { + job_name = "journal"; + journal = { + max_age = "12h"; + labels = { + job = "systemd-journal"; + host = config.networking.hostName; + }; + }; + relabel_configs = [ + { + source_labels = [ "__journal__systemd_unit" ]; + target_label = "unit"; + } + ]; + } + ]; + }; + }; + + services.grafana = { + enable = true; + + settings.server.http_addr = "127.0.0.1"; + settings.server.http_port = 2342; + settings.server.domain = cfg.domain; + settings.security.admin_password = "supersecurepass"; + }; + + services.nginx.virtualHosts.${cfg.domain} = { + forceSSL = true; + useACMEHost = cfg.tls.acmeHost; + + locations."/" = { + proxyPass = "http://localhost:${toString config.services.grafana.settings.server.http_port}"; + proxyWebsockets = true; + extraConfig = '' + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_set_header Host $host; + ''; + }; + }; + + environment.persistence."/persist".directories = [ + "/var/lib/prometheus2" + "/var/lib/loki" + "/var/lib/grafana" + ]; + }; +} + diff --git a/modules/services/misskey/config/default.yml b/modules/services/misskey/config/default.yml new file mode 100644 index 0000000..cab83b8 --- /dev/null +++ b/modules/services/misskey/config/default.yml @@ -0,0 +1,156 @@ +#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# Misskey configuration +#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +# ┌─────┐ +#───┘ URL └───────────────────────────────────────────────────── + +# Final accessible URL seen by a user. +url: https://nand.moe + +# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE +# URL SETTINGS AFTER THAT! + +# ┌───────────────────────┐ +#───┘ Port and TLS settings └─────────────────────────────────── + +# +# Misskey requires a reverse proxy to support HTTPS connections. +# +# +----- https://example.tld/ ------------+ +# +------+ |+-------------+ +----------------+| +# | User | ---> || Proxy (443) | ---> | Misskey (3000) || +# +------+ |+-------------+ +----------------+| +# +---------------------------------------+ +# +# You need to set up a reverse proxy. (e.g. nginx) +# An encrypted connection with HTTPS is highly recommended +# because tokens may be transferred in GET requests. + +# The port that your Misskey server should listen on. +port: 3000 + +# ┌──────────────────────────┐ +#───┘ PostgreSQL configuration └──────────────────────────────── + +db: + host: localhost + port: 5432 + + # Database name + db: misskey + + # Auth + user: misskey + # pass: example-misskey-pass + + # Whether disable Caching queries + #disableCache: true + + # Extra Connection options + #extra: + # ssl: true + +# ┌─────────────────────┐ +#───┘ Redis configuration └───────────────────────────────────── + +redis: + host: localhost + port: 16434 + family: 4 # 0=Both, 4=IPv4, 6=IPv6 + #pass: example-pass + #prefix: example-prefix + #db: 1 + +# ┌─────────────────────────────┐ +#───┘ Elasticsearch configuration └───────────────────────────── + +#elasticsearch: +# host: localhost +# port: 9200 +# ssl: false +# user: +# pass: + +# ┌───────────────┐ +#───┘ ID generation └─────────────────────────────────────────── + +# You can select the ID generation method. +# You don't usually need to change this setting, but you can +# change it according to your preferences. + +# Available methods: +# aid ... Short, Millisecond accuracy +# meid ... Similar to ObjectID, Millisecond accuracy +# ulid ... Millisecond accuracy +# objectid ... This is left for backward compatibility + +# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE +# ID SETTINGS AFTER THAT! + +id: 'aid' + +# ┌─────────────────────┐ +#───┘ Other configuration └───────────────────────────────────── + +# Whether disable HSTS +#disableHsts: true + +# Number of worker processes +#clusterLimit: 1 + +# Job concurrency per worker +# deliverJobConcurrency: 128 +# inboxJobConcurrency: 16 + +# Job rate limiter +# deliverJobPerSec: 128 +# inboxJobPerSec: 16 + +# Job attempts +# deliverJobMaxAttempts: 12 +# inboxJobMaxAttempts: 8 + +# IP address family used for outgoing request (ipv4, ipv6 or dual) +#outgoingAddressFamily: ipv4 + +# Proxy for HTTP/HTTPS +#proxy: http://127.0.0.1:3128 + +proxyBypassHosts: + - api.deepl.com + - api-free.deepl.com + - www.recaptcha.net + - hcaptcha.com + - challenges.cloudflare.com + +# Proxy for SMTP/SMTPS +#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT +#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4 +#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5 + +# Media Proxy +# Reference Implementation: https://github.com/misskey-dev/media-proxy +# * Deliver a common cache between instances +# * Perform image compression (on a different server resource than the main process) +#mediaProxy: https://example.com/proxy + +# Proxy remote files (default: false) +# Proxy remote files by this instance or mediaProxy to prevent remote files from running in remote domains. +#proxyRemoteFiles: true + +# Movie Thumbnail Generation URL +# There is no reference implementation. +# For example, Misskey will point to the following URL: +# https://example.com/thumbnail.webp?thumbnail=1&url=https%3A%2F%2Fstorage.example.com%2Fpath%2Fto%2Fvideo.mp4 +#videoThumbnailGenerator: https://example.com + +# Sign to ActivityPub GET request (default: true) +signToActivityPubGet: true + +#allowedPrivateNetworks: [ +# '127.0.0.1/32' +#] + +# Upload or download file size limits (bytes) +#maxFileSize: 262144000 diff --git a/modules/services/misskey/default.nix b/modules/services/misskey/default.nix new file mode 100644 index 0000000..355e91f --- /dev/null +++ b/modules/services/misskey/default.nix @@ -0,0 +1,88 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + cfg = config.modules.services.misskey; + + inherit (lib.my) wrapFile; +in +{ + options.modules.services.misskey = { + enable = mkEnableOption "Misskey, an interplanetary microblogging platform [container]"; + domain = mkOption { type = types.str; }; + realHost = mkOption { type = types.str; }; + }; + + config = mkIf cfg.enable { + # TODO: refactor + + # Misskey sets uid/gid to 991 in container, user is created here to + # ensure that misskey files directory is accessible by the container user. + users = { + users.misskey = { + description = "Misskey user"; + group = "misskey"; + extraGroups = [ "podman" ]; + isSystemUser = true; + uid = 991; + }; + groups.misskey = { gid = 991; }; + }; + + virtualisation.podman.extraPackages = [ pkgs.zfs ]; + + # Packaging misskey is too much of a hassle, so we're using containers for now. + virtualisation.oci-containers.containers.misskey = { + volumes = [ + "/var/lib/misskey-files:/misskey/files" + # TODO: manage this with nix + "${wrapFile ".config" ./config}:/misskey/.config:ro" + ]; + image = "misskey/misskey:13.10.3"; + ports = [ "3000:3000" ]; + extraOptions = [ + "--network=host" + ]; + }; + + environment.persistence."/persist".directories = [ + "/var/lib/containers" + "/var/lib/misskey-files" + ]; + + systemd.tmpfiles.rules = [ + "d /var/lib/misskey-files 0755 misskey misskey -" + ]; + + services.postgresql.enable = true; + services.postgresql.ensureDatabases = [ "misskey" ]; + services.postgresql.ensureUsers = [ + { + name = "misskey"; + ensurePermissions."DATABASE misskey" = "ALL PRIVILEGES"; + } + ]; + + services.redis.servers.misskey = { + enable = true; + bind = "127.0.0.1"; + port = 16434; + }; + + services.nginx.virtualHosts.${cfg.realHost} = { + forceSSL = true; + useACMEHost = cfg.domain; + locations."/" = { + proxyPass = "http://127.0.0.1:3000"; + proxyWebsockets = true; + }; + + extraConfig = '' + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + ''; + }; + }; +} diff --git a/modules/services/nginx.nix b/modules/services/nginx.nix new file mode 100644 index 0000000..f9a5a31 --- /dev/null +++ b/modules/services/nginx.nix @@ -0,0 +1,37 @@ +{ config, lib, ... }: + +with lib; +let + cfg = config.modules.services.nginx; +in +{ + options.modules.services.nginx = { + enable = mkEnableOption "nginx proxy"; + }; + config = mkIf cfg.enable { + modules.services.acme.enable = true; + + services.nginx = { + enable = true; + # prevent 3~5s downtime on update + enableReload = true; + + recommendedGzipSettings = true; + recommendedOptimisation = true; + recommendedProxySettings = true; + recommendedTlsSettings = true; + + # catch-all for unknown hosts. + virtualHosts."_" = { + default = true; + rejectSSL = true; + + extraConfig = '' + return 444; + ''; + }; + }; + + users.extraUsers.nginx.extraGroups = [ "acme" ]; + }; +} diff --git a/modules/services/nixos-mailserver.nix b/modules/services/nixos-mailserver.nix new file mode 100644 index 0000000..be14c7f --- /dev/null +++ b/modules/services/nixos-mailserver.nix @@ -0,0 +1,106 @@ +{ inputs, config, lib, ... }: + +with lib; +let + cfg = config.modules.services.nixos-mailserver; +in +{ + imports = [ inputs.nixos-mailserver.nixosModules.mailserver ]; + + options.modules.services.nixos-mailserver = { + enable = mkEnableOption "nixos-mailserver"; + }; + + config = mkIf cfg.enable { + sops.secrets.sefidel-imap-pass = { + mode = "0440"; + owner = "dovecot2"; + group = "dovecot2"; + }; + sops.secrets.internal-imap-pass = { + mode = "0440"; + owner = "dovecot2"; + group = "dovecot2"; + }; + + systemd.services.dovecot2 = { + serviceConfig.SupplementaryGroups = [ "acme" ]; + }; + + services.postfix = { + dnsBlacklists = [ + # TODO: add sources + "bl.spamcop.net" + ]; + dnsBlacklistOverrides = '' + exotic.sh OK + sefidel.net OK + sefidel.com OK + 192.168.0.0/16 OK + ''; + }; + + mailserver = { + enable = true; + fqdn = "mail.exotic.sh"; + domains = [ "exotic.sh" "nand.moe" "sefidel.com" "sefidel.net" ]; + mailboxes = { + Trash = { + auto = "no"; + specialUse = "Trash"; + }; + Junk = { + auto = "subscribe"; + specialUse = "Junk"; + }; + Drafts = { + auto = "subscribe"; + specialUse = "Drafts"; + }; + Sent = { + auto = "subscribe"; + specialUse = "Sent"; + }; + }; + + loginAccounts = { + "contact@sefidel.com" = { + aliases = [ "sefidel" "admin" "admin@sefidel.com" "postmaster" "postmaster@sefidel.com" ]; + hashedPasswordFile = config.sops.secrets.sefidel-imap-pass.path; + }; + "contact@sefidel.net" = { + aliases = [ "sefidel" "dev@sefidel.net" "social@sefidel.net" "media@sefidel.net" "admin" "admin@sefidel.net" "postmaster" "postmaster@sefidel.net" ]; + hashedPasswordFile = config.sops.secrets.sefidel-imap-pass.path; + }; + "sef@exotic.sh" = { + aliases = [ "sef" "sefidel" "sefidel@exotic.sh" "admin" "admin@exotic.sh" "postmaster" "postmaster@exotic.sh" "admin@nand.moe" "postmaster@nand.moe" ]; + hashedPasswordFile = config.sops.secrets.sefidel-imap-pass.path; + }; + "system@exotic.sh" = { + aliases = [ "system@nand.moe" ]; + hashedPasswordFile = config.sops.secrets.internal-imap-pass.path; + }; + }; + localDnsResolver = false; + certificateScheme = 1; + certificateFile = "${config.security.acme.certs."exotic.sh".directory}/cert.pem"; + keyFile = "${config.security.acme.certs."exotic.sh".directory}/key.pem"; + enableImap = true; + enableImapSsl = true; + enableSubmission = true; + enableSubmissionSsl = true; + virusScanning = false; + }; + + environment.persistence."/persist".directories = [ + "/var/lib/dovecot" + "/var/lib/rspamd" + "/var/lib/redis-rspamd" + "/var/vmail" + "/var/dkim" + "/var/sieve" + ]; + + networking.firewall.allowedTCPPorts = [ 143 993 465 587 ]; + }; +} diff --git a/modules/services/postgresql.nix b/modules/services/postgresql.nix new file mode 100644 index 0000000..2d5fdf5 --- /dev/null +++ b/modules/services/postgresql.nix @@ -0,0 +1,34 @@ +{ config, lib, ... }: + +with lib; +let + cfg = config.modules.services.postgresql; +in +{ + options.modules.services.postgresql = { + enable = mkEnableOption "postgresql with laxed limits"; + }; + + config = mkIf cfg.enable { + services.postgresql = { + enable = true; + settings = { + max_connections = "300"; + shared_buffers = "80MB"; + }; + authentication = lib.mkForce '' + # Generated file; do not edit! + # TYPE DATABASE USER ADDRESS METHOD + local all all trust + host all all 127.0.0.1/32 trust + host all all ::1/128 trust + ''; + }; + services.postgresqlBackup.enable = true; + + environment.persistence."/persist".directories = [ + "/var/lib/postgresql" + "/var/backup/postgresql" + ]; + }; +} diff --git a/modules/services/pubnix.nix b/modules/services/pubnix.nix new file mode 100644 index 0000000..dfe3d58 --- /dev/null +++ b/modules/services/pubnix.nix @@ -0,0 +1,20 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + cfg = config.modules.services.pubnix; +in +{ + options.modules.services.pubnix = { + enable = mkEnableOption "serve pubnix shell"; + }; + + config = mkIf cfg.enable { + nix.gc.automatic = true; + nix.gc.dates = "daily"; + + environment.systemPackages = with pkgs; [ + bsd-finger + ]; + }; +} diff --git a/modules/services/sefidel-web.nix b/modules/services/sefidel-web.nix new file mode 100644 index 0000000..fdbcb00 --- /dev/null +++ b/modules/services/sefidel-web.nix @@ -0,0 +1,26 @@ +{ inputs, config, lib, ... }: + +with lib; +let + cfg = config.modules.services.sefidel-web; +in +{ + options.modules.services.sefidel-web = { + enable = mkEnableOption "sefidel-web"; + }; + + config = mkIf cfg.enable { + services.nginx.virtualHosts."sefidel.net" = { + useACMEHost = "sefidel.net"; + forceSSL = true; + # TODO: causes css to be fetched every single time. + # This is because heuristic caching is disabled, since Nix removes the last-modified timestamp. + root = inputs.sefidel-web.defaultPackage.${config.nixpkgs.system}; + + # Fixes the problem above. + locations."~ \\.(js|css|woff|woff2?|png|jpe?g|svg)$".extraConfig = '' + add_header Cache-Control "public, max-age=14400, must-revalidate"; + ''; + }; + }; +} diff --git a/modules/services/soju.nix b/modules/services/soju.nix new file mode 100644 index 0000000..4302538 --- /dev/null +++ b/modules/services/soju.nix @@ -0,0 +1,48 @@ +{ config, lib, ... }: + +with lib; +let + cfg = config.modules.services.soju; +in +{ + disabledModules = [ + "services/networking/soju.nix" + ]; + + imports = [ + ../../overlays/soju-module.nix + ]; + + options.modules.services.soju = { + enable = mkEnableOption "soju bouncer"; + + hostName = mkOption { type = types.str; default = config.networking.hostName; }; + port = mkOption { type = types.port; default = 6697; }; + tls = { + enable = mkEnableOption "enable TLS encryption"; + acmeHost = mkOption { type = types.str; }; + }; + }; + + config = mkIf cfg.enable { + services.soju = { + enable = true; + extraGroups = [ "acme" ]; + hostName = cfg.hostName; + listen = [ ":${toString cfg.port}" ]; + } // optionalAttrs cfg.tls.enable { + tlsCertificate = "${config.security.acme.certs.${cfg.tls.acmeHost}.directory}/cert.pem"; + tlsCertificateKey = "${config.security.acme.certs.${cfg.tls.acmeHost}.directory}/key.pem"; + }; + + systemd.services.soju = { + after = [ "acme-finished-${cfg.tls.acmeHost}.target" ]; + }; + + networking.firewall.allowedTCPPorts = [ cfg.port ]; + + environment.persistence."/persist".directories = [ + "/var/lib/private/soju" + ]; + }; +} diff --git a/modules/services/userweb.nix b/modules/services/userweb.nix new file mode 100644 index 0000000..1477f59 --- /dev/null +++ b/modules/services/userweb.nix @@ -0,0 +1,36 @@ +{ config, lib, ... }: + +with lib; +let + cfg = config.modules.services.userweb; +in +{ + options.modules.services.userweb = { + enable = mkEnableOption "serve user web contents"; + domain = mkOption { type = types.str; }; + }; + + config = mkIf cfg.enable { + modules.services.nginx.enable = true; + + services.nginx.virtualHosts.${cfg.domain} = { + forceSSL = true; + useACMEHost = cfg.domain; + + serverName = "${cfg.domain} www.${cfg.domain}"; + + locations."~ ^/(~u/)(?[\w-]+)(?/.*)?$" = { + alias = "/home/$user/public_html$user_uri"; + index = "index.html index.php index.cgi index.py index.sh index.pl index.lua"; + + extraConfig = '' + error_page 404 /~$user/404.html; + ''; + }; + + extraConfig = '' + error_log /var/log/nginx/${cfg.domain}-error.log crit; + ''; + }; + }; +} diff --git a/modules/services/vikunja.nix b/modules/services/vikunja.nix new file mode 100644 index 0000000..eb5adbc --- /dev/null +++ b/modules/services/vikunja.nix @@ -0,0 +1,50 @@ +{ config, lib, ... }: + +with lib; +let + cfg = config.modules.services.vikunja; +in +{ + options.modules.services.vikunja = { + enable = mkEnableOption "vikunja"; + domain = mkOption { type = types.str; }; + realHost = mkOption { type = types.str; }; + }; + + config = mkIf cfg.enable { + services.vikunja = { + enable = true; + frontendHostname = cfg.realHost; + frontendScheme = "https"; + + settings = { + service.enableregistration = false; + }; + + database = { + type = "postgres"; + user = "vikunja"; + database = "vikunja"; + host = "/run/postgresql"; + }; + }; + + services.postgresql.enable = true; + services.postgresql.ensureDatabases = [ "vikunja" ]; + services.postgresql.ensureUsers = [ + { + name = "vikunja"; + ensurePermissions."DATABASE vikunja" = "ALL PRIVILEGES"; + } + ]; + + environment.persistence."/persist".directories = [ + "/var/lib/private/vikunja" + ]; + + services.nginx.virtualHosts.${cfg.realHost} = { + forceSSL = true; + useACMEHost = cfg.domain; + }; + }; +} diff --git a/modules/sops.nix b/modules/sops.nix new file mode 100644 index 0000000..bd5156e --- /dev/null +++ b/modules/sops.nix @@ -0,0 +1,21 @@ +{ config, lib, inputs, ... }: + +with lib; +let + cfg = config.modules.sops; + + secretsFile = ../systems/${config.networking.hostName}/secrets/secrets.yaml; +in +{ + imports = [ + inputs.sops-nix.nixosModules.sops + ]; + + options.modules.sops = { + enable = mkEnableOption "sops secret manager"; + }; + + config = mkIf cfg.enable { + sops.defaultSopsFile = secretsFile; + }; +} diff --git a/overlays/README.md b/overlays/README.md new file mode 100644 index 0000000..4d257f8 --- /dev/null +++ b/overlays/README.md @@ -0,0 +1,4 @@ +infra->overlays +=============== + +Nixpkgs overlays or module replacements. diff --git a/overlays/default.nix b/overlays/default.nix new file mode 100644 index 0000000..353d5bd --- /dev/null +++ b/overlays/default.nix @@ -0,0 +1,4 @@ +self: super: { + mjolnir = super.callPackage ./mjolnir-package { }; + sliding-sync = super.callPackage ./sliding-sync.nix { }; +} diff --git a/overlays/git-daemon-module.nix b/overlays/git-daemon-module.nix new file mode 100644 index 0000000..76b395e --- /dev/null +++ b/overlays/git-daemon-module.nix @@ -0,0 +1,137 @@ +{ config, lib, pkgs, ... }: +with lib; +let + + cfg = config.services.gitDaemon; + +in +{ + + ###### interface + + options = { + services.gitDaemon = { + + enable = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Enable Git daemon, which allows public hosting of git repositories + without any access controls. This is mostly intended for read-only access. + + You can allow write access by setting daemon.receivepack configuration + item of the repository to true. This is solely meant for a closed LAN setting + where everybody is friendly. + + If you need any access controls, use something else. + ''; + }; + + basePath = mkOption { + type = types.str; + default = ""; + example = "/srv/git/"; + description = lib.mdDoc '' + Remap all the path requests as relative to the given path. For example, + if you set base-path to /srv/git, then if you later try to pull + git://example.com/hello.git, Git daemon will interpret the path as /srv/git/hello.git. + ''; + }; + + exportAll = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Publish all directories that look like Git repositories (have the objects + and refs subdirectories), even if they do not have the git-daemon-export-ok file. + + If disabled, you need to touch .git/git-daemon-export-ok in each repository + you want the daemon to publish. + + Warning: enabling this without a repository whitelist or basePath + publishes every git repository you have. + ''; + }; + + repositories = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "/srv/git" "/home/user/git/repo2" ]; + description = lib.mdDoc '' + A whitelist of paths of git repositories, or directories containing repositories + all of which would be published. Paths must not end in "/". + + Warning: leaving this empty and enabling exportAll publishes all + repositories in your filesystem or basePath if specified. + ''; + }; + + listenAddress = mkOption { + type = types.str; + default = ""; + example = "example.com"; + description = lib.mdDoc "Listen on a specific IP address or hostname."; + }; + + port = mkOption { + type = types.port; + default = 9418; + description = lib.mdDoc "Port to listen on."; + }; + + options = mkOption { + type = types.str; + default = ""; + description = lib.mdDoc "Extra configuration options to be passed to Git daemon."; + }; + + user = mkOption { + type = types.str; + default = "git"; + description = lib.mdDoc "User under which Git daemon would be running."; + }; + + group = mkOption { + type = types.str; + default = "git"; + description = lib.mdDoc "Group under which Git daemon would be running."; + }; + + createUserAndGroup = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc '' + Create the specified group and user. + Disable this option if you want to use the existing user + ''; + }; + }; + }; + + ###### implementation + + config = mkIf cfg.enable { + + users.users.${cfg.user} = optionalAttrs (cfg.createUserAndGroup == true) { + uid = config.ids.uids.git; + group = cfg.group; + description = "Git daemon user"; + }; + + users.groups.${cfg.group} = optionalAttrs (cfg.createUserAndGroup == true) { + gid = config.ids.gids.git; + }; + + systemd.services.git-daemon = { + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + script = "${pkgs.git}/bin/git daemon --reuseaddr " + + (optionalString (cfg.basePath != "") "--base-path=${cfg.basePath} ") + + (optionalString (cfg.listenAddress != "") "--listen=${cfg.listenAddress} ") + + "--port=${toString cfg.port} --user=${cfg.user} --group=${cfg.group} ${cfg.options} " + + "--verbose " + (optionalString cfg.exportAll "--export-all ") + concatStringsSep " " cfg.repositories; + }; + + }; + +} diff --git a/overlays/mautrix-signal-module.nix b/overlays/mautrix-signal-module.nix new file mode 100644 index 0000000..22abbb5 --- /dev/null +++ b/overlays/mautrix-signal-module.nix @@ -0,0 +1,196 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + dataDir = "/var/lib/mautrix-signal"; + registrationFile = "${dataDir}/signal-registration.yaml"; + cfg = config.services.mautrix-signal; + settingsFormat = pkgs.formats.json { }; + settingsFile = + settingsFormat.generate "mautrix-signal-config.json" cfg.settings; + +in +{ + options = { + services.mautrix-signal = { + enable = mkEnableOption (lib.mdDoc "Mautrix-Signal, a Matrix-Signal puppeting bridge."); + + settings = mkOption rec { + apply = recursiveUpdate default; + inherit (settingsFormat) type; + default = { + homeserver = { + software = "standard"; + }; + + appservice = rec { + database = "sqlite:///${dataDir}/mautrix-signal.db"; + database_opts = { }; + hostname = "0.0.0.0"; + port = 8080; + address = "http://localhost:${toString port}"; + }; + + signal.socket_path = config.services.signald.socketPath; + + bridge = { + permissions."*" = "relay"; + relay.whitelist = [ ]; + double_puppet_server_map = { }; + login_shared_secret_map = { }; + }; + + logging = { + version = 1; + + formatters.precise.format = "[%(levelname)s@%(name)s] %(message)s"; + + handlers.console = { + class = "logging.StreamHandler"; + formatter = "precise"; + }; + + loggers = { + mau.level = "INFO"; + + # prevent tokens from leaking in the logs: + # https://github.com/tulir/mautrix-telegram/issues/351 + aiohttp.level = "WARNING"; + }; + + # log to console/systemd instead of file + root = { + level = "INFO"; + handlers = [ "console" ]; + }; + }; + }; + example = literalExpression '' + { + homeserver = { + address = "http://localhost:8008"; + domain = "public-domain.tld"; + }; + + appservice.public = { + prefix = "/public"; + external = "https://public-appservice-address/public"; + }; + + bridge.permissions = { + "example.com" = "full"; + "@admin:example.com" = "admin"; + }; + } + ''; + description = lib.mdDoc '' + {file}`config.yaml` configuration as a Nix attribute set. + Configuration options should match those described in + [example-config.yaml](https://github.com/mautrix/signal/blob/master/signal/example-config.yaml). + + Secret tokens should be specified using {option}`environmentFile` + instead of this world-readable attribute set. + ''; + }; + + environmentFile = mkOption { + type = types.nullOr types.path; + default = null; + description = lib.mdDoc '' + File containing environment variables to be passed to the mautrix-signal service, + in which secret tokens can be specified securely by defining values for e.g. + `MAUTRIX_SIGNAL_APPSERVICE_AS_TOKEN`, + `MAUTRIX_SIGNAL_APPSERVICE_HS_TOKEN`, + + These environment variables can also be used to set other options by + replacing hierarchy levels by `.`, converting the name to uppercase + and prepending `MAUTRIX_SIGNAL_`. + For example, the first value above maps to + {option}`settings.appservice.as_token`. + + The environment variable values can be prefixed with `json::` to have + them be parsed as JSON. For example, `login_shared_secret_map` can be + set as follows: + `MAUTRIX_SIGNAL_BRIDGE_LOGIN_SHARED_SECRET_MAP=json::{"example.com":"secret"}`. + ''; + }; + + serviceDependencies = mkOption { + type = with types; listOf str; + default = optional config.services.matrix-synapse.enable "matrix-synapse.service"; + defaultText = literalExpression '' + optional config.services.matrix-synapse.enable "matrix-synapse.service" + ''; + description = lib.mdDoc '' + List of Systemd services to require and wait for when starting the application service. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + services.signald.enable = true; + + systemd.services.mautrix-signal = { + description = "Mautrix-Signal, a Matrix-Signal puppeting bridge."; + + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ] ++ cfg.serviceDependencies; + after = [ "network-online.target" ] ++ cfg.serviceDependencies; + path = [ pkgs.lottieconverter pkgs.ffmpeg-full ]; + + # mautrix-signal tries to generate a dotfile in the home directory of + # the running user if using a postgresql database: + # + # File "python3.10/site-packages/asyncpg/connect_utils.py", line 257, in _dot_postgre> + # return (pathlib.Path.home() / '.postgresql' / filename).resolve() + # File "python3.10/pathlib.py", line 1000, in home + # return cls("~").expanduser() + # File "python3.10/pathlib.py", line 1440, in expanduser + # raise RuntimeError("Could not determine home directory.") + # RuntimeError: Could not determine home directory. + environment.HOME = dataDir; + + preStart = '' + # generate the appservice's registration file if absent + if [ ! -f '${registrationFile}' ]; then + ${pkgs.mautrix-signal}/bin/mautrix-signal \ + --generate-registration \ + --base-config='${pkgs.mautrix-signal}/${pkgs.mautrix-signal.pythonModule.sitePackages}/mautrix_signal/example-config.yaml' \ + --config='${settingsFile}' \ + --registration='${registrationFile}' + fi + '' + lib.optionalString (pkgs.mautrix-signal ? alembic) '' + # run automatic database init and migration scripts + ${pkgs.mautrix-signal.alembic}/bin/alembic -x config='${settingsFile}' upgrade head + ''; + + serviceConfig = { + Type = "simple"; + Restart = "always"; + + ProtectSystem = "strict"; + ProtectHome = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + + DynamicUser = true; + SupplementaryGroups = [ "signald" ]; + PrivateTmp = true; + WorkingDirectory = pkgs.mautrix-signal; # necessary for the database migration scripts to be found + StateDirectory = baseNameOf dataDir; + UMask = "0027"; + EnvironmentFile = cfg.environmentFile; + + ExecStart = '' + ${pkgs.mautrix-signal}/bin/mautrix-signal \ + --config='${settingsFile}' + ''; + }; + }; + }; + + # meta.maintainers = with maintainers; [ boppyt ]; +} diff --git a/overlays/mautrix-whatsapp-module.nix b/overlays/mautrix-whatsapp-module.nix new file mode 100644 index 0000000..4cebcb6 --- /dev/null +++ b/overlays/mautrix-whatsapp-module.nix @@ -0,0 +1,192 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + dataDir = "/var/lib/mautrix-whatsapp"; + registrationFile = "${dataDir}/whatsapp-registration.yaml"; + cfg = config.services.mautrix-whatsapp; + settingsFormat = pkgs.formats.json { }; + settingsFile = + settingsFormat.generate "mautrix-whatsapp-config.json" cfg.settings; +in +{ + options = { + services.mautrix-whatsapp = { + enable = mkEnableOption (lib.mdDoc "Mautrix-Whatsapp, a Matrix-Whatsapp puppeting bridge."); + + settings = mkOption rec { + apply = recursiveUpdate default; + inherit (settingsFormat) type; + default = { + homeserver = { + software = "standard"; + }; + + appservice = rec { + database = { + type = "sqlite"; + uri = "sqlite:///${dataDir}/mautrix-whatsapp.db"; + }; + hostname = "0.0.0.0"; + port = 8080; + address = "http://localhost:${toString port}"; + as_token = "$MAUTRIX_WHATSAPP_APPSERVICE_AS_TOKEN"; + hs_token = "$MAUTRIX_WHATSAPP_APPSERVICE_HS_TOKEN"; + }; + + bridge = { + permissions."*" = "relay"; + relay.whitelist = [ ]; + double_puppet_server_map = { }; + login_shared_secret_map = { }; + }; + + logging = { + version = 1; + + formatters.precise.format = "[%(levelname)s@%(name)s] %(message)s"; + + handlers.console = { + class = "logging.StreamHandler"; + formatter = "precise"; + }; + + # log to console/systemd instead of file + file_name_format = null; + + loggers = { + mau.level = "INFO"; + telethon.level = "INFO"; + + # prevent tokens from leaking in the logs: + # https://github.com/tulir/mautrix-telegram/issues/351 + aiohttp.level = "WARNING"; + }; + }; + }; + example = literalExpression '' + { + homeserver = { + address = "http://localhost:8008"; + domain = "public-domain.tld"; + }; + + appservice.public = { + prefix = "/public"; + external = "https://public-appservice-address/public"; + }; + + bridge.permissions = { + "example.com" = "full"; + "@admin:example.com" = "admin"; + }; + } + ''; + description = lib.mdDoc '' + {file}`config.yaml` configuration as a Nix attribute set. + Configuration options should match those described in + [example-config.yaml](https://github.com/mautrix/whatsapp/blob/master/mautrix_whatsapp/example-config.yaml). + + Secret tokens should be specified using {option}`environmentFile` + instead of this world-readable attribute set. + ''; + }; + + environmentFile = mkOption { + type = types.nullOr types.path; + default = null; + description = lib.mdDoc '' + File containing environment variables to be passed to the mautrix-whatsapp service, + in which secret tokens can be specified securely by defining values for e.g. + `MAUTRIX_WHATSAPP_APPSERVICE_AS_TOKEN`, + `MAUTRIX_WHATSAPP_APPSERVICE_HS_TOKEN`, + + For Mautrix-Whatsapp, only AS_TOKEN and HS_TOKEN is available. + ''; + }; + + serviceDependencies = mkOption { + type = with types; listOf str; + default = optional config.services.matrix-synapse.enable "matrix-synapse.service"; + defaultText = literalExpression '' + optional config.services.matrix-synapse.enable "matrix-synapse.service" + ''; + description = lib.mdDoc '' + List of Systemd services to require and wait for when starting the application service. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.services.mautrix-whatsapp = { + description = "Mautrix-Whatsapp, a Matrix-Whatsapp puppeting bridge."; + + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ] ++ cfg.serviceDependencies; + after = [ "network-online.target" ] ++ cfg.serviceDependencies; + path = [ pkgs.lottieconverter pkgs.ffmpeg-full ]; + + # mautrix-whatsapp tries to generate a dotfile in the home directory of + # the running user if using a postgresql database: + # + # File "python3.10/site-packages/asyncpg/connect_utils.py", line 257, in _dot_postgre> + # return (pathlib.Path.home() / '.postgresql' / filename).resolve() + # File "python3.10/pathlib.py", line 1000, in home + # return cls("~").expanduser() + # File "python3.10/pathlib.py", line 1440, in expanduser + # raise RuntimeError("Could not determine home directory.") + # RuntimeError: Could not determine home directory. + environment.HOME = dataDir; + + preStart = '' + # generate the appservice's registration file if absent + if [ ! -f '${registrationFile}' ]; then + ${pkgs.mautrix-whatsapp}/bin/mautrix-whatsapp \ + --generate-registration \ + --config='${settingsFile}' \ + --registration='${registrationFile}' + fi + + ${pkgs.envsubst}/bin/envsubst \ + -i ${settingsFile} \ + -o /run/mautrix-whatsapp/config.json + + # wait until dendrite grabs the config + sleep 5 + '' + lib.optionalString (pkgs.mautrix-whatsapp ? alembic) '' + # run automatic database init and migration scripts + ${pkgs.mautrix-whatsapp.alembic}/bin/alembic -x config='${settingsFile}' upgrade head + ''; + + serviceConfig = { + Type = "simple"; + Restart = "always"; + + ProtectSystem = "strict"; + ProtectHome = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + + DynamicUser = true; + Group = "mautrix-whatsapp"; + PrivateTmp = true; + WorkingDirectory = pkgs.mautrix-whatsapp; # necessary for the database migration scripts to be found + StateDirectory = baseNameOf dataDir; + RuntimeDirectory = "mautrix-whatsapp"; + RuntimeDirectoryMode = "0700"; + UMask = "0027"; + EnvironmentFile = cfg.environmentFile; + + ExecStart = '' + ${pkgs.mautrix-whatsapp}/bin/mautrix-whatsapp \ + --config='/run/mautrix-whatsapp/config.json' + ''; + }; + }; + }; + + # meta.maintainers = with maintainers; [ boppyt ]; +} diff --git a/overlays/mjolnir-module/default.nix b/overlays/mjolnir-module/default.nix new file mode 100644 index 0000000..87ed761 --- /dev/null +++ b/overlays/mjolnir-module/default.nix @@ -0,0 +1,242 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + cfg = config.services.mjolnir; + + yamlConfig = { + inherit (cfg) dataPath managementRoom protectedRooms; + + accessToken = "@ACCESS_TOKEN@"; # will be replaced in "generateConfig" + homeserverUrl = + if cfg.pantalaimon.enable then + "http://${cfg.pantalaimon.options.listenAddress}:${toString cfg.pantalaimon.options.listenPort}" + else + cfg.homeserverUrl; + + rawHomeserverUrl = cfg.homeserverUrl; + + pantalaimon = { + inherit (cfg.pantalaimon) username; + + use = cfg.pantalaimon.enable; + password = "@PANTALAIMON_PASSWORD@"; # will be replaced in "generateConfig" + }; + }; + + moduleConfigFile = pkgs.writeText "module-config.yaml" ( + generators.toYAML { } (filterAttrs (_: v: v != null) + (fold recursiveUpdate { } [ yamlConfig cfg.settings ]))); + + # these config files will be merged one after the other to build the final config + configFiles = [ + "${pkgs.mjolnir}/libexec/mjolnir/deps/mjolnir/config/default.yaml" + moduleConfigFile + ]; + + # this will generate the default.yaml file with all configFiles as inputs and + # replace all secret strings using replace-secret + generateConfig = pkgs.writeShellScript "mjolnir-generate-config" ( + let + yqEvalStr = concatImapStringsSep " * " (pos: _: "select(fileIndex == ${toString (pos - 1)})") configFiles; + yqEvalArgs = concatStringsSep " " configFiles; + in + '' + set -euo pipefail + + umask 077 + + # mjolnir will try to load a config from "./config/default.yaml" in the working directory + # -> let's place the generated config there + mkdir -p ${cfg.dataPath}/config + + # merge all config files into one, overriding settings of the previous one with the next config + # e.g. "eval-all 'select(fileIndex == 0) * select(fileIndex == 1)' filea.yaml fileb.yaml" will merge filea.yaml with fileb.yaml + ${pkgs.yq-go}/bin/yq eval-all -P '${yqEvalStr}' ${yqEvalArgs} > ${cfg.dataPath}/config/default.yaml + + ${optionalString (cfg.accessTokenFile != null) '' + ${pkgs.replace-secret}/bin/replace-secret '@ACCESS_TOKEN@' '${cfg.accessTokenFile}' ${cfg.dataPath}/config/default.yaml + ''} + ${optionalString (cfg.pantalaimon.passwordFile != null) '' + ${pkgs.replace-secret}/bin/replace-secret '@PANTALAIMON_PASSWORD@' '${cfg.pantalaimon.passwordFile}' ${cfg.dataPath}/config/default.yaml + ''} + '' + ); +in +{ + options.services.mjolnir = { + enable = mkEnableOption (lib.mdDoc "Mjolnir, a moderation tool for Matrix"); + + homeserverUrl = mkOption { + type = types.str; + default = "https://matrix.org"; + description = lib.mdDoc '' + Where the homeserver is located (client-server URL). + + If `pantalaimon.enable` is `true`, this option will become the homeserver to which `pantalaimon` connects. + The listen address of `pantalaimon` will then become the `homeserverUrl` of `mjolnir`. + ''; + }; + + accessTokenFile = mkOption { + type = with types; nullOr path; + default = null; + description = lib.mdDoc '' + File containing the matrix access token for the `mjolnir` user. + ''; + }; + + pantalaimon = mkOption { + description = lib.mdDoc '' + `pantalaimon` options (enables E2E Encryption support). + + This will create a `pantalaimon` instance with the name "mjolnir". + ''; + default = { }; + type = types.submodule { + options = { + enable = mkEnableOption (lib.mdDoc '' + If true, accessToken is ignored and the username/password below will be + used instead. The access token of the bot will be stored in the dataPath. + ''); + + username = mkOption { + type = types.str; + description = lib.mdDoc "The username to login with."; + }; + + passwordFile = mkOption { + type = with types; nullOr path; + default = null; + description = lib.mdDoc '' + File containing the matrix password for the `mjolnir` user. + ''; + }; + + options = mkOption { + type = types.submodule (import ./pantalaimon-options.nix); + default = { }; + description = lib.mdDoc '' + passthrough additional options to the `pantalaimon` service. + ''; + }; + }; + }; + }; + + dataPath = mkOption { + type = types.path; + default = "/var/lib/mjolnir"; + description = lib.mdDoc '' + The directory the bot should store various bits of information in. + ''; + }; + + managementRoom = mkOption { + type = types.str; + default = "#moderators:example.org"; + description = lib.mdDoc '' + The room ID where people can use the bot. The bot has no access controls, so + anyone in this room can use the bot - secure your room! + This should be a room alias or room ID - not a matrix.to URL. + Note: `mjolnir` is fairly verbose - expect a lot of messages from it. + ''; + }; + + protectedRooms = mkOption { + type = types.listOf types.str; + default = [ ]; + example = literalExpression '' + [ + "https://matrix.to/#/#yourroom:example.org" + "https://matrix.to/#/#anotherroom:example.org" + ] + ''; + description = lib.mdDoc '' + A list of rooms to protect (matrix.to URLs). + ''; + }; + + settings = mkOption { + default = { }; + type = (pkgs.formats.yaml { }).type; + example = literalExpression '' + { + autojoinOnlyIfManager = true; + automaticallyRedactForReasons = [ "spam" "advertising" ]; + } + ''; + description = lib.mdDoc '' + Additional settings (see [mjolnir default config](https://github.com/matrix-org/mjolnir/blob/main/config/default.yaml) for available settings). These settings will override settings made by the module config. + ''; + }; + }; + + config = mkIf config.services.mjolnir.enable { + assertions = [ + { + assertion = !(cfg.pantalaimon.enable && cfg.pantalaimon.passwordFile == null); + message = "Specify pantalaimon.passwordFile"; + } + { + assertion = !(cfg.pantalaimon.enable && cfg.accessTokenFile != null); + message = "Do not specify accessTokenFile when using pantalaimon"; + } + { + assertion = !(!cfg.pantalaimon.enable && cfg.accessTokenFile == null); + message = "Specify accessTokenFile when not using pantalaimon"; + } + ]; + + services.pantalaimon-headless.instances."mjolnir" = mkIf cfg.pantalaimon.enable + { + homeserver = cfg.homeserverUrl; + } // cfg.pantalaimon.options; + + systemd.services.mjolnir = { + description = "mjolnir - a moderation tool for Matrix"; + wants = [ "network-online.target" ] ++ optionals (cfg.pantalaimon.enable) [ "pantalaimon-mjolnir.service" ]; + after = [ "network-online.target" ] ++ optionals (cfg.pantalaimon.enable) [ "pantalaimon-mjolnir.service" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + ExecStart = ''${pkgs.mjolnir}/bin/mjolnir''; + ExecStartPre = [ generateConfig ]; + WorkingDirectory = cfg.dataPath; + StateDirectory = "mjolnir"; + StateDirectoryMode = "0700"; + ProtectSystem = "strict"; + ProtectHome = true; + PrivateTmp = true; + NoNewPrivileges = true; + PrivateDevices = true; + User = "mjolnir"; + Restart = "on-failure"; + + /* TODO: wait for #102397 to be resolved. Then load secrets from $CREDENTIALS_DIRECTORY+"/NAME" + DynamicUser = true; + LoadCredential = [] ++ + optionals (cfg.accessTokenFile != null) [ + "access_token:${cfg.accessTokenFile}" + ] ++ + optionals (cfg.pantalaimon.passwordFile != null) [ + "pantalaimon_password:${cfg.pantalaimon.passwordFile}" + ]; + */ + }; + }; + + users = { + users.mjolnir = { + group = "mjolnir"; + isSystemUser = true; + }; + groups.mjolnir = { }; + }; + }; + + meta = { + doc = ./mjolnir.md; + maintainers = with maintainers; [ jojosch ]; + }; +} diff --git a/overlays/mjolnir-module/mjolnir.md b/overlays/mjolnir-module/mjolnir.md new file mode 100644 index 0000000..f6994ee --- /dev/null +++ b/overlays/mjolnir-module/mjolnir.md @@ -0,0 +1,110 @@ +# Mjolnir (Matrix Moderation Tool) {#module-services-mjolnir} + +This chapter will show you how to set up your own, self-hosted +[Mjolnir](https://github.com/matrix-org/mjolnir) instance. + +As an all-in-one moderation tool, it can protect your server from +malicious invites, spam messages, and whatever else you don't want. +In addition to server-level protection, Mjolnir is great for communities +wanting to protect their rooms without having to use their personal +accounts for moderation. + +The bot by default includes support for bans, redactions, anti-spam, +server ACLs, room directory changes, room alias transfers, account +deactivation, room shutdown, and more. + +See the [README](https://github.com/matrix-org/mjolnir#readme) +page and the [Moderator's guide](https://github.com/matrix-org/mjolnir/blob/main/docs/moderators.md) +for additional instructions on how to setup and use Mjolnir. + +For [additional settings](#opt-services.mjolnir.settings) +see [the default configuration](https://github.com/matrix-org/mjolnir/blob/main/config/default.yaml). + +## Mjolnir Setup {#module-services-mjolnir-setup} + +First create a new Room which will be used as a management room for Mjolnir. In +this room, Mjolnir will log possible errors and debugging information. You'll +need to set this Room-ID in [services.mjolnir.managementRoom](#opt-services.mjolnir.managementRoom). + +Next, create a new user for Mjolnir on your homeserver, if not present already. + +The Mjolnir Matrix user expects to be free of any rate limiting. +See [Synapse #6286](https://github.com/matrix-org/synapse/issues/6286) +for an example on how to achieve this. + +If you want Mjolnir to be able to deactivate users, move room aliases, shutdown rooms, etc. +you'll need to make the Mjolnir user a Matrix server admin. + +Now invite the Mjolnir user to the management room. + +It is recommended to use [Pantalaimon](https://github.com/matrix-org/pantalaimon), +so your management room can be encrypted. This also applies if you are looking to moderate an encrypted room. + +To enable the Pantalaimon E2E Proxy for mjolnir, enable +[services.mjolnir.pantalaimon](#opt-services.mjolnir.pantalaimon.enable). This will +autoconfigure a new Pantalaimon instance, which will connect to the homeserver +set in [services.mjolnir.homeserverUrl](#opt-services.mjolnir.homeserverUrl) and Mjolnir itself +will be configured to connect to the new Pantalaimon instance. + +``` +{ + services.mjolnir = { + enable = true; + homeserverUrl = "https://matrix.domain.tld"; + pantalaimon = { + enable = true; + username = "mjolnir"; + passwordFile = "/run/secrets/mjolnir-password"; + }; + protectedRooms = [ + "https://matrix.to/#/!xxx:domain.tld" + ]; + managementRoom = "!yyy:domain.tld"; + }; +} +``` + +### Element Matrix Services (EMS) {#module-services-mjolnir-setup-ems} + +If you are using a managed ["Element Matrix Services (EMS)"](https://ems.element.io/) +server, you will need to consent to the terms and conditions. Upon startup, an error +log entry with a URL to the consent page will be generated. + +## Synapse Antispam Module {#module-services-mjolnir-matrix-synapse-antispam} + +A Synapse module is also available to apply the same rulesets the bot +uses across an entire homeserver. + +To use the Antispam Module, add `matrix-synapse-plugins.matrix-synapse-mjolnir-antispam` +to the Synapse plugin list and enable the `mjolnir.Module` module. + +``` +{ + services.matrix-synapse = { + plugins = with pkgs; [ + matrix-synapse-plugins.matrix-synapse-mjolnir-antispam + ]; + extraConfig = '' + modules: + - module: mjolnir.Module + config: + # Prevent servers/users in the ban lists from inviting users on this + # server to rooms. Default true. + block_invites: true + # Flag messages sent by servers/users in the ban lists as spam. Currently + # this means that spammy messages will appear as empty to users. Default + # false. + block_messages: false + # Remove users from the user directory search by filtering matrix IDs and + # display names by the entries in the user ban list. Default false. + block_usernames: false + # The room IDs of the ban lists to honour. Unlike other parts of Mjolnir, + # this list cannot be room aliases or permalinks. This server is expected + # to already be joined to the room - Mjolnir will not automatically join + # these rooms. + ban_lists: + - "!roomid:example.org" + ''; + }; +} +``` diff --git a/overlays/mjolnir-module/pantalaimon-options.nix b/overlays/mjolnir-module/pantalaimon-options.nix new file mode 100644 index 0000000..3945a70 --- /dev/null +++ b/overlays/mjolnir-module/pantalaimon-options.nix @@ -0,0 +1,70 @@ +{ config, lib, name, ... }: + +with lib; +{ + options = { + dataPath = mkOption { + type = types.path; + default = "/var/lib/pantalaimon-${name}"; + description = lib.mdDoc '' + The directory where `pantalaimon` should store its state such as the database file. + ''; + }; + + logLevel = mkOption { + type = types.enum [ "info" "warning" "error" "debug" ]; + default = "warning"; + description = lib.mdDoc '' + Set the log level of the daemon. + ''; + }; + + homeserver = mkOption { + type = types.str; + example = "https://matrix.org"; + description = lib.mdDoc '' + The URI of the homeserver that the `pantalaimon` proxy should + forward requests to, without the matrix API path but including + the http(s) schema. + ''; + }; + + ssl = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc '' + Whether or not SSL verification should be enabled for outgoing + connections to the homeserver. + ''; + }; + + listenAddress = mkOption { + type = types.str; + default = "localhost"; + description = lib.mdDoc '' + The address where the daemon will listen to client connections + for this homeserver. + ''; + }; + + listenPort = mkOption { + type = types.port; + default = 8009; + description = lib.mdDoc '' + The port where the daemon will listen to client connections for + this homeserver. Note that the listen address/port combination + needs to be unique between different homeservers. + ''; + }; + + extraSettings = mkOption { + type = types.attrs; + default = { }; + description = lib.mdDoc '' + Extra configuration options. See + [pantalaimon(5)](https://github.com/matrix-org/pantalaimon/blob/master/docs/man/pantalaimon.5.md) + for available options. + ''; + }; + }; +} diff --git a/overlays/mjolnir-package/default.nix b/overlays/mjolnir-package/default.nix new file mode 100644 index 0000000..833124b --- /dev/null +++ b/overlays/mjolnir-package/default.nix @@ -0,0 +1,80 @@ +{ lib +, nixosTests +, mkYarnPackage +, fetchYarnDeps +, fetchFromGitHub +, makeWrapper +, nodejs +, pkgs +, matrix-sdk-crypto-nodejs +}: + +let + pin = lib.importJSON ./pin.json; +in +mkYarnPackage rec { + pname = "mjolnir"; + inherit (pin) version; + + src = fetchFromGitHub { + owner = "matrix-org"; + repo = "mjolnir"; + rev = "v${version}"; + sha256 = pin.srcSha256; + }; + + packageJSON = ./package.json; + offlineCache = fetchYarnDeps { + yarnLock = "${src}/yarn.lock"; + sha256 = pin.yarnSha256; + }; + + packageResolutions = { + "@matrix-org/matrix-sdk-crypto-nodejs" = "${matrix-sdk-crypto-nodejs}/lib/node_modules/@matrix-org/matrix-sdk-crypto-nodejs"; + }; + + nativeBuildInputs = [ + makeWrapper + ]; + + buildPhase = '' + runHook preBuild + yarn --offline build + runHook postBuild + ''; + + postInstall = '' + makeWrapper ${nodejs}/bin/node $out/bin/mjolnir \ + --add-flags $out/libexec/mjolnir/deps/mjolnir/lib/index.js + ''; + + doDist = false; + + passthru = { + tests = { + inherit (nixosTests) mjolnir; + }; + updateScript = ./update.sh; + }; + + meta = with lib; { + description = "A moderation tool for Matrix"; + homepage = "https://github.com/matrix-org/mjolnir"; + longDescription = '' + As an all-in-one moderation tool, it can protect your server from + malicious invites, spam messages, and whatever else you don't want. + In addition to server-level protection, Mjolnir is great for communities + wanting to protect their rooms without having to use their personal + accounts for moderation. + + The bot by default includes support for bans, redactions, anti-spam, + server ACLs, room directory changes, room alias transfers, account + deactivation, room shutdown, and more. + + A Synapse module is also available to apply the same rulesets the bot + uses across an entire homeserver. + ''; + license = licenses.asl20; + maintainers = with maintainers; [ jojosch ]; + }; +} diff --git a/overlays/mjolnir-package/package.json b/overlays/mjolnir-package/package.json new file mode 100644 index 0000000..f7ed5ab --- /dev/null +++ b/overlays/mjolnir-package/package.json @@ -0,0 +1,69 @@ +{ + "name": "mjolnir", + "version": "1.6.3", + "description": "A moderation tool for Matrix", + "main": "lib/index.js", + "repository": "git@github.com:matrix-org/mjolnir.git", + "author": "The Matrix.org Foundation C.I.C.", + "license": "Apache-2.0", + "private": true, + "scripts": { + "build": "tsc", + "postbuild": "rm -rf lib/test/ && cp -r lib/src/* lib/ && rm -rf lib/src/", + "lint": "tslint --project ./tsconfig.json -t stylish", + "start:dev": "yarn build && node --async-stack-traces lib/index.js", + "test": "ts-mocha --project ./tsconfig.json test/commands/**/*.ts", + "test:integration": "NODE_ENV=harness ts-mocha --async-stack-traces --require test/integration/fixtures.ts --timeout 300000 --project ./tsconfig.json \"test/integration/**/*Test.ts\"", + "test:integration:single": "NODE_ENV=harness npx ts-mocha --require test/integration/fixtures.ts --timeout 300000 --project ./tsconfig.json", + "test:appservice:integration": "NODE_ENV=harness ts-mocha --async-stack-traces --timeout 300000 --project ./tsconfig.json \"test/appservice/integration/**/*Test.ts\"", + "test:appservice:integration:single": "NODE_ENV=harness npx ts-mocha --timeout 300000 --project ./tsconfig.json", + "test:manual": "NODE_ENV=harness ts-node test/integration/manualLaunchScript.ts", + "version": "sed -i '/# version automated/s/[0-9][0-9]*\\.[0-9][0-9]*\\.[0-9][0-9]*/'$npm_package_version'/' synapse_antispam/setup.py && git add synapse_antispam/setup.py && cat synapse_antispam/setup.py" + }, + "devDependencies": { + "@types/config": "^3.3.0", + "@types/crypto-js": "^4.0.2", + "@types/express": "^4.17.13", + "@types/html-to-text": "^8.0.1", + "@types/humanize-duration": "^3.27.1", + "@types/js-yaml": "^4.0.5", + "@types/jsdom": "^16.2.11", + "@types/mocha": "^9.0.0", + "@types/nedb": "^1.8.12", + "@types/node": "^16.7.10", + "@types/pg": "^8.6.5", + "@types/request": "^2.48.8", + "@types/shell-quote": "1.7.1", + "crypto-js": "^4.1.1", + "eslint": "^7.32", + "expect": "^27.0.6", + "mocha": "^9.0.1", + "ts-mocha": "^9.0.2", + "tslint": "^6.1.3", + "typescript": "^4.8.4", + "typescript-formatter": "^7.2" + }, + "dependencies": { + "@sentry/node": "^7.17.2", + "@sentry/tracing": "^7.17.2", + "await-lock": "^2.2.2", + "body-parser": "^1.20.1", + "config": "^3.3.8", + "express": "^4.17", + "html-to-text": "^8.0.0", + "humanize-duration": "^3.27.1", + "humanize-duration-ts": "^2.1.1", + "js-yaml": "^4.1.0", + "jsdom": "^16.6.0", + "matrix-appservice-bridge": "8.0.0", + "parse-duration": "^1.0.2", + "pg": "^8.8.0", + "prom-client": "^14.1.0", + "shell-quote": "^1.7.3", + "ulidx": "^0.3.0", + "yaml": "^2.1.1" + }, + "engines": { + "node": ">=16.0.0" + } +} diff --git a/overlays/mjolnir-package/pin.json b/overlays/mjolnir-package/pin.json new file mode 100644 index 0000000..73953b8 --- /dev/null +++ b/overlays/mjolnir-package/pin.json @@ -0,0 +1,5 @@ +{ + "version": "1.6.4", + "srcSha256": "sha256-/vnojWLpu/fktqPUhAdL1QTESxDwFrBVYAkyF79Fj9w=", + "yarnSha256": "sha256-B4s0CYr5Ihoh4gkckwZ3z0Nb4LMET48WvRXuhk3fpQM=" +} diff --git a/overlays/mjolnir-package/update.sh b/overlays/mjolnir-package/update.sh new file mode 100755 index 0000000..1ada429 --- /dev/null +++ b/overlays/mjolnir-package/update.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env nix-shell +#!nix-shell -i bash -p nix curl jq prefetch-yarn-deps nix-prefetch-github + +if [ "$#" -gt 1 ] || [[ "$1" == -* ]]; then + echo "Regenerates packaging data for mjolnir." + echo "Usage: $0 [git release tag]" + exit 1 +fi + +version=$1 + +set -euo pipefail + +if [ -z "$version" ]; then + version=$(curl "https://api.github.com/repos/matrix-org/mjolnir/releases/latest" | jq -r '.tag_name') +fi + +src="https://raw.githubusercontent.com/matrix-org/mjolnir/$version" +src_hash=$(nix-prefetch-github matrix-org mjolnir --rev ${version} | jq -r .sha256) + +tmpdir=$(mktemp -d) +trap 'rm -rf "$tmpdir"' EXIT + +pushd $tmpdir +curl -O "$src/yarn.lock" +yarn_hash=$(prefetch-yarn-deps yarn.lock) +popd + +curl -O "$src/package.json" +cat > pin.json << EOF +{ + "version": "$version", + "srcHash": "$src_hash", + "yarnHash": "$yarn_hash" +} +EOF diff --git a/overlays/sliding-sync-module.nix b/overlays/sliding-sync-module.nix new file mode 100644 index 0000000..692818b --- /dev/null +++ b/overlays/sliding-sync-module.nix @@ -0,0 +1,87 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.services.sliding-sync; +in +{ + # TODO: add default values + options.services.sliding-sync = { + enable = lib.mkEnableOption (lib.mdDoc "matrix.org sliding-sync"); + server = lib.mkOption { + type = lib.types.str; + # default = "https://matrix-client.matrix.org" # TODO: required? + description = lib.mdDoc '' + The destination homeserver to talk to (CS API HTTPS URL) + ''; + }; + db = lib.mkOption { + type = lib.types.str; + description = lib.mdDoc '' + The postgres connection string: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING + ''; + }; + bindAddr = lib.mkOption { + type = lib.types.str; + default = "0.0.0.0:8008"; + description = lib.mdDoc '' + The interface and port to listen on. + ''; + }; + secret = lib.mkOption { + type = lib.types.str; + description = lib.mdDoc '' + A secret to use to encrypt access tokens. + Must remain the same for the lifetime of the database. + ''; + }; + pprof = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = lib.mdDoc '' + The bind addr for pprof debugging e.g ':6060'. + If not set, does not listen. + ''; + }; + prom = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = lib.mdDoc '' + The bind addr for Prometheus metrics, + which will be accessible at /metrics at this address. + ''; + }; + jaegerUrl = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = lib.mdDoc '' + The Jaeger URL to send spans to e.g http://localhost:14268/api/traces + If unset does not send OTLP traces. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.sliding-sync = { + description = "MSC3575 Matrix Sliding Sync Proxy"; + after = [ + "network.target" + ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "simple"; + DynamicUser = true; + ExecStart = + "${pkgs.sliding-sync}/bin/syncv3"; + Restart = "on-failure"; + }; + environment = { + SYNCV3_SERVER = cfg.server; + SYNCV3_DB = cfg.db; + SYNCV3_SECRET = cfg.secret; + SYNCV3_BINDADDR = cfg.bindAddr; + SYNCV3_PPROF = cfg.pprof; + SYNCV3_PROM = cfg.prom; + SYNCV3_JAEGER_URL = cfg.jaegerUrl; + }; + }; + }; +} diff --git a/overlays/sliding-sync.nix b/overlays/sliding-sync.nix new file mode 100644 index 0000000..50c058d --- /dev/null +++ b/overlays/sliding-sync.nix @@ -0,0 +1,22 @@ +{ lib, buildGoModule, fetchFromGitHub }: + +# TODO: needs: +# - tests +# - `meta` attribute + +buildGoModule rec { + pname = "sliding-sync"; + version = "0.99.1"; + src = fetchFromGitHub { + owner = "matrix-org"; + repo = "sliding-sync"; + rev = "v${version}"; + sha256 = "sha256-g1yMGb8taToEFG6N057yPcdZB855r0f6EwnJ98FIiic="; + }; + + vendorHash = "sha256-FmibAVjKeJUrMSlhoE7onLoa4EVjQvjDI4oU4PB5LBE="; + + subPackages = [ + "cmd/syncv3" + ]; +} diff --git a/overlays/soju-module.nix b/overlays/soju-module.nix new file mode 100644 index 0000000..d14082c --- /dev/null +++ b/overlays/soju-module.nix @@ -0,0 +1,132 @@ +# Not an overlay, module replacement +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.soju; + stateDir = "/var/lib/soju"; + listenCfg = concatMapStringsSep "\n" (l: "listen ${l}") cfg.listen; + tlsCfg = optionalString (cfg.tlsCertificate != null) + "tls ${cfg.tlsCertificate} ${cfg.tlsCertificateKey}"; + logCfg = optionalString cfg.enableMessageLogging + "log fs ${stateDir}/logs"; + + configFile = pkgs.writeText "soju.conf" '' + ${listenCfg} + hostname ${cfg.hostName} + ${tlsCfg} + db sqlite3 ${stateDir}/soju.db + ${logCfg} + http-origin ${concatStringsSep " " cfg.httpOrigins} + accept-proxy-ip ${concatStringsSep " " cfg.acceptProxyIP} + + ${cfg.extraConfig} + ''; +in +{ + ###### interface + + options.services.soju = { + enable = mkEnableOption (lib.mdDoc "soju"); + + listen = mkOption { + type = types.listOf types.str; + default = [ ":6697" ]; + description = lib.mdDoc '' + Where soju should listen for incoming connections. See the + `listen` directive in + {manpage}`soju(1)`. + ''; + }; + + hostName = mkOption { + type = types.str; + default = config.networking.hostName; + defaultText = literalExpression "config.networking.hostName"; + description = lib.mdDoc "Server hostname."; + }; + + tlsCertificate = mkOption { + type = types.nullOr types.path; + default = null; + example = "/var/host.cert"; + description = lib.mdDoc "Path to server TLS certificate."; + }; + + tlsCertificateKey = mkOption { + type = types.nullOr types.path; + default = null; + example = "/var/host.key"; + description = lib.mdDoc "Path to server TLS certificate key."; + }; + + enableMessageLogging = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc "Whether to enable message logging."; + }; + + httpOrigins = mkOption { + type = types.listOf types.str; + default = [ ]; + description = lib.mdDoc '' + List of allowed HTTP origins for WebSocket listeners. The parameters are + interpreted as shell patterns, see + {manpage}`glob(7)`. + ''; + }; + + acceptProxyIP = mkOption { + type = types.listOf types.str; + default = [ ]; + description = lib.mdDoc '' + Allow the specified IPs to act as a proxy. Proxys have the ability to + overwrite the remote and local connection addresses (via the X-Forwarded-\* + HTTP header fields). The special name "localhost" accepts the loopback + addresses 127.0.0.0/8 and ::1/128. By default, all IPs are rejected. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = lib.mdDoc "Lines added verbatim to the configuration file."; + }; + + extraGroups = mkOption { + type = types.listOf types.str; + default = [ ]; + description = lib.mdDoc "Extra groups for the dynamic user."; + }; + }; + + ###### implementation + + config = mkIf cfg.enable { + assertions = [ + { + assertion = (cfg.tlsCertificate != null) == (cfg.tlsCertificateKey != null); + message = '' + services.soju.tlsCertificate and services.soju.tlsCertificateKey + must both be specified to enable TLS. + ''; + } + ]; + + systemd.services.soju = { + description = "soju IRC bouncer"; + wantedBy = [ "multi-user.target" ]; + after = [ "network-online.target" ]; + serviceConfig = { + DynamicUser = true; + SupplementaryGroups = cfg.extraGroups; + Restart = "always"; + ExecStart = "${pkgs.soju}/bin/soju -config ${configFile}"; + StateDirectory = "soju"; + }; + }; + }; + + meta.maintainers = with maintainers; [ malvo ]; +} diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..1a7a229 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,5 @@ +infra->scripts +============== + +Various scripts for managing users. +Currently unused, might be used when we open registrations. diff --git a/scripts/manage-user b/scripts/manage-user new file mode 100755 index 0000000..43c7526 --- /dev/null +++ b/scripts/manage-user @@ -0,0 +1,79 @@ +#!/usr/bin/env bash + +domain='exotic.sh' +group="$(echo $domain | awk -F. '{printf $1}')" + +cmd="$1" +user="$2" +email="$3" +ssh_pubkey="$4" + +die() { + echo "$@" >&2; exit 1; +} + +confirm() { + printf '%s\n' "$@" + select yn in "Yes" "No"; do + case $yn in + Yes) return 0; break;; + No) return 1; break;; + esac; + done +} + +help() { + printf '%s user management\n\n' $domain + printf 'Usage: %s [args]\n' "$(basename "$0")" + printf '\tadd - add user\n' + printf '\tdel - delete user\n' + printf '\tlist - list users\n' + printf '\thelp - display this help command\n' +} + +add_user() { + echo "" +} + +del_user() { + echo "" +} + +list_users() { + echo 'TODO: list LDAP' +} + + +[ "$(id -u)" -ne 0 ] && die 'This script should be run as root!' + +case "$cmd" in + add) [ $# -lt 3 ] && die 'Insufficient arguments!' + if ! id -u "$user" >/dev/null 2>&1; then + printf 'Creating user:\n' + printf '\tUsername: %s\n' $user + printf '\tEmail: %s\n' $email + printf '\tSSH: %s\n\n' $ssh_pubkey + if confirm 'Are those details correct?'; then + add_user + else + echo 'Aborting.' + fi + else + die "User '$user' already exists!" + fi + ;; + del) [ $# -lt 2 ] && die 'Insufficient arguments!' + if id -u "$user" >/dev/null 2>&1; then + printf 'Deleting user: %s\n' $user + if confirm 'Are you sure?'; then + del_user + else + echo 'Aborting.' + fi + else + die "User '$user' doesn't exist!" + fi + ;; + list) list_users;; + help|*) help;; +esac diff --git a/systems/.sops.yaml b/systems/.sops.yaml new file mode 100644 index 0000000..11331c6 --- /dev/null +++ b/systems/.sops.yaml @@ -0,0 +1,10 @@ +keys: + - &sefidel age1jt8xg0lvzj5q4f7fn7nw670qsszm3kv3caa654eh62azra4x44zss4fad8 + - &sefidel_pgp 8BDFDFB56842239382A0441B9238BC709E05516A + - &host_cobalt age14a2amn7memzvctf2nnrt6uj458x3g4jpcvs04tlkww2z02p05syqawxrwh +creation_rules: + - path_regex: cobalt/secrets/[^/]+\.yaml$ + key_groups: + - age: + - *sefidel + - *host_cobalt diff --git a/systems/cobalt/default.nix b/systems/cobalt/default.nix new file mode 100644 index 0000000..0a5cfe0 --- /dev/null +++ b/systems/cobalt/default.nix @@ -0,0 +1,303 @@ +{ config, pkgs, lib, ... }: + +with lib; +let + ipv4 = { + address = "95.216.74.104"; + gateway = "95.216.74.65"; + netmask = "255.255.255.192"; + prefixLength = 26; # https://www.pawprint.net/designresources/netmask-converter.php + }; + ipv6 = { + address = "2a01:4f9:2b:a98::"; + gateway = "fe80::1"; + prefixLength = 64; + }; + networkInterface = "eth0"; + hostName = "cobalt"; + hostId = "712ae82a"; + hostAddr = "cobalt.exotic.sh"; + + poorObfuscation = y: x: "${x}@${y}"; +in +{ + deployment = { + targetHost = hostAddr; + targetPort = 22; + targetUser = "root"; + }; + + imports = [ ./hardware-configuration.nix ]; + + boot.supportedFilesystems = [ "zfs" ]; + networking.hostId = hostId; + + boot.loader.grub.enable = true; + # boot.loader.grub.version = 2; + boot.loader.grub.efiSupport = false; + # boot.loader.grub.device = "nodev"; + + # This should be done automatically, but explicitly declare it just in case. + boot.loader.grub.copyKernels = true; + # Make sure that you've listed all of the boot partitions here. + boot.loader.grub.mirroredBoots = [ + { path = "/boot"; devices = [ "/dev/disk/by-id/ata-ST4000NM0245-1Z2107_ZC17GW7G" ]; } + { path = "/boot-fallback"; devices = [ "/dev/disk/by-id/ata-ST4000NM0245-1Z2107_ZC17GWB2" ]; } + ]; + + # Boot normally when one of the boot partitions are missing + fileSystems."/boot".options = [ "nofail" ]; + fileSystems."/boot-fallback".options = [ "nofail" ]; + + # Erase your darlings + # boot.initrd.postDeviceCommands = lib.mkAfter ''nix systemd environ + # zfs rollback -r rpool/local/root@blank + # ''; + + # NOTE: replace these to boot.initrd.availableKernelModules? + boot.kernelModules = [ "e1000e" ]; + boot.initrd.kernelModules = [ "e1000e" ]; + + boot.kernelParams = [ + # See for documentation. + # ip=::::::::: + # The server ip refers to the NFS server -- not needed in this case. + "ip=${ipv4.address}::${ipv4.gateway}:${ipv4.netmask}:${hostName}-initrd:${networkInterface}:off:8.8.8.8" + ]; + + boot.initrd.network.enable = true; + boot.initrd.network.ssh = { + enable = true; + + # Using the same port as the actual SSH will cause clients to throw errors + # related to host key mismatch. + port = 2222; + + # This takes 'path's, not 'string's. + hostKeys = [ + /boot/initrd-ssh-key + /boot-fallback/initrd-ssh-key + ]; + + # Public ssh key to log into the initrd ssh + authorizedKeys = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILN14b5Fu+StHeMXq4ClyLG4G+/vCAfS7adxceEFria/ openpgp:0x1D5BCD11" ]; + }; + boot.initrd.network.postCommands = '' + cat < /root/.profile + if pgrep -x "zfs" > /dev/null + then + zfs load-key -a + killall zfs + else + echo "ZFS is not running -- this could be a sign of failure." + fi + EOF + ''; + + + networking.hostName = hostName; + + networking.useDHCP = false; + networking.interfaces.${networkInterface} = { + ipv4 = { addresses = [{ address = ipv4.address; prefixLength = ipv4.prefixLength; }]; }; + ipv6 = { addresses = [{ address = ipv6.address; prefixLength = ipv6.prefixLength; }]; }; + }; + networking.defaultGateway = ipv4.gateway; + networking.defaultGateway6 = { address = ipv6.gateway; interface = networkInterface; }; + networking.nameservers = [ "8.8.8.8" ]; + + networking.firewall.enable = true; + + time.timeZone = "UTC"; + + users.users.root.initialHashedPassword = ""; # FIXME: use proper secret + users.users.root.openssh.authorizedKeys.keys = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILN14b5Fu+StHeMXq4ClyLG4G+/vCAfS7adxceEFria/ openpgp:0x1D5BCD11" ]; + services.openssh.enable = true; + services.openssh.settings.permitRootLogin = "prohibit-password"; + + services.openssh.hostKeys = [ + { + path = "/persist/ssh/ssh_host_ed25519_key"; + type = "ed25519"; + } + { + path = "/persist/ssh/ssh_host_rsa_key"; + type = "rsa"; + bits = 4096; + } + ]; + + # impermanence requirement + fileSystems."/persist".neededForBoot = true; + + environment.systemPackages = with pkgs; [ + bsd-finger + ]; + + sops.secrets.acme-envs = { + owner = "acme"; + }; + sops.secrets.matrix-server-key = { }; + sops.secrets.dendrite-envs = { }; + sops.secrets.sliding-sync-secret = { }; + sops.secrets.mjolnir-password = { owner = "mjolnir"; }; + sops.secrets.mautrix-telegram-envs = { }; + sops.secrets.turn-secret = { }; + # sops.secrets.openldap-admin-key = { + # owner = "openldap"; + # }; + + modules = { + sops.enable = true; + + services.metrics = { + enable = true; + domain = "status.exotic.sh"; + tls.acmeHost = "exotic.sh"; + }; + + services.coredns.enable = false; + services.nginx.enable = true; + services.acme = { + enable = true; + email = poorObfuscation "exotic.sh" "postmaster"; + + certs = { + "exotic.sh" = { + subDomains = [ + "git" + "matrix" + "*.labs" + "social" + "bouncer" + "meet" + "chat" + "turn" + "status" + "mail" + "todo" + ]; + }; + "nand.moe" = { + subDomains = [ ]; + }; + "sefidel.net" = { + subDomains = [ ]; + }; + }; + + secrets.acme-credentials = config.sops.secrets.acme-envs.path; + }; + + services.gitolite = { + enable = true; + adminPubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILN14b5Fu+StHeMXq4ClyLG4G+/vCAfS7adxceEFria/ openpgp:0x1D5BCD11"; + }; + services.gitDaemon.enable = true; + services.cgit = { + enable = true; + domain = "exotic.sh"; + realHost = "git.exotic.sh"; + settings.description = "Exotic place."; + }; + services.fail2ban.enable = true; + services.postgresql.enable = true; + services.dendrite = { + enable = true; + domain = "exotic.sh"; + realHost = "matrix.exotic.sh"; + slidingSyncHost = "slidingsync.labs.exotic.sh"; + turn = { + enable = true; + domain = "turn.exotic.sh"; + shared_secret = "$TURN_SECRET"; # dendrite envs + }; + secrets = { + matrix-server-key = config.sops.secrets.matrix-server-key.path; + dendrite-envs = config.sops.secrets.dendrite-envs.path; + sliding-sync-secret = config.sops.secrets.sliding-sync-secret.path; + }; + }; + services.coturn = { + enable = true; + domain = "turn.exotic.sh"; + tls.acmeHost = "exotic.sh"; + shared_secret = config.sops.secrets.turn-secret.path; + }; + services.matrix-moderation = { + enable = true; + domain = "exotic.sh"; + secrets.userPassword = config.sops.secrets.mjolnir-password.path; + }; + services.matrix-bridge = { + enable = true; + domain = "exotic.sh"; + secrets.mautrix-envs = config.sops.secrets.mautrix-telegram-envs.path; + }; + services.element-web = { + enable = true; + hostName = "chat.exotic.sh"; + matrix = { + baseUrl = "https://matrix.exotic.sh"; + serverName = "exotic.sh"; + }; + tls.acmeHost = "exotic.sh"; + jitsi.domain = "meet.exotic.sh"; + }; + services.akkoma = { + enable = true; + domain = "exotic.sh"; + realHost = "social.exotic.sh"; + instanceName = "exotic.sh social"; + }; + services.misskey = { + enable = true; + domain = "nand.moe"; + realHost = "nand.moe"; + }; + services.soju = { + enable = true; + hostName = "bouncer.exotic.sh"; + tls.enable = true; + tls.acmeHost = "exotic.sh"; + }; + services.vikunja = { + enable = true; + domain = "exotic.sh"; + realHost = "todo.exotic.sh"; + }; + + + services.nixos-mailserver.enable = true; # TODO: replace with dovecot.nix? + + services.jitsi = { + enable = true; + hostName = "meet.exotic.sh"; + tls.acmeHost = "exotic.sh"; + }; + + services.ldap = { + enable = false; + dc = "exotic"; + tld = "sh"; + tls.acmeHost = "exotic.sh"; + secrets.rootPass = config.sops.secrets.openldap-admin-key.path; + }; + services.pubnix.enable = false; + services.userweb = { + enable = false; + domain = "exotic.sh"; + }; + + services.sefidel-web.enable = true; + }; + + # This value determines the NixOS release from which the default + # settings for stateful data, like file locations and database versions + # on your system were taken. It‘s perfectly fine and recommended to leave + # this value at the release version of the first install of this system. + # Before changing this value read the documentation for this option + # (e.g. man configuration.nix or on https://nixos.org/nixos/options.html). + system.stateVersion = "23.05"; # Did you read the comment? +} + diff --git a/systems/cobalt/hardware-configuration.nix b/systems/cobalt/hardware-configuration.nix new file mode 100644 index 0000000..95ecb96 --- /dev/null +++ b/systems/cobalt/hardware-configuration.nix @@ -0,0 +1,65 @@ +# Do not modify this file! It was generated by ‘nixos-generate-config’ +# and may be overwritten by future invocations. Please make changes +# to /etc/nixos/configuration.nix instead. +{ config, lib, pkgs, modulesPath, ... }: + +{ + imports = + [ + (modulesPath + "/installer/scan/not-detected.nix") + ]; + + boot.initrd.availableKernelModules = [ "xhci_pci" "ahci" "sd_mod" ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ "kvm-intel" ]; + boot.extraModulePackages = [ ]; + + fileSystems."/" = + { + device = "rpool/local/root"; + fsType = "zfs"; + }; + + fileSystems."/boot" = + { + device = "/dev/disk/by-uuid/445A-0C55"; + fsType = "vfat"; + }; + + fileSystems."/boot-fallback" = + { + device = "/dev/disk/by-uuid/445C-198F"; + fsType = "vfat"; + }; + + fileSystems."/nix" = + { + device = "rpool/local/nix"; + fsType = "zfs"; + }; + + fileSystems."/home" = + { + device = "rpool/safe/home"; + fsType = "zfs"; + }; + + fileSystems."/persist" = + { + device = "rpool/safe/persist"; + fsType = "zfs"; + }; + + swapDevices = [ ]; + + # Enables DHCP on each ethernet and wireless interface. In case of scripted networking + # (the default) this is the recommended approach. When using systemd-networkd it's + # still possible to use this option, but it's recommended to use it in conjunction + # with explicit per-interface declarations with `networking.interfaces..useDHCP`. + networking.useDHCP = lib.mkDefault false; + # networking.interfaces.enp0s31f6.useDHCP = lib.mkDefault true; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + powerManagement.cpuFreqGovernor = lib.mkDefault "powersave"; + hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; +} diff --git a/systems/cobalt/secrets/secrets.yaml b/systems/cobalt/secrets/secrets.yaml new file mode 100644 index 0000000..8e0c0e5 --- /dev/null +++ b/systems/cobalt/secrets/secrets.yaml @@ -0,0 +1,39 @@ +acme-envs: ENC[AES256_GCM,data:9IvoY1E2VLikZgPcNnEl2e33SMgLOJsX7aVTEbld1ggl8Z77a2iau17d/ZLWs0+u,iv:gr2iHuYmtZp2eWhX0E0OKolIn/Nm5+9hJqFTYZagV4c=,tag:9mqUFzqe8+3T+Nwbu5V0Fg==,type:str] +matrix-server-key: ENC[AES256_GCM,data:MsPH8g0bc0cY+k5XvVIyi1hDpX2up7+noU9P4Dfm3Z9f1eXv7SaJhlRnR40qrk4sQN1uG0R/ro7S2z9EswX0iZx10PfjWF8Igrc9w9b2+EM/gb0O3dGskPAnDrm9JYh+gF6SsqmwJUEYh+Lx1EfuGMTWpnXZjLrimIWCfmz/1qxJECQds/aQwuA=,iv:B8yzXZ1IUVVvxFQ0MzzS5LSHZXQirnXiLoOru4S2H78=,tag:TNp1Y3C/iYgq5itqXGIt/g==,type:str] +mjolnir-password: ENC[AES256_GCM,data:dyM2VVxn1PFRXy5dgfvq3EuWyGDhDZvJOd1sTnKE5q0Arv1y,iv:DD80um8QXLybj1w4ZsxPbv3+s2NrQfpPDAEpkztkMFo=,tag:3ZEJ7V+ICh2Ip5gZt06zjA==,type:str] +mautrix-telegram-envs: ENC[AES256_GCM,data:hDfEI6tshgPHn/yPHkqO9/VZnB/vTnuyopVm2/0CKoWska66UXOHbXwvO6UXXUZldzkvKe6min3O9xjEGBC+SZdLWmfi6AxJvoKnmU2MzufNkrDG4k0turK7LwwVnm8ADZd9tEII+P3h42t78jhkgerailjP1rtVKleAZpr+ioRGhCTAE6tVWnhV4jveUwNkml+0AhMc3aY0N2PLvedw8p/jsX6Yujn2baKc7HOkvAasctqq/p8kdiTZCA+ASIVC15iR+KtGu2V7vXjDuzYU7Xv7+RbXN7rUTCBIJwyc3XxpwOmD46XR+/2iSZ3nL0PmDftbIEaA9LatjUap7mRFOwX5JruAl1XxAGdG965HZFyTHp1UggjjWvoHQmyb1xJXD5ZE+EU8obkFV0N1o7Q4XIF6/REz9+01JrOd1F5GxjoAo29JV+1lhdxGykrFcA63VLsXdsUgZflWH7Rxol2nOMI5t88+zohYJ5OpeLlRD0gVaX8dvgSzvzmZaNxASW+9tHOQBSzkZsM6jEon6ZXeAa0oh/ee+W82jHhe/ohlj0UV7TtpW/QWs3NR3mv1x41TMBawnVQ7sb9UH/sbdbajJYEAkSD/32ETSjXiQJsmhGEj5kRBNAQA16s1o0M24+UnpHo71jqQnRQpOUtFhLQqakOWvSdM4KgIXExc87Fkw+bQxLtpIup+T4WpJkCe5f5rB/jHvob33oitzjS1mam9UTtzwQjTuR/P4V8IORygDHlcq8W2PtDhWOdgtWI9cfIlqIIgu6+7Rg4+1dq5aw8j06eWrNH2NElPMji0opN+onT3VflgwGPXHlemId20Z2TXNuE6LlnUhyyxdh0rw3tVuCDcodzj4HfXk9mbjlTufyGKY41K/A6QTotiHANojizuTyulyjI3Gx1Wvzv8+Ib84SZGl9ZSsg7g6Ig5U12IJhNtZ/vMj8MHpRjxnyRmZ4jsQi9UctDKrhHjulD1WJyhBveoEv8L79c6e0xZYAwkSlxvdaOPK3OMGbbtbH8vy7WYXSwHZomwJ7BrsrQ/5lWav2xXzgM7ASnEg/rKBPr9YQYOBA==,iv:K2qdi99cjom4mzcNH26qqX3gQNSvZzuAh9QY4zmZv9I=,tag:e9/4biFz6mVsGHV/YOA1jg==,type:str] +dendrite-envs: ENC[AES256_GCM,data:67FnrGQUZWFfHAoUM/idTZlBX7aek3fbPkswB9+3pjLNQuXpIWYoa2vpdGt7zec2n9o9z0V3LdlkookjS95aPpZmKYwPaKkH2L7Jaxw=,iv:c4lEReLizcQeTTiG7cJwd+2sBH+EKBGycKeoDgJ/394=,tag:zBBxIcXn+8Q90BkPidltfQ==,type:str] +sliding-sync-secret: ENC[AES256_GCM,data:lNIlUtNbXw1/w44m7RwqmvOTmc4MYfag7Nvo0izTM6Al7eOZXjEXGgfr7b4PB3QNewghN4LEEj1ae8HqpivT/w==,iv:Qw2XLjYp3/78OuEakUDJ0Sp3fmjw36IJTg8UKTZ7UEU=,tag:tp3gACPEZNMzj+aDP3Ax4A==,type:str] +turn-secret: ENC[AES256_GCM,data:JA5/BlGwH6yIjYsFZGa8Nm8XVbOBKpre+NFybniOtlmbSx89ldKBvuqF2ZoPltJS+vzQ/+wxM/VorhF7M+s4jA==,iv:rK5SFj4VOzgfaP/LIzWTVFyCBmklGMSyd9iWbet2CVc=,tag:QycYCHH72bMMX5UubDHTlg==,type:str] +openldap-admin-key: ENC[AES256_GCM,data:WBBDPFDW6Q4sJ5+/pK8kAe6iFgJ8gGgi3eCVNvZB,iv:1rnmhu29UGsXLxD9Ptbv7P67EAYgKVk1dlkM6p0L4vA=,tag:yNRrHMI2yT8Oo7qkwxSeUg==,type:str] +sefidel-imap-pass: ENC[AES256_GCM,data:rx9hZb+BARs9gB+XLLRMLWDSx67KqkKB1/4nOOtU9i56uagMprFEeDnh8pEaioZbNlqjJRO8kWTBBvWZ,iv:WxKLp0VmwfxVFZt9cnZUbp4wn5WEHubImp8fQy2bXyg=,tag:Vzh0Ntz8iFaSIEf2wjbOKg==,type:str] +internal-imap-pass: ENC[AES256_GCM,data:ydjz/NthnJZFLrR1M+p0xEy5xhM8MbPtqE10r0s1DWDFZoyXwRRrIYefFZheW29EjY3VBfr3zWcRIbNm,iv:6hU/dHADbn4pNi0vlJG8BoyQW1ohByINSO6y+nJddfY=,tag:j67D2stmq2A+ulhFIYkZPA==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age1jt8xg0lvzj5q4f7fn7nw670qsszm3kv3caa654eh62azra4x44zss4fad8 + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvbE1CVEFaWXQ4M0lxR1dH + c2dJMGFxTS85ZDhlamRyQWl2Y3VxcXhVdm5RCml3MWl4dkZQK1Z5T3JqTnBLaVU1 + R25PdXJBVWczMGt6RkNWWllXOFJicDAKLS0tIElJYytrUHhzb2xHeTJTRy82bFJR + bm11bEpUaFJjcHB0c2pRendiakRpS2cKNa9ZrFkOLfOqEEN9ATktvrQgANceDj2c + mkUjhxPfti4jNE2c6gsq3DegJT/08QpFJYYuAx/sO2R8wld3kFVyrQ== + -----END AGE ENCRYPTED FILE----- + - recipient: age14a2amn7memzvctf2nnrt6uj458x3g4jpcvs04tlkww2z02p05syqawxrwh + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4TEpsUmRQNUxDUU00VENS + MTgxN2ZjM0NYOEdaem5yaW1XaHdlandsQmtNCk5YSzdHdTFqa1BOSkZnajJJenhY + SFdnKzI2WmhONnJ2UWJjRWNGNUhuT0kKLS0tIHNRcVo2VXJEY0tSVUEwVkF0Z2l6 + cUpBZ01CMEFjNnNuWjlYejVKajkwcGMKehqYCZP0zZHDTfJrC/5LYiE/3doa0OiM + OKXhOuUX8HF8RfkyiOSMpntxuNX2jSvd9sQRYnHkUvgm793+IuQjrg== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2023-03-27T15:19:33Z" + mac: ENC[AES256_GCM,data:CyVH0paaTqnff98h5CSCas3YYYYAxEtyYdkjyFBfN/Nwfpe3e71O6YwLZgzAZoiaN+1FuF0kls5WmvDNdx95rEC4yvxQACA75iRyP95B5Q9iN9SGGld0Ii8wPY6s0QkJX+OL7mCllH/gC0J2gOpnPxRB9k5v5FXtKHmJtj5kfaI=,iv:ytWBOy2VTWtVlPbrXiHF5BNxbCmQ194x6aeMh1pd7vc=,tag:0J77TO1y8OTXzdODqANkEw==,type:str] + pgp: [] + unencrypted_suffix: _unencrypted + version: 3.7.3 -- cgit 1.4.1