diff --git a/hosts/nachtigall/apps/matrix/mjolnir.nix b/hosts/nachtigall/apps/matrix/mjolnir.nix
new file mode 100644
index 0000000..ee717c5
--- /dev/null
+++ b/hosts/nachtigall/apps/matrix/mjolnir.nix
@@ -0,0 +1,47 @@
+{ lib, flake, ... }:
+{
+  age.secrets."matrix-mjolnir-password" = {
+    file = "${flake.self}/secrets/matrix-mjolnir-password.age";
+    mode = "640";
+    owner = "root";
+    group = "mjolnir";
+  };
+
+  # Adopted from:
+  # https://github.com/NixOS/nixos-org-configurations/blob/42ab3d94c0b5995f2ea05eb0b20b4759192c01ff/non-critical-infra/modules/mjolnir.nix
+  #
+  # pantalaimon takes ages to start up, so mjolnir could hit the systemd burst
+  # limit and then just be down forever. We don't want mjolnir to ever go down,
+  # so disable rate-limiting and allow it to flap until pantalaimon is alive.
+  systemd.services.mjolnir.serviceConfig.Restart = lib.mkForce "always";
+  systemd.services.mjolnir.serviceConfig.RestartSec = 3;
+  systemd.services.mjolnir.unitConfig.StartLimitIntervalSec = 0;
+
+  services.pantalaimon-headless.instances.mjolnir.listenAddress = "127.0.0.1";
+
+  services.mjolnir = {
+    enable = true;
+    homeserverUrl = "https://matrix.pub.solar:443";
+
+    pantalaimon = {
+      enable = true;
+      username = "mjolnir";
+      passwordFile = "/run/agenix/matrix-mjolnir-password";
+      options = {
+        listenAddress = "127.0.0.1";
+      };
+    };
+
+    managementRoom = "#moderators:pub.solar";
+
+    # https://github.com/matrix-org/mjolnir/blob/master/config/default.yaml
+    settings = {
+      noop = false;
+      protectAllJoinedRooms = true;
+      fasterMembershipChecks = true;
+
+      # too noisy
+      verboseLogging = false;
+    };
+  };
+}
diff --git a/hosts/nachtigall/apps/matrix/synapse.nix b/hosts/nachtigall/apps/matrix/synapse.nix
index 9a16c36..f1f3515 100644
--- a/hosts/nachtigall/apps/matrix/synapse.nix
+++ b/hosts/nachtigall/apps/matrix/synapse.nix
@@ -254,6 +254,31 @@ in
         # "/matrix-mautrix-signal-registration.yaml"
         # "/matrix-mautrix-telegram-registration.yaml"
       ];
+
+      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"
+            ];
+          };
+        }
+      ];
     };
 
     withJemalloc = true;
@@ -275,8 +300,9 @@ in
       "redis"
     ];
 
-    plugins = [
-      config.services.matrix-synapse.package.plugins.matrix-synapse-shared-secret-auth
+    plugins = with config.services.matrix-synapse.package.plugins; [
+      matrix-synapse-shared-secret-auth
+      matrix-synapse-mjolnir-antispam
     ];
 
     sliding-sync = {
diff --git a/secrets/matrix-mjolnir-password.age b/secrets/matrix-mjolnir-password.age
new file mode 100644
index 0000000..d9f7de1
--- /dev/null
+++ b/secrets/matrix-mjolnir-password.age
@@ -0,0 +1,25 @@
+age-encryption.org/v1
+-> ssh-ed25519 iDKjwg k0qY4jLPEdz8HDYS8Ubh5sUp+BidUJ9j3nPYqxwYwX0
+ZWqonJ8wEFkt7iC4I6RzoVMcRRaK5yjFORz2ysTzrp4
+-> ssh-ed25519 uYcDNw 4GC7Rc2iDtDKNObkZGzt6TLhY49SkYNSz4JbZtKva04
+Z4q6od9qzaN28tizJhoO/lm1U2ymnu1hbUWoAMtNM+8
+-> ssh-rsa kFDS0A
+KNQhAEi5o9kk+EljFMRXjNoWa3xY+QEq3OaCkqkuEpr65wPmtrjVq/eMxAX31SgU
+EwIjUlBf3XsdAZkYmrItBuPgwxKBClDhnOHZQS6GowYPOW+CDNlRzcp947kfCcdG
+ZrbrMZb/zwqDNijOgjh1zn6kdaX2clp3wA5GdLP1pSRRBQWh7ZkGQkgiyQSLIHWu
+nfo/liBJ6qMAGtVwlAHcYhQDiYsYoquRvQ7TsgdQtu9NPmKhwjWbpEaJSt7AMC2y
+e9B8Lp46oPZHCptPqMBpvi5SPxg9X0wvj9Vg+3OG+dn0zvQmyTtEHq15I9MKSPCB
+oNVgvrgEcgaKxMdJCqFdbCj5I+eyZZc9tHTggSzSLAYHzoY3TbYx6TOpeHbub3lc
+cBCnbNTRwQCNQoBLXAKIkhcIv968D3RvtY5lPdQdU7MoW5GFHy67vmERMDBVCiYI
+29HSxdLGTLUKOgzLdR0pxQnRPSdxEBw06gHRP3q6MDXH066Of5e/RRqvYzJX1VlH
+cMhJjGTVZNnqP3RIVg1FMLfz6uooki8J9w42JOa9VVB3Zf5ics8vf7m3EObcHXL9
+B/Wh6oy9L+q6vZHs8ix5cHmIQA3GLsSsdQ2NimVG+YO9zwUPq9MNqPpoZfXH+wa6
+gUpANLeJjYzuo0Ob0gDMHSaFBfuyn1MxPipbccgnXG4
+-> ssh-ed25519 YFSOsg AiVh32W3+y52eDKrMBU0qjertV661tD8jqb8q4ZAyy8
+zIN8hgZ4ynWAt/HOcY4zzYHZUmeBNyk0TgtmztkGXi4
+-> ssh-ed25519 iHV63A EfbQmp++H8mgZzmYpsrZNRo2tfRurA66Z7fk4NQuzxM
+e6pH0+P/rtCPNcsuIZKop2RTd9eSv3hPcReNaZ/GkTs
+-> ssh-ed25519 BVsyTA ngQM3zUSkkt855E1MI25RuEWRYqaMVstY338Tq/n8yM
+wWtAV3MI0jQ9rlgeIO5DbPv0INH2KgV5Ic9NbXyNPDk
+--- JAi1rNmpk8X4L+TLJfqZ5r+AyFVd/rkUHBA/Mjjde3I
+�DO)S����x���y��|�3�~�`2�*ʆ�	�������ݣ���ˣ'aA�fXF�������QXv[�&a!�L��	�
\ No newline at end of file
diff --git a/secrets/secrets.nix b/secrets/secrets.nix
index 12bebb9..2e1c390 100644
--- a/secrets/secrets.nix
+++ b/secrets/secrets.nix
@@ -46,6 +46,7 @@ in
   "matrix-synapse-signing-key.age".publicKeys = nachtigallKeys ++ baseKeys;
   "matrix-synapse-secret-config.yaml.age".publicKeys = nachtigallKeys ++ baseKeys;
   "matrix-synapse-sliding-sync-secret.age".publicKeys = nachtigallKeys ++ baseKeys;
+  "matrix-mjolnir-password.age".publicKeys = nachtigallKeys ++ baseKeys;
 
   "nextcloud-secrets.age".publicKeys = nachtigallKeys ++ baseKeys;
   "nextcloud-admin-pass.age".publicKeys = nachtigallKeys ++ baseKeys;