From fda16a85b7579bae4af03b1994c62ed2504f3092 Mon Sep 17 00:00:00 2001 From: mulatta <67085791+mulatta@users.noreply.github.com> Date: Fri, 3 Jul 2026 03:38:55 +0900 Subject: [PATCH 1/7] github: manage Pages with dedicated resource The GitHub provider deprecated the inline pages block on repositories, and keeping it there left a persistent plan diff. Move Pages management to github_repository_pages so live state converges cleanly. --- terraform/github/repo.tf | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/terraform/github/repo.tf b/terraform/github/repo.tf index cadb248..dd5410f 100644 --- a/terraform/github/repo.tf +++ b/terraform/github/repo.tf @@ -25,13 +25,11 @@ resource "github_repository" "infra" { allow_update_branch = true web_commit_signoff_required = true - pages { - build_type = "workflow" - source { - branch = "main" - path = "/" - } - } +} + +resource "github_repository_pages" "infra" { + repository = github_repository.infra.name + build_type = "workflow" } resource "github_repository_ruleset" "infra" { From 473307da13bbb6da5aca85072cb91b2b2571902c Mon Sep 17 00:00:00 2001 From: mulatta <67085791+mulatta@users.noreply.github.com> Date: Fri, 3 Jul 2026 00:04:20 +0900 Subject: [PATCH 2/7] terraform: stop publishing internal service names Keep Cloudflare DNS limited to endpoints that need public resolution so private services cannot be discovered through the public zone. --- terraform/cloudflare/main.tf | 40 ------------------------------------ 1 file changed, 40 deletions(-) diff --git a/terraform/cloudflare/main.tf b/terraform/cloudflare/main.tf index 45930ef..e7fe720 100644 --- a/terraform/cloudflare/main.tf +++ b/terraform/cloudflare/main.tf @@ -54,16 +54,6 @@ resource "cloudflare_dns_record" "buildbot" { comment = "Nixbot CI/CD edge proxy (eta -> psi)" } -resource "cloudflare_dns_record" "s3" { - zone_id = data.sops_file.secrets.data["CLOUDFLARE_ZONE_ID"] - name = "s3.sjanglab.org" - content = "141.164.53.203" - type = "A" - ttl = 300 - proxied = false - comment = "MinIO S3 API" -} - resource "cloudflare_dns_record" "ntfy" { zone_id = data.sops_file.secrets.data["CLOUDFLARE_ZONE_ID"] name = "ntfy.sjanglab.org" @@ -74,16 +64,6 @@ resource "cloudflare_dns_record" "ntfy" { comment = "ntfy notification service" } -resource "cloudflare_dns_record" "logging" { - zone_id = data.sops_file.secrets.data["CLOUDFLARE_ZONE_ID"] - name = "logging.sjanglab.org" - content = "141.164.53.203" - type = "A" - ttl = 300 - proxied = false - comment = "Grafana logging dashboard" -} - resource "cloudflare_dns_record" "headscale" { zone_id = data.sops_file.secrets.data["CLOUDFLARE_ZONE_ID"] name = "hs.sjanglab.org" @@ -104,26 +84,6 @@ resource "cloudflare_dns_record" "authentik" { comment = "Authentik SSO server" } -resource "cloudflare_dns_record" "vaultwarden" { - zone_id = data.sops_file.secrets.data["CLOUDFLARE_ZONE_ID"] - name = "vault.sjanglab.org" - content = "141.164.53.203" - type = "A" - ttl = 300 - proxied = false - comment = "Vaultwarden password manager" -} - -resource "cloudflare_dns_record" "gatus" { - zone_id = data.sops_file.secrets.data["CLOUDFLARE_ZONE_ID"] - name = "gatus.sjanglab.org" - content = "141.164.53.203" - type = "A" - ttl = 300 - proxied = false - comment = "Gatus status monitoring" -} - resource "cloudflare_dns_record" "n8n" { zone_id = data.sops_file.secrets.data["CLOUDFLARE_ZONE_ID"] name = "n8n.sjanglab.org" From f91d542f4d3b0fab4426d22259e20c4d4511d069 Mon Sep 17 00:00:00 2001 From: mulatta <67085791+mulatta@users.noreply.github.com> Date: Fri, 3 Jul 2026 00:05:48 +0900 Subject: [PATCH 3/7] monitoring: serve Grafana through private auth Keep dashboards reachable to administrators without relying on a public Cloudflare record or eta public ingress. --- hosts/eta.nix | 14 ++++- hosts/rho.nix | 1 + modules/headscale/acl-rules.nix | 1 + modules/headscale/default.nix | 5 ++ modules/monitoring/reverse-proxy.nix | 76 ++++++++++++++++------------ 5 files changed, 63 insertions(+), 34 deletions(-) diff --git a/hosts/eta.nix b/hosts/eta.nix index 82afe80..2030a98 100644 --- a/hosts/eta.nix +++ b/hosts/eta.nix @@ -14,7 +14,6 @@ in ../modules/uptermd ../modules/gatus ../modules/monitoring/vector - ../modules/monitoring/reverse-proxy.nix ../modules/n8n/reverse-proxy.nix ../modules/acme/sync.nix ]; @@ -36,6 +35,12 @@ in serviceName = "acme-sync-docling-to-psi"; remoteHost = hosts.psi.wg-admin; } + { + domain = "logging.sjanglab.org"; + serviceName = "acme-sync-logging-to-rho"; + remoteUser = "acme-sync-logging"; + remoteHost = hosts.rho.wg-admin; + } { domain = "vllm.sjanglab.org"; serviceName = "acme-sync-vllm-to-psi"; @@ -61,6 +66,13 @@ in group = "acme"; }; + security.acme.certs."logging.sjanglab.org" = { + dnsProvider = "cloudflare"; + environmentFile = config.sops.secrets.cloudflare-credentials.path; + webroot = null; + group = "acme"; + }; + networking.hostName = "eta"; system.stateVersion = "25.05"; } diff --git a/hosts/rho.nix b/hosts/rho.nix index 967598e..ba830e1 100644 --- a/hosts/rho.nix +++ b/hosts/rho.nix @@ -9,6 +9,7 @@ ../modules/borgbackup/rho/client.nix ../modules/borgbackup/mirror.nix ../modules/monitoring/vector/monitor-systems.nix + ../modules/monitoring/reverse-proxy.nix ]; disko.rootDisk = "/dev/disk/by-id/nvme-eui.00000000000000006479a79cdac0038a"; diff --git a/modules/headscale/acl-rules.nix b/modules/headscale/acl-rules.nix index 284cb98..f1fb573 100644 --- a/modules/headscale/acl-rules.nix +++ b/modules/headscale/acl-rules.nix @@ -26,6 +26,7 @@ "tag:ai:443" "tag:apps:80" "tag:apps:443" + "tag:monitoring:443" "tag:monitoring:3000" ]; } diff --git a/modules/headscale/default.nix b/modules/headscale/default.nix index e210b8e..f30eb0f 100644 --- a/modules/headscale/default.nix +++ b/modules/headscale/default.nix @@ -56,6 +56,11 @@ in type = "A"; value = "100.64.0.1"; # psi headscale IP } + { + name = "logging.sjanglab.org"; + type = "A"; + value = "100.64.0.2"; # rho headscale IP + } { name = "upterm.sjanglab.org"; type = "A"; diff --git a/modules/monitoring/reverse-proxy.nix b/modules/monitoring/reverse-proxy.nix index d22354d..93a9edd 100644 --- a/modules/monitoring/reverse-proxy.nix +++ b/modules/monitoring/reverse-proxy.nix @@ -1,44 +1,54 @@ -# Grafana reverse proxy (deployed on eta) -# Proxies requests to rho where Grafana is running on wg-admin -{ config, ... }: +{ + config, + ... +}: let - inherit (config.networking.sbee) hosts; + inherit (config.networking.sbee) currentHost hosts; + authentikAuth = import ../authentik/nginx-locations.nix { inherit hosts; }; loggingDomain = "logging.sjanglab.org"; + certDir = "/var/lib/acme/${loggingDomain}"; in { - imports = [ ../acme ]; + imports = [ ../acme/sync.nix ]; - services.nginx.virtualHosts.${loggingDomain} = { - forceSSL = true; - useACMEHost = loggingDomain; + acmeSyncer.mkReceiver = [ + { + domain = loggingDomain; + user = "acme-sync-logging"; + } + ]; - locations = { - "/" = { - proxyPass = "http://${hosts.rho.wg-admin}:3000"; - extraConfig = '' - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - ''; - }; - # Grafana live (WebSocket) - "/api/live/" = { - proxyPass = "http://${hosts.rho.wg-admin}:3000"; - proxyWebsockets = true; - extraConfig = '' - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - ''; + services.nginx = { + enable = true; + + virtualHosts.${loggingDomain} = { + forceSSL = true; + sslCertificate = "${certDir}/fullchain.pem"; + sslCertificateKey = "${certDir}/key.pem"; + + locations = authentikAuth.locations // { + "/" = { + proxyPass = "http://${currentHost.wg-admin}:3000"; + extraConfig = authentikAuth.protectLocation + '' + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + ''; + }; + "/api/live/" = { + proxyPass = "http://${currentHost.wg-admin}:3000"; + proxyWebsockets = true; + extraConfig = authentikAuth.protectLocation + '' + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + ''; + }; }; }; }; - security.acme.certs.${loggingDomain} = { - dnsProvider = "cloudflare"; - environmentFile = config.sops.secrets.cloudflare-credentials.path; - group = "nginx"; - }; + networking.firewall.interfaces.tailscale0.allowedTCPPorts = [ 443 ]; } From 2b0de41110bca4f854e6cb82dfc273fc8477b374 Mon Sep 17 00:00:00 2001 From: mulatta <67085791+mulatta@users.noreply.github.com> Date: Fri, 3 Jul 2026 00:06:12 +0900 Subject: [PATCH 4/7] gatus: publish status page inside the tailnet Expose the status dashboard on a Headscale-resolved name without browser auth, while keeping the HTTPS vhost reachable only from tailnet clients. Internal users can inspect service health without sharing a dashboard password, and no public DNS record is published for the service. --- docs/admin/monitoring.md | 2 +- docs/admin/network.md | 2 +- docs/admin/security.md | 2 +- hosts/eta.nix | 14 ++++++++++ hosts/rho.nix | 1 + modules/gatus/check.nix | 5 +--- modules/gatus/default.nix | 48 +++++---------------------------- modules/gatus/reverse-proxy.nix | 39 +++++++++++++++++++++++++++ modules/headscale/default.nix | 5 ++++ 9 files changed, 69 insertions(+), 49 deletions(-) create mode 100644 modules/gatus/reverse-proxy.nix diff --git a/docs/admin/monitoring.md b/docs/admin/monitoring.md index 7a635a9..f54a585 100644 --- a/docs/admin/monitoring.md +++ b/docs/admin/monitoring.md @@ -3,7 +3,7 @@ ## 대시보드 - **Grafana**: `https://logging.sjanglab.org` (익명 Viewer 접근 가능, wg-admin 경유) -- **Gatus**: `https://gatus.sjanglab.org` (외부 상태 페이지) +- **Gatus**: `https://status.sjanglab.org` (tailnet 내부 공개 상태 페이지) ## 스택 구성 diff --git a/docs/admin/network.md b/docs/admin/network.md index ad254f2..bc8fe1b 100644 --- a/docs/admin/network.md +++ b/docs/admin/network.md @@ -124,7 +124,7 @@ sequenceDiagram | 호스트 | 외부 개방 포트 | wg-admin 개방 포트 | |--------|--------------|-------------------| -| eta | 80, 443, 10022 (SSH + Rate limiting), 2323 (Upterm relay) | 10022 | +| eta | 80, 443, 10022 (SSH + Rate limiting), 2323 (Upterm relay) | 10022, 8081 (Gatus) | | psi | — | 80/443 (Nixbot upstream), 10022, 5000 (Harmonia), 5432 (Nixbot/PostgreSQL) | | rho | — | 10022, 5432 (PostgreSQL), 3000 (Grafana) | | tau | — | 10022, 5678 (n8n 웹훅) | diff --git a/docs/admin/security.md b/docs/admin/security.md index 2b71da2..acdc6cc 100644 --- a/docs/admin/security.md +++ b/docs/admin/security.md @@ -237,4 +237,4 @@ flowchart LR ### 서비스 복구 -systemd가 서비스 실패 시 자동 재시작합니다. 모니터링은 Gatus(`gatus.sjanglab.org`)에서 헬스체크를 수행합니다. +systemd가 서비스 실패 시 자동 재시작합니다. 모니터링은 Gatus(`status.sjanglab.org`)에서 헬스체크를 수행합니다. diff --git a/hosts/eta.nix b/hosts/eta.nix index 2030a98..b837462 100644 --- a/hosts/eta.nix +++ b/hosts/eta.nix @@ -35,6 +35,12 @@ in serviceName = "acme-sync-docling-to-psi"; remoteHost = hosts.psi.wg-admin; } + { + domain = "status.sjanglab.org"; + serviceName = "acme-sync-status-to-rho"; + remoteUser = "acme-sync-status"; + remoteHost = hosts.rho.wg-admin; + } { domain = "logging.sjanglab.org"; serviceName = "acme-sync-logging-to-rho"; @@ -66,6 +72,14 @@ in group = "acme"; }; + + security.acme.certs."status.sjanglab.org" = { + dnsProvider = "cloudflare"; + environmentFile = config.sops.secrets.cloudflare-credentials.path; + webroot = null; + group = "acme"; + }; + security.acme.certs."logging.sjanglab.org" = { dnsProvider = "cloudflare"; environmentFile = config.sops.secrets.cloudflare-credentials.path; diff --git a/hosts/rho.nix b/hosts/rho.nix index ba830e1..4d4fa2c 100644 --- a/hosts/rho.nix +++ b/hosts/rho.nix @@ -10,6 +10,7 @@ ../modules/borgbackup/mirror.nix ../modules/monitoring/vector/monitor-systems.nix ../modules/monitoring/reverse-proxy.nix + ../modules/gatus/reverse-proxy.nix ]; disko.rootDisk = "/dev/disk/by-id/nvme-eui.00000000000000006479a79cdac0038a"; diff --git a/modules/gatus/check.nix b/modules/gatus/check.nix index 343ebdf..574583f 100644 --- a/modules/gatus/check.nix +++ b/modules/gatus/check.nix @@ -18,7 +18,7 @@ }: let cfg = config.gatusCheck; - gatusApi = "https://gatus.sjanglab.org"; + gatusApi = "http://${config.networking.sbee.hosts.eta.wg-admin}:8081"; # Gatus external endpoint key: "${group}_${name}" with spaces/special chars → hyphens mkKey = @@ -135,9 +135,6 @@ in message = "gatusCheck.push '${ep.name}': exactly one of 'url' or 'systemdService' must be set"; }) cfg.push; - # Resolve gatus.sjanglab.org → eta WG IP for hosts behind NAT/WG - networking.hosts.${config.networking.sbee.hosts.eta.wg-admin} = [ "gatus.sjanglab.org" ]; - sops.secrets.gatus-push-token = { sopsFile = ./secrets.yaml; }; diff --git a/modules/gatus/default.nix b/modules/gatus/default.nix index b15270d..4cfb9ff 100644 --- a/modules/gatus/default.nix +++ b/modules/gatus/default.nix @@ -1,29 +1,22 @@ { config, lib, ... }: let inherit (config.networking.sbee) hosts; - domain = "gatus.sjanglab.org"; port = 8081; # 8080 is used by headscale systemCollector = hosts.rho.wg-admin; cfg = config.gatusCheck; in { - imports = [ - ../acme - ./check.nix - ]; + imports = [ ./check.nix ]; services.gatus = { enable = true; environmentFile = config.sops.secrets.gatus-env.path; settings = { - web.port = port; - metrics = true; - - security.basic = { - username = "admin"; - # bcrypt hash → base64 encoded, interpolated from environmentFile - password-bcrypt-base64 = "\${GATUS_SECURITY_BASIC_PASSWORD}"; + web = { + address = "0.0.0.0"; + inherit port; }; + metrics = true; alerting.ntfy = { topic = "gatus"; @@ -63,9 +56,7 @@ in # psi (mkExtEndpoint "Nixbot" "ci") (mkExtEndpoint "Nixbot PostgreSQL" "ci") - (mkExtEndpoint "Ollama" "ai") (mkExtEndpoint "Docling" "ai") - (mkExtEndpoint "vLLM" "ai") # tau (mkExtEndpoint "Nextcloud" "apps") (mkExtEndpoint "n8n" "apps") @@ -82,30 +73,6 @@ in sopsFile = ./secrets.yaml; }; - # ACME certificate (DNS challenge via Cloudflare) - security.acme.certs.${domain} = { - dnsProvider = "cloudflare"; - environmentFile = config.sops.secrets.cloudflare-credentials.path; - webroot = null; - group = "nginx"; - }; - - # Nginx reverse proxy - services.nginx.virtualHosts.${domain} = { - forceSSL = true; - useACMEHost = domain; - locations."/" = { - proxyPass = "http://127.0.0.1:${toString port}"; - proxyWebsockets = true; - extraConfig = '' - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - ''; - }; - }; - # Vector: scrape Gatus /metrics → push to rho Prometheus # Independent source+sink pair, does not interfere with existing Vector pipeline services.vector.settings = { @@ -123,8 +90,5 @@ in }; }; - networking.firewall.allowedTCPPorts = [ - 80 - 443 - ]; + networking.firewall.interfaces.wg-admin.allowedTCPPorts = [ port ]; } diff --git a/modules/gatus/reverse-proxy.nix b/modules/gatus/reverse-proxy.nix new file mode 100644 index 0000000..122e5bd --- /dev/null +++ b/modules/gatus/reverse-proxy.nix @@ -0,0 +1,39 @@ +{ config, ... }: +let + inherit (config.networking.sbee) hosts; + domain = "status.sjanglab.org"; + certDir = "/var/lib/acme/${domain}"; +in +{ + imports = [ ../acme/sync.nix ]; + + acmeSyncer.mkReceiver = [ + { + inherit domain; + user = "acme-sync-status"; + } + ]; + + services.nginx = { + enable = true; + + virtualHosts.${domain} = { + forceSSL = true; + sslCertificate = "${certDir}/fullchain.pem"; + sslCertificateKey = "${certDir}/key.pem"; + + locations."/" = { + proxyPass = "http://${hosts.eta.wg-admin}:8081"; + proxyWebsockets = true; + extraConfig = '' + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + ''; + }; + }; + }; + + networking.firewall.interfaces.tailscale0.allowedTCPPorts = [ 443 ]; +} diff --git a/modules/headscale/default.nix b/modules/headscale/default.nix index f30eb0f..291940e 100644 --- a/modules/headscale/default.nix +++ b/modules/headscale/default.nix @@ -56,6 +56,11 @@ in type = "A"; value = "100.64.0.1"; # psi headscale IP } + { + name = "status.sjanglab.org"; + type = "A"; + value = "100.64.0.2"; # rho headscale IP + } { name = "logging.sjanglab.org"; type = "A"; From 4ac79462ebb93b602fa809ba0f30fa4561343321 Mon Sep 17 00:00:00 2001 From: mulatta <67085791+mulatta@users.noreply.github.com> Date: Fri, 3 Jul 2026 00:06:47 +0900 Subject: [PATCH 5/7] vaultwarden: keep password access inside the tailnet Preserve the existing eta data service while moving browser ingress behind a Headscale-resolved tau proxy. --- docs/admin/network.md | 2 +- hosts/eta.nix | 13 +++++++++ hosts/tau.nix | 1 + modules/headscale/default.nix | 5 ++++ modules/vaultwarden/default.nix | 39 ++++----------------------- modules/vaultwarden/reverse-proxy.nix | 38 ++++++++++++++++++++++++++ 6 files changed, 63 insertions(+), 35 deletions(-) create mode 100644 modules/vaultwarden/reverse-proxy.nix diff --git a/docs/admin/network.md b/docs/admin/network.md index bc8fe1b..3bd6def 100644 --- a/docs/admin/network.md +++ b/docs/admin/network.md @@ -127,7 +127,7 @@ sequenceDiagram | eta | 80, 443, 10022 (SSH + Rate limiting), 2323 (Upterm relay) | 10022, 8081 (Gatus) | | psi | — | 80/443 (Nixbot upstream), 10022, 5000 (Harmonia), 5432 (Nixbot/PostgreSQL) | | rho | — | 10022, 5432 (PostgreSQL), 3000 (Grafana) | -| tau | — | 10022, 5678 (n8n 웹훅) | +| tau | — | 10022, 5678 (n8n 웹훅), 8000 (Vaultwarden) | ## ACME 인증서 diff --git a/hosts/eta.nix b/hosts/eta.nix index b837462..fc57713 100644 --- a/hosts/eta.nix +++ b/hosts/eta.nix @@ -47,6 +47,12 @@ in remoteUser = "acme-sync-logging"; remoteHost = hosts.rho.wg-admin; } + { + domain = "vault.sjanglab.org"; + serviceName = "acme-sync-vaultwarden-to-tau"; + remoteUser = "acme-sync-vaultwarden"; + remoteHost = hosts.tau.wg-admin; + } { domain = "vllm.sjanglab.org"; serviceName = "acme-sync-vllm-to-psi"; @@ -87,6 +93,13 @@ in group = "acme"; }; + security.acme.certs."vault.sjanglab.org" = { + dnsProvider = "cloudflare"; + environmentFile = config.sops.secrets.cloudflare-credentials.path; + webroot = null; + group = "acme"; + }; + networking.hostName = "eta"; system.stateVersion = "25.05"; } diff --git a/hosts/tau.nix b/hosts/tau.nix index eadbe88..c1bd393 100644 --- a/hosts/tau.nix +++ b/hosts/tau.nix @@ -11,6 +11,7 @@ ../modules/monitoring/vector/monitor-services.nix ../modules/nextcloud ../modules/n8n + ../modules/vaultwarden/reverse-proxy.nix ]; disko.rootDisk = "/dev/disk/by-id/nvme-eui.00000000000000006479a79cdac0038f"; diff --git a/modules/headscale/default.nix b/modules/headscale/default.nix index 291940e..407c296 100644 --- a/modules/headscale/default.nix +++ b/modules/headscale/default.nix @@ -66,6 +66,11 @@ in type = "A"; value = "100.64.0.2"; # rho headscale IP } + { + name = "vault.sjanglab.org"; + type = "A"; + value = "100.64.0.3"; # tau headscale IP + } { name = "upterm.sjanglab.org"; type = "A"; diff --git a/modules/vaultwarden/default.nix b/modules/vaultwarden/default.nix index 779b569..f817f19 100644 --- a/modules/vaultwarden/default.nix +++ b/modules/vaultwarden/default.nix @@ -1,15 +1,13 @@ { config, ... }: { - imports = [ - ../acme - ../gatus/check.nix - ]; + imports = [ ../gatus/check.nix ]; gatusCheck.pull = [ { name = "Vaultwarden"; - url = "https://vault.sjanglab.org/alive"; + url = "http://127.0.0.1:8000/alive"; group = "apps"; + conditions = [ "[STATUS] == 200" ]; } ]; @@ -35,6 +33,7 @@ # Organization ORG_CREATION_USERS = "sjang.bioe@gmail.com,admin@sjanglab.org"; + ROCKET_ADDRESS = "0.0.0.0"; ROCKET_PORT = 8000; }; }; @@ -46,37 +45,9 @@ mode = "0400"; }; - # ACME certificate (DNS challenge) - security.acme.certs."vault.sjanglab.org" = { - dnsProvider = "cloudflare"; - environmentFile = config.sops.secrets.cloudflare-credentials.path; - webroot = null; - group = "nginx"; - }; - - # Nginx reverse proxy - services.nginx.virtualHosts."vault.sjanglab.org" = { - forceSSL = true; - useACMEHost = "vault.sjanglab.org"; - - locations."/" = { - proxyPass = "http://127.0.0.1:8000"; - proxyWebsockets = true; - extraConfig = '' - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - ''; - }; - }; - systemd.tmpfiles.rules = [ "d /var/backup/vaultwarden 0700 vaultwarden vaultwarden -" ]; - networking.firewall.allowedTCPPorts = [ - 80 - 443 - ]; + networking.firewall.interfaces.wg-admin.allowedTCPPorts = [ 8000 ]; } diff --git a/modules/vaultwarden/reverse-proxy.nix b/modules/vaultwarden/reverse-proxy.nix new file mode 100644 index 0000000..680113a --- /dev/null +++ b/modules/vaultwarden/reverse-proxy.nix @@ -0,0 +1,38 @@ +_: +let + domain = "vault.sjanglab.org"; + certDir = "/var/lib/acme/${domain}"; +in +{ + imports = [ ../acme/sync.nix ]; + + acmeSyncer.mkReceiver = [ + { + inherit domain; + user = "acme-sync-vaultwarden"; + } + ]; + + services.nginx = { + enable = true; + + virtualHosts.${domain} = { + forceSSL = true; + sslCertificate = "${certDir}/fullchain.pem"; + sslCertificateKey = "${certDir}/key.pem"; + + locations."/" = { + proxyPass = "http://127.0.0.1:8000"; + proxyWebsockets = true; + extraConfig = '' + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + ''; + }; + }; + }; + + networking.firewall.interfaces.tailscale0.allowedTCPPorts = [ 443 ]; +} From 4ad5b9a89514816c58c9ae09da44f99b7b182d15 Mon Sep 17 00:00:00 2001 From: mulatta <67085791+mulatta@users.noreply.github.com> Date: Fri, 3 Jul 2026 03:09:36 +0900 Subject: [PATCH 6/7] nginx: preserve host headers for private auth Authentik forward auth selects proxy providers by request host. Forward the normalized host headers to the outpost so protected internal services return an auth challenge instead of 404-backed 500 responses. Vaultwarden still runs on eta, while split-DNS sends clients to tau. Point tau's tailnet proxy at eta over wg-admin to avoid a dead localhost upstream. --- modules/authentik/nginx-locations.nix | 12 +++++++++--- modules/vaultwarden/reverse-proxy.nix | 5 +++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/modules/authentik/nginx-locations.nix b/modules/authentik/nginx-locations.nix index 066aff5..2d35bd1 100644 --- a/modules/authentik/nginx-locations.nix +++ b/modules/authentik/nginx-locations.nix @@ -27,8 +27,11 @@ in extraConfig = '' internal; proxy_pass_request_body off; - proxy_set_header Content-Length ""; - proxy_set_header X-Original-URL $scheme://$http_host$request_uri; + proxy_set_header Content-Length 0; + proxy_set_header Host $host; + proxy_set_header X-Original-URL $scheme://$host$request_uri; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Authorization $http_authorization; ''; }; @@ -37,7 +40,10 @@ in "/outpost.goauthentik.io" = { proxyPass = "${authentikOutpost}/outpost.goauthentik.io"; extraConfig = '' - proxy_set_header X-Original-URL $scheme://$http_host$request_uri; + proxy_set_header Host $host; + proxy_set_header X-Original-URL $scheme://$host$request_uri; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Authorization $http_authorization; ''; }; diff --git a/modules/vaultwarden/reverse-proxy.nix b/modules/vaultwarden/reverse-proxy.nix index 680113a..d62b965 100644 --- a/modules/vaultwarden/reverse-proxy.nix +++ b/modules/vaultwarden/reverse-proxy.nix @@ -1,5 +1,6 @@ -_: +{ config, ... }: let + inherit (config.networking.sbee) hosts; domain = "vault.sjanglab.org"; certDir = "/var/lib/acme/${domain}"; in @@ -22,7 +23,7 @@ in sslCertificateKey = "${certDir}/key.pem"; locations."/" = { - proxyPass = "http://127.0.0.1:8000"; + proxyPass = "http://${hosts.eta.wg-admin}:8000"; proxyWebsockets = true; extraConfig = '' proxy_set_header Host $host; From 09f440440151484c0a1110c5322c2a80ce4cf54e Mon Sep 17 00:00:00 2001 From: mulatta <67085791+mulatta@users.noreply.github.com> Date: Fri, 3 Jul 2026 00:07:08 +0900 Subject: [PATCH 7/7] ai: keep inactive API services dormant Avoid renewing certificates for disabled Ollama and vLLM deployments while documenting that API clients need token-style auth instead of browser redirects. --- hosts/eta.nix | 27 --------------------------- modules/ollama/default.nix | 3 ++- modules/vllm/default.nix | 2 ++ 3 files changed, 4 insertions(+), 28 deletions(-) diff --git a/hosts/eta.nix b/hosts/eta.nix index fc57713..14f1fa1 100644 --- a/hosts/eta.nix +++ b/hosts/eta.nix @@ -24,12 +24,6 @@ in serviceName = "acme-sync-to-tau"; remoteHost = hosts.tau.wg-admin; } - { - domain = "ollama.sjanglab.org"; - serviceName = "acme-sync-ollama-to-psi"; - remoteUser = "acme-sync-ollama"; - remoteHost = hosts.psi.wg-admin; - } { domain = "docling.sjanglab.org"; serviceName = "acme-sync-docling-to-psi"; @@ -53,32 +47,11 @@ in remoteUser = "acme-sync-vaultwarden"; remoteHost = hosts.tau.wg-admin; } - { - domain = "vllm.sjanglab.org"; - serviceName = "acme-sync-vllm-to-psi"; - remoteUser = "acme-sync-vllm"; - remoteHost = hosts.psi.wg-admin; - } ]; disko.rootDisk = "/dev/vda"; # ACME certificates for internal services - security.acme.certs."ollama.sjanglab.org" = { - dnsProvider = "cloudflare"; - environmentFile = config.sops.secrets.cloudflare-credentials.path; - webroot = null; - group = "acme"; - }; - - security.acme.certs."vllm.sjanglab.org" = { - dnsProvider = "cloudflare"; - environmentFile = config.sops.secrets.cloudflare-credentials.path; - webroot = null; - group = "acme"; - }; - - security.acme.certs."status.sjanglab.org" = { dnsProvider = "cloudflare"; environmentFile = config.sops.secrets.cloudflare-credentials.path; diff --git a/modules/ollama/default.nix b/modules/ollama/default.nix index c34965f..471ebf5 100644 --- a/modules/ollama/default.nix +++ b/modules/ollama/default.nix @@ -94,7 +94,8 @@ in sslCertificate = "${certDir}/fullchain.pem"; sslCertificateKey = "${certDir}/key.pem"; - # Access control: Headscale ACL (network-level, no forward auth) + # API clients do not handle browser redirects from Authentik forward auth. + # Keep application authorization in API credentials when this module is enabled. locations."/" = { proxyPass = "http://127.0.0.1:${toString port}"; recommendedProxySettings = false; # Override Host header manually diff --git a/modules/vllm/default.nix b/modules/vllm/default.nix index fa3a265..b29e453 100644 --- a/modules/vllm/default.nix +++ b/modules/vllm/default.nix @@ -135,6 +135,8 @@ in sslCertificate = "/var/lib/acme/${domain}/fullchain.pem"; sslCertificateKey = "/var/lib/acme/${domain}/key.pem"; + # API clients do not handle browser redirects from Authentik forward auth. + # Keep application authorization in API credentials when this module is enabled. locations."/" = { proxyPass = "http://127.0.0.1:8100"; extraConfig = ''