{ 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 = ""; }; # List of extra caddy.virtualHost extraVirtualHosts = lib.mkOption { type = lib.types.listOf ( lib.types.submodule { options = { subdomain = lib.mkOption { type = lib.types.str; description = "The subdomain for the virtual host."; }; host = lib.mkOption { type = lib.types.str; description = "The host address for the reverse proxy."; }; domain = lib.mkOption { type = lib.types.str; description = "The domain for the virtual host."; }; }; } ); description = '' A list of virtual hosts outside nixos/colmena config. ''; 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 ) + "\n" + '' (cors) { @cors_preflight{args[0]} method OPTIONS @cors{args[0]} header Origin {args[0]} handle @cors_preflight{args[0]} { header { Access-Control-Allow-Origin "{args[0]}" Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" Access-Control-Allow-Headers * Access-Control-Max-Age "3600" defer } respond "" 204 } handle @cors{args[0]} { header { Access-Control-Allow-Origin "{args[0]}" Access-Control-Expose-Headers * defer } } } ''; virtualHosts = lib.foldl' ( acc: extraVirtualHost: acc // { "${extraVirtualHost.subdomain}.${extraVirtualHost.domain}".extraConfig = '' reverse_proxy ${extraVirtualHost.host} import cloudflare_${extraVirtualHost.domain} import cors https://home.pasetto.me ''; } ) { } cfg.extraVirtualHosts; }; 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 ]; }; }