at 25.11-pre 11 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7let 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 DIFFICULTY = mkDefaultOption "settings.DIFFICULTY" { 126 default = 4; 127 description = '' 128 The difficulty required for clients to solve the challenge. 129 130 Currently, this means the amount of leading zeros in a successful response. 131 ''; 132 type = types.int; 133 example = 5; 134 }; 135 SERVE_ROBOTS_TXT = mkDefaultOption "settings.SERVE_ROBOTS_TXT" { 136 default = false; 137 description = '' 138 Whether to serve a default robots.txt that denies access to common AI bots by name and all other 139 bots by wildcard. 140 ''; 141 type = types.bool; 142 }; 143 OG_PASSTHROUGH = mkDefaultOption "settings.OG_PASSTHROUGH" { 144 default = false; 145 description = '' 146 Whether to enable Open Graph tag passthrough. 147 148 This enables social previews of resources protected by 149 Anubis without having to exempt each scraper individually. 150 ''; 151 type = types.bool; 152 }; 153 WEBMASTER_EMAIL = mkDefaultOption "settings.WEBMASTER_EMAIL" { 154 default = null; 155 description = '' 156 If set, shows a contact email address when rendering error pages. 157 158 This email address will be how users can get in contact with administrators. 159 ''; 160 example = "alice@example.com"; 161 type = types.nullOr types.str; 162 }; 163 164 # generated by default 165 POLICY_FNAME = mkDefaultOption "settings.POLICY_FNAME" { 166 default = null; 167 description = '' 168 The bot policy file to use. Leave this as `null` to respect the value set in 169 {option}`services.anubis.instances.<name>.botPolicy`. 170 ''; 171 type = types.nullOr types.path; 172 }; 173 }; 174 } 175 (lib.optionalAttrs (!isDefault) (instanceSpecificOptions name)) 176 ]; 177 }; 178 }; 179 }; 180 181 instanceSpecificOptions = name: { 182 options = { 183 # see other options above 184 BIND = lib.mkOption { 185 default = "/run/anubis/${instanceName name}.sock"; 186 description = '' 187 The address that Anubis listens to. See Go's [`net.Listen`](https://pkg.go.dev/net#Listen) for syntax. 188 189 Defaults to Unix domain sockets. To use TCP sockets, set this to a TCP address and `BIND_NETWORK` to `"tcp"`. 190 ''; 191 example = ":8080"; 192 type = types.str; 193 }; 194 METRICS_BIND = lib.mkOption { 195 default = "/run/anubis/${instanceName name}-metrics.sock"; 196 description = '' 197 The address Anubis' metrics server listens to. See Go's [`net.Listen`](https://pkg.go.dev/net#Listen) for 198 syntax. 199 200 The metrics server is enabled by default and may be disabled. However, due to implementation details, this is 201 only possible by setting a command line flag. See {option}`services.anubis.defaultOptions.extraFlags` for an 202 example. 203 204 Defaults to Unix domain sockets. To use TCP sockets, set this to a TCP address and `METRICS_BIND_NETWORK` to 205 `"tcp"`. 206 ''; 207 example = "127.0.0.1:8081"; 208 type = types.str; 209 }; 210 TARGET = lib.mkOption { 211 description = '' 212 The reverse proxy target that Anubis is protecting. This is a required option. 213 214 The usage of Unix domain sockets is supported by the following syntax: `unix:///path/to/socket.sock`. 215 ''; 216 example = "http://127.0.0.1:8000"; 217 type = types.str; 218 }; 219 }; 220 }; 221in 222{ 223 options.services.anubis = { 224 package = lib.mkPackageOption pkgs "anubis" { }; 225 226 defaultOptions = lib.mkOption { 227 default = { }; 228 description = "Default options for all instances of Anubis."; 229 type = types.submodule (commonSubmodule true); 230 }; 231 232 instances = lib.mkOption { 233 default = { }; 234 description = '' 235 An attribute set of Anubis instances. 236 237 The attribute name may be an empty string, in which case the `-<name>` suffix is not added to the service name 238 and socket paths. 239 ''; 240 type = types.attrsOf (types.submodule (commonSubmodule false)); 241 242 # Merge defaultOptions into each instance 243 apply = lib.mapAttrs (_: lib.recursiveUpdate cfg.defaultOptions); 244 }; 245 }; 246 247 config = lib.mkIf (enabledInstances != { }) { 248 users.users = lib.mkIf (cfg.defaultOptions.user == "anubis") { 249 anubis = { 250 isSystemUser = true; 251 group = cfg.defaultOptions.group; 252 }; 253 }; 254 255 users.groups = lib.mkIf (cfg.defaultOptions.group == "anubis") { 256 anubis = { }; 257 }; 258 259 systemd.services = lib.mapAttrs' ( 260 name: instance: 261 lib.nameValuePair "${instanceName name}" { 262 description = "Anubis (${if name == "" then "default" else name} instance)"; 263 wantedBy = [ "multi-user.target" ]; 264 after = [ "network-online.target" ]; 265 wants = [ "network-online.target" ]; 266 267 environment = lib.mapAttrs (lib.const (lib.generators.mkValueStringDefault { })) ( 268 lib.filterAttrs (_: v: v != null) instance.settings 269 ); 270 271 serviceConfig = { 272 User = instance.user; 273 Group = instance.group; 274 DynamicUser = true; 275 276 ExecStart = lib.concatStringsSep " " ( 277 (lib.singleton (lib.getExe cfg.package)) ++ instance.extraFlags 278 ); 279 RuntimeDirectory = 280 if 281 lib.any (lib.hasPrefix "/run/anubis") ( 282 with instance.settings; 283 [ 284 BIND 285 METRICS_BIND 286 ] 287 ) 288 then 289 "anubis" 290 else 291 null; 292 293 # hardening 294 NoNewPrivileges = true; 295 CapabilityBoundingSet = null; 296 SystemCallFilter = [ 297 "@system-service" 298 "~@privileged" 299 ]; 300 SystemCallArchitectures = "native"; 301 MemoryDenyWriteExecute = true; 302 303 PrivateUsers = true; 304 PrivateTmp = true; 305 PrivateDevices = true; 306 ProtectHome = true; 307 ProtectClock = true; 308 ProtectHostname = true; 309 ProtectKernelLogs = true; 310 ProtectKernelModules = true; 311 ProtectKernelTunables = true; 312 ProtectProc = "invisible"; 313 ProtectSystem = "strict"; 314 ProtectControlGroups = "strict"; 315 LockPersonality = true; 316 RestrictRealtime = true; 317 RestrictSUIDSGID = true; 318 RestrictNamespaces = true; 319 RestrictAddressFamilies = [ 320 "AF_UNIX" 321 "AF_INET" 322 "AF_INET6" 323 ]; 324 }; 325 } 326 ) enabledInstances; 327 }; 328 329 meta.maintainers = with lib.maintainers; [ 330 soopyc 331 nullcube 332 ]; 333 meta.doc = ./anubis.md; 334}