···165165166166- [PDS](https://github.com/bluesky-social/pds), Personal Data Server for [bsky](https://bsky.social/). Available as [services.pds](option.html#opt-services.pds).
167167168168+- [Anubis](https://github.com/TecharoHQ/anubis), a scraper defense software. Available as [services.anubis](options.html#opt-services.anubis).
169169+168170- [synapse-auto-compressor](https://github.com/matrix-org/rust-synapse-compress-state?tab=readme-ov-file#automated-tool-synapse_auto_compressor), a rust-based matrix-synapse state compressor for postgresql. Available as [services.synapse-auto-compressor](#opt-services.synapse-auto-compressor.enable).
169171170172- [mqtt-exporter](https://github.com/kpetremann/mqtt-exporter/), a Prometheus exporter for exposing messages from MQTT. Available as [services.prometheus.exporters.mqtt](#opt-services.prometheus.exporters.mqtt.enable).
···11+# Anubis {#module-services-anubis}
22+33+[Anubis](https://anubis.techaro.lol) is a scraper defense software that blocks AI scrapers. It is designed to sit
44+between a reverse proxy and the service to be protected.
55+66+## Quickstart {#module-services-anubis-quickstart}
77+88+This module is designed to use Unix domain sockets as the socket paths can be automatically configured for multiple
99+instances, but TCP sockets are also supported.
1010+1111+A minimal configuration with [nginx](#opt-services.nginx.enable) may look like the following:
1212+1313+```nix
1414+{ config, ... }: {
1515+ services.anubis.instances.default.settings.TARGET = "http://localhost:8000";
1616+1717+ # required due to unix socket permissions
1818+ users.users.nginx.extraGroups = [ config.users.groups.anubis.name ];
1919+ services.nginx.virtualHosts."example.com" = {
2020+ locations = {
2121+ "/".proxyPass = "http://unix:${config.services.anubis.instances.default.settings.BIND}";
2222+ };
2323+ };
2424+}
2525+```
2626+2727+If Unix domain sockets are not needed or desired, this module supports operating with only TCP sockets.
2828+2929+```nix
3030+{
3131+ services.anubis = {
3232+ instances.default = {
3333+ settings = {
3434+ TARGET = "http://localhost:8080";
3535+ BIND = ":9000";
3636+ BIND_NETWORK = "tcp";
3737+ METRICS_BIND = "127.0.0.1:9001";
3838+ METRICS_BIND_NETWORK = "tcp";
3939+ };
4040+ };
4141+ };
4242+}
4343+```
4444+4545+## Configuration {#module-services-anubis-configuration}
4646+4747+It is possible to configure default settings for all instances of Anubis, via {option}`services.anubis.defaultOptions`.
4848+4949+```nix
5050+{
5151+ services.anubis.defaultOptions = {
5252+ botPolicy = { dnsbl = false; };
5353+ settings.DIFFICULTY = 3;
5454+ };
5555+}
5656+```
5757+5858+Note that at the moment, a custom bot policy is not merged with the baked-in one. That means to only override a setting
5959+like `dnsbl`, copying the entire bot policy is required. Check
6060+[the upstream repository](https://github.com/TecharoHQ/anubis/blob/1509b06cb921aff842e71fbb6636646be6ed5b46/cmd/anubis/botPolicies.json)
6161+for the policy.
+314
nixos/modules/services/networking/anubis.nix
···11+{
22+ config,
33+ lib,
44+ pkgs,
55+ ...
66+}:
77+let
88+ inherit (lib) types;
99+ jsonFormat = pkgs.formats.json { };
1010+1111+ cfg = config.services.anubis;
1212+ enabledInstances = lib.filterAttrs (_: conf: conf.enable) cfg.instances;
1313+ instanceName = name: if name == "" then "anubis" else "anubis-${name}";
1414+1515+ commonSubmodule =
1616+ isDefault:
1717+ let
1818+ mkDefaultOption =
1919+ path: opts:
2020+ lib.mkOption (
2121+ opts
2222+ // lib.optionalAttrs (!isDefault && opts ? default) {
2323+ default =
2424+ lib.attrByPath (lib.splitString "." path)
2525+ (throw "This is a bug in the Anubis module. Please report this as an issue.")
2626+ cfg.defaultOptions;
2727+ defaultText = lib.literalExpression "config.services.anubis.defaultOptions.${path}";
2828+ }
2929+ );
3030+ in
3131+ { name, ... }:
3232+ {
3333+ options = {
3434+ enable = lib.mkEnableOption "this instance of Anubis" // {
3535+ default = true;
3636+ };
3737+ user = mkDefaultOption "user" {
3838+ default = "anubis";
3939+ description = ''
4040+ The user under which Anubis is run.
4141+4242+ This module utilizes systemd's DynamicUser feature. See the corresponding section in
4343+ {manpage}`systemd.exec(5)` for more details.
4444+ '';
4545+ type = types.str;
4646+ };
4747+ group = mkDefaultOption "group" {
4848+ default = "anubis";
4949+ description = ''
5050+ The group under which Anubis is run.
5151+5252+ This module utilizes systemd's DynamicUser feature. See the corresponding section in
5353+ {manpage}`systemd.exec(5)` for more details.
5454+ '';
5555+ type = types.str;
5656+ };
5757+5858+ botPolicy = lib.mkOption {
5959+ default = null;
6060+ description = ''
6161+ Anubis policy configuration in Nix syntax. Set to `null` to use the baked-in policy which should be
6262+ sufficient for most use-cases.
6363+6464+ This option has no effect if `settings.POLICY_FNAME` is set to a different value, which is useful for
6565+ importing an existing configuration.
6666+6767+ See [the documentation](https://anubis.techaro.lol/docs/admin/policies) for details.
6868+ '';
6969+ type = types.nullOr jsonFormat.type;
7070+ };
7171+7272+ extraFlags = mkDefaultOption "extraFlags" {
7373+ default = [ ];
7474+ description = "A list of extra flags to be passed to Anubis.";
7575+ example = [ "-metrics-bind \"\"" ];
7676+ type = types.listOf types.str;
7777+ };
7878+7979+ settings = lib.mkOption {
8080+ default = { };
8181+ description = ''
8282+ Freeform configuration via environment variables for Anubis.
8383+8484+ See [the documentation](https://anubis.techaro.lol/docs/admin/installation) for a complete list of
8585+ available environment variables.
8686+ '';
8787+ type = types.submodule [
8888+ {
8989+ freeformType =
9090+ with types;
9191+ attrsOf (
9292+ nullOr (oneOf [
9393+ str
9494+ int
9595+ bool
9696+ ])
9797+ );
9898+9999+ options = {
100100+ # BIND and METRICS_BIND are defined in instance specific options, since global defaults don't make sense
101101+ BIND_NETWORK = mkDefaultOption "settings.BIND_NETWORK" {
102102+ default = "unix";
103103+ description = ''
104104+ The network family that Anubis should bind to.
105105+106106+ Accepts anything supported by Go's [`net.Listen`](https://pkg.go.dev/net#Listen).
107107+108108+ Common values are `tcp` and `unix`.
109109+ '';
110110+ example = "tcp";
111111+ type = types.str;
112112+ };
113113+ METRICS_BIND_NETWORK = mkDefaultOption "settings.METRICS_BIND_NETWORK" {
114114+ default = "unix";
115115+ description = ''
116116+ The network family that the metrics server should bind to.
117117+118118+ Accepts anything supported by Go's [`net.Listen`](https://pkg.go.dev/net#Listen).
119119+120120+ Common values are `tcp` and `unix`.
121121+ '';
122122+ example = "tcp";
123123+ type = types.str;
124124+ };
125125+ SOCKET_MODE = mkDefaultOption "settings.SOCKET_MODE" {
126126+ default = "0770";
127127+ description = "The permissions on the Unix domain sockets created.";
128128+ example = "0700";
129129+ type = types.str;
130130+ };
131131+ DIFFICULTY = mkDefaultOption "settings.DIFFICULTY" {
132132+ default = 4;
133133+ description = ''
134134+ The difficulty required for clients to solve the challenge.
135135+136136+ Currently, this means the amount of leading zeros in a successful response.
137137+ '';
138138+ type = types.int;
139139+ example = 5;
140140+ };
141141+ SERVE_ROBOTS_TXT = mkDefaultOption "settings.SERVE_ROBOTS_TXT" {
142142+ default = false;
143143+ description = ''
144144+ Whether to serve a default robots.txt that denies access to common AI bots by name and all other
145145+ bots by wildcard.
146146+ '';
147147+ type = types.bool;
148148+ };
149149+150150+ # generated by default
151151+ POLICY_FNAME = mkDefaultOption "settings.POLICY_FNAME" {
152152+ default = null;
153153+ description = ''
154154+ The bot policy file to use. Leave this as `null` to respect the value set in
155155+ {option}`services.anubis.instances.<name>.botPolicy`.
156156+ '';
157157+ type = types.nullOr types.path;
158158+ };
159159+ };
160160+ }
161161+ (lib.optionalAttrs (!isDefault) (instanceSpecificOptions name))
162162+ ];
163163+ };
164164+ };
165165+ };
166166+167167+ instanceSpecificOptions = name: {
168168+ options = {
169169+ # see other options above
170170+ BIND = lib.mkOption {
171171+ default = "/run/anubis/${instanceName name}.sock";
172172+ description = ''
173173+ The address that Anubis listens to. See Go's [`net.Listen`](https://pkg.go.dev/net#Listen) for syntax.
174174+175175+ Defaults to Unix domain sockets. To use TCP sockets, set this to a TCP address and `BIND_NETWORK` to `"tcp"`.
176176+ '';
177177+ example = ":8080";
178178+ type = types.str;
179179+ };
180180+ METRICS_BIND = lib.mkOption {
181181+ default = "/run/anubis/${instanceName name}-metrics.sock";
182182+ description = ''
183183+ The address Anubis' metrics server listens to. See Go's [`net.Listen`](https://pkg.go.dev/net#Listen) for
184184+ syntax.
185185+186186+ The metrics server is enabled by default and may be disabled. However, due to implementation details, this is
187187+ only possible by setting a command line flag. See {option}`services.anubis.defaultOptions.extraFlags` for an
188188+ example.
189189+190190+ Defaults to Unix domain sockets. To use TCP sockets, set this to a TCP address and `METRICS_BIND_NETWORK` to
191191+ `"tcp"`.
192192+ '';
193193+ example = "127.0.0.1:8081";
194194+ type = types.str;
195195+ };
196196+ TARGET = lib.mkOption {
197197+ description = ''
198198+ The reverse proxy target that Anubis is protecting. This is a required option.
199199+200200+ The usage of Unix domain sockets is supported by the following syntax: `unix:///path/to/socket.sock`.
201201+ '';
202202+ example = "http://127.0.0.1:8000";
203203+ type = types.str;
204204+ };
205205+ };
206206+ };
207207+in
208208+{
209209+ options.services.anubis = {
210210+ package = lib.mkPackageOption pkgs "anubis" { };
211211+212212+ defaultOptions = lib.mkOption {
213213+ default = { };
214214+ description = "Default options for all instances of Anubis.";
215215+ type = types.submodule (commonSubmodule true);
216216+ };
217217+218218+ instances = lib.mkOption {
219219+ default = { };
220220+ description = ''
221221+ An attribute set of Anubis instances.
222222+223223+ The attribute name may be an empty string, in which case the `-<name>` suffix is not added to the service name
224224+ and socket paths.
225225+ '';
226226+ type = types.attrsOf (types.submodule (commonSubmodule false));
227227+ };
228228+ };
229229+230230+ config = lib.mkIf (enabledInstances != { }) {
231231+ users.users = lib.mkIf (cfg.defaultOptions.user == "anubis") {
232232+ anubis = {
233233+ isSystemUser = true;
234234+ group = cfg.defaultOptions.group;
235235+ };
236236+ };
237237+238238+ users.groups = lib.mkIf (cfg.defaultOptions.group == "anubis") {
239239+ anubis = { };
240240+ };
241241+242242+ systemd.services = lib.mapAttrs' (
243243+ name: instance:
244244+ lib.nameValuePair "${instanceName name}" {
245245+ description = "Anubis (${if name == "" then "default" else name} instance)";
246246+ wantedBy = [ "multi-user.target" ];
247247+ after = [ "network-online.target" ];
248248+ wants = [ "network-online.target" ];
249249+250250+ environment = lib.mapAttrs (lib.const (lib.generators.mkValueStringDefault { })) (
251251+ lib.filterAttrs (_: v: v != null) instance.settings
252252+ );
253253+254254+ serviceConfig = {
255255+ User = instance.user;
256256+ Group = instance.group;
257257+ DynamicUser = true;
258258+259259+ ExecStart = lib.concatStringsSep " " (
260260+ (lib.singleton (lib.getExe cfg.package)) ++ instance.extraFlags
261261+ );
262262+ RuntimeDirectory =
263263+ if
264264+ lib.any (lib.hasPrefix "/run/anubis") (
265265+ with instance.settings;
266266+ [
267267+ BIND
268268+ METRICS_BIND
269269+ ]
270270+ )
271271+ then
272272+ "anubis"
273273+ else
274274+ null;
275275+276276+ # hardening
277277+ NoNewPrivileges = true;
278278+ CapabilityBoundingSet = null;
279279+ SystemCallFilter = [
280280+ "@system-service"
281281+ "~@privileged"
282282+ ];
283283+ SystemCallArchitectures = "native";
284284+ MemoryDenyWriteExecute = true;
285285+286286+ PrivateUsers = true;
287287+ PrivateTmp = true;
288288+ PrivateDevices = true;
289289+ ProtectHome = true;
290290+ ProtectClock = true;
291291+ ProtectHostname = true;
292292+ ProtectKernelLogs = true;
293293+ ProtectKernelModules = true;
294294+ ProtectKernelTunables = true;
295295+ ProtectProc = "invisible";
296296+ ProtectSystem = "strict";
297297+ ProtectControlGroups = "strict";
298298+ LockPersonality = true;
299299+ RestrictRealtime = true;
300300+ RestrictSUIDSGID = true;
301301+ RestrictNamespaces = true;
302302+ RestrictAddressFamilies = [
303303+ "AF_UNIX"
304304+ "AF_INET"
305305+ "AF_INET6"
306306+ ];
307307+ };
308308+ }
309309+ ) enabledInstances;
310310+ };
311311+312312+ meta.maintainers = with lib.maintainers; [ soopyc ];
313313+ meta.doc = ./anubis.md;
314314+}