{ config, pkgs, lib, ... }: with lib; let cfg = config.my.networking.caddy; in { options.my.networking.caddy = { enable = lib.mkEnableOption "Enable caddy as reverse proxy"; domainsList = lib.mkOption { type = lib.types.listOf (lib.types.attrsOf lib.types.str); description = '' A list of sets, each containing three parameters of type string: domain, email, and cloudflareApiKeyFile. ''; default = [ { domain = "example.com"; email = "user@domain.com"; cloudflareApiKeyFile = "/path/to/cloudflare/api/key"; } ]; }; dynamicdnsDomains = lib.mkOption { type = lib.types.listOf (lib.types.attrsOf lib.types.str); description = '' A list of domains to update with the dynamicdns plugin. ''; default = [ { domain = "example.com"; cloudflareApiEnvName = "CLOUDFLARE_API_TOKEN_MY_DOMAIN"; } ]; }; configEnvFile = lib.mkOption { type = lib.types.path; description = '' Path to the environment file that contains the secrets like Cloudflare API key. In order to use the dynamicdns plugin, you need to set "cloudflareApiEnvName" for each domain in the dynamicdnsDomains list. ''; default = ""; }; }; config = lib.mkIf cfg.enable { # Insted on relying on caddy to provide TLS, we use certbot to get a certificate # https://aottr.dev/posts/2024/08/homelab-setting-up-caddy-reverse-proxy-with-ssl-on-nixos/ security.acme = { acceptTerms = true; # TESTING ONLY! # defaults.server = "https://acme-staging-v02.api.letsencrypt.org/directory"; certs = lib.mkMerge ( map (domainConfig: { "${domainConfig.domain}" = { group = config.services.caddy.group; email = domainConfig.email; domain = domainConfig.domain; extraDomainNames = [ "*.${domainConfig.domain}" ]; dnsProvider = "cloudflare"; dnsResolver = "1.1.1.1:53"; dnsPropagationCheck = true; environmentFile = domainConfig.cloudflareApiKeyFile; }; }) cfg.domainsList ); }; services.caddy = { enable = true; # Waiting for https://github.com/NixOS/nixpkgs/issues/14671 to be released package = pkgs.callPackage ../../packages/caddy.nix { externalPlugins = [ { name = "cloudflare"; repo = "github.com/caddy-dns/cloudflare"; version = "master"; } { name = "dynamicdns"; repo = "github.com/mholt/caddy-dynamicdns"; version = "7c818ab3fc3485a72a346f85c77810725f19f9cf"; } ]; vendorHash = "sha256-vkJw/92zXt5S2eUxRSjtwn1nqU/f+WHPEG8AD4Z342I="; }; globalConfig = '' admin :2024 servers { metrics } '' + lib.concatStringsSep "\n" ( map (dynamicdnsDomain: '' dynamic_dns { provider cloudflare {env.${dynamicdnsDomain.cloudflareApiEnvName}} domains { ${dynamicdnsDomain.domain} @ } dynamic_domains } '') cfg.dynamicdnsDomains ); extraConfig = lib.concatStringsSep "\n" ( map ( domainConfig: let certPath = config.security.acme.certs."${domainConfig.domain}".directory; in '' (cloudflare_${domainConfig.domain}) { tls ${certPath}/cert.pem ${certPath}/key.pem { protocols tls1.3 } } '' ) cfg.domainsList ); }; systemd.services.caddy.serviceConfig = { AmbientCapabilities = "CAP_NET_BIND_SERVICE"; EnvironmentFile = cfg.configEnvFile; }; # By default, the module create a custom user but it lacks permission to read caddy files systemd.services.promtail.serviceConfig = { Group = lib.mkForce config.services.caddy.group; User = lib.mkForce config.services.caddy.user; }; services.promtail = { enable = true; configuration = { server.http_listen_port = 9080; server.grpc_listen_port = 0; clients = [ { url = "http://metrics.internal:3100/loki/api/v1/push"; } ]; scrape_configs = [ { job_name = "journal"; journal = { max_age = "12h"; labels = { job = "systemd-journal"; }; }; relabel_configs = [ { source_labels = [ "__journal__systemd_unit" ]; regex = "(.*)\\.service"; target_label = "service"; } { source_labels = [ "__journal__hostname" ]; target_label = "hostname"; } ]; } { job_name = "caddy"; static_configs = [ { targets = [ "localhost" ]; labels = { job = "caddylogs"; __path__ = "${config.services.caddy.logDir}/*.log"; }; } ]; } ]; }; }; networking.firewall.allowedTCPPorts = [ 80 443 2024 ]; networking.firewall.allowedUDPPorts = [ 80 443 ]; }; }