mirror of
https://github.com/nmasur/dotfiles
synced 2024-12-27 08:44:52 +00:00
224 lines
7.7 KiB
Nix
224 lines
7.7 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,
|
|
...
|
|
}:
|
|
{
|
|
|
|
options = {
|
|
caddy = {
|
|
tlsPolicies = lib.mkOption {
|
|
type = lib.types.listOf lib.types.attrs;
|
|
description = "Caddy JSON TLS 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 = [ ];
|
|
};
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf config.services.caddy.enable {
|
|
|
|
# Force Caddy to 403 if not coming from allowlisted source
|
|
caddy.cidrAllowlist = [ "127.0.0.1/32" ];
|
|
caddy.routes = lib.mkBefore [
|
|
{
|
|
match = [ { not = [ { remote_ip.ranges = config.caddy.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 config.caddy.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
|
|
{
|
|
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 = config.caddy.routes;
|
|
errors.routes = config.caddy.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;
|
|
};
|
|
};
|
|
apps.http.servers.metrics = { }; # Enables Prometheus metrics
|
|
apps.tls.automation.policies = config.caddy.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
|
|
prometheus.scrapeTargets = [ "127.0.0.1:2019" ];
|
|
};
|
|
}
|