diff --git a/deployments/common/crds/k8s.nginx.org_policies.yaml b/deployments/common/crds/k8s.nginx.org_policies.yaml index 05587bd0a3..57740d90e8 100644 --- a/deployments/common/crds/k8s.nginx.org_policies.yaml +++ b/deployments/common/crds/k8s.nginx.org_policies.yaml @@ -156,6 +156,18 @@ spec: type: boolean logDest: type: string + securityLogs: + type: array + items: + description: SecurityLog defines the security log of a WAF policy. + type: object + properties: + apLogConf: + type: string + enable: + type: boolean + logDest: + type: string status: description: PolicyStatus is the status of the policy resource type: object diff --git a/deployments/helm-chart/crds/k8s.nginx.org_policies.yaml b/deployments/helm-chart/crds/k8s.nginx.org_policies.yaml index 05587bd0a3..57740d90e8 100644 --- a/deployments/helm-chart/crds/k8s.nginx.org_policies.yaml +++ b/deployments/helm-chart/crds/k8s.nginx.org_policies.yaml @@ -156,6 +156,18 @@ spec: type: boolean logDest: type: string + securityLogs: + type: array + items: + description: SecurityLog defines the security log of a WAF policy. + type: object + properties: + apLogConf: + type: string + enable: + type: boolean + logDest: + type: string status: description: PolicyStatus is the status of the policy resource type: object diff --git a/docs/content/configuration/policy-resource.md b/docs/content/configuration/policy-resource.md index 91c72e2527..9c783a0c70 100644 --- a/docs/content/configuration/policy-resource.md +++ b/docs/content/configuration/policy-resource.md @@ -355,17 +355,20 @@ For `kubectl get` and similar commands, you can also use the short name `pol` in The WAF policy configures NGINX Plus to secure client requests using App Protect policies. -For example, the following policy will enable the referenced APPolicy and APLogConf with the configured log destination: +For example, the following policy will enable the referenced APPolicy. You can configure multiple APLogConfs with log destinations: ```yaml waf: enable: true apPolicy: "default/dataguard-alarm" - securityLog: - enable: true + securityLogs: + - enable: true apLogConf: "default/logconf" logDest: "syslog:server=syslog-svc.default:514" + - enable: true + apLogConf: "default/logconf" + logDest: "syslog:server=syslog-svc-secondary.default:514" ``` - +> Note: The field `waf.securityLog` is deprecated and will be removed in future releases.It will be ignored if `waf.securityLogs` is populated. > Note: The feature is implemented using the NGINX Plus [NGINX App Protect Module](https://docs.nginx.com/nginx-app-protect/configuration/). {{% table %}} diff --git a/examples/custom-resources/waf/waf.yaml b/examples/custom-resources/waf/waf.yaml index c8d87993c9..41d68a368c 100644 --- a/examples/custom-resources/waf/waf.yaml +++ b/examples/custom-resources/waf/waf.yaml @@ -6,7 +6,7 @@ spec: waf: enable: true apPolicy: "default/dataguard-alarm" - securityLog: - enable: true + securityLogs: + - enable: true apLogConf: "default/logconf" logDest: "syslog:server=syslog-svc.default:514" diff --git a/internal/configs/ingress.go b/internal/configs/ingress.go index af1b777f99..f0f94e08a6 100644 --- a/internal/configs/ingress.go +++ b/internal/configs/ingress.go @@ -76,7 +76,8 @@ type MergeableIngresses struct { } func generateNginxCfg(ingEx *IngressEx, apResources *AppProtectResources, dosResource *appProtectDosResource, isMinion bool, - baseCfgParams *ConfigParams, isPlus bool, isResolverConfigured bool, staticParams *StaticConfigParams, isWildcardEnabled bool) (version1.IngressNginxConfig, Warnings) { + baseCfgParams *ConfigParams, isPlus bool, isResolverConfigured bool, staticParams *StaticConfigParams, isWildcardEnabled bool, +) (version1.IngressNginxConfig, Warnings) { hasAppProtect := staticParams.MainAppProtectLoadModule hasAppProtectDos := staticParams.MainAppProtectDosLoadModule @@ -290,7 +291,8 @@ func generateNginxCfg(ingEx *IngressEx, apResources *AppProtectResources, dosRes } func generateJWTConfig(owner runtime.Object, secretRefs map[string]*secrets.SecretReference, cfgParams *ConfigParams, - redirectLocationName string) (*version1.JWTAuth, *version1.JWTRedirectLocation, Warnings) { + redirectLocationName string, +) (*version1.JWTAuth, *version1.JWTRedirectLocation, Warnings) { warnings := newWarnings() secretRef := secretRefs[cfgParams.JWTKey] @@ -326,7 +328,8 @@ func generateJWTConfig(owner runtime.Object, secretRefs map[string]*secrets.Secr } func addSSLConfig(server *version1.Server, owner runtime.Object, host string, ingressTLS []networking.IngressTLS, - secretRefs map[string]*secrets.SecretReference, isWildcardEnabled bool) Warnings { + secretRefs map[string]*secrets.SecretReference, isWildcardEnabled bool, +) Warnings { warnings := newWarnings() var tlsEnabled bool @@ -427,7 +430,8 @@ func upstreamRequiresQueue(name string, ingEx *IngressEx, cfg *ConfigParams) (n } func createUpstream(ingEx *IngressEx, name string, backend *networking.IngressBackend, stickyCookie string, cfg *ConfigParams, - isPlus bool, isResolverConfigured bool, isLatencyMetricsEnabled bool) version1.Upstream { + isPlus bool, isResolverConfigured bool, isLatencyMetricsEnabled bool, +) version1.Upstream { var ups version1.Upstream labels := version1.UpstreamLabels{ Service: backend.Service.Name, @@ -534,8 +538,8 @@ func upstreamMapToSlice(upstreams map[string]version1.Upstream) []version1.Upstr func generateNginxCfgForMergeableIngresses(mergeableIngs *MergeableIngresses, apResources *AppProtectResources, dosResource *appProtectDosResource, baseCfgParams *ConfigParams, isPlus bool, isResolverConfigured bool, - staticParams *StaticConfigParams, isWildcardEnabled bool) (version1.IngressNginxConfig, Warnings) { - + staticParams *StaticConfigParams, isWildcardEnabled bool, +) (version1.IngressNginxConfig, Warnings) { var masterServer version1.Server var locations []version1.Location var upstreams []version1.Upstream diff --git a/internal/configs/version1/templates_test.go b/internal/configs/version1/templates_test.go index 230ce77e31..ef797fc2e2 100644 --- a/internal/configs/version1/templates_test.go +++ b/internal/configs/version1/templates_test.go @@ -39,7 +39,6 @@ var ( ) var ingCfg = IngressNginxConfig{ - Servers: []Server{ { Name: "test.example.com", diff --git a/internal/configs/version2/http.go b/internal/configs/version2/http.go index 0815fa208f..279dd5da5b 100644 --- a/internal/configs/version2/http.go +++ b/internal/configs/version2/http.go @@ -123,7 +123,7 @@ type WAF struct { Enable string ApPolicy string ApSecurityLogEnable bool - ApLogConf string + ApLogConf []string } // Dos defines Dos configuration. diff --git a/internal/configs/version2/nginx-plus.virtualserver.tmpl b/internal/configs/version2/nginx-plus.virtualserver.tmpl index b8c3beacca..b5b85b223a 100644 --- a/internal/configs/version2/nginx-plus.virtualserver.tmpl +++ b/internal/configs/version2/nginx-plus.virtualserver.tmpl @@ -191,7 +191,9 @@ server { {{ if .ApSecurityLogEnable }} app_protect_security_log_enable on; - app_protect_security_log {{ .ApLogConf }}; + {{ range $logconf := .ApLogConf }} + app_protect_security_log {{ $logconf }}; + {{ end }} {{ end }} {{ end }} @@ -370,7 +372,9 @@ server { {{ if .ApSecurityLogEnable }} app_protect_security_log_enable on; - app_protect_security_log {{ .ApLogConf }}; + {{ range $logconf := .ApLogConf }} + app_protect_security_log {{ $logconf }}; + {{ end }} {{ end }} {{ end }} diff --git a/internal/configs/version2/templates_test.go b/internal/configs/version2/templates_test.go index ee36cfad27..37cef3ba81 100644 --- a/internal/configs/version2/templates_test.go +++ b/internal/configs/version2/templates_test.go @@ -150,7 +150,7 @@ var virtualServerCfg = VirtualServerConfig{ WAF: &WAF{ ApPolicy: "/etc/nginx/waf/nac-policies/default-dataguard-alarm", ApSecurityLogEnable: true, - ApLogConf: "/etc/nginx/waf/nac-logconfs/default-logconf", + ApLogConf: []string{"/etc/nginx/waf/nac-logconfs/default-logconf"}, }, Snippets: []string{"# server snippet"}, InternalRedirectLocations: []InternalRedirectLocation{ diff --git a/internal/configs/virtualserver.go b/internal/configs/virtualserver.go index 0395c88025..ab16e9f999 100644 --- a/internal/configs/virtualserver.go +++ b/internal/configs/virtualserver.go @@ -1041,7 +1041,8 @@ func (p *policiesCfg) addWAFConfig( } } - if waf.SecurityLog != nil { + if waf.SecurityLog != nil && waf.SecurityLogs == nil { + glog.V(2).Info("the field securityLog is deprecated nad will be removed in future releases. Use field securityLogs instead") p.WAF.ApSecurityLogEnable = true logConfKey := waf.SecurityLog.ApLogConf @@ -1052,13 +1053,31 @@ func (p *policiesCfg) addWAFConfig( if logConfPath, ok := apResources.LogConfs[logConfKey]; ok { logDest := generateString(waf.SecurityLog.LogDest, "syslog:server=localhost:514") - p.WAF.ApLogConf = fmt.Sprintf("%s %s", logConfPath, logDest) + p.WAF.ApLogConf = []string{fmt.Sprintf("%s %s", logConfPath, logDest)} } else { res.addWarningf("WAF policy %s references an invalid or non-existing log config %s", polKey, logConfKey) res.isError = true } } + if waf.SecurityLogs != nil { + p.WAF.ApSecurityLogEnable = true + p.WAF.ApLogConf = []string{} + for _, loco := range waf.SecurityLogs { + logConfKey := loco.ApLogConf + hasNamepace := strings.Contains(logConfKey, "/") + if !hasNamepace { + logConfKey = fmt.Sprintf("%v/%v", polNamespace, logConfKey) + } + if logConfPath, ok := apResources.LogConfs[logConfKey]; ok { + logDest := generateString(loco.LogDest, "syslog:server=localhost:514") + p.WAF.ApLogConf = append(p.WAF.ApLogConf, fmt.Sprintf("%s %s", logConfPath, logDest)) + } else { + res.addWarningf("WAF policy %s references an invalid or non-existing log config %s", polKey, logConfKey) + res.isError = true + } + } + } return res } @@ -1575,7 +1594,8 @@ type errorPageDetails struct { func generateLocation(path string, upstreamName string, upstream conf_v1.Upstream, action *conf_v1.Action, cfgParams *ConfigParams, errorPages errorPageDetails, internal bool, proxySSLName string, originalPath string, locSnippets string, enableSnippets bool, retLocIndex int, isVSR bool, vsrName string, - vsrNamespace string, vscWarnings Warnings) (version2.Location, *version2.ReturnLocation) { + vsrNamespace string, vscWarnings Warnings, +) (version2.Location, *version2.ReturnLocation) { locationSnippets := generateSnippets(enableSnippets, locSnippets, cfgParams.LocationSnippets) if action.Redirect != nil { @@ -1674,7 +1694,8 @@ func generateProxyAddHeaders(proxy *conf_v1.ActionProxy) []version2.AddHeader { func generateLocationForProxying(path string, upstreamName string, upstream conf_v1.Upstream, cfgParams *ConfigParams, errorPages []conf_v1.ErrorPage, internal bool, errPageIndex int, - proxySSLName string, proxy *conf_v1.ActionProxy, originalPath string, locationSnippets []string, isVSR bool, vsrName string, vsrNamespace string) version2.Location { + proxySSLName string, proxy *conf_v1.ActionProxy, originalPath string, locationSnippets []string, isVSR bool, vsrName string, vsrNamespace string, +) version2.Location { return version2.Location{ Path: generatePath(path), Internal: internal, @@ -1741,7 +1762,8 @@ func generateLocationForRedirect( } func generateLocationForReturn(path string, locationSnippets []string, actionReturn *conf_v1.ActionReturn, - retLocIndex int) (version2.Location, *version2.ReturnLocation) { + retLocIndex int, +) (version2.Location, *version2.ReturnLocation) { defaultType := actionReturn.Type if defaultType == "" { defaultType = "text/plain" @@ -1873,7 +1895,8 @@ func generateDefaultSplitsConfig( func generateMatchesConfig(route conf_v1.Route, upstreamNamer *upstreamNamer, crUpstreams map[string]conf_v1.Upstream, variableNamer *variableNamer, index int, scIndex int, cfgParams *ConfigParams, errorPages errorPageDetails, - locSnippets string, enableSnippets bool, retLocIndex int, isVSR bool, vsrName string, vsrNamespace string, vscWarnings Warnings) routingCfg { + locSnippets string, enableSnippets bool, retLocIndex int, isVSR bool, vsrName string, vsrNamespace string, vscWarnings Warnings, +) routingCfg { // Generate maps var maps []version2.Map @@ -2101,7 +2124,8 @@ func getNameForSourceForMatchesRouteMapFromCondition(condition conf_v1.Condition } func (vsc *virtualServerConfigurator) generateSSLConfig(owner runtime.Object, tls *conf_v1.TLS, namespace string, - secretRefs map[string]*secrets.SecretReference, cfgParams *ConfigParams) *version2.SSL { + secretRefs map[string]*secrets.SecretReference, cfgParams *ConfigParams, +) *version2.SSL { if tls == nil { return nil } diff --git a/internal/configs/virtualserver_test.go b/internal/configs/virtualserver_test.go index 7571f2c0aa..306c097320 100644 --- a/internal/configs/virtualserver_test.go +++ b/internal/configs/virtualserver_test.go @@ -2938,7 +2938,7 @@ func TestGeneratePolicies(t *testing.T) { Enable: "on", ApPolicy: "/etc/nginx/waf/nac-policies/default-dataguard-alarm", ApSecurityLogEnable: true, - ApLogConf: "/etc/nginx/waf/nac-logconfs/default-logconf syslog:server=127.0.0.1:514", + ApLogConf: []string{"/etc/nginx/waf/nac-logconfs/default-logconf syslog:server=127.0.0.1:514"}, }, }, msg: "WAF reference", @@ -6723,7 +6723,6 @@ func TestGenerateHealthCheck(t *testing.T) { msg string }{ { - upstream: conf_v1.Upstream{ HealthCheck: &conf_v1.HealthCheck{ Enable: true, @@ -6905,7 +6904,6 @@ func TestGenerateGrpcHealthCheck(t *testing.T) { msg string }{ { - upstream: conf_v1.Upstream{ HealthCheck: &conf_v1.HealthCheck{ Enable: true, @@ -8248,7 +8246,6 @@ func TestAddWafConfig(t *testing.T) { msg string }{ { - wafInput: &conf_v1.WAF{ Enable: true, }, @@ -8265,7 +8262,6 @@ func TestAddWafConfig(t *testing.T) { msg: "valid waf config, default App Protect config", }, { - wafInput: &conf_v1.WAF{ Enable: true, ApPolicy: "dataguard-alarm", @@ -8288,13 +8284,42 @@ func TestAddWafConfig(t *testing.T) { wafConfig: &version2.WAF{ ApPolicy: "/etc/nginx/waf/nac-policies/default-dataguard-alarm", ApSecurityLogEnable: true, - ApLogConf: "/etc/nginx/waf/nac-logconfs/default-logconf", + ApLogConf: []string{"/etc/nginx/waf/nac-logconfs/default-logconf"}, + }, + expected: &validationResults{isError: false}, + msg: "valid waf config", + }, + { + wafInput: &conf_v1.WAF{ + Enable: true, + ApPolicy: "dataguard-alarm", + SecurityLogs: []*conf_v1.SecurityLog{ + { + Enable: true, + ApLogConf: "logconf", + LogDest: "syslog:server=127.0.0.1:514", + }, + }, + }, + polKey: "default/waf-policy", + polNamespace: "default", + apResources: &appProtectResourcesForVS{ + Policies: map[string]string{ + "default/dataguard-alarm": "/etc/nginx/waf/nac-policies/default-dataguard-alarm", + }, + LogConfs: map[string]string{ + "default/logconf": "/etc/nginx/waf/nac-logconfs/default-logconf", + }, + }, + wafConfig: &version2.WAF{ + ApPolicy: "/etc/nginx/waf/nac-policies/default-dataguard-alarm", + ApSecurityLogEnable: true, + ApLogConf: []string{"/etc/nginx/waf/nac-logconfs/default-logconf"}, }, expected: &validationResults{isError: false}, msg: "valid waf config", }, { - wafInput: &conf_v1.WAF{ Enable: true, ApPolicy: "default/dataguard-alarm", @@ -8315,7 +8340,7 @@ func TestAddWafConfig(t *testing.T) { wafConfig: &version2.WAF{ ApPolicy: "/etc/nginx/waf/nac-policies/default-dataguard-alarm", ApSecurityLogEnable: true, - ApLogConf: "/etc/nginx/waf/nac-logconfs/default-logconf", + ApLogConf: []string{"/etc/nginx/waf/nac-logconfs/default-logconf"}, }, expected: &validationResults{ isError: true, @@ -8326,7 +8351,6 @@ func TestAddWafConfig(t *testing.T) { msg: "invalid waf config, apLogConf references non-existing log conf", }, { - wafInput: &conf_v1.WAF{ Enable: true, ApPolicy: "default/dataguard-alarm", @@ -8346,7 +8370,7 @@ func TestAddWafConfig(t *testing.T) { wafConfig: &version2.WAF{ ApPolicy: "/etc/nginx/waf/nac-policies/default-dataguard-alarm", ApSecurityLogEnable: true, - ApLogConf: "/etc/nginx/waf/nac-logconfs/default-logconf", + ApLogConf: []string{"/etc/nginx/waf/nac-logconfs/default-logconf"}, }, expected: &validationResults{ isError: true, @@ -8357,7 +8381,6 @@ func TestAddWafConfig(t *testing.T) { msg: "invalid waf config, apLogConf references non-existing ap conf", }, { - wafInput: &conf_v1.WAF{ Enable: true, ApPolicy: "ns1/dataguard-alarm", @@ -8380,13 +8403,12 @@ func TestAddWafConfig(t *testing.T) { wafConfig: &version2.WAF{ ApPolicy: "/etc/nginx/waf/nac-policies/ns1-dataguard-alarm", ApSecurityLogEnable: true, - ApLogConf: "/etc/nginx/waf/nac-logconfs/ns2-logconf", + ApLogConf: []string{"/etc/nginx/waf/nac-logconfs/ns2-logconf"}, }, expected: &validationResults{}, msg: "valid waf config, cross ns reference", }, { - wafInput: &conf_v1.WAF{ Enable: false, ApPolicy: "dataguard-alarm", diff --git a/internal/k8s/configuration.go b/internal/k8s/configuration.go index af73f855a1..7c7893207d 100644 --- a/internal/k8s/configuration.go +++ b/internal/k8s/configuration.go @@ -1190,7 +1190,8 @@ func createResourceChangesForHosts(removedHosts []string, updatedHosts []string, } func createResourceChangesForListeners(removedListeners []string, updatedListeners []string, addedListeners []string, oldListeners map[string]*TransportServerConfiguration, - newListeners map[string]*TransportServerConfiguration) []ResourceChange { + newListeners map[string]*TransportServerConfiguration, +) []ResourceChange { var changes []ResourceChange var deleteChanges []ResourceChange @@ -1597,7 +1598,8 @@ func detectChangesInHosts(oldHosts map[string]Resource, newHosts map[string]Reso } func detectChangesInListeners(oldListeners map[string]*TransportServerConfiguration, newListeners map[string]*TransportServerConfiguration) (removedListeners []string, - updatedListeners []string, addedListeners []string) { + updatedListeners []string, addedListeners []string, +) { for _, l := range getSortedTransportServerConfigurationKeys(oldListeners) { _, exists := newListeners[l] if !exists { diff --git a/internal/k8s/controller.go b/internal/k8s/controller.go index 2c79db23d5..74cc9109a6 100644 --- a/internal/k8s/controller.go +++ b/internal/k8s/controller.go @@ -2802,19 +2802,37 @@ func (lbc *LoadBalancerController) addWAFPolicyRefs( apPolRef[apPolKey] = apPolicy } - if pol.Spec.WAF.SecurityLog != nil && pol.Spec.WAF.SecurityLog.ApLogConf != "" { - logConfKey := pol.Spec.WAF.SecurityLog.ApLogConf - if !strings.Contains(pol.Spec.WAF.SecurityLog.ApLogConf, "/") { - logConfKey = fmt.Sprintf("%v/%v", pol.Namespace, logConfKey) - } + if pol.Spec.WAF.SecurityLog != nil && pol.Spec.WAF.SecurityLogs == nil { + if pol.Spec.WAF.SecurityLog.ApLogConf != "" { + logConfKey := pol.Spec.WAF.SecurityLog.ApLogConf + if !strings.Contains(pol.Spec.WAF.SecurityLog.ApLogConf, "/") { + logConfKey = fmt.Sprintf("%v/%v", pol.Namespace, logConfKey) + } - logConf, err := lbc.appProtectConfiguration.GetAppResource(appprotect.LogConfGVK.Kind, logConfKey) - if err != nil { - return fmt.Errorf("WAF policy %q is invalid: %w", logConfKey, err) + logConf, err := lbc.appProtectConfiguration.GetAppResource(appprotect.LogConfGVK.Kind, logConfKey) + if err != nil { + return fmt.Errorf("WAF policy %q is invalid: %w", logConfKey, err) + } + logConfRef[logConfKey] = logConf } - logConfRef[logConfKey] = logConf } + if pol.Spec.WAF.SecurityLogs != nil { + for _, SecLog := range pol.Spec.WAF.SecurityLogs { + if SecLog.ApLogConf != "" { + logConfKey := SecLog.ApLogConf + if !strings.Contains(SecLog.ApLogConf, "/") { + logConfKey = fmt.Sprintf("%v/%v", pol.Namespace, logConfKey) + } + + logConf, err := lbc.appProtectConfiguration.GetAppResource(appprotect.LogConfGVK.Kind, logConfKey) + if err != nil { + return fmt.Errorf("WAF policy %q is invalid: %w", logConfKey, err) + } + logConfRef[logConfKey] = logConf + } + } + } } return nil } @@ -2862,6 +2880,13 @@ func getWAFPoliciesForAppProtectLogConf(pols []*conf_v1.Policy, key string) []*c if pol.Spec.WAF != nil && pol.Spec.WAF.SecurityLog != nil && isMatchingResourceRef(pol.Namespace, pol.Spec.WAF.SecurityLog.ApLogConf, key) { policies = append(policies, pol) } + if pol.Spec.WAF != nil && pol.Spec.WAF.SecurityLogs != nil { + for _, logConf := range pol.Spec.WAF.SecurityLogs { + if isMatchingResourceRef(pol.Namespace, logConf.ApLogConf, key) { + policies = append(policies, pol) + } + } + } } return policies diff --git a/internal/k8s/controller_test.go b/internal/k8s/controller_test.go index 363de456d7..0531caa256 100644 --- a/internal/k8s/controller_test.go +++ b/internal/k8s/controller_test.go @@ -1709,6 +1709,15 @@ func TestAddWAFPolicyRefs(t *testing.T) { }, } + additionalLogConf := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "namespace": "default", + "name": "additional-log-conf", + }, + }, + } + tests := []struct { policies []*conf_v1.Policy expectedApPolRefs map[string]*unstructured.Unstructured @@ -1790,6 +1799,102 @@ func TestAddWAFPolicyRefs(t *testing.T) { expectedLogConfRefs: make(map[string]*unstructured.Unstructured), msg: "logConf doesn't exist", }, + { + policies: []*conf_v1.Policy{ + { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "waf-pol", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + WAF: &conf_v1.WAF{ + Enable: true, + ApPolicy: "ap-pol", + SecurityLogs: []*conf_v1.SecurityLog{ + { + Enable: true, + ApLogConf: "log-conf", + }, + }, + }, + }, + }, + }, + wantErr: false, + expectedApPolRefs: map[string]*unstructured.Unstructured{ + "default/ap-pol": apPol, + }, + expectedLogConfRefs: map[string]*unstructured.Unstructured{ + "default/log-conf": logConf, + }, + }, + { + policies: []*conf_v1.Policy{ + { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "waf-pol", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + WAF: &conf_v1.WAF{ + Enable: true, + ApPolicy: "ap-pol", + SecurityLogs: []*conf_v1.SecurityLog{ + { + Enable: true, + ApLogConf: "log-conf", + }, + { + Enable: true, + ApLogConf: "additional-log-conf", + }, + }, + }, + }, + }, + }, + wantErr: false, + expectedApPolRefs: map[string]*unstructured.Unstructured{ + "default/ap-pol": apPol, + }, + expectedLogConfRefs: map[string]*unstructured.Unstructured{ + "default/log-conf": logConf, + "default/additional-log-conf": additionalLogConf, + }, + }, + { + policies: []*conf_v1.Policy{ + { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "waf-pol", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + WAF: &conf_v1.WAF{ + Enable: true, + ApPolicy: "ap-pol", + SecurityLog: &conf_v1.SecurityLog{ + Enable: true, + ApLogConf: "additional-log-conf", + }, + SecurityLogs: []*conf_v1.SecurityLog{ + { + Enable: true, + ApLogConf: "log-conf", + }, + }, + }, + }, + }, + }, + wantErr: false, + expectedApPolRefs: map[string]*unstructured.Unstructured{ + "default/ap-pol": apPol, + }, + expectedLogConfRefs: map[string]*unstructured.Unstructured{ + "default/log-conf": logConf, + }, + }, } lbc := LoadBalancerController{ @@ -1797,6 +1902,7 @@ func TestAddWAFPolicyRefs(t *testing.T) { } lbc.appProtectConfiguration.AddOrUpdatePolicy(apPol) lbc.appProtectConfiguration.AddOrUpdateLogConf(logConf) + lbc.appProtectConfiguration.AddOrUpdateLogConf(additionalLogConf) for _, test := range tests { resApPolicy := make(map[string]*unstructured.Unstructured) @@ -1904,6 +2010,20 @@ func TestGetWAFPoliciesForAppProtectLogConf(t *testing.T) { }, } + logConfs := &conf_v1.Policy{ + Spec: conf_v1.PolicySpec{ + WAF: &conf_v1.WAF{ + Enable: true, + SecurityLogs: []*conf_v1.SecurityLog{ + { + Enable: true, + ApLogConf: "ns1/logConfs", + }, + }, + }, + }, + } + logConfNs2 := &conf_v1.Policy{ ObjectMeta: meta_v1.ObjectMeta{ Namespace: "ns1", @@ -1935,7 +2055,7 @@ func TestGetWAFPoliciesForAppProtectLogConf(t *testing.T) { } policies := []*conf_v1.Policy{ - logConf, logConfNs2, logConfNoNs, + logConf, logConfs, logConfNs2, logConfNoNs, } tests := []struct { @@ -1956,6 +2076,12 @@ func TestGetWAFPoliciesForAppProtectLogConf(t *testing.T) { want: []*conf_v1.Policy{logConfNoNs}, msg: "WAF pols that ref logConf which has no namespace", }, + { + pols: policies, + key: "ns1/logConfs", + want: []*conf_v1.Policy{logConfs}, + msg: "WAF pols that ref logConf via logConfs field", + }, { pols: policies, key: "ns2/logConf", diff --git a/pkg/apis/configuration/v1/types.go b/pkg/apis/configuration/v1/types.go index 26137e808e..b6380e9276 100644 --- a/pkg/apis/configuration/v1/types.go +++ b/pkg/apis/configuration/v1/types.go @@ -445,9 +445,10 @@ type OIDC struct { // WAF defines an WAF policy. type WAF struct { - Enable bool `json:"enable"` - ApPolicy string `json:"apPolicy"` - SecurityLog *SecurityLog `json:"securityLog"` + Enable bool `json:"enable"` + ApPolicy string `json:"apPolicy"` + SecurityLog *SecurityLog `json:"securityLog"` + SecurityLogs []*SecurityLog `json:"securityLogs"` } // SecurityLog defines the security log of a WAF policy. diff --git a/pkg/apis/configuration/v1/zz_generated.deepcopy.go b/pkg/apis/configuration/v1/zz_generated.deepcopy.go index 5629523c61..42c0df639d 100644 --- a/pkg/apis/configuration/v1/zz_generated.deepcopy.go +++ b/pkg/apis/configuration/v1/zz_generated.deepcopy.go @@ -1166,6 +1166,17 @@ func (in *WAF) DeepCopyInto(out *WAF) { *out = new(SecurityLog) **out = **in } + if in.SecurityLogs != nil { + in, out := &in.SecurityLogs, &out.SecurityLogs + *out = make([]*SecurityLog, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(SecurityLog) + **out = **in + } + } + } return } diff --git a/pkg/apis/configuration/validation/virtualserver.go b/pkg/apis/configuration/validation/virtualserver.go index 3d059ae1da..3abf927180 100644 --- a/pkg/apis/configuration/validation/virtualserver.go +++ b/pkg/apis/configuration/validation/virtualserver.go @@ -1490,7 +1490,8 @@ func (vsv *VirtualServerValidator) ValidateVirtualServerRouteForVirtualServer(vi } func (vsv *VirtualServerValidator) validateVirtualServerRouteSpec(spec *v1.VirtualServerRouteSpec, fieldPath *field.Path, virtualServerHost string, vsPath string, - namespace string) field.ErrorList { + namespace string, +) field.ErrorList { allErrs := field.ErrorList{} allErrs = append(allErrs, validateVirtualServerRouteHost(spec.Host, virtualServerHost, fieldPath.Child("host"))...) diff --git a/tests/data/ap-waf/syslog-1.yaml b/tests/data/ap-waf/syslog-1.yaml new file mode 100644 index 0000000000..3bfe416def --- /dev/null +++ b/tests/data/ap-waf/syslog-1.yaml @@ -0,0 +1,40 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: syslog-1 +spec: + replicas: 1 + selector: + matchLabels: + app: syslog-1 + template: + metadata: + labels: + app: syslog-1 + spec: + containers: + - name: syslog + image: balabit/syslog-ng:3.35.1 + ports: + - containerPort: 514 + - containerPort: 601 + volumeMounts: + - name: config-volume + mountPath: /etc/syslog-ng/syslog-ng.conf + subPath: syslog-ng.conf + volumes: + - name: config-volume + configMap: + name: syslog-config +--- +apiVersion: v1 +kind: Service +metadata: + name: syslog-svc-1 +spec: + ports: + - port: 514 + targetPort: 514 + protocol: TCP + selector: + app: syslog-1 diff --git a/tests/suite/ap_resources_utils.py b/tests/suite/ap_resources_utils.py index cc6198fd96..721ca4cf90 100644 --- a/tests/suite/ap_resources_utils.py +++ b/tests/suite/ap_resources_utils.py @@ -30,8 +30,6 @@ def read_ap_custom_resource(custom_objects: CustomObjectsApi, namespace, plural, logging.exception(f"Exception occurred while reading CRD") raise - - def create_ap_waf_policy_from_yaml( custom_objects: CustomObjectsApi, yaml_manifest, @@ -75,6 +73,55 @@ def create_ap_waf_policy_from_yaml( logging.exception(f"Exception occurred while creating Policy: {dep['metadata']['name']}") raise +def create_ap_multilog_waf_policy_from_yaml( + custom_objects: CustomObjectsApi, + yaml_manifest, + namespace, + ap_namespace, + waf_enable, + log_enable, + appolicy, + aplogconfs, + logdests, +) -> str: + """ + Create a Policy based on yaml file. + + :param custom_objects: CustomObjectsApi + :param yaml_manifest: an absolute path to file + :param namespace: namespace for test resources + :param ap_namespace: namespace for AppProtect resources + :param waf_enable: true/false + :param log_enable: true/false + :param appolicy: AppProtect policy name + :param aplogconfs: List of Logconf names + :param logdests: List of AP log destinations (syslog) + :return: str + """ + with open(yaml_manifest) as f: + dep = yaml.safe_load(f) + try: + dep["spec"]["waf"]["enable"] = waf_enable + dep["spec"]["waf"]["apPolicy"] = f"{ap_namespace}/{appolicy}" + seclogs = [] + try: + for i in range(len(aplogconfs)): + seclogs.append({"enable": True,"apLogConf": f"{ap_namespace}/{aplogconfs[i]}", "logDest": f"{logdests[i]}"}) + dep["spec"]["waf"]["securityLogs"] = seclogs + except KeyError: + logging.exception(f"Exception occurred while creating Policy: {dep['metadata']['name']}") + raise + del dep["spec"]["waf"]["securityLog"] + + custom_objects.create_namespaced_custom_object( + "k8s.nginx.org", "v1", namespace, "policies", dep + ) + print(f"Policy created: {dep}") + return dep["metadata"]["name"] + except ApiException: + logging.exception(f"Exception occurred while creating Policy: {dep['metadata']['name']}") + raise + def create_ap_logconf_from_yaml(custom_objects: CustomObjectsApi, yaml_manifest, namespace) -> str: """ Create a logconf for AppProtect based on yaml file. diff --git a/tests/suite/test_app_protect_waf_policies.py b/tests/suite/test_app_protect_waf_policies.py index 1a47e79a79..03916169aa 100644 --- a/tests/suite/test_app_protect_waf_policies.py +++ b/tests/suite/test_app_protect_waf_policies.py @@ -3,6 +3,7 @@ from settings import TEST_DATA, DEPLOYMENTS from suite.resources_utils import ( + delete_items_from_yaml, wait_before_test, create_items_from_yaml, wait_before_test, @@ -36,6 +37,7 @@ delete_ap_policy, delete_ap_logconf, create_ap_waf_policy_from_yaml, + create_ap_multilog_waf_policy_from_yaml, ) from suite.yaml_utils import get_first_ingress_host_from_yaml, get_name_from_yaml @@ -374,7 +376,7 @@ def test_ap_waf_policy_logs( delete_policy(kube_apis.custom_objects, "waf-policy", test_namespace) self.restore_default_vs(kube_apis, virtual_server_setup) - + delete_items_from_yaml(kube_apis, src_syslog_yaml, test_namespace) assert_invalid_responses(response) assert ( f'ASM:attack_type="Non-browser Client,Abuse of Functionality,Cross Site Scripting (XSS)"' @@ -384,6 +386,81 @@ def test_ap_waf_policy_logs( assert f'request_status="blocked"' in log_contents assert f'outcome="REJECTED"' in log_contents + def test_ap_waf_policy_multi_logs( + self, + kube_apis, + crd_ingress_controller_with_ap, + virtual_server_setup, + appprotect_setup, + test_namespace, + ): + """ + Test waf policy logs + """ + src_syslog_yaml = f"{TEST_DATA}/ap-waf/syslog.yaml" + src_syslog_yaml_additional = f"{TEST_DATA}/ap-waf/syslog-1.yaml" + log_loc = f"/var/log/messages" + create_items_from_yaml(kube_apis, src_syslog_yaml, test_namespace) + create_items_from_yaml(kube_apis, src_syslog_yaml_additional, test_namespace) + syslog_dst1 = f"syslog-svc.{test_namespace}" + syslog_dst2 = f"syslog-svc-1.{test_namespace}" + syslog_pods = kube_apis.v1.list_namespaced_pod(test_namespace, label_selector="app in (syslog, syslog-1)").items + print(f"Create waf policy") + create_ap_multilog_waf_policy_from_yaml( + kube_apis.custom_objects, + waf_pol_dataguard_src, + test_namespace, + test_namespace, + True, + True, + ap_pol_name, + [log_name, log_name], + [f"syslog:server={syslog_dst1}:514",f"syslog:server={syslog_dst2}:514"] + ) + wait_before_test() + print(f"Patch vs with policy: {waf_spec_vs_src}") + patch_virtual_server_from_yaml( + kube_apis.custom_objects, + virtual_server_setup.vs_name, + waf_spec_vs_src, + virtual_server_setup.namespace, + ) + wait_before_test() + ap_crd_info = read_ap_custom_resource( + kube_apis.custom_objects, test_namespace, "appolicies", ap_policy_uds + ) + assert_ap_crd_info(ap_crd_info, ap_policy_uds) + wait_before_test(120) + + print( + "----------------------- Send request with embedded malicious script----------------------" + ) + response = requests.get( + virtual_server_setup.backend_1_url + "", + headers={"host": virtual_server_setup.vs_host}, + ) + print(response.text) + log_contents = ["",""] + retry = 0 + for i in range(2): + while "ASM:attack_type" not in log_contents[i] and retry <= 30: + log_contents[i] = get_file_contents( + kube_apis.v1, log_loc, syslog_pods[i].metadata.name, test_namespace + ) + retry += 1 + wait_before_test(1) + print(f"Security log not updated, retrying... #{retry}") + + delete_policy(kube_apis.custom_objects, "waf-policy", test_namespace) + self.restore_default_vs(kube_apis, virtual_server_setup) + + assert_invalid_responses(response) + + for log_cont in log_contents: + assert f'ASM:attack_type="Non-browser Client,Abuse of Functionality,Cross Site Scripting (XSS)"' in log_cont + assert f'severity="Critical"' in log_cont + assert f'request_status="blocked"' in log_cont + assert f'outcome="REJECTED"' in log_cont @pytest.mark.skip_for_nginx_oss @pytest.mark.appprotect