aboutsummaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/README.md9
-rw-r--r--modules/cachix/caches/nix-community.nix12
-rw-r--r--modules/cachix/default.nix13
-rw-r--r--modules/flakes.nix36
-rw-r--r--modules/nix.nix34
-rw-r--r--modules/security.nix59
-rw-r--r--modules/services/acme.nix52
-rw-r--r--modules/services/akkoma/blocklist.toml163
-rw-r--r--modules/services/akkoma/default.nix95
-rw-r--r--modules/services/akkoma/favicon-withbg.pngbin0 -> 17246 bytes
-rw-r--r--modules/services/akkoma/favicon.pngbin0 -> 16693 bytes
-rw-r--r--modules/services/akkoma/logo.pngbin0 -> 1304 bytes
-rw-r--r--modules/services/akkoma/logo.svg71
-rw-r--r--modules/services/akkoma/robots.txt2
-rw-r--r--modules/services/akkoma/terms-of-service.html26
-rw-r--r--modules/services/cgit.nix121
-rw-r--r--modules/services/coredns/_corefile.nix3
-rw-r--r--modules/services/coredns/default.nix18
-rw-r--r--modules/services/coturn.nix64
-rw-r--r--modules/services/dendrite.nix230
-rw-r--r--modules/services/dovecot.nix18
-rw-r--r--modules/services/element-web.nix47
-rw-r--r--modules/services/fail2ban.nix17
-rw-r--r--modules/services/git-daemon.nix29
-rw-r--r--modules/services/gitolite/default.nix108
-rw-r--r--modules/services/gitolite/fix-refs9
-rw-r--r--modules/services/gitolite/rename63
-rw-r--r--modules/services/jitsi.nix38
-rw-r--r--modules/services/ldap.nix76
-rw-r--r--modules/services/matrix-bridge.nix200
-rw-r--r--modules/services/matrix-moderation.nix52
-rw-r--r--modules/services/metrics.nix165
-rw-r--r--modules/services/misskey/config/default.yml156
-rw-r--r--modules/services/misskey/default.nix88
-rw-r--r--modules/services/nginx.nix37
-rw-r--r--modules/services/nixos-mailserver.nix106
-rw-r--r--modules/services/postgresql.nix34
-rw-r--r--modules/services/pubnix.nix20
-rw-r--r--modules/services/sefidel-web.nix26
-rw-r--r--modules/services/soju.nix48
-rw-r--r--modules/services/userweb.nix36
-rw-r--r--modules/services/vikunja.nix50
-rw-r--r--modules/sops.nix21
43 files changed, 2452 insertions, 0 deletions
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
+`<servicename>` for a module configuring one thing, `<capability>` 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
--- /dev/null
+++ b/modules/services/akkoma/favicon-withbg.png
Binary files differ
diff --git a/modules/services/akkoma/favicon.png b/modules/services/akkoma/favicon.png
new file mode 100644
index 0000000..d8cbce3
--- /dev/null
+++ b/modules/services/akkoma/favicon.png
Binary files differ
diff --git a/modules/services/akkoma/logo.png b/modules/services/akkoma/logo.png
new file mode 100644
index 0000000..7744b1a
--- /dev/null
+++ b/modules/services/akkoma/logo.png
Binary files 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.1"
+ id="svg4485"
+ width="512"
+ height="512"
+ viewBox="0 0 512 512"
+ sodipodi:docname="logo.svg"
+ inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)">
+ <metadata
+ id="metadata4491">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs4489" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1274"
+ inkscape:window-height="1410"
+ id="namedview4487"
+ showgrid="false"
+ inkscape:zoom="1.2636719"
+ inkscape:cx="305.99333"
+ inkscape:cy="304.30809"
+ inkscape:window-x="1280"
+ inkscape:window-y="22"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="g4612"
+ inkscape:document-rotation="0" />
+ <g
+ id="g4612">
+ <g
+ id="g850"
+ transform="matrix(0.99659595,0,0,0.99659595,0.37313949,0.87143746)">
+ <path
+ style="opacity:1;fill:#fba457;fill-opacity:1;stroke:#009bff;stroke-width:0;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.175879"
+ d="m 194.75841,124.65165 a 20.449443,20.449443 0 0 0 -20.44944,20.44945 v 242.24725 h 65.28091 v -262.6967 z"
+ id="path4497" />
+ <path
+ style="fill:#fba457;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 272.6236,124.65165 V 256 h 45.61799 a 20.449443,20.449443 0 0 0 20.44944,-20.44945 v -110.8989 z"
+ id="path4516" />
+ <path
+ style="opacity:1;fill:#fba457;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 272.6236,322.06744 v 65.28091 h 45.61799 a 20.449443,20.449443 0 0 0 20.44944,-20.44945 v -44.83146 z"
+ id="path4516-5" />
+ </g>
+ </g>
+</svg>
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 @@
+<!DOCTYPE html>
+<html>
+ <head></head>
+ <body>
+ <h2>Terms of Service</h2>
+ <p>This is a personal instance with only one user. Therefore, I'll write rules that I'll abide:</p>
+ <ol>
+ <li>
+ <p>No discrimination based on race, gender, sexual orientation, disabilities, or any other characteristic.</p>
+ </li>
+ <li>
+ <p>No harassment or doxxing towards others.</p>
+ </li>
+ <li>
+ <p>No promotion of violence.</p>
+ </li>
+ <li>
+ <p>No content that is illegal in United Kingdom, Japan, Finland, Germany, and South Korea.</p>
+ </li>
+ <li>
+ <p>Use content warnings for explicit or controversial content.</p>
+ </li>
+ </ol>
+ <p>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!</p>
+ </body>
+</html>
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] <repo1> <repo2>
+#
+# 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 <changeme>
+ # by <changeme>''
+
+ ''{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/)(?<user>[\w-]+)(?<user_uri>/.*)?$" = {
+ 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;
+ };
+}