mirror of
				https://github.com/nmasur/dotfiles
				synced 2025-11-04 01:13:15 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			231 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
			
		
		
	
	
			231 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
# Caddy is a reverse proxy, like Nginx or Traefik. This creates an ingress
 | 
						|
# point from my local network or the public (via Cloudflare). Instead of a
 | 
						|
# Caddyfile, I'm using the more expressive JSON config file format. This means
 | 
						|
# I can source routes from other areas in my config and build the JSON file
 | 
						|
# using the result of the expression.
 | 
						|
 | 
						|
# Caddy helpfully provides automatic ACME cert generation and management, but
 | 
						|
# it requires a form of validation. We are using a custom build of Caddy
 | 
						|
# (compiled with an overlay) to insert a plugin for managing DNS validation
 | 
						|
# with Cloudflare's DNS API.
 | 
						|
 | 
						|
{
 | 
						|
  config,
 | 
						|
  pkgs,
 | 
						|
  lib,
 | 
						|
  ...
 | 
						|
}:
 | 
						|
 | 
						|
let
 | 
						|
  cfg = config.nmasur.presets.services.caddy;
 | 
						|
in
 | 
						|
 | 
						|
{
 | 
						|
 | 
						|
  options.nmasur.presets.services.caddy = {
 | 
						|
    enable = lib.mkEnableOption "Caddy reverse-proxy";
 | 
						|
    tlsPolicies = lib.mkOption {
 | 
						|
      type = lib.types.listOf lib.types.attrs;
 | 
						|
      description = "Caddy JSON TLS issuer policies";
 | 
						|
      default = [ ];
 | 
						|
    };
 | 
						|
    routes = lib.mkOption {
 | 
						|
      type = lib.types.listOf lib.types.attrs;
 | 
						|
      description = "Caddy JSON routes for http servers";
 | 
						|
      default = [ ];
 | 
						|
    };
 | 
						|
    blocks = lib.mkOption {
 | 
						|
      type = lib.types.listOf lib.types.attrs;
 | 
						|
      description = "Caddy JSON error blocks for http servers";
 | 
						|
      default = [ ];
 | 
						|
    };
 | 
						|
    cidrAllowlist = lib.mkOption {
 | 
						|
      type = lib.types.listOf lib.types.str;
 | 
						|
      description = "CIDR blocks to allow for requests";
 | 
						|
      default = [ ];
 | 
						|
      # merge = lib.mkMerge; # Ensure that values are merged from default
 | 
						|
    };
 | 
						|
  };
 | 
						|
 | 
						|
  config = lib.mkIf cfg.enable {
 | 
						|
 | 
						|
    # Force Caddy to 403 if not coming from allowlisted source
 | 
						|
    nmasur.presets.services.caddy.cidrAllowlist = [ "127.0.0.1/32" ];
 | 
						|
    nmasur.presets.services.caddy.routes = lib.mkBefore [
 | 
						|
      {
 | 
						|
        match = [ { not = [ { remote_ip.ranges = cfg.cidrAllowlist; } ]; } ];
 | 
						|
        handle = [
 | 
						|
          {
 | 
						|
            handler = "static_response";
 | 
						|
            status_code = "403";
 | 
						|
          }
 | 
						|
        ];
 | 
						|
      }
 | 
						|
    ];
 | 
						|
 | 
						|
    services.caddy =
 | 
						|
      let
 | 
						|
        default_logger_name = "other";
 | 
						|
        roll_size_mb = 25;
 | 
						|
        # Extract list of hostnames (fqdns) from current caddy routes
 | 
						|
        getHostnameFromMatch = match: if (lib.hasAttr "host" match) then match.host else [ ];
 | 
						|
        getHostnameFromRoute =
 | 
						|
          route:
 | 
						|
          if (lib.hasAttr "match" route) then (lib.concatMap getHostnameFromMatch route.match) else [ ];
 | 
						|
        hostnames_non_unique = lib.concatMap getHostnameFromRoute cfg.routes;
 | 
						|
        hostnames = lib.unique hostnames_non_unique;
 | 
						|
        # Create attrset of subdomains to their fqdns
 | 
						|
        hostname_map = builtins.listToAttrs (
 | 
						|
          map (hostname: {
 | 
						|
            name = builtins.head (lib.splitString "." hostname);
 | 
						|
            value = hostname;
 | 
						|
          }) hostnames
 | 
						|
        );
 | 
						|
      in
 | 
						|
      {
 | 
						|
        enable = true;
 | 
						|
        adapter = "''"; # Required to enable JSON
 | 
						|
        configFile = pkgs.writeText "Caddyfile" (
 | 
						|
          builtins.toJSON {
 | 
						|
            apps.http.servers.main = {
 | 
						|
              listen = [ ":443" ];
 | 
						|
 | 
						|
              # These routes are pulled from the rest of this repo
 | 
						|
              routes = cfg.routes;
 | 
						|
              errors.routes = cfg.blocks;
 | 
						|
 | 
						|
              # Uncommenting collects access logs
 | 
						|
              logs = {
 | 
						|
                inherit default_logger_name;
 | 
						|
                # Invert hostnames keys and values
 | 
						|
                logger_names = lib.mapAttrs' (name: value: {
 | 
						|
                  name = value;
 | 
						|
                  value = name;
 | 
						|
                }) hostname_map;
 | 
						|
              };
 | 
						|
 | 
						|
              metrics = { }; # Enables Prometheus metrics
 | 
						|
            };
 | 
						|
            apps.tls.automation.policies = cfg.tlsPolicies;
 | 
						|
 | 
						|
            # Setup logging to journal and files
 | 
						|
            logging.logs =
 | 
						|
              {
 | 
						|
                # System logs and catch-all
 | 
						|
                # Must be called `default` to override Caddy's built-in default logger
 | 
						|
                default = {
 | 
						|
                  level = "INFO";
 | 
						|
                  encoder.format = "console";
 | 
						|
                  writer = {
 | 
						|
                    output = "stderr";
 | 
						|
                  };
 | 
						|
                  exclude = (map (hostname: "http.log.access.${hostname}") (builtins.attrNames hostname_map)) ++ [
 | 
						|
                    "http.log.access.${default_logger_name}"
 | 
						|
                  ];
 | 
						|
                };
 | 
						|
                # This is for the default access logs (anything not captured by hostname)
 | 
						|
                other = {
 | 
						|
                  level = "INFO";
 | 
						|
                  encoder.format = "json";
 | 
						|
                  writer = {
 | 
						|
                    output = "file";
 | 
						|
                    filename = "${config.services.caddy.logDir}/other.log";
 | 
						|
                    roll = true;
 | 
						|
                    inherit roll_size_mb;
 | 
						|
                  };
 | 
						|
                  include = [ "http.log.access.${default_logger_name}" ];
 | 
						|
                };
 | 
						|
                # This is for using the Caddy API, which will probably never happen
 | 
						|
                admin = {
 | 
						|
                  level = "INFO";
 | 
						|
                  encoder.format = "json";
 | 
						|
                  writer = {
 | 
						|
                    output = "file";
 | 
						|
                    filename = "${config.services.caddy.logDir}/admin.log";
 | 
						|
                    roll = true;
 | 
						|
                    inherit roll_size_mb;
 | 
						|
                  };
 | 
						|
                  include = [ "admin" ];
 | 
						|
                };
 | 
						|
                # This is for TLS cert management tracking
 | 
						|
                tls = {
 | 
						|
                  level = "INFO";
 | 
						|
                  encoder.format = "json";
 | 
						|
                  writer = {
 | 
						|
                    output = "file";
 | 
						|
                    filename = "${config.services.caddy.logDir}/tls.log";
 | 
						|
                    roll = true;
 | 
						|
                    inherit roll_size_mb;
 | 
						|
                  };
 | 
						|
                  include = [ "tls" ];
 | 
						|
                };
 | 
						|
                # This is for debugging
 | 
						|
                debug = {
 | 
						|
                  level = "DEBUG";
 | 
						|
                  encoder.format = "json";
 | 
						|
                  writer = {
 | 
						|
                    output = "file";
 | 
						|
                    filename = "${config.services.caddy.logDir}/debug.log";
 | 
						|
                    roll = true;
 | 
						|
                    roll_keep = 1;
 | 
						|
                    inherit roll_size_mb;
 | 
						|
                  };
 | 
						|
                };
 | 
						|
              }
 | 
						|
              # These are the access logs for individual hostnames
 | 
						|
              // (lib.mapAttrs (name: value: {
 | 
						|
                level = "INFO";
 | 
						|
                encoder.format = "json";
 | 
						|
                writer = {
 | 
						|
                  output = "file";
 | 
						|
                  filename = "${config.services.caddy.logDir}/${name}-access.log";
 | 
						|
                  roll = true;
 | 
						|
                  inherit roll_size_mb;
 | 
						|
                };
 | 
						|
                include = [ "http.log.access.${name}" ];
 | 
						|
              }) hostname_map)
 | 
						|
              # We also capture just the errors separately for easy debugging
 | 
						|
              // (lib.mapAttrs' (name: value: {
 | 
						|
                name = "${name}-error";
 | 
						|
                value = {
 | 
						|
                  level = "ERROR";
 | 
						|
                  encoder.format = "json";
 | 
						|
                  writer = {
 | 
						|
                    output = "file";
 | 
						|
                    filename = "${config.services.caddy.logDir}/${name}-error.log";
 | 
						|
                    roll = true;
 | 
						|
                    inherit roll_size_mb;
 | 
						|
                  };
 | 
						|
                  include = [ "http.log.access.${name}" ];
 | 
						|
                };
 | 
						|
              }) hostname_map);
 | 
						|
          }
 | 
						|
        );
 | 
						|
      };
 | 
						|
 | 
						|
    systemd.services.caddy.serviceConfig = {
 | 
						|
 | 
						|
      # Allows Caddy to serve lower ports (443, 80)
 | 
						|
      AmbientCapabilities = "CAP_NET_BIND_SERVICE";
 | 
						|
 | 
						|
      # Prevent flooding of logs by rate-limiting
 | 
						|
      LogRateLimitIntervalSec = "5s"; # Limit period
 | 
						|
      LogRateLimitBurst = 100; # Limit threshold
 | 
						|
 | 
						|
    };
 | 
						|
 | 
						|
    # Required for web traffic to reach this machine
 | 
						|
    networking.firewall.allowedTCPPorts = [
 | 
						|
      80
 | 
						|
      443
 | 
						|
    ];
 | 
						|
 | 
						|
    # HTTP/3 QUIC uses UDP (not sure if being used)
 | 
						|
    networking.firewall.allowedUDPPorts = [ 443 ];
 | 
						|
 | 
						|
    # Caddy exposes Prometheus metrics with the admin API
 | 
						|
    # https://caddyserver.com/docs/api
 | 
						|
    nmasur.presets.services.prometheus-exporters.scrapeTargets = [ "127.0.0.1:2019" ];
 | 
						|
  };
 | 
						|
}
 |