This commit is contained in:
Aner Zakobar
2026-05-20 23:21:36 +03:00
parent 171ff2f3bc
commit 08e8b5edbe
17 changed files with 419 additions and 322 deletions
+52 -222
View File
@@ -65,6 +65,38 @@ in
default = "admin@zakobar.com";
description = "Email for Let's Encrypt ACME registration.";
};
virtualHosts = lib.mkOption {
type = lib.types.listOf (lib.types.submodule {
options = {
subdomain = lib.mkOption {
type = lib.types.str;
description = "Subdomain under homeyConfig.domain (e.g. \"mealie\" mealie.zakobar.com).";
};
port = lib.mkOption {
type = lib.types.port;
description = "Host port to reverse-proxy to.";
};
auth = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Prepend Authelia forward_auth to this vhost.";
};
extraConfig = lib.mkOption {
type = lib.types.str;
default = "";
description = "Replaces the auto-generated 'reverse_proxy localhost:<port>' for HTTPS. Empty = use default.";
};
extraHttpConfig = lib.mkOption {
type = lib.types.str;
default = "";
description = "Replaces the auto-generated cfProxy for the HTTP loopback vhost. Empty = use default.";
};
};
});
default = [];
description = "Virtual hosts to generate. Each service module contributes its own entries.";
};
};
config = lib.mkIf cfg.enable {
@@ -89,229 +121,27 @@ in
acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
'';
# Each virtual host.
# Each virtual host is generated from homey.caddy.virtualHosts entries.
# Each service module contributes its own entries to that list.
#
# Each service gets two vhost entries:
# - "host" (no scheme) → Caddy handles HTTPS + auto cert (for LAN access)
# - "http://host" → plain HTTP for cloudflared on loopback (no redirect)
#
# Caddy auto-redirects HTTP→HTTPS only when no explicit http:// vhost exists.
# By defining http:// explicitly we suppress that redirect so cloudflared
# (which talks plain HTTP on port 80) gets a direct response.
virtualHosts = {
# ------------------------------------------------------------------
# Authelia — public, no auth gate (it IS the auth gate)
# ------------------------------------------------------------------
"auth.${domain}" = {
extraConfig = ''
reverse_proxy localhost:9091
'';
};
"http://auth.${domain}" = {
extraConfig = cfProxy 9091;
};
# ------------------------------------------------------------------
# Gitea — no forward_auth; git HTTP clients can't handle SSO redirects.
# Access control is handled by Gitea itself (LDAP auth + private repos).
# ------------------------------------------------------------------
"git.${domain}" = {
extraConfig = ''
reverse_proxy localhost:3000
'';
};
"http://git.${domain}" = {
extraConfig = cfProxy 3000;
};
# ------------------------------------------------------------------
# Nextcloud — public auth (Nextcloud manages its own users + LDAP)
# ------------------------------------------------------------------
"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 {
header_up X-Forwarded-For {remote_host}
}
'';
};
"http://nextcloud.${domain}" = {
extraConfig = ''
redir /.well-known/carddav /remote.php/dav/ 301
redir /.well-known/caldav /remote.php/dav/ 301
request_body {
max_size 5GB
}
reverse_proxy localhost:8080 {
header_up X-Forwarded-Proto https
header_up X-Forwarded-For {remote_host}
}
'';
};
# ------------------------------------------------------------------
# phpLDAPadmin — two_factor, admins only (enforced by authelia policy)
# ------------------------------------------------------------------
"ldapadmin.${domain}" = {
extraConfig = ''
${autheliaForwardAuth}
reverse_proxy localhost:8081
'';
};
"http://ldapadmin.${domain}" = {
extraConfig = ''
${autheliaForwardAuth}
${cfProxy 8081}
'';
};
# ------------------------------------------------------------------
# Jellyfin — no forward_auth; Jellyfin has its own login UI and
# native app clients can't handle SSO redirects.
# ------------------------------------------------------------------
"jellyfin.${domain}" = {
extraConfig = ''
reverse_proxy localhost:8096
'';
};
"http://jellyfin.${domain}" = {
extraConfig = cfProxy 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.
};
"http://torrent.${domain}" = {
extraConfig = ''
${autheliaForwardAuth}
${cfProxy 9092}
'';
};
# ------------------------------------------------------------------
# Uptime Kuma — two_factor, admins only (enforced by authelia policy)
# ------------------------------------------------------------------
"uptime.${domain}" = {
extraConfig = ''
${autheliaForwardAuth}
reverse_proxy localhost:3001
'';
};
"http://uptime.${domain}" = {
extraConfig = ''
${autheliaForwardAuth}
${cfProxy 3001}
'';
};
# ------------------------------------------------------------------
# Ntfy — no forward_auth; ntfy has its own token/password auth so the
# mobile app can connect without Authelia SSO complications.
# ------------------------------------------------------------------
"ntfy.${domain}" = {
extraConfig = ''
reverse_proxy localhost:2586
'';
};
"http://ntfy.${domain}" = {
extraConfig = cfProxy 2586;
};
# ------------------------------------------------------------------
# Eurovision Vote — one_factor for all authenticated users.
# /admin/* is restricted to group:admins by Authelia access_control.
# Caddy passes Remote-User → X-Remote-User so Django auto-logs in
# the SSO-authenticated user via RemoteUserMiddleware.
# ------------------------------------------------------------------
"eurovision-vote.${domain}" = {
extraConfig = ''
${autheliaForwardAuth}
reverse_proxy localhost:8007 {
header_up X-Remote-User {http.request.header.Remote-User}
}
'';
};
"http://eurovision-vote.${domain}" = {
extraConfig = ''
${autheliaForwardAuth}
reverse_proxy localhost:8007 {
header_up X-Forwarded-Proto https
header_up X-Remote-User {http.request.header.Remote-User}
}
'';
};
# ------------------------------------------------------------------
# Paperless — one_factor for all authenticated users (authelia policy).
# Authelia sets Remote-User; Caddy copies it to the upstream request;
# Paperless trusts HTTP_REMOTE_USER for automatic login (no separate
# Paperless login page shown).
# ------------------------------------------------------------------
"paperless.${domain}" = {
extraConfig = ''
${autheliaForwardAuth}
reverse_proxy localhost:8083
'';
};
"http://paperless.${domain}" = {
extraConfig = ''
${autheliaForwardAuth}
${cfProxy 8083}
'';
};
# ------------------------------------------------------------------
# Mealie — no forward_auth; LDAP handles auth via Mealie's login page.
# ------------------------------------------------------------------
"mealie.${domain}" = {
extraConfig = ''
reverse_proxy localhost:9093
'';
};
"http://mealie.${domain}" = {
extraConfig = cfProxy 9093;
};
# ------------------------------------------------------------------
# Grafana — two_factor, admins only (enforced by authelia policy).
# After Authelia verifies the user, Caddy maps the Remote-User header
# to X-WEBAUTH-USER so Grafana's proxy auth auto-signs the user in.
# ------------------------------------------------------------------
"grafana.${domain}" = {
extraConfig = ''
${autheliaForwardAuth}
reverse_proxy localhost:3002 {
header_up X-WEBAUTH-USER {http.request.header.Remote-User}
}
'';
};
"http://grafana.${domain}" = {
extraConfig = ''
${autheliaForwardAuth}
reverse_proxy localhost:3002 {
header_up X-Forwarded-Proto https
header_up X-WEBAUTH-USER {http.request.header.Remote-User}
}
'';
};
};
# Each entry produces two Caddy vhosts:
# - "subdomain.domain" → HTTPS (LAN access + Let's Encrypt cert)
# - "http://subdomain.domain" → plain HTTP for cloudflared loopback
virtualHosts = lib.listToAttrs (
lib.concatMap (vh:
let
d = "${vh.subdomain}.${domain}";
authSnip = lib.optionalString vh.auth autheliaForwardAuth;
httpsBody = if vh.extraConfig != "" then vh.extraConfig
else "reverse_proxy localhost:${toString vh.port}\n";
httpBody = if vh.extraHttpConfig != "" then vh.extraHttpConfig
else cfProxy vh.port;
in [
{ name = d; value.extraConfig = "${authSnip}${httpsBody}"; }
{ name = "http://${d}"; value.extraConfig = "${authSnip}${httpBody}"; }
]
) cfg.virtualHosts
);
};
# -----------------------------------------------------------------------