Skip to content

feat(gatewayapi): calico-system policy for namespaced data-plane proxies - #4970

Open
electricjesus wants to merge 7 commits into
tigera:masterfrom
electricjesus:seth/gatewayapi-proxy-allow-policy
Open

feat(gatewayapi): calico-system policy for namespaced data-plane proxies#4970
electricjesus wants to merge 7 commits into
tigera:masterfrom
electricjesus:seth/gatewayapi-proxy-allow-policy

Conversation

@electricjesus

@electricjesus electricjesus commented Jun 26, 2026

Copy link
Copy Markdown
Member

Description

Bug fix. Since #4690 the data-plane envoy proxies run in each Gateway's own namespace, not calico-system. The one calico-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, one NetworkPolicy per Gateway namespace, selecting proxy pods on k8s-app == 'calico-gateway-api-proxy'. Ingress allows that namespace's own listener ports plus 19001 for metrics, then Pass. Egress allows DNS and 18000/18002 to the controller, then Pass, 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 GatewayList the controller already reads to build GatewayNamespaces, and watchGateways is registered, so editing a listener re-renders. And the policy is owned by the GatewayAPI CR rather than the namespace's Gateways, because mergeState skips a *v3.NetworkPolicy whose Spec has not changed, so a second owner reference never merges in; sweepOrphanedProxyPolicies deletes leftovers instead, through an uncached reader so it starts no informer over every Calico policy on the cluster.

Reading guide

  • c0b1d14e2 the policy
  • a7b817e71 select on a Calico-owned label
  • 1d372cdee all three components labelled in one place
  • 3b16b346e one policy per namespace, scoped to its listener ports
  • a4e01ceb5 sweep a namespace that loses its Gateways

Test plan

go build ./..., gofmt -l clean, GOOS=linux go vet ./pkg/... clean, make static-checks 0 issues, go test green on pkg/render/gatewayapi, pkg/controller/gatewayapi, pkg/enterprise/gatewayapi and pkg/controller/utils.

Unit tests cover the port scoping including a Gateway with no listeners, the Pass after the allow, that no GlobalNetworkPolicy is 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 gets operator.tigera.io/host-networked, which podiprecovery acts on by deleting pods.

Release Note

Add a `calico-system`-tier NetworkPolicy in each Gateway namespace so Envoy Gateway data-plane proxies work under a default-deny tier, allowing the ports the namespace's Gateways listen on (deploy.type=GatewayNamespace).

For PR author

  • Tests for change.
  • If changing pkg/apis/, run make gen-files (n/a, no API change).
  • If changing versions, run make gen-versions (n/a).

@marvin-tigera marvin-tigera added this to the v1.44.0 milestone Jun 26, 2026
@electricjesus
electricjesus force-pushed the seth/gatewayapi-proxy-allow-policy branch from d3d69a5 to 311699b Compare June 26, 2026 08:09
@electricjesus
electricjesus marked this pull request as ready for review June 26, 2026 08:16
@electricjesus
electricjesus requested a review from a team as a code owner June 26, 2026 08:16
@electricjesus
electricjesus force-pushed the seth/gatewayapi-proxy-allow-policy branch from 311699b to fe034ab Compare June 26, 2026 10:13
@electricjesus electricjesus changed the title feat(gatewayapi): allow-tigera policy for namespaced data-plane proxies feat(gatewayapi): calico-system policy for namespaced data-plane proxies Jun 26, 2026
Comment thread pkg/render/gatewayapi/gateway_api.go Outdated
@electricjesus
electricjesus force-pushed the seth/gatewayapi-proxy-allow-policy branch from ed7bc96 to c549093 Compare July 29, 2026 17:28
@danudey danudey modified the milestones: v1.44.0, v1.45.0 Aug 17, 2026
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.
Copilot AI lite review requested due to automatic review settings August 19, 2026 10:47
@electricjesus
electricjesus force-pushed the seth/gatewayapi-proxy-allow-policy branch from f50b72f to 1d372cd Compare August 19, 2026 10:47
electricjesus added a commit to electricjesus/docs that referenced this pull request Aug 19, 2026
… 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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-stamped k8s-app label and allows required proxy ingress/egress under default-deny.
  • Stamp a Calico-owned k8s-app label onto proxy pods via the EnvoyProxy pod spec, and refactor controller/certgen label stamping into a helper.
  • Extend rendering tests to expect the new GlobalNetworkPolicy and 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.

Comment thread pkg/render/gatewayapi/gateway_api_test.go Outdated
Comment thread pkg/render/gatewayapi/gateway_api.go Outdated
Comment thread pkg/render/gatewayapi/gateway_api.go Outdated
Comment thread pkg/render/gatewayapi/gateway_api.go
Comment thread pkg/render/gatewayapi/gateway_api.go Outdated
Comment thread pkg/render/gatewayapi/gateway_api.go Outdated
Comment on lines +1481 to +1489
// 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread pkg/render/gatewayapi/gateway_api_test.go Outdated
@caseydavenport caseydavenport self-assigned this Aug 20, 2026
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.
Copilot AI review requested due to automatic review settings August 20, 2026 19:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

…xy-allow-policy

# Conflicts:
#	pkg/render/gatewayapi/gateway_api.go
Copilot AI review requested due to automatic review settings September 3, 2026 14:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Comment on lines +1098 to +1102
Destination: v3.EntityRule{
NamespaceSelector: "kubernetes.io/metadata.name == '" + common.CalicoNamespace + "'",
Selector: EnvoyGatewayPolicySelector,
Ports: networkpolicy.Ports(18000, 18002),
},
Comment on lines +1121 to +1125
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.
Copilot AI review requested due to automatic review settings September 3, 2026 15:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Comment on lines +698 to +702
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() {
Comment on lines +1104 to +1107
// 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 {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants