Skip to content
Open
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
45 changes: 45 additions & 0 deletions api/v1/gatewayapi_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,51 @@ type GatewayAPISpec struct {
// does not yet have any version of those CRDs.
// +optional
CRDManagement *CRDManagement `json:"crdManagement,omitempty"`

// Extensions enables and configures Tigera-built add-ons that sit on top of the
// Gateway API data plane. Each add-on is opt-in: an unset Extensions, an unset
// add-on field, and an empty add-on object all leave the add-on disabled.
// +optional
Extensions *GatewayAPIExtensions `json:"extensions,omitempty"`
}

// GatewayAPIExtensions enables and configures Tigera-built Gateway API add-ons.
type GatewayAPIExtensions struct {
// WAF enables and configures the Tigera Web Application Firewall (Coraza WASM
// + applicationlayer reconcilers). Default-off semantics: when WAF is nil,
// when WAF.State is nil, and when WAF.State is "Disabled", the operator does
// not render the WAF env vars or RBAC on calico-kube-controllers. Set
// WAF.State = "Enabled" to turn the feature on. See design
// `tigera/designs#25` (PMREQ-384) for the full surface.
// +optional
WAF *WAFExtensionSpec `json:"waf,omitempty"`
}

// WAFExtensionSpec configures the WAF Gateway API add-on.
type WAFExtensionSpec struct {
// State turns the WAF Gateway API add-on on or off. Default (nil or
// "Disabled") means the operator does not render the WAF surface on
// calico-kube-controllers. Set to "Enabled" to opt in.
// +optional
State *WAFExtensionState `json:"state,omitempty"`
}

// WAFExtensionState is the on/off enum for the WAF Gateway API add-on.
// +kubebuilder:validation:Enum=Enabled;Disabled
type WAFExtensionState string

const (
WAFExtensionStateEnabled WAFExtensionState = "Enabled"
WAFExtensionStateDisabled WAFExtensionState = "Disabled"
)

// IsWAFGatewayExtensionEnabled returns true iff spec.extensions.waf.state == Enabled.
// Unset Extensions, unset WAF, unset State, and explicit Disabled all return false.
func (s *GatewayAPISpec) IsWAFGatewayExtensionEnabled() bool {
if s == nil || s.Extensions == nil || s.Extensions.WAF == nil || s.Extensions.WAF.State == nil {
return false
}
return *s.Extensions.WAF.State == WAFExtensionStateEnabled
}

type GatewayClassSpec struct {
Expand Down
45 changes: 45 additions & 0 deletions api/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions config/enterprise_versions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ components:
dikastes:
image: dikastes
version: master
coraza-wasm:
image: coraza-wasm
version: master
egress-gateway:
image: egress-gateway
version: master
Expand Down
10 changes: 10 additions & 0 deletions hack/gen-versions/enterprise.go.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,15 @@ var (
variant: enterpriseVariant,
}
{{- end }}
{{ with index .Components "coraza-wasm" }}
ComponentCorazaWASM = Component{
Version: "{{ .Version }}",
Image: "{{ .Image }}",
Registry: "{{ .Registry }}",
imagePath: "{{ .ImagePath }}",
variant: enterpriseVariant,
}
{{- end }}
{{ with index .Components "coreos-prometheus" }}
ComponentCoreOSPrometheus = Component{
Version: "{{ .Version }}",
Expand Down Expand Up @@ -316,6 +325,7 @@ var (
ComponentGatewayL7Collector,
ComponentEnvoyProxy,
ComponentDikastes,
ComponentCorazaWASM,
ComponentPrometheus,
ComponentPrometheusAlertmanager,
ComponentTigeraNode,
Expand Down
4 changes: 4 additions & 0 deletions pkg/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ const (
EgressAccessControlFeature = "egress-access-control"
// PolicyRecommendation feature name
PolicyRecommendationFeature = "policy-recommendation"
// GatewayAddonsFeature gates Tigera-built add-ons that layer on top of an
// ingress gateway (currently the WAF v2/v3 admission webhook). The bare
// ingress gateway data path is NOT licensed by this feature.
GatewayAddonsFeature = "ingress-gateway-addons"
// MultipleOwnersLabel used to indicate multiple owner references.
// If the render code places this label on an object, the object mergeState machinery will merge owner
// references with any that already exist on the object rather than replace the owner references. Further
Expand Down
9 changes: 9 additions & 0 deletions pkg/components/enterprise.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,14 @@ var (
variant: enterpriseVariant,
}

ComponentCorazaWASM = Component{
Version: "master",
Image: "coraza-wasm",
Registry: "",
imagePath: "",
variant: enterpriseVariant,
}

ComponentCoreOSPrometheus = Component{
Version: "v3.9.1",
variant: enterpriseVariant,
Expand Down Expand Up @@ -283,6 +291,7 @@ var (
ComponentGatewayL7Collector,
ComponentEnvoyProxy,
ComponentDikastes,
ComponentCorazaWASM,
ComponentPrometheus,
ComponentPrometheusAlertmanager,
ComponentTigeraNode,
Expand Down
46 changes: 41 additions & 5 deletions pkg/controller/applicationlayer/applicationlayer_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
v3 "github.com/tigera/api/pkg/apis/projectcalico/v3"
operatorv1 "github.com/tigera/operator/api/v1"
"github.com/tigera/operator/pkg/common"
"github.com/tigera/operator/pkg/controller/gatewayapi"
"github.com/tigera/operator/pkg/controller/options"
"github.com/tigera/operator/pkg/controller/status"
"github.com/tigera/operator/pkg/controller/utils"
Expand Down Expand Up @@ -141,6 +142,13 @@ func add(mgr manager.Manager, c ctrlruntime.Controller) error {
return fmt.Errorf("applicationlayer-controller failed to watch FelixConfiguration resource: %w", err)
}

// Watch for changes to GatewayAPI; its WAF data-plane extension shares the
// FelixConfiguration WAFEventLogsFileEnabled toggle, so toggling it must re-trigger this controller.
err = c.WatchObject(&operatorv1.GatewayAPI{}, &handler.EnqueueRequestForObject{})
if err != nil {
return fmt.Errorf("applicationlayer-controller failed to watch GatewayAPI resource: %w", err)
}

// Watch for changes to TigeraStatus.
if err = utils.AddTigeraStatusWatch(c, ResourceName); err != nil {
return fmt.Errorf("applicationlayer-controller failed to watch applicationlayer Tigerastatus: %w", err)
Expand Down Expand Up @@ -177,7 +185,7 @@ func (r *ReconcileApplicationLayer) Reconcile(ctx context.Context, request recon
// Owned objects are automatically garbage collected. For additional cleanup logic use finalizers.
reqLogger.Info("ApplicationLayer object not found")
// Patch tproxyMode if it's needed after crd deletion.
if err = r.patchFelixConfiguration(ctx, nil); err != nil {
if err = r.patchFelixConfiguration(ctx, nil, r.isGatewayWAFEnabled(ctx)); err != nil {
reqLogger.Error(err, "Error patching felix configuration")
}
r.status.OnCRNotFound()
Expand Down Expand Up @@ -241,7 +249,7 @@ func (r *ReconcileApplicationLayer) Reconcile(ctx context.Context, request recon
}

// Patch felix configuration if necessary.
if err = r.patchFelixConfiguration(ctx, instance); err != nil {
if err = r.patchFelixConfiguration(ctx, instance, r.isGatewayWAFEnabled(ctx)); err != nil {
r.status.SetDegraded(operatorv1.ResourcePatchError, "Error patching felix configuration", err, reqLogger)
return reconcile.Result{}, err
}
Expand Down Expand Up @@ -515,8 +523,12 @@ func (r *ReconcileApplicationLayer) getTProxyMode(al *operatorv1.ApplicationLaye

// patchFelixConfiguration takes all application layer specs as arguments and patches felix config.
// If at least one of the specs requires TPROXYMode as "Enabled" it'll be patched as "Enabled" otherwise it is "Disabled".
func (r *ReconcileApplicationLayer) patchFelixConfiguration(ctx context.Context, al *operatorv1.ApplicationLayer) error {
// gatewayWAFEnabled reflects the GatewayAPI WAF data-plane extension (design-25): its audit events flow through
// Felix's WAF event log, so it shares the WAFEventLogsFileEnabled toggle with the ApplicationLayer WAF.
func (r *ReconcileApplicationLayer) patchFelixConfiguration(ctx context.Context, al *operatorv1.ApplicationLayer, gatewayWAFEnabled bool) error {
_, err := utils.PatchFelixConfiguration(ctx, r.client, func(fc *v3.FelixConfiguration) (bool, error) {
wafEventLogsFileEnabled := wafEventLogsFileRequired(al, gatewayWAFEnabled)

var tproxyMode string
if ok, v := r.getTProxyMode(al); ok {
tproxyMode = v
Expand All @@ -529,6 +541,14 @@ func (r *ReconcileApplicationLayer) patchFelixConfiguration(ctx context.Context,
//
// The felix bug was fixed in v3.16, v3.15.1 and v3.14.4; it should be safe to set new config fields
// once we know we're only upgrading from those versions and above.
//
// WAFEventLogsFileEnabled is an independent field: still enable it when a WAF producer
// (ApplicationLayer or the gateway data plane) requires it, without touching TPROXYMode.
if wafEventLogsFileEnabled && (fc.Spec.WAFEventLogsFileEnabled == nil || !*fc.Spec.WAFEventLogsFileEnabled) {
fc.Spec.WAFEventLogsFileEnabled = &wafEventLogsFileEnabled
log.Info("Patching FelixConfiguration: ", "wafEventLogsFileEnabled", wafEventLogsFileEnabled)
return true, nil
}
return false, nil
}

Expand All @@ -541,8 +561,6 @@ func (r *ReconcileApplicationLayer) patchFelixConfiguration(ctx context.Context,
policySyncPrefix := r.getPolicySyncPathPrefix(&fc.Spec, al)
policySyncPrefixSetDesired := fc.Spec.PolicySyncPathPrefix == policySyncPrefix
tproxyModeSetDesired := fc.Spec.TPROXYMode != "" && fc.Spec.TPROXYMode == string(tproxyMode)
wafEventLogsFileEnabled := al != nil && ((al.Spec.SidecarInjection != nil && *al.Spec.SidecarInjection == operatorv1.SidecarEnabled) ||
(al.Spec.WebApplicationFirewall != nil && *al.Spec.WebApplicationFirewall == operatorv1.WAFEnabled))
wafEventLogsFileEnabledDesired := fc.Spec.WAFEventLogsFileEnabled != nil && *fc.Spec.WAFEventLogsFileEnabled == wafEventLogsFileEnabled

// If tproxy mode is already set to desired state return false to indicate patch not needed.
Expand All @@ -565,3 +583,21 @@ func (r *ReconcileApplicationLayer) patchFelixConfiguration(ctx context.Context,

return err
}

// wafEventLogsFileRequired reports whether Felix should write WAF event logs to file, which is required
// when either the ApplicationLayer WAF/sidecar or the GatewayAPI WAF data-plane extension is enabled.
func wafEventLogsFileRequired(al *operatorv1.ApplicationLayer, gatewayWAFEnabled bool) bool {
return gatewayWAFEnabled ||
(al != nil && ((al.Spec.SidecarInjection != nil && *al.Spec.SidecarInjection == operatorv1.SidecarEnabled) ||
(al.Spec.WebApplicationFirewall != nil && *al.Spec.WebApplicationFirewall == operatorv1.WAFEnabled)))
}

// isGatewayWAFEnabled reports whether the GatewayAPI WAF data-plane extension is enabled. A missing or
// unreadable GatewayAPI CR is treated as disabled.
func (r *ReconcileApplicationLayer) isGatewayWAFEnabled(ctx context.Context) bool {
gw, _, err := gatewayapi.GetGatewayAPI(ctx, r.client)
if err != nil {
return false
}
return gw.Spec.IsWAFGatewayExtensionEnabled()
}
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,34 @@ var _ = Describe("Application layer controller tests", func() {
Expect(fc.Spec.TPROXYMode).To(Equal(""))
})

It("should enable WAFEventLogsFileEnabled when the GatewayAPI WAF extension is enabled (no ApplicationLayer CR)", func() {
// The gateway data-plane WAF (design-25) emits audit events that flow through Felix's WAF event
// log, so it requires the same FelixConfiguration toggle as the legacy ApplicationLayer WAF — even
// when no ApplicationLayer CR is present.
mockStatus.On("OnCRNotFound").Return()

By("creating a GatewayAPI CR with the WAF extension enabled")
wafEnabled := operatorv1.WAFExtensionStateEnabled
Expect(c.Create(ctx, &operatorv1.GatewayAPI{
ObjectMeta: metav1.ObjectMeta{Name: "default"},
Spec: operatorv1.GatewayAPISpec{
Extensions: &operatorv1.GatewayAPIExtensions{
WAF: &operatorv1.WAFExtensionSpec{State: &wafEnabled},
},
},
})).NotTo(HaveOccurred())

By("reconciling without an ApplicationLayer resource")
_, err := r.Reconcile(ctx, reconcile.Request{})
Expect(err).ShouldNot(HaveOccurred())

By("ensuring felix WAFEventLogsFileEnabled is true")
fc := v3.FelixConfiguration{ObjectMeta: metav1.ObjectMeta{Name: "default"}}
Expect(test.GetResource(c, &fc)).To(BeNil())
Expect(fc.Spec.WAFEventLogsFileEnabled).NotTo(BeNil())
Expect(*fc.Spec.WAFEventLogsFileEnabled).To(BeTrue())
})

It("should render accurate resources for for log collection", func() {
mockStatus.On("AddDaemonsets", mock.Anything).Return()
mockStatus.On("AddDeployments", mock.Anything).Return()
Expand Down
Loading
Loading