nix/modules/networking/caddy.nix
pazpi 49eb3ba2f0
Some checks failed
Auto Update Build / build (pull_request) Has been cancelled
Fix caddy build hash
2026-02-15 23:22:06 +01:00

263 lines
7.1 KiB
Nix

{
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}"
"*.ts.${domainConfig.domain}"
];
dnsProvider = "cloudflare";
dnsResolver = "1.1.1.1:53";
dnsPropagationCheck = true;
environmentFile = domainConfig.cloudflareApiKeyFile;
};
}) cfg.domainsList
);
};
services.caddy = {
enable = true;
package = pkgs.caddy.withPlugins {
hash = "sha256-nEjcy5FljOoWC5EdumxBA3R7Bgfo2XcTr7gCVfdPDxE=";
plugins = [
"github.com/caddy-dns/cloudflare@v0.2.2-0.20250724223520-f589a18c0f5d"
"github.com/mholt/caddy-dynamicdns@v0.0.0-20250430031602-b846b9e8fb83"
];
};
globalConfig = ''
admin :2024
servers {
metrics
}
''
+ lib.concatStringsSep "\n" (
map (dynamicdnsDomain: ''
dynamic_dns {
provider cloudflare {env.${dynamicdnsDomain.cloudflareApiEnvName}}
domains {
${dynamicdnsDomain.domain} @ *
}
}
'') 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 *
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://${extraVirtualHost.subdomain}.${extraVirtualHost.domain}
'';
}
) { } 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
];
};
}