diff --git a/doc/build-helpers/fetchers.chapter.md b/doc/build-helpers/fetchers.chapter.md index edb9ab4e02433..b38f3bb8bb77b 100644 --- a/doc/build-helpers/fetchers.chapter.md +++ b/doc/build-helpers/fetchers.chapter.md @@ -1066,3 +1066,75 @@ fetchItchIo { upload = "13371354"; } ``` + +## `fetchTraefikPlugin` {#sec-pkgs-fetchers-fetchtraefikplugin} + +This specialised fetcher can build local [Traefik](https://traefik.io) plugins in a format expected by the `experimental.localPlugins` option in a Traefik static configuration file. Built using [extendMkDerivation](https://noogle.dev/f/lib/customisation/extendMkDerivation) based on `fetchzip` {#sec-pkgs-fetchers-fetchzip}, meaning it accepts all parameters that fetchzip does. + +::::{.example #ex-fetchers-fetchtraefikplugin} +# Building a Traefik plugin hosted on `plugins.traefik.io` + +The following recipe shows how to use `fetchTraefikPlugin` to fetch the `geoblock` Traefik plugin at version `0.3.3`: + +```nix +{ fetchTraefikPlugin }: +fetchTraefikPlugin { + plugin = "geoblock"; + owner = "PascalMinder"; + version = "0.3.3"; + hash = "sha256-gLhihjsBJEKGzpIwWzu/zmvLJbqnqXsF8ZiJWWKwDIA="; +} +``` + +After building the recipe, the derivation output will set up the appropriate paths expected by Traefik: + +```shell +$ nix-build +(output removed for clarity) +/nix/store/m68kmlay1vdjzqjq7144r6ra885fd8l8-geoblock-0.3.3 + +$ tree /nix/store/m68kmlay1vdjzqjq7144r6ra885fd8l8-geoblock-0.3.3 +/nix/store/m68kmlay1vdjzqjq7144r6ra885fd8l8-geoblock-0.3.3 +└── src + └── github.com + └── PascalMinder + └── geoblock + ├── docker + │ ├── dev-geoblock + │ │ └── docker-compose.yml + │ └── traefik-config + │ ├── dynamic-configuration.yml + │ └── traefik.yml + ├── lrucache + │ ├── lru.go + │ ├── lru_interface.go + │ └── lru_test.go + ├── LICENSE + ├── Makefile + ├── geoblock.go + ├── geoblock_test.go + ├── go.mod + └── readme.md +``` + +:::: + +### Parameters {#sec-pkgs-fetchers-fetchtraefikplugin-parameters} + +- `plugin`: The plugin name, usually lowercase. + +- `owner`: The author of the plugin. + +- `version`: The version of the plugin, without the `v` prefix. + +- `hash`: The resulting hash of the derivation. + +#### Optional parameters {#sec-pkgs-fetchers-fetchtraefikplugin-parameters-optional} + +- `provider`: The forge hosting the plugin. This defaults to `github.com`, as most Traefik plugins are hosted on GitHub, but the fetcher will always fetch plugins from `plugins.traefik.io`, regardless of upstream provider. + +- `pname`: Defaults to the value of `plugin`. + +- `name`: Defaults to `${pname}-${version}`. + +- `meta`: Defaults to `{ }`, but can be configured similarly to any other package. diff --git a/doc/redirects.json b/doc/redirects.json index d1e475fa4aa0c..faaa7d8aefb01 100644 --- a/doc/redirects.json +++ b/doc/redirects.json @@ -120,6 +120,9 @@ "ex-modularServiceCompliance-nixos": [ "index.html#ex-modularServiceCompliance-nixos" ], + "ex-fetchers-fetchtraefikplugin": [ + "index.html#ex-fetchers-fetchtraefikplugin" + ], "ex-pkgs-replace-vars": [ "index.html#ex-pkgs-replace-vars", "index.html#ex-pkgs-substituteAll", @@ -786,6 +789,15 @@ "sec-debug": [ "index.html#sec-debug" ], + "sec-pkgs-fetchers-fetchtraefikplugin": [ + "index.html#sec-pkgs-fetchers-fetchtraefikplugin" + ], + "sec-pkgs-fetchers-fetchtraefikplugin-parameters": [ + "index.html#sec-pkgs-fetchers-fetchtraefikplugin-parameters" + ], + "sec-pkgs-fetchers-fetchtraefikplugin-parameters-optional": [ + "index.html#sec-pkgs-fetchers-fetchtraefikplugin-parameters-optional" + ], "sec-prefer-remote-fetch": [ "index.html#sec-prefer-remote-fetch" ], diff --git a/nixos/doc/manual/redirects.json b/nixos/doc/manual/redirects.json index 6e8a587f02c41..f5f0fbe132ce4 100644 --- a/nixos/doc/manual/redirects.json +++ b/nixos/doc/manual/redirects.json @@ -250,6 +250,30 @@ "sec-nixos-test-vms-vs-containers": [ "index.html#sec-nixos-test-vms-vs-containers" ], + "module-services-traefik": [ + "index.html#module-services-traefik" + ], + "module-services-traefik-environment": [ + "index.html#module-services-traefik-environment" + ], + "module-services-traefik-plugins": [ + "index.html#module-services-traefik-plugins" + ], + "module-services-traefik-plugins-custom": [ + "index.html#module-services-traefik-plugins-custom" + ], + "module-services-traefik-usage": [ + "index.html#module-services-traefik-usage" + ], + "module-services-traefik-usage-routing": [ + "index.html#module-services-traefik-usage-routing" + ], + "module-services-traefik-usage-install": [ + "index.html#module-services-traefik-usage-install" + ], + "module-services-traefik-migrating-to-26.11": [ + "index.html#module-services-traefik-migrating-to-26.11" + ], "sec-override-nixos-test": [ "index.html#sec-override-nixos-test" ], diff --git a/nixos/doc/manual/release-notes/rl-2611.section.md b/nixos/doc/manual/release-notes/rl-2611.section.md index e84c3bfbbd634..6cb87a792d9b3 100644 --- a/nixos/doc/manual/release-notes/rl-2611.section.md +++ b/nixos/doc/manual/release-notes/rl-2611.section.md @@ -168,6 +168,9 @@ - [firefox-syncserver.database.type](#opt-services.firefox-syncserver.database.type) no longer defaults to `"mysql"`. You must now explicitly choose between `"mysql"` and `"postgresql"`. New deployments should prefer PostgreSQL. ++ The Traefik module has been refactored to allow new ways to deploy the install and routing configuration files, as well as nixpkgs packaged plugins. See [Migrating traefik for pre 26.11 configurations](#module-services-traefik-migrating-to-26.11). ++ +>>>>>>> conflict 1 of 1 ends ## Other Notable Changes {#sec-release-26.11-notable-changes} diff --git a/nixos/modules/services/networking/pangolin.nix b/nixos/modules/services/networking/pangolin.nix index 37fb0935be874..7fe4a95f9a3ee 100644 --- a/nixos/modules/services/networking/pangolin.nix +++ b/nixos/modules/services/networking/pangolin.nix @@ -225,13 +225,10 @@ in isSystemUser = true; }; }; - groups.fossorial = { - members = [ - "pangolin" - "gerbil" - "traefik" - ]; - }; + groups.fossorial.members = [ + "pangolin" + "gerbil" + ]; }; # order is as follows # "pangolin.service" @@ -241,21 +238,18 @@ in # make tunnels declarative by calling API ### systemd = { - tmpfiles.settings."10-fossorial-paths" = { - "${cfg.dataDir}".d = { - user = "pangolin"; - group = "fossorial"; - mode = "0770"; - }; - "${cfg.dataDir}/config".d = { - user = "pangolin"; - group = "fossorial"; - mode = "0770"; - }; - "${cfg.dataDir}/config/letsencrypt".d = { - user = "traefik"; - group = "fossorial"; - mode = "0700"; + tmpfiles.settings = { + "10-fossorial-paths" = { + "${cfg.dataDir}".d = { + user = "pangolin"; + group = "fossorial"; + mode = "0770"; + }; + "${cfg.dataDir}/config".d = { + user = "pangolin"; + group = "fossorial"; + mode = "0770"; + }; }; }; services = { @@ -265,9 +259,11 @@ in requires = [ "network.target" ]; after = [ "network.target" ]; + # need to do the symlinks here because of strict + # systemd tmpfiles unsafe path transitions preStart = '' - mkdir -p ${cfg.dataDir}/config - cp -f ${cfgFile} ${cfg.dataDir}/config/config.yml + ln -sf ${cfgFile} ${cfg.dataDir}/config/config.yml + ln -sft ${cfg.dataDir}/config/ ${config.services.traefik.dataDir} ''; serviceConfig = { @@ -455,18 +451,12 @@ in services.traefik = { enable = true; - group = "fossorial"; - dataDir = "${cfg.dataDir}/config/traefik"; - staticConfigOptions = { + localPlugins = [ pkgs.fosrl-badger ]; + install.settings = { providers.http = { endpoint = "http://localhost:${toString finalSettings.server.internal_port}/api/v1/traefik-config"; pollInterval = "5s"; }; - # TODO to change this once #437073 is merged. - experimental.plugins.badger = { - moduleName = "github.com/fosrl/badger"; - version = "v1.2.0"; - }; certificatesResolvers.letsencrypt.acme = ( if finalSettings.domains.domain1.prefer_wildcard_cert then @@ -483,7 +473,7 @@ in # common { email = cfg.letsEncryptEmail; - storage = "${cfg.dataDir}/config/letsencrypt/acme.json"; + storage = "acme.json"; caServer = "https://acme-v02.api.letsencrypt.org/directory"; }; entryPoints = { @@ -495,7 +485,7 @@ in }; }; }; - dynamicConfigOptions = { + routing.files."pangolin".settings = { http = { middlewares.redirect-to-https.redirectScheme.scheme = "https"; routers = { diff --git a/nixos/modules/services/web-servers/traefik.md b/nixos/modules/services/web-servers/traefik.md new file mode 100644 index 0000000000000..1ce5c68c2eff9 --- /dev/null +++ b/nixos/modules/services/web-servers/traefik.md @@ -0,0 +1,119 @@ +# Traefik {#module-services-traefik} + +[Traefik][upstream-1] is an open-source, cloud-native reverse proxy that +can be configured in NixOS using the {option}`services.traefik` option set. + +## Basic Usage {#module-services-traefik-usage} + +A key feature of Traefik is that the reverse proxy configuration is split into +two: a **install configuration** that requires Traefik to be restarted in order +to update it, and a **routing configuration** that can change without a need to +restart the server. The [upstream documentation][upstream-2] has a detailed +overview on the difference between both configuration types. + +### install Configuration {#module-services-traefik-usage-install} + +The install configuration is controlled by the {option}`services.traefik.install` +option set. + +- The {option}`services.traefik.install.file` option allows you to pass a path to + a file containing a Traefik configuration. + +- The {option}`services.traefik.install.settings` option instead allows you to + declare the Traefik configuration directly in the NixOS configuration, using + the usual Nix syntax. + +### routing Configuration {#module-services-traefik-usage-routing} + +The routing configuration has a similar option set to the install configuration, +but it also has the {option}`services.traefik.routing.dir` option, which allows +you to define a directory containing several Traefik routing configuration files. + +## Plugins {#module-services-traefik-plugins} + +When using the structured `settings` configuration options, the Traefik module +supports [plugins][upstream-3]. Plugins in Traefik are an additional routing +configuration source and can programatically set up routes and proxies. + +The {option}`services.traefik.plugins` option takes in a list of derivations +that contain Traefik plugins. Some plugins are available in the package set, and +can be called directly from `pkgs`. The example below sets up `geoblock`, a +Traefik plugin that blocks connections from a given list of countries based on +the client's IP address, to block all connections not coming from the Netherlands. + +```nix +{ + services.traefik = { + plugins = with pkgs; [ geoblock ]; + install.settings.entryPoints.websecure.http.middlewares = "my-geoblock"; + routing.settings.http.middlewares.my-geoblock.plugin.geoblock.countries = [ "NL" ]; + }; +} +``` + +### Custom Plugins {#module-services-traefik-plugins-custom} + +Plugins that are not currently packaged in Nixpkgs can also be added to the +{option}`services.traefik.plugins` option after being built with the +`fetchTraefikPlugin` builder. See the [Nixpkgs manual section on +`fetchTraefikPlugin`][fetcher] for more information on the available options. + +```nix +{ + services.traefik.plugins = [ + (pkgs.fetchTraefikPlugin { + plugin = "example"; + owner = "example-author"; + version = "1.0.0"; + hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; + }) + ]; +} +``` + +For plugins not found in `plugins.traefik.io`, simply use the +[`lib.fileset.toSource` library function][fileset] to build a derivation from a path to a +local plugin. The module will generate a warning mentioning that the local plugin +may be misconfigured, but it can be safely ignored, or removed by updating the +derivation to include the `_isTraefikPlugin` attribute. + +```nix +{ + services.traefik.plugins = [ + ( + (lib.fileset.toSource { + root = ./my-plugin; + fileset = ./my-plugin; + }) + # Supress Traefik module warning. + # Don't forget to ensure that ./my-plugin has an appropriate + # directory structure as expected by Traefik. + // { + _isTraefikPlugin = true; + } + ) + ]; +} +``` + +## Environment Files {#module-services-traefik-environment} + +Although the Traefik module offers the {option}`services.traefik.environmentFiles` +option to set up environment files for the running server, *it is not recommended +to use them as install configuration sources*. The environment files are intended +to provision secrets for ACME/Let's Encrypt and other certificate setups. + +See the [upstream documentation][upstream-4] for more information on passing +ACME secrets for setting up DNS-01 challenges. + + +[fetcher]: https://nixos.org/manual/nixpkgs/stable/#sec-pkgs-fetchers-fetchtraefikplugin +[fileset]: https://nixos.org/manual/nixpkgs/stable/#function-library-lib.fileset.toSource +[upstream-1]: https://traefik.io +[upstream-2]: https://doc.traefik.io/traefik/getting-started/configuration-overview +[upstream-3]: https://plugins.traefik.io/plugins +[upstream-4]: https://doc.traefik.io/traefik/https/acme/#providers + +## Migrating to 26.11 {#module-services-traefik-migrating-to-26.11} + +The Traefik module now features new ways to deploy the routing and install configuration files. Move your existing declarative install and routing configurations to `services.traefik.install.settings` and `services.traefik.routing.files."my-application".settings` respectively. The `services.traefik.routing.settings` option is available for a simpler migration without needing to define a filename like `my-application`, but this option will be removed in NixOS 27.05. The option to use `EnvSubst` to substitute environment variables has been removed, as using environment variables to store secrets is already supported by the {option}`services.traefik.environmentFiles`. diff --git a/nixos/modules/services/web-servers/traefik.nix b/nixos/modules/services/web-servers/traefik.nix index e1f1f73a15e8f..73eda1f5d3587 100644 --- a/nixos/modules/services/web-servers/traefik.nix +++ b/nixos/modules/services/web-servers/traefik.nix @@ -1,166 +1,551 @@ { + options, config, lib, pkgs, ... }: -with lib; - let + inherit (lib.types) + attrsOf + bool + listOf + nullOr + path + str + submodule + package + ; + inherit (lib) + attrByPath + concatMapStringsSep + concatStringsSep + filter + getExe + id + literalExpression + maintainers + mapAttrs' + mkDefault + mkEnableOption + mkIf + mkMerge + mkOption + mkPackageOption + mkRenamedOptionModule + nameValuePair + optional + optionalAttrs + remove + splitStringBy + ; + cfg = config.services.traefik; + opt = options.services.traefik; - format = pkgs.formats.toml { }; + # check if the option has been changed + ## isDefault :: String -> bool + ## eg. isDefault "install.settings" == (cfg.install.settings == opt.install.settings.default) + isDefault = + attrPathStr: + let + sepPath = splitStringBy (prev: curr: builtins.elem curr [ "." ]) false attrPathStr; + in + attrByPath (sepPath ++ [ "default" ]) (throw "isDefault failed") opt + == attrByPath sepPath (throw "isDefault failed") cfg; - dynamicConfigFile = - if cfg.dynamicConfigFile == null then - format.generate "config.toml" cfg.dynamicConfigOptions - else - cfg.dynamicConfigFile; + # JSON is considered valid YAML by Traefik. + format = pkgs.formats.json { }; - staticConfigFile = - if cfg.staticConfigFile == null then - format.generate "config.toml" ( - recursiveUpdate cfg.staticConfigOptions { - providers.file.filename = "${dynamicConfigFile}"; - } - ) + installFile = + if cfg.install.file == null then + format.generate "install_config.json" cfg.install.settings else - cfg.staticConfigFile; - - finalStaticConfigFile = - if cfg.environmentFiles == [ ] then staticConfigFile else "/run/traefik/config.toml"; + cfg.install.file; in { + imports = [ + (mkRenamedOptionModule + [ + "services" + "traefik" + "staticConfigFile" + ] + [ + "services" + "traefik" + "install" + "file" + ] + ) + (mkRenamedOptionModule + [ + "services" + "traefik" + "staticConfigOptions" + ] + [ + "services" + "traefik" + "install" + "settings" + ] + ) + (mkRenamedOptionModule + [ + "services" + "traefik" + "dynamicConfigFile" + ] + [ + "services" + "traefik" + "routing" + "file" + ] + ) + (mkRenamedOptionModule + [ + "services" + "traefik" + "dynamicConfigOptions" + ] + [ + "services" + "traefik" + "routing" + "settings" + ] + ) + ]; options.services.traefik = { enable = mkEnableOption "Traefik web server"; + package = mkPackageOption pkgs "traefik" { }; - staticConfigFile = mkOption { - default = null; - example = literalExpression "/path/to/static_config.toml"; - type = types.nullOr types.path; - description = '' - Path to traefik's static configuration to use. - (Using that option has precedence over `staticConfigOptions` and `dynamicConfigOptions`) - ''; - }; + install = { + file = mkOption { + default = null; + example = literalExpression "/path/to/install_config.toml"; + type = nullOr path; + description = '' + Path to Traefik's install configuration file. - staticConfigOptions = mkOption { - description = '' - Static configuration for Traefik. - ''; - type = format.type; - default = { - entryPoints.http.address = ":80"; + ::: {.note} + You cannot use this option alongside the declarative configuration options. + ::: + ''; }; - example = { - entryPoints.web.address = ":8080"; - entryPoints.http.address = ":80"; + settings = mkOption { + description = '' + Install configuration for Traefik, written in Nix. - api = { }; + ::: {.note} + This will be serialized to JSON (which is considered valid YAML) at build, and passed to Traefik as `--configfile`. + ::: + ''; + type = format.type; + default = { + entryPoints.http.address = ":80"; + }; + example = { + entryPoints = { + "web" = { + address = ":80"; + http.redirections.entryPoint = { + permanent = true; + scheme = "https"; + to = "websecure"; + }; + }; + "websecure" = { + address = ":443"; + asDefault = true; + }; + }; + }; }; }; - dynamicConfigFile = mkOption { - default = null; - example = literalExpression "/path/to/dynamic_config.toml"; - type = types.nullOr types.path; - description = '' - Path to traefik's dynamic configuration to use. - (Using that option has precedence over `dynamicConfigOptions`) - ''; - }; + routing = { + file = mkOption { + default = null; + example = literalExpression "/path/to/routing_config.toml"; + type = nullOr path; + description = '' + Path to Traefik's routing configuration file. - dynamicConfigOptions = mkOption { - description = '' - Dynamic configuration for Traefik. - ''; - type = format.type; - default = { }; - example = { - http.routers.router1 = { - rule = "Host(`localhost`)"; - service = "service1"; + ::: {.note} + You cannot use this option alongside the declarative configuration options. + ::: + ''; + }; + dir = mkOption { + default = "/var/lib/traefik/routing"; + example = literalExpression "/etc/traefik/"; + type = nullOr path; + description = '' + Path to the directory Traefik should watch for configuration files. + + ::: {.warning} + Files in this directory matching the glob `_nixos-*` (reserved for Nix-managed routing configurations) will be deleted as part of + `systemd-tmpfiles-resetup.service`, _**regardless of their origin.**_. + ::: + ''; + }; + files = mkOption { + type = attrsOf (submodule { + options.settings = mkOption { + type = format.type; + description = '' + Routing configuration for Traefik, written in Nix. + + ::: {.note} + This will be serialized to JSON (which is considered valid YAML) at build, and passed as part of the install file. + ::: + ''; + example = { + http.routers."api" = { + service = "api@internal"; + rule = "Host(`localhost`)"; + }; + }; + }; + }); + default = { }; + example = { + "dashboard".settings = { + http.routers."api" = { + service = "api@internal"; + rule = "Host(`198.51.100.1`)"; + }; + }; }; + description = '' + Routing configuration files to write. These are symlinked in `services.traefik.routing.dir` upon activation, + allowing configuration to be upated without restarting the primary daemon. + + ::: {.note} + Due to [a limitation in Traefik](https://github.com/traefik/traefik/issues/10890); any syntax error in a routing configuration will cause the _**entire file provider**_ to be ignored. + This may cause interuption in service, which may include access to the Traefik dashboard, if [enabled and configured](https://doc.traefik.io/traefik/reference/install-configuration/api-dashboard/). + ::: + ''; + }; + # TODO: Drop in 27.05. + settings = mkOption { + type = format.type; + description = '' + Routing configuration for Traefik, written in Nix. + This option is intended for easily migrating pre-26.11 Traefik configurations, and will be removed in NixOS 27.05. - http.services.service1.loadBalancer.servers = [ { url = "http://localhost:8080"; } ]; + ::: {.note} + Configurations added here will be translated into a file for {option}`services.traefik.routing.files`, named `custom-migrated`. + ::: + ''; + default = { }; + example = { + http.routers."api" = { + service = "api@internal"; + rule = "Host(`localhost`)"; + }; + }; }; }; + localPlugins = mkOption { + default = [ ]; + type = listOf package; + example = literalExpression "[ pkgs.fosrl-badger pkgs.geoblock ]"; + description = '' + List of local plugins to be added to the `localPlugins` attribute in the install configuration. These plugins are usually packaged in Nixpkgs, and are managed by Nix. + ''; + }; dataDir = mkOption { default = "/var/lib/traefik"; - type = types.path; + type = path; + description = '' + Location for any persistent data Traefik creates, such as the ACME certificate store. + + ::: {.note} + If left as the default value, this directory will automatically be created + before the Traefik server starts, otherwise you are responsible for ensuring + the directory exists with appropriate ownership and permissions. + ::: + ''; + }; + + user = mkOption { + default = "traefik"; + type = str; description = '' - Location for any persistent data traefik creates, ie. acme + User under which Traefik runs. + + ::: {.note} + If left as the default value this user will automatically be created + on system activation, otherwise you are responsible for + ensuring the user exists before the Traefik service starts. + ::: ''; }; group = mkOption { default = "traefik"; - type = types.str; - example = "docker"; + type = str; description = '' - Set the group that traefik runs under. - For the docker backend this needs to be set to `docker` instead. + Primary group under which Traefik runs. + For the Docker backend, use {option}`services.traefik.supplementaryGroups` instead of overriding this option. + + ::: {.note} + If left as the default value this group will automatically be created + on system activation, otherwise you are responsible for + ensuring the group exists before the Traefik service starts. + ::: ''; }; - package = mkPackageOption pkgs "traefik" { }; + supplementaryGroups = mkOption { + default = [ ]; + type = listOf str; + example = [ "docker" ]; + description = '' + Additional groups under which Traefik runs. + This can be used to give additional permissions, such as the group required by the `docker` provider. + + ::: {.note} + With the `docker` provider, Traefik manages connection to containers via the Docker socket, + which requires membership of the `docker` group for write access. + ::: + ''; + }; environmentFiles = mkOption { default = [ ]; - type = types.listOf types.path; + type = listOf path; example = [ "/run/secrets/traefik.env" ]; description = '' - Files to load as environment file. Environment variables from this file - will be substituted into the static configuration file using envsubst. + Files to load as an environment file just before Traefik starts. + This can be used to pass secrets such as [DNS challenge API tokens](https://doc.traefik.io/traefik/reference/install-configuration/tls/certificate-resolvers/acme/#providers) or [ENV variables](https://doc.traefik.io/traefik/reference/install-configuration/boot-environment/#environment-variables). + ``` + DESEC_TOKEN= + TRAEFIK_CERTIFICATESRESOLVERS__ACME_EAB_HMACENCODED= + TRAEFIK_CERTIFICATESRESOLVERS__ACME_EAB_KID= + ``` + ::: {.warn} + The traefik install configuration methods (env, CLI, and file) are mutually exclusive. + ::: + ``` ''; }; }; config = mkIf cfg.enable { - systemd.tmpfiles.rules = [ "d '${cfg.dataDir}' 0700 traefik traefik - -" ]; + assertions = [ + { + assertion = (!(isDefault "install.file")) -> isDefault "install.settings"; + message = '' + The 'services.traefik.install.file' and 'services.traefik.install.settings' + options are mutually exclusive for the Traefik install config. + It is recommended to use 'settings'. + ''; + } + { + assertion = + (!(isDefault "install.file")) + -> (builtins.all ( + map isDefault [ + "routing.files" + "routing.dir" + "routing.file" + ] + )); + message = '' + None of the routing configuration options may be used if Traefik is being managed imperatively. + The following options have non-default values: + - ${ + concatMapStringsSep "\n - " (str: "'services.traefik.routing.${str}'") ( + filter (attr: !(isDefault "routing.${attr}")) [ + "files" + "dir" + "file" + "settings" # TODO: Drop in 27.05. + ] + ) + } + ''; + } + { + assertion = !(isDefault "routing.file") -> cfg.routing.dir == null; + message = '' + The 'services.traefik.routing.file' and 'services.traefik.routing.dir' options + are mutually exclusive for the Traefik routing config. It is recommended to use + 'services.traefik.routing.dir' with 'services.traefik.routing.files'. + ''; + } + { + assertion = !(isDefault "routing.files") -> cfg.routing.dir != null; + message = '' + 'services.traefik.routing.files' requires the routing file provider to be set + to a directory. Please set a path for 'services.traefik.routing.dir'. + ''; + } + { + assertion = cfg.group != "docker"; + message = '' + Setting the primary group to 'docker' will cause files, such as those generated + by 'services.traefik.routing.files', to be owned by the group 'docker', which + may be a security risk. Use 'services.traefik.supplementaryGroups' instead. + ''; + } + ]; + + warnings = + optional (!(builtins.elem "docker" cfg.supplementaryGroups -> config.virtualisation.docker.enable)) + "'services.traefik.supplementaryGroups' contains the 'docker' group, but 'services.docker' is not enabled." + ++ optional (!(isDefault "routing.settings")) '' + 'services.traefik.routing.settings' is in use, but that option is deprecated. + Please migrate your configuration to an explicit file instead. + + You may do so by moving the value of 'services.traefik.routing.settings' to + 'services.traefik.routing.files..settings', where is an arbitrary + string that ideally identifies the configuration's purpose. + + The following files define 'services.traefik.routing.settings' and should be migrated: + - ${ + concatStringsSep "\n - " ( + remove ./traefik.nix (map (attr: attr.file) opt.routing.settings.definitionsWithLocations) + ) + } + '' + ++ optional (!builtins.all id (map (plugin: plugin._isTraefikPlugin or false) cfg.localPlugins)) '' + Some of the Traefik local plugins in 'services.traefik.localPlugins' may be misconfigured. + The following paths are built from derivations that do not have the '_isTraefikPlugin' attribute set to 'true': + - ${ + concatMapStringsSep "\n- " (badPlugin: badPlugin.outPath) ( + filter (plugin: plugin._isTraefikPlugin or false) cfg.localPlugins + ) + } + ''; + + # https://github.com/quic-go/quic-go/wiki/UDP-Buffer-Sizes + boot.kernel.sysctl = { + "net.core.rmem_max" = 2500000; + "net.core.wmem_max" = 2500000; + }; + + # If a routing file or directory has been set, add it as a provider in the install configuration + services.traefik = mkIf (isDefault "install.file") { + routing.files = mkIf (!(isDefault "routing.settings")) { + "custom-migrated".settings = cfg.routing.settings; + }; + install.settings = mkMerge [ + + (mkIf (cfg.localPlugins != [ ]) { + experimental.localPlugins = lib.listToAttrs ( + map (plugin: lib.nameValuePair plugin.plugin { inherit (plugin) moduleName; }) cfg.localPlugins + ); + }) + + (mkIf (cfg.routing.dir != null || !(isDefault "routing.file")) { + providers.file = { + directory = mkIf (cfg.routing.dir != null) cfg.routing.dir; + filename = mkIf (!(isDefault "routing.file")) cfg.routing.file; + watch = mkDefault true; + }; + }) + ]; + }; systemd.services.traefik = { - description = "Traefik web server"; + description = "Traefik reverse proxy"; wants = [ "network-online.target" ]; after = [ "network-online.target" ]; wantedBy = [ "multi-user.target" ]; startLimitIntervalSec = 86400; startLimitBurst = 5; + unitConfig.Documentation = "https://doc.traefik.io/traefik/"; serviceConfig = { EnvironmentFile = cfg.environmentFiles; - ExecStartPre = lib.optional ( - cfg.environmentFiles != [ ] - ) "${pkgs.envsubst}/bin/envsubst -i '${staticConfigFile}' -o '${finalStaticConfigFile}'"; - ExecStart = "${cfg.package}/bin/traefik --configfile=${finalStaticConfigFile}"; - Type = "simple"; - User = "traefik"; + ExecStart = "${getExe cfg.package} --configfile=${installFile}"; + Type = "notify"; + User = cfg.user; Group = cfg.group; - Restart = "on-failure"; + SupplementaryGroups = mkIf (cfg.supplementaryGroups != [ ]) cfg.supplementaryGroups; + Restart = "always"; AmbientCapabilities = "cap_net_bind_service"; CapabilityBoundingSet = "cap_net_bind_service"; NoNewPrivileges = true; - LimitNPROC = 64; + TasksMax = 64; LimitNOFILE = 1048576; PrivateTmp = true; PrivateDevices = true; ProtectHome = true; - ProtectSystem = "full"; + ProtectSystem = "strict"; + ProtectKernelTunables = true; + ProtectControlGroups = true; ReadWritePaths = [ cfg.dataDir ]; - RuntimeDirectory = "traefik"; + ReadOnlyPaths = optional (cfg.routing.dir != null) cfg.routing.dir; RuntimeDirectoryMode = "0700"; + RuntimeDirectory = "traefik"; WorkingDirectory = cfg.dataDir; + WatchdogSec = "1s"; }; }; - users.users.traefik = { - group = "traefik"; - home = cfg.dataDir; - createHome = true; - isSystemUser = true; + systemd.tmpfiles.settings."10-traefik" = mkMerge [ + (mkIf (cfg.user == "traefik") { + ${cfg.dataDir}.d = { + inherit (cfg) user group; + mode = "0700"; + }; + }) + (mkIf (cfg.routing.dir != null) ( + { + ${cfg.routing.dir}.d = { + inherit (cfg) user group; + mode = "0700"; + }; + "${cfg.routing.dir}/_nixos-*".r = { }; + } + // (mapAttrs' ( + name: value: + nameValuePair "${cfg.routing.dir}/_nixos-${name}.yml" { + "L+" = { + mode = "0444"; + argument = toString (format.generate name value.settings); + }; + } + ) cfg.routing.files) + )) + (mkIf (cfg.localPlugins != [ ]) { + "${cfg.dataDir}/plugins-local"."L+" = { + inherit (cfg) user group; + mode = "0700"; + argument = toString ( + pkgs.symlinkJoin { + name = "traefik-plugins"; + paths = cfg.localPlugins; + } + ); + }; + }) + ]; + + users = { + users = optionalAttrs (cfg.user == "traefik") { + traefik = { + inherit (cfg) group; + isSystemUser = true; + }; + }; + groups = optionalAttrs (cfg.group == "traefik") { traefik = { }; }; }; + }; - users.groups.traefik = { }; + meta = { + maintainers = with lib.maintainers; [ + jackr + therealgramdalf + ]; + doc = ./traefik.md; }; } diff --git a/nixos/tests/traefik.nix b/nixos/tests/traefik.nix index e647528dae500..379cf617bdfe2 100644 --- a/nixos/tests/traefik.nix +++ b/nixos/tests/traefik.nix @@ -1,10 +1,13 @@ # Test Traefik as a reverse proxy of a local web service # and a Docker container. -{ pkgs, ... }: +{ pkgs, lib, ... }: { name = "traefik"; - meta = with pkgs.lib.maintainers; { - maintainers = [ joko ]; + meta = with lib.maintainers; { + maintainers = [ + joko + jackr + ]; }; nodes = { @@ -37,7 +40,7 @@ services.traefik = { enable = true; - dynamicConfigOptions = { + routing.files.NixOSTest.settings = { http.routers.simplehttp = { rule = "Host(`simplehttp.traefik.test`)"; entryPoints = [ "web" ]; @@ -53,21 +56,16 @@ }; }; - staticConfigOptions = { + install.settings = { global = { checkNewVersion = false; sendAnonymousUsage = false; }; - entryPoints.web.address = ":\${HTTP_PORT}"; + entryPoints.web.address = ":80"; providers.docker.exposedByDefault = false; }; - environmentFiles = [ - (pkgs.writeText "traefik.env" '' - HTTP_PORT=80 - '') - ]; }; systemd.services.simplehttp = { @@ -99,7 +97,7 @@ "curl -sSf -H Host:nginx.traefik.test http://traefik/" ) - with subtest("Check that dynamic configuration works"): + with subtest("Check that routing configuration works"): assert "Directory listing for " in client.succeed( "curl -sSf -H Host:simplehttp.traefik.test http://traefik/" ) diff --git a/pkgs/build-support/fetchtraefikplugin/default.nix b/pkgs/build-support/fetchtraefikplugin/default.nix new file mode 100644 index 0000000000000..e29c89d2c0af5 --- /dev/null +++ b/pkgs/build-support/fetchtraefikplugin/default.nix @@ -0,0 +1,87 @@ +{ + lib, + fetchzip, + traefik, +}: + +lib.extendMkDerivation { + constructDrv = fetchzip; + excludeDrvArgNames = [ + "plugin" + "owner" + "provider" + ]; + + extendDrvArgs = + finalAttrs: + { + plugin, + owner, + provider ? "github.com", # Most Traefik plugins are developed in GitHub repositories. + + version, + hash, + + pname ? plugin, + name ? "${pname}-${version}", + meta ? { }, + + __structuredAttrs ? true, + strictDeps ? true, + + derivationArgs ? { }, + ... + }: + + let + moduleName = lib.concatStringsSep "/" [ + provider + owner + plugin + ]; + in + { + inherit + hash + name + ; + + # Every single published Traefik plugin starts its version string with 'v'. + url = "https://plugins.traefik.io/public/download/${moduleName}/v${version}"; + extension = "zip"; + stripRoot = false; + + postFetch = '' + export tmpdir=$(mktemp -d) + mv $out/${moduleName}@v${version}/* $out/${moduleName}@v${version}/.* $tmpdir + rm -rf $out/${provider} + mkdir -p "$out/src/${moduleName}" + mv -t "$out/src/${moduleName}" $tmpdir/* $tmpdir/.* + ''; + + passthru = { + inherit + moduleName + plugin + provider + owner + ; + _isTraefikPlugin = true; + }; + + meta = { + homepage = "https://plugins.traefik.io/plugins"; + inherit (traefik.meta) platforms; + } + // meta; + derivationArgs = derivationArgs // { + inherit + plugin + owner + provider + __structuredAttrs + strictDeps + ; + }; + }; +} diff --git a/pkgs/by-name/fo/fosrl-badger/package.nix b/pkgs/by-name/fo/fosrl-badger/package.nix new file mode 100644 index 0000000000000..705d4c949d7b3 --- /dev/null +++ b/pkgs/by-name/fo/fosrl-badger/package.nix @@ -0,0 +1,20 @@ +{ + lib, + fetchTraefikPlugin, +}: + +fetchTraefikPlugin { + plugin = "badger"; + owner = "fosrl"; + version = "1.2.0"; + hash = "sha256-j6XCWW0Z9ZWoE2CQP4oNoJ/V7jgizwsY629ZiyMxhfY="; + + meta = { + description = "Traefik plugin that handles authentication for Pangolin resources"; + homepage = "https://plugins.traefik.io/plugins/676da7c6eaa878daeef9c7e9/fossorial-badger"; + license = lib.licenses.mit; + maintainers = with lib.maintainers; [ + jackr + ]; + }; +} diff --git a/pkgs/by-name/ge/geoblock/package.nix b/pkgs/by-name/ge/geoblock/package.nix new file mode 100644 index 0000000000000..8ff2698d27817 --- /dev/null +++ b/pkgs/by-name/ge/geoblock/package.nix @@ -0,0 +1,20 @@ +{ + lib, + fetchTraefikPlugin, +}: + +fetchTraefikPlugin { + plugin = "geoblock"; + owner = "PascalMinder"; + version = "0.3.3"; + hash = "sha256-gLhihjsBJEKGzpIwWzu/zmvLJbqnqXsF8ZiJWWKwDIA="; + + meta = { + description = "Traefik plugin that denies requests based on country of origin"; + homepage = "https://plugins.traefik.io/plugins/62d6ce04832ba9805374d62c/geo-block"; + license = lib.licenses.asl20; + maintainers = with lib.maintainers; [ + jackr + ]; + }; +} diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index 5a7416ead8f4e..b809aac451ab8 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -669,6 +669,8 @@ with pkgs; fetchPypiLegacy = callPackage ../build-support/fetchpypilegacy { }; + fetchTraefikPlugin = callPackage ../build-support/fetchtraefikplugin { }; + resolveMirrorURLs = { url }: fetchurl {