Ingress, TLS & External Access
Publish the dashboard and API through a trusted HTTPS edge while separating game ports, management traffic, and identity redirects.
Dashboard HTTPS and game TCP/UDP exposure are separate interfaces with different owners and security controls.
Choose the edge topology
Document every HTTP, TCP, and UDP listener from DNS through the owning controller.
Dashboard Ingress: The API Service (gameplane-api in the gameplane-system namespace) is exposed via a Kubernetes Ingress resource. Install an ingress controller (nginx-ingress by default) and cert-manager for automated certificate renewal:
# values.yaml (Helm install)
ingress:
host: gameplane.your-domain.test
tls: true
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/proxy-body-size: "64m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
Game Exposure: Each GameServer’s spec.networking.expose field controls how the game is exposed:
ClusterIP(default): Reachable only from within the cluster.NodePort: Exposed on a high port on every node. Players connect tonode-ip:node-port.LoadBalancer: Request an external load balancer (cloud, MetalLB, Cilium). Requiresoperator.addressManagerconfigured.Hostport: Advertise a specific host port via the pod’s HostPort field. Suitable for single-node clusters (k3s/kind).
# GameServer example
apiVersion: gameplane.local/v1alpha1
kind: GameServer
metadata:
name: minecraft-prod
spec:
networking:
expose: LoadBalancer
addressPool: "public-pool" # MetalLB or Cilium pool
sourceRanges:
- "0.0.0.0/0" # Restrict in production
Address Manager: Configure the load-balancer integration (operator.addressManager in values.yaml):
metallb: Operator requests addresses via Service annotations (metallb.io/address-poolandmetallb.io/loadBalancerIPs). MetalLB IPAddressPool CRs live inmetallb-system(ormetalLBNamespaceoverride).cilium: Operator sets Service labelgameplane.local/lb-pool(mirror it in yourCiliumLoadBalancerIPPool’sspec.serviceSelector).none: No external load balancer; LoadBalancer services stay inPendingstate.
Configure TLS and proxy trust
Make certificate renewal, forwarded headers, WebSockets, and backend trust explicit.
Certificate Management: The Ingress TLS termination point handles HTTPS:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: gameplane-api
annotations:
cert-manager.io/issuer: "letsencrypt-prod"
spec:
ingressClassName: nginx
tls:
- hosts:
- gameplane.your-domain.test
secretName: gameplane-api-tls
rules:
- host: gameplane.your-domain.test
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: gameplane-api
port:
number: 8080
Proxy trust: The dashboard web frontend (gameplane-web) sits behind the Ingress. Nginx annotations configure proxy behavior:
nginx.ingress.kubernetes.io/proxy-body-size: "64m" # File uploads
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" # WebSocket long-lived
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" # WebSocket long-lived
nginx.ingress.kubernetes.io/use-forwarded-headers: "true" # Trust X-Forwarded-* from reverse proxy
nginx.ingress.kubernetes.io/proxy-set-headers: "gameplane-system/custom-headers" # Custom headers
Forwarded Headers: The API inspects X-Forwarded-For, X-Forwarded-Proto, and X-Real-IP headers to determine the actual client IP and protocol. Misconfigured proxies lead to wrong client IPs in logs and OIDC callback mismatches.
Backend mTLS: The API communicates with agents over mTLS:
- API → Agent: Client certificate signed by the operator-managed CA, mounted into the API pod at
/etc/gameplane/agent-ca/. - API → Database: Optional mTLS for PostgreSQL backends (set
db.tls=truein values). - API → Audit Syslog: Plain HTTP-JSON webhook; the syslog bridge (
audit-syslog-bridge) forwards to TLS syslog endpoints.
Harden and validate
Verify certificate renewal, redirects, OIDC, console WebSockets, uploads, API requests, and real client IPs end to end.
Certificate Renewal: Check cert-manager status:
kubectl -n gameplane-system get certificate
kubectl -n gameplane-system describe certificate gameplane-api-tls
kubectl -n cert-manager logs -l app=cert-manager
Look for warnings about renewal windows or failed issuance.
OIDC Callback URLs: Every OIDC provider (Okta, Keycloak, Azure AD) requires an exact callback URL. Mismatches cause invalid_request or redirect loop errors:
Correct: https://gameplane.your-domain.test/auth/oidc/callback
Wrong: http://gameplane.your-domain.test/auth/oidc/callback (no HTTPS)
Wrong: https://gameplane.your-domain.test:8080/auth/oidc/callback (explicit port)
Console WebSocket: Test the in-browser console (uses wss:// over the same TLS edge):
# From inside the cluster (requires prior login session)
curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" \
-b "gameplane_session=$SESSION_TOKEN" \
https://gameplane.your-domain.test/ws/servers/my-server/console
The $SESSION_TOKEN is obtained from a prior dashboard or OIDC login, not from a Kubernetes ServiceAccount token.
File Uploads: The Ingress proxy-body-size limit must exceed your largest world/mod file:
# Check the configured limit
kubectl -n gameplane-system get ingress gameplane-api -o yaml | grep proxy-body-size
Client IP Verification: Check that real client IPs appear in audit logs, not proxy IPs:
# Tail API logs
kubectl -n gameplane-system logs -l app=gameplane-api --tail=50
# Look for "client_ip" field in JSON logs; it should be your actual IP, not the ingress node IP
API Requests: Test a simple API call with proper TLS and credentials:
curl -b "gameplane_session=$SESSION_TOKEN" \
https://gameplane.your-domain.test/api/v1/clusters
The $SESSION_TOKEN comes from a prior dashboard login, not a Kubernetes token.