diff --git a/doc/build-helpers/fetchers.chapter.md b/doc/build-helpers/fetchers.chapter.md index e2ac6640ef6c8..b1d1f1ea8b7e9 100644 --- a/doc/build-helpers/fetchers.chapter.md +++ b/doc/build-helpers/fetchers.chapter.md @@ -1001,3 +1001,75 @@ fetchtorrent { - `config`: When using `transmission` as the `backend`, a json configuration can be supplied to transmission. Refer to the [upstream documentation](https://github.com/transmission/transmission/blob/main/docs/Editing-Configuration-Files.md) for information on how to configure. + +## `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. + +::::{.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 cbff4ea2f2261..3c006c976e20a 100644 --- a/doc/redirects.json +++ b/doc/redirects.json @@ -102,6 +102,9 @@ "ex-build-helpers-extendMkDerivation": [ "index.html#ex-build-helpers-extendMkDerivation" ], + "ex-fetchers-fetchtraefikplugin": [ + "index.html#ex-fetchers-fetchtraefikplugin" + ], "ex-pkgs-replace-vars": [ "index.html#ex-pkgs-replace-vars", "index.html#ex-pkgs-substituteAll", @@ -549,6 +552,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 0344a0aa2f3c5..f95994653b4dc 100644 --- a/nixos/doc/manual/redirects.json +++ b/nixos/doc/manual/redirects.json @@ -76,6 +76,27 @@ "module-services-tandoor-recipes-migrating-media-option-disallow-access": [ "index.html#module-services-tandoor-recipes-migrating-media-option-disallow-access" ], + "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-dynamic": [ + "index.html#module-services-traefik-usage-dynamic" + ], + "module-services-traefik-usage-static": [ + "index.html#module-services-traefik-usage-static" + ], "sec-override-nixos-test": [ "index.html#sec-override-nixos-test" ], diff --git a/nixos/modules/services/networking/pangolin.nix b/nixos/modules/services/networking/pangolin.nix index 8f5b78ec7e114..e8ae2daf936c5 100644 --- a/nixos/modules/services/networking/pangolin.nix +++ b/nixos/modules/services/networking/pangolin.nix @@ -433,16 +433,12 @@ in enable = true; group = "fossorial"; dataDir = "${cfg.dataDir}/config/traefik"; + plugins = [ pkgs.fosrl-badger ]; staticConfigOptions = { 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 diff --git a/nixos/modules/services/web-servers/traefik.md b/nixos/modules/services/web-servers/traefik.md new file mode 100644 index 0000000000000..7ee3a11304469 --- /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 with 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 **static configuration** that requires Traefik to be restarted in order +to update it, and a **dynamic 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. + +### Static Configuration {#module-services-traefik-usage-static} + +The static configuration is controlled by the {option}`services.traefik.static` +option set. + +- The {option}`services.traefik.static.file` option allows you to pass a path to + a file containing a Traefik configuration. + +- The {option}`services.traefik.static.settings` option instead allows you to + declare the Traefik configuration directly in the NixOS configuration, using + the usual Nix syntax. + +### Dynamic Configuration {#module-services-traefik-usage-dynamic} + +The dynamic configuration has a similar option set to the static configuration, +but it also has the {option}`services.traefik.dynamic.dir` option, which allows +you to define a directory containing several Traefik dynamic 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 dynamic +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 ]; + static.settings.entryPoints.websecure.http.middlewares = "my-geoblock"; + dynamic.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 static 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. + +If you are passing secrets and other data but **not configuration options** via +the {option}`services.traefik.environmentFiles` option, then it is a good idea to +disable the {option}`services.traefik.useEnvSubst` option for increased startup +performance. + +[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 diff --git a/nixos/modules/services/web-servers/traefik.nix b/nixos/modules/services/web-servers/traefik.nix index 86534f291ee72..581f2cb194f1e 100644 --- a/nixos/modules/services/web-servers/traefik.nix +++ b/nixos/modules/services/web-servers/traefik.nix @@ -5,8 +5,6 @@ ... }: -with lib; - let cfg = config.services.traefik; @@ -21,7 +19,7 @@ let staticConfigFile = if cfg.staticConfigFile == null then format.generate "config.toml" ( - recursiveUpdate cfg.staticConfigOptions { + lib.recursiveUpdate cfg.staticConfigOptions { providers.file.filename = "${dynamicConfigFile}"; } ) @@ -33,19 +31,19 @@ let in { options.services.traefik = { - enable = mkEnableOption "Traefik web server"; + enable = lib.mkEnableOption "Traefik web server"; - staticConfigFile = mkOption { + staticConfigFile = lib.mkOption { default = null; - example = literalExpression "/path/to/static_config.toml"; - type = types.nullOr types.path; + example = lib.literalExpression "/path/to/static_config.toml"; + type = with lib.types; nullOr path; description = '' Path to traefik's static configuration to use. (Using that option has precedence over `staticConfigOptions` and `dynamicConfigOptions`) ''; }; - staticConfigOptions = mkOption { + staticConfigOptions = lib.mkOption { description = '' Static configuration for Traefik. ''; @@ -61,17 +59,17 @@ in }; }; - dynamicConfigFile = mkOption { + dynamicConfigFile = lib.mkOption { default = null; - example = literalExpression "/path/to/dynamic_config.toml"; - type = types.nullOr types.path; + example = lib.literalExpression "/path/to/dynamic_config.toml"; + type = with lib.types; nullOr path; description = '' Path to traefik's dynamic configuration to use. (Using that option has precedence over `dynamicConfigOptions`) ''; }; - dynamicConfigOptions = mkOption { + dynamicConfigOptions = lib.mkOption { description = '' Dynamic configuration for Traefik. ''; @@ -87,17 +85,26 @@ in }; }; - dataDir = mkOption { + plugins = lib.mkOption { + default = [ ]; + type = with lib.types; listOf package; + example = lib.literalExpression "[ pkgs.fosrl-badger ]"; + description = '' + List of plugins to be added to the `localPlugins` attribute in the static configuration. These plugins are usually packaged in Nixpkgs, and are managed by Nix. + ''; + }; + + dataDir = lib.mkOption { default = "/var/lib/traefik"; - type = types.path; + type = lib.types.path; description = '' Location for any persistent data traefik creates, ie. acme ''; }; - group = mkOption { + group = lib.mkOption { default = "traefik"; - type = types.str; + type = lib.types.str; example = "docker"; description = '' Set the group that traefik runs under. @@ -105,11 +112,11 @@ in ''; }; - package = mkPackageOption pkgs "traefik" { }; + package = lib.mkPackageOption pkgs "traefik" { }; - environmentFiles = mkOption { + environmentFiles = lib.mkOption { default = [ ]; - type = types.listOf types.path; + type = with lib.types; listOf path; example = [ "/run/secrets/traefik.env" ]; description = '' Files to load as environment file. Environment variables from this file @@ -118,8 +125,24 @@ in }; }; - config = mkIf cfg.enable { - systemd.tmpfiles.rules = [ "d '${cfg.dataDir}' 0700 traefik traefik - -" ]; + config = lib.mkIf cfg.enable { + services.traefik.staticConfigOptions = lib.mkIf (cfg.plugins != [ ]) { + experimental.localPlugins = lib.listToAttrs ( + map (plugin: lib.nameValuePair plugin.plugin { inherit (plugin) moduleName; }) cfg.plugins + ); + }; + + warnings = + lib.optional (!lib.all lib.id (map (plugin: plugin._isTraefikPlugin or false) cfg.plugins)) + '' + Some of the Traefik plugins in 'services.traefik.plugins' may be misconfigured. + The following paths are built from derivations that do not have the '_isTraefikPlugin' attribute set to 'true': + - ${ + lib.concatMapStringsSep "\n- " (badPlugin: badPlugin.outPath) ( + lib.filter (plugin: plugin._isTraefikPlugin or false) cfg.plugins + ) + } + ''; systemd.services.traefik = { description = "Traefik web server"; @@ -130,12 +153,25 @@ in startLimitBurst = 5; serviceConfig = { EnvironmentFile = cfg.environmentFiles; - ExecStartPre = lib.optional (cfg.environmentFiles != [ ]) ( - pkgs.writeShellScript "pre-start" '' - umask 077 - ${pkgs.envsubst}/bin/envsubst -i "${staticConfigFile}" > "${finalStaticConfigFile}" - '' - ); + ExecStartPre = + lib.optional (cfg.environmentFiles != [ ]) ( + pkgs.writeShellScript "traefik-pre-start-envsubst" '' + umask 077 + ${lib.getExe pkgs.envsubst} -i "${staticConfigFile}" > "${finalStaticConfigFile}" + '' + ) + ++ lib.optional (cfg.plugins != [ ]) ( + pkgs.writeShellScript "traefik-pre-start-ln-plugins" '' + ${lib.getExe' pkgs.coreutils "ln"} -Tsf ${ + toString ( + pkgs.symlinkJoin { + name = "traefik-plugins"; + paths = cfg.plugins; + } + ) + } plugins-local + '' + ); ExecStart = "${cfg.package}/bin/traefik --configfile=${finalStaticConfigFile}"; Type = "simple"; User = "traefik"; @@ -165,4 +201,13 @@ in users.groups.traefik = { }; }; + + meta = { + maintainers = with lib.maintainers; [ + jackr + sigmasquadron + therealgramdalf + ]; + doc = ./traefik.md; + }; } diff --git a/pkgs/build-support/fetchtraefikplugin/default.nix b/pkgs/build-support/fetchtraefikplugin/default.nix new file mode 100644 index 0000000000000..d74a41e0ad6ae --- /dev/null +++ b/pkgs/build-support/fetchtraefikplugin/default.nix @@ -0,0 +1,61 @@ +{ + lib, + fetchzip, + traefik, +}: + +lib.makeOverridable ( + { + plugin, + owner, + provider ? "github.com", # Most Traefik plugins are developed in GitHub repositories. + + version, + hash, + + pname ? plugin, + name ? "${pname}-${version}", + meta ? { }, + }: + let + moduleName = lib.concatStringsSep "/" [ + provider + owner + plugin + ]; + in + fetchzip { + 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; + } +) 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..e9ce7e1229a9f --- /dev/null +++ b/pkgs/by-name/fo/fosrl-badger/package.nix @@ -0,0 +1,21 @@ +{ + 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 + sigmasquadron + ]; + }; +} diff --git a/pkgs/by-name/ge/geoblock/package.nix b/pkgs/by-name/ge/geoblock/package.nix new file mode 100644 index 0000000000000..46b57c9bb4229 --- /dev/null +++ b/pkgs/by-name/ge/geoblock/package.nix @@ -0,0 +1,21 @@ +{ + 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 + sigmasquadron + ]; + }; +} diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index 40a578061d6f8..251b06e870b20 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -672,6 +672,8 @@ with pkgs; fetchPypiLegacy = callPackage ../build-support/fetchpypilegacy { }; + fetchTraefikPlugin = callPackage ../build-support/fetchtraefikplugin { }; + resolveMirrorURLs = { url }: fetchurl {