Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions pkg/controller/installation/core_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -1381,7 +1381,15 @@ func (r *ReconcileInstallation) Reconcile(ctx context.Context, request reconcile
// spec.extensions.waf.state != Enabled, the WAF surface is not rendered.
// See design tigera/designs#25 (PMREQ-384) §Gating.
wafGatewayExtensionEnabled := false
// gatewayAPIPresent means the GatewayAPI CR exists (regardless of waf.state),
// so the operator manages the Gateway API + Envoy Gateway CRDs the WAF
// reconcilers watch. It keeps the applicationlayer controller wired (with
// EnvoyExtensionPolicy delete RBAC) even while WAF is disabled, so the
// controller can tear down the EEPs it generated instead of being removed in
// the same reconcile that disables WAF (EV-6751).
gatewayAPIPresent := false
if gatewayAPI, msg, err := gatewayapi.GetGatewayAPI(ctx, r.client); err == nil {
gatewayAPIPresent = true
wafGatewayExtensionEnabled = gatewayAPI.Spec.IsWAFGatewayExtensionEnabled()
} else if !apierrors.IsNotFound(err) {
// Mirrors the GatewayAPI controller's handling: a read error or a
Expand Down Expand Up @@ -1698,6 +1706,7 @@ func (r *ReconcileInstallation) Reconcile(ctx context.Context, request reconcile
Namespace: common.CalicoNamespace,
BindingNamespaces: []string{common.CalicoNamespace},
WAFGatewayExtensionEnabled: wafGatewayExtensionEnabled,
GatewayAPIPresent: gatewayAPIPresent,
WAFWebhookServerTLS: wafWebhookTLS,
WASMPullSecret: wasmPullSecret,
WASMCACert: wasmCACert,
Expand Down
51 changes: 39 additions & 12 deletions pkg/render/kubecontrollers/kube-controllers.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,15 +128,26 @@ type KubeControllersConfiguration struct {
// If this is nil, then we should run in zero-tenant mode.
Tenant *operatorv1.Tenant

// WAFGatewayExtensionEnabled gates the WAF v3 (Gateway API add-on) surface
// on calico-kube-controllers: the applicationlayer controller enablement,
// the WAF / Gateway-API / EnvoyExtensionPolicy / event / secret-replication
// RBAC, the WASM_IMAGE / WASM_PULL_SECRET / WASM_CA_CERT env vars, and the
// gateway envoy-proxy wasm image resolution. Sourced from
// WAFGatewayExtensionEnabled gates the ACTIVE WAF v3 (Gateway API add-on)
// surface on calico-kube-controllers: the WASM_IMAGE / WASM_PULL_SECRET /
// WASM_CA_CERT env vars, the in-process admission webhook, and the gateway
// envoy-proxy wasm image resolution. Sourced from
// `GatewayAPI.spec.extensions.waf.state == Enabled` (default off).
// See design `tigera/designs#25` (PMREQ-384).
WAFGatewayExtensionEnabled bool

// GatewayAPIPresent is true when the GatewayAPI CR exists (regardless of
// waf.state), so the operator manages the Gateway API + Envoy Gateway CRDs the
// WAF reconcilers watch. It gates the applicationlayer controller enablement,
// its WAF / Gateway-API / EnvoyExtensionPolicy / event / secret RBAC, and the
// WAF_GATEWAY_EXTENSION_ENABLED signal env — a superset of
// WAFGatewayExtensionEnabled. The WAF controller stays wired (and keeps its
// envoyextensionpolicies delete RBAC) while WAF is disabled precisely so it can
// tear down the EnvoyExtensionPolicies it generated, instead of being removed
// in the same reconcile that disables WAF and never getting the chance
// (EV-6751). It de-programs on WAF_GATEWAY_EXTENSION_ENABLED=false.
GatewayAPIPresent bool

// WAFWebhookServerTLS is the serving certificate for the in-process WAF
// SecLang validating admission webhook hosted by calico-kube-controllers.
// When set (WAF enabled), it is mounted into the Pod and the webhook server
Expand Down Expand Up @@ -197,7 +208,11 @@ func NewCalicoKubeControllers(cfg *KubeControllersConfiguration) *kubeController
},
)
enabledControllers = append(enabledControllers, "service", "federatedservices", "usage")
if cfg.WAFGatewayExtensionEnabled {
// Wire the applicationlayer WAF controller whenever Gateway API is present,
// not only when WAF is enabled, so it stays running (and can tear down its
// generated EnvoyExtensionPolicies) when WAF is disabled. It de-programs vs
// programs based on WAF_GATEWAY_EXTENSION_ENABLED (EV-6751).
if cfg.GatewayAPIPresent {
enabledControllers = append(enabledControllers, "applicationlayer")
}
}
Expand Down Expand Up @@ -560,9 +575,12 @@ func kubeControllersRoleEnterpriseCommonRules(cfg *KubeControllersConfiguration)
},
}

if cfg.WAFGatewayExtensionEnabled {
// WAF v3 (Gateway API add-on) RBAC. Gated by
// GatewayAPI.spec.extensions.waf.state == Enabled.
if cfg.GatewayAPIPresent {
// WAF v3 (Gateway API add-on) RBAC. Gated by GatewayAPIPresent, not
// waf.state==Enabled, so the applicationlayer controller keeps the RBAC it
// needs to watch targets and DELETE the EnvoyExtensionPolicies it generated
// while WAF is disabled (EV-6751). The rule set is identical enabled vs
// disabled, so toggling waf.state causes no ClusterRole churn.
rules = append(rules,
// Application-layer (gateway-addons) reconcilers reconcile WAF resources
// against Gateway API targetRefs and emit events on the policy objects.
Expand Down Expand Up @@ -742,11 +760,20 @@ func (c *kubeControllersComponent) controllersDeployment() *appsv1.Deployment {
env = append(env, corev1.EnvVar{Name: "MULTI_INTERFACE_MODE", Value: c.cfg.Installation.CalicoNetwork.MultiInterfaceMode.Value()})
}

// Application-layer (gateway-addons / WAF v3) env vars, gated by
// The WAF reconcilers are wired whenever Gateway API is present (see the
// applicationlayer entry in enabledControllers), so they can tear down the
// EnvoyExtensionPolicies they generated when WAF is disabled.
// WAF_GATEWAY_EXTENSION_ENABLED tells them whether to program (enabled) or
// de-program (disabled) — EV-6751. Absent ⇒ the reconciler defaults to
// enabled, so an older operator that predates this var is unaffected.
if c.cfg.GatewayAPIPresent {
env = append(env, corev1.EnvVar{Name: "WAF_GATEWAY_EXTENSION_ENABLED", Value: strconv.FormatBool(c.cfg.WAFGatewayExtensionEnabled)})
}

// Application-layer (gateway-addons / WAF v3) WASM env vars, gated by
// GatewayAPI.spec.extensions.waf.state == Enabled. When the gate is
// off (default), none of the WASM_* env vars are rendered and the
// kube-controllers binary skips the WAF reconcilers entirely (see the
// applicationlayer entry in enabledControllers).
// WAF reconcilers de-program rather than attach a filter.
if c.cfg.WAFGatewayExtensionEnabled {
// Application-layer (gateway-addons) reconcilers consume the Coraza WAF
// wasm OCI reference from this env var to program WAF policy attachments.
Expand Down
40 changes: 40 additions & 0 deletions pkg/render/kubecontrollers/kube-controllers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ var _ = Describe("kube-controllers rendering tests", func() {
cfg.MetricsPort = 9094
// Opt in to the WAF Gateway API add-on so the WAF env vars + RBAC are rendered.
cfg.WAFGatewayExtensionEnabled = true
cfg.GatewayAPIPresent = true
cfg.WAFWebhookCABundle = []byte("fake-ca-bundle")
// core_controller provisions a dedicated WAF wasm pull secret (a renamed
// copy of the install pull secret) so the reconciler can replicate it into
Expand Down Expand Up @@ -460,6 +461,7 @@ var _ = Describe("kube-controllers rendering tests", func() {
cfg.MetricsPort = 9094
// Opt in to the WAF Gateway API add-on so the WAF env vars + RBAC are rendered.
cfg.WAFGatewayExtensionEnabled = true
cfg.GatewayAPIPresent = true

component := kubecontrollers.NewElasticsearchKubeControllers(&cfg)
Expect(component.ResolveImages(nil)).To(BeNil())
Expand Down Expand Up @@ -531,6 +533,7 @@ var _ = Describe("kube-controllers rendering tests", func() {
cfg.MetricsPort = 9094
// Opt in to the WAF Gateway API add-on so the WAF env vars + RBAC are rendered.
cfg.WAFGatewayExtensionEnabled = true
cfg.GatewayAPIPresent = true

component := kubecontrollers.NewCalicoKubeControllers(&cfg)
Expect(component.ResolveImages(nil)).To(BeNil())
Expand Down Expand Up @@ -661,6 +664,7 @@ var _ = Describe("kube-controllers rendering tests", func() {

instance.Variant = operatorv1.CalicoEnterprise
cfg.WAFGatewayExtensionEnabled = true
cfg.GatewayAPIPresent = true
cfg.WAFWebhookServerTLS = wafTLS

component := kubecontrollers.NewCalicoKubeControllers(&cfg)
Expand Down Expand Up @@ -694,6 +698,41 @@ var _ = Describe("kube-controllers rendering tests", func() {
}))
})

It("should keep the WAF controller wired for teardown but render no active WAF surface when WAF is disabled (EV-6751)", func() {
instance.Variant = operatorv1.CalicoEnterprise
// GatewayAPI present but WAF turned off: the applicationlayer controller
// must stay wired (with its EnvoyExtensionPolicy delete RBAC) so it can
// tear down the EEPs it generated, and be told it is disabled via the env.
cfg.GatewayAPIPresent = true
cfg.WAFGatewayExtensionEnabled = false

component := kubecontrollers.NewCalicoKubeControllers(&cfg)
Expect(component.ResolveImages(nil)).To(BeNil())
resources, _ := component.Objects()

dp := rtest.GetResource(resources, kubecontrollers.KubeController, common.CalicoNamespace, "apps", "v1", "Deployment").(*appsv1.Deployment)
c := dp.Spec.Template.Spec.Containers[0]

// Controller stays wired.
Expect(c.Env).To(ContainElement(corev1.EnvVar{
Name: "ENABLED_CONTROLLERS", Value: "node,loadbalancer,service,federatedservices,usage,applicationlayer",
}))
// Told it is disabled → the reconciler de-programs rather than attaches.
Expect(c.Env).To(ContainElement(corev1.EnvVar{Name: "WAF_GATEWAY_EXTENSION_ENABLED", Value: "false"}))
// No active WAF surface: no WASM image env, no webhook cert dir.
for _, e := range c.Env {
Expect(e.Name).NotTo(Equal("WASM_IMAGE"))
Expect(e.Name).NotTo(Equal("WAF_WEBHOOK_CERT_DIR"))
}
// But it keeps the EnvoyExtensionPolicy delete RBAC to run the teardown.
clusterRole := rtest.GetResource(resources, "calico-kube-controllers", "", "rbac.authorization.k8s.io", "v1", "ClusterRole").(*rbacv1.ClusterRole)
Expect(clusterRole.Rules).To(ContainElement(rbacv1.PolicyRule{
APIGroups: []string{"gateway.envoyproxy.io"},
Resources: []string{"envoyextensionpolicies"},
Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"},
}))
})

It("should render all es-calico-kube-controllers resources for a default configuration using CalicoEnterprise and ClusterType is Management", func() {
expectedResources := []struct {
name string
Expand All @@ -720,6 +759,7 @@ var _ = Describe("kube-controllers rendering tests", func() {
cfg.MetricsPort = 9094
// Opt in to the WAF Gateway API add-on so the WAF env vars + RBAC are rendered.
cfg.WAFGatewayExtensionEnabled = true
cfg.GatewayAPIPresent = true

component := kubecontrollers.NewElasticsearchKubeControllers(&cfg)
Expect(component.ResolveImages(nil)).To(BeNil())
Expand Down
Loading