feat(gatewayapi): calico-system policy for namespaced data-plane proxies - #4970
feat(gatewayapi): calico-system policy for namespaced data-plane proxies#4970electricjesus wants to merge 7 commits into
Conversation
d3d69a5 to
311699b
Compare
311699b to
fe034ab
Compare
ed7bc96 to
c549093
Compare
Since deploy.type=GatewayNamespace (tigera#4690) the data-plane envoy proxies run in each Gateway's own namespace, not calico-system. The only calico-system gateway policy selects the controller/certgen pods in calico-system, so the proxies match nothing and have no policy punching through a default-deny tier in the namespaces they now run in. Add a GlobalNetworkPolicy selecting the EG proxy pods (label gateway.envoyproxy.io/owning-gateway-name) so it covers every Gateway namespace with no re-render: DNS + xDS(18000)/Wasm(18002) egress to the controller in calico-system, and all inbound TCP so a managed Gateway serves traffic out of the box under a default-deny tier. Backend egress is left to the user, matching the controller policy.
Per review: instead of selecting the data-plane proxy pods on Envoy Gateway's gateway.envoyproxy.io/owning-gateway-name label, which we do not own and which could change upstream without notice, stamp our own k8s-app=calico-gateway-api-proxy label on the proxy pods through the EnvoyProxy pod spec and point the calico-system-tier proxy policy at that. Any user-supplied pod labels from a custom EnvoyProxy are kept.
Put the Calico-owned k8s-app labeling for all three envoy-gateway components in one documented place. The operator-rendered controller and certgen pods go through setGatewayComponentLabel; the runtime-created proxy pods, which the operator never renders, go through the EnvoyProxy pod spec (ensureGatewayProxyLabel). Documents why this lives in the gateway render and not the standard labeler in component.go, which keys off the object name and cannot reach a runtime-created pod.
f50b72f to
1d372cd
Compare
… cover tigera#2873 replaced the obsolete tigera-gateway namespace exclusion with a reference NetworkPolicy that users apply in each Gateway namespace. That policy allows DNS and the gateway controller hop itself. With tigera/operator#4970 the operator renders calico-system.envoy-gateway-proxy, which already allows inbound traffic to the proxy plus proxy egress to DNS and to the controller on 18000 and 18002. The reference policy would duplicate rules the operator now owns. What the operator's policy deliberately does not cover is the backend hop. It ends its egress rules with a Pass so the user keeps control of which backends the proxy may reach. Under default deny that hop stays denied, and the gateway returns 503 after the upstream connect timeout. So the step now covers only the user's part: - proxy egress to the backend, and backend ingress from the proxy. Both are needed, because default deny applies to the backend pod too. - on Enterprise, egress to the Kubernetes API server, for the log collector that runs in the proxy pod. The example selects proxies by k8s-app == "calico-gateway-api-proxy", the Calico-owned label that #4970 stamps on the proxy pod template.
There was a problem hiding this comment.
Pull request overview
This PR updates the Gateway API rendering to ensure Envoy Gateway data-plane proxy pods (which run in per-Gateway namespaces in namespaced mode) are covered by a calico-system-tier policy, so they can function under namespace default-deny.
Changes:
- Add a
GlobalNetworkPolicy(calico-system.envoy-gateway-proxy) that selects proxy pods via a Calico-stampedk8s-applabel and allows required proxy ingress/egress under default-deny. - Stamp a Calico-owned
k8s-applabel onto proxy pods via theEnvoyProxypod spec, and refactor controller/certgen label stamping into a helper. - Extend rendering tests to expect the new
GlobalNetworkPolicyand verify the proxy label is stamped.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| pkg/render/gatewayapi/gateway_api.go | Adds proxy GlobalNetworkPolicy and stamps/standardizes k8s-app labeling for controller/certgen/proxy selection. |
| pkg/render/gatewayapi/gateway_api_test.go | Updates expectations for the new proxy policy and verifies proxy pod labeling behavior. |
Suppressed comments (1)
pkg/render/gatewayapi/gateway_api.go:892
- This comment says any user-supplied pod labels are preserved, but the function unconditionally overwrites "k8s-app" (which could also be user-supplied on a custom EnvoyProxy). Clarifying that only non-"k8s-app" labels are preserved would avoid confusion.
// ensureGatewayProxyLabel stamps the same Calico-owned k8s-app label on the data-plane
// proxy pods, via the EnvoyProxy pod spec, so gatewayAPIProxyPolicy can select them.
// Any user-supplied pod labels carried over from a custom EnvoyProxy are preserved.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| // Allow all inbound TCP from any source (this also covers the 19001 metrics | ||
| // scrape). Gateway listener ports are user-defined and dynamic, so a managed | ||
| // Gateway has to accept arbitrary ports to serve traffic out of the box, | ||
| // including under a default-deny tier. Verified on a cluster: the narrower | ||
| // alternative (allow only 19001, then Pass) lets listener ingress fall | ||
| // through to the user's default-deny and silently breaks every Gateway in a | ||
| // default-deny namespace. The cost of allowing all TCP is that an Allow is | ||
| // terminal in this tier, so a user cannot narrow ingress to the proxy with | ||
| // their own policy. |
There was a problem hiding this comment.
This comment is massive AI overcommenting - we especially don't need any "Verified on a cluster" type comments.
This should just explain why we want such an open ingress rule.
Do we actually want this? Or do we want to allow users to define where ingress can be allowed from?
There was a problem hiding this comment.
@electricjesus I think my last question still stands here - it's a bit sketchy to blanket allow all traffic in a way that can't be overridden by users. Are we sure that's the right model here?
There was a problem hiding this comment.
yeah, this doesn't hold up. i'd assumed we couldn't know the listener ports. we can.
the controller already lists every Gateway to build GatewayNamespaces (gatewayapi_controller.go:490), and each item carries Spec.Listeners[].Port. we just never read it. watchGateways is already registered, so a listener edit re-runs reconcile.
so it's namespaced now, one NetworkPolicy per Gateway namespace, ingress allowing that namespace's own listener ports plus 19001 for metrics, then Pass. egress is unchanged. the controller policy right above it in the same file is already a namespaced NetworkPolicy, so the proxy one being global was the odd one out anyway.
two catches. 19001 isn't a listener, so it goes in by hand. and a new listener port won't serve until the next reconcile. small window, but real.
one thing you'll probably ask: it's owned by the GatewayAPI CR, not by the namespace's Gateways like the other per-namespace stuff. mergeState returns nil for a *v3.NetworkPolicy whose Spec hasn't changed (component.go:851), so the second Gateway's owner ref never merges in. i had a test showing owners ["gw1"] instead of ["gw1","gw2"]. rather than touch shared code i left it CR-owned and added a sweep for namespaces that drop out..
this also kills what you poked at on the other thread. the old GNP had no namespaceSelector, so any pod anywhere that set k8s-app: calico-gateway-api-proxy inherited a terminal all-TCP allow at order 100. per-namespace, that's gone.
pushed at a4e01ceb5, body updated.
Drop the narration around the proxy label helpers and the proxy policy, keeping only the parts a reader can't get from the code: why we select on our own label, why the proxy pod spec is the only labelling hook, and why the egress Pass is deliberate.
…xy-allow-policy # Conflicts: # pkg/render/gatewayapi/gateway_api.go
There was a problem hiding this comment.
🟡 Changes recommended
The new proxy GlobalNetworkPolicy only allows inbound TCP, which would still block UDPRoute/Gateway UDP listeners under default-deny.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 2
- Review effort level: Lite
| Destination: v3.EntityRule{ | ||
| NamespaceSelector: "kubernetes.io/metadata.name == '" + common.CalicoNamespace + "'", | ||
| Selector: EnvoyGatewayPolicySelector, | ||
| Ports: networkpolicy.Ports(18000, 18002), | ||
| }, |
| Ingress: []v3.Rule{ | ||
| {Action: v3.Allow, Protocol: &networkpolicy.TCPProtocol, Source: v3.EntityRule{Nets: []string{"0.0.0.0/0"}}}, | ||
| {Action: v3.Allow, Protocol: &networkpolicy.TCPProtocol, Source: v3.EntityRule{Nets: []string{"::/0"}}}, | ||
| {Action: v3.Pass}, | ||
| }, |
…stener port Replace the cluster-wide GlobalNetworkPolicy with a NetworkPolicy in each Gateway namespace, and allow ingress only on the listener ports the Gateways there declare plus the fixed metrics port. The controller already lists every Gateway to build GatewayNamespaces, so the ports come off the same list.
…heir Gateways mergeState skips a v3.NetworkPolicy whose Spec has not changed, so a second owner reference never merges in and the policy cannot be Gateway-owned. Keep it owned by the GatewayAPI CR and delete the ones left in namespaces that no longer host a Gateway. The sweep reads through an uncached reader, so it does not start an informer over every Calico policy on the cluster.
There was a problem hiding this comment.
🟡 Changes recommended
There’s a significant mismatch between the PR description and the implemented policy kind/ingress behavior, and the orphan-sweep currently lists all v3 NetworkPolicies cluster-wide each reconcile which is a potentially costly operational behavior.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 4/4 changed files
- Comments generated: 3
- Review effort level: Lite
| func (r *ReconcileGatewayAPI) sweepOrphanedProxyPolicies(ctx context.Context, gatewayNamespaces []string) error { | ||
| policies := &v3.NetworkPolicyList{} | ||
| if err := r.apiReader.List(ctx, policies); err != nil { | ||
| return err | ||
| } |
| })) | ||
| }) | ||
|
|
||
| It("reports no namespaces and no ports when a Gateway has no listeners", func() { |
| // ProxyPolicy lets the data-plane envoy proxies in ns punch through any default-deny | ||
| // there (deploy.type=GatewayNamespace). It is namespaced, not global, so a pod outside a | ||
| // Gateway namespace cannot pick up these rules by wearing the proxy label. | ||
| func ProxyPolicy(ns string, listenerPorts []uint16, openShift bool) *v3.NetworkPolicy { |
Description
Bug fix. Since #4690 the data-plane envoy proxies run in each Gateway's own namespace, not
calico-system. The onecalico-system-tier gateway policy selects the controller and certgen pods, so the proxies match nothing there and a Gateway under a default-deny tier gets no traffic.This renders
calico-system.envoy-gateway-proxy, oneNetworkPolicyper Gateway namespace, selecting proxy pods onk8s-app == 'calico-gateway-api-proxy'. Ingress allows that namespace's own listener ports plus 19001 for metrics, thenPass. Egress allows DNS and 18000/18002 to the controller, thenPass, so the user still owns the backend hop and the proxy answers 503 until they allow it.Two things the diff does not show. The listener ports come off the
GatewayListthe controller already reads to buildGatewayNamespaces, andwatchGatewaysis registered, so editing a listener re-renders. And the policy is owned by the GatewayAPI CR rather than the namespace's Gateways, becausemergeStateskips a*v3.NetworkPolicywhose Spec has not changed, so a second owner reference never merges in;sweepOrphanedProxyPoliciesdeletes leftovers instead, through an uncached reader so it starts no informer over every Calico policy on the cluster.Reading guide
c0b1d14e2the policya7b817e71select on a Calico-owned label1d372cdeeall three components labelled in one place3b16b346eone policy per namespace, scoped to its listener portsa4e01ceb5sweep a namespace that loses its GatewaysTest plan
go build ./...,gofmt -lclean,GOOS=linux go vet ./pkg/...clean,make static-checks0 issues,go testgreen onpkg/render/gatewayapi,pkg/controller/gatewayapi,pkg/enterprise/gatewayapiandpkg/controller/utils.Unit tests cover the port scoping including a Gateway with no listeners, the
Passafter the allow, that noGlobalNetworkPolicyis rendered, and the sweep deleting an orphan while leaving a live policy and a user's own policy alone.Cluster run on OSS master (eBPF, namespaced): baseline 200; under a default-deny a stock operator times out; with this operator 503, so the proxy is reachable and routing and only the backend hop is denied; 200 once the user allows the backend. That run predates the per-namespace port-scoped ingress rule, so the current rule is not cluster-tested yet. A newly added listener port only passes traffic after the next reconcile, and the 503 lands after Envoy's ~10s upstream timeout, so a curl timeout under 10s reads as a connect failure.
Related
Docs: tigera/docs#2802. The Job pod-template-labels change that used to ride here now sits on
seth/job-pod-template-labels, because a non-nil Job pod template also getsoperator.tigera.io/host-networked, whichpodiprecoveryacts on by deleting pods.Release Note
For PR author
make gen-files(n/a, no API change).make gen-versions(n/a).