{ 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. # This compiles on the Pi (slow once, cached after). caddyWithCloudflare = pkgs.caddy.override { externalPlugins = [ { name = "github.com/caddy-dns/cloudflare"; version = "89f16b99c18ef49c8bb470a82f895bce01cbaece"; } ]; vendorHash = lib.fakeHash; # replace with real hash after first build }; # 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:9091_transmission ''; # NOTE: transmission uses 9091 too; we'll bind it to 9092 in its # module to avoid a clash with authelia. }; }; }; # ----------------------------------------------------------------------- # Pass Cloudflare token as env var to the caddy systemd unit # ----------------------------------------------------------------------- systemd.services.caddy = { serviceConfig = { EnvironmentFile = pkgs.writeText "caddy-cf-env" "CLOUDFLARE_API_TOKEN_FILE=${config.sops.secrets."cloudflare/api_token".path}"; # Caddy supports _FILE suffix for env vars via its secret file reader, # but cloudflare plugin reads CLOUDFLARE_API_TOKEN directly. # We write a wrapper ExecStartPre to populate the env var from the file: ExecStartPre = [ (pkgs.writeShellScript "caddy-inject-cf-token" '' export CLOUDFLARE_API_TOKEN=$(cat ${config.sops.secrets."cloudflare/api_token".path}) systemctl set-environment CLOUDFLARE_API_TOKEN="$CLOUDFLARE_API_TOKEN" '') ]; }; 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 ]; }; }