# 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 display # lane and remotedesktop-web's pre-handoff probing. Egress is deliberately # narrow: named CoreDNS, direct Intranet web, and noc1 step-ca only. There is # no broad Traefik/VIP or internet egress from desktop sessions. If a future # Browser Lab path needs a public-style host, prefer an explicit Service rule # or include the post-DNAT backend port per the Traefik VIP lint. 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: # CoreDNS only. The old to: [] DNS rule accidentally allowed any DNS # listener in any namespace or routed network. - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: kube-system podSelector: matchLabels: k8s-app: kube-dns ports: - port: 53 protocol: UDP - port: 53 protocol: TCP # Browser Lab home / internal docs target. Use the real service port # directly rather than public Traefik host aliases. - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: intranet podSelector: matchLabels: app: intranet-web ports: - port: 5300 protocol: TCP # noc1 step-ca ACME endpoint. The lane brief called out 9000/TCP; the live # ACME directory currently answers on 9443/TCP, so both stay pinned to the # same host rather than reopening Traefik or internet egress. - to: - ipBlock: cidr: 10.0.56.10/32 ports: - port: 9000 protocol: TCP - port: 9443 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 - to: - ipBlock: cidr: 10.0.56.10/32 ports: - port: 9000 protocol: TCP - port: 9443 protocol: TCP