diff --git a/hack/gen-versions/enterprise.go.tpl b/hack/gen-versions/enterprise.go.tpl index a2fd4d4014..b343d66dad 100644 --- a/hack/gen-versions/enterprise.go.tpl +++ b/hack/gen-versions/enterprise.go.tpl @@ -164,7 +164,7 @@ var ( } {{- end }} {{ with index .Components "waf-http-filter" }} - ComponentWafHTTPFilter = Component{ + ComponentWAFHTTPFilter = Component{ Version: "{{ .Version }}", Image: "{{ .Image }}", Registry: "{{ .Registry }}", @@ -404,6 +404,7 @@ var ( ComponentFluentdWindows, ComponentGuardian, ComponentIntrusionDetectionController, + ComponentWAFHTTPFilter, ComponentSecurityEventWebhooksProcessor, ComponentKibana, ComponentManager, diff --git a/pkg/components/enterprise.go b/pkg/components/enterprise.go index 020d9ce88f..4121705f0f 100644 --- a/pkg/components/enterprise.go +++ b/pkg/components/enterprise.go @@ -143,7 +143,7 @@ var ( Registry: "", } - ComponentWafHTTPFilter = Component{ + ComponentWAFHTTPFilter = Component{ Version: "v3.22.0-1.0", Image: "tigera/waf-http-filter", Registry: "", @@ -351,6 +351,7 @@ var ( ComponentFluentdWindows, ComponentGuardian, ComponentIntrusionDetectionController, + ComponentWAFHTTPFilter, ComponentSecurityEventWebhooksProcessor, ComponentKibana, ComponentManager, diff --git a/pkg/render/gateway_api.go b/pkg/render/gateway_api.go index c653a60c04..bf3ebc7d68 100644 --- a/pkg/render/gateway_api.go +++ b/pkg/render/gateway_api.go @@ -25,9 +25,11 @@ import ( operatorv1 "github.com/tigera/operator/api/v1" "github.com/tigera/operator/pkg/common" "github.com/tigera/operator/pkg/components" + "github.com/tigera/operator/pkg/ptr" rcomp "github.com/tigera/operator/pkg/render/common/components" rmeta "github.com/tigera/operator/pkg/render/common/meta" "github.com/tigera/operator/pkg/render/common/secret" + "github.com/tigera/operator/pkg/render/common/securitycontext" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" @@ -341,6 +343,7 @@ type gatewayAPIImplementationComponent struct { envoyGatewayImage string envoyProxyImage string envoyRatelimitImage string + wafHTTPFilterImage string } func GatewayAPIImplementationComponent(cfg *GatewayAPIImplementationConfig) Component { @@ -368,6 +371,10 @@ func (pr *gatewayAPIImplementationComponent) ResolveImages(is *operatorv1.ImageS if err != nil { return err } + pr.wafHTTPFilterImage, err = components.GetReference(components.ComponentWAFHTTPFilter, reg, path, prefix, is) + if err != nil { + return err + } } else { pr.envoyGatewayImage, err = components.GetReference(components.ComponentCalicoEnvoyGateway, reg, path, prefix, is) if err != nil { @@ -402,7 +409,7 @@ func (pr *gatewayAPIImplementationComponent) Objects() ([]client.Object, []clien CreateNamespace( resources.namespace.Name, pr.cfg.Installation.KubernetesProvider, - PSSBaseline, + PSSPrivileged, // Needed for HostPath volume to write logs to pr.cfg.Installation.Azure, ), } @@ -481,8 +488,9 @@ func (pr *gatewayAPIImplementationComponent) Objects() ([]client.Object, []clien // "ShutdownManager" and "RateLimit" images.) envoyGatewayConfig.Provider.Kubernetes.RateLimitDeployment.Pod.ImagePullSecrets = secret.GetReferenceList(pr.cfg.PullSecrets) - // Enable backend APIs. + // Enable extension APIs. envoyGatewayConfig.ExtensionAPIs.EnableBackend = true + envoyGatewayConfig.ExtensionAPIs.EnableEnvoyPatchPolicy = true // Rebuild the ConfigMap with those changes. envoyGatewayConfigMap := resources.envoyGatewayConfigMap.DeepCopyObject().(*corev1.ConfigMap) @@ -654,6 +662,110 @@ func (pr *gatewayAPIImplementationComponent) envoyProxyConfig(className string, } applyEnvoyProxyServiceOverrides(envoyProxy, classSpec.GatewayService) + // Setup WAF HTTP Filter on Enterprise. + if pr.cfg.Installation.Variant == operatorv1.TigeraSecureEnterprise { + // The WAF HTTP filter is not supported when the envoy proxy is deployed as a DaemonSet + // as there is no support for init containers in a DaemonSet. + if envoyProxy.Spec.Provider.Kubernetes.EnvoyDeployment != nil { + // Add or update the Init Container to the deployment + wafHTTPFilter := corev1.Container{ + Name: "waf-http-filter", + Image: pr.wafHTTPFilterImage, + Args: []string{ + "-logFileDirectory", + "/var/log/calico/waf", + "-logFileName", + "waf.log", + "-socketPath", + "/var/run/waf-http-filter/extproc.sock", + }, + RestartPolicy: ptr.ToPtr[corev1.ContainerRestartPolicy](corev1.ContainerRestartPolicyAlways), + VolumeMounts: []corev1.VolumeMount{ + { + Name: "waf-http-filter", + MountPath: "/var/run/waf-http-filter", + }, + { + Name: "var-log-calico", + MountPath: "/var/log/calico", + }, + }, + SecurityContext: securitycontext.NewRootContext(true), + } + hasWAFHTTPFilter := false + for i, initContainer := range envoyProxy.Spec.Provider.Kubernetes.EnvoyDeployment.InitContainers { + if initContainer.Name == wafHTTPFilter.Name { + hasWAFHTTPFilter = true + // Handle update + if initContainer.Image != wafHTTPFilter.Image { + envoyProxy.Spec.Provider.Kubernetes.EnvoyDeployment.InitContainers[i] = wafHTTPFilter + } + } + } + if !hasWAFHTTPFilter { + envoyProxy.Spec.Provider.Kubernetes.EnvoyDeployment.InitContainers = append(envoyProxy.Spec.Provider.Kubernetes.EnvoyDeployment.InitContainers, wafHTTPFilter) + } + + // Add or update Container volume mount + socketVolumeMount := corev1.VolumeMount{ + Name: "waf-http-filter", + MountPath: "/var/run/waf-http-filter", + } + hasSocketVolumeMount := false + for i, volumeMount := range envoyProxy.Spec.Provider.Kubernetes.EnvoyDeployment.Container.VolumeMounts { + if volumeMount.Name == socketVolumeMount.Name { + hasSocketVolumeMount = true + if volumeMount.MountPath != socketVolumeMount.MountPath { + envoyProxy.Spec.Provider.Kubernetes.EnvoyDeployment.Container.VolumeMounts[i] = socketVolumeMount + } + } + } + if !hasSocketVolumeMount { + envoyProxy.Spec.Provider.Kubernetes.EnvoyDeployment.Container.VolumeMounts = append(envoyProxy.Spec.Provider.Kubernetes.EnvoyDeployment.Container.VolumeMounts, socketVolumeMount) + } + + // Add or update Pod volumes + logsVolume := corev1.Volume{ + VolumeSource: corev1.VolumeSource{ + HostPath: &corev1.HostPathVolumeSource{ + Path: "/var/log/calico", + Type: ptr.ToPtr(corev1.HostPathDirectoryOrCreate), + }, + }, + Name: "var-log-calico", + } + socketVolume := corev1.Volume{ + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + Name: "waf-http-filter", + } + hasLogsVolume := false + hasSocketVolume := false + for i, volume := range envoyProxy.Spec.Provider.Kubernetes.EnvoyDeployment.Pod.Volumes { + if volume.Name == logsVolume.Name { + hasLogsVolume = true + // Handle update + if volume.VolumeSource.HostPath.Path != logsVolume.VolumeSource.HostPath.Path || volume.VolumeSource.HostPath.Type != logsVolume.VolumeSource.HostPath.Type { + envoyProxy.Spec.Provider.Kubernetes.EnvoyDeployment.Pod.Volumes[i] = logsVolume + } + } + if volume.Name == socketVolume.Name { + hasSocketVolume = true + if volume.VolumeSource.EmptyDir != socketVolume.VolumeSource.EmptyDir { + envoyProxy.Spec.Provider.Kubernetes.EnvoyDeployment.Pod.Volumes[i] = socketVolume + } + } + } + if !hasLogsVolume { + envoyProxy.Spec.Provider.Kubernetes.EnvoyDeployment.Pod.Volumes = append(envoyProxy.Spec.Provider.Kubernetes.EnvoyDeployment.Pod.Volumes, logsVolume) + } + if !hasSocketVolume { + envoyProxy.Spec.Provider.Kubernetes.EnvoyDeployment.Pod.Volumes = append(envoyProxy.Spec.Provider.Kubernetes.EnvoyDeployment.Pod.Volumes, socketVolume) + } + } + } + return envoyProxy } diff --git a/pkg/render/gateway_api_test.go b/pkg/render/gateway_api_test.go index acb7da0383..17e955aff0 100644 --- a/pkg/render/gateway_api_test.go +++ b/pkg/render/gateway_api_test.go @@ -23,6 +23,7 @@ import ( envoyapi "github.com/envoyproxy/gateway/api/v1alpha1" operatorv1 "github.com/tigera/operator/api/v1" "github.com/tigera/operator/pkg/components" + "github.com/tigera/operator/pkg/ptr" rtest "github.com/tigera/operator/pkg/render/common/test" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" @@ -719,4 +720,203 @@ var _ = Describe("Gateway API rendering tests", func() { Expect(ep4.Spec.Provider.Kubernetes.EnvoyService.LoadBalancerSourceRanges).To(ConsistOf("182.98.44.55/24")) Expect(*ep4.Spec.Provider.Kubernetes.EnvoyService.LoadBalancerIP).To(Equal(lbIP)) }) + + It("should not deploy waf-http-filter for open-source", func() { + installation := &operatorv1.InstallationSpec{ + Variant: operatorv1.Calico, + } + gatewayAPI := &operatorv1.GatewayAPI{ + Spec: operatorv1.GatewayAPISpec{ + GatewayClasses: []operatorv1.GatewayClassSpec{{Name: "tigera-gateway-class"}}, + }, + } + gatewayComp := GatewayAPIImplementationComponent(&GatewayAPIImplementationConfig{ + Installation: installation, + GatewayAPI: gatewayAPI, + }) + + objsToCreate, _ := gatewayComp.Objects() + proxy, err := rtest.GetResourceOfType[*envoyapi.EnvoyProxy](objsToCreate, "tigera-gateway-class", "tigera-gateway") + Expect(err).NotTo(HaveOccurred()) + envoyDeployment := proxy.Spec.Provider.Kubernetes.EnvoyDeployment + Expect(envoyDeployment).ToNot(BeNil()) + Expect(envoyDeployment.InitContainers).To(BeNil()) + Expect(envoyDeployment.Container).ToNot(BeNil()) + Expect(envoyDeployment.Container.VolumeMounts).To(BeNil()) + }) + + It("should deploy waf-http-filter for Enterprise", func() { + installation := &operatorv1.InstallationSpec{ + Variant: operatorv1.TigeraSecureEnterprise, + } + gatewayAPI := &operatorv1.GatewayAPI{ + Spec: operatorv1.GatewayAPISpec{ + GatewayClasses: []operatorv1.GatewayClassSpec{{Name: "tigera-gateway-class"}}, + }, + } + gatewayComp := GatewayAPIImplementationComponent(&GatewayAPIImplementationConfig{ + Installation: installation, + GatewayAPI: gatewayAPI, + }) + + objsToCreate, _ := gatewayComp.Objects() + proxy, err := rtest.GetResourceOfType[*envoyapi.EnvoyProxy](objsToCreate, "tigera-gateway-class", "tigera-gateway") + Expect(err).NotTo(HaveOccurred()) + + envoyDeployment := proxy.Spec.Provider.Kubernetes.EnvoyDeployment + Expect(envoyDeployment).ToNot(BeNil()) + + Expect(envoyDeployment.Pod).ToNot(BeNil()) + Expect(envoyDeployment.Pod.Volumes).To(HaveLen(2)) + Expect(envoyDeployment.Pod.Volumes[0].Name).To(Equal("var-log-calico")) + Expect(envoyDeployment.Pod.Volumes[0].HostPath.Path).To(Equal("/var/log/calico")) + Expect(envoyDeployment.Pod.Volumes[1].Name).To(Equal("waf-http-filter")) + Expect(envoyDeployment.Pod.Volumes[1].EmptyDir).ToNot(BeNil()) + + Expect(envoyDeployment.InitContainers[0].Name).To(Equal("waf-http-filter")) + Expect(*envoyDeployment.InitContainers[0].RestartPolicy).To(Equal(corev1.ContainerRestartPolicyAlways)) + Expect(envoyDeployment.InitContainers[0].VolumeMounts).To(HaveLen(2)) + Expect(envoyDeployment.InitContainers[0].VolumeMounts).To(ContainElements([]corev1.VolumeMount{ + { + Name: "waf-http-filter", + MountPath: "/var/run/waf-http-filter", + }, + { + Name: "var-log-calico", + MountPath: "/var/log/calico", + }, + })) + + Expect(envoyDeployment.Container).ToNot(BeNil()) + Expect(envoyDeployment.Container.VolumeMounts).To(HaveLen(1)) + Expect(envoyDeployment.Container.VolumeMounts).To(ContainElement(corev1.VolumeMount{ + Name: "waf-http-filter", + MountPath: "/var/run/waf-http-filter", + })) + }) + + It("should deploy waf-http-filter for Enterprise when using a custom proxy", func() { + installation := &operatorv1.InstallationSpec{ + Variant: operatorv1.TigeraSecureEnterprise, + } + gatewayAPI := &operatorv1.GatewayAPI{ + Spec: operatorv1.GatewayAPISpec{ + GatewayClasses: []operatorv1.GatewayClassSpec{{ + Name: "custom-class", + EnvoyProxyRef: &operatorv1.NamespacedName{ + Namespace: "default", + Name: "my-proxy", + }, + }}, + }, + } + envoyProxy := &envoyapi.EnvoyProxy{ + TypeMeta: metav1.TypeMeta{ + Kind: "EnvoyProxy", + APIVersion: "gateway.envoyproxy.io/v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "my-proxy", + Namespace: "default", + }, + Spec: envoyapi.EnvoyProxySpec{ + Provider: &envoyapi.EnvoyProxyProvider{ + Type: envoyapi.ProviderTypeKubernetes, + Kubernetes: &envoyapi.EnvoyProxyKubernetesProvider{ + EnvoyDeployment: &envoyapi.KubernetesDeploymentSpec{ + InitContainers: []corev1.Container{ + { + Name: "some-other-sidecar", + RestartPolicy: ptr.ToPtr[corev1.ContainerRestartPolicy](corev1.ContainerRestartPolicyAlways), + VolumeMounts: []corev1.VolumeMount{ + { + Name: "some-other-volume", + MountPath: "/test", + }, + }, + }, + }, + Container: &envoyapi.KubernetesContainerSpec{ + VolumeMounts: []corev1.VolumeMount{ + { + Name: "some-other-volume", + MountPath: "/test", + }, + }, + }, + Pod: &envoyapi.KubernetesPodSpec{ + Volumes: []corev1.Volume{ + { + Name: "some-other-volume", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + }, + }, + }, + }, + }, + }, + } + gatewayComp := GatewayAPIImplementationComponent(&GatewayAPIImplementationConfig{ + Installation: installation, + GatewayAPI: gatewayAPI, + CustomEnvoyProxies: map[string]*envoyapi.EnvoyProxy{ + "custom-class": envoyProxy, + }, + }) + + objsToCreate, _ := gatewayComp.Objects() + + // Get the four expected GatewayClasses. + gc, err := rtest.GetResourceOfType[*gapi.GatewayClass](objsToCreate, "custom-class", "tigera-gateway") + Expect(err).NotTo(HaveOccurred()) + + // Get their four EnvoyProxies. + Expect(gc.Spec.ParametersRef).NotTo(BeNil()) + proxy, err := rtest.GetResourceOfType[*envoyapi.EnvoyProxy](objsToCreate, gc.Spec.ParametersRef.Name, string(*gc.Spec.ParametersRef.Namespace)) + Expect(err).NotTo(HaveOccurred()) + + envoyDeployment := proxy.Spec.Provider.Kubernetes.EnvoyDeployment + Expect(envoyDeployment).ToNot(BeNil()) + + Expect(envoyDeployment.InitContainers).To(HaveLen(2)) + Expect(envoyDeployment.InitContainers[0].Name).To(Equal("some-other-sidecar")) + Expect(envoyDeployment.InitContainers[1].Name).To(Equal("waf-http-filter")) + Expect(*envoyDeployment.InitContainers[1].RestartPolicy).To(Equal(corev1.ContainerRestartPolicyAlways)) + Expect(envoyDeployment.InitContainers[1].VolumeMounts).To(HaveLen(2)) + Expect(envoyDeployment.InitContainers[1].VolumeMounts).To(ContainElements([]corev1.VolumeMount{ + { + Name: "waf-http-filter", + MountPath: "/var/run/waf-http-filter", + }, + { + Name: "var-log-calico", + MountPath: "/var/log/calico", + }, + })) + + Expect(envoyDeployment.Container).ToNot(BeNil()) + Expect(envoyDeployment.Container.VolumeMounts).To(ContainElements( + corev1.VolumeMount{ + Name: "some-other-volume", + MountPath: "/test", + }, corev1.VolumeMount{ + Name: "waf-http-filter", + MountPath: "/var/run/waf-http-filter", + }, + )) + + Expect(envoyDeployment.Pod).ToNot(BeNil()) + Expect(envoyDeployment.Pod.Volumes).To(HaveLen(3)) + Expect(envoyDeployment.Pod.Volumes[0].Name).To(Equal("some-other-volume")) + Expect(envoyDeployment.Pod.Volumes[0].EmptyDir).ToNot(BeNil()) + Expect(envoyDeployment.Pod.Volumes[1].Name).To(Equal("var-log-calico")) + Expect(envoyDeployment.Pod.Volumes[1].HostPath.Path).To(Equal("/var/log/calico")) + Expect(envoyDeployment.Pod.Volumes[2].Name).To(Equal("waf-http-filter")) + Expect(envoyDeployment.Pod.Volumes[2].EmptyDir).ToNot(BeNil()) + + }) + })