{ pkgs, lib, config, ... }: let inherit (lib) mapAttrsToList mkMerge splitString concatStringsSep mkIf mkOption mkEnableOption ; inherit (lib.types) str bool nullOr anything attrsOf submodule listOf ; cfg = config.xyno.services.traefik; simpleProxyOpts = mapAttrsToList ( n: v: let router = v.routerName; service = v.serviceName; spl = splitString "." v.host; certDomain = if (builtins.length spl) > 2 then concatStringsSep "." (builtins.tail spl) else spl; in mkMerge [ (mkIf v.robotProtection { routers."${router}-robotstxt" = { service = "robotstxt"; rule = "Host(`${v.host}`) && Path(`/robots.txt`)"; tls.certResolver = "letsencrypt"; tls.domains = [ { main = certDomain; sans = [ "*.${certDomain}" ]; } ]; }; services.robotstxt = { loadBalancer.servers = [ { url = "http://127.0.0.2:8080"; } ]; }; }) { routers.${router} = { inherit service; inherit (v) middlewares; rule = if v.rule != null then v.rule else "Host(`${v.host}`)"; tls.certResolver = "letsencrypt"; tls.domains = [ { main = certDomain; sans = [ "*.${certDomain}" ]; } ]; }; services.${service} = { loadBalancer.servers = [ { url = v.internal; } ]; loadBalancer.serversTransport = mkIf (v.transport != null) v.transport; }; } ] ) cfg.simpleProxy; in { options.xyno.services.traefik.enable = mkEnableOption "enables traefik"; options.xyno.services.traefik.noBots = mkOption { type = bool; default = true; }; options.xyno.services.traefik.simpleProxy = mkOption { example = { "example" = { host = "example.org"; middlewares = [ "meow" ]; internal = "http://127.0.0.1:8080"; }; }; default = { }; type = attrsOf ( submodule ( { config, name,... }: { options = { middlewares = mkOption { type = listOf str; default = []; }; internal = mkOption { type = str; description = "where to proxy to"; }; host = mkOption { type = str; description = "used for the route and tls"; }; routerName = mkOption { type = str; default = "simpleproxy-${name}-router"; }; serviceName = mkOption { type = str; default = "simpleproxy-${name}-service"; }; robotProtection = mkOption { type = bool; default = true; description = "robots.txt and (soon) iocane"; }; rule = mkOption { type = str; default = "Host(`${config.host}`)"; description = "overrides the Host(`\${host}`) rule with something custom if set"; }; transport = mkOption { type = nullOr anything; default = null; }; }; } ) ); }; config = mkIf cfg.enable { services.nginx = { enable = mkIf cfg.noBots true; defaultListen = mkIf cfg.noBots [ { addr = "127.0.0.2"; port = 8080; } ]; virtualHosts._.default = true; virtualHosts._.locations."/".root = pkgs.writeTextFile { name = "robots.txt"; destination = "/robots.txt"; text = '' User-agent: * Disallow: / ''; }; }; services.traefik = { enable = true; environmentFiles = [ config.sops.templates."traefik.env".path ]; staticConfigOptions = { accessLog = {}; metrics = mkIf config.xyno.services.monitoring.enable { otlp.http.endpoint = "http://localhost:8429/v1/metrics"; }; entryponits.web = { address = ":80"; redirections.entryPoint = { to = "websecure"; scheme = "https"; permanent = true; }; }; entrypoints.websecure = { address = ":443"; http.tls.certResolver = "letsencrypt"; http3 = { }; }; log.level = "INFO"; certificatesResolvers.letsencrypt.acme = { email = "ssl@xyno.systems"; caServer = "https://acme-v02.api.letsencrypt.org/directory"; dnsChallenge = { resolvers = [ "8.8.8.8" "1.1.1.1" ]; provider = "desec"; }; }; }; dynamicConfigOptions = { http = mkMerge simpleProxyOpts; # tls.options.default = { # # mozilla modern # minVersion = "VersionTLS13"; # curvePreferences = [ # "X25519" # "CurveP256" # "CurveP384" # ]; # }; # tls.options.old = { # # mozilla intermediate # minVersion = "VersionTLS12"; # curvePreferences = [ # "X25519" # "CurveP256" # "CurveP384" # ]; # cipherSuites = [ # "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" # "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" # "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" # "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" # "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305" # "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305" # ]; # }; }; }; networking.firewall.allowedTCPPorts = [ 80 443 ]; networking.firewall.allowedUDPPorts = [ 443 ]; xyno.impermanence.directories = [ config.services.traefik.dataDir ]; sops.secrets."desec_token" = { }; sops.templates."traefik.env".content = '' DESEC_TOKEN=${config.sops.placeholder.desec_token} DESEC_PROPAGATION_TIMEOUT=1200 LEGO_DISABLE_CNAME_SUPPORT=true ''; sops.templates."traefik.env".reloadUnits = [ "traefik.service" ]; # services.borgmatic.settings.traefikql_databases = [ # { # name = "all"; # gets run as root anyways so can log in # } # ]; }; }