{ config, lib, pkgs, homeyConfig, ... }: # Caddy reverse proxy. # # Features: # - DNS-01 ACME via Cloudflare API → real wildcard cert for *.home.zakobar.com # - forward_auth to Authelia for protected vhosts # - Plain reverse_proxy for public vhosts (authelia itself, nextcloud) # - Listens on :80 (redirect) and :443 (TLS) # # Because nixpkgs ships Caddy without the cloudflare DNS plugin by default, # we build a custom Caddy with it using the xcaddy wrapper from nixpkgs. # # Secrets consumed from sops: # cloudflare/api_token let cfg = config.homey.caddy; domain = homeyConfig.domain; # Build Caddy with the Cloudflare DNS plugin using the nixos-25.05 API. # `withPlugins` is a passthru function on the caddy package; it uses xcaddy # under the hood to produce a fixed-output derivation. caddyWithCloudflare = pkgs.caddy.withPlugins { plugins = [ "github.com/caddy-dns/cloudflare@v0.2.2-0.20250724223520-f589a18c0f5d" ]; hash = "sha256-2Fb2fgM7YhWk9kBnnNGb85MJkAkgzXiI1fb6eK3ykIE="; }; # Reusable Authelia forward_auth snippet # Returns a Caddyfile snippet block that applies forward_auth. # copy_headers makes Authelia's Remote-* headers available downstream. autheliaForwardAuth = '' forward_auth localhost:9091 { uri /api/verify?rd=https://auth.${domain} copy_headers Remote-User Remote-Name Remote-Groups Remote-Email # On auth failure, redirect to the authelia login page @goauth status 401 handle_response @goauth { redir https://auth.${domain}?rm={method} 302 } } ''; in { options.homey.caddy = { enable = lib.mkEnableOption "Caddy reverse proxy"; acmeEmail = lib.mkOption { type = lib.types.str; default = "admin@zakobar.com"; description = "Email for Let's Encrypt ACME registration."; }; }; config = lib.mkIf cfg.enable { # ----------------------------------------------------------------------- # Secrets # ----------------------------------------------------------------------- sops.secrets."cloudflare/api_token" = { owner = config.services.caddy.user; }; # ----------------------------------------------------------------------- # Caddy service # ----------------------------------------------------------------------- services.caddy = { enable = true; package = caddyWithCloudflare; # Global options globalConfig = '' email ${cfg.acmeEmail} # Use Cloudflare DNS-01 challenge for wildcard cert acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN} ''; # Each virtual host virtualHosts = { # ------------------------------------------------------------------ # Authelia — public, no auth gate (it IS the auth gate) # ------------------------------------------------------------------ "auth.${domain}" = { extraConfig = '' reverse_proxy localhost:9091 ''; }; # ------------------------------------------------------------------ # Gitea — protected behind one_factor Authelia # ------------------------------------------------------------------ "git.${domain}" = { extraConfig = '' ${autheliaForwardAuth} reverse_proxy localhost:3000 ''; }; # ------------------------------------------------------------------ # Nextcloud — public auth (Nextcloud manages its own users + LDAP) # Authelia is not gating nextcloud directly because NC has its own # login flow. We still want HTTPS. # ------------------------------------------------------------------ "nextcloud.${domain}" = { extraConfig = '' # Redirect CardDAV/CalDAV discovery redir /.well-known/carddav /remote.php/dav/ 301 redir /.well-known/caldav /remote.php/dav/ 301 # Large uploads (5 GB) request_body { max_size 5GB } reverse_proxy localhost:8080 ''; }; # ------------------------------------------------------------------ # phpLDAPadmin — two_factor, admins only (enforced by authelia policy) # ------------------------------------------------------------------ "ldapadmin.${domain}" = { extraConfig = '' ${autheliaForwardAuth} reverse_proxy localhost:8081 ''; }; # ------------------------------------------------------------------ # Jellyfin — one_factor (added when enabled) # ------------------------------------------------------------------ "jellyfin.${domain}" = { extraConfig = '' ${autheliaForwardAuth} reverse_proxy localhost:8096 ''; }; # ------------------------------------------------------------------ # Transmission — two_factor, admins only (enforced by authelia policy) # ------------------------------------------------------------------ "torrent.${domain}" = { extraConfig = '' ${autheliaForwardAuth} reverse_proxy localhost:9092 ''; # NOTE: transmission is bound to 9092 to avoid clash with authelia on 9091. }; }; }; # ----------------------------------------------------------------------- # Pass Cloudflare token as env var to the caddy systemd unit. # # The caddy-dns/cloudflare plugin reads CLOUDFLARE_API_TOKEN directly. # sops decrypts the secret to a file at runtime; we write a transient # env file to /run/ in ExecStartPre so systemd picks it up via # EnvironmentFile. The file is removed in ExecStopPost. # ----------------------------------------------------------------------- systemd.services.caddy = { serviceConfig = { EnvironmentFile = "/run/caddy-secrets.env"; ExecStartPre = [ (pkgs.writeShellScript "caddy-inject-cf-token" '' install -m 0600 /dev/null /run/caddy-secrets.env printf 'CLOUDFLARE_API_TOKEN=%s\n' \ "$(cat ${config.sops.secrets."cloudflare/api_token".path})" \ > /run/caddy-secrets.env '') ]; ExecStopPost = [ (pkgs.writeShellScript "caddy-cleanup-env" '' rm -f /run/caddy-secrets.env '') ]; }; after = lib.mkAfter [ "podman-authelia.service" ]; wants = lib.mkAfter [ "podman-authelia.service" ]; }; # ----------------------------------------------------------------------- # Firewall — open HTTP + HTTPS (already in common.nix, explicit here too) # ----------------------------------------------------------------------- networking.firewall.allowedTCPPorts = [ 80 443 ]; }; }