lol

nixos/anubis: init module (#392018)

authored by

Adam C. Stephens and committed by
GitHub
f2200ca6 72d4fb8c

+486
+9
nixos/doc/manual/redirects.json
··· 2 2 "book-nixos-manual": [ 3 3 "index.html#book-nixos-manual" 4 4 ], 5 + "module-services-anubis": [ 6 + "index.html#module-services-anubis" 7 + ], 8 + "module-services-anubis-configuration": [ 9 + "index.html#module-services-anubis-configuration" 10 + ], 11 + "module-services-anubis-quickstart": [ 12 + "index.html#module-services-anubis-quickstart" 13 + ], 5 14 "module-services-crab-hole": [ 6 15 "index.html#module-services-crab-hole" 7 16 ],
+2
nixos/doc/manual/release-notes/rl-2505.section.md
··· 165 165 166 166 - [PDS](https://github.com/bluesky-social/pds), Personal Data Server for [bsky](https://bsky.social/). Available as [services.pds](option.html#opt-services.pds). 167 167 168 + - [Anubis](https://github.com/TecharoHQ/anubis), a scraper defense software. Available as [services.anubis](options.html#opt-services.anubis). 169 + 168 170 - [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). 169 171 170 172 - [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).
+1
nixos/modules/module-list.nix
··· 1048 1048 ./services/networking/adguardhome.nix 1049 1049 ./services/networking/alice-lg.nix 1050 1050 ./services/networking/amuled.nix 1051 + ./services/networking/anubis.nix 1051 1052 ./services/networking/aria2.nix 1052 1053 ./services/networking/asterisk.nix 1053 1054 ./services/networking/atftpd.nix
+61
nixos/modules/services/networking/anubis.md
··· 1 + # Anubis {#module-services-anubis} 2 + 3 + [Anubis](https://anubis.techaro.lol) is a scraper defense software that blocks AI scrapers. It is designed to sit 4 + between a reverse proxy and the service to be protected. 5 + 6 + ## Quickstart {#module-services-anubis-quickstart} 7 + 8 + This module is designed to use Unix domain sockets as the socket paths can be automatically configured for multiple 9 + instances, but TCP sockets are also supported. 10 + 11 + A minimal configuration with [nginx](#opt-services.nginx.enable) may look like the following: 12 + 13 + ```nix 14 + { config, ... }: { 15 + services.anubis.instances.default.settings.TARGET = "http://localhost:8000"; 16 + 17 + # required due to unix socket permissions 18 + users.users.nginx.extraGroups = [ config.users.groups.anubis.name ]; 19 + services.nginx.virtualHosts."example.com" = { 20 + locations = { 21 + "/".proxyPass = "http://unix:${config.services.anubis.instances.default.settings.BIND}"; 22 + }; 23 + }; 24 + } 25 + ``` 26 + 27 + If Unix domain sockets are not needed or desired, this module supports operating with only TCP sockets. 28 + 29 + ```nix 30 + { 31 + services.anubis = { 32 + instances.default = { 33 + settings = { 34 + TARGET = "http://localhost:8080"; 35 + BIND = ":9000"; 36 + BIND_NETWORK = "tcp"; 37 + METRICS_BIND = "127.0.0.1:9001"; 38 + METRICS_BIND_NETWORK = "tcp"; 39 + }; 40 + }; 41 + }; 42 + } 43 + ``` 44 + 45 + ## Configuration {#module-services-anubis-configuration} 46 + 47 + It is possible to configure default settings for all instances of Anubis, via {option}`services.anubis.defaultOptions`. 48 + 49 + ```nix 50 + { 51 + services.anubis.defaultOptions = { 52 + botPolicy = { dnsbl = false; }; 53 + settings.DIFFICULTY = 3; 54 + }; 55 + } 56 + ``` 57 + 58 + Note that at the moment, a custom bot policy is not merged with the baked-in one. That means to only override a setting 59 + like `dnsbl`, copying the entire bot policy is required. Check 60 + [the upstream repository](https://github.com/TecharoHQ/anubis/blob/1509b06cb921aff842e71fbb6636646be6ed5b46/cmd/anubis/botPolicies.json) 61 + for the policy.
+314
nixos/modules/services/networking/anubis.nix
··· 1 + { 2 + config, 3 + lib, 4 + pkgs, 5 + ... 6 + }: 7 + let 8 + inherit (lib) types; 9 + jsonFormat = pkgs.formats.json { }; 10 + 11 + cfg = config.services.anubis; 12 + enabledInstances = lib.filterAttrs (_: conf: conf.enable) cfg.instances; 13 + instanceName = name: if name == "" then "anubis" else "anubis-${name}"; 14 + 15 + commonSubmodule = 16 + isDefault: 17 + let 18 + mkDefaultOption = 19 + path: opts: 20 + lib.mkOption ( 21 + opts 22 + // lib.optionalAttrs (!isDefault && opts ? default) { 23 + default = 24 + lib.attrByPath (lib.splitString "." path) 25 + (throw "This is a bug in the Anubis module. Please report this as an issue.") 26 + cfg.defaultOptions; 27 + defaultText = lib.literalExpression "config.services.anubis.defaultOptions.${path}"; 28 + } 29 + ); 30 + in 31 + { name, ... }: 32 + { 33 + options = { 34 + enable = lib.mkEnableOption "this instance of Anubis" // { 35 + default = true; 36 + }; 37 + user = mkDefaultOption "user" { 38 + default = "anubis"; 39 + description = '' 40 + The user under which Anubis is run. 41 + 42 + This module utilizes systemd's DynamicUser feature. See the corresponding section in 43 + {manpage}`systemd.exec(5)` for more details. 44 + ''; 45 + type = types.str; 46 + }; 47 + group = mkDefaultOption "group" { 48 + default = "anubis"; 49 + description = '' 50 + The group under which Anubis is run. 51 + 52 + This module utilizes systemd's DynamicUser feature. See the corresponding section in 53 + {manpage}`systemd.exec(5)` for more details. 54 + ''; 55 + type = types.str; 56 + }; 57 + 58 + botPolicy = lib.mkOption { 59 + default = null; 60 + description = '' 61 + Anubis policy configuration in Nix syntax. Set to `null` to use the baked-in policy which should be 62 + sufficient for most use-cases. 63 + 64 + This option has no effect if `settings.POLICY_FNAME` is set to a different value, which is useful for 65 + importing an existing configuration. 66 + 67 + See [the documentation](https://anubis.techaro.lol/docs/admin/policies) for details. 68 + ''; 69 + type = types.nullOr jsonFormat.type; 70 + }; 71 + 72 + extraFlags = mkDefaultOption "extraFlags" { 73 + default = [ ]; 74 + description = "A list of extra flags to be passed to Anubis."; 75 + example = [ "-metrics-bind \"\"" ]; 76 + type = types.listOf types.str; 77 + }; 78 + 79 + settings = lib.mkOption { 80 + default = { }; 81 + description = '' 82 + Freeform configuration via environment variables for Anubis. 83 + 84 + See [the documentation](https://anubis.techaro.lol/docs/admin/installation) for a complete list of 85 + available environment variables. 86 + ''; 87 + type = types.submodule [ 88 + { 89 + freeformType = 90 + with types; 91 + attrsOf ( 92 + nullOr (oneOf [ 93 + str 94 + int 95 + bool 96 + ]) 97 + ); 98 + 99 + options = { 100 + # BIND and METRICS_BIND are defined in instance specific options, since global defaults don't make sense 101 + BIND_NETWORK = mkDefaultOption "settings.BIND_NETWORK" { 102 + default = "unix"; 103 + description = '' 104 + The network family that Anubis should bind to. 105 + 106 + Accepts anything supported by Go's [`net.Listen`](https://pkg.go.dev/net#Listen). 107 + 108 + Common values are `tcp` and `unix`. 109 + ''; 110 + example = "tcp"; 111 + type = types.str; 112 + }; 113 + METRICS_BIND_NETWORK = mkDefaultOption "settings.METRICS_BIND_NETWORK" { 114 + default = "unix"; 115 + description = '' 116 + The network family that the metrics server should bind to. 117 + 118 + Accepts anything supported by Go's [`net.Listen`](https://pkg.go.dev/net#Listen). 119 + 120 + Common values are `tcp` and `unix`. 121 + ''; 122 + example = "tcp"; 123 + type = types.str; 124 + }; 125 + SOCKET_MODE = mkDefaultOption "settings.SOCKET_MODE" { 126 + default = "0770"; 127 + description = "The permissions on the Unix domain sockets created."; 128 + example = "0700"; 129 + type = types.str; 130 + }; 131 + DIFFICULTY = mkDefaultOption "settings.DIFFICULTY" { 132 + default = 4; 133 + description = '' 134 + The difficulty required for clients to solve the challenge. 135 + 136 + Currently, this means the amount of leading zeros in a successful response. 137 + ''; 138 + type = types.int; 139 + example = 5; 140 + }; 141 + SERVE_ROBOTS_TXT = mkDefaultOption "settings.SERVE_ROBOTS_TXT" { 142 + default = false; 143 + description = '' 144 + Whether to serve a default robots.txt that denies access to common AI bots by name and all other 145 + bots by wildcard. 146 + ''; 147 + type = types.bool; 148 + }; 149 + 150 + # generated by default 151 + POLICY_FNAME = mkDefaultOption "settings.POLICY_FNAME" { 152 + default = null; 153 + description = '' 154 + The bot policy file to use. Leave this as `null` to respect the value set in 155 + {option}`services.anubis.instances.<name>.botPolicy`. 156 + ''; 157 + type = types.nullOr types.path; 158 + }; 159 + }; 160 + } 161 + (lib.optionalAttrs (!isDefault) (instanceSpecificOptions name)) 162 + ]; 163 + }; 164 + }; 165 + }; 166 + 167 + instanceSpecificOptions = name: { 168 + options = { 169 + # see other options above 170 + BIND = lib.mkOption { 171 + default = "/run/anubis/${instanceName name}.sock"; 172 + description = '' 173 + The address that Anubis listens to. See Go's [`net.Listen`](https://pkg.go.dev/net#Listen) for syntax. 174 + 175 + Defaults to Unix domain sockets. To use TCP sockets, set this to a TCP address and `BIND_NETWORK` to `"tcp"`. 176 + ''; 177 + example = ":8080"; 178 + type = types.str; 179 + }; 180 + METRICS_BIND = lib.mkOption { 181 + default = "/run/anubis/${instanceName name}-metrics.sock"; 182 + description = '' 183 + The address Anubis' metrics server listens to. See Go's [`net.Listen`](https://pkg.go.dev/net#Listen) for 184 + syntax. 185 + 186 + The metrics server is enabled by default and may be disabled. However, due to implementation details, this is 187 + only possible by setting a command line flag. See {option}`services.anubis.defaultOptions.extraFlags` for an 188 + example. 189 + 190 + Defaults to Unix domain sockets. To use TCP sockets, set this to a TCP address and `METRICS_BIND_NETWORK` to 191 + `"tcp"`. 192 + ''; 193 + example = "127.0.0.1:8081"; 194 + type = types.str; 195 + }; 196 + TARGET = lib.mkOption { 197 + description = '' 198 + The reverse proxy target that Anubis is protecting. This is a required option. 199 + 200 + The usage of Unix domain sockets is supported by the following syntax: `unix:///path/to/socket.sock`. 201 + ''; 202 + example = "http://127.0.0.1:8000"; 203 + type = types.str; 204 + }; 205 + }; 206 + }; 207 + in 208 + { 209 + options.services.anubis = { 210 + package = lib.mkPackageOption pkgs "anubis" { }; 211 + 212 + defaultOptions = lib.mkOption { 213 + default = { }; 214 + description = "Default options for all instances of Anubis."; 215 + type = types.submodule (commonSubmodule true); 216 + }; 217 + 218 + instances = lib.mkOption { 219 + default = { }; 220 + description = '' 221 + An attribute set of Anubis instances. 222 + 223 + The attribute name may be an empty string, in which case the `-<name>` suffix is not added to the service name 224 + and socket paths. 225 + ''; 226 + type = types.attrsOf (types.submodule (commonSubmodule false)); 227 + }; 228 + }; 229 + 230 + config = lib.mkIf (enabledInstances != { }) { 231 + users.users = lib.mkIf (cfg.defaultOptions.user == "anubis") { 232 + anubis = { 233 + isSystemUser = true; 234 + group = cfg.defaultOptions.group; 235 + }; 236 + }; 237 + 238 + users.groups = lib.mkIf (cfg.defaultOptions.group == "anubis") { 239 + anubis = { }; 240 + }; 241 + 242 + systemd.services = lib.mapAttrs' ( 243 + name: instance: 244 + lib.nameValuePair "${instanceName name}" { 245 + description = "Anubis (${if name == "" then "default" else name} instance)"; 246 + wantedBy = [ "multi-user.target" ]; 247 + after = [ "network-online.target" ]; 248 + wants = [ "network-online.target" ]; 249 + 250 + environment = lib.mapAttrs (lib.const (lib.generators.mkValueStringDefault { })) ( 251 + lib.filterAttrs (_: v: v != null) instance.settings 252 + ); 253 + 254 + serviceConfig = { 255 + User = instance.user; 256 + Group = instance.group; 257 + DynamicUser = true; 258 + 259 + ExecStart = lib.concatStringsSep " " ( 260 + (lib.singleton (lib.getExe cfg.package)) ++ instance.extraFlags 261 + ); 262 + RuntimeDirectory = 263 + if 264 + lib.any (lib.hasPrefix "/run/anubis") ( 265 + with instance.settings; 266 + [ 267 + BIND 268 + METRICS_BIND 269 + ] 270 + ) 271 + then 272 + "anubis" 273 + else 274 + null; 275 + 276 + # hardening 277 + NoNewPrivileges = true; 278 + CapabilityBoundingSet = null; 279 + SystemCallFilter = [ 280 + "@system-service" 281 + "~@privileged" 282 + ]; 283 + SystemCallArchitectures = "native"; 284 + MemoryDenyWriteExecute = true; 285 + 286 + PrivateUsers = true; 287 + PrivateTmp = true; 288 + PrivateDevices = true; 289 + ProtectHome = true; 290 + ProtectClock = true; 291 + ProtectHostname = true; 292 + ProtectKernelLogs = true; 293 + ProtectKernelModules = true; 294 + ProtectKernelTunables = true; 295 + ProtectProc = "invisible"; 296 + ProtectSystem = "strict"; 297 + ProtectControlGroups = "strict"; 298 + LockPersonality = true; 299 + RestrictRealtime = true; 300 + RestrictSUIDSGID = true; 301 + RestrictNamespaces = true; 302 + RestrictAddressFamilies = [ 303 + "AF_UNIX" 304 + "AF_INET" 305 + "AF_INET6" 306 + ]; 307 + }; 308 + } 309 + ) enabledInstances; 310 + }; 311 + 312 + meta.maintainers = with lib.maintainers; [ soopyc ]; 313 + meta.doc = ./anubis.md; 314 + }
+1
nixos/tests/all-tests.nix
··· 198 198 amd-sev = runTest ./amd-sev.nix; 199 199 angie-api = runTest ./angie-api.nix; 200 200 anki-sync-server = runTest ./anki-sync-server.nix; 201 + anubis = runTest ./anubis.nix; 201 202 anuko-time-tracker = runTest ./anuko-time-tracker.nix; 202 203 apcupsd = runTest ./apcupsd.nix; 203 204 apfs = runTest ./apfs.nix;
+98
nixos/tests/anubis.nix
··· 1 + { lib, ... }: 2 + { 3 + name = "anubis"; 4 + meta.maintainers = [ lib.maintainers.soopyc ]; 5 + 6 + nodes.machine = 7 + { 8 + config, 9 + pkgs, 10 + ... 11 + }: 12 + { 13 + services.anubis.instances = { 14 + "".settings.TARGET = "http://localhost:8080"; 15 + 16 + "tcp" = { 17 + user = "anubis-tcp"; 18 + group = "anubis-tcp"; 19 + settings = { 20 + TARGET = "http://localhost:8080"; 21 + BIND = ":9000"; 22 + BIND_NETWORK = "tcp"; 23 + METRICS_BIND = ":9001"; 24 + METRICS_BIND_NETWORK = "tcp"; 25 + }; 26 + }; 27 + 28 + "unix-upstream" = { 29 + group = "nginx"; 30 + settings.TARGET = "unix:///run/nginx/nginx.sock"; 31 + }; 32 + }; 33 + 34 + # support 35 + users.users.nginx.extraGroups = [ config.users.groups.anubis.name ]; 36 + services.nginx = { 37 + enable = true; 38 + recommendedProxySettings = true; 39 + virtualHosts."basic.localhost".locations = { 40 + "/".proxyPass = "http://unix:${config.services.anubis.instances."".settings.BIND}"; 41 + "/metrics".proxyPass = "http://unix:${config.services.anubis.instances."".settings.METRICS_BIND}"; 42 + }; 43 + 44 + virtualHosts."tcp.localhost".locations = { 45 + "/".proxyPass = "http://localhost:9000"; 46 + "/metrics".proxyPass = "http://localhost:9001"; 47 + }; 48 + 49 + virtualHosts."unix.localhost".locations = { 50 + "/".proxyPass = "http://unix:${config.services.anubis.instances.unix-upstream.settings.BIND}"; 51 + }; 52 + 53 + # emulate an upstream with nginx, listening on tcp and unix sockets. 54 + virtualHosts."upstream.localhost" = { 55 + default = true; # make nginx match this vhost for `localhost` 56 + listen = [ 57 + { addr = "unix:/run/nginx/nginx.sock"; } 58 + { 59 + addr = "localhost"; 60 + port = 8080; 61 + } 62 + ]; 63 + locations."/" = { 64 + tryFiles = "$uri $uri/index.html =404"; 65 + root = pkgs.runCommand "anubis-test-upstream" { } '' 66 + mkdir $out 67 + echo "it works" >> $out/index.html 68 + ''; 69 + }; 70 + }; 71 + }; 72 + }; 73 + 74 + testScript = '' 75 + for unit in ["nginx", "anubis", "anubis-tcp", "anubis-unix-upstream"]: 76 + machine.wait_for_unit(unit + ".service") 77 + 78 + for port in [9000, 9001]: 79 + machine.wait_for_open_port(port) 80 + 81 + for instance in ["anubis", "anubis-unix-upstream"]: 82 + machine.wait_for_open_unix_socket(f"/run/anubis/{instance}.sock") 83 + machine.wait_for_open_unix_socket(f"/run/anubis/{instance}-metrics.sock") 84 + 85 + # Default unix socket mode 86 + machine.succeed('curl -f http://basic.localhost | grep "it works"') 87 + machine.succeed('curl -f http://basic.localhost -H "User-Agent: Mozilla" | grep anubis') 88 + machine.succeed('curl -f http://basic.localhost/metrics | grep anubis_challenges_issued') 89 + machine.succeed('curl -f -X POST http://basic.localhost/.within.website/x/cmd/anubis/api/make-challenge | grep challenge') 90 + 91 + # TCP mode 92 + machine.succeed('curl -f http://tcp.localhost -H "User-Agent: Mozilla" | grep anubis') 93 + machine.succeed('curl -f http://tcp.localhost/metrics | grep anubis_challenges_issued') 94 + 95 + # Upstream is a unix socket mode 96 + machine.succeed('curl -f http://unix.localhost/index.html | grep "it works"') 97 + ''; 98 + }