Compare commits

...
1 Commits
Author SHA1 Message Date
Denozordec e15768b25b feat(firewall): implement public HTTPS endpoints for firewall scripts and enhance URL handling
CI / changes (push) Successful in 15s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 1m0s
CI / go (push) Successful in 1m0s
CI / bird2 (push) Successful in 16s
CI / release (push) Successful in 3m39s
Added public HTTPS endpoints for firewall installation and enrollment scripts, allowing access without API keys. Updated the URL handling in the firewall code to ensure all suggested control plane URLs are served over HTTPS. Enhanced documentation to reflect the new public endpoints and their usage. Updated tests to verify the correct behavior of the new URL handling logic.
2026-07-08 18:54:27 +07:00
9 changed files with 53 additions and 16 deletions
+12 -2
View File
@@ -34,6 +34,16 @@ import {
} from '@/queries/firewall' } from '@/queries/firewall'
import type { BgpCommunity, FirewallClient } from '@/types/api' 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')({ export const Route = createFileRoute('/_auth/firewall')({
component: FirewallPage, component: FirewallPage,
}) })
@@ -51,13 +61,13 @@ function FirewallPage() {
const [clientName, setClientName] = useState('web-01') const [clientName, setClientName] = useState('web-01')
const [cpUrl, setCpUrl] = useState(() => 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('') const [seed, setSeed] = useState('')
useEffect(() => { useEffect(() => {
if (installCtx?.suggested_cp_url) { if (installCtx?.suggested_cp_url) {
setCpUrl(installCtx.suggested_cp_url) setCpUrl(httpsOrigin(installCtx.suggested_cp_url))
} }
if (installCtx?.bundle_seed) { if (installCtx?.bundle_seed) {
setSeed(installCtx.bundle_seed) setSeed(installCtx.bundle_seed)
@@ -159,10 +159,18 @@ services:
condition: service_started condition: service_started
labels: labels:
- traefik.enable=true - 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.rule=Host(`${WEBUI_DOMAIN}`)
- traefik.http.routers.evobgp-web.entrypoints=websecure - traefik.http.routers.evobgp-web.entrypoints=websecure
- traefik.http.routers.evobgp-web.tls=true - traefik.http.routers.evobgp-web.tls=true
- traefik.http.routers.evobgp-web.tls.certresolver=letsencrypt - 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.routers.evobgp-web.middlewares=webui-ipwhitelist@docker
- traefik.http.middlewares.webui-ipwhitelist.ipallowlist.sourcerange=${WEBUI_IP_WHITELIST} - traefik.http.middlewares.webui-ipwhitelist.ipallowlist.sourcerange=${WEBUI_IP_WHITELIST}
- traefik.http.services.evobgp-web.loadbalancer.server.port=80 - traefik.http.services.evobgp-web.loadbalancer.server.port=80
+8
View File
@@ -247,10 +247,18 @@ services:
- evobgp-all - evobgp-all
labels: labels:
- traefik.enable=true - 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.rule=Host(`${WEBUI_DOMAIN}`)
- traefik.http.routers.evobgp-web.entrypoints=websecure - traefik.http.routers.evobgp-web.entrypoints=websecure
- traefik.http.routers.evobgp-web.tls=true - traefik.http.routers.evobgp-web.tls=true
- traefik.http.routers.evobgp-web.tls.certresolver=letsencrypt - 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.routers.evobgp-web.middlewares=webui-ipwhitelist@docker
- traefik.http.middlewares.webui-ipwhitelist.ipallowlist.sourcerange=${WEBUI_IP_WHITELIST} - traefik.http.middlewares.webui-ipwhitelist.ipallowlist.sourcerange=${WEBUI_IP_WHITELIST}
- traefik.http.services.evobgp-web.loadbalancer.server.port=80 - traefik.http.services.evobgp-web.loadbalancer.server.port=80
+8
View File
@@ -162,10 +162,18 @@ services:
condition: service_started condition: service_started
labels: labels:
- traefik.enable=true - 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.rule=Host(`${WEBUI_DOMAIN}`)
- traefik.http.routers.evobgp-web.entrypoints=websecure - traefik.http.routers.evobgp-web.entrypoints=websecure
- traefik.http.routers.evobgp-web.tls=true - traefik.http.routers.evobgp-web.tls=true
- traefik.http.routers.evobgp-web.tls.certresolver=letsencrypt - 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.routers.evobgp-web.middlewares=webui-ipwhitelist@docker
- traefik.http.middlewares.webui-ipwhitelist.ipallowlist.sourcerange=${WEBUI_IP_WHITELIST} - traefik.http.middlewares.webui-ipwhitelist.ipallowlist.sourcerange=${WEBUI_IP_WHITELIST}
- traefik.http.services.evobgp-web.loadbalancer.server.port=80 - traefik.http.services.evobgp-web.loadbalancer.server.port=80
+1 -1
View File
@@ -17,7 +17,7 @@ server {
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 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 { location = /metrics {
+2
View File
@@ -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 ```bash
curl -fsSL https://<api>/v1/firewall/install.sh | \ curl -fsSL https://<api>/v1/firewall/install.sh | \
EVOBGP_CP_URL=https://<api> \ EVOBGP_CP_URL=https://<api> \
+1
View File
@@ -153,6 +153,7 @@ docker compose --env-file .env --env-file .env.web-sec --profile microvps-full u
- `http://<WEBUI_DOMAIN>` должен редиректить на `https://<WEBUI_DOMAIN>`; - `http://<WEBUI_DOMAIN>` должен редиректить на `https://<WEBUI_DOMAIN>`;
- с IP из `WEBUI_IP_WHITELIST` UI доступен по HTTPS; - с IP из `WEBUI_IP_WHITELIST` UI доступен по HTTPS;
- с неразрешенного IP Traefik вернет `403`. - с неразрешенного 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://<IP>:8080/v1/health`. Health API: `http://<IP>:8080/v1/health`.
+9 -12
View File
@@ -50,20 +50,13 @@ func (s *Server) handleFirewallInstallContext(w http.ResponseWriter, r *http.Req
writeJSON(w, http.StatusOK, map[string]any{ writeJSON(w, http.StatusOK, map[string]any{
"bundle_seed": seed, "bundle_seed": seed,
"bundle_seed_configured": seed != "", "bundle_seed_configured": seed != "",
"suggested_cp_url": requestBaseURL(r), "suggested_cp_url": publicHTTPSBaseURL(r),
"install_sh_url": requestBaseURL(r) + "/v1/firewall/install.sh", "install_sh_url": publicHTTPSBaseURL(r) + "/v1/firewall/install.sh",
}) })
} }
func requestBaseURL(r *http.Request) string { // publicHTTPSBaseURL is the external HTTPS origin for firewall install/enroll links.
scheme := "https" func publicHTTPSBaseURL(r *http.Request) string {
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"
}
}
host := strings.TrimSpace(r.Host) host := strings.TrimSpace(r.Host)
if xf := strings.TrimSpace(r.Header.Get("X-Forwarded-Host")); xf != "" { if xf := strings.TrimSpace(r.Header.Get("X-Forwarded-Host")); xf != "" {
host = strings.TrimSpace(strings.Split(xf, ",")[0]) host = strings.TrimSpace(strings.Split(xf, ",")[0])
@@ -71,7 +64,11 @@ func requestBaseURL(r *http.Request) string {
if host == "" { if host == "" {
return "" 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) { func (s *Server) handleFirewallEnrollPublic(w http.ResponseWriter, r *http.Request) {
+4 -1
View File
@@ -186,7 +186,10 @@ func TestFirewallInstallContext(t *testing.T) {
if configured, _ := ctx["bundle_seed_configured"].(bool); !configured { if configured, _ := ctx["bundle_seed_configured"].(bool); !configured {
t.Fatal("bundle_seed_configured want true") 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) t.Fatalf("install_sh_url=%q", url)
} }