gateway-routing
npx skills add https://github.com/ionfury/homelab --skill gateway-routing
Agent 安装分布
Skill 文档
Gateway Routing
The homelab uses Kubernetes Gateway API with Istio as the gateway controller. Two gateways handle traffic:
- internal — accessible only within the home network
- external — accessible from the internet, protected by Coraza WAF
All gateway resources live in the istio-gateway namespace. HTTPRoutes in any namespace reference these gateways via parentRefs.
Gateway Selection Decision Tree
Does this service need internet access?
|
+-- YES --> external gateway
| - Domain: *.${external_domain}
| - TLS: letsencrypt-production (Cloudflare DNS-01)
| - WAF: Coraza OWASP CRS active
| - IP: ${external_ingress_ip} (Cilium LB)
|
+-- NO --> internal gateway
| - Domain: *.${internal_domain}
| - TLS: homelab-ca (self-signed CA)
| - WAF: None
| - IP: ${internal_ingress_ip} (Cilium LB)
|
+-- BOTH -> Create two HTTPRoutes (one per gateway)
Examples: Authelia, Immich, Kromgo
Rule of thumb: Most platform dashboards (Grafana, Prometheus, Alertmanager, Longhorn, Hubble, Garage) are internal-only. User-facing apps (Authelia, Immich, Zipline) need external access and often also an internal route for LAN users.
Creating an HTTPRoute
Step 1: Choose Gateway and Hostname
Determine which gateway (or both) your service needs and the subdomain.
Step 2: Create the HTTPRoute YAML
Internal-only route (most common for platform services):
---
# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/gateway.networking.k8s.io/httproute_v1.json
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: my-app
spec:
parentRefs:
- name: internal
namespace: istio-gateway
hostnames:
- "my-app.${internal_domain}"
rules:
- backendRefs:
- name: my-app-service
port: 8080
External route (internet-facing, WAF-protected):
---
# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/gateway.networking.k8s.io/httproute_v1.json
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: my-app-external
namespace: my-app
spec:
parentRefs:
- name: external
namespace: istio-gateway
hostnames:
- "my-app.${external_domain}"
rules:
- backendRefs:
- name: my-app-service
port: 8080
Step 3: Place the Route File
| Service Type | Location | Example |
|---|---|---|
| Platform service | kubernetes/platform/config/<subsystem>/ |
config/monitoring/grafana-route.yaml |
| Cluster-specific app | kubernetes/clusters/<cluster>/config/<app>/ |
clusters/live/config/authelia/external-route.yaml |
Add the route file to the subsystem’s kustomization.yaml.
Step 4: Network Policy
Ensure the app namespace has the correct network policy profile label:
| Gateway Used | Required Profile |
|---|---|
| Internal only | internal or internal-egress |
| External only | standard |
| Both | standard |
Set in kubernetes/platform/namespaces.yaml:
- name: my-app
labels:
network-policy.homelab/profile: standard
parentRefs Structure
The parentRefs field links an HTTPRoute to a Gateway listener. Key details:
parentRefs:
- name: internal # Gateway name: "internal" or "external"
namespace: istio-gateway # Gateways live in istio-gateway namespace
sectionName: https # Optional: target specific listener (https or http)
- namespace is required when the HTTPRoute is in a different namespace than the Gateway (which is always the case — gateways are in
istio-gateway, routes are in app namespaces or the gateway namespace for platform routes) - sectionName is optional. Omit it to match any listener. Use
httponly for redirect routes. - Both gateways use
allowedRoutes.namespaces.from: Allon the HTTPS listener, so any namespace can attach routes.
Route Patterns from the Codebase
Simple Backend (most common)
rules:
- backendRefs:
- name: service-name
port: 80
Used by: Grafana, Longhorn, Hubble, Alertmanager, Prometheus, Garage, Kromgo.
Dual Gateway Exposure (external + internal)
Create two separate HTTPRoute resources, one per gateway. Examples:
authelia-external+authelia-internal(same backend, different gateways/domains)immich-external+immich-internalkromgo-external+kromgo-internal
The routes are identical except for parentRefs.name and hostnames domain.
HTTP-to-HTTPS Redirect (platform-managed)
Both gateways have automatic HTTP-to-HTTPS redirects configured in config/gateway/http-to-https-redirect.yaml. You do not need to create redirect routes for new services.
TLS Certificate Setup
Architecture
Certificates are provisioned at the gateway level, not per-route. Each gateway has a wildcard certificate:
| Gateway | Certificate | Secret | Issuer | Domain |
|---|---|---|---|---|
| external | external |
external-tls |
${tls_issuer:-cloudflare} |
*.${external_domain} |
| internal | internal |
internal-tls |
${tls_issuer:-cloudflare} |
*.${internal_domain} |
The tls_issuer variable defaults to cloudflare (Let’s Encrypt DNS-01) but can be overridden to homelab-ca per cluster via .cluster-vars.env.
ClusterIssuers
| Issuer Name | Type | Use Case | Secret Source |
|---|---|---|---|
cloudflare |
ACME (DNS-01) | Public certs via Let’s Encrypt | ExternalSecret from SSM (cloudflare-api-token) |
homelab-ca |
CA | Internal services, dev/integration clusters | ExternalSecret from SSM (homelab-ingress-root-ca) |
istio-mesh-ca |
CA | Istio mesh mTLS (workload identity) | ExternalSecret from SSM (shared across clusters) |
Adding a New Subdomain
No certificate changes needed — the wildcard *.${external_domain} and *.${internal_domain} cover all subdomains. Just create the HTTPRoute.
Debugging TLS Issues
# Check certificate status
KUBECONFIG=~/.kube/<cluster>.yaml kubectl get certificates -n istio-gateway
# Check certificate details (Ready condition)
KUBECONFIG=~/.kube/<cluster>.yaml kubectl describe certificate external -n istio-gateway
# Check issuer health
KUBECONFIG=~/.kube/<cluster>.yaml kubectl get clusterissuers
# Check CertificateRequests (shows issuance attempts)
KUBECONFIG=~/.kube/<cluster>.yaml kubectl get certificaterequests -n istio-gateway
# Check the actual TLS secret
KUBECONFIG=~/.kube/<cluster>.yaml kubectl get secret external-tls -n istio-gateway -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -text
# If cert is stuck, check cert-manager logs
KUBECONFIG=~/.kube/<cluster>.yaml kubectl logs -n cert-manager deploy/cert-manager -f
| Symptom | Likely Cause | Fix |
|---|---|---|
| Certificate not Ready | Issuer secret missing | Check ExternalSecret sync for cloudflare-api-token |
| ACME challenge failing | DNS propagation / API token issue | Verify Cloudflare token has Zone:DNS:Edit permission |
homelab-ca not Ready |
Root CA secret missing | Check ExternalSecret for homelab-ingress-root-ca |
| Browser TLS warning (internal) | Self-signed CA not trusted | Expected for homelab-ca; add CA to trusted store or use -k flag |
Coraza WAF (External Gateway Only)
How It Works
The Coraza Web Application Firewall runs as an Istio WasmPlugin attached only to the external gateway:
# kubernetes/platform/config/gateway/coraza-wasm-plugin.yaml
apiVersion: extensions.istio.io/v1alpha1
kind: WasmPlugin
metadata:
name: coraza-waf
spec:
selector:
matchLabels:
gateway.networking.k8s.io/gateway-name: external # External only
url: oci://ghcr.io/corazawaf/coraza-proxy-wasm:0.6.0@sha256:...
phase: AUTHN # Runs before authentication
failStrategy: FAIL_OPEN # Traffic flows if WAF errors
pluginConfig:
directives_map:
default:
- Include @recommended-conf
- Include @crs-setup-conf
- Include @owasp_crs/*.conf
- SecRuleEngine On
- SecAction "id:900000,phase:1,pass,t:none,nolog,setvar:tx.blocking_paranoia_level=1"
Key settings:
- Paranoia Level 1: Lowest false positive rate, catches common attacks
- FAIL_OPEN: Prioritizes availability over security — if WASM fails to load, traffic passes unfiltered
- AUTHN phase: WAF runs early in the filter chain, before any authentication checks
- External only: Internal gateway traffic is not filtered by WAF
FAIL_OPEN Implications
If the WASM binary fails to load (wrong digest, image unavailable, OOM), traffic flows unfiltered. Check gateway pod logs for:
error in converting the wasm config to local: cannot fetch Wasm module...
applying allow RBAC filter
WAF Rule Customization
Rules are inlined in the WasmPlugin spec (Istio WasmPlugin does not support volume mounts). The coraza-config.yaml ConfigMap serves as documentation only.
To disable a rule causing false positives:
# Add to the directives_map.default array in coraza-wasm-plugin.yaml
- SecRuleRemoveById 920350 # Example: Host header validation
Testing WAF-Protected Endpoints
Istio gateway listeners match on SNI (Server Name Indication). Raw IP requests are rejected:
# WRONG -- no SNI, connection reset
curl -kI "https://192.168.10.53/"
# CORRECT -- send proper SNI with --resolve
GATEWAY_IP=$(KUBECONFIG=~/.kube/<cluster>.yaml kubectl get gateway external -n istio-gateway -o jsonpath='{.metadata.annotations.lbipam\.cilium\.io/ips}')
curl -kI --resolve "app.${external_domain}:443:${GATEWAY_IP}" \
"https://app.${external_domain}/"
Attack Pattern Verification (expect 403)
# SQL Injection
curl -k --resolve "app.${external_domain}:443:${GATEWAY_IP}" \
"https://app.${external_domain}/?id=1'%20OR%20'1'='1"
# XSS
curl -k --resolve "app.${external_domain}:443:${GATEWAY_IP}" \
"https://app.${external_domain}/?q=<script>alert(1)</script>"
# Command Injection
curl -k --resolve "app.${external_domain}:443:${GATEWAY_IP}" \
"https://app.${external_domain}/?cmd=;cat%20/etc/passwd"
WAF Monitoring
| Metric | What It Shows |
|---|---|
istio_requests_total{source_workload=~"external-istio", response_code="403"} |
WAF-blocked requests |
istio_requests_total{source_workload=~"external-istio"} |
Total external gateway traffic |
Alerts configured in config/gateway/coraza-waf-rules.yaml:
| Alert | Condition | Meaning |
|---|---|---|
CorazaWAFDegraded |
No Istio metrics from external gateway for 5m | Gateway may not be processing traffic |
CorazaWAFHighBlockRate |
>10% of requests returning 403 for 10m | Possible attack or WAF false positives |
CorazaWAFHighLatency |
p99 gateway latency >50ms for 5m | WAF overhead too high, tune rule exclusions |
Common Issues
| Issue | Cause | Resolution |
|---|---|---|
| Route not working | Missing namespace: istio-gateway in parentRefs |
Add namespace to parentRefs |
| 404 on valid hostname | HTTPRoute not attached to gateway | Check parentRefs gateway name matches exactly |
| Connection reset on external | SNI mismatch (testing with IP) | Use --resolve flag with proper hostname |
| Pods unreachable from gateway | Missing network policy profile | Add network-policy.homelab/profile label to namespace |
| 503 Service Unavailable | Backend service not found or port wrong | Verify service name and port in backendRefs |
| Both internal and external needed | Only one route created | Create two separate HTTPRoute resources |
| WAF blocking legitimate traffic | False positive on CRS rule | Add SecRuleRemoveById <ID> to WasmPlugin directives |
Cross-References
| Document | Focus |
|---|---|
kubernetes/platform/config/gateway/ |
Gateway definitions, WAF config |
kubernetes/platform/config/issuers/ |
ClusterIssuer definitions |
kubernetes/platform/config/certs/ |
Certificate resources |
kubernetes/platform/config/network-policy/CLAUDE.md |
Network policy profiles |
kubernetes/platform/CLAUDE.md |
Variable substitution, platform structure |
deploy-app skill |
Full app deployment workflow including routing |