- Updated AGENTS.md to include new directories for execution plans and implemented feature reports. - Modified Ansible host variables for dev-02 and gpu-01 to change API endpoints and add new configurations for Scaleway. - Adjusted firewall rules in gpu-01 to allow HTTPS traffic instead of the previous Ollama port. - Added OAuth2 proxy configurations to gpu-01 for enhanced security. - Removed deprecated open-webui module from Terraform app configurations. - Updated Terraform configurations to reflect changes in local variables and removed unused secrets. - Created a new sectool.json file for CloudNS integration with Bitwarden. - Enhanced SELinux configurations for nginx to allow connections to any port and added oauth2-proxy port.
14 KiB
Gateway API Migration & Cloudflare Integration
Overview
Migrate prod-01 and dev-01 clusters from the deprecated nginx-ingress controller (retired March 2026) to NGINX Gateway Fabric via the Gateway API, improve cross-cluster WireGuard connectivity, and add Cloudflare Tunnel for edge protection.
Current Architecture
Internet → Contabo Public IP → OPNsense → prod-01
↓
nginx-ingress plugin (MicroK8s)
↓
MetalLB (10.2.0.40-49)
↓
┌───────────────┼─────────────────┐
↓ ↓ ↓
[proxy pod] [app pods] [TCP streams]
(nginx + WG) (gitea, m3uproxy, etc) (hostPort)
↓
WireGuard → aristotle (Contabo VPN)
↓
dev-01 backends (open-webui, nextcloud)
Key facts:
- 14
kubernetes_ingress_v1resources across both clusters - All use
ingress.class: public+ nginx-specific annotations - Proxy module on prod-01 creates a double-proxy (Ingress → proxy pod → backends)
- TCP/UDP streams use nginx with hostPorts
- DNS: ClouDNS, no Cloudflare involvement
- cert-manager with DNS-01 (dev-01) and HTTP-01 (prod-01) for Let's Encrypt
- Both clusters are single-node — no need for MetalLB, NodePort is sufficient
Decisions Made
| Decision | Choice | Rationale |
|---|---|---|
| Gateway API implementation | NGINX Gateway Fabric | Keeps nginx data plane, minimal behavioral changes, conformant v1.4.1 |
| Cross-cluster connectivity | Improve WireGuard | Keep through aristotle, allowed_ips to dev-01 only (10.19.4.136/32), remove gpu-01 |
| Ollama removal | Remove | gpu-01 runs llamacpp (not ollama), prod-01 ollama proxy is broken, dev-01 DNS CNAME stale |
| Cloudflare scope | Tunnels only | Get DDoS+WAF, keep DNS with ClouDNS, least disruptive |
| Proxy module | All through proxy | Gateway routes everything to proxy Service, simpler Terraform |
| Website module | Direct Gateway route | Keep separate pods, no proxy overhead for static sites |
| TLS termination | At Cloudflare edge | Simpler cert management, Cloudflare provides origin cert |
Target Architecture
Internet → Cloudflare Tunnel → Cloudflare Edge (TLS termination)
↓
cloudflared pods (DaemonSet)
↓
Gateway (NGINX Gateway Fabric, NodePort Service)
HTTP listener :80 (from cloudflared)
↓
┌───────────────┼─────────────────┐
↓ ↓ ↓
[proxy Service] [app Services] [TCP/UDP Routes]
↓ ↓ ↓
[proxy pod + WG] [direct pods] [direct pods]
(nginx) (website, etc.) (rustdesk, etc.)
↓
Cross-cluster → dev-01 backends
(open-webui, nextcloud)
Phase 1: Gateway API Migration
1A. Install NGINX Gateway Fabric (Ansible)
New file: ansible/roles/microk8s/tasks/gateway-api.yml
- Install `snap` package manager (if needed)
- Install helm via snap
- Add nginx-stable helm repo
- Install Gateway API CRDs (standard channel + experimental for TCPRoute/UDPRoute)
- Install NGINX Gateway Fabric via Helm (experimental features enabled)
- Create GatewayClass named `public`
- Create Gateway Service with type = NodePort (single-node, no MetalLB needed)
Modify: ansible/roles/microk8s/tasks/main.yml
- Include
gateway-api.ymlAFTER existing setup (runs alongside nginx-ingress initially)
Modify: ansible/roles/microk8s/defaults/main.yml
- Add variables:
gateway_fabric_version,gateway_fabric_experimental_enabled - Remove MetalLB-related vars (
microk8s_lb_provider,microk8s_lb_ips,microk8s_metallb_version)
1B. Create Gateway Module (Terraform)
New module: terraform/modules/utils/gateway/
main.tf: Gateway resource with HTTP (:80) listener,ServiceType: NodePortvariables.tf: TLS secret names, listener configoutputs.tf: Gateway service name, NodePort port numbers
Deploy in both clusters (prod-01/apps.tf, dev-01/apps.tf)
1C. Migrate Terraform Modules (Ingress → Gateway API Resources)
Proxy module (terraform/modules/utils/proxy/main.tf)
- Remove:
kubernetes_ingress_v1.ingressresource (lines 160-203) - Remove:
host_portfrom stream container ports in Deployment - Remove: ollama proxy service definition from prod-01 config.tf
- Add: HTTPRoute for cross-cluster services (open-webui, nextcloud)
- Add: TCPRoute resources for RustDesk (21115, 21116, 21117 TCP) and Prometheus (9090)
- Add: UDPRoute resource for RustDesk (21116 UDP)
- The proxy pod's nginx stream block can be removed (Gateway handles L4 routing)
- Keep: WireGuard sidecar, auth sidecar, internal nginx reverse proxy config
Website module (terraform/modules/apps/nginx/main.tf)
- Remove:
kubernetes_ingress_v1.website_ingress(lines 103-145) - Add: HTTPRoute per website with backendRef → website Service
- Gateway handles TLS termination, website pods receive plain HTTP
- Website pods can drop internal TLS (simplification)
Gitea module (terraform/modules/apps/gitea/ingress.tf)
- Remove:
kubernetes_ingress_v1.gitea_ingress - Add: HTTPRoute with backendRef → Gitea Service
- Use
BackendTLSPolicyif Gitea serves HTTPS internally
M3UProxy module (terraform/modules/apps/m3uproxy/main.tf)
- Remove:
kubernetes_ingress_v1.m3uproxy_ingress_http(lines 241-274) - Add: HTTPRoute with backendRef → m3uproxy Service
dev-01 modules
| Module | Current | Change |
|---|---|---|
open-webui/ingress.tf |
kubernetes_ingress_v1 | HTTPRoute |
grafana/grafana.tf |
kubernetes_ingress_v1 | HTTPRoute |
mstream/ingress.tf |
kubernetes_ingress_v1 | HTTPRoute |
searxng/ingress.tf |
kubernetes_ingress_v1 | HTTPRoute |
vaultwarden/ingress.tf |
kubernetes_ingress_v1 | HTTPRoute |
terraform-mcp/ingress.tf |
kubernetes_ingress_v1 | HTTPRoute |
nextcloud/ingress.tf |
kubernetes_ingress_v1 | HTTPRoute |
proxy/main.tf |
kubernetes_ingress_v1 | HTTPRoute (opencode, vscode) |
Annotation → Gateway API Mapping
| Current nginx annotation | Gateway API equivalent |
|---|---|
nginx.org/ssl-redirect: true |
HTTPRoute with RequestRedirect filter |
nginx.org/redirect-to-https: true |
HTTPRoute on HTTP listener with redirect |
nginx.ingress.kubernetes.io/backend-protocol: HTTPS |
BackendTLSPolicy resource |
nginx.ingress.kubernetes.io/proxy-body-size |
UpstreamSettings policy (NGF custom) |
nginx.ingress.kubernetes.io/proxy-request-buffering |
ProxySettings policy (NGF custom) |
nginx.ingress.kubernetes.io/proxy-buffer-size |
UpstreamSettings policy |
cert-manager.io/cluster-issuer |
cert-manager annotation on Gateway |
1D. cert-manager Integration
Use approach B: Keep existing Certificate resources, reference secrets in Gateway TLS config.
- Each module's
kubernetes_manifestCertificate resource stays unchanged - Gateway HTTPS listener references cert secrets via
certificateRefs - This avoids alpha cert-manager Gateway integration
1E. Migration Execution Order
- Apply new Ansible role (installs Gateway Fabric alongside nginx-ingress)
- Deploy Gateway resource to both clusters
- Migrate modules on dev-01 first (lower risk):
- Deploy HTTPRoutes for each module, one at a time
- Verify each service works
- Remove old Ingress resources
- Migrate modules on prod-01:
- Same process, one module at a time
- Disable nginx-ingress plugin on both clusters
- Clean up IngressClass, old LoadBalancer Service
Phase 2: Cross-Cluster Connectivity
2A. Improve WireGuard
Modify: terraform/apps/prod-01/config.tf (WireGuard config):
- Change
allowed_ipsfrom[10.19.4.106/32, 10.19.4.136/32]→["10.19.4.136/32"](dev-01 only) - Remove gpu-01 (10.19.4.106) - ollama proxy being removed, no need to reach gpu-01
persistent_keepalivealready set to 25
Modify: terraform/modules/utils/proxy/resources/wireguard/wireguard.conf.tpl:
- Template
PersistentKeepalivefrom config variable - Template
AllowedIPsfrom config list
Also clean up:
- Remove
"ollama"entry fromproxy_servicesin prod-01 config.tf - Remove
ollamaCNAME from dev-01 DNS zones in dev-01 config.tf
Modify: terraform/modules/utils/proxy/main.tf (WireGuard container):
- Improve liveness probe:
wg showinstead ofwg show | grep -q transfer - Add readiness probe
- Add resource limits (memory cap)
2B. DNS Resolution
No changes needed. The proxy's per-location resolver in default.conf.tpl already handles DNS correctly for cross-cluster backends.
Phase 3: Cloudflare Tunnels
3A. Cloudflare Setup
- Create/verify Cloudflare account with
alexpires.medomain - Keep DNS with ClouDNS (Cloudflare acts as reverse proxy only)
- Create Cloudflare Tunnel for prod-01
- Configure route rules:
https://*.alexpires.me→ Gateway Service - Set up origin rules (TLS terminates at Cloudflare edge → HTTP to cloudflared)
3B. Deploy cloudflared
New module: terraform/modules/utils/cloudflare-tunnel/
- DaemonSet of
cloudflare/cloudflaredpods - ConfigMap with tunnel credentials and route definitions
- Each public FQDN mapped to
http://<gateway-service>.<namespace>:80
3C. Gateway Configuration Changes
- Gateway uses HTTP (:80) NodePort listener for Cloudflare Tunnel traffic
- Cloudflare Tunnel connects to
http://<node_ip>:<nodeport> - No HTTPS listener needed since all traffic comes through Cloudflare Tunnel
3D. Firewall Changes
- Close UFW ports 80/443 (cloudflared uses ephemeral outbound ports)
- Keep SSH + WireGuard ports open
3E. Security Benefits
With Cloudflare in front:
- No open ports on prod-01 (80, 443, etc. closed)
- DDoS protection (automatic with Cloudflare)
- WAF rules (OWASP Top 10, custom rules)
- Rate limiting on public endpoints
- Bot management
Phase 4: Cleanup
- Remove nginx-ingress plugin from MicroK8s (
microk8s.disable ingress) - Remove unused IngressClass resources
- Remove existing MetalLB (delete metallb-system namespace, IPAddressPool, L2Advertisement)
- Delete orphaned
tasks/loadbalancer.ymlandtasks/loadbalancer/metallb.yaml - Remove MetalLB vars from
defaults/main.yml - Update documentation
File Change Summary
New Files
| File | Purpose |
|---|---|
ansible/roles/microk8s/tasks/gateway-api.yml |
NGINX Gateway Fabric install |
terraform/modules/utils/gateway/main.tf |
Gateway + ReferenceGrant resources |
terraform/modules/utils/gateway/variables.tf |
Gateway configuration |
terraform/modules/utils/gateway/outputs.tf |
Gateway outputs |
terraform/modules/utils/cloudflare-tunnel/main.tf |
cloudflared DaemonSet |
terraform/modules/utils/cloudflare-tunnel/variables.tf |
Tunnel config |
Deleted Files
| File | Reason |
|---|---|
ansible/roles/microk8s/tasks/loadbalancer.yml |
Not wired into main.yml, was for nginx-ingress LB |
ansible/roles/microk8s/tasks/loadbalancer/metallb.yaml |
MetalLB no longer needed (single-node, NodePort) |
Modified Files
| File | Change |
|---|---|
ansible/roles/microk8s/tasks/main.yml |
Include gateway-api.yml |
ansible/roles/microk8s/defaults/main.yml |
Add Gateway Fabric defaults, remove MetalLB vars |
terraform/modules/utils/proxy/main.tf |
Ingress → HTTPRoute + TCPRoute + UDPRoute |
terraform/modules/utils/proxy/variables.tf |
Update for Gateway API |
terraform/modules/utils/proxy/resources/wireguard/wireguard.conf.tpl |
Add PersistentKeepalive |
terraform/modules/apps/nginx/main.tf |
Ingress → HTTPRoute |
terraform/modules/apps/gitea/ingress.tf |
Ingress → HTTPRoute |
terraform/modules/apps/m3uproxy/main.tf |
Ingress → HTTPRoute |
terraform/modules/apps/open-webui/ingress.tf |
Ingress → HTTPRoute |
terraform/modules/apps/grafana/grafana.tf |
Ingress → HTTPRoute |
terraform/modules/apps/mstream/ingress.tf |
Ingress → HTTPRoute |
terraform/modules/apps/searxng/ingress.tf |
Ingress → HTTPRoute |
terraform/modules/apps/vaultwarden/ingress.tf |
Ingress → HTTPRoute |
terraform/modules/apps/terraform-mcp/ingress.tf |
Ingress → HTTPRoute |
terraform/modules/apps/nextcloud/ingress.tf |
Ingress → HTTPRoute |
terraform/modules/apps/ssh_locker_web/ |
Ingress → HTTPRoute |
terraform/apps/prod-01/apps.tf |
Add gateway module |
terraform/apps/prod-01/config.tf |
WireGuard (remove gpu-01), remove ollama proxy |
terraform/apps/dev-01/config.tf |
Remove ollama DNS CNAME |
terraform/apps/dev-01/apps.tf |
Add gateway module |
Risk Assessment
| Risk | Mitigation |
|---|---|
| TCPRoute/UDPRoute experimental in NGF | Start with HTTPRoute only, keep proxy streams temporarily |
| Cloudflare Tunnel adds latency | Monitor, acceptable for reverse proxy traffic |
| cert-manager Gateway integration alpha | Use approach B (separate Certificate resources) |
| Downtime during migration | Run both controllers in parallel, switch via DNS TTL |
| Removing gpu-01 from WireGuard allowed_ips | Verify no active proxy depends on gpu-01 reachability |
| Removing existing MetalLB on prod-01 | Do after Cloudflare Tunnel is fully operational |
Timeline Estimate
| Phase | Duration | Dependencies |
|---|---|---|
| Phase 1 (Gateway API) | 2-3 weeks | None |
| Phase 2 (WireGuard) | 1-2 weeks | Can run parallel with Phase 1 |
| Phase 3 (Cloudflare) | 1-2 weeks | After Phase 1 |
| Phase 4 (Cleanup) | 1 week | After Phase 3 |