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
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.
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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> \
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user