# FlowerCore Remote Desktop — NetworkPolicies (GitOps-managed) # # Moved into bluejay-infra 2026-05-07 as part of the regroup audit. These # four policies were previously applied via FlowerCore.RemoteDesktop's # scripts/deploy-web.sh `kubectl apply` calls, which meant a fresh cluster # rebuild from bluejay-infra alone would miss them — Browser Lab session # isolation, control-plane allow-list, and HTTP-01 cert renewal would all # silently fail to come up. # # Source-of-truth contract: # - bluejay-infra OWNS all NetworkPolicy + Certificate + IngressRoute # resources for fc-desktop. # - FlowerCore.RemoteDesktop's scripts/deploy-web.sh continues to own # the Deployment + Service apply (because the image ref # `localhost/fc-desktop:linux-xfce` only exists on each node's # containerd after a manual import — it can't be pulled from a # registry, so a Deployment manifest in bluejay-infra would race the # image-import step and crash-loop). --- # 1) desktop-isolation — Browser Lab session pods. # # Locks down pods labeled `app.kubernetes.io/name=remote-desktop` (every # session pod regardless of template). Allows guacd ingress for the VNC/RDP # display lane and remotedesktop-web's pre-handoff probing. Egress: NFS to # Synology, DNS, Traefik (cluster + LB VIP), Intranet (Browser Lab home). apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: desktop-isolation namespace: fc-desktop labels: app.kubernetes.io/part-of: remotedesktop app.kubernetes.io/component: isolation spec: podSelector: matchLabels: app.kubernetes.io/name: remote-desktop policyTypes: - Ingress - Egress ingress: - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: guacamole ports: - port: 3000 protocol: TCP - port: 3001 protocol: TCP - port: 5901 protocol: TCP - port: 3389 protocol: TCP - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: fc-desktop podSelector: matchLabels: app.kubernetes.io/name: remotedesktop-web ports: - port: 3000 protocol: TCP - port: 5901 protocol: TCP egress: # NFS to Synology - to: - ipBlock: cidr: 10.0.58.3/32 ports: - port: 2049 protocol: TCP - port: 2049 protocol: UDP - port: 111 protocol: TCP - port: 111 protocol: UDP - to: - ipBlock: cidr: 10.0.58.3/32 ports: - port: 445 protocol: TCP - to: [] ports: - port: 53 protocol: UDP - port: 53 protocol: TCP - to: - ipBlock: cidr: 10.0.56.200/32 - ipBlock: cidr: 10.43.33.87/32 - namespaceSelector: matchLabels: kubernetes.io/metadata.name: traefik-system podSelector: matchLabels: app.kubernetes.io/name: traefik ports: - port: 80 protocol: TCP - port: 443 protocol: TCP - port: 8000 protocol: TCP - port: 8443 protocol: TCP - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: intranet podSelector: matchLabels: app: intranet-web ports: - port: 5300 protocol: TCP --- # 2) fc-desktop-default-deny — namespace-wide catch-all. # # Selects every pod EXCEPT remotedesktop-web (the public-surface control # plane) and applies default-deny semantics for both Ingress and Egress. # Closes the gap where session pods land WITHOUT the desktop-isolation # policy's `app.kubernetes.io/name=remote-desktop` label, plus prevents # arbitrary debug sidecars / kubectl debug images from getting cluster # access. # # CRITICAL: also catches transient cm-acme-http-solver pods (that's the # bug this whole regroup chased). The cm-acme-http-solver-allow policy # below is the explicit carve-out. apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: fc-desktop-default-deny namespace: fc-desktop labels: app.kubernetes.io/part-of: remotedesktop app.kubernetes.io/component: isolation spec: podSelector: matchExpressions: - key: app.kubernetes.io/name operator: NotIn values: - remotedesktop-web policyTypes: - Ingress - Egress --- # 3) remotedesktop-web-isolation — control plane explicit allow-list. # # remotedesktop-web is the only pod label the default-deny excludes, so # without this policy the control plane would have wide-open Ingress AND # Egress. This re-introduces a tight allow-list: # - Ingress: Traefik only on TCP/8080 # - Egress: CoreDNS, K8s API, Guacamole admin, NFS, Intranet, # Traefik (cluster + LB), and the fc-desktop namespace itself # (for session pod readiness probing). apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: remotedesktop-web-isolation namespace: fc-desktop labels: app.kubernetes.io/part-of: remotedesktop app.kubernetes.io/component: isolation spec: podSelector: matchLabels: app.kubernetes.io/name: remotedesktop-web policyTypes: - Ingress - Egress ingress: - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: traefik-system podSelector: matchLabels: app.kubernetes.io/name: traefik ports: - port: 8080 protocol: TCP egress: # CoreDNS - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: kube-system podSelector: matchLabels: k8s-app: kube-dns ports: - port: 53 protocol: UDP - port: 53 protocol: TCP # K8s API server - to: [] ports: - port: 443 protocol: TCP - port: 6443 protocol: TCP # Guacamole admin - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: guacamole ports: - port: 8080 protocol: TCP # NFS to Synology - to: - ipBlock: cidr: 10.0.58.3/32 ports: - port: 2049 protocol: TCP - port: 2049 protocol: UDP - port: 111 protocol: TCP - port: 111 protocol: UDP # Intranet web - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: intranet podSelector: matchLabels: app: intranet-web ports: - port: 5300 protocol: TCP # Cluster Traefik pods (in-cluster service resolution + Guacamole # routing handoff where web app builds URLs against the public host # but resolves internally). - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: traefik-system podSelector: matchLabels: app.kubernetes.io/name: traefik ports: - port: 80 protocol: TCP - port: 443 protocol: TCP - port: 8080 protocol: TCP - port: 8443 protocol: TCP # fc-desktop namespace — session pod probing during browser-access # readiness checks. - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: fc-desktop ports: - port: 3000 protocol: TCP - port: 3001 protocol: TCP - port: 5901 protocol: TCP - port: 3389 protocol: TCP --- # 4) cm-acme-http-solver-allow — cert-manager HTTP-01 carve-out. # # Without this, fc-desktop-default-deny catches the transient solver pods # cert-manager creates for each renewal (they don't carry the # remotedesktop-web label). Caused 8-day silent renewal failure on # desktop.iamworkin.lan in 2026-04-28..2026-05-07 (see # feedback_certmanager_renewal_stuck_when_solver_blocked_by_namespace_default_deny.md). # # Authorizes: # - Ingress on TCP/8089 from cluster Traefik (which proxies the external # HTTP-01 GET on port 80 through to the solver). # - Egress for cluster DNS (defensive — newer cert-manager probes from # inside the solver too). # # The `acme.cert-manager.io/http01-solver=true` label is set by # cert-manager itself on every solver pod automatically. apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: cm-acme-http-solver-allow namespace: fc-desktop labels: app.kubernetes.io/part-of: remotedesktop app.kubernetes.io/component: cert-renewal spec: podSelector: matchLabels: acme.cert-manager.io/http01-solver: "true" policyTypes: - Ingress - Egress ingress: - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: traefik-system podSelector: matchLabels: app.kubernetes.io/name: traefik ports: - port: 8089 protocol: TCP egress: - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: kube-system podSelector: matchLabels: k8s-app: kube-dns ports: - port: 53 protocol: UDP - port: 53 protocol: TCP