diff --git a/apps/web/src/routes/_auth/firewall.tsx b/apps/web/src/routes/_auth/firewall.tsx index b035e79..5ce3c99 100644 --- a/apps/web/src/routes/_auth/firewall.tsx +++ b/apps/web/src/routes/_auth/firewall.tsx @@ -34,6 +34,16 @@ import { } from '@/queries/firewall' import type { BgpCommunity, FirewallClient } from '@/types/api' +function httpsOrigin(origin: string): string { + try { + const u = new URL(origin) + u.protocol = 'https:' + return u.origin + } catch { + return origin.replace(/^http:/i, 'https:') + } +} + export const Route = createFileRoute('/_auth/firewall')({ component: FirewallPage, }) @@ -51,13 +61,13 @@ function FirewallPage() { const [clientName, setClientName] = useState('web-01') const [cpUrl, setCpUrl] = useState(() => - typeof window !== 'undefined' ? window.location.origin : 'https://api.example.com', + typeof window !== 'undefined' ? httpsOrigin(window.location.origin) : 'https://api.example.com', ) const [seed, setSeed] = useState('') useEffect(() => { if (installCtx?.suggested_cp_url) { - setCpUrl(installCtx.suggested_cp_url) + setCpUrl(httpsOrigin(installCtx.suggested_cp_url)) } if (installCtx?.bundle_seed) { setSeed(installCtx.bundle_seed) diff --git a/deploy/compose/docker-compose.production.example.yaml b/deploy/compose/docker-compose.production.example.yaml index 5a68009..2745677 100644 --- a/deploy/compose/docker-compose.production.example.yaml +++ b/deploy/compose/docker-compose.production.example.yaml @@ -159,10 +159,18 @@ services: condition: service_started labels: - traefik.enable=true + # Публичные firewall-эндпоинты — без WEBUI_IP_WHITELIST (установка с произвольных серверов). + - traefik.http.routers.evobgp-firewall-public.rule=Host(`${WEBUI_DOMAIN}`) && (Path(`/v1/firewall/install.sh`) || Path(`/v1/firewall/sync-script`) || PathPrefix(`/v1/firewall/enroll`)) + - traefik.http.routers.evobgp-firewall-public.entrypoints=websecure + - traefik.http.routers.evobgp-firewall-public.tls=true + - traefik.http.routers.evobgp-firewall-public.tls.certresolver=letsencrypt + - traefik.http.routers.evobgp-firewall-public.priority=100 + - traefik.http.routers.evobgp-firewall-public.service=evobgp-web - traefik.http.routers.evobgp-web.rule=Host(`${WEBUI_DOMAIN}`) - traefik.http.routers.evobgp-web.entrypoints=websecure - traefik.http.routers.evobgp-web.tls=true - traefik.http.routers.evobgp-web.tls.certresolver=letsencrypt + - traefik.http.routers.evobgp-web.priority=10 - traefik.http.routers.evobgp-web.middlewares=webui-ipwhitelist@docker - traefik.http.middlewares.webui-ipwhitelist.ipallowlist.sourcerange=${WEBUI_IP_WHITELIST} - traefik.http.services.evobgp-web.loadbalancer.server.port=80 diff --git a/deploy/compose/docker-compose.yaml b/deploy/compose/docker-compose.yaml index 048abba..a3ccd7e 100644 --- a/deploy/compose/docker-compose.yaml +++ b/deploy/compose/docker-compose.yaml @@ -247,10 +247,18 @@ services: - evobgp-all labels: - traefik.enable=true + # Публичные firewall-эндпоинты — без WEBUI_IP_WHITELIST (установка с произвольных серверов). + - traefik.http.routers.evobgp-firewall-public.rule=Host(`${WEBUI_DOMAIN}`) && (Path(`/v1/firewall/install.sh`) || Path(`/v1/firewall/sync-script`) || PathPrefix(`/v1/firewall/enroll`)) + - traefik.http.routers.evobgp-firewall-public.entrypoints=websecure + - traefik.http.routers.evobgp-firewall-public.tls=true + - traefik.http.routers.evobgp-firewall-public.tls.certresolver=letsencrypt + - traefik.http.routers.evobgp-firewall-public.priority=100 + - traefik.http.routers.evobgp-firewall-public.service=evobgp-web - traefik.http.routers.evobgp-web.rule=Host(`${WEBUI_DOMAIN}`) - traefik.http.routers.evobgp-web.entrypoints=websecure - traefik.http.routers.evobgp-web.tls=true - traefik.http.routers.evobgp-web.tls.certresolver=letsencrypt + - traefik.http.routers.evobgp-web.priority=10 - traefik.http.routers.evobgp-web.middlewares=webui-ipwhitelist@docker - traefik.http.middlewares.webui-ipwhitelist.ipallowlist.sourcerange=${WEBUI_IP_WHITELIST} - traefik.http.services.evobgp-web.loadbalancer.server.port=80 diff --git a/deploy/compose/stack.microvps-full.yaml b/deploy/compose/stack.microvps-full.yaml index 0ace3cb..31fb19a 100644 --- a/deploy/compose/stack.microvps-full.yaml +++ b/deploy/compose/stack.microvps-full.yaml @@ -162,10 +162,18 @@ services: condition: service_started labels: - traefik.enable=true + # Публичные firewall-эндпоинты — без WEBUI_IP_WHITELIST (установка с произвольных серверов). + - traefik.http.routers.evobgp-firewall-public.rule=Host(`${WEBUI_DOMAIN}`) && (Path(`/v1/firewall/install.sh`) || Path(`/v1/firewall/sync-script`) || PathPrefix(`/v1/firewall/enroll`)) + - traefik.http.routers.evobgp-firewall-public.entrypoints=websecure + - traefik.http.routers.evobgp-firewall-public.tls=true + - traefik.http.routers.evobgp-firewall-public.tls.certresolver=letsencrypt + - traefik.http.routers.evobgp-firewall-public.priority=100 + - traefik.http.routers.evobgp-firewall-public.service=evobgp-web - traefik.http.routers.evobgp-web.rule=Host(`${WEBUI_DOMAIN}`) - traefik.http.routers.evobgp-web.entrypoints=websecure - traefik.http.routers.evobgp-web.tls=true - traefik.http.routers.evobgp-web.tls.certresolver=letsencrypt + - traefik.http.routers.evobgp-web.priority=10 - traefik.http.routers.evobgp-web.middlewares=webui-ipwhitelist@docker - traefik.http.middlewares.webui-ipwhitelist.ipallowlist.sourcerange=${WEBUI_IP_WHITELIST} - traefik.http.services.evobgp-web.loadbalancer.server.port=80 diff --git a/deploy/docker/evobgp-web/nginx.conf b/deploy/docker/evobgp-web/nginx.conf index 4f88710..7b25710 100644 --- a/deploy/docker/evobgp-web/nginx.conf +++ b/deploy/docker/evobgp-web/nginx.conf @@ -17,7 +17,7 @@ server { 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; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; } location = /metrics { diff --git a/docs/firewall.md b/docs/firewall.md index 0d6a2fd..3ee2121 100644 --- a/docs/firewall.md +++ b/docs/firewall.md @@ -21,6 +21,8 @@ ## Установка на сервер +Публичные URL (без API-ключа, вне `WEBUI_IP_WHITELIST` Traefik): `GET /v1/firewall/install.sh`, `GET /v1/firewall/sync-script`, `POST /v1/firewall/enroll`. Всегда **HTTPS**. + ```bash curl -fsSL https:///v1/firewall/install.sh | \ EVOBGP_CP_URL=https:// \ diff --git a/docs/quickstart.md b/docs/quickstart.md index 3476347..9544bf5 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -153,6 +153,7 @@ docker compose --env-file .env --env-file .env.web-sec --profile microvps-full u - `http://` должен редиректить на `https://`; - с IP из `WEBUI_IP_WHITELIST` UI доступен по HTTPS; - с неразрешенного IP Traefik вернет `403`. +- исключение: `GET /v1/firewall/install.sh`, `GET /v1/firewall/sync-script`, `POST /v1/firewall/enroll` — публичные, без whitelist (см. [firewall.md](firewall.md)). Health API: `http://:8080/v1/health`. diff --git a/internal/httpapi/routes_firewall.go b/internal/httpapi/routes_firewall.go index 1a82965..51a95a5 100644 --- a/internal/httpapi/routes_firewall.go +++ b/internal/httpapi/routes_firewall.go @@ -50,20 +50,13 @@ func (s *Server) handleFirewallInstallContext(w http.ResponseWriter, r *http.Req writeJSON(w, http.StatusOK, map[string]any{ "bundle_seed": seed, "bundle_seed_configured": seed != "", - "suggested_cp_url": requestBaseURL(r), - "install_sh_url": requestBaseURL(r) + "/v1/firewall/install.sh", + "suggested_cp_url": publicHTTPSBaseURL(r), + "install_sh_url": publicHTTPSBaseURL(r) + "/v1/firewall/install.sh", }) } -func requestBaseURL(r *http.Request) string { - scheme := "https" - if r.TLS == nil { - if xf := strings.TrimSpace(r.Header.Get("X-Forwarded-Proto")); xf != "" { - scheme = strings.ToLower(strings.Split(xf, ",")[0]) - } else if strings.EqualFold(r.URL.Scheme, "http") { - scheme = "http" - } - } +// publicHTTPSBaseURL is the external HTTPS origin for firewall install/enroll links. +func publicHTTPSBaseURL(r *http.Request) string { host := strings.TrimSpace(r.Host) if xf := strings.TrimSpace(r.Header.Get("X-Forwarded-Host")); xf != "" { host = strings.TrimSpace(strings.Split(xf, ",")[0]) @@ -71,7 +64,11 @@ func requestBaseURL(r *http.Request) string { if host == "" { return "" } - return scheme + "://" + host + return "https://" + host +} + +func requestBaseURL(r *http.Request) string { + return publicHTTPSBaseURL(r) } func (s *Server) handleFirewallEnrollPublic(w http.ResponseWriter, r *http.Request) { diff --git a/internal/httpapi/routes_firewall_test.go b/internal/httpapi/routes_firewall_test.go index cc57ede..ae0a640 100644 --- a/internal/httpapi/routes_firewall_test.go +++ b/internal/httpapi/routes_firewall_test.go @@ -186,7 +186,10 @@ func TestFirewallInstallContext(t *testing.T) { if configured, _ := ctx["bundle_seed_configured"].(bool); !configured { t.Fatal("bundle_seed_configured want true") } - if url, _ := ctx["install_sh_url"].(string); !strings.HasSuffix(url, "/v1/firewall/install.sh") { + if url, _ := ctx["suggested_cp_url"].(string); !strings.HasPrefix(url, "https://") { + t.Fatalf("suggested_cp_url=%q want https", url) + } + if url, _ := ctx["install_sh_url"].(string); !strings.HasPrefix(url, "https://") || !strings.HasSuffix(url, "/v1/firewall/install.sh") { t.Fatalf("install_sh_url=%q", url) }