# heather — Caddy как единый edge (TLS termination, reverse proxy). # # Заменяет Traefik (k3s built-in, отключён) и старые Caddy на iscg-dev/jul11. # Паттерн как на iscg-dev: system Caddy, конфиг декларативно в nix. # # Caddy терминирует TLS на :80/:443 и роутит по Host на: # - host-сервисы (syncthing GUI на localhost:8384) — напрямую # - docker-контейнеры (forgejo на localhost:3030 и т.д.) — проброшенные порты # # ACME: пока staging (чтобы не забанили Let's Encrypt пока DNS не переведён на # heather). Когда A-записи iscg.dev → 51.250.45.111 — переключим на production # (убрать acme_ca, Caddy использует production LE по умолчанию). { config, pkgs, lib, ... }: let # caddy/basic-auth под паролем. hash сгенерить: # caddy hash-password --plaintext '' # (на heather: nix-shell -p caddy --run 'caddy hash-password ...') # Защищает syncthing GUI снаружи (GUI слушает только 127.0.0.1:8384, # Caddy проксирует sync.iscg.dev + basicauth). syncBasicAuth = '' basicauth { mikl $2a$14$ozHN9/aa4VhPX43NN8MKdODKvOoJ5MBpOUFhzkxwvb58ZI5b6eqEK } ''; in { services.caddy = { enable = true; # DNS переведён на heather (2026-07-12). Production ACME. # Если нужен staging обратно: раскомментировать acmeCA ниже. # acmeCA = "https://acme-staging-v02.api.letsencrypt.org/directory"; email = "mikl@iscg.dev"; virtualHosts = { # --- Syncthing GUI (host-сервис) --- # header_up Host 127.0.0.1:8384 — обходим syncthing host-check (DNS # rebinding protection): syncthing сравнивает Host header с адресом # прослушивания (127.0.0.1:8384) и отдаёт 403 "Host check error" для # внешних доменов. Подменяем Host на бэкенде. "sync.iscg.dev".extraConfig = syncBasicAuth + '' reverse_proxy 127.0.0.1:8384 { header_up Host 127.0.0.1:8384 } ''; # --- Контейнеры (arion, см. arion-compose.nix) --- # Forgejo "git.iscg.dev".extraConfig = '' handle /robots.txt { header Content-Type text/plain respond "User-agent: * Allow: /" 200 } reverse_proxy 127.0.0.1:3030 ''; # Matrix tuwunel (homeserver) "iscg.dev".extraConfig = '' # Static file sharing (https://iscg.dev/files/). # handle_path stripает /files/ prefix → file_server ищет в root. # Не конфликтует с matrix (использует /_matrix/*, /.well-known/matrix). handle_path /files/* { root * /var/www/files file_server } reverse_proxy 127.0.0.1:8008 encode zstd gzip ''; # Matrix federation "iscg.dev:8448".extraConfig = '' reverse_proxy 127.0.0.1:8008 ''; # tuwunel-admin "admin.iscg.dev".extraConfig = '' reverse_proxy 127.0.0.1:8009 ''; # Cinny (matrix web client) "chat.iscg.dev".extraConfig = '' reverse_proxy 127.0.0.1:8080 encode zstd gzip ''; # Jitsi Meet (Caddy v2 auto-handles WebSocket upgrade) # Docs: https://jitsi.github.io/handbook/docs/devops-guide/devops-guide-docker#running-behind-a-reverse-proxy "jitsi.iscg.dev".extraConfig = '' reverse_proxy 127.0.0.1:8000 ''; # Teable "teable.iscg.dev".extraConfig = '' reverse_proxy 127.0.0.1:2345 ''; # Silverbullet "note.iscg.dev".extraConfig = '' reverse_proxy 127.0.0.1:3001 ''; # Homepage (dashboard со всеми сервисами heather) "home.iscg.dev".extraConfig = '' reverse_proxy 127.0.0.1:3002 ''; # Public OAuth consent pages for the personal gcalcli application. "gcalcli.iscg.dev".extraConfig = '' root * /etc/gcalcli-site try_files {path} {path}.html file_server encode zstd gzip ''; # paseo daemon endpoint для desktop/mobile-клиентов. # Basic Auth здесь намеренно нет: мобильный клиент Paseo его не умеет. # Доступ защищён встроенным PASEO_PASSWORD самого демона. "paseo.iscg.dev".extraConfig = '' reverse_proxy 127.0.0.1:6767 ''; # JupyterLab: native login form (пароль хранится как bcrypt-хеш # в /var/lib/jupyter-secrets/jupyter.env), наружу только HTTPS. "jupyter.iscg.dev".extraConfig = '' reverse_proxy 127.0.0.1:8890 ''; # Nick timer prototype. Public timer pages; creation has no auth for now. "timer.michaotic.space".extraConfig = '' reverse_proxy 127.0.0.1:8787 ''; # File explorer (directory listing) для раздачи файлов через браузер. # browse включает Caddy's built-in directory listing. Корень /var/www/pub. # Без auth — класть только публичные файлы. "files.iscg.dev".extraConfig = '' # Retired preview: landing and API live only on help2travel.ru. @help2travel_preview path /help2travel /help2travel/* handle @help2travel_preview { respond 404 } handle { root * /var/www/pub file_server browse } ''; # Help2Travel: static landing independent of the polling bot. "help2travel.ru".extraConfig = '' encode zstd gzip header { X-Content-Type-Options nosniff Referrer-Policy strict-origin-when-cross-origin Content-Security-Policy "default-src 'self'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'" } handle /api/* { reverse_proxy 127.0.0.1:8791 { header_up X-Client-IP {remote_host} } } handle { root * /var/www/help2travel file_server } ''; "www.help2travel.ru".extraConfig = '' redir https://help2travel.ru{uri} permanent ''; }; }; }