diff --git a/Makefile b/Makefile index 3c1e5f05c9..0510d5ce40 100644 --- a/Makefile +++ b/Makefile @@ -45,7 +45,7 @@ update-codegen: ## Generate code .PHONY: update-crds update-crds: ## Update CRDs - go run sigs.k8s.io/controller-tools/cmd/controller-gen crd:crdVersions=v1 schemapatch:manifests=./deployments/common/crds/ paths=./pkg/apis/configuration/... output:dir=./deployments/common/crds + go run sigs.k8s.io/controller-tools/cmd/controller-gen crd:crdVersions=v1 schemapatch:manifests=./deployments/common/crds/ paths=./pkg/apis/.../... output:dir=./deployments/common/crds @cp -Rp deployments/common/crds/ deployments/helm-chart/crds .PHONY: certificate-and-key @@ -97,6 +97,14 @@ debian-image-plus: build ## Create Docker image for Ingress Controller (Debian w debian-image-nap-plus: build ## Create Docker image for Ingress Controller (Debian with NGINX Plus and App Protect WAF) $(DOCKER_CMD) $(PLUS_ARGS) --build-arg BUILD_OS=debian-plus-nap --build-arg DEBIAN_VERSION=buster-slim +.PHONY: debian-image-dos-plus +debian-image-dos-plus: build ## Create Docker image for Ingress Controller (nginx plus with nap-dos) + $(DOCKER_CMD) $(PLUS_ARGS) --build-arg BUILD_OS=debian-plus-dos + +.PHONY: debian-image-nap-dos-plus +debian-image-nap-dos-plus: build ## Create Docker image for Ingress Controller (nginx plus with nap and nap-dos) + $(DOCKER_CMD) $(PLUS_ARGS) --build-arg BUILD_OS=debian-plus-nap-dos --build-arg FILES=nap-common + .PHONY: openshift-image openshift-image: build ## Create Docker image for Ingress Controller (UBI) $(DOCKER_CMD) --build-arg BUILD_OS=ubi @@ -113,6 +121,14 @@ openshift-image-nap-plus: build ## Create Docker image for Ingress Controller (U alpine-image-opentracing: build ## Create Docker image for Ingress Controller (Alpine with OpenTracing) $(DOCKER_CMD) --build-arg BUILD_OS=alpine-opentracing +.PHONY: openshift-image-dos-plus +openshift-image-dos-plus: build ## Create Docker image for Ingress Controller (ubi with plus and dos) + $(DOCKER_CMD) $(PLUS_ARGS) $(NAP_ARGS) --secret id=rhel_license,src=rhel_license --build-arg BUILD_OS=ubi-plus-dos --build-arg UBI_VERSION=7 + +.PHONY: openshift-image-nap-dos-plus +openshift-image-nap-dos-plus: build ## Create Docker image for Ingress Controller (ubi with plus, nap and dos) + $(DOCKER_CMD) $(PLUS_ARGS) $(NAP_ARGS) --secret id=rhel_license,src=rhel_license --build-arg BUILD_OS=ubi-plus-nap-dos --build-arg UBI_VERSION=7 + .PHONY: debian-image-opentracing debian-image-opentracing: build ## Create Docker image for Ingress Controller (Debian with OpenTracing) $(DOCKER_CMD) --build-arg BUILD_OS=opentracing @@ -122,7 +138,7 @@ debian-image-opentracing-plus: build ## Create Docker image for Ingress Controll $(DOCKER_CMD) $(PLUS_ARGS) --build-arg BUILD_OS=opentracing-plus .PHONY: all-images ## Create all the Docker images for Ingress Controller -all-images: alpine-image alpine-image-plus debian-image debian-image-plus debian-image-nap-plus debian-image-opentracing debian-image-opentracing-plus openshift-image openshift-image-plus openshift-image-nap-plus +all-images: alpine-image alpine-image-plus debian-image debian-image-plus debian-image-nap-plus debian-image-dos-plus debian-image-nap-dos-plus debian-image-opentracing debian-image-opentracing-plus openshift-image openshift-image-plus openshift-image-nap-plus .PHONY: push push: ## Docker push to PREFIX and TAG diff --git a/build/Dockerfile b/build/Dockerfile index 6fc6a680f9..402ce3ef49 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -81,6 +81,37 @@ RUN --mount=type=secret,id=nginx-repo.crt,dst=/etc/ssl/nginx/nginx-repo.crt,mode # COPY build/*.crt /usr/local/share/ca-certificates/ # RUN update-ca-certificates +############################################# Base image for Debian with NGINX Plus and App Protect Dos ############################################# +FROM debian-plus as debian-plus-dos +ARG IC_VERSION +ARG NGINX_PLUS_VERSION + +RUN --mount=type=secret,id=nginx-repo.crt,dst=/etc/ssl/nginx/nginx-repo.crt,mode=0644 \ + --mount=type=secret,id=nginx-repo.key,dst=/etc/ssl/nginx/nginx-repo.key,mode=0644 \ + set -x \ + && apt-get update \ + && apt-get -y install lsb-release ca-certificates \ + && printf "deb https://pkgs.nginx.com/app-protect-dos/${NGINX_PLUS_VERSION^^}/debian `lsb_release -cs` nginx-plus\n" | tee /etc/apt/sources.list.d/nginx-app-protect-dos.list \ + && apt-get update \ + && apt-get -y install app-protect-dos \ + && rm -rf /var/lib/apt/lists/* \ + && rm /etc/apt/sources.list.d/nginx-app-protect-dos.list + +############################################# Base image for Debian with NGINX, App Protect and App Protect Dos ############################################# +FROM debian-plus-nap as debian-plus-nap-dos +ARG IC_VERSION +ARG NGINX_PLUS_VERSION + +RUN --mount=type=secret,id=nginx-repo.crt,dst=/etc/ssl/nginx/nginx-repo.crt,mode=0644 \ + --mount=type=secret,id=nginx-repo.key,dst=/etc/ssl/nginx/nginx-repo.key,mode=0644 \ + set -x \ + && apt-get update \ + && apt-get -y install lsb-release ca-certificates \ + && printf "deb https://pkgs.nginx.com/app-protect-dos/debian `lsb_release -cs` nginx-plus\n" | tee /etc/apt/sources.list.d/nginx-app-protect-dos.list \ + && apt-get update \ + && apt-get -y install app-protect-dos \ + && rm -rf /var/lib/apt/lists/* \ + && rm /etc/apt/sources.list.d/nginx-app-protect-dos.list ############################################# Base image for UBI 8 ############################################# FROM redhat/ubi8-minimal AS ubi-base-8 @@ -162,6 +193,41 @@ RUN --mount=type=secret,id=nginx-repo.crt,dst=/etc/ssl/nginx/nginx-repo.crt,mode # COPY build/*.crt /etc/pki/ca-trust/source/anchors/ # RUN update-ca-trust extract +############################################# Base image for UBI with NGINX Plus and App Protect Dos ############################################# +FROM ubi-plus as ubi-plus-dos +ARG NGINX_PLUS_VERSION + +RUN --mount=type=secret,id=nginx-repo.crt,dst=/etc/ssl/nginx/nginx-repo.crt,mode=0644 \ + --mount=type=secret,id=nginx-repo.key,dst=/etc/ssl/nginx/nginx-repo.key,mode=0644 \ + --mount=type=secret,id=rhel_license,dst=/tmp/rhel_license,mode=0644 \ + source /tmp/rhel_license \ + && subscription-manager register --org=${RHEL_ORGANIZATION} --activationkey=${RHEL_ACTIVATION_KEY} || true \ + && subscription-manager attach \ + && curl -sS https://cs.nginx.com/static/files/app-protect-dos-7.repo > /etc/yum.repos.d/app-protect-dos-7.repo \ + && subscription-manager repos --enable rhel-7-server-optional-rpms --enable rhel-7-server-extras-rpms \ + && rpm -ivh https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm \ + && yum clean all \ + && yum -y install epel-release \ + && yum -y install app-protect-dos-${NGINX_PLUS_VERSION#r}* \ + && rm /etc/yum.repos.d/app-protect-dos-7.repo \ + && subscription-manager unregister + +############################################# Base image for UBI with NGINX Plus and App Protect and App Protect Dos ############################################# +FROM ubi-plus-nap as ubi-plus-nap-dos +ARG NGINX_PLUS_VERSION + +RUN --mount=type=secret,id=nginx-repo.crt,dst=/etc/ssl/nginx/nginx-repo.crt,mode=0644 \ + --mount=type=secret,id=nginx-repo.key,dst=/etc/ssl/nginx/nginx-repo.key,mode=0644 \ + --mount=type=secret,id=rhel_license,dst=/tmp/rhel_license,mode=0644 \ + source /tmp/rhel_license \ + && subscription-manager register --org=${RHEL_ORGANIZATION} --activationkey=${RHEL_ACTIVATION_KEY} || true \ + && subscription-manager attach \ + && curl -sS https://cs.nginx.com/static/files/app-protect-dos-7.repo > /etc/yum.repos.d/app-protect-dos-7.repo \ + && yum clean all \ + && yum -y install epel-release \ + && yum -y install app-protect-dos-${NGINX_PLUS_VERSION#r}* \ + && rm /etc/yum.repos.d/app-protect-dos-7.repo \ + && subscription-manager unregister ############################################# Base images containing libs for Opentracing ############################################# FROM opentracing/nginx-opentracing:nginx-1.21.4 as opentracing-lib @@ -225,6 +291,9 @@ RUN --mount=target=/tmp [ -n "${BUILD_OS##*nap*}" ] && exit 0; mkdir -p /etc/ngi ; done \ && cp -a /tmp/build/log-default.json /etc/nginx +# run only on dos build +RUN --mount=target=/tmp [ -n "${BUILD_OS##*dos*}" ] && exit 0; mkdir -p /root/app_protect_dos /etc/nginx/dos/policies /etc/nginx/dos/logconfs /shared/cores /var/log/adm /var/run/adm && chmod 777 /shared/cores /var/log/adm /var/run/adm /etc/app_protect_dos + RUN --mount=target=/tmp mkdir -p /var/lib/nginx /etc/nginx/secrets /etc/nginx/stream-conf.d \ && setcap 'cap_net_bind_service=+ep' /usr/sbin/nginx 'cap_net_bind_service=+ep' /usr/sbin/nginx-debug \ && setcap -v 'cap_net_bind_service=+ep' /usr/sbin/nginx 'cap_net_bind_service=+ep' /usr/sbin/nginx-debug \ diff --git a/cmd/nginx-ingress/main.go b/cmd/nginx-ingress/main.go index 52ae587239..8ae028f9e4 100644 --- a/cmd/nginx-ingress/main.go +++ b/cmd/nginx-ingress/main.go @@ -70,6 +70,14 @@ var ( appProtect = flag.Bool("enable-app-protect", false, "Enable support for NGINX App Protect. Requires -nginx-plus.") + appProtectDos = flag.Bool("enable-app-protect-dos", false, "Enable support for NGINX App Protect dos. Requires -nginx-plus.") + + appProtectDosDebug = flag.Bool("app-protect-dos-debug", false, "Enable debugging for App Protect Dos. Requires -nginx-plus and -enable-app-protect-dos.") + + appProtectDosMaxDaemons = flag.Int("app-protect-dos-max-daemons", 0, "Max number of ADMD instances. Requires -nginx-plus and -enable-app-protect-dos.") + appProtectDosMaxWorkers = flag.Int("app-protect-dos-max-workers", 0, "Max number of nginx processes to support. Requires -nginx-plus and -enable-app-protect-dos.") + appProtectDosMemory = flag.Int("app-protect-dos-memory", 0, "RAM memory size to consume in MB. Requires -nginx-plus and -enable-app-protect-dos.") + ingressClass = flag.String("ingress-class", "nginx", `A class of the Ingress controller. @@ -239,6 +247,26 @@ func main() { glog.Fatal("NGINX App Protect support is for NGINX Plus only") } + if *appProtectDos && !*nginxPlus { + glog.Fatal("NGINX App Protect Dos support is for NGINX Plus only") + } + + if *appProtectDosDebug && !*appProtectDos && !*nginxPlus { + glog.Fatal("NGINX App Protect Dos debug support is for NGINX Plus only and App Protect Dos is enable") + } + + if *appProtectDosMaxDaemons != 0 && !*appProtectDos && !*nginxPlus { + glog.Fatal("NGINX App Protect Dos max daemons support is for NGINX Plus only and App Protect Dos is enable") + } + + if *appProtectDosMaxWorkers != 0 && !*appProtectDos && !*nginxPlus { + glog.Fatal("NGINX App Protect Dos max workers support is for NGINX Plus and App Protect Dos is enable") + } + + if *appProtectDosMemory != 0 && !*appProtectDos && !*nginxPlus { + glog.Fatal("NGINX App Protect Dos memory support is for NGINX Plus and App Protect Dos is enable") + } + if *spireAgentAddress != "" && !*nginxPlus { glog.Fatal("spire-agent-address support is for NGINX Plus only") } @@ -303,7 +331,7 @@ func main() { } var dynClient dynamic.Interface - if *appProtect || *ingressLink != "" { + if *appProtectDos || *appProtect || *ingressLink != "" { dynClient, err = dynamic.NewForConfig(config) if err != nil { glog.Fatalf("Failed to create dynamic client: %v.", err) @@ -423,6 +451,13 @@ func main() { nginxManager.AppProtectPluginStart(aPPluginDone) } + var aPPDosAgentDone chan error + + if *appProtectDos { + aPPDosAgentDone = make(chan error, 1) + nginxManager.AppProtectDosAgentStart(aPPDosAgentDone, *appProtectDosDebug, *appProtectDosMaxDaemons, *appProtectDosMaxWorkers, *appProtectDosMemory) + } + var sslRejectHandshake bool if *defaultServerSecret != "" { @@ -487,7 +522,7 @@ func main() { if err != nil { glog.Fatalf("Error when getting %v: %v", *nginxConfigMaps, err) } - cfgParams = configs.ParseConfigMap(cfm, *nginxPlus, *appProtect) + cfgParams = configs.ParseConfigMap(cfm, *nginxPlus, *appProtect, *appProtectDos) if cfgParams.MainServerSSLDHParamFileContent != nil { fileName, err := nginxManager.CreateDHParam(*cfgParams.MainServerSSLDHParamFileContent) if err != nil { @@ -520,6 +555,7 @@ func main() { EnableSnippets: *enableSnippets, NginxServiceMesh: *spireAgentAddress != "", MainAppProtectLoadModule: *appProtect, + MainAppProtectDosLoadModule: *appProtectDos, EnableLatencyMetrics: *enableLatencyMetrics, EnablePreviewPolicies: *enablePreviewPolicies, SSLRejectHandshake: sslRejectHandshake, @@ -616,6 +652,7 @@ func main() { NginxConfigurator: cnf, DefaultServerSecret: *defaultServerSecret, AppProtectEnabled: *appProtect, + AppProtectDosEnabled: *appProtectDos, IsNginxPlus: *nginxPlus, IngressClass: *ingressClass, ExternalServiceName: *externalService, @@ -652,8 +689,8 @@ func main() { }() } - if *appProtect { - go handleTerminationWithAppProtect(lbc, nginxManager, syslogListener, nginxDone, aPAgentDone, aPPluginDone) + if *appProtect || *appProtectDos { + go handleTerminationWithAppProtect(lbc, nginxManager, syslogListener, nginxDone, aPAgentDone, aPPluginDone, aPPDosAgentDone, *appProtect, *appProtectDos) } else { go handleTermination(lbc, nginxManager, syslogListener, nginxDone) } @@ -811,7 +848,7 @@ func validateLocation(location string) error { return nil } -func handleTerminationWithAppProtect(lbc *k8s.LoadBalancerController, nginxManager nginx.Manager, listener metrics.SyslogListener, nginxDone, agentDone, pluginDone chan error) { +func handleTerminationWithAppProtect(lbc *k8s.LoadBalancerController, nginxManager nginx.Manager, listener metrics.SyslogListener, nginxDone, agentDone, pluginDone, agentDosDone chan error, appProtectEnabled, appProtectDosEnabled bool) { signalChan := make(chan os.Signal, 1) signal.Notify(signalChan, syscall.SIGTERM) @@ -822,15 +859,23 @@ func handleTerminationWithAppProtect(lbc *k8s.LoadBalancerController, nginxManag glog.Fatalf("AppProtectPlugin command exited unexpectedly with status: %v", err) case err := <-agentDone: glog.Fatalf("AppProtectAgent command exited unexpectedly with status: %v", err) + case err := <-agentDosDone: + glog.Fatalf("AppProtectDosAgent command exited unexpectedly with status: %v", err) case <-signalChan: glog.Infof("Received SIGTERM, shutting down") lbc.Stop() nginxManager.Quit() <-nginxDone - nginxManager.AppProtectPluginQuit() - <-pluginDone - nginxManager.AppProtectAgentQuit() - <-agentDone + if appProtectEnabled { + nginxManager.AppProtectPluginQuit() + <-pluginDone + nginxManager.AppProtectAgentQuit() + <-agentDone + } + if appProtectDosEnabled { + nginxManager.AppProtectDosAgentQuit() + <-agentDosDone + } listener.Stop() } glog.Info("Exiting successfully") diff --git a/deployments/common/crds/appprotectdos.f5.com_apdoslogconfs.yaml b/deployments/common/crds/appprotectdos.f5.com_apdoslogconfs.yaml new file mode 100644 index 0000000000..d41efc5347 --- /dev/null +++ b/deployments/common/crds/appprotectdos.f5.com_apdoslogconfs.yaml @@ -0,0 +1,70 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.0 + creationTimestamp: null + name: apdoslogconfs.appprotectdos.f5.com +spec: + group: appprotectdos.f5.com + names: + kind: APDosLogConf + listKind: APDosLogConfList + plural: apdoslogconfs + singular: apdoslogconf + preserveUnknownFields: false + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: APDosLogConf is the Schema for the APDosLogConfs API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: APDosLogConfSpec defines the desired state of APDosLogConf + properties: + content: + properties: + format: + enum: + - splunk + - arcsight + - user-defined + default: splunk + type: string + format_string: + type: string + max_message_size: + pattern: ^([1-9]|[1-5][0-9]|6[0-4])k$ + default: 5k + type: string + type: object + filter: + properties: + traffic-mitigation-stats: + enum: + - none + - all + default: all + type: string + bad-actors: + pattern: ^(none|all|top ([1-9]|[1-9][0-9]|[1-9][0-9]{2,4}|100000))$ + default: top 10 + type: string + attack-signatures: + pattern: ^(none|all|top ([1-9]|[1-9][0-9]|[1-9][0-9]{2,4}|100000))$ + default: top 10 + type: string + type: object + type: object + type: object + served: true + storage: true diff --git a/deployments/common/crds/appprotectdos.f5.com_apdospolicy.yaml b/deployments/common/crds/appprotectdos.f5.com_apdospolicy.yaml new file mode 100644 index 0000000000..d9609d30c7 --- /dev/null +++ b/deployments/common/crds/appprotectdos.f5.com_apdospolicy.yaml @@ -0,0 +1,66 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + creationTimestamp: null + name: apdospolicies.appprotectdos.f5.com +spec: + group: appprotectdos.f5.com + names: + kind: APDosPolicy + listKind: APDosPoliciesList + plural: apdospolicies + singular: apdospolicy + preserveUnknownFields: false + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + type: object + description: APDosPolicy is the Schema for the APDosPolicy API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + type: object + description: APDosPolicySpec defines the desired state of APDosPolicy + properties: + mitigation_mode: + enum: + - "standard" + - "conservative" + - "none" + default: "standard" + type: string + signatures: + enum: + - "on" + - "off" + default: "on" + type: string + bad_actors: + enum: + - "on" + - "off" + default: "on" + type: string + automation_tools_detection: + enum: + - "on" + - "off" + default: "on" + type: string + tls_fingerprint: + enum: + - "on" + - "off" + default: "on" + type: string + served: true + storage: true diff --git a/deployments/common/crds/appprotectdos.f5.com_dosprotectedresources.yaml b/deployments/common/crds/appprotectdos.f5.com_dosprotectedresources.yaml new file mode 100644 index 0000000000..a985ddaa8e --- /dev/null +++ b/deployments/common/crds/appprotectdos.f5.com_dosprotectedresources.yaml @@ -0,0 +1,86 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + name: dosprotectedresources.appprotectdos.f5.com +spec: + group: appprotectdos.f5.com + names: + kind: DosProtectedResource + listKind: DosProtectedResourceList + plural: dosprotectedresources + shortNames: + - pr + singular: dosprotectedresource + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: DosProtectedResource defines a Dos protected resource. + type: object + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + type: object + properties: + apDosMonitor: + description: 'ApDosMonitor is how NGINX App Protect DoS monitors the stress level of the protected object. The monitor requests are sent from localhost (127.0.0.1). Default value: URI - None, protocol - http1, timeout - NGINX App Protect DoS default.' + type: object + properties: + protocol: + description: Protocol determines if the server listens on http1 / http2 / grpc. The default is http1. + type: string + enum: + - http1 + - http2 + - grpc + timeout: + description: Timeout determines how long (in seconds) should NGINX App Protect DoS wait for a response. Default is 10 seconds for http1/http2 and 5 seconds for grpc. + type: integer + format: int64 + uri: + description: 'Uri is the destination to the desired protected object in the nginx.conf:' + type: string + apDosPolicy: + description: ApDosPolicy is the namespace/name of a ApDosPolicy resource + type: string + dosAccessLogDest: + description: DosAccessLogDest is the network address for the access logs + type: string + dosSecurityLog: + description: DosSecurityLog defines the security log of the DosProtectedResource. + type: object + properties: + apDosLogConf: + description: ApDosLogConf is the namespace/name of a APDosLogConf resource + type: string + dosLogDest: + description: DosLogDest is the network address of a logging service, can be either IP or DNS name. + type: string + enable: + description: Enable enables the security logging feature if set to true + type: boolean + enable: + description: Enable enables the DOS feature if set to true + type: boolean + name: + description: Name is the name of protected object, max of 63 characters. + type: string + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/deployments/common/crds/k8s.nginx.org_virtualserverroutes.yaml b/deployments/common/crds/k8s.nginx.org_virtualserverroutes.yaml index 7a9b0fcc6d..b8645f5582 100644 --- a/deployments/common/crds/k8s.nginx.org_virtualserverroutes.yaml +++ b/deployments/common/crds/k8s.nginx.org_virtualserverroutes.yaml @@ -137,6 +137,8 @@ spec: type: integer type: type: string + dos: + type: string errorPages: type: array items: diff --git a/deployments/common/crds/k8s.nginx.org_virtualservers.yaml b/deployments/common/crds/k8s.nginx.org_virtualservers.yaml index e9759cf553..ad7cefb4fb 100644 --- a/deployments/common/crds/k8s.nginx.org_virtualservers.yaml +++ b/deployments/common/crds/k8s.nginx.org_virtualservers.yaml @@ -51,6 +51,8 @@ spec: description: VirtualServerSpec is the spec of the VirtualServer resource. type: object properties: + dos: + type: string host: type: string http-snippets: @@ -149,6 +151,8 @@ spec: type: integer type: type: string + dos: + type: string errorPages: type: array items: diff --git a/deployments/daemon-set/nginx-plus-ingress.yaml b/deployments/daemon-set/nginx-plus-ingress.yaml index a029bcbf46..eb3695d900 100644 --- a/deployments/daemon-set/nginx-plus-ingress.yaml +++ b/deployments/daemon-set/nginx-plus-ingress.yaml @@ -59,6 +59,7 @@ spec: - -nginx-configmaps=$(POD_NAMESPACE)/nginx-config - -default-server-tls-secret=$(POD_NAMESPACE)/default-server-secret #- -enable-app-protect + #- -enable-app-protect-dos #- -v=3 # Enables extensive logging. Useful for troubleshooting. #- -report-ingress-status #- -external-service=nginx-ingress diff --git a/deployments/deployment/appprotect-dos-arb.yaml b/deployments/deployment/appprotect-dos-arb.yaml new file mode 100644 index 0000000000..ebd5775156 --- /dev/null +++ b/deployments/deployment/appprotect-dos-arb.yaml @@ -0,0 +1,31 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: appprotect-dos-arb + namespace: nginx-ingress +spec: + replicas: 1 + selector: + matchLabels: + app: appprotect-dos-arb + template: + metadata: + labels: + app: appprotect-dos-arb + spec: + containers: + - name: appprotect-dos-arb + image: docker-registry.nginx.com/nap-dos/app_protect_dos_arb:1.1.0 + imagePullPolicy: IfNotPresent + resources: + limits: + memory: "128Mi" + cpu: "500m" + ports: + - containerPort: 3000 + securityContext: + allowPrivilegeEscalation: false + runAsUser: 1001 + capabilities: + drop: + - ALL diff --git a/deployments/deployment/nginx-plus-ingress.yaml b/deployments/deployment/nginx-plus-ingress.yaml index 09553e3845..0ac019649e 100644 --- a/deployments/deployment/nginx-plus-ingress.yaml +++ b/deployments/deployment/nginx-plus-ingress.yaml @@ -58,8 +58,9 @@ spec: - -nginx-configmaps=$(POD_NAMESPACE)/nginx-config - -default-server-tls-secret=$(POD_NAMESPACE)/default-server-secret #- -enable-app-protect + #- -enable-app-protect-dos #- -v=3 # Enables extensive logging. Useful for troubleshooting. #- -report-ingress-status #- -external-service=nginx-ingress #- -enable-prometheus-metrics - #- -global-configuration=$(POD_NAMESPACE)/nginx-configuration + #- -global-configuration=$(POD_NAMESPACE)/nginx-configuration \ No newline at end of file diff --git a/deployments/helm-chart-dos-arbitrator/.helmignore b/deployments/helm-chart-dos-arbitrator/.helmignore new file mode 100644 index 0000000000..c1347c2c27 --- /dev/null +++ b/deployments/helm-chart-dos-arbitrator/.helmignore @@ -0,0 +1,2 @@ +# Patterns to ignore when building packages. +*.png diff --git a/deployments/helm-chart-dos-arbitrator/Chart.yaml b/deployments/helm-chart-dos-arbitrator/Chart.yaml new file mode 100644 index 0000000000..53ebe2e67d --- /dev/null +++ b/deployments/helm-chart-dos-arbitrator/Chart.yaml @@ -0,0 +1,17 @@ +name: nginx-appprotect-dos-arbitrator +version: 0.0.1 +appVersion: 1.0.0 +apiVersion: v1 +kubeVersion: ">= 1.19.0-0" +description: NGINX App Protect Dos arbitrator +icon: https://raw.githubusercontent.com/nginxinc/kubernetes-ingress/v1.12.1/deployments/helm-chart-dos-arbitrator/chart-icon.png +home: https://github.com/nginxinc/kubernetes-ingress +sources: + - https://github.com/nginxinc/kubernetes-ingress/tree/v1.12.1/deployments/helm-chart-dos-arbitrator +keywords: + - appprotect-dos + - nginx + - arbitrator +maintainers: + - name: nginxinc + email: kubernetes@nginx.com diff --git a/deployments/helm-chart-dos-arbitrator/README.md b/deployments/helm-chart-dos-arbitrator/README.md new file mode 100644 index 0000000000..8eded96932 --- /dev/null +++ b/deployments/helm-chart-dos-arbitrator/README.md @@ -0,0 +1,96 @@ +# NGINX App Protect Dos Arbitrator Helm Chart + +## Introduction + +This chart deploys the NGINX App Protect Dos Arbitrator in your Kubernetes cluster. + +## Prerequisites + + - A [Kubernetes Version Supported by the Ingress Controller](https://docs.nginx.com/nginx-ingress-controller/technical-specifications/#supported-kubernetes-versions) + - Helm 3.0+. + - Git. + +## Getting the Chart Sources + +This step is required if you're installing the chart using its sources. Additionally, the step is also required for managing the custom resource definitions (CRDs), which the Ingress Controller requires by default, or for upgrading/deleting the CRDs. + +1. Clone the Ingress controller repo: + ```console + $ git clone https://github.com/nginxinc/kubernetes-ingress/ + ``` +2. Change your working directory to /deployments/helm-chart-dos-arbitrator: + ```console + $ cd kubernetes-ingress/deployments/helm-chart-dos-arbitrator + $ git checkout v1.12.1 + ``` + +## Adding the Helm Repository + +This step is required if you're installing the chart via the helm repository. + +```console +$ helm repo add nginx-stable https://helm.nginx.com/stable +$ helm repo update +``` + +## Installing the Chart + +### Installing via Helm Repository + +To install the chart with the release name my-release-dos (my-release-dos is the name that you choose): + +```console +$ helm install my-release-dos nginx-stable/nginx-appprotect-dos-arbitrator +``` + + +### Installing Using Chart Sources + +To install the chart with the release name my-release-dos (my-release-dos is the name that you choose): + +```console +$ helm install my-release-dos . +``` + +The command deploys the App Protect Dos Arbitrator in your Kubernetes cluster in the default configuration. The configuration section lists the parameters that can be configured during installation. + +## Upgrading the Chart + +### Upgrading the Release + +To upgrade the release `my-release-dos`: + +#### Upgrade Using Chart Sources: + +```console +$ helm upgrade my-release-dos . +``` + +#### Upgrade via Helm Repository: + +```console +$ helm upgrade my-release-dos nginx-stable/nginx-appprotect-dos-arbitrator +``` + +## Uninstalling the Chart + +### Uninstalling the Release + +To uninstall/delete the release `my-release-dos`: + +```console +$ helm uninstall my-release-dos +``` + +The command removes all the Kubernetes components associated with the release and deletes the release. + +## Configuration + +The following tables lists the configurable parameters of the NGINX App Protect Dos Arbitrator chart and their default values. + +Parameter | Description | Default +--- | --- | --- +`arbitrator.resources` | The resources of the Arbitrator pods. | limits:
cpu: 500m
memory: 128Mi +`appprotectdos.arbitrator.image.repository` | The image repository of the Arbitrator image. | docker-registry.nginx.com/nap-dos/app_protect_dos_arb +`arbitrator.image.tag` | The tag of the Arbitrator image. | 1.1.0 +`arbitrator.image.pullPolicy` | The pull policy for the Arbitrator image. | IfNotPresent diff --git a/deployments/helm-chart-dos-arbitrator/chart-icon.png b/deployments/helm-chart-dos-arbitrator/chart-icon.png new file mode 100644 index 0000000000..52961c9a6f Binary files /dev/null and b/deployments/helm-chart-dos-arbitrator/chart-icon.png differ diff --git a/deployments/helm-chart-dos-arbitrator/templates/_helpers.tpl b/deployments/helm-chart-dos-arbitrator/templates/_helpers.tpl new file mode 100644 index 0000000000..029b5434ce --- /dev/null +++ b/deployments/helm-chart-dos-arbitrator/templates/_helpers.tpl @@ -0,0 +1,18 @@ +{{/* vim: set filetype=mustache: */}} + +{{/* +Expand the name of the chart. +*/}} +{{- define "arbitrator.name" -}} +{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create labels +*/}} +{{- define "arbitrator.labels" -}} +app.kubernetes.io/name: {{ include "arbitrator.name" . }} +helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} \ No newline at end of file diff --git a/deployments/helm-chart-dos-arbitrator/templates/controller-deployment.yaml b/deployments/helm-chart-dos-arbitrator/templates/controller-deployment.yaml new file mode 100644 index 0000000000..66d448a4ba --- /dev/null +++ b/deployments/helm-chart-dos-arbitrator/templates/controller-deployment.yaml @@ -0,0 +1,30 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "arbitrator.name" . }} + namespace: {{ .Release.Namespace }} + labels: {{- include "arbitrator.labels" . | nindent 4 }} +spec: + replicas: 1 + selector: + matchLabels: + app: {{ include "arbitrator.name" . }} + template: + metadata: + labels: + app: {{ include "arbitrator.name" . }} + spec: + containers: + - name: {{ include "arbitrator.name" . }} + image: "{{ .Values.arbitrator.image.repository }}:{{ .Values.arbitrator.image.tag }}" + imagePullPolicy: "{{ .Values.arbitrator.image.pullPolicy }}" + resources: +{{ toYaml .Values.arbitrator.resources | indent 12 }} + ports: + - containerPort: 3000 + securityContext: + allowPrivilegeEscalation: false + runAsUser: 1001 + capabilities: + drop: + - ALL diff --git a/deployments/helm-chart-dos-arbitrator/templates/controller-service.yaml b/deployments/helm-chart-dos-arbitrator/templates/controller-service.yaml new file mode 100644 index 0000000000..f55a9fd8d2 --- /dev/null +++ b/deployments/helm-chart-dos-arbitrator/templates/controller-service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: svc-appprotect-dos-arb + namespace: {{ .Release.Namespace }} + labels: {{- include "arbitrator.labels" . | nindent 4 }} +spec: + selector: + app: {{ include "arbitrator.name" . }} + ports: + - name: arb + port: 3000 + protocol: TCP + targetPort: 3000 + clusterIP: None diff --git a/deployments/helm-chart-dos-arbitrator/values.yaml b/deployments/helm-chart-dos-arbitrator/values.yaml new file mode 100644 index 0000000000..1c42666205 --- /dev/null +++ b/deployments/helm-chart-dos-arbitrator/values.yaml @@ -0,0 +1,16 @@ +arbitrator: + ## The resources of the Arbitrator pods. + resources: + limits: + cpu: 500m + memory: 128Mi + + image: + ## The image repository of the Arbitrator. + repository: docker-registry.nginx.com/nap-dos/app_protect_dos_arb + + ## The tag of the Arbitrator image. + tag: "1.1.0" + + ## The pull policy for the Arbitrator image. + pullPolicy: IfNotPresent diff --git a/deployments/helm-chart/README.md b/deployments/helm-chart/README.md index aa41b80c41..29136e2af7 100644 --- a/deployments/helm-chart/README.md +++ b/deployments/helm-chart/README.md @@ -14,6 +14,8 @@ This chart deploys the NGINX Ingress controller in your Kubernetes cluster. - Alternatively, pull an Ingress controller image with NGINX Plus and push it to your private registry by following the instructions from [here](https://docs.nginx.com/nginx-ingress-controller/installation/pulling-ingress-controller-image). - Alternatively, you can build an Ingress controller image with NGINX Plus and push it to your private registry by following the instructions from [here](https://docs.nginx.com/nginx-ingress-controller/installation/building-ingress-controller-image). - Update the `controller.image.repository` field of the `values-plus.yaml` accordingly. + - If you’d like to use App Protect Dos, please install App Protect Dos Arbitrator helm chart + ## Getting the Chart Sources @@ -44,7 +46,7 @@ $ helm repo update By default, the Ingress Controller requires a number of custom resource definitions (CRDs) installed in the cluster. The Helm client will install those CRDs. If the CRDs are not installed, the Ingress Controller pods will not become `Ready`. -If you do not use the custom resources that require those CRDs (which corresponds to `controller.enableCustomResources` set to `false` and `controller.appprotect.enable` set to `false`), the installation of the CRDs can be skipped by specifying `--skip-crds` for the helm install command. +If you do not use the custom resources that require those CRDs (which corresponds to `controller.enableCustomResources` set to `false` and `controller.appprotect.enable` set to `false` and `controller.appprotectdos.enable` set to `false`), the installation of the CRDs can be skipped by specifying `--skip-crds` for the helm install command. ### Installing via Helm Repository @@ -59,6 +61,10 @@ For NGINX Plus: (assuming you have pushed the Ingress controller image `nginx-pl ```console $ helm install my-release nginx-stable/nginx-ingress --set controller.image.repository=myregistry.example.com/nginx-plus-ingress --set controller.nginxplus=true ``` +For App Protect Dos: (assuming you have pushed the Ingress controller image `nginx-plus-ingress` to your private registry `myregistry.example.com` +```console +$ helm install --create-namespace -n nginx-ingress my-release nginx-stable/nginx-ingress --set controller.image.repository=myregistry.example.com/nginx-plus-ingress --set controller.nginxplus=true --set controller.appprotectdos.enable=true +``` **Note**: If you wish to use the experimental repository, replace `stable` with `edge` and add the `--devel` flag. @@ -76,6 +82,14 @@ For NGINX Plus: $ helm install my-release -f values-plus.yaml . ``` +For App Protect Dos: + +replace the value in the `appprotectdos.enable` field inside the values.yaml file with `true` + +```console +$ helm install --create-namespace -n nginx-ingress my-release -f values-plus.yaml . +``` + **Note**: If you wish to use the experimental repository, replace the value in the `tag` field inside the yaml files with `edge`. The command deploys the Ingress controller in your Kubernetes cluster in the default configuration. The configuration section lists the parameters that can be configured during installation. @@ -105,12 +119,22 @@ To upgrade the release `my-release`: $ helm upgrade my-release . ``` +For App Protect Dos: +```console +$ helm upgrade -n nginx-ingress my-release . +``` + #### Upgrade via Helm Repository: ```console $ helm upgrade my-release nginx-stable/nginx-ingress ``` +For App Protect Dos: +```console +$ helm upgrade -n nginx-ingress my-release nginx-stable/nginx-ingress +``` + ## Uninstalling the Chart ### Uninstalling the Release @@ -121,6 +145,12 @@ To uninstall/delete the release `my-release`: $ helm uninstall my-release ``` +For App Protect Dos: +```console +$ helm uninstall -n nginx-ingress my-release +$ kubectl delete ns nginx-ingress +``` + The command removes all the Kubernetes components associated with the release and deletes the release. ### Uninstalling the CRDs @@ -217,6 +247,11 @@ Parameter | Description | Default `controller.pod.annotations` | The annotations of the Ingress Controller pod. | {} `controller.pod.extraLabels` | The additional extra labels of the Ingress Controller pod. | {} `controller.appprotect.enable` | Enables the App Protect module in the Ingress Controller. | false +`controller.appprotectdos.enable` | Enables the App Protect Dos module in the Ingress Controller. | false +`controller.appprotectdos.debug` | Enable debugging for App Protect Dos. | false +`controller.appprotectdos.maxDaemons` | Max number of ADMD instances. | 1 +`controller.appprotectdos.maxWorkers` | Max number of nginx processes to support. | Number of CPU cores in the machine +`controller.appprotectdos.memory` | RAM memory size to consume in MB. | 50% of free RAM in the container or 80MB, the smaller `controller.readyStatus.enable` | Enables the readiness endpoint `"/nginx-ready"`. The endpoint returns a success code when NGINX has loaded all the config after the startup. This also configures a readiness probe for the Ingress Controller pods that uses the readiness endpoint. | true `controller.readyStatus.port` | The HTTP port for the readiness endpoint. | 8081 `controller.enableLatencyMetrics` | Enable collection of latency metrics for upstreams. Requires `prometheus.create`. | false diff --git a/deployments/helm-chart/crds/appprotectdos.f5.com_apdoslogconfs.yaml b/deployments/helm-chart/crds/appprotectdos.f5.com_apdoslogconfs.yaml new file mode 100644 index 0000000000..d41efc5347 --- /dev/null +++ b/deployments/helm-chart/crds/appprotectdos.f5.com_apdoslogconfs.yaml @@ -0,0 +1,70 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.0 + creationTimestamp: null + name: apdoslogconfs.appprotectdos.f5.com +spec: + group: appprotectdos.f5.com + names: + kind: APDosLogConf + listKind: APDosLogConfList + plural: apdoslogconfs + singular: apdoslogconf + preserveUnknownFields: false + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: APDosLogConf is the Schema for the APDosLogConfs API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: APDosLogConfSpec defines the desired state of APDosLogConf + properties: + content: + properties: + format: + enum: + - splunk + - arcsight + - user-defined + default: splunk + type: string + format_string: + type: string + max_message_size: + pattern: ^([1-9]|[1-5][0-9]|6[0-4])k$ + default: 5k + type: string + type: object + filter: + properties: + traffic-mitigation-stats: + enum: + - none + - all + default: all + type: string + bad-actors: + pattern: ^(none|all|top ([1-9]|[1-9][0-9]|[1-9][0-9]{2,4}|100000))$ + default: top 10 + type: string + attack-signatures: + pattern: ^(none|all|top ([1-9]|[1-9][0-9]|[1-9][0-9]{2,4}|100000))$ + default: top 10 + type: string + type: object + type: object + type: object + served: true + storage: true diff --git a/deployments/helm-chart/crds/appprotectdos.f5.com_apdospolicy.yaml b/deployments/helm-chart/crds/appprotectdos.f5.com_apdospolicy.yaml new file mode 100644 index 0000000000..d9609d30c7 --- /dev/null +++ b/deployments/helm-chart/crds/appprotectdos.f5.com_apdospolicy.yaml @@ -0,0 +1,66 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + creationTimestamp: null + name: apdospolicies.appprotectdos.f5.com +spec: + group: appprotectdos.f5.com + names: + kind: APDosPolicy + listKind: APDosPoliciesList + plural: apdospolicies + singular: apdospolicy + preserveUnknownFields: false + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + type: object + description: APDosPolicy is the Schema for the APDosPolicy API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + type: object + description: APDosPolicySpec defines the desired state of APDosPolicy + properties: + mitigation_mode: + enum: + - "standard" + - "conservative" + - "none" + default: "standard" + type: string + signatures: + enum: + - "on" + - "off" + default: "on" + type: string + bad_actors: + enum: + - "on" + - "off" + default: "on" + type: string + automation_tools_detection: + enum: + - "on" + - "off" + default: "on" + type: string + tls_fingerprint: + enum: + - "on" + - "off" + default: "on" + type: string + served: true + storage: true diff --git a/deployments/helm-chart/crds/appprotectdos.f5.com_dosprotectedresources.yaml b/deployments/helm-chart/crds/appprotectdos.f5.com_dosprotectedresources.yaml new file mode 100644 index 0000000000..a985ddaa8e --- /dev/null +++ b/deployments/helm-chart/crds/appprotectdos.f5.com_dosprotectedresources.yaml @@ -0,0 +1,86 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + name: dosprotectedresources.appprotectdos.f5.com +spec: + group: appprotectdos.f5.com + names: + kind: DosProtectedResource + listKind: DosProtectedResourceList + plural: dosprotectedresources + shortNames: + - pr + singular: dosprotectedresource + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: DosProtectedResource defines a Dos protected resource. + type: object + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + type: object + properties: + apDosMonitor: + description: 'ApDosMonitor is how NGINX App Protect DoS monitors the stress level of the protected object. The monitor requests are sent from localhost (127.0.0.1). Default value: URI - None, protocol - http1, timeout - NGINX App Protect DoS default.' + type: object + properties: + protocol: + description: Protocol determines if the server listens on http1 / http2 / grpc. The default is http1. + type: string + enum: + - http1 + - http2 + - grpc + timeout: + description: Timeout determines how long (in seconds) should NGINX App Protect DoS wait for a response. Default is 10 seconds for http1/http2 and 5 seconds for grpc. + type: integer + format: int64 + uri: + description: 'Uri is the destination to the desired protected object in the nginx.conf:' + type: string + apDosPolicy: + description: ApDosPolicy is the namespace/name of a ApDosPolicy resource + type: string + dosAccessLogDest: + description: DosAccessLogDest is the network address for the access logs + type: string + dosSecurityLog: + description: DosSecurityLog defines the security log of the DosProtectedResource. + type: object + properties: + apDosLogConf: + description: ApDosLogConf is the namespace/name of a APDosLogConf resource + type: string + dosLogDest: + description: DosLogDest is the network address of a logging service, can be either IP or DNS name. + type: string + enable: + description: Enable enables the security logging feature if set to true + type: boolean + enable: + description: Enable enables the DOS feature if set to true + type: boolean + name: + description: Name is the name of protected object, max of 63 characters. + type: string + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/deployments/helm-chart/crds/k8s.nginx.org_virtualserverroutes.yaml b/deployments/helm-chart/crds/k8s.nginx.org_virtualserverroutes.yaml index 7a9b0fcc6d..b8645f5582 100644 --- a/deployments/helm-chart/crds/k8s.nginx.org_virtualserverroutes.yaml +++ b/deployments/helm-chart/crds/k8s.nginx.org_virtualserverroutes.yaml @@ -137,6 +137,8 @@ spec: type: integer type: type: string + dos: + type: string errorPages: type: array items: diff --git a/deployments/helm-chart/crds/k8s.nginx.org_virtualservers.yaml b/deployments/helm-chart/crds/k8s.nginx.org_virtualservers.yaml index e9759cf553..ad7cefb4fb 100644 --- a/deployments/helm-chart/crds/k8s.nginx.org_virtualservers.yaml +++ b/deployments/helm-chart/crds/k8s.nginx.org_virtualservers.yaml @@ -51,6 +51,8 @@ spec: description: VirtualServerSpec is the spec of the VirtualServer resource. type: object properties: + dos: + type: string host: type: string http-snippets: @@ -149,6 +151,8 @@ spec: type: integer type: type: string + dos: + type: string errorPages: type: array items: diff --git a/deployments/helm-chart/templates/controller-daemonset.yaml b/deployments/helm-chart/templates/controller-daemonset.yaml index 36c6119a7b..09ff1b800c 100644 --- a/deployments/helm-chart/templates/controller-daemonset.yaml +++ b/deployments/helm-chart/templates/controller-daemonset.yaml @@ -105,6 +105,7 @@ spec: - -nginx-plus={{ .Values.controller.nginxplus }} - -nginx-reload-timeout={{ .Values.controller.nginxReloadTimeout }} - -enable-app-protect={{ .Values.controller.appprotect.enable }} + - -enable-app-protect-dos={{ .Values.controller.appprotectdos.enable }} - -nginx-configmaps=$(POD_NAMESPACE)/{{ include "nginx-ingress.configName" . }} {{- if .Values.controller.defaultTLS.secret }} - -default-server-tls-secret={{ .Values.controller.defaultTLS.secret }} diff --git a/deployments/helm-chart/templates/controller-deployment.yaml b/deployments/helm-chart/templates/controller-deployment.yaml index 682ffa8cf8..40e14c622b 100644 --- a/deployments/helm-chart/templates/controller-deployment.yaml +++ b/deployments/helm-chart/templates/controller-deployment.yaml @@ -103,6 +103,13 @@ spec: - -nginx-plus={{ .Values.controller.nginxplus }} - -nginx-reload-timeout={{ .Values.controller.nginxReloadTimeout }} - -enable-app-protect={{ .Values.controller.appprotect.enable }} + - -enable-app-protect-dos={{ .Values.controller.appprotectdos.enable }} +{{- if .Values.controller.appprotectdos.enable }} + - -app-protect-dos-debug={{ .Values.controller.appprotectdos.debug }} + - -app-protect-dos-max-daemons={{ .Values.controller.appprotectdos.maxWorkers }} + - -app-protect-dos-max-workers={{ .Values.controller.appprotectdos.maxDaemons }} + - -app-protect-dos-memory={{ .Values.controller.appprotectdos.memory }} +{{ end }} - -nginx-configmaps=$(POD_NAMESPACE)/{{ include "nginx-ingress.configName" . }} {{- if .Values.controller.defaultTLS.secret }} - -default-server-tls-secret={{ .Values.controller.defaultTLS.secret }} diff --git a/deployments/helm-chart/templates/rbac.yaml b/deployments/helm-chart/templates/rbac.yaml index 7538b4830d..dc68b06998 100644 --- a/deployments/helm-chart/templates/rbac.yaml +++ b/deployments/helm-chart/templates/rbac.yaml @@ -18,6 +18,18 @@ rules: - watch - list {{- end }} +{{- if .Values.controller.appprotectdos.enable }} +- apiGroups: + - appprotectdos.f5.com + resources: + - apdospolicies + - apdoslogconfs + - dosprotectedresources + verbs: + - get + - watch + - list +{{- end }} - apiGroups: - "" resources: diff --git a/deployments/helm-chart/values.yaml b/deployments/helm-chart/values.yaml index ba1ec96e5e..62871a960c 100644 --- a/deployments/helm-chart/values.yaml +++ b/deployments/helm-chart/values.yaml @@ -17,6 +17,15 @@ controller: ## Enable the App Protect module in the Ingress Controller. enable: false + ## Support for App Protect Dos + appprotectdos: + ## Enable the App Protect Dos module in the Ingress Controller. + enable: false + debug: false + maxWorkers: 0 + maxDaemons: 0 + memory: 0 + ## Enables the Ingress controller pods to use the host's network namespace. hostNetwork: false diff --git a/deployments/rbac/apdos-rbac.yaml b/deployments/rbac/apdos-rbac.yaml new file mode 100644 index 0000000000..9c23452d72 --- /dev/null +++ b/deployments/rbac/apdos-rbac.yaml @@ -0,0 +1,28 @@ +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: nginx-ingress-app-protect-dos +rules: + - apiGroups: + - appprotectdos.f5.com + resources: + - apdospolicies + - apdoslogconfs + - dosprotectedresources + verbs: + - "get" + - "watch" + - "list" +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: nginx-ingress-app-protect-dos +subjects: + - kind: ServiceAccount + name: nginx-ingress + namespace: nginx-ingress +roleRef: + kind: ClusterRole + name: nginx-ingress-app-protect-dos + apiGroup: rbac.authorization.k8s.io diff --git a/deployments/service/appprotect-dos-arb-svc.yaml b/deployments/service/appprotect-dos-arb-svc.yaml new file mode 100644 index 0000000000..b7632351fd --- /dev/null +++ b/deployments/service/appprotect-dos-arb-svc.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: svc-appprotect-dos-arb + namespace: nginx-ingress +spec: + selector: + app: appprotect-dos-arb + ports: + - name: arb + port: 3000 + protocol: TCP + targetPort: 3000 + clusterIP: None \ No newline at end of file diff --git a/docs/content/app-protect-dos/_index.md b/docs/content/app-protect-dos/_index.md new file mode 100644 index 0000000000..b6f0eb537d --- /dev/null +++ b/docs/content/app-protect-dos/_index.md @@ -0,0 +1,8 @@ +--- +title: Using with NGINX App Protect Dos +description: Learn how to use NGINX Ingress Controller for Kubernetes with NGINX App Protect Dos. +weight: 1600 +menu: + docs: + parent: NGINX Ingress Controller +--- diff --git a/docs/content/app-protect-dos/configuration.md b/docs/content/app-protect-dos/configuration.md new file mode 100644 index 0000000000..62d857594a --- /dev/null +++ b/docs/content/app-protect-dos/configuration.md @@ -0,0 +1,158 @@ +--- +title: Configuration + +description: +weight: 1900 +doctypes: [""] +toc: true +--- + +This document describes how to configure the NGINX App Protect Dos module +> Check out the complete [NGINX Ingress Controller with App Protect Dos example resources on GitHub](https://github.com/nginxinc/kubernetes-ingress/tree/v1.12.0/examples/appprotect-dos). + +## Global Configuration + +The NGINX Ingress Controller has a set of global configuration parameters that align with those available in the NGINX App Protect Dos module. See [ConfigMap keys](/nginx-ingress-controller/configuration/global-configuration/configmap-resource/#modules) for the complete list. The App Protect parameters use the `app-protect-dos*` prefix. + +## Enable App Protect Dos for Ingress + +You can enable and configure NGINX App Protect Dos on a per-Ingress-resource basis. To do so, you can apply the [App Protect Dos annotation](/nginx-ingress-controller/configuration/ingress-resources/advanced-configuration-with-annotations/#app-protect-dos) to each desired resource. + +## App Protect Dos Protected Resources + +An `DosProtectedResource` is a [Custom Resource](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) that holds the configuration of a collection of protected resources. +An Ingress or VirtualServer can be protected by adding a reference to the Dos Protected Resource. + +To enable DOS protection to an Ingress: + +1. Create an `DosProtectedResource` Custom resource manifest. As an example: + ```yaml + apiVersion: appprotectdos.f5.com/v1beta1 + kind: DosProtectedResource + metadata: + name: dos-protected + spec: + enable: true + name: "my-dos" + apDosMonitor: + uri: "webapp.example.com" + ``` +2. Add an annotation to an Ingress that refers to that resource by `namespace/name`: + ```yaml + apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: webapp-ingress + annotations: + appprotectdos.f5.com/app-protect-dos-resource: "default/dos-protected" + ``` +## Dos Policy configuration + +You can set the App Protect Dos Policy configurations by creating an `APDosPolicy` [Custom Resource](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) and referencing that in the `DosProtectedResource`. + + +For example, say you want to use Dos Policy as shown below: + + ```json + { + mitigation_mode: "standard", + signatures: "on", + bad_actors: "on", + automation_tools_detection: "on", + tls_fingerprint: "on", +} + ``` + +You would create an `APDosPolicy` resource with the policy defined in the `spec`, as shown below: + + ```yaml + apiVersion: appprotectdos.f5.com/v1beta1 + kind: APDosPolicy + metadata: + name: dospolicy + spec: + mitigation_mode: "standard" + signatures: "on" + bad_actors: "on" + automation_tools_detection: "on" + tls_fingerprint: "on" + ``` + +Then add a reference in the `DosProtectedResrouce` to the `ApDosPolicy`: + ```yaml + apiVersion: appprotectdos.f5.com/v1beta1 + kind: DosProtectedResource + metadata: + name: dos-protected + spec: + enable: true + name: "my-dos" + apDosMonitor: + uri: "webapp.example.com" + apDosPolicy: "default/dospolicy" + ``` + +> Notice how the fields match exactly in name and level. The Ingress Controller will transform the YAML into a valid JSON App Protect Dos policy config. + +> **Note**: The relationship between the Policy JSON and the resource spec is 1:1. If you're defining your resources in YAML, as we do in our examples, you'll need to represent the policy as YAML. The fields must match those in the source JSON exactly in name and level. + + +## App Protect Dos Logs + +You can set the [App Protect Dos Log configurations](/nginx-app-protect-dos/logs-overview/types-of-logs/) by creating an `APDosLogConf` [Custom Resource](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/). + +To add the App Protect Dos log configurations to an Ingress resource: + +1. Create an `APDosLogConf` Custom resource manifest. +2. Add the desired log configuration to the `spec` field in the `APDosLogConf` resource. + + > **Note**: The fields from the JSON must be presented in the YAML *exactly* the same, in name and level. The Ingress Controller will transform the YAML into a valid JSON App Protect Dos log config. + +For example, say you want to log state changing requests for your Ingress resources using App Protect Dos. The App Protect Dos log configuration looks like this: + +```json +{ + "filter": { + "request_type": "all" + }, + "content": { + "format": "default", + "max_request_size": "any", + "max_message_size": "64k" + } +} +``` + +You would add define that config in the `spec` of your `APDosLogConf` resource as follows: + +```yaml +apiVersion: appprotectdos.f5.com/v1beta1 +kind: APDosLogConf +metadata: + name: doslogconf +spec: + content: + format: splunk + max_message_size: 64k + filter: + traffic-mitigation-stats: all + bad-actors: top 10 + attack-signatures: top 10 +``` + +Then add a reference in the `DosProtectedResrouce` to the `APDosLogConf`: + ```yaml + apiVersion: appprotectdos.f5.com/v1beta1 + kind: DosProtectedResource + metadata: + name: dos-protected + spec: + enable: true + name: "my-dos" + apDosMonitor: + uri: "webapp.example.com" + dosSecurityLog: + enable: true + apDosLogConf: "doslogconf" + dosLogDest: "syslog-svc.default.svc.cluster.local:514" + ``` \ No newline at end of file diff --git a/docs/content/app-protect-dos/installation.md b/docs/content/app-protect-dos/installation.md new file mode 100644 index 0000000000..e9f339e9b1 --- /dev/null +++ b/docs/content/app-protect-dos/installation.md @@ -0,0 +1,47 @@ +--- +title: Installation with NGINX App Protect Dos +description: +weight: 1800 +doctypes: [""] +toc: true +--- + +> **Note**: The NGINX Kubernetes Ingress Controller integration with NGINX App Protect requires the use of NGINX Plus. + +This document provides an overview of the steps required to use NGINX App Protect Dos with your NGINX Ingress Controller deployment. You can visit the linked documents to find additional information and instructions. + +## Install the app-protect-dos-arb + +- Deploy the app protect dos arbitrator + ```bash + kubectl apply -f deployment/appprotect-dos-arb.yaml + kubectl apply -f service/appprotect-dos-arb-svc.yaml + ``` + +## Build the Docker Image + +Take the steps below to create the Docker image that you'll use to deploy NGINX Ingress Controller with App Protect Dos in Kubernetes. + +- [Build the NGINX Ingress Controller image](/nginx-ingress-controller/installation/building-ingress-controller-image). + + When running the `make` command to build the image, be sure to use the `debian-image-dos-plus` target. For example: + + ```bash + make debian-image-dos-plus PREFIX=/nginx-plus-ingress + ``` + +- [Push the image to your local Docker registry](/nginx-ingress-controller/installation/building-ingress-controller-image/#building-the-image-and-pushing-it-to-the-private-registry). + +## Install the Ingress Controller + +Take the steps below to set up and deploy the NGINX Ingress Controller and App Protect Dos module in your Kubernetes cluster. + +1. [Configure role-based access control (RBAC)](/nginx-ingress-controller/installation/installation-with-manifests/#configure-rbac). + + > **Important**: You must have an admin role to configure RBAC in your Kubernetes cluster. + +2. [Create the common Kubernetes resources](/nginx-ingress-controller/installation/installation-with-manifests/#create-common-resources). +3. Enable the App Protect Dos module by adding the `enable-app-protect-dos` [cli argument](/nginx-ingress-controller/configuration/global-configuration/command-line-arguments/#cmdoption-enable-app-protect-dos) to your Deployment or DaemonSet file. +5. [Deploy the Ingress Controller](/nginx-ingress-controller/installation/installation-with-manifests/#deploy-the-ingress-controller). + +For more information, see the [Configuration guide](/nginx-ingress-controller/app-protect-dos/configuration) and the [NGINX Ingress Controller with App Protect Dos examples on GitHub](https://github.com/nginxinc/kubernetes-ingress/tree/v1.12.0/examples/appprotect-dos). diff --git a/docs/content/configuration/dos-protected.md b/docs/content/configuration/dos-protected.md new file mode 100644 index 0000000000..8c3869d6d9 --- /dev/null +++ b/docs/content/configuration/dos-protected.md @@ -0,0 +1,129 @@ +--- +title: Dos Protected Resource + +description: +weight: 1800 +doctypes: [""] +toc: true +--- + + +The DosProtectedResource allows you to specify App Protect Dos configuration as a Kubernetes resource that can then be referenced by your [Ingress](/nginx-ingress-controller/configuration/ingress-resources/basic-configuration) and [VirtualServer and VirtualServerRoute](/nginx-ingress-controller/configuration/virtualserver-and-virtualserverroute-resources/) resources. + +The resource is implemented as a [Custom Resource](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/). + +> **Feature Status**: DOS is available as a preview feature: it is suitable for experimenting and testing; however, it must be used with caution in production environments. Additionally, while the feature is in preview status, we might introduce some backward-incompatible changes to the resource specification in the next releases. The feature is disabled by default. To enable it, set the [enable-preview-policies](/nginx-ingress-controller/configuration/global-configuration/command-line-arguments/#cmdoption-enable-preview-policies) command-line argument of the Ingress Controller. + +> Note: This feature is only available in NGINX Plus with AppProtectDos. + +> Note: The feature is implemented using the NGINX Plus [NGINX App Protect Dos Module](https://docs.nginx.com/nginx-app-protect-dos/configuration/). + + +## Dos Protected Resource Specification + +Below is an example of a dos protected resource. It defines it's own configuration and references to policy configuration and to log configuration: +```yaml +apiVersion: appprotectdos.f5.com/v1beta1 +kind: DosProtectedResource +metadata: + name: dos-protected +spec: + enable: true + name: "my-dos" + apDosMonitor: + uri: "webapp.example.com" + apDosPolicy: "dospolicy" + dosSecurityLog: + enable: true + apDosLogConf: "doslogconf" + dosLogDest: "syslog-svc.default.svc.cluster.local:514" + +``` + +{{% table %}} +|Field | Description | Type | Required | +| ---| ---| ---| --- | +|``enable`` | Enables NGINX App Protect Dos. | ``bool`` | Yes | +|``name`` | Name of the protected object, max of 63 characters. | ``string`` | No | +|``apDosMonitor.uri`` | The destination to the desired protected object. [App Protect Dos monitor](#dosprotectedresourceapdosmonitor) Default value: None, URL will be extracted from the first request which arrives and taken from "Host" header or from destination ip+port. | ``string`` | No | +|``apDosMonitor.protocol`` | Determines if the server listens on http1 / http2 / grpc. [App Protect Dos monitor](#dosprotectedresourceapdosmonitor) Default value: http1. | ``enum`` | No | +|``apDosMonitor.timeout`` | Determines how long (in seconds) should NGINX App Protect DoS wait for a response. [App Protect Dos monitor](#dosprotectedresourceapdosmonitor) Default value: 10 seconds for http1/http2 and 5 seconds for grpc. | ``int64`` | No | +|``apDosPolicy`` | The [App Protect Dos policy](#dosprotectedresourceapdospolicy) of the dos. Accepts an optional namespace. | ``string`` | No | +|``dosSecurityLog.enable`` | Enables security log. | ``bool`` | No | +|``dosSecurityLog.apDosLogConf`` | The [App Protect Dos log conf](/nginx-ingress-controller/app-protect-dos/configuration/#app-protect-dos-logs) resource. Accepts an optional namespace. | ``string`` | No | +|``dosSecurityLog.dosLogDest`` | The log destination for the security log. Accepted variables are ``syslog:server=:``, ``stderr``, ````. Default is ``"syslog:server=127.0.0.1:514"``. | ``string`` | No | +{{% /table %}} + +### DosProtectedResource.apDosPolicy + +The `apDosPolicy` is a reference to the policy configuration defined as an `ApDosPolicy`. + +### DosProtectedResource.apDosMonitor + +This is how NGINX App Protect DoS monitors the stress level of the protected object. The monitor requests are sent from localhost (127.0.0.1). + +### Applying Policies + +You can apply policies to both VirtualServer and VirtualServerRoute resources. For example: + * VirtualServer: + ```yaml + apiVersion: k8s.nginx.org/v1 + kind: VirtualServer + metadata: + name: cafe + namespace: cafe + spec: + host: cafe.example.com + dos: "default/dos-protected" # virtual server dos configuration + upstreams: + - name: coffee + service: coffee-svc + port: 80 + routes: + - path: /tea + dos: "other/other-dos-protected" # route dos configuration + route: tea/tea + - path: /coffee + action: + pass: coffee + ``` + + For VirtualServer, you can apply a policy: + - to all routes (spec dos) + - to a specific route (route dos) + + Route dos configuration override spec dos configuration. + + * VirtualServerRoute, which is referenced by the VirtualServer above: + ```yaml + apiVersion: k8s.nginx.org/v1 + kind: VirtualServerRoute + metadata: + name: tea + namespace: tea + spec: + host: cafe.example.com + upstreams: + - name: tea + service: tea-svc + port: 80 + subroutes: + - path: /tea + dos: "default/dos-protected" + action: + pass: tea + ``` + + For VirtualServerRoute, you can apply dos configuration to a subroute (subroute policies). + +### Invalid Dos Protected Resources + +NGINX will treat a dos protected resource as invalid if one of the following conditions is met: +* The dos protected resource doesn't pass the [comprehensive validation](#comprehensive-validation). +* The dos protected resource isn't present in the cluster. + +### Validation + +Two types of validation are available for the Dos Protected resource: +* *Structural validation*, done by `kubectl` and the Kubernetes API server. +* *Comprehensive validation*, done by the Ingress Controller. diff --git a/docs/content/configuration/global-configuration/command-line-arguments.md b/docs/content/configuration/global-configuration/command-line-arguments.md index 419e51ffaa..752a016808 100644 --- a/docs/content/configuration/global-configuration/command-line-arguments.md +++ b/docs/content/configuration/global-configuration/command-line-arguments.md @@ -336,6 +336,69 @@ Requires [-nginx-plus](#cmdoption-nginx-plus). * If the argument is set, but `nginx-plus` is set to false, the Ingress Controller will fail to start. +  + + +### -enable-app-protect-dos + +Enables support for App Protect Dos. + +Requires [-nginx-plus](#cmdoption-nginx-plus). + +* If the argument is set, but `nginx-plus` is set to false, the Ingress Controller will fail to start. + +  + + +### -app-protect-dos-debug + +Enable debugging for App Protect Dos. + +Requires [-nginx-plus](#cmdoption-nginx-plus) and [-enable-app-protect-dos](#cmdoption-enable-app-protect-dos). + +* If the argument is set, but `nginx-plus` and `enable-app-protect-dos` are set to false, the Ingress Controller will fail to start. + +  + + +### -app-protect-dos-max-daemons + +Max number of ADMD instances. + +Default `1`. + +Requires [-nginx-plus](#cmdoption-nginx-plus) and [-enable-app-protect-dos](#cmdoption-enable-app-protect-dos). + +* If the argument is set, but `nginx-plus` and `enable-app-protect-dos` are set to false, the Ingress Controller will fail to start. + +  + + +### -app-protect-dos-max-workers + +Max number of nginx processes to support. + +Default `Number of CPU cores in the machine`. + +Requires [-nginx-plus](#cmdoption-nginx-plus) and [-enable-app-protect-dos](#cmdoption-enable-app-protect-dos). + +* If the argument is set, but `nginx-plus` and `enable-app-protect-dos` are set to false, the Ingress Controller will fail to start. + + +  + + +### -app-protect-dos-memory + +RAM memory size to consume in MB + +Default `50% of free RAM in the container or 80MB, the smaller`. + +Requires [-nginx-plus](#cmdoption-nginx-plus) and [-enable-app-protect-dos](#cmdoption-enable-app-protect-dos). + +* If the argument is set, but `nginx-plus` and `enable-app-protect-dos` are set to false, the Ingress Controller will fail to start. + +   diff --git a/docs/content/configuration/global-configuration/configmap-resource.md b/docs/content/configuration/global-configuration/configmap-resource.md index 152a4972af..1ec356e4d2 100644 --- a/docs/content/configuration/global-configuration/configmap-resource.md +++ b/docs/content/configuration/global-configuration/configmap-resource.md @@ -79,7 +79,7 @@ See the doc about [VirtualServer and VirtualServerRoute resources](/nginx-ingres |``default-server-return`` | Configures the [return](https://nginx.org/en/docs/http/ngx_http_rewrite_module.html#return) directive in the default server, which handles a client request if none of the hosts of Ingress or VirtualServer resources match. The default value configures NGINX to return a 404 error page. You can configure a fixed response or a redirect. For example, ``default-server-return: 302 https://nginx.org`` will redirect a client to ``https://nginx.org``. | ``404`` | | |``server-tokens`` | Enables or disables the [server_tokens](https://nginx.org/en/docs/http/ngx_http_core_module.html#server_tokens) directive. Additionally, with the NGINX Plus, you can specify a custom string value, including the empty string value, which disables the emission of the “Server” field. | ``True`` | | |``worker-processes`` | Sets the value of the [worker_processes](https://nginx.org/en/docs/ngx_core_module.html#worker_processes) directive. | ``auto`` | | -|``worker-rlimit-nofile`` | Sets the value of the [worker_rlimit_nofile](https://nginx.org/en/docs/ngx_core_module.html#worker_rlimit_nofile) directive. | N/A | | +|``worker-rlimit-nofile`` | Sets the value of the [worker_rlimit_nofile](https://nginx.org/en/docs/ngx_core_module.html#worker_rlimit_nofile) directive. | N/A | | |``worker-connections`` | Sets the value of the [worker_connections](https://nginx.org/en/docs/ngx_core_module.html#worker_connections) directive. | ``1024`` | | |``worker-cpu-affinity`` | Sets the value of the [worker_cpu_affinity](https://nginx.org/en/docs/ngx_core_module.html#worker_cpu_affinity) directive. | N/A | | |``worker-shutdown-timeout`` | Sets the value of the [worker_shutdown_timeout](https://nginx.org/en/docs/ngx_core_module.html#worker_shutdown_timeout) directive. | N/A | | @@ -184,4 +184,6 @@ See the doc about [VirtualServer and VirtualServerRoute resources](/nginx-ingres |``app-protect-failure-mode-action`` | Sets the ``app_protect_failure_mode_action`` [global directive](/nginx-app-protect/configuration/#global-directives). | ``pass`` | | |``app-protect-cpu-thresholds`` | Sets the ``app_protect_cpu_thresholds`` [global directive](/nginx-app-protect/configuration/#global-directives). | ``high=100 low=100`` | | |``app-protect-physical-memory-util-thresholds`` | Sets the ``app_protect_physical_memory_util_thresholds`` [global directive](/nginx-app-protect/configuration/#global-directives). | ``high=100 low=100`` | | +|``app-protect-dos-log-format`` | Sets the custom [log format](https://nginx.org/en/docs/http/ngx_http_log_module.html#log_format) for Dos Access log traffic. For convenience, it is possible to define the log format across multiple lines (each line separated by ``\n``). In that case, the Ingress Controller will replace every ``\n`` character with a space character. All ``'`` characters must be escaped. | `, vs_name_al=$app_protect_dos_vs_name, ip=$remote_addr, tls_fp=$app_protect_dos_tls_fp, outcome=$app_protect_dos_outcome, reason=$app_protect_dos_outcome_reason, policy_name=$app_protect_dos_policy_name, dos_version=$app_protect_dos_version, ip_tls=$remote_addr:$app_protect_dos_tls_fp,` | | +|``app-protect-dos-log-format-escaping`` | Sets the characters escaping for the variables of the stream log format. Supported values: ``json`` (JSON escaping), ``default`` (the default escaping) ``none`` (disables escaping). | ``default`` | | {{% /table %}} diff --git a/docs/content/configuration/ingress-resources/advanced-configuration-with-annotations.md b/docs/content/configuration/ingress-resources/advanced-configuration-with-annotations.md index fce3532b11..9e68ae1968 100644 --- a/docs/content/configuration/ingress-resources/advanced-configuration-with-annotations.md +++ b/docs/content/configuration/ingress-resources/advanced-configuration-with-annotations.md @@ -205,3 +205,13 @@ The table below summarizes the available annotations. |``appprotect.f5.com/app-protect-security-log`` | N/A | The App Protect log configuration for the Ingress Resource. Format is ``namespace/name``. If no namespace is specified, the same namespace as the Ingress Resource is used. If not specified the default is used which is: filter: ``illegal``, format: ``default``. Multiple configurations can be specified in a comma separated list. Both log configurations and destinations list (see below) must be of equal length. Configs and destinations are paired by the list indices. | N/A | [Example for App Protect](https://github.com/nginxinc/kubernetes-ingress/tree/v2.0.3/examples/appprotect). | |``appprotect.f5.com/app-protect-security-log-destination`` | N/A | The destination of the security log. For more information check the [DESTINATION argument](/nginx-app-protect/troubleshooting/#app-protect-logging-overview). Multiple destinations can be specified in a coma separated list. Both log configurations and destinations list (see above) must be of equal length. Configs and destinations are paired by the list indices. | ``syslog:server=localhost:514`` | [Example for App Protect](https://github.com/nginxinc/kubernetes-ingress/tree/v2.0.3/examples/appprotect). | {{% /table %}} + +### App Protect Dos + +**Note**: The App Protect Dos annotations only work if App Protect Dos module is [installed](/nginx-ingress-controller/app-protect-dos/installation/). + +{{% table %}} +|Annotation | ConfigMap Key | Description | Default | Example | +| ---| ---| ---| ---| --- | +|``appprotectdos.f5.com/app-protect-dos-resource`` | N/A | Enable App Protect Dos for the Ingress Resource by specifying a DosProtectedResource. | N/A | [Example for App Protect Dos](https://github.com/nginxinc/kubernetes-ingress/tree/v1.12.1/examples/appprotect-dos). | +{% /table %}} diff --git a/docs/content/installation/building-ingress-controller-image.md b/docs/content/installation/building-ingress-controller-image.md index e85e603927..878c3ae2b7 100644 --- a/docs/content/installation/building-ingress-controller-image.md +++ b/docs/content/installation/building-ingress-controller-image.md @@ -83,11 +83,15 @@ Below you can find some of the most useful targets in the **Makefile**: * **debian-image**: for building a debian-based image with NGINX. * **debian-image-plus**: for building a debian-based image with NGINX Plus. * **debian-image-nap-plus**: for building a debian-based image with NGINX Plus and the [appprotect](/nginx-app-protect/) module. +* **debian-image-dos-plus**: for building a debian-based image with NGINX Plus and the [appprotect-dos](/nginx-app-protect-dos/) module. +* **debian-image-nap-dos-plus**: for building a debian-based image with NGINX Plus appprotect and appprotect-dos modules. * **debian-image-opentracing**: for building a debian-based image with NGINX, [opentracing](https://github.com/opentracing-contrib/nginx-opentracing) module and the [Jaeger](https://www.jaegertracing.io/) tracer. * **debian-image-opentracing-plus**: for building a debian-based image with NGINX Plus, [opentracing](https://github.com/opentracing-contrib/nginx-opentracing) module and the [Jaeger](https://www.jaegertracing.io/) tracer. * **openshift-image**: for building an ubi-based image with NGINX for [Openshift](https://www.openshift.com/) clusters. * **openshift-image-plus**: for building an ubi-based image with NGINX Plus for [Openshift](https://www.openshift.com/) clusters. * **openshift-image-nap-plus**: for building an ubi-based image with NGINX Plus and the [appprotect](/nginx-app-protect/) module for [Openshift](https://www.openshift.com/) clusters. +* **openshift-image-dos-plus**: for building an ubi-based image with NGINX Plus and the [appprotect_dos](/nginx-app-protect-dos/) module for [Openshift](https://www.openshift.com/) clusters. +* **openshift-image-nap-dos-plus**: for building an ubi-based image with NGINX Plus, [appprotect](/nginx-app-protect/) and the [appprotect_dos](/nginx-app-protect-dos/) module for [Openshift](https://www.openshift.com/) clusters. Note: You need to store your RHEL organization and activation keys in a file named `rhel_license` in the project root. Example: ```bash RHEL_ORGANIZATION=1111111 diff --git a/docs/content/installation/installation-with-helm-dos-arbitrator.md b/docs/content/installation/installation-with-helm-dos-arbitrator.md new file mode 100644 index 0000000000..73cc09d6a5 --- /dev/null +++ b/docs/content/installation/installation-with-helm-dos-arbitrator.md @@ -0,0 +1,98 @@ +--- +title: Installation with Helm App Protect Dos Arbitrator +description: +weight: 1900 +doctypes: [""] +toc: true +--- + +## Prerequisites + + - A [Kubernetes Version Supported by the Ingress Controller](https://docs.nginx.com/nginx-ingress-controller/technical-specifications/#supported-kubernetes-versions) + - Helm 3.0+. + - Git. + +## Getting the Chart Sources + +This step is required if you're installing the chart using its sources. Additionally, the step is also required for managing the custom resource definitions (CRDs), which the Ingress Controller requires by default, or for upgrading/deleting the CRDs. + +1. Clone the Ingress controller repo: + ```console + $ git clone https://github.com/nginxinc/kubernetes-ingress/ + ``` +2. Change your working directory to /deployments/helm-chart-dos-arbitrator: + ```console + $ cd kubernetes-ingress/deployments/helm-chart-dos-arbitrator + $ git checkout v1.12.1 + ``` + +## Adding the Helm Repository + +This step is required if you're installing the chart via the helm repository. + +```console +$ helm repo add nginx-stable https://helm.nginx.com/stable +$ helm repo update +``` + +## Installing the Chart + +### Installing via Helm Repository + +To install the chart with the release name my-release-dos (my-release-dos is the name that you choose): + +```console +$ helm install my-release-dos nginx-stable/nginx-appprotect-dos-arbitrator +``` + + +### Installing Using Chart Sources + +To install the chart with the release name my-release-dos (my-release-dos is the name that you choose): + +```console +$ helm install my-release-dos . +``` + +The command deploys the App Protect Dos Arbitrator in your Kubernetes cluster in the default configuration. The configuration section lists the parameters that can be configured during installation. + +## Upgrading the Chart + +### Upgrading the Release + +To upgrade the release `my-release-dos`: + +#### Upgrade Using Chart Sources: + +```console +$ helm upgrade my-release-dos . +``` + +#### Upgrade via Helm Repository: + +```console +$ helm upgrade my-release-dos nginx-stable/nginx-appprotect-dos-arbitrator +``` + +## Uninstalling the Chart + +### Uninstalling the Release + +To uninstall/delete the release `my-release-dos`: + +```console +$ helm uninstall my-release-dos +``` + +The command removes all the Kubernetes components associated with the release and deletes the release. + +## Configuration + +The following tables lists the configurable parameters of the NGINX App Protect Dos Arbitrator chart and their default values. + +Parameter | Description | Default +--- | --- | --- +`arbitrator.resources` | The resources of the Arbitrator pods. | limits:
cpu: 500m
memory: 128Mi +`appprotectdos.arbitrator.image.repository` | The image repository of the Arbitrator image. | docker-registry.nginx.com/nap-dos/app_protect_dos_arb +`arbitrator.image.tag` | The tag of the Arbitrator image. | latest +`arbitrator.image.pullPolicy` | The pull policy for the Arbitrator image. | IfNotPresent diff --git a/docs/content/installation/installation-with-helm.md b/docs/content/installation/installation-with-helm.md index cf6f69315e..515277f889 100644 --- a/docs/content/installation/installation-with-helm.md +++ b/docs/content/installation/installation-with-helm.md @@ -19,6 +19,7 @@ This document describes how to install the NGINX Ingress Controller in your Kube - Alternatively, pull an Ingress controller image with NGINX Plus and push it to your private registry by following the instructions from [here](/nginx-ingress-controller/installation/pulling-ingress-controller-image). - Alternatively, you can build an Ingress controller image with NGINX Plus and push it to your private registry by following the instructions from [here](/nginx-ingress-controller/installation/building-ingress-controller-image). - Update the `controller.image.repository` field of the `values-plus.yaml` accordingly. + - If you’d like to use App Protect Dos, please [install App Protect Dos Arbitrator helm chart](nginx-ingress-controller/installation/installation-with-helm) ## Getting the Chart Sources @@ -149,7 +150,12 @@ The following tables lists the configurable parameters of the NGINX Ingress cont |``controller.kind`` | The kind of the Ingress controller installation - deployment or daemonset. | deployment | |``controller.nginxplus`` | Deploys the Ingress controller for NGINX Plus. | false | |``controller.nginxReloadTimeout`` | The timeout in milliseconds which the Ingress Controller will wait for a successful NGINX reload after a change or at the initial start. The default is 4000 (or 20000 if `controller.appprotect.enable` is true). If set to 0, the default value will be used. | 0 | -|``controller.appprotect.enable`` | Enables the App Protect module in the Ingress Controller. | false | +|``controller.appprotect.enable`` | Enables the App Protect module in the Ingress Controller. | false | +|``controller.appprotectdos.enable`` | Enables the App Protect Dos module in the Ingress Controller. | false | +|``controller.appprotectdos.enable`` | Enables the App Protect Dos module in the Ingress Controller. | false | +|``controller.appprotectdos.arbitrator.name`` | The name of the App Protect Dos Arbitrator deployment. | Autogenerated | +|``controller.appprotectdos.arbitrator.repository`` | The image repository of the Arbitrator. | nginx/nginx-arbitrator | +|``controller.appprotectdos.debug`` | Enables App Protect Dos debug logs. | false | |``controller.hostNetwork`` | Enables the Ingress controller pods to use the host's network namespace. | false | |``controller.nginxDebug`` | Enables debugging for NGINX. Uses the ``nginx-debug`` binary. Requires ``error-log-level: debug`` in the ConfigMap via ``controller.config.entries``. | false | |``controller.logLevel`` | The log level of the Ingress Controller. | 1 | diff --git a/docs/content/installation/installation-with-manifests.md b/docs/content/installation/installation-with-manifests.md index bdad432b0a..61bd8d9fd1 100644 --- a/docs/content/installation/installation-with-manifests.md +++ b/docs/content/installation/installation-with-manifests.md @@ -41,7 +41,12 @@ This document describes how to install the NGINX Ingress Controller in your Kube ``` $ kubectl apply -f rbac/ap-rbac.yaml ``` + +4. (App Protect Dos only) Create the App Protect Dos role and role binding: + ``` + $ kubectl apply -f rbac/apdos-rbac.yaml + ``` **Note**: To perform this step you must be a cluster admin. Follow the documentation of your Kubernetes platform to configure the admin access. For GKE, see the [Role-Based Access Control](https://cloud.google.com/kubernetes-engine/docs/how-to/role-based-access-control) doc. ## 2. Create Common Resources @@ -99,6 +104,17 @@ If you would like to use the App Protect module, create the following additional $ kubectl apply -f common/crds/appprotect.f5.com_apusersigs.yaml ``` +### Resources for NGINX App Protect Dos + +If you would like to use the App Protect Dos module, create the following additional resources: + +1. Create a custom resource definition for `APDosPolicy` and `APDosLogConf`: + + ``` + $ kubectl apply -f common/crds/appprotectdos.f5.com_apdoslogconfs.yaml + $ kubectl apply -f common/crds/appprotectdos.f5.com_apdospolicies.yaml + ``` + ## 3. Deploy the Ingress Controller We include two options for deploying the Ingress controller: @@ -107,6 +123,18 @@ We include two options for deploying the Ingress controller: > Before creating a Deployment or Daemonset resource, make sure to update the [command-line arguments](/nginx-ingress-controller/configuration/global-configuration/command-line-arguments) of the Ingress Controller container in the corresponding manifest file according to your requirements. +### Deploy Arbitrator for NGINX App Protect Dos +If you would like to use the App Protect Dos module, need to add arbitrator deployment. + +* build your own image and push it to your private Docker registry by following the instructions from [here](/nginx-ingress-controller/app-protect-dos/installation#Build-the-app-protect-dos-arb-Docker-Image). + +* run the Arbitrator by using a Deployment and Service + + ``` + $ kubectl apply -f deployment/appprotect-dos-arb.yaml + $ kubectl apply -f service/appprotect-dos-arb-svc.yaml + ``` + ### 3.1 Run the Ingress Controller * *Use a Deployment*. When you run the Ingress Controller by using a Deployment, by default, Kubernetes will create one Ingress controller pod. diff --git a/docs/content/troubleshooting/troubleshooting-with-app-protect-dos.md b/docs/content/troubleshooting/troubleshooting-with-app-protect-dos.md new file mode 100644 index 0000000000..bb6daa9223 --- /dev/null +++ b/docs/content/troubleshooting/troubleshooting-with-app-protect-dos.md @@ -0,0 +1,90 @@ +--- +title: Troubleshooting with NGINX App Protect Dos +description: +weight: 2000 +doctypes: [""] +aliases: +- /app-protect/troubleshooting/ +toc: true +--- + +This document describes how to troubleshoot problems with the Ingress Controller with the App Protect Dos module enabled. + +For general troubleshooting of the Ingress Controller, check the general [troubleshooting](/nginx-ingress-controller/troubleshooting/) documentation. + +## Potential Problems + +The table below categorizes some potential problems with the Ingress Controller when App Protect module is enabled. It suggests how to troubleshoot those problems, using one or more methods from the next section. + +```eval_rst +.. list-table:: + :header-rows: 1 + + * - Problem area + - Symptom + - Troubleshooting method + - Common cause + * - Start. + - The Ingress Controller fails to start. + - Check the logs. + - Misconfigured APDosLogConf or APDosPolicy. + * - APDosLogConf, APDosPolicy or Ingress Resource. + - The configuration is not applied. + - Check the events of the APDosLogConf, APDosPolicy and Ingress Resource, check the logs, replace the policy. + - APDosLogConf or APDosPolicy is invalid. + * - NGINX. + - The Ingress Controller NGINX verification timeouts while starting for the first time or while reloading after a change. + - Check the logs for ``Unable to fetch version: X`` message. Check the Availability of APDosPolicy External References. + - Too many Ingress Resources with App Protect Dos enabled. Check the `NGINX fails to start/reload section <#nginx-fails-to-start-or-reload>`_ of the Known Issues. +``` + +## Troubleshooting Methods + +### Check the Ingress Controller and App Protect Dos logs + +App Protect Dos logs are part of the Ingress Controller logs when the module is enabled. To check the Ingress Controller logs, follow the steps of [Checking the Ingress Controller Logs](/nginx-ingress-controller/troubleshooting/#checking-the-ingress-controller-logs) of the Troubleshooting guide. + +For App Protect Dos specific logs, look for messages starting with `APP_PROTECT_DOS`, for example: +``` +2021/06/14 08:17:50 [notice] 242#242: APP_PROTECT_DOS { "event": "shared_memory_connected", "worker_pid": 242, "mode": "operational", "mode_changed": true } +``` + +### Check events of an Ingress Resource + +Follow the steps of [Checking the Events of an Ingress Resource](/troubleshooting/#checking-the-events-of-an-ingress-resource). + +### Check events of APDosLogConf + +After you create or update an APDosLogConf, you can immediately check if the NGINX configuration was successfully applied by NGINX: +``` +$ kubectl describe apdoslogconf logconf +Name: logconf +Namespace: default +. . . +Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Normal AddedOrUpdated 11s nginx-ingress-controller AppProtectDosLogConfig default/logconf was added or updated +``` +Note that in the events section, we have a `Normal` event with the `AddedOrUpdated` reason, which informs us that the configuration was successfully applied. + +### Check events of APDosPolicy + +After you create or update an APDosPolicy, you can immediately check if the NGINX configuration was successfully applied by NGINX: +``` +$ kubectl describe apdospolicy dospolicy +Name: dospolicy +Namespace: default +. . . +Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Normal AddedOrUpdated 2m25s nginx-ingress-controller AppProtectDosPolicy default/dospolicy was added or updated +``` +Note that in the events section, we have a `Normal` event with the `AddedOrUpdated` reason, which informs us that the configuration was successfully applied. + +## Run App Protect Dos in Debug log Mode + +When you set the Ingress Controller to use debug log mode, the setting also applies to the App Protect Dos module. See [Running NGINX in the Debug Mode](/nginx-ingress-controller/troubleshooting/#running-nginx-in-the-debug-mode) for instructions. + +You can enable debug log mode to App Protect Dos module only by setting the `app-protect-dos-debug` [configmap](/nginx-ingress-controller/configuration/global-configuration/configmap-resource#modules). diff --git a/examples/appprotect-dos/README.md b/examples/appprotect-dos/README.md new file mode 100644 index 0000000000..aa831fa555 --- /dev/null +++ b/examples/appprotect-dos/README.md @@ -0,0 +1,67 @@ +# NGINX App Protect Dos Support + +In this example we deploy the NGINX Plus Ingress controller with [NGINX App Protect Dos](https://www.nginx.com/products/nginx-app-protect-dos/), a simple web application and then configure load balancing and DOS protection for that application using the Ingress resource. + +## Running the Example + +## 1. Deploy the Ingress Controller + +1. Follow the installation [instructions](../../docs/installation.md) to deploy the Ingress controller with NGINX App Protect Dos. + +2. Save the public IP address of the Ingress controller into a shell variable: + ``` + $ IC_IP=XXX.YYY.ZZZ.III + ``` +3. Save the HTTPS port of the Ingress controller into a shell variable: + ``` + $ IC_HTTPS_PORT= + ``` + +## 2. Deploy the Webapp Application + +Create the webapp deployment and service: +``` +$ kubectl create -f webapp.yaml +``` + +## 3. Configure Load Balancing +1. Create the syslog service and pod for the App Protect Dos security logs: + ``` + $ kubectl create -f syslog.yaml + ``` +2. Create a secret with an SSL certificate and a key: + ``` + $ kubectl create -f webapp-secret.yaml + ``` +3. Create the App Protect Dos Protected Resource: + ``` + $ kubectl create -f apdos-protected.yaml + ``` +4. Create the App Protect Dos policy and log configuration: + ``` + $ kubectl create -f apdos-policy.yaml + $ kubectl create -f apdos-logconf.yaml + ``` +5. Create an Ingress Resource: + + ``` + $ kubectl create -f webapp-ingress.yaml + ``` + Note the App Protect Dos annotation in the Ingress resource. This enables DOS protection by specifying the DOS protected resource configuration that applies to this Ingress. + +## 4. Test the Application + +1. To access the application, curl the Webapp service. We'll use `curl`'s --insecure option to turn off certificate verification of our self-signed +certificate and the --resolve option to set the Host header of a request with `webapp.example.com` + + Send a request to the application:: + ``` + $ curl --resolve webapp.example.com:$IC_HTTPS_PORT:$IC_IP https://webapp.example.com:$IC_HTTPS_PORT/ --insecure + Server address: 10.12.0.18:80 + Server name: coffee-7586895968-r26zn + ... + ``` +1. To check the security logs in the syslog pod: + ``` + $ kubectl exec -it -- cat /var/log/messages + ``` diff --git a/examples/appprotect-dos/apdos-logconf.yaml b/examples/appprotect-dos/apdos-logconf.yaml new file mode 100644 index 0000000000..885a524647 --- /dev/null +++ b/examples/appprotect-dos/apdos-logconf.yaml @@ -0,0 +1,12 @@ +apiVersion: appprotectdos.f5.com/v1beta1 +kind: APDosLogConf +metadata: + name: doslogconf +spec: + content: + format: splunk + max_message_size: 64k + filter: + traffic-mitigation-stats: all + bad-actors: top 10 + attack-signatures: top 10 diff --git a/examples/appprotect-dos/apdos-policy.yaml b/examples/appprotect-dos/apdos-policy.yaml new file mode 100644 index 0000000000..add82acd69 --- /dev/null +++ b/examples/appprotect-dos/apdos-policy.yaml @@ -0,0 +1,10 @@ +apiVersion: appprotectdos.f5.com/v1beta1 +kind: APDosPolicy +metadata: + name: dospolicy +spec: + mitigation_mode: "standard" + signatures: "on" + bad_actors: "on" + automation_tools_detection: "on" + tls_fingerprint: "on" diff --git a/examples/appprotect-dos/apdos-protected.yaml b/examples/appprotect-dos/apdos-protected.yaml new file mode 100644 index 0000000000..684d6b904b --- /dev/null +++ b/examples/appprotect-dos/apdos-protected.yaml @@ -0,0 +1,17 @@ +apiVersion: appprotectdos.f5.com/v1beta1 +kind: DosProtectedResource +metadata: + name: dos-protected +spec: + enable: true + name: "webapp.example.com" + apDosPolicy: "dospolicy" + apDosMonitor: + uri: "webapp.example.com" + protocol: "http1" + timeout: 5 + dosAccessLogDest: "127.0.0.1:5561" + dosSecurityLog: + enable: true + apDosLogConf: "doslogconf" + dosLogDest: "syslog-svc.default.svc.cluster.local:514" diff --git a/examples/appprotect-dos/syslog.yaml b/examples/appprotect-dos/syslog.yaml new file mode 100644 index 0000000000..de25eeb3d0 --- /dev/null +++ b/examples/appprotect-dos/syslog.yaml @@ -0,0 +1,32 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: syslog +spec: + replicas: 1 + selector: + matchLabels: + app: syslog + template: + metadata: + labels: + app: syslog + spec: + containers: + - name: syslog + image: balabit/syslog-ng:3.28.1 + ports: + - containerPort: 514 + - containerPort: 601 +--- +apiVersion: v1 +kind: Service +metadata: + name: syslog-svc +spec: + ports: + - port: 514 + targetPort: 514 + protocol: TCP + selector: + app: syslog \ No newline at end of file diff --git a/examples/appprotect-dos/webapp-ingress.yaml b/examples/appprotect-dos/webapp-ingress.yaml new file mode 100644 index 0000000000..6ba4017353 --- /dev/null +++ b/examples/appprotect-dos/webapp-ingress.yaml @@ -0,0 +1,23 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: webapp-ingress + annotations: + appprotectdos.f5.com/app-protect-dos-resource: "default/dos-protected" +spec: + ingressClassName: nginx + tls: + - hosts: + - webapp.example.com + secretName: webapp-secret + rules: + - host: webapp.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: webapp-svc + port: + number: 80 diff --git a/examples/appprotect-dos/webapp-secret.yaml b/examples/appprotect-dos/webapp-secret.yaml new file mode 100644 index 0000000000..20960b0e16 --- /dev/null +++ b/examples/appprotect-dos/webapp-secret.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + name: webapp-secret +type: kubernetes.io/tls +data: + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURMakNDQWhZQ0NRREFPRjl0THNhWFdqQU5CZ2txaGtpRzl3MEJBUXNGQURCYU1Rc3dDUVlEVlFRR0V3SlYKVXpFTE1Ba0dBMVVFQ0F3Q1EwRXhJVEFmQmdOVkJBb01HRWx1ZEdWeWJtVjBJRmRwWkdkcGRITWdVSFI1SUV4MApaREViTUJrR0ExVUVBd3dTWTJGbVpTNWxlR0Z0Y0d4bExtTnZiU0FnTUI0WERURTRNRGt4TWpFMk1UVXpOVm9YCkRUSXpNRGt4TVRFMk1UVXpOVm93V0RFTE1Ba0dBMVVFQmhNQ1ZWTXhDekFKQmdOVkJBZ01Ba05CTVNFd0h3WUQKVlFRS0RCaEpiblJsY201bGRDQlhhV1JuYVhSeklGQjBlU0JNZEdReEdUQVhCZ05WQkFNTUVHTmhabVV1WlhoaApiWEJzWlM1amIyMHdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFDcDZLbjdzeTgxCnAwanVKL2N5ayt2Q0FtbHNmanRGTTJtdVpOSzBLdGVjcUcyZmpXUWI1NXhRMVlGQTJYT1N3SEFZdlNkd0kyaloKcnVXOHFYWENMMnJiNENaQ0Z4d3BWRUNyY3hkam0zdGVWaVJYVnNZSW1tSkhQUFN5UWdwaW9iczl4N0RsTGM2SQpCQTBaalVPeWwwUHFHOVNKZXhNVjczV0lJYTVyRFZTRjJyNGtTa2JBajREY2o3TFhlRmxWWEgySTVYd1hDcHRDCm42N0pDZzQyZitrOHdnemNSVnA4WFprWldaVmp3cTlSVUtEWG1GQjJZeU4xWEVXZFowZXdSdUtZVUpsc202OTIKc2tPcktRajB2a29QbjQxRUUvK1RhVkVwcUxUUm9VWTNyemc3RGtkemZkQml6Rk8yZHNQTkZ4MkNXMGpYa05MdgpLbzI1Q1pyT2hYQUhBZ01CQUFFd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFLSEZDY3lPalp2b0hzd1VCTWRMClJkSEliMzgzcFdGeW5acS9MdVVvdnNWQTU4QjBDZzdCRWZ5NXZXVlZycTVSSWt2NGxaODFOMjl4MjFkMUpINnIKalNuUXgrRFhDTy9USkVWNWxTQ1VwSUd6RVVZYVVQZ1J5anNNL05VZENKOHVIVmhaSitTNkZBK0NuT0Q5cm4yaQpaQmVQQ0k1ckh3RVh3bm5sOHl3aWozdnZRNXpISXV5QmdsV3IvUXl1aTlmalBwd1dVdlVtNG52NVNNRzl6Q1Y3ClBwdXd2dWF0cWpPMTIwOEJqZkUvY1pISWc4SHc5bXZXOXg5QytJUU1JTURFN2IvZzZPY0s3TEdUTHdsRnh2QTgKN1dqRWVxdW5heUlwaE1oS1JYVmYxTjM0OWVOOThFejM4Zk9USFRQYmRKakZBL1BjQytHeW1lK2lHdDVPUWRGaAp5UkU9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + tls.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBcWVpcCs3TXZOYWRJN2lmM01wUHJ3Z0pwYkg0N1JUTnBybVRTdENyWG5LaHRuNDFrCkcrZWNVTldCUU5semtzQndHTDBuY0NObzJhN2x2S2wxd2k5cTIrQW1RaGNjS1ZSQXEzTVhZNXQ3WGxZa1YxYkcKQ0pwaVJ6ejBza0lLWXFHN1BjZXc1UzNPaUFRTkdZMURzcGRENmh2VWlYc1RGZTkxaUNHdWF3MVVoZHErSkVwRwp3SStBM0kreTEzaFpWVng5aU9WOEZ3cWJRcCt1eVFvT05uL3BQTUlNM0VWYWZGMlpHVm1WWThLdlVWQ2cxNWhRCmRtTWpkVnhGbldkSHNFYmltRkNaYkp1dmRySkRxeWtJOUw1S0Q1K05SQlAvazJsUkthaTAwYUZHTjY4NE93NUgKYzMzUVlzeFR0bmJEelJjZGdsdEkxNURTN3lxTnVRbWF6b1Z3QndJREFRQUJBb0lCQVFDUFNkU1luUXRTUHlxbApGZlZGcFRPc29PWVJoZjhzSStpYkZ4SU91UmF1V2VoaEp4ZG01Uk9ScEF6bUNMeUw1VmhqdEptZTIyM2dMcncyCk45OUVqVUtiL1ZPbVp1RHNCYzZvQ0Y2UU5SNThkejhjbk9SVGV3Y290c0pSMXBuMWhobG5SNUhxSkpCSmFzazEKWkVuVVFmY1hackw5NGxvOUpIM0UrVXFqbzFGRnM4eHhFOHdvUEJxalpzVjdwUlVaZ0MzTGh4bndMU0V4eUZvNApjeGI5U09HNU9tQUpvelN0Rm9RMkdKT2VzOHJKNXFmZHZ5dGdnOXhiTGFRTC94MGtwUTYyQm9GTUJEZHFPZVBXCktmUDV6WjYvMDcvdnBqNDh5QTFRMzJQem9idWJzQkxkM0tjbjMyamZtMUU3cHJ0V2wrSmVPRmlPem5CUUZKYk4KNHFQVlJ6NWhBb0dCQU50V3l4aE5DU0x1NFArWGdLeWNrbGpKNkY1NjY4Zk5qNUN6Z0ZScUowOXpuMFRsc05ybwpGVExaY3hEcW5SM0hQWU00MkpFUmgySi9xREZaeW5SUW8zY2czb2VpdlVkQlZHWTgrRkkxVzBxZHViL0w5K3l1CmVkT1pUUTVYbUdHcDZyNmpleHltY0ppbS9Pc0IzWm5ZT3BPcmxEN1NQbUJ2ek5MazRNRjZneGJYQW9HQkFNWk8KMHA2SGJCbWNQMHRqRlhmY0tFNzdJbUxtMHNBRzR1SG9VeDBlUGovMnFyblRuT0JCTkU0TXZnRHVUSnp5K2NhVQprOFJxbWRIQ2JIelRlNmZ6WXEvOWl0OHNaNzdLVk4xcWtiSWN1YytSVHhBOW5OaDFUanNSbmU3NFowajFGQ0xrCmhIY3FIMHJpN1BZU0tIVEU4RnZGQ3haWWRidUI4NENtWmlodnhicFJBb0dBSWJqcWFNWVBUWXVrbENkYTVTNzkKWVNGSjFKelplMUtqYS8vdER3MXpGY2dWQ0thMzFqQXdjaXowZi9sU1JxM0hTMUdHR21lemhQVlRpcUxmZVpxYwpSMGlLYmhnYk9jVlZrSkozSzB5QXlLd1BUdW14S0haNnpJbVpTMGMwYW0rUlk5WUdxNVQ3WXJ6cHpjZnZwaU9VCmZmZTNSeUZUN2NmQ21mb09oREN0enVrQ2dZQjMwb0xDMVJMRk9ycW40M3ZDUzUxemM1em9ZNDR1QnpzcHd3WU4KVHd2UC9FeFdNZjNWSnJEakJDSCtULzZzeXNlUGJKRUltbHpNK0l3eXRGcEFOZmlJWEV0LzQ4WGY2ME54OGdXTQp1SHl4Wlp4L05LdER3MFY4dlgxUE9ucTJBNWVpS2ErOGpSQVJZS0pMWU5kZkR1d29seHZHNmJaaGtQaS80RXRUCjNZMThzUUtCZ0h0S2JrKzdsTkpWZXN3WEU1Y1VHNkVEVXNEZS8yVWE3ZlhwN0ZjanFCRW9hcDFMU3crNlRYcDAKWmdybUtFOEFSek00NytFSkhVdmlpcS9udXBFMTVnMGtKVzNzeWhwVTl6WkxPN2x0QjBLSWtPOVpSY21Vam84UQpjcExsSE1BcWJMSjhXWUdKQ2toaVd4eWFsNmhZVHlXWTRjVmtDMHh0VGwvaFVFOUllTktvCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== diff --git a/examples/appprotect-dos/webapp.yaml b/examples/appprotect-dos/webapp.yaml new file mode 100644 index 0000000000..58c1446184 --- /dev/null +++ b/examples/appprotect-dos/webapp.yaml @@ -0,0 +1,32 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: webapp +spec: + replicas: 2 + selector: + matchLabels: + app: webapp + template: + metadata: + labels: + app: webapp + spec: + containers: + - name: webapp + image: nginxdemos/hello:plain-text + ports: + - containerPort: 80 +--- +apiVersion: v1 +kind: Service +metadata: + name: webapp-svc +spec: + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: http + selector: + app: webapp diff --git a/examples/custom-resources/dos/README.md b/examples/custom-resources/dos/README.md new file mode 100644 index 0000000000..8a3a12a4c7 --- /dev/null +++ b/examples/custom-resources/dos/README.md @@ -0,0 +1,63 @@ +# DOS + +In this example we deploy the NGINX Plus Ingress controller with [NGINX App Protect Dos](https://www.nginx.com/products/nginx-app-protect-dos/), a simple web application and then configure load balancing and DOS protection for that application using the VirtualServer resource. + +## Prerequisites + +1. Follow the installation [instructions](../../docs/installation.md) to deploy the Ingress controller with NGINX App Protect Dos. +1. Save the public IP address of the Ingress Controller into a shell variable: + ``` + $ IC_IP=XXX.YYY.ZZZ.III + ``` +1. Save the HTTP port of the Ingress Controller into a shell variable: + ``` + $ IC_HTTP_PORT= + ``` + +## Step 1. Deploy a Web Application + +Create the application deployment and service: +``` +$ kubectl apply -f webapp.yaml +``` + +## Step 2 - Deploy the DOS configuration resources + +1. Create the syslog service and pod for the App Protect security logs: + ``` + $ kubectl apply -f syslog.yaml + ``` +2. Create the Dos protected resource configuration: + ``` + $ kubectl apply -f apdos-protected.yaml + ``` +3. Create the App Protect Dos policy and log configuration: + ``` + $ kubectl apply -f apdos-policy.yaml + $ kubectl apply -f apdos-logconf.yaml + ``` + +## Step 3 - Configure Load Balancing + +1. Create the VirtualServer Resource: + ``` + $ kubectl apply -f virtual-server.yaml + ``` +Note the reference to the DOS protected resource in the Policy resource. By specifying the resource it enables DOS protection for the VirtualServer. + +## Step 4 - Test the Application + +To access the application, curl the Webapp service. We'll use the --resolve option to set the Host header of a request with `webapp.example.com` + +1. Send a request to the application: + ``` + $ curl --resolve webapp.example.com:$IC_HTTP_PORT:$IC_IP http://webapp.example.com:$IC_HTTP_PORT/ + Server address: 10.12.0.18:80 + Server name: webapp-7586895968-r26zn + ... + ``` + +1. To check the security logs in the syslog pod: + ``` + $ kubectl exec -it -- cat /var/log/messages + ``` diff --git a/examples/custom-resources/dos/apdos-logconf.yaml b/examples/custom-resources/dos/apdos-logconf.yaml new file mode 100644 index 0000000000..885a524647 --- /dev/null +++ b/examples/custom-resources/dos/apdos-logconf.yaml @@ -0,0 +1,12 @@ +apiVersion: appprotectdos.f5.com/v1beta1 +kind: APDosLogConf +metadata: + name: doslogconf +spec: + content: + format: splunk + max_message_size: 64k + filter: + traffic-mitigation-stats: all + bad-actors: top 10 + attack-signatures: top 10 diff --git a/examples/custom-resources/dos/apdos-policy.yaml b/examples/custom-resources/dos/apdos-policy.yaml new file mode 100644 index 0000000000..add82acd69 --- /dev/null +++ b/examples/custom-resources/dos/apdos-policy.yaml @@ -0,0 +1,10 @@ +apiVersion: appprotectdos.f5.com/v1beta1 +kind: APDosPolicy +metadata: + name: dospolicy +spec: + mitigation_mode: "standard" + signatures: "on" + bad_actors: "on" + automation_tools_detection: "on" + tls_fingerprint: "on" diff --git a/examples/custom-resources/dos/apdos-protected.yaml b/examples/custom-resources/dos/apdos-protected.yaml new file mode 100644 index 0000000000..684d6b904b --- /dev/null +++ b/examples/custom-resources/dos/apdos-protected.yaml @@ -0,0 +1,17 @@ +apiVersion: appprotectdos.f5.com/v1beta1 +kind: DosProtectedResource +metadata: + name: dos-protected +spec: + enable: true + name: "webapp.example.com" + apDosPolicy: "dospolicy" + apDosMonitor: + uri: "webapp.example.com" + protocol: "http1" + timeout: 5 + dosAccessLogDest: "127.0.0.1:5561" + dosSecurityLog: + enable: true + apDosLogConf: "doslogconf" + dosLogDest: "syslog-svc.default.svc.cluster.local:514" diff --git a/examples/custom-resources/dos/syslog.yaml b/examples/custom-resources/dos/syslog.yaml new file mode 100644 index 0000000000..de25eeb3d0 --- /dev/null +++ b/examples/custom-resources/dos/syslog.yaml @@ -0,0 +1,32 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: syslog +spec: + replicas: 1 + selector: + matchLabels: + app: syslog + template: + metadata: + labels: + app: syslog + spec: + containers: + - name: syslog + image: balabit/syslog-ng:3.28.1 + ports: + - containerPort: 514 + - containerPort: 601 +--- +apiVersion: v1 +kind: Service +metadata: + name: syslog-svc +spec: + ports: + - port: 514 + targetPort: 514 + protocol: TCP + selector: + app: syslog \ No newline at end of file diff --git a/examples/custom-resources/dos/virtual-server.yaml b/examples/custom-resources/dos/virtual-server.yaml new file mode 100644 index 0000000000..b713d08528 --- /dev/null +++ b/examples/custom-resources/dos/virtual-server.yaml @@ -0,0 +1,15 @@ +apiVersion: k8s.nginx.org/v1 +kind: VirtualServer +metadata: + name: webapp +spec: + host: webapp.example.com + upstreams: + - name: webapp + service: webapp-svc + port: 80 + routes: + - path: / + dos: dos-protected + action: + pass: webapp \ No newline at end of file diff --git a/examples/custom-resources/dos/webapp.yaml b/examples/custom-resources/dos/webapp.yaml new file mode 100644 index 0000000000..31fde92a6e --- /dev/null +++ b/examples/custom-resources/dos/webapp.yaml @@ -0,0 +1,32 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: webapp +spec: + replicas: 1 + selector: + matchLabels: + app: webapp + template: + metadata: + labels: + app: webapp + spec: + containers: + - name: webapp + image: nginxdemos/nginx-hello:plain-text + ports: + - containerPort: 8080 +--- +apiVersion: v1 +kind: Service +metadata: + name: webapp-svc +spec: + ports: + - port: 80 + targetPort: 8080 + protocol: TCP + name: http + selector: + app: webapp diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index f14bdf45cd..a4ded2c387 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -33,6 +33,6 @@ fi # instead of the $GOPATH directly. For normal projects this can be dropped. bash "${CODEGEN_PKG}"/generate-groups.sh "deepcopy,client,informer,lister" \ github.com/nginxinc/kubernetes-ingress/pkg/client github.com/nginxinc/kubernetes-ingress/pkg/apis \ - configuration:v1alpha1,v1 \ + "configuration:v1alpha1,v1 dos:v1beta1" \ --output-base "$(dirname "${BASH_SOURCE[0]}")/../../../.." \ --go-header-file "${SCRIPT_ROOT}"/hack/boilerplate.go.txt diff --git a/internal/configs/annotations.go b/internal/configs/annotations.go index a70c0a6696..777709d032 100644 --- a/internal/configs/annotations.go +++ b/internal/configs/annotations.go @@ -13,9 +13,12 @@ const AppProtectPolicyAnnotation = "appprotect.f5.com/app-protect-policy" // AppProtectLogConfAnnotation is where the NGINX AppProtect Log Configuration is specified const AppProtectLogConfAnnotation = "appprotect.f5.com/app-protect-security-log" -// AppProtectLogConfDstAnnotation is where the NGINX AppProtect Log Configuration is specified +// AppProtectLogConfDstAnnotation is where the NGINX AppProtect Log Configuration destination is specified const AppProtectLogConfDstAnnotation = "appprotect.f5.com/app-protect-security-log-destination" +// AppProtectDosProtectedAnnotation is the namespace/name reference of a DosProtectedResource +const AppProtectDosProtectedAnnotation = "appprotectdos.f5.com/app-protect-dos-resource" + // nginxMeshInternalRoute specifies if the ingress resource is an internal route. const nginxMeshInternalRouteAnnotation = "nsm.nginx.com/internal-route" @@ -46,6 +49,7 @@ var minionBlacklist = map[string]bool{ "appprotect.f5.com/app_protect_policy": true, "appprotect.f5.com/app_protect_security_log_enable": true, "appprotect.f5.com/app_protect_security_log": true, + "appprotectdos.f5.com/app-protect-dos-resource": true, } var minionInheritanceList = map[string]bool{ @@ -66,7 +70,7 @@ var minionInheritanceList = map[string]bool{ "nginx.org/fail-timeout": true, } -func parseAnnotations(ingEx *IngressEx, baseCfgParams *ConfigParams, isPlus bool, hasAppProtect bool, enableInternalRoutes bool) ConfigParams { +func parseAnnotations(ingEx *IngressEx, baseCfgParams *ConfigParams, isPlus bool, hasAppProtect bool, hasAppProtectDos bool, enableInternalRoutes bool) ConfigParams { cfgParams := *baseCfgParams if lbMethod, exists := ingEx.Ingress.Annotations["nginx.org/lb-method"]; exists { @@ -373,6 +377,11 @@ func parseAnnotations(ingEx *IngressEx, baseCfgParams *ConfigParams, isPlus bool } } + if hasAppProtectDos { + if appProtectDosResource, exists := ingEx.Ingress.Annotations["appprotectdos.f5.com/app-protect-dos-resource"]; exists { + cfgParams.AppProtectDosResource = appProtectDosResource + } + } if enableInternalRoutes { if spiffeServerCerts, exists, err := GetMapKeyAsBool(ingEx.Ingress.Annotations, nginxMeshInternalRouteAnnotation, ingEx.Ingress); exists { if err != nil { diff --git a/internal/configs/config_params.go b/internal/configs/config_params.go index 37d00698fe..b1f7705789 100644 --- a/internal/configs/config_params.go +++ b/internal/configs/config_params.go @@ -53,6 +53,9 @@ type ConfigParams struct { MainAppProtectCookieSeed string MainAppProtectCPUThresholds string MainAppProtectPhysicalMemoryThresholds string + AppProtectDosResource string + MainAppProtectDosLogFormat []string + MainAppProtectDosLogFormatEscaping string ProxyBuffering bool ProxyBuffers string ProxyBufferSize string @@ -114,6 +117,7 @@ type StaticConfigParams struct { NginxServiceMesh bool EnableInternalRoutes bool MainAppProtectLoadModule bool + MainAppProtectDosLoadModule bool PodName string EnableLatencyMetrics bool EnablePreviewPolicies bool diff --git a/internal/configs/configmaps.go b/internal/configs/configmaps.go index 7d4e219624..3da7d4a476 100644 --- a/internal/configs/configmaps.go +++ b/internal/configs/configmaps.go @@ -10,7 +10,7 @@ import ( ) // ParseConfigMap parses ConfigMap into ConfigParams. -func ParseConfigMap(cfgm *v1.ConfigMap, nginxPlus bool, hasAppProtect bool) *ConfigParams { +func ParseConfigMap(cfgm *v1.ConfigMap, nginxPlus bool, hasAppProtect bool, hasAppProtectDos bool) *ConfigParams { cfgParams := NewDefaultConfigParams(nginxPlus) if serverTokens, exists, err := GetMapKeyAsBool(cfgm.Data, "server-tokens", cfgm); exists { @@ -501,6 +501,23 @@ func ParseConfigMap(cfgm *v1.ConfigMap, nginxPlus bool, hasAppProtect bool) *Con } } + if hasAppProtectDos { + if appProtectDosLogFormat, exists, err := GetMapKeyAsStringSlice(cfgm.Data, "app-protect-dos-log-format", cfgm, "\n"); exists { + if err != nil { + glog.Error(err) + } else { + cfgParams.MainAppProtectDosLogFormat = appProtectDosLogFormat + } + } + + if appProtectDosLogFormatEscaping, exists := cfgm.Data["app-protect-dos-log-format-escaping"]; exists { + appProtectDosLogFormatEscaping = strings.TrimSpace(appProtectDosLogFormatEscaping) + if appProtectDosLogFormatEscaping != "" { + cfgParams.MainAppProtectDosLogFormatEscaping = appProtectDosLogFormatEscaping + } + } + } + return cfgParams } @@ -556,11 +573,14 @@ func GenerateNginxMainConfig(staticCfgParams *StaticConfigParams, config *Config VariablesHashBucketSize: config.VariablesHashBucketSize, VariablesHashMaxSize: config.VariablesHashMaxSize, AppProtectLoadModule: staticCfgParams.MainAppProtectLoadModule, + AppProtectDosLoadModule: staticCfgParams.MainAppProtectDosLoadModule, AppProtectFailureModeAction: config.MainAppProtectFailureModeAction, AppProtectCompressedRequestsAction: config.MainAppProtectCompressedRequestsAction, AppProtectCookieSeed: config.MainAppProtectCookieSeed, AppProtectCPUThresholds: config.MainAppProtectCPUThresholds, AppProtectPhysicalMemoryThresholds: config.MainAppProtectPhysicalMemoryThresholds, + AppProtectDosLogFormat: config.MainAppProtectDosLogFormat, + AppProtectDosLogFormatEscaping: config.MainAppProtectDosLogFormatEscaping, InternalRouteServer: staticCfgParams.EnableInternalRoutes, InternalRouteServerName: staticCfgParams.PodName, LatencyMetrics: staticCfgParams.EnableLatencyMetrics, diff --git a/internal/configs/configmaps_test.go b/internal/configs/configmaps_test.go index 54dea43cb9..1a00106222 100644 --- a/internal/configs/configmaps_test.go +++ b/internal/configs/configmaps_test.go @@ -35,13 +35,14 @@ func TestParseConfigMapWithAppProtectCompressedRequestsAction(t *testing.T) { } nginxPlus := true hasAppProtect := true + hasAppProtectDos := false for _, test := range tests { cm := &v1.ConfigMap{ Data: map[string]string{ "app-protect-compressed-requests-action": test.action, }, } - result := ParseConfigMap(cm, nginxPlus, hasAppProtect) + result := ParseConfigMap(cm, nginxPlus, hasAppProtect, hasAppProtectDos) if result.MainAppProtectCompressedRequestsAction != test.expect { t.Errorf("ParseConfigMap() returned %q but expected %q for the case %s", result.MainAppProtectCompressedRequestsAction, test.expect, test.msg) } diff --git a/internal/configs/configurator.go b/internal/configs/configurator.go index 71ff8a9548..cd4cd43db1 100644 --- a/internal/configs/configurator.go +++ b/internal/configs/configurator.go @@ -10,6 +10,8 @@ import ( "os" "strings" + "github.com/nginxinc/kubernetes-ingress/pkg/apis/dos/v1beta1" + "github.com/nginxinc/kubernetes-ingress/internal/k8s/secrets" "github.com/nginxinc/nginx-prometheus-exporter/collector" "github.com/spiffe/go-spiffe/workload" @@ -37,6 +39,8 @@ const ( appProtectLogConfFolder = "/etc/nginx/waf/nac-logconfs/" appProtectUserSigFolder = "/etc/nginx/waf/nac-usersigs/" appProtectUserSigIndex = "/etc/nginx/waf/nac-usersigs/index.conf" + appProtectDosPolicyFolder = "/etc/nginx/dos/policies/" + appProtectDosLogConfFolder = "/etc/nginx/dos/logconfs/" ) // DefaultServerSecretPath is the full path to the Secret with a TLS cert and a key for the default server. #nosec G101 @@ -264,6 +268,9 @@ func (cnf *Configurator) AddOrUpdateIngress(ingEx *IngressEx) (Warnings, error) func (cnf *Configurator) addOrUpdateIngress(ingEx *IngressEx) (Warnings, error) { apResources := cnf.updateApResources(ingEx) + cnf.updateDosResource(ingEx.DosEx) + dosResource := getAppProtectDosResource(ingEx.DosEx) + if jwtKey, exists := ingEx.Ingress.Annotations[JWTKeyAnnotation]; exists { // LocalSecretStore will not set Path if the secret is not on the filesystem. // However, NGINX configuration for an Ingress resource, to handle the case of a missing secret, @@ -272,7 +279,7 @@ func (cnf *Configurator) addOrUpdateIngress(ingEx *IngressEx) (Warnings, error) } isMinion := false - nginxCfg, warnings := generateNginxCfg(ingEx, apResources, isMinion, cnf.cfgParams, cnf.isPlus, cnf.IsResolverConfigured(), + nginxCfg, warnings := generateNginxCfg(ingEx, apResources, dosResource, isMinion, cnf.cfgParams, cnf.isPlus, cnf.IsResolverConfigured(), cnf.staticCfgParams, cnf.isWildcardEnabled) name := objectMetaToFileName(&ingEx.Ingress.ObjectMeta) content, err := cnf.templateExecutor.ExecuteIngressConfigTemplate(&nginxCfg) @@ -303,7 +310,9 @@ func (cnf *Configurator) AddOrUpdateMergeableIngress(mergeableIngs *MergeableIng } func (cnf *Configurator) addOrUpdateMergeableIngress(mergeableIngs *MergeableIngresses) (Warnings, error) { - masterApResources := cnf.updateApResources(mergeableIngs.Master) + apResources := cnf.updateApResources(mergeableIngs.Master) + cnf.updateDosResource(mergeableIngs.Master.DosEx) + dosResource := getAppProtectDosResource(mergeableIngs.Master.DosEx) // LocalSecretStore will not set Path if the secret is not on the filesystem. // However, NGINX configuration for an Ingress resource, to handle the case of a missing secret, @@ -317,7 +326,7 @@ func (cnf *Configurator) addOrUpdateMergeableIngress(mergeableIngs *MergeableIng } } - nginxCfg, warnings := generateNginxCfgForMergeableIngresses(mergeableIngs, masterApResources, cnf.cfgParams, cnf.isPlus, + nginxCfg, warnings := generateNginxCfgForMergeableIngresses(mergeableIngs, apResources, dosResource, cnf.cfgParams, cnf.isPlus, cnf.IsResolverConfigured(), cnf.staticCfgParams, cnf.isWildcardEnabled) name := objectMetaToFileName(&mergeableIngs.Master.Ingress.ObjectMeta) @@ -440,11 +449,19 @@ func (cnf *Configurator) addOrUpdateOpenTracingTracerConfig(content string) erro func (cnf *Configurator) addOrUpdateVirtualServer(virtualServerEx *VirtualServerEx) (Warnings, error) { apResources := cnf.updateApResourcesForVs(virtualServerEx) + dosResources := map[string]*appProtectDosResource{} + for k, v := range virtualServerEx.DosProtectedEx { + cnf.updateDosResource(v) + dosRes := getAppProtectDosResource(v) + if dosRes != nil { + dosResources[k] = dosRes + } + } name := getFileNameForVirtualServer(virtualServerEx.VirtualServer) vsc := newVirtualServerConfigurator(cnf.cfgParams, cnf.isPlus, cnf.IsResolverConfigured(), cnf.staticCfgParams, cnf.isWildcardEnabled) - vsCfg, warnings := vsc.GenerateVirtualServerConfig(virtualServerEx, apResources) + vsCfg, warnings := vsc.GenerateVirtualServerConfig(virtualServerEx, apResources, dosResources) content, err := cnf.templateExecutorV2.ExecuteVirtualServerTemplate(&vsCfg) if err != nil { return warnings, fmt.Errorf("Error generating VirtualServer config: %v: %w", name, err) @@ -953,7 +970,7 @@ func (cnf *Configurator) updatePlusEndpointsForTransportServer(transportServerEx } func (cnf *Configurator) updatePlusEndpoints(ingEx *IngressEx) error { - ingCfg := parseAnnotations(ingEx, cnf.cfgParams, cnf.isPlus, cnf.staticCfgParams.MainAppProtectLoadModule, cnf.staticCfgParams.EnableInternalRoutes) + ingCfg := parseAnnotations(ingEx, cnf.cfgParams, cnf.isPlus, cnf.staticCfgParams.MainAppProtectLoadModule, cnf.staticCfgParams.MainAppProtectDosLoadModule, cnf.staticCfgParams.EnableInternalRoutes) cfg := nginx.ServerConfig{ MaxFails: ingCfg.MaxFails, @@ -1262,46 +1279,59 @@ func createSpiffeCert(certs []*x509.Certificate) []byte { return pemData } -func (cnf *Configurator) updateApResources(ingEx *IngressEx) (apRes AppProtectResources) { +func (cnf *Configurator) updateApResources(ingEx *IngressEx) *AppProtectResources { + var apResources AppProtectResources + if ingEx.AppProtectPolicy != nil { policyFileName := appProtectPolicyFileNameFromUnstruct(ingEx.AppProtectPolicy) policyContent := generateApResourceFileContent(ingEx.AppProtectPolicy) cnf.nginxManager.CreateAppProtectResourceFile(policyFileName, policyContent) - apRes.AppProtectPolicy = policyFileName + apResources.AppProtectPolicy = policyFileName } for _, logConf := range ingEx.AppProtectLogs { logConfFileName := appProtectLogConfFileNameFromUnstruct(logConf.LogConf) logConfContent := generateApResourceFileContent(logConf.LogConf) cnf.nginxManager.CreateAppProtectResourceFile(logConfFileName, logConfContent) - apRes.AppProtectLogconfs = append(apRes.AppProtectLogconfs, logConfFileName+" "+logConf.Dest) + apResources.AppProtectLogconfs = append(apResources.AppProtectLogconfs, logConfFileName+" "+logConf.Dest) } - return apRes + return &apResources } -func (cnf *Configurator) updateApResourcesForVs(vsEx *VirtualServerEx) map[string]string { - apRes := make(map[string]string) - - if vsEx.ApPolRefs != nil { - for apPolKey, apPol := range vsEx.ApPolRefs { - policyFileName := appProtectPolicyFileNameFromUnstruct(apPol) - policyContent := generateApResourceFileContent(apPol) +func (cnf *Configurator) updateDosResource(dosEx *DosEx) { + if dosEx != nil { + if dosEx.DosPolicy != nil { + policyFileName := appProtectDosPolicyFileName(dosEx.DosPolicy.GetNamespace(), dosEx.DosPolicy.GetName()) + policyContent := generateApResourceFileContent(dosEx.DosPolicy) cnf.nginxManager.CreateAppProtectResourceFile(policyFileName, policyContent) - apRes[apPolKey] = policyFileName } - } - - if vsEx.LogConfRefs != nil { - for logConfKey, logConf := range vsEx.LogConfRefs { - logConfFileName := appProtectLogConfFileNameFromUnstruct(logConf) - logConfContent := generateApResourceFileContent(logConf) + if dosEx.DosLogConf != nil { + logConfFileName := appProtectDosLogConfFileName(dosEx.DosLogConf.GetNamespace(), dosEx.DosLogConf.GetName()) + logConfContent := generateApResourceFileContent(dosEx.DosLogConf) cnf.nginxManager.CreateAppProtectResourceFile(logConfFileName, logConfContent) - apRes[logConfKey] = logConfFileName } } +} + +func (cnf *Configurator) updateApResourcesForVs(vsEx *VirtualServerEx) *appProtectResourcesForVS { + resources := newAppProtectVSResourcesForVS() + + for apPolKey, apPol := range vsEx.ApPolRefs { + policyFileName := appProtectPolicyFileNameFromUnstruct(apPol) + policyContent := generateApResourceFileContent(apPol) + cnf.nginxManager.CreateAppProtectResourceFile(policyFileName, policyContent) + resources.Policies[apPolKey] = policyFileName + } + + for logConfKey, logConf := range vsEx.LogConfRefs { + logConfFileName := appProtectLogConfFileNameFromUnstruct(logConf) + logConfContent := generateApResourceFileContent(logConf) + cnf.nginxManager.CreateAppProtectResourceFile(logConfFileName, logConfContent) + resources.LogConfs[logConfKey] = logConfFileName + } - return apRes + return resources } func appProtectPolicyFileNameFromUnstruct(unst *unstructured.Unstructured) string { @@ -1316,6 +1346,13 @@ func appProtectUserSigFileNameFromUnstruct(unst *unstructured.Unstructured) stri return fmt.Sprintf("%s%s_%s", appProtectUserSigFolder, unst.GetNamespace(), unst.GetName()) } +func generateDosLogDest(dest string) string { + if dest == "stderr" { + return dest + } + return "syslog:server=" + dest +} + func generateApResourceFileContent(apResource *unstructured.Unstructured) []byte { // Safe to ignore errors since validation already checked those spec, _, _ := unstructured.NestedMap(apResource.Object, "spec") @@ -1323,49 +1360,40 @@ func generateApResourceFileContent(apResource *unstructured.Unstructured) []byte return data } -// AddOrUpdateAppProtectResource updates Ingresses and VirtualServers that use App Protect Resources -func (cnf *Configurator) AddOrUpdateAppProtectResource(resource *unstructured.Unstructured, ingExes []*IngressEx, mergeableIngresses []*MergeableIngresses, vsExes []*VirtualServerEx) (Warnings, error) { - allWarnings := newWarnings() +// ResourceOperation represents a function that changes configuration in relation to an unstructured resource. +type ResourceOperation func(resource *v1beta1.DosProtectedResource, ingExes []*IngressEx, mergeableIngresses []*MergeableIngresses, vsExes []*VirtualServerEx) (Warnings, error) - for _, ingEx := range ingExes { - warnings, err := cnf.addOrUpdateIngress(ingEx) - if err != nil { - return allWarnings, fmt.Errorf("Error adding or updating ingress %v/%v: %w", ingEx.Ingress.Namespace, ingEx.Ingress.Name, err) - } - allWarnings.Add(warnings) +// AddOrUpdateAppProtectResource updates Ingresses and VirtualServers that use App Protect or App Protect DoS resources. +func (cnf *Configurator) AddOrUpdateAppProtectResource(resource *unstructured.Unstructured, ingExes []*IngressEx, mergeableIngresses []*MergeableIngresses, vsExes []*VirtualServerEx) (Warnings, error) { + warnings, err := cnf.addOrUpdateIngressesAndVirtualServers(ingExes, mergeableIngresses, vsExes) + if err != nil { + return warnings, fmt.Errorf("Error when updating %v %v/%v: %w", resource.GetKind(), resource.GetNamespace(), resource.GetName(), err) } - for _, m := range mergeableIngresses { - warnings, err := cnf.addOrUpdateMergeableIngress(m) - if err != nil { - return allWarnings, fmt.Errorf("Error adding or updating mergeableIngress %v/%v: %w", m.Master.Ingress.Namespace, m.Master.Ingress.Name, err) - } - allWarnings.Add(warnings) + err = cnf.reload(nginx.ReloadForOtherUpdate) + if err != nil { + return warnings, fmt.Errorf("Error when reloading NGINX when updating %v %v/%v: %w", resource.GetKind(), resource.GetNamespace(), resource.GetName(), err) } - for _, vs := range vsExes { - warnings, err := cnf.addOrUpdateVirtualServer(vs) - if err != nil { - return allWarnings, fmt.Errorf("Error adding or updating VirtualServer %v/%v: %w", vs.VirtualServer.Namespace, vs.VirtualServer.Name, err) - } - allWarnings.Add(warnings) + return warnings, nil +} + +// AddOrUpdateResourcesThatUseDosProtected updates Ingresses and VirtualServers that use DoS resources. +func (cnf *Configurator) AddOrUpdateResourcesThatUseDosProtected(ingExes []*IngressEx, mergeableIngresses []*MergeableIngresses, vsExes []*VirtualServerEx) (Warnings, error) { + warnings, err := cnf.addOrUpdateIngressesAndVirtualServers(ingExes, mergeableIngresses, vsExes) + if err != nil { + return warnings, fmt.Errorf("error when updating resources that use Dos: %w", err) } - if err := cnf.reload(nginx.ReloadForOtherUpdate); err != nil { - return allWarnings, fmt.Errorf("Error when reloading NGINX when updating %v: %w", resource.GetKind(), err) + err = cnf.reload(nginx.ReloadForOtherUpdate) + if err != nil { + return warnings, fmt.Errorf("error when updating resources that use Dos: %w", err) } - return allWarnings, nil + return warnings, nil } -// DeleteAppProtectPolicy updates Ingresses and VirtualServers that use AP Policy after that policy is deleted -func (cnf *Configurator) DeleteAppProtectPolicy(polNamespaceName string, ingExes []*IngressEx, mergeableIngresses []*MergeableIngresses, vsExes []*VirtualServerEx) (Warnings, error) { - if len(ingExes)+len(mergeableIngresses)+len(vsExes) > 0 { - fName := strings.Replace(polNamespaceName, "/", "_", 1) - polFileName := appProtectPolicyFolder + fName - cnf.nginxManager.DeleteAppProtectResourceFile(polFileName) - } - +func (cnf *Configurator) addOrUpdateIngressesAndVirtualServers(ingExes []*IngressEx, mergeableIngresses []*MergeableIngresses, vsExes []*VirtualServerEx) (Warnings, error) { allWarnings := newWarnings() for _, ingEx := range ingExes { @@ -1384,88 +1412,43 @@ func (cnf *Configurator) DeleteAppProtectPolicy(polNamespaceName string, ingExes allWarnings.Add(warnings) } - for _, v := range vsExes { - warnings, err := cnf.addOrUpdateVirtualServer(v) + for _, vs := range vsExes { + warnings, err := cnf.addOrUpdateVirtualServer(vs) if err != nil { - return allWarnings, fmt.Errorf("Error adding or updating VirtualServer %v/%v: %w", v.VirtualServer.Namespace, v.VirtualServer.Name, err) + return allWarnings, fmt.Errorf("Error adding or updating VirtualServer %v/%v: %w", vs.VirtualServer.Namespace, vs.VirtualServer.Name, err) } allWarnings.Add(warnings) } - if err := cnf.reload(nginx.ReloadForOtherUpdate); err != nil { - return allWarnings, fmt.Errorf("Error when reloading NGINX when removing App Protect Policy: %w", err) - } - return allWarnings, nil } -// DeleteAppProtectLogConf updates Ingresses and VirtualServers that use AP Log Configuration after that policy is deleted -func (cnf *Configurator) DeleteAppProtectLogConf(logConfNamespaceName string, ingExes []*IngressEx, mergeableIngresses []*MergeableIngresses, vsExes []*VirtualServerEx) (Warnings, error) { +// DeleteAppProtectPolicy updates Ingresses and VirtualServers that use AP Policy after that policy is deleted +func (cnf *Configurator) DeleteAppProtectPolicy(resource *unstructured.Unstructured, ingExes []*IngressEx, mergeableIngresses []*MergeableIngresses, vsExes []*VirtualServerEx) (Warnings, error) { if len(ingExes)+len(mergeableIngresses)+len(vsExes) > 0 { - fName := strings.Replace(logConfNamespaceName, "/", "_", 1) - logConfFileName := appProtectLogConfFolder + fName - cnf.nginxManager.DeleteAppProtectResourceFile(logConfFileName) - } - allWarnings := newWarnings() - - for _, ingEx := range ingExes { - warnings, err := cnf.addOrUpdateIngress(ingEx) - if err != nil { - return allWarnings, fmt.Errorf("Error adding or updating ingress %v/%v: %w", ingEx.Ingress.Namespace, ingEx.Ingress.Name, err) - } - allWarnings.Add(warnings) - } - - for _, m := range mergeableIngresses { - warnings, err := cnf.addOrUpdateMergeableIngress(m) - if err != nil { - return allWarnings, fmt.Errorf("Error adding or updating mergeableIngress %v/%v: %w", m.Master.Ingress.Namespace, m.Master.Ingress.Name, err) - } - allWarnings.Add(warnings) + cnf.nginxManager.DeleteAppProtectResourceFile(appProtectPolicyFileNameFromUnstruct(resource)) } - for _, v := range vsExes { - warnings, err := cnf.addOrUpdateVirtualServer(v) - if err != nil { - return allWarnings, fmt.Errorf("Error adding or updating VirtualServer %v/%v: %w", v.VirtualServer.Namespace, v.VirtualServer.Name, err) - } - allWarnings.Add(warnings) - } + return cnf.AddOrUpdateAppProtectResource(resource, ingExes, mergeableIngresses, vsExes) +} - if err := cnf.reload(nginx.ReloadForOtherUpdate); err != nil { - return allWarnings, fmt.Errorf("Error when reloading NGINX when removing App Protect Log Configuration: %w", err) +// DeleteAppProtectLogConf updates Ingresses and VirtualServers that use AP Log Configuration after that policy is deleted +func (cnf *Configurator) DeleteAppProtectLogConf(resource *unstructured.Unstructured, ingExes []*IngressEx, mergeableIngresses []*MergeableIngresses, vsExes []*VirtualServerEx) (Warnings, error) { + if len(ingExes)+len(mergeableIngresses)+len(vsExes) > 0 { + cnf.nginxManager.DeleteAppProtectResourceFile(appProtectLogConfFileNameFromUnstruct(resource)) } - return allWarnings, nil + return cnf.AddOrUpdateAppProtectResource(resource, ingExes, mergeableIngresses, vsExes) } // RefreshAppProtectUserSigs writes all valid UDS files to fs and reloads NGINX func (cnf *Configurator) RefreshAppProtectUserSigs( userSigs []*unstructured.Unstructured, delPols []string, ingExes []*IngressEx, mergeableIngresses []*MergeableIngresses, vsExes []*VirtualServerEx, ) (Warnings, error) { - allWarnings := newWarnings() - for _, ingEx := range ingExes { - warnings, err := cnf.addOrUpdateIngress(ingEx) - if err != nil { - return allWarnings, fmt.Errorf("Error adding or updating ingress %v/%v: %w", ingEx.Ingress.Namespace, ingEx.Ingress.Name, err) - } - allWarnings.Add(warnings) - } - for _, m := range mergeableIngresses { - warnings, err := cnf.addOrUpdateMergeableIngress(m) - if err != nil { - return allWarnings, fmt.Errorf("Error adding or updating mergeableIngress %v/%v: %w", m.Master.Ingress.Namespace, m.Master.Ingress.Name, err) - } - allWarnings.Add(warnings) - } - - for _, v := range vsExes { - warnings, err := cnf.addOrUpdateVirtualServer(v) - if err != nil { - return allWarnings, fmt.Errorf("Error adding or updating VirtualServer %v/%v: %w", v.VirtualServer.Namespace, v.VirtualServer.Name, err) - } - allWarnings.Add(warnings) + allWarnings, err := cnf.addOrUpdateIngressesAndVirtualServers(ingExes, mergeableIngresses, vsExes) + if err != nil { + return allWarnings, err } for _, file := range delPols { @@ -1484,6 +1467,24 @@ func (cnf *Configurator) RefreshAppProtectUserSigs( return allWarnings, cnf.reload(nginx.ReloadForOtherUpdate) } +func appProtectDosPolicyFileName(namespace string, name string) string { + return fmt.Sprintf("%s%s_%s.json", appProtectDosPolicyFolder, namespace, name) +} + +func appProtectDosLogConfFileName(namespace string, name string) string { + return fmt.Sprintf("%s%s_%s.json", appProtectDosLogConfFolder, namespace, name) +} + +// DeleteAppProtectDosPolicy updates Ingresses and VirtualServers that use AP Dos Policy after that policy is deleted +func (cnf *Configurator) DeleteAppProtectDosPolicy(resource *unstructured.Unstructured) { + cnf.nginxManager.DeleteAppProtectResourceFile(appProtectDosPolicyFileName(resource.GetNamespace(), resource.GetName())) +} + +// DeleteAppProtectDosLogConf updates Ingresses and VirtualServers that use AP Log Configuration after that policy is deleted +func (cnf *Configurator) DeleteAppProtectDosLogConf(resource *unstructured.Unstructured) { + cnf.nginxManager.DeleteAppProtectResourceFile(appProtectDosLogConfFileName(resource.GetNamespace(), resource.GetName())) +} + // AddInternalRouteConfig adds internal route server to NGINX Configuration and reloads NGINX func (cnf *Configurator) AddInternalRouteConfig() error { cnf.staticCfgParams.EnableInternalRoutes = true diff --git a/internal/configs/configurator_test.go b/internal/configs/configurator_test.go index 75e37f700b..8d7cf01e87 100644 --- a/internal/configs/configurator_test.go +++ b/internal/configs/configurator_test.go @@ -5,6 +5,7 @@ import ( "reflect" "testing" + "github.com/google/go-cmp/cmp" "github.com/prometheus/client_golang/prometheus" networking "k8s.io/api/networking/v1" meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -1105,7 +1106,7 @@ func TestUpdateApResources(t *testing.T) { tests := []struct { ingEx *IngressEx - expected AppProtectResources + expected *AppProtectResources msg string }{ { @@ -1114,7 +1115,7 @@ func TestUpdateApResources(t *testing.T) { ObjectMeta: meta_v1.ObjectMeta{}, }, }, - expected: AppProtectResources{}, + expected: &AppProtectResources{}, msg: "no app protect resources", }, { @@ -1124,7 +1125,7 @@ func TestUpdateApResources(t *testing.T) { }, AppProtectPolicy: appProtectPolicy, }, - expected: AppProtectResources{ + expected: &AppProtectResources{ AppProtectPolicy: "/etc/nginx/waf/nac-policies/test-ns_test-name", }, msg: "app protect policy", @@ -1141,7 +1142,7 @@ func TestUpdateApResources(t *testing.T) { }, }, }, - expected: AppProtectResources{ + expected: &AppProtectResources{ AppProtectLogconfs: []string{"/etc/nginx/waf/nac-logconfs/test-ns_test-name test-dst"}, }, msg: "app protect log conf", @@ -1159,7 +1160,7 @@ func TestUpdateApResources(t *testing.T) { }, }, }, - expected: AppProtectResources{ + expected: &AppProtectResources{ AppProtectPolicy: "/etc/nginx/waf/nac-policies/test-ns_test-name", AppProtectLogconfs: []string{"/etc/nginx/waf/nac-logconfs/test-ns_test-name test-dst"}, }, @@ -1175,7 +1176,7 @@ func TestUpdateApResources(t *testing.T) { for _, test := range tests { result := conf.updateApResources(test.ingEx) if !reflect.DeepEqual(result, test.expected) { - t.Errorf("updateApResources() returned \n%v but exexpected\n%v for the case of %s", result, test.expected, test.msg) + t.Errorf("updateApResources() returned \n%v but expected\n%v for the case of %s", result, test.expected, test.msg) } } } @@ -1220,7 +1221,7 @@ func TestUpdateApResourcesForVs(t *testing.T) { tests := []struct { vsEx *VirtualServerEx - expected map[string]string + expected *appProtectResourcesForVS msg string }{ { @@ -1229,8 +1230,11 @@ func TestUpdateApResourcesForVs(t *testing.T) { ObjectMeta: meta_v1.ObjectMeta{}, }, }, - expected: map[string]string{}, - msg: "no app protect resources", + expected: &appProtectResourcesForVS{ + Policies: map[string]string{}, + LogConfs: map[string]string{}, + }, + msg: "no app protect resources", }, { vsEx: &VirtualServerEx{ @@ -1239,9 +1243,12 @@ func TestUpdateApResourcesForVs(t *testing.T) { }, ApPolRefs: apPolRefs, }, - expected: map[string]string{ - "test-ns-1/test-name-1": "/etc/nginx/waf/nac-policies/test-ns-1_test-name-1", - "test-ns-2/test-name-2": "/etc/nginx/waf/nac-policies/test-ns-2_test-name-2", + expected: &appProtectResourcesForVS{ + Policies: map[string]string{ + "test-ns-1/test-name-1": "/etc/nginx/waf/nac-policies/test-ns-1_test-name-1", + "test-ns-2/test-name-2": "/etc/nginx/waf/nac-policies/test-ns-2_test-name-2", + }, + LogConfs: map[string]string{}, }, msg: "app protect policies", }, @@ -1252,9 +1259,12 @@ func TestUpdateApResourcesForVs(t *testing.T) { }, LogConfRefs: logConfRefs, }, - expected: map[string]string{ - "test-ns-1/test-name-1": "/etc/nginx/waf/nac-logconfs/test-ns-1_test-name-1", - "test-ns-2/test-name-2": "/etc/nginx/waf/nac-logconfs/test-ns-2_test-name-2", + expected: &appProtectResourcesForVS{ + Policies: map[string]string{}, + LogConfs: map[string]string{ + "test-ns-1/test-name-1": "/etc/nginx/waf/nac-logconfs/test-ns-1_test-name-1", + "test-ns-2/test-name-2": "/etc/nginx/waf/nac-logconfs/test-ns-2_test-name-2", + }, }, msg: "app protect log confs", }, @@ -1266,10 +1276,15 @@ func TestUpdateApResourcesForVs(t *testing.T) { ApPolRefs: apPolRefs, LogConfRefs: logConfRefs, }, - expected: map[string]string{ - // this is a bug - the result needs to include both policies and log confs - "test-ns-1/test-name-1": "/etc/nginx/waf/nac-logconfs/test-ns-1_test-name-1", - "test-ns-2/test-name-2": "/etc/nginx/waf/nac-logconfs/test-ns-2_test-name-2", + expected: &appProtectResourcesForVS{ + Policies: map[string]string{ + "test-ns-1/test-name-1": "/etc/nginx/waf/nac-policies/test-ns-1_test-name-1", + "test-ns-2/test-name-2": "/etc/nginx/waf/nac-policies/test-ns-2_test-name-2", + }, + LogConfs: map[string]string{ + "test-ns-1/test-name-1": "/etc/nginx/waf/nac-logconfs/test-ns-1_test-name-1", + "test-ns-2/test-name-2": "/etc/nginx/waf/nac-logconfs/test-ns-2_test-name-2", + }, }, msg: "app protect policies and log confs", }, @@ -1282,8 +1297,8 @@ func TestUpdateApResourcesForVs(t *testing.T) { for _, test := range tests { result := conf.updateApResourcesForVs(test.vsEx) - if !reflect.DeepEqual(result, test.expected) { - t.Errorf("updateApResourcesForVs() returned \n%v but exexpected\n%v for the case of %s", result, test.expected, test.msg) + if diff := cmp.Diff(test.expected, result); diff != "" { + t.Errorf("updateApResourcesForVs() '%s' mismatch (-want +got):\n%s", test.msg, diff) } } } diff --git a/internal/configs/dos.go b/internal/configs/dos.go new file mode 100644 index 0000000000..552da85d34 --- /dev/null +++ b/internal/configs/dos.go @@ -0,0 +1,49 @@ +package configs + +// appProtectDosResource holds the file names of APDosPolicy and APDosLogConf resources used in an Ingress resource. +type appProtectDosResource struct { + AppProtectDosEnable string + AppProtectDosLogEnable bool + AppProtectDosMonitorUri string + AppProtectDosMonitorProtocol string + AppProtectDosMonitorTimeout uint64 + AppProtectDosName string + AppProtectDosAccessLogDst string + AppProtectDosPolicyFile string + AppProtectDosLogConfFile string +} + +func getAppProtectDosResource(dosEx *DosEx) *appProtectDosResource { + var dosResource appProtectDosResource + if dosEx == nil || dosEx.DosProtected == nil { + return nil + } + + protected := dosEx.DosProtected + dosResource.AppProtectDosEnable = "off" + if protected.Spec.Enable { + dosResource.AppProtectDosEnable = "on" + } + dosResource.AppProtectDosName = protected.Namespace + "/" + protected.Name + "/" + protected.Spec.Name + + if protected.Spec.ApDosMonitor != nil { + dosResource.AppProtectDosMonitorUri = protected.Spec.ApDosMonitor.Uri + dosResource.AppProtectDosMonitorProtocol = protected.Spec.ApDosMonitor.Protocol + dosResource.AppProtectDosMonitorTimeout = protected.Spec.ApDosMonitor.Timeout + } + + dosResource.AppProtectDosAccessLogDst = generateDosLogDest(protected.Spec.DosAccessLogDest) + + if dosEx.DosPolicy != nil { + dosResource.AppProtectDosPolicyFile = appProtectDosPolicyFileName(dosEx.DosPolicy.GetNamespace(), dosEx.DosPolicy.GetName()) + } + + if dosEx.DosLogConf != nil { + log := dosEx.DosLogConf + logConfFileName := appProtectDosLogConfFileName(log.GetNamespace(), log.GetName()) + dosResource.AppProtectDosLogConfFile = logConfFileName + " " + generateDosLogDest(protected.Spec.DosSecurityLog.DosLogDest) + dosResource.AppProtectDosLogEnable = protected.Spec.DosSecurityLog.Enable + } + + return &dosResource +} diff --git a/internal/configs/dos_test.go b/internal/configs/dos_test.go new file mode 100644 index 0000000000..fe7f14db67 --- /dev/null +++ b/internal/configs/dos_test.go @@ -0,0 +1,137 @@ +package configs + +import ( + "reflect" + "testing" + + "github.com/nginxinc/kubernetes-ingress/pkg/apis/dos/v1beta1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func TestUpdateApDosResource(t *testing.T) { + appProtectDosPolicy := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "namespace": "test-ns", + "name": "test-name", + }, + }, + } + appProtectDosLogConf := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "namespace": "test-ns", + "name": "test-name", + }, + "spec": map[string]interface{}{ + "enable": true, + "name": "dos-protected", + "apDosMonitor": "example.com", + "dosAccessLogDest": "127.0.0.1:5561", + }, + }, + } + appProtectDosProtected := &v1beta1.DosProtectedResource{ + TypeMeta: v1.TypeMeta{}, + ObjectMeta: v1.ObjectMeta{ + Name: "dosOnly", + Namespace: "test-ns", + }, + Spec: v1beta1.DosProtectedResourceSpec{ + Enable: true, + Name: "dos-protected", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "127.0.0.1:5561", + }, + } + appProtectDosProtectedWithLog := &v1beta1.DosProtectedResource{ + TypeMeta: v1.TypeMeta{}, + ObjectMeta: v1.ObjectMeta{ + Name: "dosWithLogConf", + Namespace: "test-ns", + }, + Spec: v1beta1.DosProtectedResourceSpec{ + Enable: true, + Name: "dos-protected", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "127.0.0.1:5561", + DosSecurityLog: &v1beta1.DosSecurityLog{ + Enable: true, + ApDosLogConf: "dosLogConf", + DosLogDest: "syslog-svc.default.svc.cluster.local:514", + }, + }, + } + + tests := []struct { + dosProtectedEx *DosEx + expected *appProtectDosResource + msg string + }{ + { + dosProtectedEx: nil, + expected: nil, + msg: "nil app protect dos resources", + }, + { + dosProtectedEx: &DosEx{}, + expected: nil, + msg: "empty app protect dos resources", + }, + { + dosProtectedEx: &DosEx{ + DosProtected: appProtectDosProtected, + }, + expected: &appProtectDosResource{ + AppProtectDosEnable: "on", + AppProtectDosName: "test-ns/dosOnly/dos-protected", + AppProtectDosMonitorUri: "example.com", + AppProtectDosAccessLogDst: "syslog:server=127.0.0.1:5561", + }, + msg: "app protect basic protected config", + }, + { + dosProtectedEx: &DosEx{ + DosProtected: appProtectDosProtected, + DosPolicy: appProtectDosPolicy, + }, + expected: &appProtectDosResource{ + AppProtectDosEnable: "on", + AppProtectDosName: "test-ns/dosOnly/dos-protected", + AppProtectDosMonitorUri: "example.com", + AppProtectDosAccessLogDst: "syslog:server=127.0.0.1:5561", + AppProtectDosPolicyFile: "/etc/nginx/dos/policies/test-ns_test-name.json", + }, + msg: "app protect dos policy", + }, + { + dosProtectedEx: &DosEx{ + DosProtected: appProtectDosProtectedWithLog, + DosPolicy: appProtectDosPolicy, + DosLogConf: appProtectDosLogConf, + }, + expected: &appProtectDosResource{ + AppProtectDosEnable: "on", + AppProtectDosName: "test-ns/dosWithLogConf/dos-protected", + AppProtectDosMonitorUri: "example.com", + AppProtectDosAccessLogDst: "syslog:server=127.0.0.1:5561", + AppProtectDosPolicyFile: "/etc/nginx/dos/policies/test-ns_test-name.json", + AppProtectDosLogEnable: true, + AppProtectDosLogConfFile: "/etc/nginx/dos/logconfs/test-ns_test-name.json syslog:server=syslog-svc.default.svc.cluster.local:514", + }, + msg: "app protect dos policy and log conf", + }, + } + + for _, test := range tests { + result := getAppProtectDosResource(test.dosProtectedEx) + if !reflect.DeepEqual(result, test.expected) { + t.Errorf("getAppProtectDosResource() returned:\n%v\nbut expected:\n%v\n for the case of '%s'", result, test.expected, test.msg) + } + } +} diff --git a/internal/configs/ingress.go b/internal/configs/ingress.go index fcf402c653..33cd57aa2c 100644 --- a/internal/configs/ingress.go +++ b/internal/configs/ingress.go @@ -6,6 +6,8 @@ import ( "strconv" "strings" + "github.com/nginxinc/kubernetes-ingress/pkg/apis/dos/v1beta1" + "github.com/golang/glog" "github.com/nginxinc/kubernetes-ingress/internal/k8s/secrets" api_v1 "k8s.io/api/core/v1" @@ -42,9 +44,16 @@ type IngressEx struct { ValidMinionPaths map[string]bool AppProtectPolicy *unstructured.Unstructured AppProtectLogs []AppProtectLog + DosEx *DosEx SecretRefs map[string]*secrets.SecretReference } +type DosEx struct { + DosProtected *v1beta1.DosProtectedResource + DosPolicy *unstructured.Unstructured + DosLogConf *unstructured.Unstructured +} + // JWTKey represents a secret that holds JSON Web Key. type JWTKey struct { Name string @@ -65,10 +74,12 @@ type MergeableIngresses struct { Minions []*IngressEx } -func generateNginxCfg(ingEx *IngressEx, apResources AppProtectResources, isMinion bool, baseCfgParams *ConfigParams, isPlus bool, - isResolverConfigured bool, staticParams *StaticConfigParams, isWildcardEnabled bool) (version1.IngressNginxConfig, Warnings) { +func generateNginxCfg(ingEx *IngressEx, apResources *AppProtectResources, dosResource *appProtectDosResource, isMinion bool, + baseCfgParams *ConfigParams, isPlus bool, isResolverConfigured bool, staticParams *StaticConfigParams, isWildcardEnabled bool) (version1.IngressNginxConfig, Warnings) { hasAppProtect := staticParams.MainAppProtectLoadModule - cfgParams := parseAnnotations(ingEx, baseCfgParams, isPlus, hasAppProtect, staticParams.EnableInternalRoutes) + hasAppProtectDos := staticParams.MainAppProtectDosLoadModule + + cfgParams := parseAnnotations(ingEx, baseCfgParams, isPlus, hasAppProtect, hasAppProtectDos, staticParams.EnableInternalRoutes) wsServices := getWebsocketServices(ingEx) spServices := getSessionPersistenceServices(ingEx) @@ -153,6 +164,18 @@ func generateNginxCfg(ingEx *IngressEx, apResources AppProtectResources, isMinio server.AppProtectLogConfs = apResources.AppProtectLogconfs } + if hasAppProtectDos && dosResource != nil { + server.AppProtectDosEnable = dosResource.AppProtectDosEnable + server.AppProtectDosLogEnable = dosResource.AppProtectDosLogEnable + server.AppProtectDosMonitorUri = dosResource.AppProtectDosMonitorUri + server.AppProtectDosMonitorProtocol = dosResource.AppProtectDosMonitorProtocol + server.AppProtectDosMonitorTimeout = dosResource.AppProtectDosMonitorTimeout + server.AppProtectDosName = dosResource.AppProtectDosName + server.AppProtectDosAccessLogDst = dosResource.AppProtectDosAccessLogDst + server.AppProtectDosPolicyFile = dosResource.AppProtectDosPolicyFile + server.AppProtectDosLogConfFile = dosResource.AppProtectDosLogConfFile + } + if !isMinion && cfgParams.JWTKey != "" { jwtAuth, redirectLoc, warnings := generateJWTConfig(ingEx.Ingress, ingEx.SecretRefs, &cfgParams, getNameForRedirectLocation(ingEx.Ingress)) server.JWTAuth = jwtAuth @@ -510,9 +533,9 @@ func upstreamMapToSlice(upstreams map[string]version1.Upstream) []version1.Upstr return result } -func generateNginxCfgForMergeableIngresses(mergeableIngs *MergeableIngresses, masterApResources AppProtectResources, - baseCfgParams *ConfigParams, isPlus bool, isResolverConfigured bool, staticParams *StaticConfigParams, - isWildcardEnabled bool) (version1.IngressNginxConfig, Warnings) { +func generateNginxCfgForMergeableIngresses(mergeableIngs *MergeableIngresses, apResources *AppProtectResources, + dosResource *appProtectDosResource, baseCfgParams *ConfigParams, isPlus bool, isResolverConfigured bool, + staticParams *StaticConfigParams, isWildcardEnabled bool) (version1.IngressNginxConfig, Warnings) { var masterServer version1.Server var locations []version1.Location @@ -531,7 +554,7 @@ func generateNginxCfgForMergeableIngresses(mergeableIngs *MergeableIngresses, ma } isMinion := false - masterNginxCfg, warnings := generateNginxCfg(mergeableIngs.Master, masterApResources, isMinion, baseCfgParams, isPlus, isResolverConfigured, staticParams, isWildcardEnabled) + masterNginxCfg, warnings := generateNginxCfg(mergeableIngs.Master, apResources, dosResource, isMinion, baseCfgParams, isPlus, isResolverConfigured, staticParams, isWildcardEnabled) // because mergeableIngs.Master.Ingress is a deepcopy of the original master // we need to change the key in the warnings to the original master @@ -569,8 +592,9 @@ func generateNginxCfgForMergeableIngresses(mergeableIngs *MergeableIngresses, ma isMinion := true // App Protect Resources not allowed in minions - pass empty struct - dummyApResources := AppProtectResources{} - nginxCfg, minionWarnings := generateNginxCfg(minion, dummyApResources, isMinion, baseCfgParams, isPlus, isResolverConfigured, staticParams, isWildcardEnabled) + dummyApResources := &AppProtectResources{} + dummyDosResource := &appProtectDosResource{} + nginxCfg, minionWarnings := generateNginxCfg(minion, dummyApResources, dummyDosResource, isMinion, baseCfgParams, isPlus, isResolverConfigured, staticParams, isWildcardEnabled) warnings.Add(minionWarnings) // because minion.Ingress is a deepcopy of the original minion diff --git a/internal/configs/ingress_test.go b/internal/configs/ingress_test.go index 7c9c147b03..3c603ee898 100644 --- a/internal/configs/ingress_test.go +++ b/internal/configs/ingress_test.go @@ -22,8 +22,7 @@ func TestGenerateNginxCfg(t *testing.T) { expected := createExpectedConfigForCafeIngressEx(isPlus) - apRes := AppProtectResources{} - result, warnings := generateNginxCfg(&cafeIngressEx, apRes, false, configParams, isPlus, false, &StaticConfigParams{}, false) + result, warnings := generateNginxCfg(&cafeIngressEx, nil, nil, false, configParams, isPlus, false, &StaticConfigParams{}, false) if diff := cmp.Diff(expected, result); diff != "" { t.Errorf("generateNginxCfg() returned unexpected result (-want +got):\n%s", diff) @@ -63,8 +62,7 @@ func TestGenerateNginxCfgForJWT(t *testing.T) { }, } - apRes := AppProtectResources{} - result, warnings := generateNginxCfg(&cafeIngressEx, apRes, false, configParams, true, false, &StaticConfigParams{}, false) + result, warnings := generateNginxCfg(&cafeIngressEx, nil, nil, false, configParams, true, false, &StaticConfigParams{}, false) if !reflect.DeepEqual(result.Servers[0].JWTAuth, expected.Servers[0].JWTAuth) { t.Errorf("generateNginxCfg returned \n%v, but expected \n%v", result.Servers[0].JWTAuth, expected.Servers[0].JWTAuth) @@ -82,8 +80,7 @@ func TestGenerateNginxCfgWithMissingTLSSecret(t *testing.T) { cafeIngressEx.SecretRefs["cafe-secret"].Error = errors.New("secret doesn't exist") configParams := NewDefaultConfigParams(false) - apRes := AppProtectResources{} - result, resultWarnings := generateNginxCfg(&cafeIngressEx, apRes, false, configParams, false, false, &StaticConfigParams{}, false) + result, resultWarnings := generateNginxCfg(&cafeIngressEx, nil, nil, false, configParams, false, false, &StaticConfigParams{}, false) expectedSSLRejectHandshake := true expectedWarnings := Warnings{ @@ -106,8 +103,7 @@ func TestGenerateNginxCfgWithWildcardTLSSecret(t *testing.T) { cafeIngressEx.Ingress.Spec.TLS[0].SecretName = "" configParams := NewDefaultConfigParams(false) - apRes := AppProtectResources{} - result, warnings := generateNginxCfg(&cafeIngressEx, apRes, false, configParams, false, false, &StaticConfigParams{}, true) + result, warnings := generateNginxCfg(&cafeIngressEx, nil, nil, false, configParams, false, false, &StaticConfigParams{}, true) resultServer := result.Servers[0] if !reflect.DeepEqual(resultServer.SSLCertificate, pemFileNameForWildcardTLSSecret) { @@ -362,8 +358,7 @@ func TestGenerateNginxCfgForMergeableIngresses(t *testing.T) { configParams := NewDefaultConfigParams(isPlus) - masterApRes := AppProtectResources{} - result, warnings := generateNginxCfgForMergeableIngresses(mergeableIngresses, masterApRes, configParams, false, false, &StaticConfigParams{}, false) + result, warnings := generateNginxCfgForMergeableIngresses(mergeableIngresses, nil, nil, configParams, false, false, &StaticConfigParams{}, false) if diff := cmp.Diff(expected, result); diff != "" { t.Errorf("generateNginxCfgForMergeableIngresses() returned unexpected result (-want +got):\n%s", diff) @@ -387,8 +382,7 @@ func TestGenerateNginxConfigForCrossNamespaceMergeableIngresses(t *testing.T) { expected := createExpectedConfigForCrossNamespaceMergeableCafeIngress() configParams := NewDefaultConfigParams(false) - emptyApResources := AppProtectResources{} - result, warnings := generateNginxCfgForMergeableIngresses(mergeableIngresses, emptyApResources, configParams, false, false, &StaticConfigParams{}, false) + result, warnings := generateNginxCfgForMergeableIngresses(mergeableIngresses, nil, nil, configParams, false, false, &StaticConfigParams{}, false) if diff := cmp.Diff(expected, result); diff != "" { t.Errorf("generateNginxCfgForMergeableIngresses() returned unexpected result (-want +got):\n%s", diff) @@ -452,8 +446,7 @@ func TestGenerateNginxCfgForMergeableIngressesForJWT(t *testing.T) { minionJwtKeyFileNames[objectMetaToFileName(&mergeableIngresses.Minions[0].Ingress.ObjectMeta)] = "/etc/nginx/secrets/default-coffee-jwk" configParams := NewDefaultConfigParams(isPlus) - masterApRes := AppProtectResources{} - result, warnings := generateNginxCfgForMergeableIngresses(mergeableIngresses, masterApRes, configParams, isPlus, false, &StaticConfigParams{}, false) + result, warnings := generateNginxCfgForMergeableIngresses(mergeableIngresses, nil, nil, configParams, isPlus, false, &StaticConfigParams{}, false) if !reflect.DeepEqual(result.Servers[0].JWTAuth, expected.Servers[0].JWTAuth) { t.Errorf("generateNginxCfgForMergeableIngresses returned \n%v, but expected \n%v", result.Servers[0].JWTAuth, expected.Servers[0].JWTAuth) @@ -860,8 +853,7 @@ func TestGenerateNginxCfgForSpiffe(t *testing.T) { expected.Servers[0].Locations[i].SSL = true } - apResources := AppProtectResources{} - result, warnings := generateNginxCfg(&cafeIngressEx, apResources, false, configParams, false, false, + result, warnings := generateNginxCfg(&cafeIngressEx, nil, nil, false, configParams, false, false, &StaticConfigParams{NginxServiceMesh: true}, false) if diff := cmp.Diff(expected, result); diff != "" { @@ -883,8 +875,7 @@ func TestGenerateNginxCfgForInternalRoute(t *testing.T) { expected.Servers[0].SpiffeCerts = true expected.Ingress.Annotations[internalRouteAnnotation] = "true" - apResources := AppProtectResources{} - result, warnings := generateNginxCfg(&cafeIngressEx, apResources, false, configParams, false, false, + result, warnings := generateNginxCfg(&cafeIngressEx, nil, nil, false, configParams, false, false, &StaticConfigParams{NginxServiceMesh: true, EnableInternalRoutes: true}, false) if diff := cmp.Diff(expected, result); diff != "" { @@ -1349,7 +1340,7 @@ func TestGenerateNginxCfgForAppProtect(t *testing.T) { isPlus := true configParams := NewDefaultConfigParams(isPlus) - apRes := AppProtectResources{ + apResources := &AppProtectResources{ AppProtectPolicy: "/etc/nginx/waf/nac-policies/default_dataguard-alarm", AppProtectLogconfs: []string{"/etc/nginx/waf/nac-logconfs/default_logconf syslog:server=127.0.0.1:514"}, } @@ -1364,7 +1355,7 @@ func TestGenerateNginxCfgForAppProtect(t *testing.T) { expected.Servers[0].AppProtectLogEnable = "on" expected.Ingress.Annotations = cafeIngressEx.Ingress.Annotations - result, warnings := generateNginxCfg(&cafeIngressEx, apRes, false, configParams, isPlus, false, staticCfgParams, false) + result, warnings := generateNginxCfg(&cafeIngressEx, apResources, nil, false, configParams, isPlus, false, staticCfgParams, false) if diff := cmp.Diff(expected, result); diff != "" { t.Errorf("generateNginxCfg() returned unexpected result (-want +got):\n%s", diff) } @@ -1400,7 +1391,7 @@ func TestGenerateNginxCfgForMergeableIngressesForAppProtect(t *testing.T) { isPlus := true configParams := NewDefaultConfigParams(isPlus) - apRes := AppProtectResources{ + apResources := &AppProtectResources{ AppProtectPolicy: "/etc/nginx/waf/nac-policies/default_dataguard-alarm", AppProtectLogconfs: []string{"/etc/nginx/waf/nac-logconfs/default_logconf syslog:server=127.0.0.1:514"}, } @@ -1415,7 +1406,101 @@ func TestGenerateNginxCfgForMergeableIngressesForAppProtect(t *testing.T) { expected.Servers[0].AppProtectLogEnable = "on" expected.Ingress.Annotations = mergeableIngresses.Master.Ingress.Annotations - result, warnings := generateNginxCfgForMergeableIngresses(mergeableIngresses, apRes, configParams, isPlus, false, staticCfgParams, false) + result, warnings := generateNginxCfgForMergeableIngresses(mergeableIngresses, apResources, nil, configParams, isPlus, false, staticCfgParams, false) + if diff := cmp.Diff(expected, result); diff != "" { + t.Errorf("generateNginxCfgForMergeableIngresses() returned unexpected result (-want +got):\n%s", diff) + } + if len(warnings) != 0 { + t.Errorf("generateNginxCfgForMergeableIngresses() returned warnings: %v", warnings) + } +} + +func TestGenerateNginxCfgForAppProtectDos(t *testing.T) { + cafeIngressEx := createCafeIngressEx() + cafeIngressEx.Ingress.Annotations["appprotectdos.f5.com/app-protect-dos-resource"] = "dos-policy" + + isPlus := true + configParams := NewDefaultConfigParams(isPlus) + dosResource := &appProtectDosResource{ + AppProtectDosEnable: "on", + AppProtectDosName: "dos.example.com", + AppProtectDosMonitorUri: "monitor-name", + AppProtectDosAccessLogDst: "access-log-dest", + AppProtectDosPolicyFile: "/etc/nginx/dos/policies/default_policy", + AppProtectDosLogEnable: true, + AppProtectDosLogConfFile: "/etc/nginx/dos/logconfs/default_logconf syslog:server=127.0.0.1:514", + } + staticCfgParams := &StaticConfigParams{ + MainAppProtectDosLoadModule: true, + } + + expected := createExpectedConfigForCafeIngressEx(isPlus) + expected.Servers[0].AppProtectDosEnable = "on" + expected.Servers[0].AppProtectDosPolicyFile = "/etc/nginx/dos/policies/default_policy" + expected.Servers[0].AppProtectDosLogConfFile = "/etc/nginx/dos/logconfs/default_logconf syslog:server=127.0.0.1:514" + expected.Servers[0].AppProtectDosLogEnable = true + expected.Servers[0].AppProtectDosName = "dos.example.com" + expected.Servers[0].AppProtectDosMonitorUri = "monitor-name" + expected.Servers[0].AppProtectDosAccessLogDst = "access-log-dest" + expected.Ingress.Annotations = cafeIngressEx.Ingress.Annotations + + result, warnings := generateNginxCfg(&cafeIngressEx, nil, dosResource, false, configParams, isPlus, false, staticCfgParams, false) + if diff := cmp.Diff(expected, result); diff != "" { + t.Errorf("generateNginxCfg() returned unexpected result (-want +got):\n%s", diff) + } + if len(warnings) != 0 { + t.Errorf("generateNginxCfg() returned warnings: %v", warnings) + } +} + +func TestGenerateNginxCfgForMergeableIngressesForAppProtectDos(t *testing.T) { + mergeableIngresses := createMergeableCafeIngress() + mergeableIngresses.Master.Ingress.Annotations["appprotectdos.f5.com/app-protect-dos-enable"] = "True" + mergeableIngresses.Master.DosEx = &DosEx{ + DosPolicy: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "namespace": "default", + "name": "policy", + }, + }, + }, + DosLogConf: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "namespace": "default", + "name": "logconf", + }, + }, + }, + } + + isPlus := true + configParams := NewDefaultConfigParams(isPlus) + apRes := &appProtectDosResource{ + AppProtectDosEnable: "on", + AppProtectDosName: "dos.example.com", + AppProtectDosMonitorUri: "monitor-name", + AppProtectDosAccessLogDst: "access-log-dest", + AppProtectDosPolicyFile: "/etc/nginx/dos/policies/default_policy", + AppProtectDosLogEnable: true, + AppProtectDosLogConfFile: "/etc/nginx/dos/logconfs/default_logconf syslog:server=127.0.0.1:514", + } + staticCfgParams := &StaticConfigParams{ + MainAppProtectDosLoadModule: true, + } + + expected := createExpectedConfigForMergeableCafeIngress(isPlus) + expected.Servers[0].AppProtectDosEnable = "on" + expected.Servers[0].AppProtectDosPolicyFile = "/etc/nginx/dos/policies/default_policy" + expected.Servers[0].AppProtectDosLogConfFile = "/etc/nginx/dos/logconfs/default_logconf syslog:server=127.0.0.1:514" + expected.Servers[0].AppProtectDosLogEnable = true + expected.Servers[0].AppProtectDosName = "dos.example.com" + expected.Servers[0].AppProtectDosMonitorUri = "monitor-name" + expected.Servers[0].AppProtectDosAccessLogDst = "access-log-dest" + expected.Ingress.Annotations = mergeableIngresses.Master.Ingress.Annotations + + result, warnings := generateNginxCfgForMergeableIngresses(mergeableIngresses, nil, apRes, configParams, isPlus, false, staticCfgParams, false) if diff := cmp.Diff(expected, result); diff != "" { t.Errorf("generateNginxCfgForMergeableIngresses() returned unexpected result (-want +got):\n%s", diff) } diff --git a/internal/configs/version1/config.go b/internal/configs/version1/config.go index 53f3c19a95..5f4da9780c 100644 --- a/internal/configs/version1/config.go +++ b/internal/configs/version1/config.go @@ -93,12 +93,21 @@ type Server struct { JWTAuth *JWTAuth JWTRedirectLocations []JWTRedirectLocation - Ports []int - SSLPorts []int - AppProtectEnable string - AppProtectPolicy string - AppProtectLogConfs []string - AppProtectLogEnable string + Ports []int + SSLPorts []int + AppProtectEnable string + AppProtectPolicy string + AppProtectLogConfs []string + AppProtectLogEnable string + AppProtectDosEnable string + AppProtectDosPolicyFile string + AppProtectDosLogConfFile string + AppProtectDosLogEnable bool + AppProtectDosMonitorUri string + AppProtectDosMonitorProtocol string + AppProtectDosMonitorTimeout uint64 + AppProtectDosName string + AppProtectDosAccessLogDst string SpiffeCerts bool } @@ -197,6 +206,9 @@ type MainConfig struct { AppProtectCookieSeed string AppProtectCPUThresholds string AppProtectPhysicalMemoryThresholds string + AppProtectDosLoadModule bool + AppProtectDosLogFormat []string + AppProtectDosLogFormatEscaping string InternalRouteServer bool InternalRouteServerName string LatencyMetrics bool diff --git a/internal/configs/version1/nginx-plus.ingress.tmpl b/internal/configs/version1/nginx-plus.ingress.tmpl index 0fb18c5962..c6059d45f0 100644 --- a/internal/configs/version1/nginx-plus.ingress.tmpl +++ b/internal/configs/version1/nginx-plus.ingress.tmpl @@ -73,7 +73,29 @@ server { {{range $AppProtectLogConf := $server.AppProtectLogConfs}}app_protect_security_log {{$AppProtectLogConf}}; {{end}} {{- end}} - + + {{- if $server.AppProtectDosEnable}} + app_protect_dos_enable {{$server.AppProtectDosEnable}}; + {{if $server.AppProtectDosPolicyFile}}app_protect_dos_policy_file {{$server.AppProtectDosPolicyFile}};{{end}} + {{- if $server.AppProtectDosLogEnable}} + app_protect_dos_security_log_enable on; + {{if $server.AppProtectDosLogConfFile}}app_protect_dos_security_log {{$server.AppProtectDosLogConfFile}};{{end}} + {{- end}} + {{- if $server.AppProtectDosAccessLogDst}} + set $loggable '0'; + # app-protect-dos module will set it to '1' if a request doesn't pass the rate limit + access_log {{ .AppProtectDosAccessLogDst }} log_dos if=$loggable; + {{- end}} + {{- if $server.AppProtectDosMonitorUri}} + {{- if or $server.AppProtectDosMonitorProtocol $server.AppProtectDosMonitorTimeout}} + app_protect_dos_monitor uri={{$server.AppProtectDosMonitorUri}}{{if $server.AppProtectDosMonitorProtocol}} protocol={{$server.AppProtectDosMonitorProtocol}}{{end}}{{if $server.AppProtectDosMonitorTimeout}} timeout={{$server.AppProtectDosMonitorTimeout}}{{end}}; + {{- else}} + app_protect_dos_monitor "{{$server.AppProtectDosMonitorUri}}"; + {{- end}} + {{- end}} + {{if $server.AppProtectDosName}}app_protect_dos_name "{{$server.AppProtectDosName}}";{{end}} + {{- end}} + {{if not $server.GRPCOnly}} {{range $proxyHideHeader := $server.ProxyHideHeaders}} proxy_hide_header {{$proxyHideHeader}};{{end}} diff --git a/internal/configs/version1/nginx-plus.tmpl b/internal/configs/version1/nginx-plus.tmpl index d7cfcaf5d0..81d9f0cee3 100644 --- a/internal/configs/version1/nginx-plus.tmpl +++ b/internal/configs/version1/nginx-plus.tmpl @@ -18,6 +18,9 @@ load_module modules/ngx_http_opentracing_module.so; {{- if .AppProtectLoadModule}} load_module modules/ngx_http_app_protect_module.so; {{- end}} +{{- if .AppProtectDosLoadModule}} +load_module modules/ngx_http_app_protect_dos_module.so; +{{- end}} {{- if .MainSnippets}} {{range $value := .MainSnippets}} {{$value}}{{end}} @@ -51,6 +54,20 @@ http { '"$http_user_agent" "$http_x_forwarded_for"'; {{- end}} + {{- if .AppProtectDosLoadModule}} + {{- if .AppProtectDosLogFormat}} + log_format log_dos {{if .AppProtectDosLogFormatEscaping}}escape={{ .AppProtectDosLogFormatEscaping }} {{end}} + {{range $i, $value := .AppProtectDosLogFormat -}} + {{with $value}}'{{if $i}} {{end}}{{$value}}' + {{end}}{{end}}; + {{- else -}} + log_format log_dos ', vs_name_al=$app_protect_dos_vs_name, ip=$remote_addr, tls_fp=$app_protect_dos_tls_fp, ' + 'outcome=$app_protect_dos_outcome, reason=$app_protect_dos_outcome_reason, ' + 'ip_tls=$remote_addr:$app_protect_dos_tls_fp, '; + + {{- end}} + {{- end}} + {{if .AccessLogOff}} access_log off; {{else}} @@ -70,6 +87,7 @@ http { {{if .AppProtectPhysicalMemoryThresholds}}app_protect_physical_memory_util_thresholds {{.AppProtectPhysicalMemoryThresholds}};{{end}} include /etc/nginx/waf/nac-usersigs/index.conf; {{- end}} + sendfile on; #tcp_nopush on; diff --git a/internal/configs/version2/http.go b/internal/configs/version2/http.go index ce31fb7b87..f6ba07bf0e 100644 --- a/internal/configs/version2/http.go +++ b/internal/configs/version2/http.go @@ -72,6 +72,7 @@ type Server struct { EgressMTLS *EgressMTLS OIDC *OIDC WAF *WAF + Dos *Dos PoliciesErrorReturn *Return VSNamespace string VSName string @@ -124,6 +125,19 @@ type WAF struct { ApLogConf string } +// Dos defines Dos configuration. +type Dos struct { + Enable string + Name string + ApDosPolicy string + ApDosSecurityLogEnable bool + ApDosLogConf string + ApDosMonitorUri string + ApDosMonitorProtocol string + ApDosMonitorTimeout uint64 + ApDosAccessLogDest string +} + // Location defines a location. type Location struct { Path string @@ -162,6 +176,7 @@ type Location struct { EgressMTLS *EgressMTLS OIDC bool WAF *WAF + Dos *Dos PoliciesErrorReturn *Return ServiceName string IsVSR bool diff --git a/internal/configs/version2/nginx-plus.virtualserver.tmpl b/internal/configs/version2/nginx-plus.virtualserver.tmpl index 79b50dd057..df5b1c0d9e 100644 --- a/internal/configs/version2/nginx-plus.virtualserver.tmpl +++ b/internal/configs/version2/nginx-plus.virtualserver.tmpl @@ -193,6 +193,37 @@ server { {{ end }} {{ end }} + {{- with $s.Dos }} + app_protect_dos_enable {{ .Enable }}; + {{- if .Name }} + app_protect_dos_name "{{ .Name }}"; + {{- end }} + + {{- if .ApDosPolicy }} + app_protect_dos_policy_file {{ .ApDosPolicy }}; + {{- end }} + + {{- if .ApDosSecurityLogEnable }} + app_protect_dos_security_log_enable on; + app_protect_dos_security_log {{ .ApDosLogConf }}; + {{- end }} + + {{- if .ApDosAccessLogDest }} + set $loggable '0'; + # app-protect-dos module will set it to '1' if a request doesn't pass the rate limit + access_log {{ .ApDosAccessLogDest }} log_dos if=$loggable; + {{- end }} + + {{- if .ApDosMonitor }} + {{- if or .ApDosMonitorProtocol .ApDosMonitorTimeout}} + app_protect_dos_monitor uri={{ .ApDosMonitor }}{{if .ApDosMonitorProtocol}} protocol={{.ApDosMonitorProtocol}}{{end}}{{if .ApDosMonitorTimeout}} timeout={{.ApDosMonitorTimeout}}{{end}}; + {{- else}} + app_protect_dos_monitor "{{ .ApDosMonitor }}"; + {{- end}} + {{- end}} + + {{- end }} + {{ range $snippet := $s.Snippets }} {{- $snippet }} {{ end }} @@ -340,6 +371,36 @@ server { {{ end }} {{ end }} + {{- with $l.Dos }} + app_protect_dos_enable {{ .Enable }}; + + {{- if .Name }} + app_protect_dos_name "{{ .Name }}"; + {{- end }} + + {{- if .ApDosPolicy }} + app_protect_dos_policy_file {{ .ApDosPolicy }}; + {{- end }} + + {{ if .ApDosSecurityLogEnable }} + app_protect_dos_security_log_enable on; + app_protect_dos_security_log {{ .ApDosLogConf }}; + {{ end }} + {{- if .ApDosAccessLogDest }} + set $loggable '0'; + # app-protect-dos module will set it to '1' if a request doesn't pass the rate limit + access_log {{ .ApDosAccessLogDest }} log_dos if=$loggable; + {{- end }} + + {{- if .ApDosMonitorUri }} + {{- if or .ApDosMonitorProtocol .ApDosMonitorTimeout}} + app_protect_dos_monitor uri={{ .ApDosMonitorUri }}{{if .ApDosMonitorProtocol}} protocol={{.ApDosMonitorProtocol}}{{end}}{{if .ApDosMonitorTimeout}} timeout={{.ApDosMonitorTimeout}}{{end}}; + {{- else}} + app_protect_dos_monitor "{{ .ApDosMonitorUri }}"; + {{- end}} + {{- end}} + {{- end }} + {{ range $e := $l.ErrorPages }} error_page {{ $e.Codes }} {{ if ne 0 $e.ResponseCode }}={{ $e.ResponseCode }}{{ end }} "{{ $e.Name }}"; {{ end }} diff --git a/internal/configs/virtualserver.go b/internal/configs/virtualserver.go index b688901af8..a0d43df1c4 100644 --- a/internal/configs/virtualserver.go +++ b/internal/configs/virtualserver.go @@ -63,6 +63,8 @@ type VirtualServerEx struct { SecretRefs map[string]*secrets.SecretReference ApPolRefs map[string]*unstructured.Unstructured LogConfRefs map[string]*unstructured.Unstructured + DosProtectedRefs map[string]*unstructured.Unstructured + DosProtectedEx map[string]*DosEx } func (vsx *VirtualServerEx) String() string { @@ -77,6 +79,19 @@ func (vsx *VirtualServerEx) String() string { return fmt.Sprintf("%s/%s", vsx.VirtualServer.Namespace, vsx.VirtualServer.Name) } +// appProtectResourcesForVS holds file names of APPolicy and APLogConf resources used in a VirtualServer. +type appProtectResourcesForVS struct { + Policies map[string]string + LogConfs map[string]string +} + +func newAppProtectVSResourcesForVS() *appProtectResourcesForVS { + return &appProtectResourcesForVS{ + Policies: make(map[string]string), + LogConfs: make(map[string]string), + } +} + // GenerateEndpointsKey generates a key for the Endpoints map in VirtualServerEx. func GenerateEndpointsKey( serviceNamespace string, @@ -266,7 +281,11 @@ func (vsc *virtualServerConfigurator) generateEndpointsForUpstream( } // GenerateVirtualServerConfig generates a full configuration for a VirtualServer -func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(vsEx *VirtualServerEx, apResources map[string]string) (version2.VirtualServerConfig, Warnings) { +func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig( + vsEx *VirtualServerEx, + apResources *appProtectResourcesForVS, + dosResources map[string]*appProtectDosResource, +) (version2.VirtualServerConfig, Warnings) { vsc.clearWarnings() sslConfig := vsc.generateSSLConfig(vsEx.VirtualServer, vsEx.VirtualServer.Spec.TLS, vsEx.VirtualServer.Namespace, vsEx.SecretRefs, vsc.cfgParams) @@ -285,6 +304,7 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(vsEx *VirtualS vsName: vsEx.VirtualServer.Name, } policiesCfg := vsc.generatePolicies(ownerDetails, vsEx.VirtualServer.Spec.Policies, vsEx.Policies, specContext, policyOpts) + dosCfg := generateDosCfg(dosResources[""]) // crUpstreams maps an UpstreamName to its conf_v1.Upstream as they are generated // necessary for generateLocation to know what Upstream each Location references @@ -417,6 +437,8 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(vsEx *VirtualS } limitReqZones = append(limitReqZones, routePoliciesCfg.LimitReqZones...) + dosRouteCfg := generateDosCfg(dosResources[r.Path]) + if len(r.Matches) > 0 { cfg := generateMatchesConfig( r, @@ -435,6 +457,7 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(vsEx *VirtualS "", "", ) addPoliciesCfgToLocations(routePoliciesCfg, cfg.Locations) + addDosConfigToLocations(dosRouteCfg, cfg.Locations) maps = append(maps, cfg.Maps...) locations = append(locations, cfg.Locations...) @@ -446,6 +469,7 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(vsEx *VirtualS cfg := generateDefaultSplitsConfig(r, virtualServerUpstreamNamer, crUpstreams, variableNamer, len(splitClients), vsc.cfgParams, r.ErrorPages, errorPageIndex, r.Path, vsLocSnippets, vsc.enableSnippets, len(returnLocations), isVSR, "", "") addPoliciesCfgToLocations(routePoliciesCfg, cfg.Locations) + addDosConfigToLocations(dosRouteCfg, cfg.Locations) splitClients = append(splitClients, cfg.SplitClients...) locations = append(locations, cfg.Locations...) @@ -460,6 +484,7 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(vsEx *VirtualS loc, returnLoc := generateLocation(r.Path, upstreamName, upstream, r.Action, vsc.cfgParams, r.ErrorPages, false, errorPageIndex, proxySSLName, r.Path, vsLocSnippets, vsc.enableSnippets, len(returnLocations), isVSR, "", "") addPoliciesCfgToLocation(routePoliciesCfg, &loc) + loc.Dos = dosRouteCfg locations = append(locations, loc) if returnLoc != nil { @@ -611,6 +636,7 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(vsEx *VirtualS EgressMTLS: policiesCfg.EgressMTLS, OIDC: vsc.oidcPolCfg.oidc, WAF: policiesCfg.WAF, + Dos: dosCfg, PoliciesErrorReturn: policiesCfg.ErrorReturn, VSNamespace: vsEx.VirtualServer.Namespace, VSName: vsEx.VirtualServer.Name, @@ -649,7 +675,7 @@ type policyOwnerDetails struct { type policyOptions struct { tls bool secretRefs map[string]*secrets.SecretReference - apResources map[string]string + apResources *appProtectResourcesForVS } type validationResults struct { @@ -951,7 +977,7 @@ func (p *policiesCfg) addWAFConfig( waf *conf_v1.WAF, polKey string, polNamespace string, - apResources map[string]string, + apResources *appProtectResourcesForVS, ) *validationResults { res := newValidationResults() if p.WAF != nil { @@ -972,7 +998,7 @@ func (p *policiesCfg) addWAFConfig( apPolKey = fmt.Sprintf("%v/%v", polNamespace, apPolKey) } - if apPolPath, exists := apResources[apPolKey]; exists { + if apPolPath, exists := apResources.Policies[apPolKey]; exists { p.WAF.ApPolicy = apPolPath } else { res.addWarningf("WAF policy %s references an invalid or non-existing App Protect policy %s", polKey, apPolKey) @@ -990,7 +1016,7 @@ func (p *policiesCfg) addWAFConfig( logConfKey = fmt.Sprintf("%v/%v", polNamespace, logConfKey) } - if logConfPath, ok := apResources[logConfKey]; ok { + 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) } else { @@ -1139,6 +1165,12 @@ func addPoliciesCfgToLocations(cfg policiesCfg, locations []version2.Location) { } } +func addDosConfigToLocations(dosCfg *version2.Dos, locations []version2.Location) { + for i := range locations { + locations[i].Dos = dosCfg + } +} + func getUpstreamResourceLabels(owner runtime.Object) version2.UpstreamLabels { var resourceType, resourceName, resourceNamespace string @@ -2263,3 +2295,20 @@ func isTLSEnabled(u conf_v1.Upstream, spiffeCerts bool) bool { func isGRPC(protocolType string) bool { return protocolType == "grpc" } + +func generateDosCfg(dosResource *appProtectDosResource) *version2.Dos { + if dosResource == nil { + return nil + } + dos := &version2.Dos{} + dos.Enable = dosResource.AppProtectDosEnable + dos.Name = dosResource.AppProtectDosName + dos.ApDosMonitorUri = dosResource.AppProtectDosMonitorUri + dos.ApDosMonitorProtocol = dosResource.AppProtectDosMonitorProtocol + dos.ApDosMonitorTimeout = dosResource.AppProtectDosMonitorTimeout + dos.ApDosAccessLogDest = dosResource.AppProtectDosAccessLogDst + dos.ApDosPolicy = dosResource.AppProtectDosPolicyFile + dos.ApDosSecurityLogEnable = dosResource.AppProtectDosLogEnable + dos.ApDosLogConf = dosResource.AppProtectDosLogConfFile + return dos +} diff --git a/internal/configs/virtualserver_test.go b/internal/configs/virtualserver_test.go index 6c40b631b9..f51383dad5 100644 --- a/internal/configs/virtualserver_test.go +++ b/internal/configs/virtualserver_test.go @@ -647,7 +647,7 @@ func TestGenerateVirtualServerConfig(t *testing.T) { isWildcardEnabled, ) - result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, nil) + result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, nil, nil) if diff := cmp.Diff(expected, result); diff != "" { t.Errorf("GenerateVirtualServerConfig() mismatch (-want +got):\n%s", diff) } @@ -756,7 +756,7 @@ func TestGenerateVirtualServerConfigWithSpiffeCerts(t *testing.T) { isWildcardEnabled := false vsc := newVirtualServerConfigurator(&baseCfgParams, isPlus, isResolverConfigured, staticConfigParams, isWildcardEnabled) - result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, nil) + result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, nil, nil) if diff := cmp.Diff(expected, result); diff != "" { t.Errorf("GenerateVirtualServerConfig() mismatch (-want +got):\n%s", diff) } @@ -1042,7 +1042,7 @@ func TestGenerateVirtualServerConfigForVirtualServerWithSplits(t *testing.T) { isWildcardEnabled := false vsc := newVirtualServerConfigurator(&baseCfgParams, isPlus, isResolverConfigured, &StaticConfigParams{}, isWildcardEnabled) - result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, nil) + result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, nil, nil) if diff := cmp.Diff(expected, result); diff != "" { t.Errorf("GenerateVirtualServerConfig() mismatch (-want +got):\n%s", diff) } @@ -1360,7 +1360,7 @@ func TestGenerateVirtualServerConfigForVirtualServerWithMatches(t *testing.T) { isWildcardEnabled := false vsc := newVirtualServerConfigurator(&baseCfgParams, isPlus, isResolverConfigured, &StaticConfigParams{}, isWildcardEnabled) - result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, nil) + result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, nil, nil) if diff := cmp.Diff(expected, result); diff != "" { t.Errorf("GenerateVirtualServerConfig() mismatch (-want +got):\n%s", diff) } @@ -1834,7 +1834,7 @@ func TestGenerateVirtualServerConfigForVirtualServerWithReturns(t *testing.T) { isWildcardEnabled := false vsc := newVirtualServerConfigurator(&baseCfgParams, isPlus, isResolverConfigured, &StaticConfigParams{}, isWildcardEnabled) - result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, nil) + result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, nil, nil) if !reflect.DeepEqual(result, expected) { t.Errorf("GenerateVirtualServerConfig returned \n%+v but expected \n%+v", result, expected) } @@ -1888,9 +1888,13 @@ func TestGeneratePolicies(t *testing.T) { }, }, }, - apResources: map[string]string{ - "default/logconf": "/etc/nginx/waf/nac-logconfs/default-logconf", - "default/dataguard-alarm": "/etc/nginx/waf/nac-policies/default-dataguard-alarm", + 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", + }, }, } @@ -3331,9 +3335,13 @@ func TestGeneratePoliciesFails(t *testing.T) { }, }, policyOpts: policyOptions{ - apResources: map[string]string{ - "default/logconf": "/etc/nginx/waf/nac-logconfs/default-logconf", - "default/dataguard-alarm": "/etc/nginx/waf/nac-policies/default-dataguard-alarm", + 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", + }, }, }, context: "route", @@ -7405,7 +7413,7 @@ func TestAddWafConfig(t *testing.T) { wafInput *conf_v1.WAF polKey string polNamespace string - apResources map[string]string + apResources *appProtectResourcesForVS wafConfig *version2.WAF expected *validationResults msg string @@ -7417,7 +7425,10 @@ func TestAddWafConfig(t *testing.T) { }, polKey: "default/waf-policy", polNamespace: "default", - apResources: map[string]string{}, + apResources: &appProtectResourcesForVS{ + Policies: map[string]string{}, + LogConfs: map[string]string{}, + }, wafConfig: &version2.WAF{ Enable: "on", }, @@ -7437,9 +7448,13 @@ func TestAddWafConfig(t *testing.T) { }, polKey: "default/waf-policy", polNamespace: "default", - apResources: map[string]string{ - "default/dataguard-alarm": "/etc/nginx/waf/nac-policies/default-dataguard-alarm", - "default/logconf": "/etc/nginx/waf/nac-logconfs/default-logconf", + 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", @@ -7462,8 +7477,11 @@ func TestAddWafConfig(t *testing.T) { }, polKey: "default/waf-policy", polNamespace: "", - apResources: map[string]string{ - "default/dataguard-alarm": "/etc/nginx/waf/nac-policies/default-dataguard-alarm", + apResources: &appProtectResourcesForVS{ + Policies: map[string]string{ + "default/dataguard-alarm": "/etc/nginx/waf/nac-policies/default-dataguard-alarm", + }, + LogConfs: map[string]string{}, }, wafConfig: &version2.WAF{ ApPolicy: "/etc/nginx/waf/nac-policies/default-dataguard-alarm", @@ -7490,8 +7508,11 @@ func TestAddWafConfig(t *testing.T) { }, polKey: "default/waf-policy", polNamespace: "", - apResources: map[string]string{ - "default/logconf": "/etc/nginx/waf/nac-logconfs/default-logconf", + apResources: &appProtectResourcesForVS{ + Policies: map[string]string{}, + 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", @@ -7519,9 +7540,13 @@ func TestAddWafConfig(t *testing.T) { }, polKey: "default/waf-policy", polNamespace: "", - apResources: map[string]string{ - "ns1/dataguard-alarm": "/etc/nginx/waf/nac-policies/ns1-dataguard-alarm", - "ns2/logconf": "/etc/nginx/waf/nac-logconfs/ns2-logconf", + apResources: &appProtectResourcesForVS{ + Policies: map[string]string{ + "ns1/dataguard-alarm": "/etc/nginx/waf/nac-policies/ns1-dataguard-alarm", + }, + LogConfs: map[string]string{ + "ns2/logconf": "/etc/nginx/waf/nac-logconfs/ns2-logconf", + }, }, wafConfig: &version2.WAF{ ApPolicy: "/etc/nginx/waf/nac-policies/ns1-dataguard-alarm", @@ -7539,9 +7564,13 @@ func TestAddWafConfig(t *testing.T) { }, polKey: "default/waf-policy", polNamespace: "default", - apResources: map[string]string{ - "default/dataguard-alarm": "/etc/nginx/waf/nac-policies/ns1-dataguard-alarm", - "default/logconf": "/etc/nginx/waf/nac-logconfs/ns2-logconf", + apResources: &appProtectResourcesForVS{ + Policies: map[string]string{ + "default/dataguard-alarm": "/etc/nginx/waf/nac-policies/ns1-dataguard-alarm", + }, + LogConfs: map[string]string{ + "default/logconf": "/etc/nginx/waf/nac-logconfs/ns2-logconf", + }, }, wafConfig: &version2.WAF{ Enable: "off", diff --git a/internal/k8s/appprotect/app_protect_configuration.go b/internal/k8s/appprotect/app_protect_configuration.go index 3d1edd2f89..5094c346dd 100644 --- a/internal/k8s/appprotect/app_protect_configuration.go +++ b/internal/k8s/appprotect/app_protect_configuration.go @@ -5,6 +5,10 @@ import ( "sort" "time" + "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/validation" + + "github.com/nginxinc/kubernetes-ingress/internal/k8s/appprotect_common" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -201,7 +205,7 @@ func (s appProtectUserSigSlice) Swap(i, j int) { } func createAppProtectPolicyEx(policyObj *unstructured.Unstructured) (*PolicyEx, error) { - err := validateAppProtectPolicy(policyObj) + err := validation.ValidateAppProtectPolicy(policyObj) if err != nil { errMsg := fmt.Sprintf("Error validating policy %s: %v", policyObj.GetName(), err) return &PolicyEx{Obj: policyObj, IsValid: false, ErrorMsg: failedValidationErrorMsg}, fmt.Errorf(errMsg) @@ -255,7 +259,7 @@ func buildRevTimes(requirement map[string]interface{}) (RevTimes, error) { } func createAppProtectLogConfEx(logConfObj *unstructured.Unstructured) (*LogConfEx, error) { - err := validateAppProtectLogConf(logConfObj) + err := validation.ValidateAppProtectLogConf(logConfObj) if err != nil { return &LogConfEx{ Obj: logConfObj, @@ -271,7 +275,7 @@ func createAppProtectLogConfEx(logConfObj *unstructured.Unstructured) (*LogConfE func createAppProtectUserSigEx(userSigObj *unstructured.Unstructured) (*UserSigEx, error) { sTag := "" - err := validateAppProtectUserSig(userSigObj) + err := validation.ValidateAppProtectUserSig(userSigObj) if err != nil { errMsg := failedValidationErrorMsg return &UserSigEx{Obj: userSigObj, IsValid: false, Tag: sTag, ErrorMsg: errMsg}, fmt.Errorf(errMsg) @@ -341,7 +345,7 @@ func (ci *ConfigurationImpl) verifyPolicyAgainstUserSigs(policy *PolicyEx) bool // AddOrUpdatePolicy adds or updates an App Protect Policy to App Protect Configuration func (ci *ConfigurationImpl) AddOrUpdatePolicy(policyObj *unstructured.Unstructured) (changes []Change, problems []Problem) { - resNsName := GetNsName(policyObj) + resNsName := appprotect_common.GetNsName(policyObj) policy, err := createAppProtectPolicyEx(policyObj) if err != nil { ci.Policies[resNsName] = policy @@ -361,7 +365,7 @@ func (ci *ConfigurationImpl) AddOrUpdatePolicy(policyObj *unstructured.Unstructu // AddOrUpdateLogConf adds or updates App Protect Log Configuration to App Protect Configuration func (ci *ConfigurationImpl) AddOrUpdateLogConf(logconfObj *unstructured.Unstructured) (changes []Change, problems []Problem) { - resNsName := GetNsName(logconfObj) + resNsName := appprotect_common.GetNsName(logconfObj) logConf, err := createAppProtectLogConfEx(logconfObj) ci.LogConfs[resNsName] = logConf if err != nil { @@ -373,7 +377,7 @@ func (ci *ConfigurationImpl) AddOrUpdateLogConf(logconfObj *unstructured.Unstruc // AddOrUpdateUserSig adds or updates App Protect User Defined Signature to App Protect Configuration func (ci *ConfigurationImpl) AddOrUpdateUserSig(userSigObj *unstructured.Unstructured) (change UserSigChange, problems []Problem) { - resNsName := GetNsName(userSigObj) + resNsName := appprotect_common.GetNsName(userSigObj) userSig, err := createAppProtectUserSigEx(userSigObj) ci.UserSigs[resNsName] = userSig if err != nil { @@ -563,7 +567,7 @@ func NewFakeConfiguration() Configuration { // AddOrUpdatePolicy adds or updates an App Protect Policy to App Protect Configuration func (fc *FakeConfiguration) AddOrUpdatePolicy(policyObj *unstructured.Unstructured) (changes []Change, problems []Problem) { - resNsName := GetNsName(policyObj) + resNsName := appprotect_common.GetNsName(policyObj) policy := &PolicyEx{ Obj: policyObj, IsValid: true, @@ -574,7 +578,7 @@ func (fc *FakeConfiguration) AddOrUpdatePolicy(policyObj *unstructured.Unstructu // AddOrUpdateLogConf adds or updates App Protect Log Configuration to App Protect Configuration func (fc *FakeConfiguration) AddOrUpdateLogConf(logConfObj *unstructured.Unstructured) (changes []Change, problems []Problem) { - resNsName := GetNsName(logConfObj) + resNsName := appprotect_common.GetNsName(logConfObj) logConf := &LogConfEx{ Obj: logConfObj, IsValid: true, diff --git a/internal/k8s/appprotect/app_protect_resources.go b/internal/k8s/appprotect/app_protect_resources.go deleted file mode 100644 index 0907f1590f..0000000000 --- a/internal/k8s/appprotect/app_protect_resources.go +++ /dev/null @@ -1,150 +0,0 @@ -package appprotect - -import ( - "fmt" - "net" - "regexp" - "strconv" - "strings" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -var appProtectPolicyRequiredFields = [][]string{ - {"spec", "policy"}, -} - -var appProtectLogConfRequiredFields = [][]string{ - {"spec", "content"}, - {"spec", "filter"}, -} - -var appProtectUserSigRequiredSlices = [][]string{ - {"spec", "signatures"}, -} - -func validateRequiredFields(obj *unstructured.Unstructured, fieldsList [][]string) error { - for _, fields := range fieldsList { - field, found, err := unstructured.NestedMap(obj.Object, fields...) - if err != nil { - return fmt.Errorf("Error checking for required field %v: %w", field, err) - } - if !found { - return fmt.Errorf("Required field %v not found", field) - } - } - return nil -} - -func validateRequiredSlices(obj *unstructured.Unstructured, fieldsList [][]string) error { - for _, fields := range fieldsList { - field, found, err := unstructured.NestedSlice(obj.Object, fields...) - if err != nil { - return fmt.Errorf("Error checking for required field %v: %w", field, err) - } - if !found { - return fmt.Errorf("Required field %v not found", field) - } - } - return nil -} - -// validateAppProtectPolicy validates Policy resource -func validateAppProtectPolicy(policy *unstructured.Unstructured) error { - polName := policy.GetName() - - err := validateRequiredFields(policy, appProtectPolicyRequiredFields) - if err != nil { - return fmt.Errorf("Error validating App Protect Policy %v: %w", polName, err) - } - - return nil -} - -// validateAppProtectLogConf validates LogConfiguration resource -func validateAppProtectLogConf(logConf *unstructured.Unstructured) error { - lcName := logConf.GetName() - err := validateRequiredFields(logConf, appProtectLogConfRequiredFields) - if err != nil { - return fmt.Errorf("Error validating App Protect Log Configuration %v: %w", lcName, err) - } - - return nil -} - -var ( - logDstEx = regexp.MustCompile(`(?:syslog:server=((?:\d{1,3}\.){3}\d{1,3}|localhost|[a-zA-Z0-9._-]+):\d{1,5})|stderr|(?:\/[\S]+)+`) - logDstFileEx = regexp.MustCompile(`(?:\/[\S]+)+`) - logDstFQDNEx = regexp.MustCompile(`(?:[a-zA-Z0-9_-]+\.)+[a-zA-Z0-9_-]+`) -) - -// ValidateAppProtectLogDestination validates destination for log configuration -func ValidateAppProtectLogDestination(dstAntn string) error { - errormsg := "Error parsing App Protect Log config: Destination must follow format: syslog:server=: or fqdn or stderr or absolute path to file" - if !logDstEx.MatchString(dstAntn) { - return fmt.Errorf("%s Log Destination did not follow format", errormsg) - } - if dstAntn == "stderr" { - return nil - } - - if logDstFileEx.MatchString(dstAntn) { - return nil - } - - dstchunks := strings.Split(dstAntn, ":") - - // This error can be ignored since the regex check ensures this string will be parsable - port, _ := strconv.Atoi(dstchunks[2]) - - if port > 65535 || port < 1 { - return fmt.Errorf("Error parsing port: %v not a valid port number", port) - } - - ipstr := strings.Split(dstchunks[1], "=")[1] - if ipstr == "localhost" { - return nil - } - - if logDstFQDNEx.MatchString(ipstr) { - return nil - } - - if net.ParseIP(ipstr) == nil { - return fmt.Errorf("Error parsing host: %v is not a valid ip address or host name", ipstr) - } - - return nil -} - -// ParseResourceReferenceAnnotation returns a namespace/name string -func ParseResourceReferenceAnnotation(ns, antn string) string { - if !strings.Contains(antn, "/") { - return ns + "/" + antn - } - return antn -} - -// ParseResourceReferenceAnnotationList returns a slice of ns/names strings -func ParseResourceReferenceAnnotationList(ns, annotations string) []string { - var out []string - for _, antn := range strings.Split(annotations, ",") { - out = append(out, ParseResourceReferenceAnnotation(ns, antn)) - } - return out -} - -func validateAppProtectUserSig(userSig *unstructured.Unstructured) error { - sigName := userSig.GetName() - err := validateRequiredSlices(userSig, appProtectUserSigRequiredSlices) - if err != nil { - return fmt.Errorf("Error validating App Protect User Signature %v: %w", sigName, err) - } - - return nil -} - -// GetNsName gets the key of a resource in the format: "resNamespace/resName" -func GetNsName(obj *unstructured.Unstructured) string { - return obj.GetNamespace() + "/" + obj.GetName() -} diff --git a/internal/k8s/appprotect/app_protect_resources_test.go b/internal/k8s/appprotect/app_protect_resources_test.go deleted file mode 100644 index 5aa7ba8817..0000000000 --- a/internal/k8s/appprotect/app_protect_resources_test.go +++ /dev/null @@ -1,478 +0,0 @@ -package appprotect - -import ( - "reflect" - "strings" - "testing" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -func TestValidateRequiredFields(t *testing.T) { - tests := []struct { - obj *unstructured.Unstructured - fieldsList [][]string - expectErr bool - msg string - }{ - { - obj: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "a": map[string]interface{}{}, - "b": map[string]interface{}{}, - }, - }, - fieldsList: [][]string{{"a"}, {"b"}}, - expectErr: false, - msg: "valid object with 2 fields", - }, - { - obj: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "a": map[string]interface{}{}, - }, - }, - fieldsList: [][]string{{"a"}, {"b"}}, - expectErr: true, - msg: "invalid object with a missing field", - }, - { - obj: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "a": map[string]interface{}{}, - "x": map[string]interface{}{}, - }, - }, - fieldsList: [][]string{{"a"}, {"b"}}, - expectErr: true, - msg: "invalid object with a wrong field", - }, - { - obj: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "a": map[string]interface{}{ - "b": map[string]interface{}{}, - }, - }, - }, - fieldsList: [][]string{{"a", "b"}}, - expectErr: false, - msg: "valid object with nested field", - }, - { - obj: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "a": map[string]interface{}{ - "x": map[string]interface{}{}, - }, - }, - }, - fieldsList: [][]string{{"a", "b"}}, - expectErr: true, - msg: "invalid object with a wrong nested field", - }, - { - obj: &unstructured.Unstructured{ - Object: map[string]interface{}{}, - }, - fieldsList: nil, - expectErr: false, - msg: "valid object with no validation", - }, - { - obj: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "a": "wrong-type", // must be map[string]interface{} - }, - }, - fieldsList: [][]string{{"a"}}, - expectErr: true, - msg: "invalid object with a field of wrong type", - }, - } - - for _, test := range tests { - err := validateRequiredFields(test.obj, test.fieldsList) - if test.expectErr && err == nil { - t.Errorf("validateRequiredFields() returned no error for the case of %s", test.msg) - } - if !test.expectErr && err != nil { - t.Errorf("validateRequiredFields() returned unexpected error %v for the case of %s", err, test.msg) - } - } -} - -func TestValidateRequiredSlices(t *testing.T) { - tests := []struct { - obj *unstructured.Unstructured - fieldsList [][]string - expectErr bool - msg string - }{ - { - obj: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "a": []interface{}{}, - "b": []interface{}{}, - }, - }, - fieldsList: [][]string{{"a"}, {"b"}}, - expectErr: false, - msg: "valid object with 2 fields", - }, - { - obj: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "a": []interface{}{}, - }, - }, - fieldsList: [][]string{{"a"}, {"b"}}, - expectErr: true, - msg: "invalid object with a field", - }, - { - obj: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "a": []interface{}{}, - "x": []interface{}{}, - }, - }, - fieldsList: [][]string{{"a"}, {"b"}}, - expectErr: true, - msg: "invalid object with a wrong field", - }, - { - obj: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "a": map[string]interface{}{ - "b": []interface{}{}, - }, - }, - }, - fieldsList: [][]string{{"a", "b"}}, - expectErr: false, - msg: "valid object with nested field", - }, - { - obj: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "a": map[string]interface{}{ - "x": []interface{}{}, - }, - }, - }, - fieldsList: [][]string{{"a", "b"}}, - expectErr: true, - msg: "invalid object with a wrong nested field", - }, - { - obj: &unstructured.Unstructured{ - Object: map[string]interface{}{}, - }, - fieldsList: nil, - expectErr: false, - msg: "valid object with no validation", - }, - { - obj: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "a": "wrong-type", // must be [string]interface{} - }, - }, - fieldsList: [][]string{{"a"}}, - expectErr: true, - msg: "invalid object with a field of wrong type", - }, - } - - for _, test := range tests { - err := validateRequiredSlices(test.obj, test.fieldsList) - if test.expectErr && err == nil { - t.Errorf("validateRequiredSlices() returned no error for the case of %s", test.msg) - } - if !test.expectErr && err != nil { - t.Errorf("validateRequiredSlices() returned unexpected error %v for the case of %s", err, test.msg) - } - } -} - -func TestValidateAppProtectPolicy(t *testing.T) { - tests := []struct { - policy *unstructured.Unstructured - expectErr bool - msg string - }{ - { - policy: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "policy": map[string]interface{}{}, - }, - }, - }, - expectErr: false, - msg: "valid policy", - }, - { - policy: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "something": map[string]interface{}{}, - }, - }, - }, - expectErr: true, - msg: "invalid policy with no policy field", - }, - { - policy: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "something": map[string]interface{}{ - "policy": map[string]interface{}{}, - }, - }, - }, - expectErr: true, - msg: "invalid policy with no spec field", - }, - } - - for _, test := range tests { - err := validateAppProtectPolicy(test.policy) - if test.expectErr && err == nil { - t.Errorf("validateAppProtectPolicy() returned no error for the case of %s", test.msg) - } - if !test.expectErr && err != nil { - t.Errorf("validateAppProtectPolicy() returned unexpected error %v for the case of %s", err, test.msg) - } - } -} - -func TestValidateAppProtectLogConf(t *testing.T) { - tests := []struct { - logConf *unstructured.Unstructured - expectErr bool - msg string - }{ - { - logConf: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "content": map[string]interface{}{}, - "filter": map[string]interface{}{}, - }, - }, - }, - expectErr: false, - msg: "valid log conf", - }, - { - logConf: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "filter": map[string]interface{}{}, - }, - }, - }, - expectErr: true, - msg: "invalid log conf with no content field", - }, - { - logConf: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "content": map[string]interface{}{}, - }, - }, - }, - expectErr: true, - msg: "invalid log conf with no filter field", - }, - { - logConf: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "something": map[string]interface{}{ - "content": map[string]interface{}{}, - "filter": map[string]interface{}{}, - }, - }, - }, - expectErr: true, - msg: "invalid log conf with no spec field", - }, - } - - for _, test := range tests { - err := validateAppProtectLogConf(test.logConf) - if test.expectErr && err == nil { - t.Errorf("validateAppProtectLogConf() returned no error for the case of %s", test.msg) - } - if !test.expectErr && err != nil { - t.Errorf("validateAppProtectLogConf() returned unexpected error %v for the case of %s", err, test.msg) - } - } -} - -func TestValidateAppProtectLogDestinationAnnotation(t *testing.T) { - // Positive test cases - posDstAntns := []string{"stderr", "syslog:server=localhost:9000", "syslog:server=10.1.1.2:9000", "/var/log/ap.log", "syslog:server=my-syslog-server.my-namespace:515"} - - // Negative test cases item, expected error message - negDstAntns := [][]string{ - {"stdout", "Log Destination did not follow format"}, - {"syslog:server=localhost:99999", "not a valid port number"}, - {"syslog:server=mysyslog-server:999", "not a valid ip address"}, - } - - for _, tCase := range posDstAntns { - err := ValidateAppProtectLogDestination(tCase) - if err != nil { - t.Errorf("got %v expected nil", err) - } - } - for _, nTCase := range negDstAntns { - err := ValidateAppProtectLogDestination(nTCase[0]) - if err == nil { - t.Errorf("got no error expected error containing %s", nTCase[1]) - } else { - if !strings.Contains(err.Error(), nTCase[1]) { - t.Errorf("got %v expected to contain: %s", err, nTCase[1]) - } - } - } -} - -func TestValidateAppProtectUserSig(t *testing.T) { - tests := []struct { - userSig *unstructured.Unstructured - expectErr bool - msg string - }{ - { - userSig: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "signatures": []interface{}{}, - }, - }, - }, - expectErr: false, - msg: "valid user sig", - }, - { - userSig: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "something": []interface{}{}, - }, - }, - }, - expectErr: true, - msg: "invalid user sig with no signatures", - }, - { - userSig: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "something": map[string]interface{}{ - "signatures": []interface{}{}, - }, - }, - }, - expectErr: true, - msg: "invalid user sign with no spec field", - }, - } - - for _, test := range tests { - err := validateAppProtectUserSig(test.userSig) - if test.expectErr && err == nil { - t.Errorf("validateAppProtectUserSig() returned no error for the case of %s", test.msg) - } - if !test.expectErr && err != nil { - t.Errorf("validateAppProtectUserSig() returned unexpected error %v for the case of %s", err, test.msg) - } - } -} - -func TestParseResourceReferenceAnnotation(t *testing.T) { - tests := []struct { - ns, antn, expected string - }{ - { - ns: "default", - antn: "resource", - expected: "default/resource", - }, - { - ns: "default", - antn: "ns-1/resource", - expected: "ns-1/resource", - }, - } - - for _, test := range tests { - result := ParseResourceReferenceAnnotation(test.ns, test.antn) - if result != test.expected { - t.Errorf("ParseResourceReferenceAnnotation(%q,%q) returned %q but expected %q", test.ns, test.antn, result, test.expected) - } - } -} - -func TestGenNsName(t *testing.T) { - obj := &unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "namespace": "default", - "name": "resource", - }, - }, - } - - expected := "default/resource" - - result := GetNsName(obj) - if result != expected { - t.Errorf("GetNsName() returned %q but expected %q", result, expected) - } -} - -func TestParseResourceReferenceAnnotationList(t *testing.T) { - namespace := "test_ns" - tests := []struct { - annotation string - expected []string - msg string - }{ - { - annotation: "test", - expected: []string{namespace + "/test"}, - msg: "single resource no namespace", - }, - { - annotation: "different_ns/test", - expected: []string{"different_ns/test"}, - msg: "single resource with namespace", - }, - { - annotation: "test,test1", - expected: []string{namespace + "/test", namespace + "/test1"}, - msg: "multiple resource no namespace", - }, - { - annotation: "different_ns/test,different_ns/test1", - expected: []string{"different_ns/test", "different_ns/test1"}, - msg: "multiple resource with namespaces", - }, - { - annotation: "different_ns/test,test1", - expected: []string{"different_ns/test", namespace + "/test1"}, - msg: "multiple resource with mixed namespaces", - }, - } - for _, test := range tests { - result := ParseResourceReferenceAnnotationList(namespace, test.annotation) - if !reflect.DeepEqual(result, test.expected) { - t.Errorf("Error in test case %s: got: %v, expected: %v", test.msg, result, test.expected) - } - } -} diff --git a/internal/k8s/appprotect_common/app_protect_common_resources.go b/internal/k8s/appprotect_common/app_protect_common_resources.go new file mode 100644 index 0000000000..973592c7f6 --- /dev/null +++ b/internal/k8s/appprotect_common/app_protect_common_resources.go @@ -0,0 +1,29 @@ +package appprotect_common + +import ( + "strings" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// GetNsName gets the key of a resource in the format: "resNamespace/resName" +func GetNsName(obj *unstructured.Unstructured) string { + return obj.GetNamespace() + "/" + obj.GetName() +} + +// ParseResourceReferenceAnnotation returns a namespace/name string +func ParseResourceReferenceAnnotation(ns, antn string) string { + if !strings.Contains(antn, "/") { + return ns + "/" + antn + } + return antn +} + +// ParseResourceReferenceAnnotationList returns a slice of ns/names strings +func ParseResourceReferenceAnnotationList(ns, annotations string) []string { + var out []string + for _, antn := range strings.Split(annotations, ",") { + out = append(out, ParseResourceReferenceAnnotation(ns, antn)) + } + return out +} diff --git a/internal/k8s/appprotect_common/app_protect_common_resources_test.go b/internal/k8s/appprotect_common/app_protect_common_resources_test.go new file mode 100644 index 0000000000..ec32c046b2 --- /dev/null +++ b/internal/k8s/appprotect_common/app_protect_common_resources_test.go @@ -0,0 +1,91 @@ +package appprotect_common + +import ( + "reflect" + "testing" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func TestParseResourceReferenceAnnotation(t *testing.T) { + tests := []struct { + ns, antn, expected string + }{ + { + ns: "default", + antn: "resource", + expected: "default/resource", + }, + { + ns: "default", + antn: "ns-1/resource", + expected: "ns-1/resource", + }, + } + + for _, test := range tests { + result := ParseResourceReferenceAnnotation(test.ns, test.antn) + if result != test.expected { + t.Errorf("ParseResourceReferenceAnnotation(%q,%q) returned %q but expected %q", test.ns, test.antn, result, test.expected) + } + } +} + +func TestGenNsName(t *testing.T) { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "namespace": "default", + "name": "resource", + }, + }, + } + + expected := "default/resource" + + result := GetNsName(obj) + if result != expected { + t.Errorf("GetNsName() returned %q but expected %q", result, expected) + } +} + +func TestParseResourceReferenceAnnotationList(t *testing.T) { + namespace := "test_ns" + tests := []struct { + annotation string + expected []string + msg string + }{ + { + annotation: "test", + expected: []string{namespace + "/test"}, + msg: "single resource no namespace", + }, + { + annotation: "different_ns/test", + expected: []string{"different_ns/test"}, + msg: "single resource with namespace", + }, + { + annotation: "test,test1", + expected: []string{namespace + "/test", namespace + "/test1"}, + msg: "multiple resource no namespace", + }, + { + annotation: "different_ns/test,different_ns/test1", + expected: []string{"different_ns/test", "different_ns/test1"}, + msg: "multiple resource with namespaces", + }, + { + annotation: "different_ns/test,test1", + expected: []string{"different_ns/test", namespace + "/test1"}, + msg: "multiple resource with mixed namespaces", + }, + } + for _, test := range tests { + result := ParseResourceReferenceAnnotationList(namespace, test.annotation) + if !reflect.DeepEqual(result, test.expected) { + t.Errorf("Error in test case %s: got: %v, expected: %v", test.msg, result, test.expected) + } + } +} diff --git a/internal/k8s/appprotectdos/app_protect_dos_configuration.go b/internal/k8s/appprotectdos/app_protect_dos_configuration.go new file mode 100644 index 0000000000..2f1d03d1ae --- /dev/null +++ b/internal/k8s/appprotectdos/app_protect_dos_configuration.go @@ -0,0 +1,385 @@ +package appprotectdos + +import ( + "fmt" + "strings" + + "github.com/nginxinc/kubernetes-ingress/internal/configs" + "github.com/nginxinc/kubernetes-ingress/internal/k8s/appprotect_common" + "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/validation" + "github.com/nginxinc/kubernetes-ingress/pkg/apis/dos/v1beta1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var ( + // DosPolicyGVR is the group version resource of the appprotectdos policy + DosPolicyGVR = schema.GroupVersionResource{ + Group: "appprotectdos.f5.com", + Version: "v1beta1", + Resource: "apdospolicies", + } + + // DosPolicyGVK is the group version kind of the appprotectdos policy + DosPolicyGVK = schema.GroupVersionKind{ + Group: "appprotectdos.f5.com", + Version: "v1beta1", + Kind: "APDosPolicy", + } + + // DosLogConfGVR is the group version resource of the appprotectdos policy + DosLogConfGVR = schema.GroupVersionResource{ + Group: "appprotectdos.f5.com", + Version: "v1beta1", + Resource: "apdoslogconfs", + } + // DosLogConfGVK is the group version kind of the appprotectdos policy + DosLogConfGVK = schema.GroupVersionKind{ + Group: "appprotectdos.f5.com", + Version: "v1beta1", + Kind: "APDosLogConf", + } +) + +// Operation defines an operation to perform for an App Protect Dos resource. +type Operation int + +const ( + // Delete the config of the resource + Delete Operation = iota + // AddOrUpdate the config of the resource + AddOrUpdate +) + +// Change represents a change in an App Protect Dos resource +type Change struct { + // Op is an operation that needs be performed on the resource. + Op Operation + // Resource is the target resource. + Resource interface{} +} + +// Problem represents a problem with an App Protect Dos resource +type Problem struct { + // Object is a configuration object. + Object runtime.Object + // Reason tells the reason. It matches the reason in the events of our configuration objects. + Reason string + // Message gives the details about the problem. It matches the message in the events of our configuration objects. + Message string +} + +// Configuration holds representations of App Protect Dos cluster resources +type Configuration struct { + dosPolicies map[string]*DosPolicyEx + dosLogConfs map[string]*DosLogConfEx + dosProtectedResource map[string]*DosProtectedResourceEx + isDosEnabled bool +} + +// NewConfiguration creates a new App Protect Dos Configuration +func NewConfiguration(isDosEnabled bool) *Configuration { + return &Configuration{ + dosPolicies: make(map[string]*DosPolicyEx), + dosLogConfs: make(map[string]*DosLogConfEx), + dosProtectedResource: make(map[string]*DosProtectedResourceEx), + isDosEnabled: isDosEnabled, + } +} + +// DosProtectedResourceEx represents an DosProtectedResource cluster resource +type DosProtectedResourceEx struct { + Obj *v1beta1.DosProtectedResource + IsValid bool + ErrorMsg string +} + +// DosPolicyEx represents an DosPolicy cluster resource +type DosPolicyEx struct { + Obj *unstructured.Unstructured + IsValid bool + ErrorMsg string +} + +// DosLogConfEx represents an DosLogConf cluster resource +type DosLogConfEx struct { + Obj *unstructured.Unstructured + IsValid bool + ErrorMsg string +} + +// AddOrUpdatePolicy adds or updates an App Protect Dos Policy to App Protect Dos Configuration +func (ci *Configuration) AddOrUpdatePolicy(policyObj *unstructured.Unstructured) (changes []Change, problems []Problem) { + resNsName := appprotect_common.GetNsName(policyObj) + policy, err := createAppProtectDosPolicyEx(policyObj) + ci.dosPolicies[resNsName] = policy + if err != nil { + changes = append(changes, Change{Op: Delete, Resource: policy}) + problems = append(problems, Problem{Object: policyObj, Reason: "Rejected", Message: err.Error()}) + } + + protectedResources := ci.GetDosProtectedThatReferencedDosPolicy(resNsName) + for _, p := range protectedResources { + proChanges, proProblems := ci.AddOrUpdateDosProtectedResource(p) + changes = append(changes, proChanges...) + problems = append(problems, proProblems...) + } + + return changes, problems +} + +// AddOrUpdateLogConf adds or updates App Protect Dos Log Configuration to App Protect Dos Configuration +func (ci *Configuration) AddOrUpdateLogConf(logConfObj *unstructured.Unstructured) (changes []Change, problems []Problem) { + resNsName := appprotect_common.GetNsName(logConfObj) + logConf, err := createAppProtectDosLogConfEx(logConfObj) + ci.dosLogConfs[resNsName] = logConf + if err != nil { + changes = append(changes, Change{Op: Delete, Resource: logConf}) + problems = append(problems, Problem{Object: logConfObj, Reason: "Rejected", Message: err.Error()}) + } + + protectedResources := ci.GetDosProtectedThatReferencedDosLogConf(resNsName) + for _, p := range protectedResources { + proChanges, proProblems := ci.AddOrUpdateDosProtectedResource(p) + changes = append(changes, proChanges...) + problems = append(problems, proProblems...) + } + + return changes, problems +} + +// AddOrUpdateDosProtectedResource adds or updates App Protect Dos ProtectedResource Configuration +func (ci *Configuration) AddOrUpdateDosProtectedResource(protectedConf *v1beta1.DosProtectedResource) ([]Change, []Problem) { + resNsName := protectedConf.Namespace + "/" + protectedConf.Name + protectedEx, err := createDosProtectedResourceEx(protectedConf) + ci.dosProtectedResource[resNsName] = protectedEx + if err != nil { + return []Change{{Op: Delete, Resource: protectedEx}}, + []Problem{{Object: protectedConf, Reason: "Rejected", Message: err.Error()}} + } + if protectedEx.Obj.Spec.ApDosPolicy != "" { + policyReference := protectedEx.Obj.Spec.ApDosPolicy + // if the policy reference does not have a namespace, use the dos protected' namespace + if !strings.Contains(policyReference, "/") { + policyReference = protectedEx.Obj.Namespace + "/" + policyReference + } + _, err := ci.getPolicy(policyReference) + if err != nil { + return []Change{{Op: Delete, Resource: protectedEx}}, + []Problem{{Object: protectedConf, Reason: "Rejected", Message: fmt.Sprintf("dos protected refers (%s) to an invalid DosPolicy: %s", policyReference, err.Error())}} + } + } + if protectedEx.Obj.Spec.DosSecurityLog != nil && protectedEx.Obj.Spec.DosSecurityLog.ApDosLogConf != "" { + logConfReference := protectedEx.Obj.Spec.DosSecurityLog.ApDosLogConf + // if the log conf reference does not have a namespace, use the dos protected' namespace + if !strings.Contains(logConfReference, "/") { + logConfReference = protectedEx.Obj.Namespace + "/" + logConfReference + } + _, err := ci.getLogConf(logConfReference) + if err != nil { + return []Change{{Op: Delete, Resource: protectedEx}}, + []Problem{{Object: protectedConf, Reason: "Rejected", Message: fmt.Sprintf("dos protected refers (%s) to an invalid DosLogConf: %s", logConfReference, err.Error())}} + } + } + return []Change{{Op: AddOrUpdate, Resource: protectedEx}}, nil +} + +func (ci *Configuration) getPolicy(key string) (*unstructured.Unstructured, error) { + obj, ok := ci.dosPolicies[key] + if !ok { + return nil, fmt.Errorf("DosPolicy %s not found", key) + } + if !obj.IsValid { + return nil, fmt.Errorf(obj.ErrorMsg) + } + return obj.Obj, nil +} + +func (ci *Configuration) getLogConf(key string) (*unstructured.Unstructured, error) { + obj, ok := ci.dosLogConfs[key] + if !ok { + return nil, fmt.Errorf("DosLogConf %s not found", key) + } + if !obj.IsValid { + return nil, fmt.Errorf(obj.ErrorMsg) + } + return obj.Obj, nil +} + +func (ci *Configuration) getDosProtected(key string) (*v1beta1.DosProtectedResource, error) { + if obj, ok := ci.dosProtectedResource[key]; ok { + if obj.IsValid { + return obj.Obj, nil + } + return nil, fmt.Errorf(obj.ErrorMsg) + } + return nil, fmt.Errorf("DosProtectedResource %s not found", key) +} + +// GetValidDosEx returns a valid DosProtectedResource - extended with referenced policies and logs +func (ci *Configuration) GetValidDosEx(parentNamespace string, nsName string) (*configs.DosEx, error) { + var key = getNsName(parentNamespace, nsName) + if !ci.isDosEnabled { + return nil, fmt.Errorf("DosProtectedResource is referenced but Dos feature is not enabled. resource: %v", key) + } + dosEx := &configs.DosEx{} + protectedEx, ok := ci.dosProtectedResource[key] + if !ok { + return nil, fmt.Errorf("DosProtectedResource %s not found", key) + } + if !protectedEx.IsValid { + return nil, fmt.Errorf(protectedEx.ErrorMsg) + } + dosEx.DosProtected = protectedEx.Obj + if protectedEx.Obj.Spec.ApDosPolicy != "" { + policyReference := protectedEx.Obj.Spec.ApDosPolicy + // if the policy reference does not have a namespace, use the dos protected' namespace + if !strings.Contains(policyReference, "/") { + policyReference = protectedEx.Obj.Namespace + "/" + policyReference + } + pol, err := ci.getPolicy(policyReference) + if err != nil { + return nil, fmt.Errorf("DosProtectedResource references a missing DosPolicy: %w", err) + } + dosEx.DosPolicy = pol + } + if protectedEx.Obj.Spec.DosSecurityLog != nil && protectedEx.Obj.Spec.DosSecurityLog.ApDosLogConf != "" { + logConfReference := protectedEx.Obj.Spec.DosSecurityLog.ApDosLogConf + // if the log conf reference does not have a namespace, use the dos protected' namespace + if !strings.Contains(logConfReference, "/") { + logConfReference = protectedEx.Obj.Namespace + "/" + logConfReference + } + log, err := ci.getLogConf(logConfReference) + if err != nil { + return nil, fmt.Errorf("DosProtectedResource references a missing DosLogConf: %w", err) + } + dosEx.DosLogConf = log + } + return dosEx, nil +} + +func getNsName(defaultNamespace string, name string) string { + if !strings.Contains(name, "/") { + return defaultNamespace + "/" + name + } + return name +} + +func (ci *Configuration) GetDosProtectedThatReferencedDosPolicy(key string) []*v1beta1.DosProtectedResource { + var protectedResources []*v1beta1.DosProtectedResource + for _, protectedEx := range ci.dosProtectedResource { + protected := protectedEx.Obj + dosPolRef := protected.Spec.ApDosPolicy + if key == dosPolRef || key == protected.Namespace+"/"+dosPolRef { + protectedResources = append(protectedResources, protected) + } + } + return protectedResources +} + +func (ci *Configuration) GetDosProtectedThatReferencedDosLogConf(key string) []*v1beta1.DosProtectedResource { + var protectedResources []*v1beta1.DosProtectedResource + for _, protectedEx := range ci.dosProtectedResource { + protected := protectedEx.Obj + if protected.Spec.DosSecurityLog != nil { + dosLogConf := protected.Spec.DosSecurityLog.ApDosLogConf + if key == dosLogConf || key == protected.Namespace+"/"+dosLogConf { + protectedResources = append(protectedResources, protected) + } + } + } + return protectedResources +} + +// DeletePolicy deletes an App Protect Policy from App Protect Dos Configuration +func (ci *Configuration) DeletePolicy(key string) (changes []Change, problems []Problem) { + _, has := ci.dosPolicies[key] + if has { + changes = append(changes, Change{Op: Delete, Resource: ci.dosPolicies[key]}) + delete(ci.dosPolicies, key) + } + + protectedResources := ci.GetDosProtectedThatReferencedDosPolicy(key) + for _, p := range protectedResources { + proChanges, proProblems := ci.AddOrUpdateDosProtectedResource(p) + changes = append(changes, proChanges...) + problems = append(problems, proProblems...) + } + + return changes, problems +} + +// DeleteLogConf deletes an App Protect Dos LogConf from App Protect Dos Configuration +func (ci *Configuration) DeleteLogConf(key string) (changes []Change, problems []Problem) { + _, has := ci.dosLogConfs[key] + if has { + changes = append(changes, Change{Op: Delete, Resource: ci.dosLogConfs[key]}) + delete(ci.dosLogConfs, key) + } + + protectedResources := ci.GetDosProtectedThatReferencedDosLogConf(key) + for _, p := range protectedResources { + proChanges, proProblems := ci.AddOrUpdateDosProtectedResource(p) + changes = append(changes, proChanges...) + problems = append(problems, proProblems...) + } + + return changes, problems +} + +// DeleteProtectedResource deletes an App Protect Dos ProtectedResource Configuration +func (ci *Configuration) DeleteProtectedResource(key string) (changes []Change, problems []Problem) { + if _, has := ci.dosProtectedResource[key]; has { + change := Change{Op: Delete, Resource: ci.dosProtectedResource[key]} + delete(ci.dosProtectedResource, key) + return append(changes, change), problems + } + return changes, problems +} + +func createAppProtectDosPolicyEx(policyObj *unstructured.Unstructured) (*DosPolicyEx, error) { + err := validation.ValidateAppProtectDosPolicy(policyObj) + if err != nil { + return &DosPolicyEx{ + Obj: policyObj, + IsValid: false, + ErrorMsg: fmt.Sprintf("failed to store ApDosPolicy: %v", err), + }, err + } + + return &DosPolicyEx{ + Obj: policyObj, + IsValid: true, + }, nil +} + +func createAppProtectDosLogConfEx(dosLogConfObj *unstructured.Unstructured) (*DosLogConfEx, error) { + err := validation.ValidateAppProtectDosLogConf(dosLogConfObj) + if err != nil { + return &DosLogConfEx{ + Obj: dosLogConfObj, + IsValid: false, + ErrorMsg: fmt.Sprintf("failed to store ApDosLogconf: %v", err), + }, err + } + return &DosLogConfEx{ + Obj: dosLogConfObj, + IsValid: true, + }, nil +} + +func createDosProtectedResourceEx(protectedConf *v1beta1.DosProtectedResource) (*DosProtectedResourceEx, error) { + err := validation.ValidateDosProtectedResource(protectedConf) + if err != nil { + return &DosProtectedResourceEx{ + Obj: protectedConf, + IsValid: false, + ErrorMsg: fmt.Sprintf("failed to store DosProtectedResource: %v", err), + }, err + } + return &DosProtectedResourceEx{ + Obj: protectedConf, + IsValid: true, + }, nil +} diff --git a/internal/k8s/appprotectdos/app_protect_dos_configuration_test.go b/internal/k8s/appprotectdos/app_protect_dos_configuration_test.go new file mode 100644 index 0000000000..50dac7ef3c --- /dev/null +++ b/internal/k8s/appprotectdos/app_protect_dos_configuration_test.go @@ -0,0 +1,1323 @@ +package appprotectdos + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/nginxinc/kubernetes-ingress/internal/configs" + "github.com/nginxinc/kubernetes-ingress/pkg/apis/dos/v1beta1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func TestCreateAppProtectDosPolicyEx(t *testing.T) { + tests := []struct { + policy *unstructured.Unstructured + expectedPolicyEx *DosPolicyEx + wantErr bool + msg string + }{ + { + policy: &unstructured.Unstructured{ + Object: map[string]interface{}{}, + }, + expectedPolicyEx: &DosPolicyEx{ + IsValid: false, + ErrorMsg: "failed to store ApDosPolicy: error validating DosPolicy : Required field map[] not found", + }, + wantErr: true, + msg: "dos policy no spec", + }, + { + policy: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{}, + }, + }, + expectedPolicyEx: &DosPolicyEx{ + IsValid: true, + ErrorMsg: "", + }, + wantErr: false, + msg: "dos policy is valid", + }, + } + + for _, test := range tests { + test.expectedPolicyEx.Obj = test.policy + + policyEx, err := createAppProtectDosPolicyEx(test.policy) + if (err != nil) != test.wantErr { + t.Errorf("createAppProtectDosPolicyEx() returned %v, for the case of %s", err, test.msg) + } + if diff := cmp.Diff(test.expectedPolicyEx, policyEx); diff != "" { + t.Errorf("createAppProtectDosPolicyEx() %q returned unexpected result (-want +got):\n%s", test.msg, diff) + } + } +} + +func TestCreateAppProtectDosLogConfEx(t *testing.T) { + tests := []struct { + logConf *unstructured.Unstructured + expectedLogConfEx *DosLogConfEx + wantErr bool + msg string + }{ + { + logConf: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "content": map[string]interface{}{}, + "filter": map[string]interface{}{}, + }, + }, + }, + expectedLogConfEx: &DosLogConfEx{ + IsValid: true, + ErrorMsg: "", + }, + wantErr: false, + msg: "Valid DosLogConf", + }, + { + logConf: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "content": map[string]interface{}{}, + }, + }, + }, + expectedLogConfEx: &DosLogConfEx{ + IsValid: false, + ErrorMsg: "failed to store ApDosLogconf: error validating App Protect Dos Log Configuration : Required field map[] not found", + }, + wantErr: true, + msg: "Invalid DosLogConf", + }, + } + + for _, test := range tests { + test.expectedLogConfEx.Obj = test.logConf + + policyEx, err := createAppProtectDosLogConfEx(test.logConf) + if (err != nil) != test.wantErr { + t.Errorf("createAppProtectDosLogConfEx() returned %v, for the case of %s", err, test.msg) + } + if diff := cmp.Diff(test.expectedLogConfEx, policyEx); diff != "" { + t.Errorf("createAppProtectDosLogConfEx() %q returned unexpected result (-want +got):\n%s", test.msg, diff) + } + } +} + +func TestAddOrUpdateDosProtected(t *testing.T) { + basicResource := &v1beta1.DosProtectedResource{ + TypeMeta: v1.TypeMeta{}, + ObjectMeta: v1.ObjectMeta{ + Name: "dosOnly", + Namespace: "default", + }, + Spec: v1beta1.DosProtectedResourceSpec{ + Enable: true, + Name: "dos-protected", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "127.0.0.1:5561", + }, + } + invalidResource := &v1beta1.DosProtectedResource{ + TypeMeta: v1.TypeMeta{}, + ObjectMeta: v1.ObjectMeta{ + Name: "invalidResource", + Namespace: "default", + }, + Spec: v1beta1.DosProtectedResourceSpec{ + Enable: true, + DosAccessLogDest: "127.0.0.1:5561", + }, + } + apc := NewConfiguration(true) + tests := []struct { + resource *v1beta1.DosProtectedResource + expectedChanges []Change + expectedProblems []Problem + msg string + }{ + { + resource: basicResource, + expectedChanges: []Change{ + { + Resource: &DosProtectedResourceEx{ + Obj: basicResource, + IsValid: true, + }, + Op: AddOrUpdate, + }, + }, + expectedProblems: nil, + msg: "Basic Case", + }, + { + resource: invalidResource, + expectedChanges: []Change{ + { + Resource: &DosProtectedResourceEx{ + Obj: invalidResource, + IsValid: false, + ErrorMsg: "failed to store DosProtectedResource: error validating DosProtectedResource: invalidResource missing value for field: name", + }, + Op: Delete, + }, + }, + expectedProblems: []Problem{ + { + Object: invalidResource, + Reason: "Rejected", + Message: "error validating DosProtectedResource: invalidResource missing value for field: name", + }, + }, + msg: "validation failed", + }, + } + for _, test := range tests { + changes, problems := apc.AddOrUpdateDosProtectedResource(test.resource) + if diff := cmp.Diff(test.expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateDosProtectedResource() %q changes returned unexpected result (-want +got):\n%s", test.msg, diff) + } + if diff := cmp.Diff(test.expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateDosProtectedResource() %q problems returned unexpected result (-want +got):\n%s", test.msg, diff) + } + } +} + +func TestAddOrUpdateDosPolicy(t *testing.T) { + basicTestPolicy := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "namespace": "testing", + "name": "name", + }, + "spec": map[string]interface{}{ + "mitigation_mode": "standard", + "automation_tools_detection": "on", + "tls_fingerprint": "on", + "signatures": "on", + "bad_actors": "on", + }, + }, + } + invalidTestPolicy := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "namespace": "testing", + }, + }, + } + basicResource := &v1beta1.DosProtectedResource{ + TypeMeta: v1.TypeMeta{}, + ObjectMeta: v1.ObjectMeta{ + Name: "dosOnly", + Namespace: "default", + }, + Spec: v1beta1.DosProtectedResourceSpec{ + Enable: true, + Name: "dos-protected", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "127.0.0.1:5561", + ApDosPolicy: "testing/name", + }, + } + apc := NewConfiguration(true) + apc.dosProtectedResource[""] = &DosProtectedResourceEx{Obj: basicResource, IsValid: true} + tests := []struct { + policy *unstructured.Unstructured + expectedChanges []Change + expectedProblems []Problem + msg string + }{ + { + policy: basicTestPolicy, + expectedChanges: []Change{ + { + Resource: &DosProtectedResourceEx{ + Obj: basicResource, + IsValid: true, + }, + Op: AddOrUpdate, + }, + }, + expectedProblems: nil, + msg: "Basic Case", + }, + { + policy: invalidTestPolicy, + expectedChanges: []Change{ + { + Resource: &DosPolicyEx{ + Obj: invalidTestPolicy, + IsValid: false, + ErrorMsg: "failed to store ApDosPolicy: error validating DosPolicy : Required field map[] not found", + }, + Op: Delete, + }, + }, + expectedProblems: []Problem{ + { + Object: invalidTestPolicy, + Reason: "Rejected", + Message: "error validating DosPolicy : Required field map[] not found", + }, + }, + msg: "validation failed", + }, + } + for _, test := range tests { + changes, problems := apc.AddOrUpdatePolicy(test.policy) + if diff := cmp.Diff(test.expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdatePolicy() %q changes returned unexpected result (-want +got):\n%s", test.msg, diff) + } + if diff := cmp.Diff(test.expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdatePolicy() %q problems returned unexpected result (-want +got):\n%s", test.msg, diff) + } + } +} + +func TestAddOrUpdateDosLogConf(t *testing.T) { + validLogConf := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "namespace": "testing", + "name": "testlogconf", + }, + "spec": map[string]interface{}{ + "content": map[string]interface{}{}, + "filter": map[string]interface{}{}, + }, + }, + } + invalidLogConf := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "namespace": "testing", + "name": "invalid-logconf", + }, + "spec": map[string]interface{}{ + "content": map[string]interface{}{}, + }, + }, + } + basicResource := &v1beta1.DosProtectedResource{ + TypeMeta: v1.TypeMeta{}, + ObjectMeta: v1.ObjectMeta{ + Name: "dosOnly", + Namespace: "default", + }, + Spec: v1beta1.DosProtectedResourceSpec{ + Enable: true, + Name: "dos-protected", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "127.0.0.1:5561", + DosSecurityLog: &v1beta1.DosSecurityLog{ + Enable: true, + ApDosLogConf: "testing/testlogconf", + DosLogDest: "test.dns.com:123", + }, + }, + } + apc := NewConfiguration(true) + apc.dosProtectedResource["single"] = &DosProtectedResourceEx{Obj: basicResource, IsValid: true} + tests := []struct { + logconf *unstructured.Unstructured + expectedChanges []Change + expectedProblems []Problem + msg string + }{ + { + logconf: validLogConf, + expectedChanges: []Change{ + { + Resource: &DosProtectedResourceEx{ + Obj: basicResource, + IsValid: true, + }, + Op: AddOrUpdate, + }, + }, + expectedProblems: nil, + msg: "Basic Case", + }, + { + logconf: invalidLogConf, + expectedChanges: []Change{ + { + Resource: &DosLogConfEx{ + Obj: invalidLogConf, + IsValid: false, + ErrorMsg: "failed to store ApDosLogconf: error validating App Protect Dos Log Configuration invalid-logconf: Required field map[] not found", + }, + Op: Delete, + }, + }, + expectedProblems: []Problem{ + { + Object: invalidLogConf, + Reason: "Rejected", + Message: "error validating App Protect Dos Log Configuration invalid-logconf: Required field map[] not found", + }, + }, + msg: "validation failed", + }, + } + for _, test := range tests { + changes, problems := apc.AddOrUpdateLogConf(test.logconf) + if diff := cmp.Diff(test.expectedChanges, changes); diff != "" { + t.Errorf("AddOrUpdateLogConf() %q changes returned unexpected result (-want +got):\n%s", test.msg, diff) + } + if diff := cmp.Diff(test.expectedProblems, problems); diff != "" { + t.Errorf("AddOrUpdateLogConf() %q problems returned unexpected result (-want +got):\n%s", test.msg, diff) + } + } +} + +func TestDeletePolicy(t *testing.T) { + appProtectConfiguration := NewConfiguration(true) + appProtectConfiguration.dosPolicies["testing/test"] = &DosPolicyEx{} + tests := []struct { + key string + expectedChanges []Change + expectedProblems []Problem + msg string + }{ + { + key: "testing/test", + expectedChanges: []Change{ + { + Op: Delete, + Resource: &DosPolicyEx{}, + }, + }, + expectedProblems: nil, + msg: "Positive", + }, + { + key: "testing/notpresent", + expectedChanges: nil, + expectedProblems: nil, + msg: "Negative", + }, + } + for _, test := range tests { + apChan, apProbs := appProtectConfiguration.DeletePolicy(test.key) + if diff := cmp.Diff(test.expectedChanges, apChan); diff != "" { + t.Errorf("DeletePolicy() %q changes returned unexpected result (-want +got):\n%s", test.msg, diff) + } + if diff := cmp.Diff(test.expectedProblems, apProbs); diff != "" { + t.Errorf("DeletePolicy() %q problems returned unexpected result (-want +got):\n%s", test.msg, diff) + } + } +} + +func TestDeleteDosLogConf(t *testing.T) { + appProtectConfiguration := NewConfiguration(true) + appProtectConfiguration.dosLogConfs["testing/test"] = &DosLogConfEx{} + tests := []struct { + key string + expectedChanges []Change + expectedProblems []Problem + msg string + }{ + { + key: "testing/test", + expectedChanges: []Change{ + { + Op: Delete, + Resource: &DosLogConfEx{}, + }, + }, + expectedProblems: nil, + msg: "Positive", + }, + { + key: "testing/notpresent", + expectedChanges: nil, + expectedProblems: nil, + msg: "Negative", + }, + } + for _, test := range tests { + apChan, apProbs := appProtectConfiguration.DeleteLogConf(test.key) + if diff := cmp.Diff(test.expectedChanges, apChan); diff != "" { + t.Errorf("DeleteLogConf() %q changes returned unexpected result (-want +got):\n%s", test.msg, diff) + } + if diff := cmp.Diff(test.expectedProblems, apProbs); diff != "" { + t.Errorf("DeleteLogConf() %q problems returned unexpected result (-want +got):\n%s", test.msg, diff) + } + } +} + +func TestDeleteDosProtected(t *testing.T) { + appProtectConfiguration := NewConfiguration(true) + appProtectConfiguration.dosProtectedResource["testing/test"] = &DosProtectedResourceEx{} + tests := []struct { + key string + expectedChanges []Change + expectedProblems []Problem + msg string + }{ + { + key: "testing/test", + expectedChanges: []Change{ + { + Op: Delete, + Resource: &DosProtectedResourceEx{}, + }, + }, + expectedProblems: nil, + msg: "Positive", + }, + { + key: "testing/notpresent", + expectedChanges: nil, + expectedProblems: nil, + msg: "Negative", + }, + } + for _, test := range tests { + changes, problems := appProtectConfiguration.DeleteProtectedResource(test.key) + if diff := cmp.Diff(test.expectedChanges, changes); diff != "" { + t.Errorf("DeleteProtectedResource() %q changes returned unexpected result (-want +got):\n%s", test.msg, diff) + } + if diff := cmp.Diff(test.expectedProblems, problems); diff != "" { + t.Errorf("DeleteProtectedResource() %q problems returned unexpected result (-want +got):\n%s", test.msg, diff) + } + } +} + +func TestGetDosProtected(t *testing.T) { + tests := []struct { + kind string + key string + wantErr bool + errMsg string + msg string + }{ + { + kind: "DosProtectedResource", + key: "testing/test1", + wantErr: false, + msg: "DosProtectedResource, positive", + }, + { + kind: "DosProtectedResource", + key: "testing/test2", + wantErr: true, + errMsg: "Validation Failed", + msg: "DosProtectedResource, Negative, invalid object", + }, + { + kind: "DosProtectedResource", + key: "testing/test3", + wantErr: true, + errMsg: "DosProtectedResource testing/test3 not found", + msg: "DosProtectedResource, Negative, Object Does not exist", + }, + } + appProtectConfiguration := NewConfiguration(true) + appProtectConfiguration.dosProtectedResource["testing/test1"] = &DosProtectedResourceEx{IsValid: true, Obj: &v1beta1.DosProtectedResource{}} + appProtectConfiguration.dosProtectedResource["testing/test2"] = &DosProtectedResourceEx{IsValid: false, Obj: &v1beta1.DosProtectedResource{}, ErrorMsg: "Validation Failed"} + + for _, test := range tests { + _, err := appProtectConfiguration.getDosProtected(test.key) + if (err != nil) != test.wantErr { + t.Errorf("getDosProtected() returned %v on case %s", err, test.msg) + } + if test.wantErr || err != nil { + if test.errMsg != err.Error() { + t.Errorf("getDosProtected() returned error message '%s' on case '%s' (expected '%s')", err.Error(), test.msg, test.errMsg) + } + } + } +} + +func TestGetPolicy(t *testing.T) { + tests := []struct { + kind string + key string + wantErr bool + errMsg string + msg string + }{ + { + kind: "APDosPolicy", + key: "testing/test1", + wantErr: false, + msg: "Policy, positive", + }, + { + kind: "APDosPolicy", + key: "testing/test2", + wantErr: true, + errMsg: "Validation Failed", + msg: "Policy, Negative, invalid object", + }, + { + kind: "APDosPolicy", + key: "testing/test3", + wantErr: true, + errMsg: "DosPolicy testing/test3 not found", + msg: "Policy, Negative, Object Does not exist", + }, + } + appProtectConfiguration := NewConfiguration(true) + appProtectConfiguration.dosPolicies["testing/test1"] = &DosPolicyEx{IsValid: true, Obj: &unstructured.Unstructured{}} + appProtectConfiguration.dosPolicies["testing/test2"] = &DosPolicyEx{IsValid: false, Obj: &unstructured.Unstructured{}, ErrorMsg: "Validation Failed"} + + for _, test := range tests { + _, err := appProtectConfiguration.getPolicy(test.key) + if (err != nil) != test.wantErr { + t.Errorf("getPolicy() returned %v on case %s", err, test.msg) + } + if test.wantErr || err != nil { + if test.errMsg != err.Error() { + t.Errorf("getPolicy() returned error message '%s' on case '%s' (expected '%s')", err.Error(), test.msg, test.errMsg) + } + } + } +} + +func TestGetLogConf(t *testing.T) { + tests := []struct { + kind string + key string + wantErr bool + errMsg string + msg string + }{ + { + kind: "APDosLogConf", + key: "testing/test1", + wantErr: false, + msg: "LogConf, positive", + }, + { + kind: "APDosLogConf", + key: "testing/test2", + wantErr: true, + errMsg: "Validation Failed", + msg: "LogConf, Negative, invalid object", + }, + { + kind: "APDosLogConf", + key: "testing/test3", + wantErr: true, + errMsg: "DosLogConf testing/test3 not found", + msg: "LogConf, Negative, Object Does not exist", + }, + } + appProtectConfiguration := NewConfiguration(true) + appProtectConfiguration.dosLogConfs["testing/test1"] = &DosLogConfEx{IsValid: true, Obj: &unstructured.Unstructured{}} + appProtectConfiguration.dosLogConfs["testing/test2"] = &DosLogConfEx{IsValid: false, Obj: &unstructured.Unstructured{}, ErrorMsg: "Validation Failed"} + + for _, test := range tests { + _, err := appProtectConfiguration.getLogConf(test.key) + if (err != nil) != test.wantErr { + t.Errorf("getLogConf() returned %v on case %s", err, test.msg) + } + if test.wantErr || err != nil { + if test.errMsg != err.Error() { + t.Errorf("getLogConf() returned error message '%s' on case '%s' (expected '%s')", err.Error(), test.msg, test.errMsg) + } + } + } +} + +func TestGetDosEx(t *testing.T) { + dosConf := NewConfiguration(true) + dosLogConf := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "namespace": "default", + "name": "dosLogConf", + }, + "spec": map[string]interface{}{ + "content": map[string]interface{}{}, + "filter": map[string]interface{}{}, + }, + }, + } + dosPolicy := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "namespace": "default", + "name": "dosPolicy", + }, + "spec": map[string]interface{}{}, + }, + } + dosProtectedOnly := &v1beta1.DosProtectedResource{ + TypeMeta: v1.TypeMeta{}, + ObjectMeta: v1.ObjectMeta{ + Name: "dosOnly", + Namespace: "default", + }, + Spec: v1beta1.DosProtectedResourceSpec{ + Enable: true, + Name: "dos-protected", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "127.0.0.1:5561", + }, + } + dosProtectedWithLogConf := &v1beta1.DosProtectedResource{ + TypeMeta: v1.TypeMeta{}, + ObjectMeta: v1.ObjectMeta{ + Name: "dosWithLogConf", + Namespace: "default", + }, + Spec: v1beta1.DosProtectedResourceSpec{ + Enable: true, + Name: "dos-protected", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "127.0.0.1:5561", + DosSecurityLog: &v1beta1.DosSecurityLog{ + Enable: true, + ApDosLogConf: "dosLogConf", + DosLogDest: "syslog-svc.default.svc.cluster.local:514", + }, + }, + } + dosProtectedWithPolicy := &v1beta1.DosProtectedResource{ + TypeMeta: v1.TypeMeta{}, + ObjectMeta: v1.ObjectMeta{ + Name: "dosWithPolicy", + Namespace: "default", + }, + Spec: v1beta1.DosProtectedResourceSpec{ + Enable: true, + Name: "dos-protected", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "127.0.0.1:5561", + ApDosPolicy: "dosPolicy", + }, + } + dosProtectedWithInvalidLogConf := &v1beta1.DosProtectedResource{ + TypeMeta: v1.TypeMeta{}, + ObjectMeta: v1.ObjectMeta{ + Name: "dosWithInvalidLogConf", + Namespace: "default", + }, + Spec: v1beta1.DosProtectedResourceSpec{ + Enable: true, + Name: "dos-protected", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "127.0.0.1:5561", + DosSecurityLog: &v1beta1.DosSecurityLog{ + Enable: true, + ApDosLogConf: "invalid-dosLogConf", + DosLogDest: "syslog-svc.default.svc.cluster.local:514", + }, + }, + } + dosProtectedWithInvalidPolicy := &v1beta1.DosProtectedResource{ + TypeMeta: v1.TypeMeta{}, + ObjectMeta: v1.ObjectMeta{ + Name: "dosWithInvalidPolicy", + Namespace: "default", + }, + Spec: v1beta1.DosProtectedResourceSpec{ + Enable: true, + Name: "dos-protected", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "127.0.0.1:5561", + ApDosPolicy: "invalid-dosPolicy", + }, + } + dosConf.AddOrUpdateDosProtectedResource(dosProtectedOnly) + dosConf.AddOrUpdateDosProtectedResource(dosProtectedWithLogConf) + dosConf.AddOrUpdateDosProtectedResource(dosProtectedWithPolicy) + dosConf.AddOrUpdateDosProtectedResource(dosProtectedWithInvalidLogConf) + dosConf.AddOrUpdateDosProtectedResource(dosProtectedWithInvalidPolicy) + dosConf.AddOrUpdateLogConf(dosLogConf) + dosConf.AddOrUpdatePolicy(dosPolicy) + + tests := []struct { + namespace string + ref string + expected *configs.DosEx + msg string + error string + }{ + { + namespace: "default", + ref: "dosOnly", + expected: &configs.DosEx{ + DosProtected: dosProtectedOnly, + }, + msg: "return the referenced resource, use parent namespace", + }, + { + namespace: "", + ref: "default/dosOnly", + expected: &configs.DosEx{ + DosProtected: dosProtectedOnly, + }, + msg: "return the referenced resource, use own namespace", + }, + { + namespace: "default", + ref: "dosNotExist", + error: "DosProtectedResource default/dosNotExist not found", + msg: "fails to find the referenced resource", + }, + { + namespace: "default", + ref: "default/dosWithLogConf", + expected: &configs.DosEx{ + DosProtected: dosProtectedWithLogConf, + DosLogConf: dosLogConf, + }, + msg: "return the referenced resource, including reference to logconf", + }, + { + namespace: "default", + ref: "default/dosWithPolicy", + expected: &configs.DosEx{ + DosProtected: dosProtectedWithPolicy, + DosPolicy: dosPolicy, + }, + msg: "return the referenced resource, including reference to policy", + }, + { + namespace: "default", + ref: "default/dosWithInvalidLogConf", + error: "DosProtectedResource references a missing DosLogConf: DosLogConf default/invalid-dosLogConf not found", + msg: "fails to find the referenced logconf resource", + }, + { + namespace: "default", + ref: "default/dosWithInvalidPolicy", + error: "DosProtectedResource references a missing DosPolicy: DosPolicy default/invalid-dosPolicy not found", + msg: "fails to find the referenced policy resource", + }, + } + for _, test := range tests { + dosEx, err := dosConf.GetValidDosEx(test.namespace, test.ref) + if err != nil { + if test.error != "" { + // we expect an error, check if it matches + if test.error != err.Error() { + t.Errorf("GetValidDosEx() returned different error than expected for the case of: %v \nexpected error '%v' \nactual error '%v' \n", test.msg, test.error, err.Error()) + } + // all good + } else { + t.Errorf("GetValidDosEx() returned unexpected error for the case of: %v \n%v", test.msg, err) + } + } + if diff := cmp.Diff(test.expected, dosEx); diff != "" { + t.Errorf("GetValidDosEx() returned unexpected result for the case of: %v (-want +got):\n%s", test.msg, diff) + } + } +} + +func TestGetDosExDosDisabled(t *testing.T) { + dosConf := NewConfiguration(false) + dosLogConf := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "namespace": "default", + "name": "dosLogConf", + }, + "spec": map[string]interface{}{ + "content": map[string]interface{}{}, + "filter": map[string]interface{}{}, + }, + }, + } + dosPolicy := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "namespace": "default", + "name": "dosPolicy", + }, + "spec": map[string]interface{}{}, + }, + } + dosProtectedOnly := &v1beta1.DosProtectedResource{ + TypeMeta: v1.TypeMeta{}, + ObjectMeta: v1.ObjectMeta{ + Name: "dosOnly", + Namespace: "default", + }, + Spec: v1beta1.DosProtectedResourceSpec{ + Enable: true, + Name: "dos-protected", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "127.0.0.1:5561", + }, + } + dosProtectedWithLogConf := &v1beta1.DosProtectedResource{ + TypeMeta: v1.TypeMeta{}, + ObjectMeta: v1.ObjectMeta{ + Name: "dosWithLogConf", + Namespace: "default", + }, + Spec: v1beta1.DosProtectedResourceSpec{ + Enable: true, + Name: "dos-protected", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "127.0.0.1:5561", + DosSecurityLog: &v1beta1.DosSecurityLog{ + Enable: true, + ApDosLogConf: "dosLogConf", + DosLogDest: "syslog-svc.default.svc.cluster.local:514", + }, + }, + } + dosProtectedWithPolicy := &v1beta1.DosProtectedResource{ + TypeMeta: v1.TypeMeta{}, + ObjectMeta: v1.ObjectMeta{ + Name: "dosWithPolicy", + Namespace: "default", + }, + Spec: v1beta1.DosProtectedResourceSpec{ + Enable: true, + Name: "dos-protected", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "127.0.0.1:5561", + ApDosPolicy: "dosPolicy", + }, + } + dosProtectedWithInvalidLogConf := &v1beta1.DosProtectedResource{ + TypeMeta: v1.TypeMeta{}, + ObjectMeta: v1.ObjectMeta{ + Name: "dosWithInvalidLogConf", + Namespace: "default", + }, + Spec: v1beta1.DosProtectedResourceSpec{ + Enable: true, + Name: "dos-protected", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "127.0.0.1:5561", + DosSecurityLog: &v1beta1.DosSecurityLog{ + Enable: true, + ApDosLogConf: "invalid-dosLogConf", + DosLogDest: "syslog-svc.default.svc.cluster.local:514", + }, + }, + } + dosProtectedWithInvalidPolicy := &v1beta1.DosProtectedResource{ + TypeMeta: v1.TypeMeta{}, + ObjectMeta: v1.ObjectMeta{ + Name: "dosWithInvalidPolicy", + Namespace: "default", + }, + Spec: v1beta1.DosProtectedResourceSpec{ + Enable: true, + Name: "dos-protected", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "127.0.0.1:5561", + ApDosPolicy: "invalid-dosPolicy", + }, + } + dosConf.AddOrUpdateDosProtectedResource(dosProtectedOnly) + dosConf.AddOrUpdateDosProtectedResource(dosProtectedWithLogConf) + dosConf.AddOrUpdateDosProtectedResource(dosProtectedWithPolicy) + dosConf.AddOrUpdateDosProtectedResource(dosProtectedWithInvalidLogConf) + dosConf.AddOrUpdateDosProtectedResource(dosProtectedWithInvalidPolicy) + dosConf.AddOrUpdateLogConf(dosLogConf) + dosConf.AddOrUpdatePolicy(dosPolicy) + + tests := []struct { + namespace string + ref string + expected *configs.DosEx + msg string + error string + }{ + { + namespace: "default", + ref: "dosOnly", + error: "DosProtectedResource is referenced but Dos feature is not enabled. resource: default/dosOnly", + msg: "fails to return a resource, using parent namespace", + }, + { + namespace: "", + ref: "default/dosOnly", + error: "DosProtectedResource is referenced but Dos feature is not enabled. resource: default/dosOnly", + msg: "fails to return the referenced resource, using own namespace", + }, + { + namespace: "default", + ref: "dosNotExist", + error: "DosProtectedResource is referenced but Dos feature is not enabled. resource: default/dosNotExist", + msg: "fails to find the referenced resource", + }, + { + namespace: "default", + ref: "default/dosWithLogConf", + error: "DosProtectedResource is referenced but Dos feature is not enabled. resource: default/dosWithLogConf", + msg: "fails to return the referenced resource, including reference to logconf", + }, + { + namespace: "default", + ref: "default/dosWithPolicy", + error: "DosProtectedResource is referenced but Dos feature is not enabled. resource: default/dosWithPolicy", + msg: "fails to return the referenced resource, including reference to policy", + }, + { + namespace: "default", + ref: "default/dosWithInvalidLogConf", + error: "DosProtectedResource is referenced but Dos feature is not enabled. resource: default/dosWithInvalidLogConf", + msg: "fails to find the referenced logconf resource", + }, + { + namespace: "default", + ref: "default/dosWithInvalidPolicy", + error: "DosProtectedResource is referenced but Dos feature is not enabled. resource: default/dosWithInvalidPolicy", + msg: "fails to find the referenced policy resource", + }, + } + for _, test := range tests { + dosEx, err := dosConf.GetValidDosEx(test.namespace, test.ref) + if err != nil { + if test.error != "" { + // we expect an error, check if it matches + if test.error != err.Error() { + t.Errorf("GetValidDosEx() returned different error than expected for the case of: %v \nexpected error '%v' \nactual error '%v' \n", test.msg, test.error, err.Error()) + } + // all good + } else { + t.Errorf("GetValidDosEx() returned unexpected error for the case of: %v \n%v", test.msg, err) + } + } + if diff := cmp.Diff(test.expected, dosEx); diff != "" { + t.Errorf("GetValidDosEx() returned unexpected result for the case of: %v (-want +got):\n%s", test.msg, diff) + } + } +} + +func TestGetDosProtectedThatReferencedDosPolicy(t *testing.T) { + dosConf := NewConfiguration(true) + dosPolicy := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "namespace": "nginx", + "name": "dosPolicyOne", + }, + "spec": map[string]interface{}{}, + }, + } + dosPolicyTwo := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "namespace": "dev", + "name": "dosPolicyTwo", + }, + "spec": map[string]interface{}{}, + }, + } + dosPolicyThree := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "namespace": "dev", + "name": "dosPolicyThree", + }, + "spec": map[string]interface{}{}, + }, + } + dosProtectedNoRefs := &v1beta1.DosProtectedResource{ + TypeMeta: v1.TypeMeta{}, + ObjectMeta: v1.ObjectMeta{ + Name: "dosProtectedNoRefs", + Namespace: "nginx", + }, + Spec: v1beta1.DosProtectedResourceSpec{ + Enable: true, + Name: "dos-protected", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "127.0.0.1:5561", + }, + } + dosProtectedWithPolicyOne := &v1beta1.DosProtectedResource{ + TypeMeta: v1.TypeMeta{}, + ObjectMeta: v1.ObjectMeta{ + Name: "dosWithPolicyOne", + Namespace: "nginx", + }, + Spec: v1beta1.DosProtectedResourceSpec{ + Enable: true, + Name: "dos-protected", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "127.0.0.1:5561", + ApDosPolicy: "dosPolicyOne", + }, + } + dosProtectedWithPolicyTwo := &v1beta1.DosProtectedResource{ + TypeMeta: v1.TypeMeta{}, + ObjectMeta: v1.ObjectMeta{ + Name: "dosWithPolicyTwo", + Namespace: "nginx", + }, + Spec: v1beta1.DosProtectedResourceSpec{ + Enable: true, + Name: "dos-protected", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "127.0.0.1:5561", + ApDosPolicy: "dev/dosPolicyTwo", + }, + } + anotherDosProtectedWithPolicyTwo := &v1beta1.DosProtectedResource{ + TypeMeta: v1.TypeMeta{}, + ObjectMeta: v1.ObjectMeta{ + Name: "anotherDosWithPolicyTwo", + Namespace: "dev", + }, + Spec: v1beta1.DosProtectedResourceSpec{ + Enable: true, + Name: "dos-protected", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "127.0.0.1:5561", + ApDosPolicy: "dosPolicyTwo", + }, + } + + dosConf.AddOrUpdateDosProtectedResource(dosProtectedNoRefs) + dosConf.AddOrUpdateDosProtectedResource(dosProtectedWithPolicyOne) + dosConf.AddOrUpdateDosProtectedResource(dosProtectedWithPolicyTwo) + dosConf.AddOrUpdateDosProtectedResource(anotherDosProtectedWithPolicyTwo) + dosConf.AddOrUpdatePolicy(dosPolicy) + dosConf.AddOrUpdatePolicy(dosPolicyTwo) + dosConf.AddOrUpdatePolicy(dosPolicyThree) + + tests := []struct { + policyNamespace string + policyName string + expected []*v1beta1.DosProtectedResource + msg string + }{ + { + policyNamespace: "nginx", + policyName: "dosPolicyThree", + expected: nil, + msg: "returns nothing", + }, + { + policyNamespace: "nginx", + policyName: "dosPolicyOne", + expected: []*v1beta1.DosProtectedResource{ + dosProtectedWithPolicyOne, + }, + msg: "return a single referenced obj, from policy reference", + }, + { + policyNamespace: "different", + policyName: "dosPolicyOne", + expected: nil, + msg: "return nothing as namespace doesn't match", + }, + { + policyNamespace: "dev", + policyName: "dosPolicyTwo", + expected: []*v1beta1.DosProtectedResource{ + dosProtectedWithPolicyTwo, + anotherDosProtectedWithPolicyTwo, + }, + msg: "return two referenced objects, from policy reference with mixed namespaces", + }, + } + for _, test := range tests { + resources := dosConf.GetDosProtectedThatReferencedDosPolicy(test.policyNamespace + "/" + test.policyName) + + if diff := cmp.Diff(test.expected, resources); diff != "" { + t.Errorf("GetDosProtectedThatReferencedDosPolicy() returned unexpected result for the case of: %v (-want +got):\n%s", test.msg, diff) + } + } +} + +func TestGetDosProtectedThatReferencedDosLogConf(t *testing.T) { + dosConf := NewConfiguration(true) + dosLogConf := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "namespace": "nginx", + "name": "dosLogConfOne", + }, + "spec": map[string]interface{}{}, + }, + } + dosLogConfTwo := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "namespace": "dev", + "name": "dosLogConfTwo", + }, + "spec": map[string]interface{}{}, + }, + } + dosLogConfThree := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "namespace": "dev", + "name": "dosLogConfThree", + }, + "spec": map[string]interface{}{}, + }, + } + dosProtectedNoRefs := &v1beta1.DosProtectedResource{ + TypeMeta: v1.TypeMeta{}, + ObjectMeta: v1.ObjectMeta{ + Name: "dosProtectedNoRefs", + Namespace: "nginx", + }, + Spec: v1beta1.DosProtectedResourceSpec{ + Enable: true, + Name: "dos-protected", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "127.0.0.1:5561", + }, + } + dosProtectedWithLogConfOne := &v1beta1.DosProtectedResource{ + TypeMeta: v1.TypeMeta{}, + ObjectMeta: v1.ObjectMeta{ + Name: "dosWithLogConfOne", + Namespace: "nginx", + }, + Spec: v1beta1.DosProtectedResourceSpec{ + Enable: true, + Name: "dos-protected", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "127.0.0.1:5561", + DosSecurityLog: &v1beta1.DosSecurityLog{ + Enable: true, + ApDosLogConf: "dosLogConfOne", + DosLogDest: "syslog.dev:514", + }, + }, + } + dosProtectedWithLogConfTwo := &v1beta1.DosProtectedResource{ + TypeMeta: v1.TypeMeta{}, + ObjectMeta: v1.ObjectMeta{ + Name: "dosWithLogConfTwo", + Namespace: "nginx", + }, + Spec: v1beta1.DosProtectedResourceSpec{ + Enable: true, + Name: "dos-protected", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "127.0.0.1:5561", + DosSecurityLog: &v1beta1.DosSecurityLog{ + Enable: true, + ApDosLogConf: "dev/dosLogConfTwo", + DosLogDest: "syslog.dev:514", + }, + }, + } + anotherDosProtectedWithLogConfTwo := &v1beta1.DosProtectedResource{ + TypeMeta: v1.TypeMeta{}, + ObjectMeta: v1.ObjectMeta{ + Name: "anotherDosWithLogConfTwo", + Namespace: "dev", + }, + Spec: v1beta1.DosProtectedResourceSpec{ + Enable: true, + Name: "dos-protected", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "127.0.0.1:5561", + DosSecurityLog: &v1beta1.DosSecurityLog{ + Enable: true, + ApDosLogConf: "dosLogConfTwo", + DosLogDest: "syslog.dev:514", + }, + }, + } + + dosConf.AddOrUpdateDosProtectedResource(dosProtectedNoRefs) + dosConf.AddOrUpdateDosProtectedResource(dosProtectedWithLogConfOne) + dosConf.AddOrUpdateDosProtectedResource(dosProtectedWithLogConfTwo) + dosConf.AddOrUpdateDosProtectedResource(anotherDosProtectedWithLogConfTwo) + dosConf.AddOrUpdateLogConf(dosLogConf) + dosConf.AddOrUpdateLogConf(dosLogConfTwo) + dosConf.AddOrUpdateLogConf(dosLogConfThree) + + tests := []struct { + policyNamespace string + policyName string + expected []*v1beta1.DosProtectedResource + msg string + }{ + { + policyNamespace: "nginx", + policyName: "dosLogConfThree", + expected: nil, + msg: "returns nothing", + }, + { + policyNamespace: "nginx", + policyName: "dosLogConfOne", + expected: []*v1beta1.DosProtectedResource{ + dosProtectedWithLogConfOne, + }, + msg: "return a single referenced obj, from log conf reference", + }, + { + policyNamespace: "different", + policyName: "dosLogConfOne", + expected: nil, + msg: "return nothing as namespace doesn't match", + }, + { + policyNamespace: "dev", + policyName: "dosLogConfTwo", + expected: []*v1beta1.DosProtectedResource{ + dosProtectedWithLogConfTwo, + anotherDosProtectedWithLogConfTwo, + }, + msg: "return two referenced objects, from log conf reference with mixed namespaces", + }, + } + for _, test := range tests { + resources := dosConf.GetDosProtectedThatReferencedDosLogConf(test.policyNamespace + "/" + test.policyName) + if diff := cmp.Diff(test.expected, resources); diff != "" { + t.Errorf("GetDosProtectedThatReferencedDosLogConf() returned unexpected result for the case of: %v (-want +got):\n%s", test.msg, diff) + } + } +} diff --git a/internal/k8s/configuration.go b/internal/k8s/configuration.go index d4102bde95..0f56677a95 100644 --- a/internal/k8s/configuration.go +++ b/internal/k8s/configuration.go @@ -350,9 +350,11 @@ type Configuration struct { policyReferenceChecker *policyReferenceChecker appPolicyReferenceChecker *appProtectResourceReferenceChecker appLogConfReferenceChecker *appProtectResourceReferenceChecker + appDosProtectedChecker *dosResourceReferenceChecker isPlus bool appProtectEnabled bool + appProtectDosEnabled bool internalRoutesEnabled bool isTLSPassthroughEnabled bool snippetsEnabled bool @@ -365,6 +367,7 @@ func NewConfiguration( hasCorrectIngressClass func(interface{}) bool, isPlus bool, appProtectEnabled bool, + appProtectDosEnabled bool, internalRoutesEnabled bool, virtualServerValidator *validation.VirtualServerValidator, globalConfigurationValidator *validation.GlobalConfigurationValidator, @@ -388,10 +391,10 @@ func NewConfiguration( serviceReferenceChecker: newServiceReferenceChecker(false), endpointReferenceChecker: newServiceReferenceChecker(true), policyReferenceChecker: newPolicyReferenceChecker(), - appPolicyReferenceChecker: newAppProtectResourceReferenceChecker(configs.AppProtectPolicyAnnotation), - appLogConfReferenceChecker: newAppProtectResourceReferenceChecker(configs.AppProtectLogConfAnnotation), + appDosProtectedChecker: newDosResourceReferenceChecker(configs.AppProtectDosProtectedAnnotation), isPlus: isPlus, appProtectEnabled: appProtectEnabled, + appProtectDosEnabled: appProtectDosEnabled, internalRoutesEnabled: internalRoutesEnabled, isTLSPassthroughEnabled: isTLSPassthroughEnabled, snippetsEnabled: snippetsEnabled, @@ -409,7 +412,7 @@ func (c *Configuration) AddOrUpdateIngress(ing *networking.Ingress) ([]ResourceC if !c.hasCorrectIngressClass(ing) { delete(c.ingresses, key) } else { - validationError = validateIngress(ing, c.isPlus, c.appProtectEnabled, c.internalRoutesEnabled, c.snippetsEnabled).ToAggregate() + validationError = validateIngress(ing, c.isPlus, c.appProtectEnabled, c.appProtectDosEnabled, c.internalRoutesEnabled, c.snippetsEnabled).ToAggregate() if validationError != nil { delete(c.ingresses, key) } else { @@ -864,6 +867,11 @@ func (c *Configuration) FindResourcesForAppProtectLogConfAnnotation(logConfNames return c.findResourcesForResourceReference(logConfNamespace, logConfName, c.appLogConfReferenceChecker) } +// FindResourcesForAppProtectDosProtected finds resources that reference the specified AppProtectDos DosLogConf. +func (c *Configuration) FindResourcesForAppProtectDosProtected(namespace string, name string) []Resource { + return c.findResourcesForResourceReference(namespace, name, c.appDosProtectedChecker) +} + func (c *Configuration) findResourcesForResourceReference(namespace string, name string, checker resourceReferenceChecker) []Resource { c.lock.RLock() defer c.lock.RUnlock() diff --git a/internal/k8s/configuration_test.go b/internal/k8s/configuration_test.go index 4e2723a479..0aadfd7777 100644 --- a/internal/k8s/configuration_test.go +++ b/internal/k8s/configuration_test.go @@ -19,6 +19,7 @@ func createTestConfiguration() *Configuration { } isPlus := false appProtectEnabled := false + appProtectDosEnabled := false internalRoutesEnabled := false isTLSPassthroughEnabled := true snippetsEnabled := true @@ -26,6 +27,7 @@ func createTestConfiguration() *Configuration { lbc.HasCorrectIngressClass, isPlus, appProtectEnabled, + appProtectDosEnabled, internalRoutesEnabled, validation.NewVirtualServerValidator(isTLSPassthroughEnabled), validation.NewGlobalConfigurationValidator(map[int]bool{ diff --git a/internal/k8s/controller.go b/internal/k8s/controller.go index 15a8c5d6ba..b5c82f9842 100644 --- a/internal/k8s/controller.go +++ b/internal/k8s/controller.go @@ -23,7 +23,11 @@ import ( "sync" "time" + "github.com/nginxinc/kubernetes-ingress/pkg/apis/dos/v1beta1" + "github.com/nginxinc/kubernetes-ingress/internal/k8s/appprotect" + "github.com/nginxinc/kubernetes-ingress/internal/k8s/appprotect_common" + "github.com/nginxinc/kubernetes-ingress/internal/k8s/appprotectdos" "k8s.io/client-go/informers" "github.com/golang/glog" @@ -108,6 +112,9 @@ type LoadBalancerController struct { virtualServerRouteLister cache.Store appProtectPolicyLister cache.Store appProtectLogConfLister cache.Store + appProtectDosPolicyLister cache.Store + appProtectDosLogConfLister cache.Store + appProtectDosProtectedLister cache.Store globalConfigurationLister cache.Store appProtectUserSigLister cache.Store transportServerLister cache.Store @@ -122,6 +129,7 @@ type LoadBalancerController struct { watchIngressLink bool isNginxPlus bool appProtectEnabled bool + appProtectDosEnabled bool recorder record.EventRecorder defaultServerSecret string ingressClass string @@ -148,6 +156,7 @@ type LoadBalancerController struct { configuration *Configuration secretStore secrets.SecretStore appProtectConfiguration appprotect.Configuration + dosConfiguration *appprotectdos.Configuration configMap *api_v1.ConfigMap } @@ -163,6 +172,7 @@ type NewLoadBalancerControllerInput struct { NginxConfigurator *configs.Configurator DefaultServerSecret string AppProtectEnabled bool + AppProtectDosEnabled bool IsNginxPlus bool IngressClass string ExternalServiceName string @@ -197,6 +207,7 @@ func NewLoadBalancerController(input NewLoadBalancerControllerInput) *LoadBalanc configurator: input.NginxConfigurator, defaultServerSecret: input.DefaultServerSecret, appProtectEnabled: input.AppProtectEnabled, + appProtectDosEnabled: input.AppProtectDosEnabled, isNginxPlus: input.IsNginxPlus, ingressClass: input.IngressClass, reportIngressStatus: input.ReportIngressStatus, @@ -244,14 +255,6 @@ func NewLoadBalancerController(input NewLoadBalancerControllerInput) *LoadBalanc lbc.addEndpointHandler(createEndpointHandlers(lbc)) lbc.addPodHandler() - if lbc.appProtectEnabled { - lbc.dynInformerFactory = dynamicinformer.NewFilteredDynamicSharedInformerFactory(lbc.dynClient, 0, lbc.namespace, nil) - - lbc.addAppProtectPolicyHandler(createAppProtectPolicyHandlers(lbc)) - lbc.addAppProtectLogConfHandler(createAppProtectLogConfHandlers(lbc)) - lbc.addAppProtectUserSigHandler(createAppProtectUserSigHandlers(lbc)) - } - if lbc.areCustomResourcesEnabled { lbc.confSharedInformerFactorry = k8s_nginx_informers.NewSharedInformerFactoryWithOptions(lbc.confClient, input.ResyncPeriod, k8s_nginx_informers.WithNamespace(lbc.namespace)) @@ -267,6 +270,22 @@ func NewLoadBalancerController(input NewLoadBalancerControllerInput) *LoadBalanc } } + if lbc.appProtectEnabled || lbc.appProtectDosEnabled { + lbc.dynInformerFactory = dynamicinformer.NewDynamicSharedInformerFactory(lbc.dynClient, 0) + + if lbc.appProtectEnabled { + lbc.addAppProtectPolicyHandler(createAppProtectPolicyHandlers(lbc)) + lbc.addAppProtectLogConfHandler(createAppProtectLogConfHandlers(lbc)) + lbc.addAppProtectUserSigHandler(createAppProtectUserSigHandlers(lbc)) + } + + if lbc.appProtectDosEnabled { + lbc.addAppProtectDosPolicyHandler(createAppProtectDosPolicyHandlers(lbc)) + lbc.addAppProtectDosLogConfHandler(createAppProtectDosLogConfHandlers(lbc)) + lbc.addAppProtectDosProtectedResourceHandler(createAppProtectDosProtectedResourceHandlers(lbc)) + } + } + if input.ConfigMaps != "" { nginxConfigMapsNS, nginxConfigMapsName, err := ParseNamespaceName(input.ConfigMaps) if err != nil { @@ -304,6 +323,7 @@ func NewLoadBalancerController(input NewLoadBalancerControllerInput) *LoadBalanc lbc.HasCorrectIngressClass, input.IsNginxPlus, input.AppProtectEnabled, + input.AppProtectDosEnabled, input.InternalRoutesEnabled, input.VirtualServerValidator, input.GlobalConfigurationValidator, @@ -312,6 +332,7 @@ func NewLoadBalancerController(input NewLoadBalancerControllerInput) *LoadBalanc input.SnippetsEnabled) lbc.appProtectConfiguration = appprotect.NewConfiguration() + lbc.dosConfiguration = appprotectdos.NewConfiguration(input.AppProtectDosEnabled) lbc.secretStore = secrets.NewLocalSecretStore(lbc.configurator) @@ -359,6 +380,33 @@ func (lbc *LoadBalancerController) addAppProtectUserSigHandler(handlers cache.Re lbc.cacheSyncs = append(lbc.cacheSyncs, informer.HasSynced) } +// addAppProtectDosPolicyHandler creates dynamic informers for custom appprotectdos policy resource +func (lbc *LoadBalancerController) addAppProtectDosPolicyHandler(handlers cache.ResourceEventHandlerFuncs) { + informer := lbc.dynInformerFactory.ForResource(appprotectdos.DosPolicyGVR).Informer() + informer.AddEventHandler(handlers) + lbc.appProtectDosPolicyLister = informer.GetStore() + + lbc.cacheSyncs = append(lbc.cacheSyncs, informer.HasSynced) +} + +// addAppProtectDosLogConfHandler creates dynamic informer for custom appprotectdos logging config resource +func (lbc *LoadBalancerController) addAppProtectDosLogConfHandler(handlers cache.ResourceEventHandlerFuncs) { + informer := lbc.dynInformerFactory.ForResource(appprotectdos.DosLogConfGVR).Informer() + informer.AddEventHandler(handlers) + lbc.appProtectDosLogConfLister = informer.GetStore() + + lbc.cacheSyncs = append(lbc.cacheSyncs, informer.HasSynced) +} + +// addAppProtectDosLogConfHandler creates dynamic informer for custom appprotectdos logging config resource +func (lbc *LoadBalancerController) addAppProtectDosProtectedResourceHandler(handlers cache.ResourceEventHandlerFuncs) { + informer := lbc.confSharedInformerFactorry.Appprotectdos().V1beta1().DosProtectedResources().Informer() + informer.AddEventHandler(handlers) + lbc.appProtectDosProtectedLister = informer.GetStore() + + lbc.cacheSyncs = append(lbc.cacheSyncs, informer.HasSynced) +} + // addSecretHandler adds the handler for secrets to the controller func (lbc *LoadBalancerController) addSecretHandler(handlers cache.ResourceEventHandlerFuncs) { informer := lbc.sharedInformerFactory.Core().V1().Secrets().Informer() @@ -506,7 +554,7 @@ func (lbc *LoadBalancerController) Run() { if lbc.watchIngressLink { go lbc.ingressLinkInformer.Run(lbc.ctx.Done()) } - if lbc.appProtectEnabled { + if lbc.appProtectEnabled || lbc.appProtectDosEnabled { go lbc.dynInformerFactory.Start(lbc.ctx.Done()) } @@ -595,6 +643,7 @@ func (lbc *LoadBalancerController) createExtendedResources(resources []Resource) vsEx := lbc.createVirtualServerEx(vs, impl.VirtualServerRoutes) result.VirtualServerExes = append(result.VirtualServerExes, vsEx) case *IngressConfiguration: + if impl.IsMaster { mergeableIng := lbc.createMergeableIngresses(impl) result.MergeableIngresses = append(result.MergeableIngresses, mergeableIng) @@ -639,7 +688,7 @@ func (lbc *LoadBalancerController) updateAllConfigs() { cfgParams := configs.NewDefaultConfigParams(lbc.isNginxPlus) if lbc.configMap != nil { - cfgParams = configs.ParseConfigMap(lbc.configMap, lbc.isNginxPlus, lbc.appProtectEnabled) + cfgParams = configs.ParseConfigMap(lbc.configMap, lbc.isNginxPlus, lbc.appProtectEnabled, lbc.appProtectDosEnabled) } resources := lbc.configuration.GetResources() @@ -741,6 +790,12 @@ func (lbc *LoadBalancerController) sync(task task) { lbc.syncAppProtectLogConf(task) case appProtectUserSig: lbc.syncAppProtectUserSig(task) + case appProtectDosPolicy: + lbc.syncAppProtectDosPolicy(task) + case appProtectDosLogConf: + lbc.syncAppProtectDosLogConf(task) + case appProtectDosProtectedResource: + lbc.syncDosProtectedResource(task) case ingressLink: lbc.syncIngressLink(task) } @@ -1167,7 +1222,7 @@ func (lbc *LoadBalancerController) processAppProtectChanges(changes []appprotect resourceExes := lbc.createExtendedResources(resources) - warnings, deleteErr := lbc.configurator.DeleteAppProtectPolicy(namespace+"/"+name, resourceExes.IngressExes, resourceExes.MergeableIngresses, resourceExes.VirtualServerExes) + warnings, deleteErr := lbc.configurator.DeleteAppProtectPolicy(impl.Obj, resourceExes.IngressExes, resourceExes.MergeableIngresses, resourceExes.VirtualServerExes) lbc.updateResourcesStatusAndEvents(resources, warnings, deleteErr) @@ -1182,7 +1237,7 @@ func (lbc *LoadBalancerController) processAppProtectChanges(changes []appprotect resourceExes := lbc.createExtendedResources(resources) - warnings, deleteErr := lbc.configurator.DeleteAppProtectLogConf(namespace+"/"+name, resourceExes.IngressExes, resourceExes.MergeableIngresses, resourceExes.VirtualServerExes) + warnings, deleteErr := lbc.configurator.DeleteAppProtectLogConf(impl.Obj, resourceExes.IngressExes, resourceExes.MergeableIngresses, resourceExes.VirtualServerExes) lbc.updateResourcesStatusAndEvents(resources, warnings, deleteErr) } @@ -1200,7 +1255,7 @@ func (lbc *LoadBalancerController) processAppProtectUserSigChange(change appprot for _, poladd := range change.PolicyAddsOrUpdates { resources := lbc.configuration.FindResourcesForAppProtectPolicyAnnotation(poladd.GetNamespace(), poladd.GetName()) - for _, wafPol := range getWAFPoliciesForAppProtectPolicy(lbc.getAllPolicies(), appprotect.GetNsName(poladd)) { + for _, wafPol := range getWAFPoliciesForAppProtectPolicy(lbc.getAllPolicies(), appprotect_common.GetNsName(poladd)) { resources = append(resources, lbc.configuration.FindResourcesForPolicy(wafPol.Namespace, wafPol.Name)...) } @@ -1213,7 +1268,7 @@ func (lbc *LoadBalancerController) processAppProtectUserSigChange(change appprot for _, poldel := range change.PolicyDeletions { resources := lbc.configuration.FindResourcesForAppProtectPolicyAnnotation(poldel.GetNamespace(), poldel.GetName()) - polNsName := appprotect.GetNsName(poldel) + polNsName := appprotect_common.GetNsName(poldel) for _, wafPol := range getWAFPoliciesForAppProtectPolicy(lbc.getAllPolicies(), polNsName) { resources = append(resources, lbc.configuration.FindResourcesForPolicy(wafPol.Namespace, wafPol.Name)...) } @@ -1244,6 +1299,50 @@ func (lbc *LoadBalancerController) processAppProtectProblems(problems []appprote } } +func (lbc *LoadBalancerController) processAppProtectDosChanges(changes []appprotectdos.Change) { + glog.V(3).Infof("Processing %v App Protect Dos changes", len(changes)) + + for _, c := range changes { + if c.Op == appprotectdos.AddOrUpdate { + switch impl := c.Resource.(type) { + case *appprotectdos.DosProtectedResourceEx: + glog.V(3).Infof("handling change UPDATE OR ADD for DOS protected %s/%s", impl.Obj.Namespace, impl.Obj.Name) + resources := lbc.configuration.FindResourcesForAppProtectDosProtected(impl.Obj.Namespace, impl.Obj.Name) + resourceExes := lbc.createExtendedResources(resources) + warnings, err := lbc.configurator.AddOrUpdateResourcesThatUseDosProtected(resourceExes.IngressExes, resourceExes.MergeableIngresses, resourceExes.VirtualServerExes) + lbc.updateResourcesStatusAndEvents(resources, warnings, err) + msg := fmt.Sprintf("Configuration for %s/%s was added or updated", impl.Obj.Namespace, impl.Obj.Name) + lbc.recorder.Event(impl.Obj, api_v1.EventTypeNormal, "AddedOrUpdated", msg) + } + } else if c.Op == appprotectdos.Delete { + switch impl := c.Resource.(type) { + case *appprotectdos.DosPolicyEx: + lbc.configurator.DeleteAppProtectDosPolicy(impl.Obj) + + case *appprotectdos.DosLogConfEx: + lbc.configurator.DeleteAppProtectDosLogConf(impl.Obj) + + case *appprotectdos.DosProtectedResourceEx: + glog.V(3).Infof("handling change DELETE for DOS protected %s/%s", impl.Obj.Namespace, impl.Obj.Name) + resources := lbc.configuration.FindResourcesForAppProtectDosProtected(impl.Obj.Namespace, impl.Obj.Name) + resourceExes := lbc.createExtendedResources(resources) + warnings, err := lbc.configurator.AddOrUpdateResourcesThatUseDosProtected(resourceExes.IngressExes, resourceExes.MergeableIngresses, resourceExes.VirtualServerExes) + lbc.updateResourcesStatusAndEvents(resources, warnings, err) + } + + } + } +} + +func (lbc *LoadBalancerController) processAppProtectDosProblems(problems []appprotectdos.Problem) { + glog.V(3).Infof("Processing %v App Protect Dos problems", len(problems)) + + for _, p := range problems { + eventType := api_v1.EventTypeWarning + lbc.recorder.Event(p.Object, eventType, p.Reason, p.Message) + } +} + func (lbc *LoadBalancerController) updateTransportServerStatusAndEventsOnDelete(tsConfig *TransportServerConfiguration, changeError string, deleteErr error) { eventType := api_v1.EventTypeWarning eventTitle := "Rejected" @@ -2113,6 +2212,18 @@ func (lbc *LoadBalancerController) createIngressEx(ing *networking.Ingress, vali } } } + + if lbc.appProtectDosEnabled { + if dosProtectedAnnotationValue, exists := ingEx.Ingress.Annotations[configs.AppProtectDosProtectedAnnotation]; exists { + dosResEx, err := lbc.dosConfiguration.GetValidDosEx(ing.Namespace, dosProtectedAnnotationValue) + if err != nil { + glog.Warningf("Error Getting Dos Protected Resource %v for Ingress %v/%v: %v", dosProtectedAnnotationValue, ing.Namespace, ing.Name, err) + } + if dosResEx != nil { + ingEx.DosEx = dosResEx + } + } + } } ingEx.Endpoints = make(map[string][]string) @@ -2226,13 +2337,13 @@ func (lbc *LoadBalancerController) getAppProtectLogConfAndDst(ing *networking.In } logDsts := strings.Split(ing.Annotations[configs.AppProtectLogConfDstAnnotation], ",") - logConfNsNs := appprotect.ParseResourceReferenceAnnotationList(ing.Namespace, ing.Annotations[configs.AppProtectLogConfAnnotation]) + logConfNsNs := appprotect_common.ParseResourceReferenceAnnotationList(ing.Namespace, ing.Annotations[configs.AppProtectLogConfAnnotation]) if len(logDsts) != len(logConfNsNs) { return apLogs, fmt.Errorf("Error Validating App Protect Destination and Config for Ingress %v: LogConf and LogDestination must have equal number of items", ing.Name) } for _, logDst := range logDsts { - err := appprotect.ValidateAppProtectLogDestination(logDst) + err := validation.ValidateAppProtectLogDestination(logDst) if err != nil { return apLogs, fmt.Errorf("Error Validating App Protect Destination Config for Ingress %v: %w", ing.Name, err) } @@ -2253,7 +2364,7 @@ func (lbc *LoadBalancerController) getAppProtectLogConfAndDst(ing *networking.In } func (lbc *LoadBalancerController) getAppProtectPolicy(ing *networking.Ingress) (apPolicy *unstructured.Unstructured, err error) { - polNsN := appprotect.ParseResourceReferenceAnnotation(ing.Namespace, ing.Annotations[configs.AppProtectPolicyAnnotation]) + polNsN := appprotect_common.ParseResourceReferenceAnnotation(ing.Namespace, ing.Annotations[configs.AppProtectPolicyAnnotation]) apPolicy, err = lbc.appProtectConfiguration.GetAppResource(appprotect.PolicyGVK.Kind, polNsN) if err != nil { @@ -2265,10 +2376,11 @@ func (lbc *LoadBalancerController) getAppProtectPolicy(ing *networking.Ingress) func (lbc *LoadBalancerController) createVirtualServerEx(virtualServer *conf_v1.VirtualServer, virtualServerRoutes []*conf_v1.VirtualServerRoute) *configs.VirtualServerEx { virtualServerEx := configs.VirtualServerEx{ - VirtualServer: virtualServer, - SecretRefs: make(map[string]*secrets.SecretReference), - ApPolRefs: make(map[string]*unstructured.Unstructured), - LogConfRefs: make(map[string]*unstructured.Unstructured), + VirtualServer: virtualServer, + SecretRefs: make(map[string]*secrets.SecretReference), + ApPolRefs: make(map[string]*unstructured.Unstructured), + LogConfRefs: make(map[string]*unstructured.Unstructured), + DosProtectedEx: make(map[string]*configs.DosEx), } if virtualServer.Spec.TLS != nil && virtualServer.Spec.TLS.Secret != "" { @@ -2309,6 +2421,16 @@ func (lbc *LoadBalancerController) createVirtualServerEx(virtualServer *conf_v1. glog.Warningf("Error getting App Protect resource for VirtualServer %v/%v: %v", virtualServer.Namespace, virtualServer.Name, err) } + if virtualServer.Spec.Dos != "" { + dosEx, err := lbc.dosConfiguration.GetValidDosEx(virtualServer.Namespace, virtualServer.Spec.Dos) + if err != nil { + glog.Warningf("Error getting App Protect Dos resource for VirtualServer %v/%v: %v", virtualServer.Namespace, virtualServer.Name, err) + } + if dosEx != nil { + virtualServerEx.DosProtectedEx[""] = dosEx + } + } + endpoints := make(map[string][]string) externalNameSvcs := make(map[string]bool) podsByIP := make(map[string]configs.PodInfo) @@ -2380,6 +2502,15 @@ func (lbc *LoadBalancerController) createVirtualServerEx(virtualServer *conf_v1. if err != nil { glog.Warningf("Error getting WAF policies for VirtualServer %v/%v: %v", virtualServer.Namespace, virtualServer.Name, err) } + + if r.Dos != "" { + routeDosEx, err := lbc.dosConfiguration.GetValidDosEx(virtualServer.Namespace, r.Dos) + if err != nil { + glog.Warningf("Error getting App Protect Dos resource for VirtualServer %v/%v: %v", virtualServer.Namespace, virtualServer.Name, err) + } + virtualServerEx.DosProtectedEx[r.Path] = routeDosEx + } + err = lbc.addOIDCSecretRefs(virtualServerEx.SecretRefs, vsRoutePolicies) if err != nil { glog.Warningf("Error getting OIDC secrets for VirtualServer %v/%v: %v", virtualServer.Namespace, virtualServer.Name, err) @@ -3207,6 +3338,78 @@ func (lbc *LoadBalancerController) syncAppProtectUserSig(task task) { lbc.processAppProtectProblems(problems) } +func (lbc *LoadBalancerController) syncAppProtectDosPolicy(task task) { + key := task.Key + glog.V(3).Infof("Syncing AppProtectDosPolicy %v", key) + obj, polExists, err := lbc.appProtectDosPolicyLister.GetByKey(key) + if err != nil { + lbc.syncQueue.Requeue(task, err) + return + } + + var changes []appprotectdos.Change + var problems []appprotectdos.Problem + + if !polExists { + glog.V(2).Infof("Deleting APDosPolicy: %v\n", key) + changes, problems = lbc.dosConfiguration.DeletePolicy(key) + } else { + glog.V(2).Infof("Adding or Updating APDosPolicy: %v\n", key) + changes, problems = lbc.dosConfiguration.AddOrUpdatePolicy(obj.(*unstructured.Unstructured)) + } + + lbc.processAppProtectDosChanges(changes) + lbc.processAppProtectDosProblems(problems) +} + +func (lbc *LoadBalancerController) syncAppProtectDosLogConf(task task) { + key := task.Key + glog.V(3).Infof("Syncing APDosLogConf %v", key) + obj, confExists, err := lbc.appProtectDosLogConfLister.GetByKey(key) + if err != nil { + lbc.syncQueue.Requeue(task, err) + return + } + + var changes []appprotectdos.Change + var problems []appprotectdos.Problem + + if !confExists { + glog.V(2).Infof("Deleting APDosLogConf: %v\n", key) + changes, problems = lbc.dosConfiguration.DeleteLogConf(key) + } else { + glog.V(2).Infof("Adding or Updating APDosLogConf: %v\n", key) + changes, problems = lbc.dosConfiguration.AddOrUpdateLogConf(obj.(*unstructured.Unstructured)) + } + + lbc.processAppProtectDosChanges(changes) + lbc.processAppProtectDosProblems(problems) +} + +func (lbc *LoadBalancerController) syncDosProtectedResource(task task) { + key := task.Key + glog.V(3).Infof("Syncing DosProtectedResource %v", key) + obj, confExists, err := lbc.appProtectDosProtectedLister.GetByKey(key) + if err != nil { + lbc.syncQueue.Requeue(task, err) + return + } + + var changes []appprotectdos.Change + var problems []appprotectdos.Problem + + if confExists { + glog.V(2).Infof("Adding or Updating DosProtectedResource: %v\n", key) + changes, problems = lbc.dosConfiguration.AddOrUpdateDosProtectedResource(obj.(*v1beta1.DosProtectedResource)) + } else { + glog.V(2).Infof("Deleting DosProtectedResource: %v\n", key) + changes, problems = lbc.dosConfiguration.DeleteProtectedResource(key) + } + + lbc.processAppProtectDosChanges(changes) + lbc.processAppProtectDosProblems(problems) +} + // IsNginxReady returns ready status of NGINX func (lbc *LoadBalancerController) IsNginxReady() bool { return lbc.isNginxReady diff --git a/internal/k8s/controller_test.go b/internal/k8s/controller_test.go index d64686d209..bb137b19cb 100644 --- a/internal/k8s/controller_test.go +++ b/internal/k8s/controller_test.go @@ -682,7 +682,7 @@ func TestGetPolicies(t *testing.T) { expectedPolicies := []*conf_v1.Policy{validPolicy} expectedErrors := []error{ - errors.New("Policy default/invalid-policy is invalid: spec: Invalid value: \"\": must specify exactly one of: `accessControl`, `rateLimit`, `ingressMTLS`, `egressMTLS`, `jwt`, `oidc`, `waf`"), + errors.New("Policy default/invalid-policy is invalid: spec: Invalid value: \"\": must specify exactly one of: `accessControl`, `rateLimit`, `ingressMTLS`, `egressMTLS`, `jwt`, `oidc`, `waf`, `dos`"), errors.New("Policy nginx-ingress/valid-policy doesn't exist"), errors.New("Failed to get policy nginx-ingress/some-policy: GetByKey error"), errors.New("referenced policy default/valid-policy-ingress-class has incorrect ingress class: test-class (controller ingress class: )"), diff --git a/internal/k8s/handlers.go b/internal/k8s/handlers.go index 378bc48af7..92a41df388 100644 --- a/internal/k8s/handlers.go +++ b/internal/k8s/handlers.go @@ -5,6 +5,8 @@ import ( "reflect" "sort" + "github.com/nginxinc/kubernetes-ingress/pkg/apis/dos/v1beta1" + "github.com/golang/glog" "github.com/nginxinc/kubernetes-ingress/internal/k8s/secrets" v1 "k8s.io/api/core/v1" @@ -599,3 +601,80 @@ func createAppProtectUserSigHandlers(lbc *LoadBalancerController) cache.Resource } return handlers } + +func createAppProtectDosPolicyHandlers(lbc *LoadBalancerController) cache.ResourceEventHandlerFuncs { + handlers := cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + pol := obj.(*unstructured.Unstructured) + glog.V(3).Infof("Adding AppProtectDosPolicy: %v", pol.GetName()) + lbc.AddSyncQueue(pol) + }, + UpdateFunc: func(oldObj, obj interface{}) { + oldPol := oldObj.(*unstructured.Unstructured) + newPol := obj.(*unstructured.Unstructured) + different, err := areResourcesDifferent(oldPol, newPol) + if err != nil { + glog.V(3).Infof("Error when comparing policy %v", err) + lbc.AddSyncQueue(newPol) + } + if different { + glog.V(3).Infof("ApDosPolicy %v changed, syncing", oldPol.GetName()) + lbc.AddSyncQueue(newPol) + } + }, + DeleteFunc: func(obj interface{}) { + lbc.AddSyncQueue(obj) + }, + } + return handlers +} + +func createAppProtectDosLogConfHandlers(lbc *LoadBalancerController) cache.ResourceEventHandlerFuncs { + handlers := cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + conf := obj.(*unstructured.Unstructured) + glog.V(3).Infof("Adding AppProtectDosLogConf: %v", conf.GetName()) + lbc.AddSyncQueue(conf) + }, + UpdateFunc: func(oldObj, obj interface{}) { + oldConf := oldObj.(*unstructured.Unstructured) + newConf := obj.(*unstructured.Unstructured) + different, err := areResourcesDifferent(oldConf, newConf) + if err != nil { + glog.V(3).Infof("Error when comparing DosLogConfs %v", err) + lbc.AddSyncQueue(newConf) + } + if different { + glog.V(3).Infof("ApDosLogConf %v changed, syncing", oldConf.GetName()) + lbc.AddSyncQueue(newConf) + } + }, + DeleteFunc: func(obj interface{}) { + lbc.AddSyncQueue(obj) + }, + } + return handlers +} + +func createAppProtectDosProtectedResourceHandlers(lbc *LoadBalancerController) cache.ResourceEventHandlerFuncs { + handlers := cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + conf := obj.(*v1beta1.DosProtectedResource) + glog.V(3).Infof("Adding DosProtectedResource: %v", conf.GetName()) + lbc.AddSyncQueue(conf) + }, + UpdateFunc: func(oldObj, obj interface{}) { + oldConf := oldObj.(*v1beta1.DosProtectedResource) + newConf := obj.(*v1beta1.DosProtectedResource) + + if !reflect.DeepEqual(oldConf.Spec, newConf.Spec) { + glog.V(3).Infof("DosProtectedResource %v changed, syncing", oldConf.GetName()) + lbc.AddSyncQueue(newConf) + } + }, + DeleteFunc: func(obj interface{}) { + lbc.AddSyncQueue(obj) + }, + } + return handlers +} diff --git a/internal/k8s/reference_checkers.go b/internal/k8s/reference_checkers.go index 69d2d02b61..331262c4fd 100644 --- a/internal/k8s/reference_checkers.go +++ b/internal/k8s/reference_checkers.go @@ -219,7 +219,6 @@ func newAppProtectResourceReferenceChecker(annotation string) *appProtectResourc return &appProtectResourceReferenceChecker{annotation} } -// In App Protect logConfs can be a coma separated list. func (rc *appProtectResourceReferenceChecker) IsReferencedByIngress(namespace string, name string, ing *networking.Ingress) bool { if resName, exists := ing.Annotations[rc.annotation]; exists { resNames := strings.Split(resName, ",") @@ -262,3 +261,43 @@ func isPolicyReferenced(policies []v1.PolicyReference, resourceNamespace string, return false } + +type dosResourceReferenceChecker struct { + annotation string +} + +func newDosResourceReferenceChecker(annotation string) *dosResourceReferenceChecker { + return &dosResourceReferenceChecker{annotation} +} + +func (rc *dosResourceReferenceChecker) IsReferencedByIngress(namespace string, name string, ing *networking.Ingress) bool { + res, exists := ing.Annotations[rc.annotation] + if !exists { + return false + } + return res == namespace+"/"+name || (namespace == ing.Namespace && res == name) +} + +func (rc *dosResourceReferenceChecker) IsReferencedByMinion(namespace string, name string, ing *networking.Ingress) bool { + return false +} + +func (rc *dosResourceReferenceChecker) IsReferencedByVirtualServer(namespace string, name string, vs *v1.VirtualServer) bool { + if vs.Spec.Dos == namespace+"/"+name || (namespace == vs.Namespace && vs.Spec.Dos == name) { + return true + } + for _, route := range vs.Spec.Routes { + if route.Dos == namespace+"/"+name || (namespace == vs.Namespace && route.Dos == name) { + return true + } + } + return false +} + +func (rc *dosResourceReferenceChecker) IsReferencedByVirtualServerRoute(namespace string, name string, vsr *v1.VirtualServerRoute) bool { + return false +} + +func (rc *dosResourceReferenceChecker) IsReferencedByTransportServer(namespace string, name string, ts *conf_v1alpha1.TransportServer) bool { + return false +} diff --git a/internal/k8s/reference_checkers_test.go b/internal/k8s/reference_checkers_test.go index ffe2ac4a4d..4548a54dc1 100644 --- a/internal/k8s/reference_checkers_test.go +++ b/internal/k8s/reference_checkers_test.go @@ -996,7 +996,7 @@ func TestAppProtectResourceIsReferencedByIngresses(t *testing.T) { result := rc.IsReferencedByIngress(test.resourceNamespace, test.resourceName, test.ing) if result != test.expected { - t.Errorf("IsReferencedByIngress() returned %v but expected %v for the case of %s", result, test.expected, test.msg) + t.Errorf("IsReferencedByIngress() returned '%v' but expected '%v' for the case of '%s'", result, test.expected, test.msg) } result = rc.IsReferencedByMinion(test.resourceNamespace, test.resourceName, test.ing) @@ -1220,3 +1220,202 @@ func TestEndpointIsReferencedByVirtualServerAndVirtualServerRoutes(t *testing.T) } } } + +func TestDosProtectedIsReferencedByIngresses(t *testing.T) { + tests := []struct { + ing *networking.Ingress + resourceNamespace string + resourceName string + expected bool + msg string + }{ + { + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{ + configs.AppProtectDosProtectedAnnotation: "default/test-resource", + }, + }, + }, + resourceNamespace: "default", + resourceName: "test-resource", + expected: true, + msg: "resource is referenced", + }, + { + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{ + configs.AppProtectDosProtectedAnnotation: "test-resource", + }, + }, + }, + resourceNamespace: "default", + resourceName: "test-resource", + expected: true, + msg: "resource is referenced with implicit namespace", + }, + { + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{ + configs.AppProtectDosProtectedAnnotation: "default/test-resource", + }, + }, + }, + resourceNamespace: "default", + resourceName: "some-resource", + expected: false, + msg: "wrong name", + }, + { + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{ + configs.AppProtectDosProtectedAnnotation: "default/test-resource", + }, + }, + }, + resourceNamespace: "some-namespace", + resourceName: "test-resource", + expected: false, + msg: "wrong namespace", + }, + { + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{ + configs.AppProtectDosProtectedAnnotation: "test-resource", + }, + }, + }, + resourceNamespace: "some-namespace", + resourceName: "test-resource", + expected: false, + msg: "wrong namespace with implicit namespace", + }, + { + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{ + configs.AppProtectDosProtectedAnnotation: "some-namespace/test-resource,some-namespace/different-resource", + }, + }, + }, + resourceNamespace: "some-namespace", + resourceName: "test-resource", + expected: false, + msg: "resource is referenced with namespace within multiple resources", + }, + { + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{ + configs.AppProtectDosProtectedAnnotation: "test-resource,some-namespace/different-resource", + }, + }, + }, + resourceNamespace: "default", + resourceName: "test-resource", + expected: false, + msg: "resource is referenced with implicit namespace within multiple resources", + }, + { + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{ + configs.AppProtectDosProtectedAnnotation: "test-resource,some-namespace/different-resource", + }, + }, + }, + resourceNamespace: "some-namespace", + resourceName: "test-resource", + expected: false, + msg: "wrong namespace within multiple resources", + }, + } + + for _, test := range tests { + rc := newDosResourceReferenceChecker(configs.AppProtectDosProtectedAnnotation) + + result := rc.IsReferencedByIngress(test.resourceNamespace, test.resourceName, test.ing) + if result != test.expected { + t.Errorf("IsReferencedByIngress() returned %v but expected %v for the case of %s", result, test.expected, test.msg) + } + + result = rc.IsReferencedByMinion(test.resourceNamespace, test.resourceName, test.ing) + if result != false { + t.Errorf("IsReferencedByMinion() returned true but expected false for the case of %s", test.msg) + } + } +} + +func TestDosProtectedIsReferencedByVirtualServer(t *testing.T) { + tests := []struct { + vs *conf_v1.VirtualServer + protectedNamespace string + protectedName string + expected bool + msg string + }{ + { + vs: &conf_v1.VirtualServer{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: conf_v1.VirtualServerSpec{ + Dos: "test-dos", + }, + }, + protectedNamespace: "default", + protectedName: "test-dos", + expected: true, + msg: "dos protected is referenced", + }, + { + vs: &conf_v1.VirtualServer{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: conf_v1.VirtualServerSpec{ + Dos: "test-dos", + }, + }, + protectedNamespace: "default", + protectedName: "some-dos", + expected: false, + msg: "wrong name for dos protected", + }, + { + vs: &conf_v1.VirtualServer{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: conf_v1.VirtualServerSpec{ + Dos: "test-dos", + }, + }, + protectedNamespace: "some-namespace", + protectedName: "test-dos", + expected: false, + msg: "wrong namespace for dos protected", + }, + } + + for _, test := range tests { + rc := newDosResourceReferenceChecker(configs.AppProtectDosProtectedAnnotation) + + result := rc.IsReferencedByVirtualServer(test.protectedNamespace, test.protectedName, test.vs) + if result != test.expected { + t.Errorf("IsReferencedByVirtualServer() returned %v but expected %v for the case of %s", result, test.expected, test.msg) + } + } +} diff --git a/internal/k8s/task_queue.go b/internal/k8s/task_queue.go index 95c07172f4..5a77f4e718 100644 --- a/internal/k8s/task_queue.go +++ b/internal/k8s/task_queue.go @@ -4,8 +4,11 @@ import ( "fmt" "time" + "github.com/nginxinc/kubernetes-ingress/pkg/apis/dos/v1beta1" + "github.com/golang/glog" "github.com/nginxinc/kubernetes-ingress/internal/k8s/appprotect" + "github.com/nginxinc/kubernetes-ingress/internal/k8s/appprotectdos" conf_v1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/v1" conf_v1alpha1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/v1alpha1" v1 "k8s.io/api/core/v1" @@ -118,6 +121,9 @@ const ( appProtectPolicy appProtectLogConf appProtectUserSig + appProtectDosPolicy + appProtectDosLogConf + appProtectDosProtectedResource ingressLink ) @@ -151,6 +157,8 @@ func newTask(key string, obj interface{}) (task, error) { k = globalConfiguration case *conf_v1alpha1.TransportServer: k = transportserver + case *v1beta1.DosProtectedResource: + k = appProtectDosProtectedResource case *unstructured.Unstructured: if objectKind := obj.(*unstructured.Unstructured).GetKind(); objectKind == appprotect.PolicyGVK.Kind { k = appProtectPolicy @@ -160,6 +168,10 @@ func newTask(key string, obj interface{}) (task, error) { k = ingressLink } else if objectKind == appprotect.UserSigGVK.Kind { k = appProtectUserSig + } else if objectKind == appprotectdos.DosPolicyGVK.Kind { + k = appProtectDosPolicy + } else if objectKind == appprotectdos.DosLogConfGVK.Kind { + k = appProtectDosLogConf } else { return task{}, fmt.Errorf("Unknown unstructured kind: %v", objectKind) } diff --git a/internal/k8s/validation.go b/internal/k8s/validation.go index 90c4bf9e97..b1b85d53f6 100644 --- a/internal/k8s/validation.go +++ b/internal/k8s/validation.go @@ -9,6 +9,7 @@ import ( "github.com/nginxinc/kubernetes-ingress/internal/configs" networking "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" ) @@ -51,6 +52,7 @@ const ( failTimeoutAnnotation = "nginx.org/fail-timeout" appProtectEnableAnnotation = "appprotect.f5.com/app-protect-enable" appProtectSecurityLogEnableAnnotation = "appprotect.f5.com/app-protect-security-log-enable" + appProtectDosProtectedAnnotation = "appprotectdos.f5.com/app-protect-dos-resource" internalRouteAnnotation = "nsm.nginx.com/internal-route" websocketServicesAnnotation = "nginx.org/websocket-services" sslServicesAnnotation = "nginx.org/ssl-services" @@ -66,6 +68,7 @@ type annotationValidationContext struct { value string isPlus bool appProtectEnabled bool + appProtectDosEnabled bool internalRoutesEnabled bool fieldPath *field.Path snippetsEnabled bool @@ -231,6 +234,11 @@ var ( validateRequiredAnnotation, validateBoolAnnotation, }, + appProtectDosProtectedAnnotation: { + validateAppProtectDosOnlyAnnotation, + validatePlusOnlyAnnotation, + validateQualifiedName, + }, internalRouteAnnotation: { validateInternalRoutesOnlyAnnotation, validateRequiredAnnotation, @@ -276,6 +284,7 @@ func validateIngress( ing *networking.Ingress, isPlus bool, appProtectEnabled bool, + appProtectDosEnabled bool, internalRoutesEnabled bool, snippetsEnabled bool, ) field.ErrorList { @@ -285,6 +294,7 @@ func validateIngress( getSpecServices(ing.Spec), isPlus, appProtectEnabled, + appProtectDosEnabled, internalRoutesEnabled, field.NewPath("annotations"), snippetsEnabled, @@ -306,6 +316,7 @@ func validateIngressAnnotations( specServices map[string]bool, isPlus bool, appProtectEnabled bool, + appProtectDosEnabled bool, internalRoutesEnabled bool, fieldPath *field.Path, snippetsEnabled bool, @@ -321,6 +332,7 @@ func validateIngressAnnotations( value: value, isPlus: isPlus, appProtectEnabled: appProtectEnabled, + appProtectDosEnabled: appProtectDosEnabled, internalRoutesEnabled: internalRoutesEnabled, fieldPath: fieldPath.Child(name), snippetsEnabled: snippetsEnabled, @@ -361,6 +373,29 @@ func validateRelatedAnnotation(name string, validator validatorFunc) annotationV } } +func validateExistAnnotation(name string) annotationValidationFunc { + return func(context *annotationValidationContext) field.ErrorList { + allErrs := field.ErrorList{} + _, exists := context.annotations[name] + if !exists { + return append(allErrs, field.Forbidden(context.fieldPath, fmt.Sprintf("related annotation %s must exist", name))) + } + return allErrs + } +} + +func validateQualifiedName(context *annotationValidationContext) field.ErrorList { + allErrs := field.ErrorList{} + + err := validation.IsQualifiedName(context.value) + + if err != nil { + return append(allErrs, field.Invalid(context.fieldPath, context.value, "must be a qualified name")) + } + + return allErrs +} + func validateMergeableIngressTypeAnnotation(context *annotationValidationContext) field.ErrorList { allErrs := field.ErrorList{} if context.value != "master" && context.value != "minion" { @@ -417,6 +452,14 @@ func validateAppProtectOnlyAnnotation(context *annotationValidationContext) fiel return allErrs } +func validateAppProtectDosOnlyAnnotation(context *annotationValidationContext) field.ErrorList { + allErrs := field.ErrorList{} + if !context.appProtectDosEnabled { + return append(allErrs, field.Forbidden(context.fieldPath, "annotation requires AppProtectDos")) + } + return allErrs +} + func validateInternalRoutesOnlyAnnotation(context *annotationValidationContext) field.ErrorList { allErrs := field.ErrorList{} if !context.internalRoutesEnabled { diff --git a/internal/k8s/validation_test.go b/internal/k8s/validation_test.go index 3ac9044337..4697e2bce2 100644 --- a/internal/k8s/validation_test.go +++ b/internal/k8s/validation_test.go @@ -14,9 +14,13 @@ import ( func TestValidateIngress(t *testing.T) { tests := []struct { - ing *networking.Ingress - expectedErrors []string - msg string + ing *networking.Ingress + isPlus bool + appProtectEnabled bool + appProtectDosEnabled bool + internalRoutesEnabled bool + expectedErrors []string + msg string }{ { ing: &networking.Ingress{ @@ -28,8 +32,12 @@ func TestValidateIngress(t *testing.T) { }, }, }, - expectedErrors: nil, - msg: "valid input", + isPlus: false, + appProtectEnabled: false, + appProtectDosEnabled: false, + internalRoutesEnabled: false, + expectedErrors: nil, + msg: "valid input", }, { ing: &networking.Ingress{ @@ -46,6 +54,10 @@ func TestValidateIngress(t *testing.T) { }, }, }, + isPlus: false, + appProtectEnabled: false, + appProtectDosEnabled: false, + internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/mergeable-ingress-type: Invalid value: "invalid": must be one of: 'master' or 'minion'`, "spec.rules[0].host: Required value", @@ -76,6 +88,10 @@ func TestValidateIngress(t *testing.T) { }, }, }, + isPlus: false, + appProtectEnabled: false, + appProtectDosEnabled: false, + internalRoutesEnabled: false, expectedErrors: []string{ "spec.rules[0].http.paths: Too many: 1: must have at most 0 items", }, @@ -97,6 +113,10 @@ func TestValidateIngress(t *testing.T) { }, }, }, + isPlus: false, + appProtectEnabled: false, + appProtectDosEnabled: false, + internalRoutesEnabled: false, expectedErrors: []string{ "spec.rules[0].http.paths: Required value: must include at least one path", }, @@ -105,7 +125,7 @@ func TestValidateIngress(t *testing.T) { } for _, test := range tests { - allErrs := validateIngress(test.ing, false, false, false, false) + allErrs := validateIngress(test.ing, test.isPlus, test.appProtectEnabled, test.appProtectDosEnabled, test.internalRoutesEnabled, false) assertion := assertErrors("validateIngress()", test.msg, allErrs, test.expectedErrors) if assertion != "" { t.Error(assertion) @@ -119,6 +139,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices map[string]bool isPlus bool appProtectEnabled bool + appProtectDosEnabled bool internalRoutesEnabled bool snippetsEnabled bool expectedErrors []string @@ -129,6 +150,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid no annotations", @@ -142,6 +164,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/lb-method: Invalid value: "invalid_method": Invalid load balancing method: "invalid_method"`, @@ -157,6 +180,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid input with master annotation", @@ -168,6 +192,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid input with minion annotation", @@ -179,6 +204,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ "annotations.nginx.org/mergeable-ingress-type: Required value", @@ -192,6 +218,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/mergeable-ingress-type: Invalid value: "abc": must be one of: 'master' or 'minion'`, @@ -206,6 +233,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/lb-method annotation, nginx normal", @@ -217,6 +245,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/lb-method: Invalid value: "least_time header": Invalid load balancing method: "least_time header"`, @@ -230,6 +259,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/lb-method: Invalid value: "invalid_method": Invalid load balancing method: "invalid_method"`, @@ -244,6 +274,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ "annotations.nginx.com/health-checks: Forbidden: annotation requires NGINX Plus", @@ -257,6 +288,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: true, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.com/health-checks annotation", @@ -268,6 +300,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: true, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.com/health-checks: Invalid value: "not_a_boolean": must be a boolean`, @@ -282,6 +315,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ "annotations.nginx.com/health-checks-mandatory: Forbidden: annotation requires NGINX Plus", @@ -296,6 +330,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: true, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.com/health-checks-mandatory annotation", @@ -308,6 +343,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: true, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.com/health-checks-mandatory: Invalid value: "not_a_boolean": must be a boolean`, @@ -321,6 +357,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: true, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ "annotations.nginx.com/health-checks-mandatory: Forbidden: related annotation nginx.com/health-checks: must be set", @@ -335,6 +372,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: true, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ "annotations.nginx.com/health-checks-mandatory: Forbidden: related annotation nginx.com/health-checks: must be true", @@ -349,6 +387,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ "annotations.nginx.com/health-checks-mandatory-queue: Forbidden: annotation requires NGINX Plus", @@ -364,6 +403,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: true, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.com/health-checks-mandatory-queue annotation", @@ -377,6 +417,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: true, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.com/health-checks-mandatory-queue: Invalid value: "not_a_number": must be a non-negative integer`, @@ -390,6 +431,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: true, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ "annotations.nginx.com/health-checks-mandatory-queue: Forbidden: related annotation nginx.com/health-checks-mandatory: must be set", @@ -405,6 +447,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: true, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ "annotations.nginx.com/health-checks-mandatory-queue: Forbidden: related annotation nginx.com/health-checks-mandatory: must be true", @@ -419,6 +462,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ "annotations.nginx.com/slow-start: Forbidden: annotation requires NGINX Plus", @@ -432,6 +476,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: true, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.com/slow-start annotation", @@ -443,6 +488,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: true, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.com/slow-start: Invalid value: "not_a_time": must be a time`, @@ -457,6 +503,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/server-tokens annotation, nginx", @@ -468,6 +515,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: true, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/server-tokens annotation, nginx plus", @@ -479,6 +527,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/server-tokens: Invalid value: "custom_setting": must be a boolean`, @@ -493,6 +542,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, snippetsEnabled: true, expectedErrors: nil, @@ -505,6 +555,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, snippetsEnabled: true, expectedErrors: nil, @@ -532,6 +583,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, snippetsEnabled: true, expectedErrors: nil, @@ -544,6 +596,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, snippetsEnabled: true, expectedErrors: nil, @@ -571,6 +624,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/proxy-connect-timeout annotation", @@ -582,6 +636,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/proxy-connect-timeout: Invalid value: "not_a_time": must be a time`, @@ -596,6 +651,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/proxy-read-timeout annotation", @@ -607,6 +663,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/proxy-read-timeout: Invalid value: "not_a_time": must be a time`, @@ -621,6 +678,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/proxy-send-timeout annotation", @@ -632,6 +690,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/proxy-send-timeout: Invalid value: "not_a_time": must be a time`, @@ -646,6 +705,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/proxy-hide-headers annotation, single-value", @@ -657,6 +717,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/proxy-hide-headers annotation, multi-value", @@ -669,6 +730,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/proxy-pass-headers annotation, single-value", @@ -680,6 +742,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/proxy-pass-headers annotation, multi-value", @@ -692,6 +755,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/client-max-body-size annotation", @@ -703,6 +767,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/client-max-body-size: Invalid value: "not_an_offset": must be an offset`, @@ -717,6 +782,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/redirect-to-https annotation", @@ -728,6 +794,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/redirect-to-https: Invalid value: "not_a_boolean": must be a boolean`, @@ -742,6 +809,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid ingress.kubernetes.io/ssl-redirect annotation", @@ -753,6 +821,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.ingress.kubernetes.io/ssl-redirect: Invalid value: "not_a_boolean": must be a boolean`, @@ -767,6 +836,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/proxy-buffering annotation", @@ -778,6 +848,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/proxy-buffering: Invalid value: "not_a_boolean": must be a boolean`, @@ -792,6 +863,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/hsts annotation", @@ -803,6 +875,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/hsts: Invalid value: "not_a_boolean": must be a boolean`, @@ -818,6 +891,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/hsts-max-age annotation", @@ -830,6 +904,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/hsts-max-age nginx.org/hsts can be false", @@ -842,6 +917,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/hsts-max-age: Invalid value: "not_a_number": must be an integer`, @@ -855,6 +931,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ "annotations.nginx.org/hsts-max-age: Forbidden: related annotation nginx.org/hsts: must be set", @@ -870,6 +947,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/hsts-include-subdomains annotation", @@ -882,6 +960,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/hsts-include-subdomains, nginx.org/hsts can be false", @@ -894,6 +973,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/hsts-include-subdomains: Invalid value: "not_a_boolean": must be a boolean`, @@ -907,6 +987,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ "annotations.nginx.org/hsts-include-subdomains: Forbidden: related annotation nginx.org/hsts: must be set", @@ -922,6 +1003,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/hsts-behind-proxy annotation", @@ -934,6 +1016,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/hsts-behind-proxy, nginx.org/hsts can be false", @@ -946,6 +1029,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/hsts-behind-proxy: Invalid value: "not_a_boolean": must be a boolean`, @@ -959,6 +1043,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ "annotations.nginx.org/hsts-behind-proxy: Forbidden: related annotation nginx.org/hsts: must be set", @@ -973,6 +1058,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/proxy-buffers annotation", @@ -984,6 +1070,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/proxy-buffers: Invalid value: "not_a_proxy_buffers_spec": must be a proxy buffer spec`, @@ -998,6 +1085,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/proxy-buffer-size annotation", @@ -1009,6 +1097,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/proxy-buffer-size: Invalid value: "not_a_size": must be a size`, @@ -1023,6 +1112,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/proxy-max-temp-file-size annotation", @@ -1034,6 +1124,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/proxy-max-temp-file-size: Invalid value: "not_a_size": must be a size`, @@ -1048,6 +1139,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/upstream-zone-size annotation", @@ -1059,6 +1151,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/upstream-zone-size: Invalid value: "not a size": must be a size`, @@ -1073,6 +1166,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ "annotations.nginx.com/jwt-realm: Forbidden: annotation requires NGINX Plus", @@ -1086,6 +1180,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: true, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.com/jwt-realm annotation", @@ -1098,6 +1193,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ "annotations.nginx.com/jwt-key: Forbidden: annotation requires NGINX Plus", @@ -1111,6 +1207,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: true, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.com/jwt-key annotation", @@ -1123,6 +1220,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ "annotations.nginx.com/jwt-token: Forbidden: annotation requires NGINX Plus", @@ -1136,6 +1234,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: true, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.com/jwt-token annotation", @@ -1148,6 +1247,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ "annotations.nginx.com/jwt-login-url: Forbidden: annotation requires NGINX Plus", @@ -1161,6 +1261,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: true, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.com/jwt-login-url annotation", @@ -1173,6 +1274,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/listen-ports annotation", @@ -1184,6 +1286,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/listen-ports: Invalid value: "not_a_port_list": must be a comma-separated list of port numbers`, @@ -1198,6 +1301,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/listen-ports-ssl annotation", @@ -1209,6 +1313,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/listen-ports-ssl: Invalid value: "not_a_port_list": must be a comma-separated list of port numbers`, @@ -1223,6 +1328,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/keepalive annotation", @@ -1234,6 +1340,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/keepalive: Invalid value: "not_a_number": must be an integer`, @@ -1248,6 +1355,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/max-fails annotation", @@ -1259,6 +1367,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/max-fails: Invalid value: "-100": must be a non-negative integer`, @@ -1272,6 +1381,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/max-fails: Invalid value: "not_a_number": must be a non-negative integer`, @@ -1286,6 +1396,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/max-conns annotation", @@ -1297,6 +1408,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/max-conns: Invalid value: "-100": must be a non-negative integer`, @@ -1310,6 +1422,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/max-conns: Invalid value: "not_a_number": must be a non-negative integer`, @@ -1324,6 +1437,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/fail-timeout annotation", @@ -1335,6 +1449,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/fail-timeout: Invalid value: "not_a_time": must be a time`, @@ -1349,6 +1464,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: true, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ "annotations.appprotect.f5.com/app-protect-enable: Forbidden: annotation requires AppProtect", @@ -1362,6 +1478,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: true, appProtectEnabled: true, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid appprotect.f5.com/app-protect-enable annotation", @@ -1373,6 +1490,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: true, appProtectEnabled: true, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.appprotect.f5.com/app-protect-enable: Invalid value: "not_a_boolean": must be a boolean`, @@ -1387,6 +1505,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: true, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ "annotations.appprotect.f5.com/app-protect-security-log-enable: Forbidden: annotation requires AppProtect", @@ -1400,6 +1519,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: true, appProtectEnabled: true, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid appprotect.f5.com/app-protect-security-log-enable annotation", @@ -1411,13 +1531,79 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: true, appProtectEnabled: true, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.appprotect.f5.com/app-protect-security-log-enable: Invalid value: "not_a_boolean": must be a boolean`, }, msg: "invalid appprotect.f5.com/app-protect-security-log-enable annotation", }, - + { + annotations: map[string]string{ + "appprotectdos.f5.com/app-protect-dos-resource": "dos-resource-name", + }, + specServices: map[string]bool{}, + isPlus: true, + appProtectEnabled: false, + appProtectDosEnabled: false, + internalRoutesEnabled: false, + expectedErrors: []string{ + "annotations.appprotectdos.f5.com/app-protect-dos-resource: Forbidden: annotation requires AppProtectDos", + }, + msg: "invalid appprotectdos.f5.com/app-protect-dos-resource annotation, requires app protect dos", + }, + { + annotations: map[string]string{ + "appprotectdos.f5.com/app-protect-dos-resource": "dos-resource-name", + }, + specServices: map[string]bool{}, + isPlus: true, + appProtectEnabled: false, + appProtectDosEnabled: true, + internalRoutesEnabled: false, + expectedErrors: nil, + msg: "valid appprotectdos.f5.com/app-protect-dos-enable annotation", + }, + { + annotations: map[string]string{ + "appprotectdos.f5.com/app-protect-dos-resource": "some-namespace/dos-resource-name", + }, + specServices: map[string]bool{}, + isPlus: true, + appProtectEnabled: false, + appProtectDosEnabled: true, + internalRoutesEnabled: false, + expectedErrors: nil, + msg: "valid appprotectdos.f5.com/app-protect-dos-enable annotation", + }, + { + annotations: map[string]string{ + "appprotectdos.f5.com/app-protect-dos-resource": "special-chars-&%^", + }, + specServices: map[string]bool{}, + isPlus: true, + appProtectEnabled: false, + appProtectDosEnabled: true, + internalRoutesEnabled: false, + expectedErrors: []string{ + "annotations.appprotectdos.f5.com/app-protect-dos-resource: Invalid value: \"special-chars-&%^\": must be a qualified name", + }, + msg: "valid appprotectdos.f5.com/app-protect-dos-enable annotation", + }, + { + annotations: map[string]string{ + "appprotectdos.f5.com/app-protect-dos-resource": "too/many/qualifiers", + }, + specServices: map[string]bool{}, + isPlus: true, + appProtectEnabled: false, + appProtectDosEnabled: true, + internalRoutesEnabled: false, + expectedErrors: []string{ + "annotations.appprotectdos.f5.com/app-protect-dos-resource: Invalid value: \"too/many/qualifiers\": must be a qualified name", + }, + msg: "valid appprotectdos.f5.com/app-protect-dos-enable annotation", + }, { annotations: map[string]string{ "nsm.nginx.com/internal-route": "true", @@ -1425,6 +1611,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: true, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ "annotations.nsm.nginx.com/internal-route: Forbidden: annotation requires Internal Routes enabled", @@ -1438,6 +1625,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: true, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: true, expectedErrors: nil, msg: "valid nsm.nginx.com/internal-route annotation", @@ -1449,6 +1637,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: true, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: true, expectedErrors: []string{ `annotations.nsm.nginx.com/internal-route: Invalid value: "not_a_boolean": must be a boolean`, @@ -1465,6 +1654,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { }, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/websocket-services annotation, single-value", @@ -1479,6 +1669,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { }, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/websocket-services annotation, multi-value", @@ -1492,6 +1683,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { }, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/websocket-services: Invalid value: "service-1,service-2": must be a comma-separated list of services. The following services were not found: service-2`, @@ -1508,6 +1700,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { }, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/ssl-services annotation, single-value", @@ -1522,6 +1715,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { }, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/ssl-services annotation, multi-value", @@ -1535,6 +1729,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { }, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/ssl-services: Invalid value: "service-1,service-2": must be a comma-separated list of services. The following services were not found: service-2`, @@ -1551,6 +1746,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { }, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/grpc-services annotation, single-value", @@ -1565,6 +1761,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { }, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/grpc-services annotation, multi-value", @@ -1578,6 +1775,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { }, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.org/grpc-services: Invalid value: "service-1,service-2": must be a comma-separated list of services. The following services were not found: service-2`, @@ -1592,6 +1790,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/rewrites annotation, single-value", @@ -1603,6 +1802,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.org/rewrites annotation, multi-value", @@ -1614,6 +1814,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: true, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: true, expectedErrors: []string{ `annotations.nginx.org/rewrites: Invalid value: "not_a_rewrite": must be a semicolon-separated list of rewrites`, @@ -1628,6 +1829,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: false, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ "annotations.nginx.com/sticky-cookie-services: Forbidden: annotation requires NGINX Plus", @@ -1641,6 +1843,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: true, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.com/sticky-cookie-services annotation, single-value", @@ -1652,6 +1855,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: true, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: nil, msg: "valid nginx.com/sticky-cookie-services annotation, multi-value", @@ -1663,6 +1867,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { specServices: map[string]bool{}, isPlus: true, appProtectEnabled: false, + appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ `annotations.nginx.com/sticky-cookie-services: Invalid value: "not_a_rewrite": must be a semicolon-separated list of sticky services`, @@ -1678,6 +1883,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { test.specServices, test.isPlus, test.appProtectEnabled, + test.appProtectDosEnabled, test.internalRoutesEnabled, field.NewPath("annotations"), test.snippetsEnabled, diff --git a/internal/nginx/fake_manager.go b/internal/nginx/fake_manager.go index 334d6cdfff..29f63493a0 100644 --- a/internal/nginx/fake_manager.go +++ b/internal/nginx/fake_manager.go @@ -169,3 +169,13 @@ func (*FakeManager) AppProtectPluginStart(appDone chan error) { func (*FakeManager) AppProtectPluginQuit() { glog.V(3).Infof("Quitting FakeAppProtectPlugin") } + +// AppProtectDosAgentQuit is a fake implementtion AppProtectAgentQuit +func (*FakeManager) AppProtectDosAgentQuit() { + glog.V(3).Infof("Quitting FakeAppProtectDosAgent") +} + +// AppProtectDosAgentStart is a fake implementation of AppProtectAgentStart +func (*FakeManager) AppProtectDosAgentStart(apdaDone chan error, debug bool, maxDaemon int, maxWorkers int, memory int) { + glog.V(3).Infof("Starting FakeAppProtectDosAgent") +} diff --git a/internal/nginx/manager.go b/internal/nginx/manager.go index 219de351f8..79b695726e 100644 --- a/internal/nginx/manager.go +++ b/internal/nginx/manager.go @@ -7,6 +7,7 @@ import ( "os" "os/exec" "path" + "strconv" "strings" "time" @@ -37,6 +38,10 @@ const ( // appProtectLogConfigFileName is the location of the NGINX App Protect logging configuration file appProtectLogConfigFileName = "/etc/app_protect/bd/logger.cfg" + + appProtectDosAgentInstallCmd = "/usr/bin/adminstall" + appProtectDosAgentStartCmd = "/usr/bin/admd -d --standalone" + appProtectDosAgentStartDebugCmd = "/usr/bin/admd -d --standalone --log debug" ) // ServerConfig holds the config data for an upstream server in NGINX Plus. @@ -77,6 +82,8 @@ type Manager interface { AppProtectAgentQuit() AppProtectPluginStart(appDone chan error) AppProtectPluginQuit() + AppProtectDosAgentStart(apdaDone chan error, debug bool, maxDaemon int, maxWorkers int, memory int) + AppProtectDosAgentQuit() } // LocalManager updates NGINX configuration, starts, reloads and quits NGINX, @@ -99,6 +106,7 @@ type LocalManager struct { OpenTracing bool appProtectPluginPid int appProtectAgentPid int + appProtectDosAgentPid int } // NewLocalManager creates a LocalManager. @@ -512,6 +520,58 @@ func (lm *LocalManager) AppProtectPluginQuit() { } } +// AppProtectDosAgentQuit gracefully ends AppProtect Agent. +func (lm *LocalManager) AppProtectDosAgentQuit() { + glog.V(3).Info("Quitting AppProtectDos Agent") + killcmd := fmt.Sprintf("kill %d", lm.appProtectDosAgentPid) + if err := shellOut(killcmd); err != nil { + glog.Fatalf("Failed to quit AppProtect Agent: %v", err) + } +} + +// AppProtectDosAgentStart starts the AppProtectDos agent +func (lm *LocalManager) AppProtectDosAgentStart(apdaDone chan error, debug bool, maxDaemon int, maxWorkers int, memory int) { + glog.V(3).Info("Starting AppProtectDos Agent") + + // Perform installation by adminstall + appProtectDosAgentInstallCmdFull := appProtectDosAgentInstallCmd + + if maxDaemon != 0 { + appProtectDosAgentInstallCmdFull += " -d " + strconv.Itoa(maxDaemon) + } + + if maxWorkers != 0 { + appProtectDosAgentInstallCmdFull += " -w " + strconv.Itoa(maxWorkers) + } + + if memory != 0 { + appProtectDosAgentInstallCmdFull += " -m " + strconv.Itoa(memory) + } + + cmdInstall := exec.Command("sh", "-c", appProtectDosAgentInstallCmdFull) + + if err := cmdInstall.Run(); err != nil { + glog.Fatalf("Failed to install AppProtectDos: %v", err) + } + + // case debug add debug flag to admd + appProtectDosAgentCmd := appProtectDosAgentStartCmd + if debug { + appProtectDosAgentCmd = appProtectDosAgentStartDebugCmd + } + + cmd := exec.Command("sh", "-c", appProtectDosAgentCmd) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stdout + if err := cmd.Start(); err != nil { + glog.Fatalf("Failed to start AppProtectDos Agent: %v", err) + } + lm.appProtectDosAgentPid = cmd.Process.Pid + go func() { + apdaDone <- cmd.Wait() + }() +} + func getBinaryFileName(debug bool) string { if debug { return nginxBinaryPathDebug diff --git a/pkg/apis/configuration/v1/types.go b/pkg/apis/configuration/v1/types.go index fb40e407a9..a6136ebd9c 100644 --- a/pkg/apis/configuration/v1/types.go +++ b/pkg/apis/configuration/v1/types.go @@ -43,6 +43,7 @@ type VirtualServerSpec struct { Routes []Route `json:"routes"` HTTPSnippets string `json:"http-snippets"` ServerSnippets string `json:"server-snippets"` + Dos string `json:"dos"` } // PolicyReference references a policy by name and an optional namespace. @@ -139,6 +140,7 @@ type Route struct { Matches []Match `json:"matches"` ErrorPages []ErrorPage `json:"errorPages"` LocationSnippets string `json:"location-snippets"` + Dos string `json:"dos"` } // Action defines an action. diff --git a/pkg/apis/configuration/v1/zz_generated.deepcopy.go b/pkg/apis/configuration/v1/zz_generated.deepcopy.go index 09835f3a8d..a84a2ecab2 100644 --- a/pkg/apis/configuration/v1/zz_generated.deepcopy.go +++ b/pkg/apis/configuration/v1/zz_generated.deepcopy.go @@ -1,3 +1,4 @@ +//go:build !ignore_autogenerated // +build !ignore_autogenerated // Code generated by deepcopy-gen. DO NOT EDIT. diff --git a/pkg/apis/configuration/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/configuration/v1alpha1/zz_generated.deepcopy.go index 8f93f27d36..a5792f1703 100644 --- a/pkg/apis/configuration/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/configuration/v1alpha1/zz_generated.deepcopy.go @@ -1,3 +1,4 @@ +//go:build !ignore_autogenerated // +build !ignore_autogenerated // Code generated by deepcopy-gen. DO NOT EDIT. @@ -8,6 +9,32 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AccessControl) DeepCopyInto(out *AccessControl) { + *out = *in + if in.Allow != nil { + in, out := &in.Allow, &out.Allow + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Deny != nil { + in, out := &in.Deny, &out.Deny + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessControl. +func (in *AccessControl) DeepCopy() *AccessControl { + if in == nil { + return nil + } + out := new(AccessControl) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Action) DeepCopyInto(out *Action) { *out = *in @@ -24,6 +51,32 @@ func (in *Action) DeepCopy() *Action { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EgressMTLS) DeepCopyInto(out *EgressMTLS) { + *out = *in + if in.VerifyDepth != nil { + in, out := &in.VerifyDepth, &out.VerifyDepth + *out = new(int) + **out = **in + } + if in.SessionReuse != nil { + in, out := &in.SessionReuse, &out.SessionReuse + *out = new(bool) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EgressMTLS. +func (in *EgressMTLS) DeepCopy() *EgressMTLS { + if in == nil { + return nil + } + out := new(EgressMTLS) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GlobalConfiguration) DeepCopyInto(out *GlobalConfiguration) { *out = *in @@ -126,6 +179,43 @@ func (in *HealthCheck) DeepCopy() *HealthCheck { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IngressMTLS) DeepCopyInto(out *IngressMTLS) { + *out = *in + if in.VerifyDepth != nil { + in, out := &in.VerifyDepth, &out.VerifyDepth + *out = new(int) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressMTLS. +func (in *IngressMTLS) DeepCopy() *IngressMTLS { + if in == nil { + return nil + } + out := new(IngressMTLS) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JWTAuth) DeepCopyInto(out *JWTAuth) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JWTAuth. +func (in *JWTAuth) DeepCopy() *JWTAuth { + if in == nil { + return nil + } + out := new(JWTAuth) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Listener) DeepCopyInto(out *Listener) { *out = *in @@ -158,6 +248,148 @@ func (in *Match) DeepCopy() *Match { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Policy) DeepCopyInto(out *Policy) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Policy. +func (in *Policy) DeepCopy() *Policy { + if in == nil { + return nil + } + out := new(Policy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Policy) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PolicyList) DeepCopyInto(out *PolicyList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Policy, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PolicyList. +func (in *PolicyList) DeepCopy() *PolicyList { + if in == nil { + return nil + } + out := new(PolicyList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PolicyList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PolicySpec) DeepCopyInto(out *PolicySpec) { + *out = *in + if in.AccessControl != nil { + in, out := &in.AccessControl, &out.AccessControl + *out = new(AccessControl) + (*in).DeepCopyInto(*out) + } + if in.RateLimit != nil { + in, out := &in.RateLimit, &out.RateLimit + *out = new(RateLimit) + (*in).DeepCopyInto(*out) + } + if in.JWTAuth != nil { + in, out := &in.JWTAuth, &out.JWTAuth + *out = new(JWTAuth) + **out = **in + } + if in.IngressMTLS != nil { + in, out := &in.IngressMTLS, &out.IngressMTLS + *out = new(IngressMTLS) + (*in).DeepCopyInto(*out) + } + if in.EgressMTLS != nil { + in, out := &in.EgressMTLS, &out.EgressMTLS + *out = new(EgressMTLS) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PolicySpec. +func (in *PolicySpec) DeepCopy() *PolicySpec { + if in == nil { + return nil + } + out := new(PolicySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RateLimit) DeepCopyInto(out *RateLimit) { + *out = *in + if in.Delay != nil { + in, out := &in.Delay, &out.Delay + *out = new(int) + **out = **in + } + if in.NoDelay != nil { + in, out := &in.NoDelay, &out.NoDelay + *out = new(bool) + **out = **in + } + if in.Burst != nil { + in, out := &in.Burst, &out.Burst + *out = new(int) + **out = **in + } + if in.DryRun != nil { + in, out := &in.DryRun, &out.DryRun + *out = new(bool) + **out = **in + } + if in.RejectCode != nil { + in, out := &in.RejectCode, &out.RejectCode + *out = new(int) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RateLimit. +func (in *RateLimit) DeepCopy() *RateLimit { + if in == nil { + return nil + } + out := new(RateLimit) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SessionParameters) DeepCopyInto(out *SessionParameters) { *out = *in diff --git a/pkg/apis/configuration/validation/appprotect.go b/pkg/apis/configuration/validation/appprotect.go new file mode 100644 index 0000000000..6acfe6c04a --- /dev/null +++ b/pkg/apis/configuration/validation/appprotect.go @@ -0,0 +1,53 @@ +package validation + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +var appProtectPolicyRequiredFields = [][]string{ + {"spec", "policy"}, +} + +var appProtectLogConfRequiredFields = [][]string{ + {"spec", "content"}, + {"spec", "filter"}, +} + +var appProtectUserSigRequiredSlices = [][]string{ + {"spec", "signatures"}, +} + +// ValidateAppProtectPolicy validates Policy resource +func ValidateAppProtectPolicy(policy *unstructured.Unstructured) error { + polName := policy.GetName() + + err := ValidateRequiredFields(policy, appProtectPolicyRequiredFields) + if err != nil { + return fmt.Errorf("Error validating App Protect Policy %v: %w", polName, err) + } + + return nil +} + +// ValidateAppProtectLogConf validates LogConfiguration resource +func ValidateAppProtectLogConf(logConf *unstructured.Unstructured) error { + lcName := logConf.GetName() + err := ValidateRequiredFields(logConf, appProtectLogConfRequiredFields) + if err != nil { + return fmt.Errorf("Error validating App Protect Log Configuration %v: %w", lcName, err) + } + + return nil +} + +func ValidateAppProtectUserSig(userSig *unstructured.Unstructured) error { + sigName := userSig.GetName() + err := ValidateRequiredSlices(userSig, appProtectUserSigRequiredSlices) + if err != nil { + return fmt.Errorf("Error validating App Protect User Signature %v: %w", sigName, err) + } + + return nil +} diff --git a/pkg/apis/configuration/validation/appprotect_common.go b/pkg/apis/configuration/validation/appprotect_common.go new file mode 100644 index 0000000000..7b8279a4e9 --- /dev/null +++ b/pkg/apis/configuration/validation/appprotect_common.go @@ -0,0 +1,72 @@ +package validation + +import ( + "fmt" + "net" + "regexp" + "strconv" + "strings" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func ValidateRequiredSlices(obj *unstructured.Unstructured, fieldsList [][]string) error { + for _, fields := range fieldsList { + field, found, err := unstructured.NestedSlice(obj.Object, fields...) + if err != nil { + return fmt.Errorf("Error checking for required field %v: %w", field, err) + } + if !found { + return fmt.Errorf("Required field %v not found", field) + } + } + return nil +} + +func ValidateRequiredFields(obj *unstructured.Unstructured, fieldsList [][]string) error { + for _, fields := range fieldsList { + field, found, err := unstructured.NestedMap(obj.Object, fields...) + if err != nil { + return fmt.Errorf("Error checking for required field %v: %w", field, err) + } + if !found { + return fmt.Errorf("Required field %v not found", field) + } + } + return nil +} + +var ( + logDstEx = regexp.MustCompile(`(?:syslog:server=((?:\d{1,3}\.){3}\d{1,3}|localhost):\d{1,5})|stderr`) +) + +// ValidateAppProtectLogDestination validates destination for log configuration +func ValidateAppProtectLogDestination(dstAntn string) error { + errormsg := "Error parsing App Protect Log config: Destination must follow format: syslog:server=: or stderr" + if !logDstEx.MatchString(dstAntn) { + return fmt.Errorf("%s Log Destination did not follow format", errormsg) + } + if dstAntn == "stderr" { + return nil + } + + dstchunks := strings.Split(dstAntn, ":") + + // // This error can be ignored since the regex check ensures this string will be parsable + port, _ := strconv.Atoi(dstchunks[2]) + + if port > 65535 || port < 1 { + return fmt.Errorf("Error parsing port: %v not a valid port number", port) + } + + ipstr := strings.Split(dstchunks[1], "=")[1] + if ipstr == "localhost" { + return nil + } + + if net.ParseIP(ipstr) == nil { + return fmt.Errorf("Error parsing host: %v is not a valid ip address", ipstr) + } + + return nil +} diff --git a/pkg/apis/configuration/validation/appprotect_common_test.go b/pkg/apis/configuration/validation/appprotect_common_test.go new file mode 100644 index 0000000000..e7662160d2 --- /dev/null +++ b/pkg/apis/configuration/validation/appprotect_common_test.go @@ -0,0 +1,226 @@ +package validation + +import ( + "strings" + "testing" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func TestValidateRequiredFields(t *testing.T) { + tests := []struct { + obj *unstructured.Unstructured + fieldsList [][]string + expectErr bool + msg string + }{ + { + obj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "a": map[string]interface{}{}, + "b": map[string]interface{}{}, + }, + }, + fieldsList: [][]string{{"a"}, {"b"}}, + expectErr: false, + msg: "valid object with 2 fields", + }, + { + obj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "a": map[string]interface{}{}, + }, + }, + fieldsList: [][]string{{"a"}, {"b"}}, + expectErr: true, + msg: "invalid object with a missing field", + }, + { + obj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "a": map[string]interface{}{}, + "x": map[string]interface{}{}, + }, + }, + fieldsList: [][]string{{"a"}, {"b"}}, + expectErr: true, + msg: "invalid object with a wrong field", + }, + { + obj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{}, + }, + }, + }, + fieldsList: [][]string{{"a", "b"}}, + expectErr: false, + msg: "valid object with nested field", + }, + { + obj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "a": map[string]interface{}{ + "x": map[string]interface{}{}, + }, + }, + }, + fieldsList: [][]string{{"a", "b"}}, + expectErr: true, + msg: "invalid object with a wrong nested field", + }, + { + obj: &unstructured.Unstructured{ + Object: map[string]interface{}{}, + }, + fieldsList: nil, + expectErr: false, + msg: "valid object with no validation", + }, + { + obj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "a": "wrong-type", // must be map[string]interface{} + }, + }, + fieldsList: [][]string{{"a"}}, + expectErr: true, + msg: "invalid object with a field of wrong type", + }, + } + + for _, test := range tests { + err := ValidateRequiredFields(test.obj, test.fieldsList) + if test.expectErr && err == nil { + t.Errorf("ValidateRequiredFields() returned no error for the case of %s", test.msg) + } + if !test.expectErr && err != nil { + t.Errorf("ValidateRequiredFields() returned unexpected error %v for the case of %s", err, test.msg) + } + } +} + +func TestValidateRequiredSlices(t *testing.T) { + tests := []struct { + obj *unstructured.Unstructured + fieldsList [][]string + expectErr bool + msg string + }{ + { + obj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "a": []interface{}{}, + "b": []interface{}{}, + }, + }, + fieldsList: [][]string{{"a"}, {"b"}}, + expectErr: false, + msg: "valid object with 2 fields", + }, + { + obj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "a": []interface{}{}, + }, + }, + fieldsList: [][]string{{"a"}, {"b"}}, + expectErr: true, + msg: "invalid object with a field", + }, + { + obj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "a": []interface{}{}, + "x": []interface{}{}, + }, + }, + fieldsList: [][]string{{"a"}, {"b"}}, + expectErr: true, + msg: "invalid object with a wrong field", + }, + { + obj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "a": map[string]interface{}{ + "b": []interface{}{}, + }, + }, + }, + fieldsList: [][]string{{"a", "b"}}, + expectErr: false, + msg: "valid object with nested field", + }, + { + obj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "a": map[string]interface{}{ + "x": []interface{}{}, + }, + }, + }, + fieldsList: [][]string{{"a", "b"}}, + expectErr: true, + msg: "invalid object with a wrong nested field", + }, + { + obj: &unstructured.Unstructured{ + Object: map[string]interface{}{}, + }, + fieldsList: nil, + expectErr: false, + msg: "valid object with no validation", + }, + { + obj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "a": "wrong-type", // must be [string]interface{} + }, + }, + fieldsList: [][]string{{"a"}}, + expectErr: true, + msg: "invalid object with a field of wrong type", + }, + } + + for _, test := range tests { + err := ValidateRequiredSlices(test.obj, test.fieldsList) + if test.expectErr && err == nil { + t.Errorf("ValidateRequiredSlices() returned no error for the case of %s", test.msg) + } + if !test.expectErr && err != nil { + t.Errorf("ValidateRequiredSlices() returned unexpected error %v for the case of %s", err, test.msg) + } + } +} + +func TestValidateAppProtectLogDestinationAnnotation(t *testing.T) { + // Positive test cases + posDstAntns := []string{"stderr", "syslog:server=localhost:9000", "syslog:server=10.1.1.2:9000"} + + // Negative test cases item, expected error message + negDstAntns := [][]string{ + {"stdout", "Log Destination did not follow format"}, + {"syslog:server=localhost:99999", "not a valid port number"}, + {"syslog:server=999.99.99.99:5678", "is not a valid ip address"}, + {"/var/log/ap.log", "Error parsing App Protect Log config: Destination must follow format: syslog:server=: or stderr"}, + } + + for _, tCase := range posDstAntns { + err := ValidateAppProtectLogDestination(tCase) + if err != nil { + t.Errorf("got %v expected nil", err) + } + } + for _, nTCase := range negDstAntns { + err := ValidateAppProtectLogDestination(nTCase[0]) + if err == nil { + t.Errorf("got no error expected error containing %s", nTCase[1]) + } else { + if !strings.Contains(err.Error(), nTCase[1]) { + t.Errorf("got %v expected to contain: %s", err, nTCase[1]) + } + } + } +} diff --git a/pkg/apis/configuration/validation/appprotect_test.go b/pkg/apis/configuration/validation/appprotect_test.go new file mode 100644 index 0000000000..bebd3e8718 --- /dev/null +++ b/pkg/apis/configuration/validation/appprotect_test.go @@ -0,0 +1,176 @@ +package validation + +import ( + "testing" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func TestValidateAppProtectPolicy(t *testing.T) { + tests := []struct { + policy *unstructured.Unstructured + expectErr bool + msg string + }{ + { + policy: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "policy": map[string]interface{}{}, + }, + }, + }, + expectErr: false, + msg: "valid policy", + }, + { + policy: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "something": map[string]interface{}{}, + }, + }, + }, + expectErr: true, + msg: "invalid policy with no policy field", + }, + { + policy: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "something": map[string]interface{}{ + "policy": map[string]interface{}{}, + }, + }, + }, + expectErr: true, + msg: "invalid policy with no spec field", + }, + } + + for _, test := range tests { + err := ValidateAppProtectPolicy(test.policy) + if test.expectErr && err == nil { + t.Errorf("validateAppProtectPolicy() returned no error for the case of %s", test.msg) + } + if !test.expectErr && err != nil { + t.Errorf("validateAppProtectPolicy() returned unexpected error %v for the case of %s", err, test.msg) + } + } +} + +func TestValidateAppProtectLogConf(t *testing.T) { + tests := []struct { + logConf *unstructured.Unstructured + expectErr bool + msg string + }{ + { + logConf: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "content": map[string]interface{}{}, + "filter": map[string]interface{}{}, + }, + }, + }, + expectErr: false, + msg: "valid log conf", + }, + { + logConf: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "filter": map[string]interface{}{}, + }, + }, + }, + expectErr: true, + msg: "invalid log conf with no content field", + }, + { + logConf: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "content": map[string]interface{}{}, + }, + }, + }, + expectErr: true, + msg: "invalid log conf with no filter field", + }, + { + logConf: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "something": map[string]interface{}{ + "content": map[string]interface{}{}, + "filter": map[string]interface{}{}, + }, + }, + }, + expectErr: true, + msg: "invalid log conf with no spec field", + }, + } + + for _, test := range tests { + err := ValidateAppProtectLogConf(test.logConf) + if test.expectErr && err == nil { + t.Errorf("validateAppProtectLogConf() returned no error for the case of %s", test.msg) + } + if !test.expectErr && err != nil { + t.Errorf("validateAppProtectLogConf() returned unexpected error %v for the case of %s", err, test.msg) + } + } +} + +func TestValidateAppProtectUserSig(t *testing.T) { + tests := []struct { + userSig *unstructured.Unstructured + expectErr bool + msg string + }{ + { + userSig: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "signatures": []interface{}{}, + }, + }, + }, + expectErr: false, + msg: "valid user sig", + }, + { + userSig: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "something": []interface{}{}, + }, + }, + }, + expectErr: true, + msg: "invalid user sig with no signatures", + }, + { + userSig: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "something": map[string]interface{}{ + "signatures": []interface{}{}, + }, + }, + }, + expectErr: true, + msg: "invalid user sign with no spec field", + }, + } + + for _, test := range tests { + err := ValidateAppProtectUserSig(test.userSig) + if test.expectErr && err == nil { + t.Errorf("validateAppProtectUserSig() returned no error for the case of %s", test.msg) + } + if !test.expectErr && err != nil { + t.Errorf("validateAppProtectUserSig() returned unexpected error %v for the case of %s", err, test.msg) + } + } +} diff --git a/pkg/apis/configuration/validation/common.go b/pkg/apis/configuration/validation/common.go index ee7a097fd1..c1b8d88df9 100644 --- a/pkg/apis/configuration/validation/common.go +++ b/pkg/apis/configuration/validation/common.go @@ -17,6 +17,14 @@ const ( var escapedStringsFmtRegexp = regexp.MustCompile("^" + escapedStringsFmt + "$") +func validateEscapedString(body string, examples ...string) error { + if !escapedStringsFmtRegexp.MatchString(body) { + msg := validation.RegexError(escapedStringsErrMsg, escapedStringsFmt, examples...) + return fmt.Errorf(msg) + } + return nil +} + func validateVariable(nVar string, validVars map[string]bool, fieldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} diff --git a/pkg/apis/configuration/validation/dos.go b/pkg/apis/configuration/validation/dos.go new file mode 100644 index 0000000000..17eadf9d09 --- /dev/null +++ b/pkg/apis/configuration/validation/dos.go @@ -0,0 +1,183 @@ +package validation + +import ( + "fmt" + "net/url" + "regexp" + "strconv" + "strings" + + "github.com/nginxinc/kubernetes-ingress/pkg/apis/dos/v1beta1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/util/validation" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +var appProtectDosPolicyRequiredFields = [][]string{ + {"spec"}, +} + +var appProtectDosLogConfRequiredFields = [][]string{ + {"spec", "content"}, + {"spec", "filter"}, +} + +const MaxNameLength = 63 + +func ValidateDosProtectedResource(protected *v1beta1.DosProtectedResource) error { + var err error + + // name + if protected.Spec.Name == "" { + return fmt.Errorf("error validating DosProtectedResource: %v missing value for field: %v", protected.Name, "name") + } + err = validateAppProtectDosName(protected.Spec.Name) + if err != nil { + return fmt.Errorf("error validating DosProtectedResource: %v invalid field: %v err: %w", protected.Name, "name", err) + } + + // apDosMonitor + if protected.Spec.ApDosMonitor != nil { + err = validateAppProtectDosMonitor(*protected.Spec.ApDosMonitor) + if err != nil { + return fmt.Errorf("error validating DosProtectedResource: %v invalid field: %v err: %w", protected.Name, "apDosMonitor", err) + } + } + + // dosAccessLogDest + if protected.Spec.DosAccessLogDest == "" { + return fmt.Errorf("error validating DosProtectedResource: %v missing value for field: %v", protected.Name, "dosAccessLogDest") + } + err = validateAppProtectDosLogDest(protected.Spec.DosAccessLogDest) + if err != nil { + return fmt.Errorf("error validating DosProtectedResource: %v invalid field: %v err: %w", protected.Name, "dosAccessLogDest", err) + } + + // apDosPolicy + if protected.Spec.ApDosPolicy != "" { + err = validateResourceReference(protected.Spec.ApDosPolicy) + if err != nil { + return fmt.Errorf("error validating DosProtectedResource: %v invalid field: %v err: %w", protected.Name, "apDosPolicy", err) + } + } + + // dosSecurityLog + if protected.Spec.DosSecurityLog != nil { + // dosLogDest + err = validateAppProtectDosLogDest(protected.Spec.DosSecurityLog.DosLogDest) + if err != nil { + return fmt.Errorf("error validating DosProtectedResource: %v invalid field: %v err: %w", protected.Name, "dosSecurityLog/dosLogDest", err) + } + // apDosLogConf + err = validateResourceReference(protected.Spec.DosSecurityLog.ApDosLogConf) + if err != nil { + return fmt.Errorf("error validating DosProtectedResource: %v invalid field: %v err: %w", protected.Name, "dosSecurityLog/apDosLogConf", err) + } + } + + return nil +} + +// validateResourceReference validates a resource reference. A valid resource can be either namespace/name or name. +func validateResourceReference(ref string) error { + errs := validation.IsQualifiedName(ref) + if len(errs) != 0 { + return fmt.Errorf("reference name is invalid: %v", ref) + } + + return nil +} + +// ValidateAppProtectDosLogConf validates LogConfiguration resource +func ValidateAppProtectDosLogConf(logConf *unstructured.Unstructured) error { + lcName := logConf.GetName() + err := ValidateRequiredFields(logConf, appProtectDosLogConfRequiredFields) + if err != nil { + return fmt.Errorf("error validating App Protect Dos Log Configuration %v: %w", lcName, err) + } + + return nil +} + +var ( + validDnsRegex = regexp.MustCompile(`^([A-Za-z0-9][A-Za-z0-9-]{1,62}\.)([A-Za-z0-9-]{1,63}\.)*[A-Za-z]{2,6}:\d{1,5}$`) + validIpRegex = regexp.MustCompile(`^(\d{1,3}\.){3}\d{1,3}:\d{1,5}$`) + validLocalhostRegex = regexp.MustCompile(`^localhost:\d{1,5}$`) +) + +func validateAppProtectDosLogDest(dstAntn string) error { + if validIpRegex.MatchString(dstAntn) || validDnsRegex.MatchString(dstAntn) || validLocalhostRegex.MatchString(dstAntn) { + chunks := strings.Split(dstAntn, ":") + err := validatePort(chunks[1]) + if err != nil { + return fmt.Errorf("invalid log destination: %w", err) + } + return nil + } + if dstAntn == "stderr" { + return nil + } + + return fmt.Errorf("invalid log destination: %s, must follow format: : or stderr", dstAntn) +} + +func validatePort(value string) error { + port, _ := strconv.Atoi(value) + if port > 65535 || port < 1 { + return fmt.Errorf("error parsing port: %v not a valid port number", port) + } + return nil +} + +func validateAppProtectDosName(name string) error { + if len(name) > MaxNameLength { + return fmt.Errorf("app Protect Dos Name max length is %v", MaxNameLength) + } + + if err := validateEscapedString(name, "protected-object-one"); err != nil { + return err + } + + return nil +} + +var validMonitorProtocol = map[string]bool{ + "http1": true, + "http2": true, + "grpc": true, +} + +func validateAppProtectDosMonitor(apDosMonitor v1beta1.ApDosMonitor) error { + _, err := url.Parse(apDosMonitor.Uri) + if err != nil { + return fmt.Errorf("app Protect Dos Monitor must have valid URL") + } + + if err := validateEscapedString(apDosMonitor.Uri, "http://www.example.com"); err != nil { + return err + } + + if apDosMonitor.Protocol != "" { + allErrs := field.ErrorList{} + fieldPath := field.NewPath("dosMonitorProtocol") + allErrs = append(allErrs, validateParameter(apDosMonitor.Protocol, validMonitorProtocol, fieldPath)...) + err := allErrs.ToAggregate() + if err != nil { + return fmt.Errorf("app Protect Dos Monitor Protocol must be: %v", err) + } + } + + return nil +} + +// ValidateAppProtectDosPolicy validates Policy resource +func ValidateAppProtectDosPolicy(policy *unstructured.Unstructured) error { + polName := policy.GetName() + + err := ValidateRequiredFields(policy, appProtectDosPolicyRequiredFields) + if err != nil { + return fmt.Errorf("error validating DosPolicy %v: %w", polName, err) + } + + return nil +} diff --git a/pkg/apis/configuration/validation/dos_test.go b/pkg/apis/configuration/validation/dos_test.go new file mode 100644 index 0000000000..e382961ad9 --- /dev/null +++ b/pkg/apis/configuration/validation/dos_test.go @@ -0,0 +1,436 @@ +package validation + +import ( + "fmt" + "strings" + "testing" + + "github.com/nginxinc/kubernetes-ingress/pkg/apis/dos/v1beta1" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func TestValidateDosProtectedResource(t *testing.T) { + tests := []struct { + protected *v1beta1.DosProtectedResource + expectErr string + msg string + }{ + { + protected: &v1beta1.DosProtectedResource{}, + expectErr: "error validating DosProtectedResource: missing value for field: name", + msg: "empty resource", + }, + { + protected: &v1beta1.DosProtectedResource{ + Spec: v1beta1.DosProtectedResourceSpec{}, + }, + expectErr: "error validating DosProtectedResource: missing value for field: name", + msg: "empty spec", + }, + { + protected: &v1beta1.DosProtectedResource{ + Spec: v1beta1.DosProtectedResourceSpec{ + Name: "name", + }, + }, + expectErr: "error validating DosProtectedResource: missing value for field: dosAccessLogDest", + msg: "only name specified", + }, + { + protected: &v1beta1.DosProtectedResource{ + Spec: v1beta1.DosProtectedResourceSpec{ + Name: "name", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + }, + }, + expectErr: "error validating DosProtectedResource: missing value for field: dosAccessLogDest", + msg: "name and apDosMonitor specified", + }, + { + protected: &v1beta1.DosProtectedResource{ + Spec: v1beta1.DosProtectedResourceSpec{ + Name: "name", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "exabad-$%^$-example.com", + }, + }, + }, + expectErr: "error validating DosProtectedResource: invalid field: apDosMonitor err: app Protect Dos Monitor must have valid URL", + msg: "invalid apDosMonitor specified", + }, + { + protected: &v1beta1.DosProtectedResource{ + Spec: v1beta1.DosProtectedResourceSpec{ + Name: "name", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "example.service.com:123", + }, + }, + msg: "name, dosAccessLogDest and apDosMonitor specified", + }, + { + protected: &v1beta1.DosProtectedResource{ + Spec: v1beta1.DosProtectedResourceSpec{ + Name: "name", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "bad&$%^logdest", + }, + }, + expectErr: "error validating DosProtectedResource: invalid field: dosAccessLogDest err: invalid log destination: bad&$%^logdest, must follow format: : or stderr", + msg: "invalid DosAccessLogDest specified", + }, + { + protected: &v1beta1.DosProtectedResource{ + Spec: v1beta1.DosProtectedResourceSpec{ + Name: "name", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "example.service.com:123", + ApDosPolicy: "ns/name", + }, + }, + expectErr: "", + msg: "required fields and apdospolicy specified", + }, + { + protected: &v1beta1.DosProtectedResource{ + Spec: v1beta1.DosProtectedResourceSpec{ + Name: "name", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "example.service.com:123", + ApDosPolicy: "bad$%^name", + }, + }, + expectErr: "error validating DosProtectedResource: invalid field: apDosPolicy err: reference name is invalid: bad$%^name", + msg: "invalid apdospolicy specified", + }, + { + protected: &v1beta1.DosProtectedResource{ + Spec: v1beta1.DosProtectedResourceSpec{ + Name: "name", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "example.service.com:123", + DosSecurityLog: &v1beta1.DosSecurityLog{}, + }, + }, + expectErr: "error validating DosProtectedResource: invalid field: dosSecurityLog/dosLogDest err: invalid log destination: , must follow format: : or stderr", + msg: "empty DosSecurityLog specified", + }, + { + protected: &v1beta1.DosProtectedResource{ + Spec: v1beta1.DosProtectedResourceSpec{ + Name: "name", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "example.service.com:123", + DosSecurityLog: &v1beta1.DosSecurityLog{ + DosLogDest: "service.org:123", + }, + }, + }, + expectErr: "error validating DosProtectedResource: invalid field: dosSecurityLog/apDosLogConf err: reference name is invalid: ", + msg: "DosSecurityLog with missing apDosLogConf", + }, + { + protected: &v1beta1.DosProtectedResource{ + Spec: v1beta1.DosProtectedResourceSpec{ + Name: "name", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "example.service.com:123", + DosSecurityLog: &v1beta1.DosSecurityLog{ + DosLogDest: "service.org:123", + ApDosLogConf: "bad$%^$%name", + }, + }, + }, + expectErr: "error validating DosProtectedResource: invalid field: dosSecurityLog/apDosLogConf err: reference name is invalid: bad$%^$%name", + msg: "DosSecurityLog with invalid apDosLogConf", + }, + { + protected: &v1beta1.DosProtectedResource{ + Spec: v1beta1.DosProtectedResourceSpec{ + Name: "name", + ApDosMonitor: &v1beta1.ApDosMonitor{ + Uri: "example.com", + }, + DosAccessLogDest: "example.service.com:123", + DosSecurityLog: &v1beta1.DosSecurityLog{ + DosLogDest: "service.org:123", + ApDosLogConf: "ns/name", + }, + }, + }, + expectErr: "", + msg: "DosSecurityLog with valid apDosLogConf", + }, + } + + for _, test := range tests { + err := ValidateDosProtectedResource(test.protected) + if err != nil { + if test.expectErr == "" { + t.Errorf("ValidateDosProtectedResource() returned unexpected error: '%v' for the case of: '%s'", err, test.msg) + continue + } + if test.expectErr != err.Error() { + t.Errorf("ValidateDosProtectedResource() returned error for the case of '%s', expected err: '%s' got err: '%s'", test.msg, test.expectErr, err.Error()) + } + } else { + if test.expectErr != "" { + t.Errorf("ValidateDosProtectedResource() failed to return expected error: '%v' for the case of: '%s'", test.expectErr, test.msg) + } + } + } +} + +func TestValidateAppProtectDosAccessLogDest(t *testing.T) { + // Positive test cases + posDstAntns := []string{ + "10.10.1.1:514", + "localhost:514", + "dns.test.svc.cluster.local:514", + "cluster.local:514", + "dash-test.cluster.local:514", + } + + // Negative test cases item, expected error message + negDstAntns := [][]string{ + {"NotValid", "invalid log destination: NotValid, must follow format: : or stderr"}, + {"cluster.local", "invalid log destination: cluster.local, must follow format: : or stderr"}, + {"-cluster.local:514", "invalid log destination: -cluster.local:514, must follow format: : or stderr"}, + {"10.10.1.1:99999", "not a valid port number"}, + } + + for _, tCase := range posDstAntns { + err := validateAppProtectDosLogDest(tCase) + if err != nil { + t.Errorf("expected nil, got %v", err) + } + } + + for _, nTCase := range negDstAntns { + err := validateAppProtectDosLogDest(nTCase[0]) + if err == nil { + t.Errorf("got no error expected error containing '%s'", nTCase[1]) + } else { + if !strings.Contains(err.Error(), nTCase[1]) { + t.Errorf("got '%v', expected: '%s'", err, nTCase[1]) + } + } + } +} + +func TestValidateAppProtectDosLogConf(t *testing.T) { + tests := []struct { + logConf *unstructured.Unstructured + expectErr bool + msg string + }{ + { + logConf: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "content": map[string]interface{}{}, + "filter": map[string]interface{}{}, + }, + }, + }, + expectErr: false, + msg: "valid log conf", + }, + { + logConf: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "filter": map[string]interface{}{}, + }, + }, + }, + expectErr: true, + msg: "invalid log conf with no content field", + }, + { + logConf: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "content": map[string]interface{}{}, + }, + }, + }, + expectErr: true, + msg: "invalid log conf with no filter field", + }, + { + logConf: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "something": map[string]interface{}{ + "content": map[string]interface{}{}, + "filter": map[string]interface{}{}, + }, + }, + }, + expectErr: true, + msg: "invalid log conf with no spec field", + }, + } + + for _, test := range tests { + err := ValidateAppProtectDosLogConf(test.logConf) + if test.expectErr && err == nil { + t.Errorf("validateAppProtectDosLogConf() returned no error for the case of %s", test.msg) + } + if !test.expectErr && err != nil { + t.Errorf("validateAppProtectDosLogConf() returned unexpected error %v for the case of %s", err, test.msg) + } + } +} + +func TestValidateAppProtectDosPolicy(t *testing.T) { + tests := []struct { + policy *unstructured.Unstructured + expectErr bool + msg string + }{ + { + policy: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{}, + }, + }, + expectErr: false, + msg: "valid policy", + }, + { + policy: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "something": map[string]interface{}{}, + }, + }, + expectErr: true, + msg: "invalid policy with no spec field", + }, + } + + for _, test := range tests { + err := ValidateAppProtectDosPolicy(test.policy) + if test.expectErr && err == nil { + t.Errorf("validateAppProtectPolicy() returned no error for the case of %s", test.msg) + } + if !test.expectErr && err != nil { + t.Errorf("validateAppProtectPolicy() returned unexpected error %v for the case of %s", err, test.msg) + } + } +} + +func TestValidateAppProtectDosName(t *testing.T) { + // Positive test cases + posDstAntns := []string{"example.com", "\\\"example.com\\\""} + + // Negative test cases item, expected error message + negDstAntns := [][]string{ + {"very very very very very very very very very very very very very very very very very very long Name", fmt.Sprintf(`app Protect Dos Name max length is %v`, MaxNameLength)}, + {"example.com\\", "must have all '\"' (double quotes) escaped and must not end with an unescaped '\\' (backslash) (e.g. 'protected-object-one', regex used for validation is '([^\"\\\\]|\\\\.)*')"}, + {"\"example.com\"", "must have all '\"' (double quotes) escaped and must not end with an unescaped '\\' (backslash) (e.g. 'protected-object-one', regex used for validation is '([^\"\\\\]|\\\\.)*')"}, + } + + for _, tCase := range posDstAntns { + err := validateAppProtectDosName(tCase) + if err != nil { + t.Errorf("got %v expected nil", err) + } + } + + for _, nTCase := range negDstAntns { + err := validateAppProtectDosName(nTCase[0]) + if err == nil { + t.Errorf("got no error expected error containing %s", nTCase[1]) + } else { + if !strings.Contains(err.Error(), nTCase[1]) { + t.Errorf("got '%v'\n expected: '%s'\n", err, nTCase[1]) + } + } + } +} + +func TestValidateAppProtectDosMonitor(t *testing.T) { + // Positive test cases + posDstAntns := []v1beta1.ApDosMonitor{ + { + Uri: "example.com", + Protocol: "http1", + Timeout: 5, + }, + { + Uri: "https://example.com/good_path", + Protocol: "http2", + Timeout: 10, + }, + { + Uri: "https://example.com/good_path", + Protocol: "grpc", + Timeout: 10, + }, + } + negDstAntns := []struct { + apDosMonitor v1beta1.ApDosMonitor + msg string + }{ + { + apDosMonitor: v1beta1.ApDosMonitor{ + Uri: "http://example.com/%", + Protocol: "http1", + Timeout: 5, + }, + msg: "app Protect Dos Monitor must have valid URL", + }, + { + apDosMonitor: v1beta1.ApDosMonitor{ + Uri: "http://example.com/\\", + Protocol: "http1", + Timeout: 5, + }, + msg: "must have all '\"' (double quotes) escaped and must not end with an unescaped '\\' (backslash) (e.g. 'http://www.example.com', regex used for validation is '([^\"\\\\]|\\\\.)*')", + }, + { + apDosMonitor: v1beta1.ApDosMonitor{ + Uri: "example.com", + Protocol: "http3", + Timeout: 5, + }, + msg: "app Protect Dos Monitor Protocol must be: dosMonitorProtocol: Invalid value: \"http3\": 'http3' contains an invalid NGINX parameter. Accepted parameters are: http1, http2, grpc", + }, + } + + for _, tCase := range posDstAntns { + err := validateAppProtectDosMonitor(tCase) + if err != nil { + t.Errorf("got %v expected nil", err) + } + } + + for _, nTCase := range negDstAntns { + err := validateAppProtectDosMonitor(nTCase.apDosMonitor) + if err == nil { + t.Errorf("got no error expected error containing %s", nTCase.msg) + } else { + if !strings.Contains(err.Error(), nTCase.msg) { + t.Errorf("got %v expected to contain: %s", err, nTCase.msg) + } + } + } +} diff --git a/pkg/apis/configuration/validation/policy.go b/pkg/apis/configuration/validation/policy.go index 7d367e5c44..1a261392fc 100644 --- a/pkg/apis/configuration/validation/policy.go +++ b/pkg/apis/configuration/validation/policy.go @@ -8,7 +8,6 @@ import ( "strconv" "strings" - "github.com/nginxinc/kubernetes-ingress/internal/k8s/appprotect" v1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/v1" "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" @@ -103,7 +102,7 @@ func validatePolicySpec(spec *v1.PolicySpec, fieldPath *field.Path, isPlus, enab if fieldCount != 1 { msg := "must specify exactly one of: `accessControl`, `rateLimit`, `ingressMTLS`, `egressMTLS`" if isPlus { - msg = fmt.Sprint(msg, ", `jwt`, `oidc`, `waf`") + msg = fmt.Sprint(msg, ", `jwt`, `oidc`, `waf`, `dos`") } allErrs = append(allErrs, field.Invalid(fieldPath, "", msg)) } @@ -277,7 +276,7 @@ func validateLogConf(logConf, logDest string, fieldPath *field.Path) field.Error } } - err := appprotect.ValidateAppProtectLogDestination(logDest) + err := ValidateAppProtectLogDestination(logDest) if err != nil { allErrs = append(allErrs, field.Invalid(fieldPath.Child("logDest"), logDest, err.Error())) } @@ -456,9 +455,8 @@ func validateRateLimitKey(key string, fieldPath *field.Path, isPlus bool) field. return append(allErrs, field.Required(fieldPath, "")) } - if !escapedStringsFmtRegexp.MatchString(key) { - msg := validation.RegexError(escapedStringsErrMsg, escapedStringsFmt, `Hello World! \n`, `\"${request_uri}\" is unavailable. \n`) - allErrs = append(allErrs, field.Invalid(fieldPath, key, msg)) + if err := validateEscapedString(key, `Hello World! \n`, `\"${request_uri}\" is unavailable. \n`); err != nil { + allErrs = append(allErrs, field.Invalid(fieldPath, key, err.Error())) } allErrs = append(allErrs, validateStringWithVariables(key, fieldPath, rateLimitKeySpecialVariables, rateLimitKeyVariables, isPlus)...) diff --git a/pkg/apis/configuration/validation/transportserver.go b/pkg/apis/configuration/validation/transportserver.go index a65c742b9e..5f66cc8af1 100644 --- a/pkg/apis/configuration/validation/transportserver.go +++ b/pkg/apis/configuration/validation/transportserver.go @@ -249,8 +249,8 @@ func validateHashLoadBalancingMethod(method string, fieldPath *field.Path, isPlu allErrs = append(allErrs, varErrs...) } - if !escapedStringsFmtRegexp.MatchString(method) { - msg := fmt.Sprintf("invalid value for hash: %v", validation.RegexError(escapedStringsErrMsg, escapedStringsFmt)) + if err := validateEscapedString(method); err != nil { + msg := fmt.Sprintf("invalid value for hash: %v", err) return append(allErrs, field.Invalid(fieldPath, method, msg)) } @@ -297,9 +297,8 @@ func validateMatchExpect(expect string, fieldPath *field.Path) field.ErrorList { return allErrs } - if !escapedStringsFmtRegexp.MatchString(expect) { - msg := validation.RegexError(escapedStringsErrMsg, escapedStringsFmt) - return append(allErrs, field.Invalid(fieldPath, expect, msg)) + if err := validateEscapedString(expect); err != nil { + return append(allErrs, field.Invalid(fieldPath, expect, err.Error())) } if strings.HasPrefix(expect, "~") { @@ -328,9 +327,9 @@ func validateMatchSend(send string, fieldPath *field.Path) field.ErrorList { if send == "" { return allErrs } - if !escapedStringsFmtRegexp.MatchString(send) { - msg := validation.RegexError(escapedStringsErrMsg, escapedStringsFmt) - return append(allErrs, field.Invalid(fieldPath, send, msg)) + + if err := validateEscapedString(send); err != nil { + return append(allErrs, field.Invalid(fieldPath, send, err.Error())) } err := validateHexString(send) diff --git a/pkg/apis/configuration/validation/virtualserver.go b/pkg/apis/configuration/validation/virtualserver.go index 21e32581d9..192f5378f1 100644 --- a/pkg/apis/configuration/validation/virtualserver.go +++ b/pkg/apis/configuration/validation/virtualserver.go @@ -44,6 +44,8 @@ func (vsv *VirtualServerValidator) validateVirtualServerSpec(spec *v1.VirtualSer allErrs = append(allErrs, vsv.validateVirtualServerRoutes(spec.Routes, fieldPath.Child("routes"), upstreamNames, namespace)...) + allErrs = append(allErrs, validateDos(spec.Dos, fieldPath.Child("dos"))...) + return allErrs } @@ -114,6 +116,21 @@ func validateTLS(tls *v1.TLS, fieldPath *field.Path) field.ErrorList { return allErrs } +func validateDos(dos string, fieldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if dos == "" { + // valid, dos is not required + return allErrs + } + + for _, msg := range validation.IsQualifiedName(dos) { + allErrs = append(allErrs, field.Invalid(fieldPath, dos, msg)) + } + + return allErrs +} + func validateTLSRedirect(redirect *v1.TLSRedirect, fieldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} @@ -718,9 +735,8 @@ func (vsv *VirtualServerValidator) validateErrorPageHeader(h v1.Header, fieldPat allErrs = append(allErrs, field.Invalid(fieldPath.Child("name"), h.Name, msg)) } - if !escapedStringsFmtRegexp.MatchString(h.Value) { - msg := validation.RegexError(escapedStringsErrMsg, escapedStringsFmt, "value", `\"${status}\"`) - allErrs = append(allErrs, field.Invalid(fieldPath.Child("value"), h.Value, msg)) + if err := validateEscapedString(h.Value, "value", `\"${status}\"`); err != nil { + allErrs = append(allErrs, field.Invalid(fieldPath.Child("value"), h.Value, err.Error())) } allErrs = append(allErrs, validateStringWithVariables(h.Value, fieldPath.Child("value"), nil, errorPageHeaderValueVariables, vsv.isPlus)...) @@ -859,9 +875,8 @@ func (vsv *VirtualServerValidator) validateRedirectURL(redirectURL string, field return append(allErrs, field.Invalid(fieldPath, redirectURL, "must contain the protocol with '://', for example http://, https:// or ${scheme}://")) } - if !escapedStringsFmtRegexp.MatchString(redirectURL) { - msg := validation.RegexError(escapedStringsErrMsg, escapedStringsFmt, "http://www.nginx.com", "${scheme}://${host}/green/", `\"http://www.nginx.com\"`) - return append(allErrs, field.Invalid(fieldPath, redirectURL, msg)) + if err := validateEscapedString(redirectURL, "http://www.nginx.com", "${scheme}://${host}/green/", `\"http://www.nginx.com\"`); err != nil { + return append(allErrs, field.Invalid(fieldPath, redirectURL, err.Error())) } allErrs = append(allErrs, validateStringWithVariables(redirectURL, fieldPath, nil, validVars, vsv.isPlus)...) @@ -903,9 +918,8 @@ func (vsv *VirtualServerValidator) validateActionReturn(r *v1.ActionReturn, fiel func validateEscapedStringWithVariables(body string, fieldPath *field.Path, specialValidVars []string, validVars map[string]bool, isPlus bool) field.ErrorList { allErrs := field.ErrorList{} - if !escapedStringsFmtRegexp.MatchString(body) { - msg := validation.RegexError(escapedStringsErrMsg, escapedStringsFmt, `Hello World! \n`, `\"${request_uri}\" is unavailable. \n`) - allErrs = append(allErrs, field.Invalid(fieldPath, body, msg)) + if err := validateEscapedString(body, `Hello World! \n`, `\"${request_uri}\" is unavailable. \n`); err != nil { + allErrs = append(allErrs, field.Invalid(fieldPath, body, err.Error())) } allErrs = append(allErrs, validateStringWithVariables(body, fieldPath, specialValidVars, validVars, isPlus)...) @@ -1006,9 +1020,8 @@ func validateActionProxyRewritePathForRegexp(rewritePath string, fieldPath *fiel allErrs = append(allErrs, validateStringNoVariables(rewritePath, fieldPath)...) - if !escapedStringsFmtRegexp.MatchString(rewritePath) { - msg := validation.RegexError(escapedStringsErrMsg, escapedStringsFmt, "/rewrite$1", "/images") - allErrs = append(allErrs, field.Invalid(fieldPath, rewritePath, msg)) + if err := validateEscapedString(rewritePath, "/rewrite$1", "/images"); err != nil { + allErrs = append(allErrs, field.Invalid(fieldPath, rewritePath, err.Error())) } return allErrs @@ -1211,9 +1224,8 @@ func validateRegexPath(path string, fieldPath *field.Path) field.ErrorList { return append(allErrs, field.Invalid(fieldPath, path, fmt.Sprintf("must be a valid regular expression: %v", err))) } - if !escapedStringsFmtRegexp.MatchString(path) { - msg := validation.RegexError(escapedStringsErrMsg, escapedStringsFmt, "*.jpg", "^/images/image_*.png$") - return append(allErrs, field.Invalid(fieldPath, path, msg)) + if err := validateEscapedString(path, "*.jpg", "^/images/image_*.png$"); err != nil { + return append(allErrs, field.Invalid(fieldPath, path, err.Error())) } return allErrs @@ -1394,8 +1406,8 @@ func validateVariableName(name string, fieldPath *field.Path) field.ErrorList { } func isValidMatchValue(value string) []string { - if !escapedStringsFmtRegexp.MatchString(value) { - return []string{validation.RegexError(escapedStringsErrMsg, escapedStringsFmt, "value-123")} + if err := validateEscapedString(value, "value-123"); err != nil { + return []string{err.Error()} } return nil } diff --git a/pkg/apis/configuration/validation/virtualserver_test.go b/pkg/apis/configuration/validation/virtualserver_test.go index fc7bd2f405..38ea2ce5f3 100644 --- a/pkg/apis/configuration/validation/virtualserver_test.go +++ b/pkg/apis/configuration/validation/virtualserver_test.go @@ -52,6 +52,7 @@ func TestValidateVirtualServer(t *testing.T) { }, }, }, + Dos: "some-ns/some-name", }, } @@ -93,6 +94,37 @@ func TestValidateHost(t *testing.T) { } } +func TestValidateDos(t *testing.T) { + validDosResources := []string{ + "hello", + "ns/hello", + "hello-world-1", + } + + for _, h := range validDosResources { + allErrs := validateDos(h, field.NewPath("dos")) + if len(allErrs) > 0 { + t.Errorf("validateDos(%q) returned errors %v for valid input", h, allErrs) + } + } + + invalidDos := []string{ + "*", + "..", + ".example.com", + "-hello-world-1", + "/hello/world-1", + "hello/world/other", + } + + for _, h := range invalidDos { + allErrs := validateDos(h, field.NewPath("dos")) + if len(allErrs) == 0 { + t.Errorf("validateDos(%q) returned no errors for invalid input", h) + } + } +} + func TestValidatePolicies(t *testing.T) { tests := []struct { policies []v1.PolicyReference diff --git a/pkg/apis/dos/register.go b/pkg/apis/dos/register.go new file mode 100644 index 0000000000..f446244e9c --- /dev/null +++ b/pkg/apis/dos/register.go @@ -0,0 +1,5 @@ +package dos + +const ( + GroupName = "appprotectdos.f5.com" +) diff --git a/pkg/apis/dos/v1beta1/doc.go b/pkg/apis/dos/v1beta1/doc.go new file mode 100644 index 0000000000..eb58836459 --- /dev/null +++ b/pkg/apis/dos/v1beta1/doc.go @@ -0,0 +1,5 @@ +// +k8s:deepcopy-gen=package +// +groupName=appprotectdos.f5.com + +// Package v1beta1 is the v1beta1 version of the API. +package v1beta1 diff --git a/pkg/apis/dos/v1beta1/register.go b/pkg/apis/dos/v1beta1/register.go new file mode 100644 index 0000000000..504033d0bc --- /dev/null +++ b/pkg/apis/dos/v1beta1/register.go @@ -0,0 +1,35 @@ +package v1beta1 + +import ( + "github.com/nginxinc/kubernetes-ingress/pkg/apis/dos" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// SchemeGroupVersion is group version used to register these object. +var SchemeGroupVersion = schema.GroupVersion{Group: dos.GroupName, Version: "v1beta1"} + +// Kind takes an unqualified kind and returns back a Group qualified GroupKind. +func Kind(kind string) schema.GroupKind { + return SchemeGroupVersion.WithKind(kind).GroupKind() +} + +// Resource takes an unqualified resource and returns a Group qualified GroupResource. +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +var ( + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + AddToScheme = SchemeBuilder.AddToScheme +) + +// Adds the list of known types to Scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &DosProtectedResource{}, + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} diff --git a/pkg/apis/dos/v1beta1/types.go b/pkg/apis/dos/v1beta1/types.go new file mode 100644 index 0000000000..79ae56ea68 --- /dev/null +++ b/pkg/apis/dos/v1beta1/types.go @@ -0,0 +1,60 @@ +package v1beta1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:validation:Optional +// +kubebuilder:resource:shortName=pr + +// DosProtectedResource defines a Dos protected resource. +type DosProtectedResource struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec DosProtectedResourceSpec `json:"spec"` +} + +type DosProtectedResourceSpec struct { + // Enable enables the DOS feature if set to true + Enable bool `json:"enable"` + // Name is the name of protected object, max of 63 characters. + Name string `json:"name"` + ApDosMonitor *ApDosMonitor `json:"apDosMonitor"` + // DosAccessLogDest is the network address for the access logs + DosAccessLogDest string `json:"dosAccessLogDest"` + // ApDosPolicy is the namespace/name of a ApDosPolicy resource + ApDosPolicy string `json:"apDosPolicy"` + DosSecurityLog *DosSecurityLog `json:"dosSecurityLog"` +} + +// ApDosMonitor is how NGINX App Protect DoS monitors the stress level of the protected object. The monitor requests are sent from localhost (127.0.0.1). Default value: URI - None, protocol - http1, timeout - NGINX App Protect DoS default. +type ApDosMonitor struct { + // Uri is the destination to the desired protected object in the nginx.conf: + Uri string `json:"uri"` + // +kubebuilder:validation:Enum=http1;http2;grpc + // Protocol determines if the server listens on http1 / http2 / grpc. The default is http1. + Protocol string `json:"protocol"` + // Timeout determines how long (in seconds) should NGINX App Protect DoS wait for a response. Default is 10 seconds for http1/http2 and 5 seconds for grpc. + Timeout uint64 `json:"timeout"` +} + +// DosSecurityLog defines the security log of the DosProtectedResource. +type DosSecurityLog struct { + // Enable enables the security logging feature if set to true + Enable bool `json:"enable"` + // ApDosLogConf is the namespace/name of a APDosLogConf resource + ApDosLogConf string `json:"apDosLogConf"` + // DosLogDest is the network address of a logging service, can be either IP or DNS name. + DosLogDest string `json:"dosLogDest"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// DosProtectedResourceList is a list of the DosProtectedResource resources. +type DosProtectedResourceList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + + Items []DosProtectedResource `json:"items"` +} diff --git a/pkg/apis/dos/v1beta1/zz_generated.deepcopy.go b/pkg/apis/dos/v1beta1/zz_generated.deepcopy.go new file mode 100644 index 0000000000..d244537393 --- /dev/null +++ b/pkg/apis/dos/v1beta1/zz_generated.deepcopy.go @@ -0,0 +1,127 @@ +// +build !ignore_autogenerated + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1beta1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ApDosMonitor) DeepCopyInto(out *ApDosMonitor) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApDosMonitor. +func (in *ApDosMonitor) DeepCopy() *ApDosMonitor { + if in == nil { + return nil + } + out := new(ApDosMonitor) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DosProtectedResource) DeepCopyInto(out *DosProtectedResource) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DosProtectedResource. +func (in *DosProtectedResource) DeepCopy() *DosProtectedResource { + if in == nil { + return nil + } + out := new(DosProtectedResource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DosProtectedResource) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DosProtectedResourceList) DeepCopyInto(out *DosProtectedResourceList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DosProtectedResource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DosProtectedResourceList. +func (in *DosProtectedResourceList) DeepCopy() *DosProtectedResourceList { + if in == nil { + return nil + } + out := new(DosProtectedResourceList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DosProtectedResourceList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DosProtectedResourceSpec) DeepCopyInto(out *DosProtectedResourceSpec) { + *out = *in + if in.ApDosMonitor != nil { + in, out := &in.ApDosMonitor, &out.ApDosMonitor + *out = new(ApDosMonitor) + **out = **in + } + if in.DosSecurityLog != nil { + in, out := &in.DosSecurityLog, &out.DosSecurityLog + *out = new(DosSecurityLog) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DosProtectedResourceSpec. +func (in *DosProtectedResourceSpec) DeepCopy() *DosProtectedResourceSpec { + if in == nil { + return nil + } + out := new(DosProtectedResourceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DosSecurityLog) DeepCopyInto(out *DosSecurityLog) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DosSecurityLog. +func (in *DosSecurityLog) DeepCopy() *DosSecurityLog { + if in == nil { + return nil + } + out := new(DosSecurityLog) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/client/clientset/versioned/clientset.go b/pkg/client/clientset/versioned/clientset.go index ab12dac6ae..f5744d0826 100644 --- a/pkg/client/clientset/versioned/clientset.go +++ b/pkg/client/clientset/versioned/clientset.go @@ -7,6 +7,7 @@ import ( k8sv1 "github.com/nginxinc/kubernetes-ingress/pkg/client/clientset/versioned/typed/configuration/v1" k8sv1alpha1 "github.com/nginxinc/kubernetes-ingress/pkg/client/clientset/versioned/typed/configuration/v1alpha1" + appprotectdosv1beta1 "github.com/nginxinc/kubernetes-ingress/pkg/client/clientset/versioned/typed/dos/v1beta1" discovery "k8s.io/client-go/discovery" rest "k8s.io/client-go/rest" flowcontrol "k8s.io/client-go/util/flowcontrol" @@ -16,14 +17,16 @@ type Interface interface { Discovery() discovery.DiscoveryInterface K8sV1alpha1() k8sv1alpha1.K8sV1alpha1Interface K8sV1() k8sv1.K8sV1Interface + AppprotectdosV1beta1() appprotectdosv1beta1.AppprotectdosV1beta1Interface } // Clientset contains the clients for groups. Each group has exactly one // version included in a Clientset. type Clientset struct { *discovery.DiscoveryClient - k8sV1alpha1 *k8sv1alpha1.K8sV1alpha1Client - k8sV1 *k8sv1.K8sV1Client + k8sV1alpha1 *k8sv1alpha1.K8sV1alpha1Client + k8sV1 *k8sv1.K8sV1Client + appprotectdosV1beta1 *appprotectdosv1beta1.AppprotectdosV1beta1Client } // K8sV1alpha1 retrieves the K8sV1alpha1Client @@ -36,6 +39,11 @@ func (c *Clientset) K8sV1() k8sv1.K8sV1Interface { return c.k8sV1 } +// AppprotectdosV1beta1 retrieves the AppprotectdosV1beta1Client +func (c *Clientset) AppprotectdosV1beta1() appprotectdosv1beta1.AppprotectdosV1beta1Interface { + return c.appprotectdosV1beta1 +} + // Discovery retrieves the DiscoveryClient func (c *Clientset) Discovery() discovery.DiscoveryInterface { if c == nil { @@ -65,6 +73,10 @@ func NewForConfig(c *rest.Config) (*Clientset, error) { if err != nil { return nil, err } + cs.appprotectdosV1beta1, err = appprotectdosv1beta1.NewForConfig(&configShallowCopy) + if err != nil { + return nil, err + } cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(&configShallowCopy) if err != nil { @@ -79,6 +91,7 @@ func NewForConfigOrDie(c *rest.Config) *Clientset { var cs Clientset cs.k8sV1alpha1 = k8sv1alpha1.NewForConfigOrDie(c) cs.k8sV1 = k8sv1.NewForConfigOrDie(c) + cs.appprotectdosV1beta1 = appprotectdosv1beta1.NewForConfigOrDie(c) cs.DiscoveryClient = discovery.NewDiscoveryClientForConfigOrDie(c) return &cs @@ -89,6 +102,7 @@ func New(c rest.Interface) *Clientset { var cs Clientset cs.k8sV1alpha1 = k8sv1alpha1.New(c) cs.k8sV1 = k8sv1.New(c) + cs.appprotectdosV1beta1 = appprotectdosv1beta1.New(c) cs.DiscoveryClient = discovery.NewDiscoveryClient(c) return &cs diff --git a/pkg/client/clientset/versioned/fake/clientset_generated.go b/pkg/client/clientset/versioned/fake/clientset_generated.go index 770578b3e2..c6b371b0a8 100644 --- a/pkg/client/clientset/versioned/fake/clientset_generated.go +++ b/pkg/client/clientset/versioned/fake/clientset_generated.go @@ -8,6 +8,8 @@ import ( fakek8sv1 "github.com/nginxinc/kubernetes-ingress/pkg/client/clientset/versioned/typed/configuration/v1/fake" k8sv1alpha1 "github.com/nginxinc/kubernetes-ingress/pkg/client/clientset/versioned/typed/configuration/v1alpha1" fakek8sv1alpha1 "github.com/nginxinc/kubernetes-ingress/pkg/client/clientset/versioned/typed/configuration/v1alpha1/fake" + appprotectdosv1beta1 "github.com/nginxinc/kubernetes-ingress/pkg/client/clientset/versioned/typed/dos/v1beta1" + fakeappprotectdosv1beta1 "github.com/nginxinc/kubernetes-ingress/pkg/client/clientset/versioned/typed/dos/v1beta1/fake" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/discovery" @@ -60,7 +62,10 @@ func (c *Clientset) Tracker() testing.ObjectTracker { return c.tracker } -var _ clientset.Interface = &Clientset{} +var ( + _ clientset.Interface = &Clientset{} + _ testing.FakeClient = &Clientset{} +) // K8sV1alpha1 retrieves the K8sV1alpha1Client func (c *Clientset) K8sV1alpha1() k8sv1alpha1.K8sV1alpha1Interface { @@ -71,3 +76,8 @@ func (c *Clientset) K8sV1alpha1() k8sv1alpha1.K8sV1alpha1Interface { func (c *Clientset) K8sV1() k8sv1.K8sV1Interface { return &fakek8sv1.FakeK8sV1{Fake: &c.Fake} } + +// AppprotectdosV1beta1 retrieves the AppprotectdosV1beta1Client +func (c *Clientset) AppprotectdosV1beta1() appprotectdosv1beta1.AppprotectdosV1beta1Interface { + return &fakeappprotectdosv1beta1.FakeAppprotectdosV1beta1{Fake: &c.Fake} +} diff --git a/pkg/client/clientset/versioned/fake/register.go b/pkg/client/clientset/versioned/fake/register.go index 685ea92d8b..48e1f1eb51 100644 --- a/pkg/client/clientset/versioned/fake/register.go +++ b/pkg/client/clientset/versioned/fake/register.go @@ -5,6 +5,7 @@ package fake import ( k8sv1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/v1" k8sv1alpha1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/v1alpha1" + appprotectdosv1beta1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/dos/v1beta1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" @@ -12,14 +13,13 @@ import ( utilruntime "k8s.io/apimachinery/pkg/util/runtime" ) -var ( - scheme = runtime.NewScheme() - codecs = serializer.NewCodecFactory(scheme) -) +var scheme = runtime.NewScheme() +var codecs = serializer.NewCodecFactory(scheme) var localSchemeBuilder = runtime.SchemeBuilder{ k8sv1alpha1.AddToScheme, k8sv1.AddToScheme, + appprotectdosv1beta1.AddToScheme, } // AddToScheme adds all types of this clientset into the given scheme. This allows composition diff --git a/pkg/client/clientset/versioned/scheme/register.go b/pkg/client/clientset/versioned/scheme/register.go index d5d196545a..4d37ce9f1b 100644 --- a/pkg/client/clientset/versioned/scheme/register.go +++ b/pkg/client/clientset/versioned/scheme/register.go @@ -5,6 +5,7 @@ package scheme import ( k8sv1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/v1" k8sv1alpha1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/v1alpha1" + appprotectdosv1beta1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/dos/v1beta1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" @@ -12,15 +13,14 @@ import ( utilruntime "k8s.io/apimachinery/pkg/util/runtime" ) -var ( - Scheme = runtime.NewScheme() - Codecs = serializer.NewCodecFactory(Scheme) - ParameterCodec = runtime.NewParameterCodec(Scheme) - localSchemeBuilder = runtime.SchemeBuilder{ - k8sv1alpha1.AddToScheme, - k8sv1.AddToScheme, - } -) +var Scheme = runtime.NewScheme() +var Codecs = serializer.NewCodecFactory(Scheme) +var ParameterCodec = runtime.NewParameterCodec(Scheme) +var localSchemeBuilder = runtime.SchemeBuilder{ + k8sv1alpha1.AddToScheme, + k8sv1.AddToScheme, + appprotectdosv1beta1.AddToScheme, +} // AddToScheme adds all types of this clientset into the given scheme. This allows composition // of clientsets, like in: diff --git a/pkg/client/clientset/versioned/typed/configuration/v1/fake/fake_policy.go b/pkg/client/clientset/versioned/typed/configuration/v1/fake/fake_policy.go index 0c6b8e9e45..027e0e8fc9 100644 --- a/pkg/client/clientset/versioned/typed/configuration/v1/fake/fake_policy.go +++ b/pkg/client/clientset/versioned/typed/configuration/v1/fake/fake_policy.go @@ -61,6 +61,7 @@ func (c *FakePolicies) List(ctx context.Context, opts v1.ListOptions) (result *c func (c *FakePolicies) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { return c.Fake. InvokesWatch(testing.NewWatchAction(policiesResource, c.ns, opts)) + } // Create takes the representation of a policy and creates it. Returns the server's representation of the policy, and an error, if there is any. diff --git a/pkg/client/clientset/versioned/typed/configuration/v1/fake/fake_virtualserver.go b/pkg/client/clientset/versioned/typed/configuration/v1/fake/fake_virtualserver.go index 6a90420c88..9e0bdbd8be 100644 --- a/pkg/client/clientset/versioned/typed/configuration/v1/fake/fake_virtualserver.go +++ b/pkg/client/clientset/versioned/typed/configuration/v1/fake/fake_virtualserver.go @@ -61,6 +61,7 @@ func (c *FakeVirtualServers) List(ctx context.Context, opts v1.ListOptions) (res func (c *FakeVirtualServers) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { return c.Fake. InvokesWatch(testing.NewWatchAction(virtualserversResource, c.ns, opts)) + } // Create takes the representation of a virtualServer and creates it. Returns the server's representation of the virtualServer, and an error, if there is any. diff --git a/pkg/client/clientset/versioned/typed/configuration/v1/fake/fake_virtualserverroute.go b/pkg/client/clientset/versioned/typed/configuration/v1/fake/fake_virtualserverroute.go index f459f75a98..d6cd85d7d9 100644 --- a/pkg/client/clientset/versioned/typed/configuration/v1/fake/fake_virtualserverroute.go +++ b/pkg/client/clientset/versioned/typed/configuration/v1/fake/fake_virtualserverroute.go @@ -61,6 +61,7 @@ func (c *FakeVirtualServerRoutes) List(ctx context.Context, opts v1.ListOptions) func (c *FakeVirtualServerRoutes) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { return c.Fake. InvokesWatch(testing.NewWatchAction(virtualserverroutesResource, c.ns, opts)) + } // Create takes the representation of a virtualServerRoute and creates it. Returns the server's representation of the virtualServerRoute, and an error, if there is any. diff --git a/pkg/client/clientset/versioned/typed/configuration/v1alpha1/configuration_client.go b/pkg/client/clientset/versioned/typed/configuration/v1alpha1/configuration_client.go index fafd2ede6e..28b5a9809a 100644 --- a/pkg/client/clientset/versioned/typed/configuration/v1alpha1/configuration_client.go +++ b/pkg/client/clientset/versioned/typed/configuration/v1alpha1/configuration_client.go @@ -11,6 +11,7 @@ import ( type K8sV1alpha1Interface interface { RESTClient() rest.Interface GlobalConfigurationsGetter + PoliciesGetter TransportServersGetter } @@ -23,6 +24,10 @@ func (c *K8sV1alpha1Client) GlobalConfigurations(namespace string) GlobalConfigu return newGlobalConfigurations(c, namespace) } +func (c *K8sV1alpha1Client) Policies(namespace string) PolicyInterface { + return newPolicies(c, namespace) +} + func (c *K8sV1alpha1Client) TransportServers(namespace string) TransportServerInterface { return newTransportServers(c, namespace) } diff --git a/pkg/client/clientset/versioned/typed/configuration/v1alpha1/fake/fake_configuration_client.go b/pkg/client/clientset/versioned/typed/configuration/v1alpha1/fake/fake_configuration_client.go index 3a9d183f70..9ad22127bd 100644 --- a/pkg/client/clientset/versioned/typed/configuration/v1alpha1/fake/fake_configuration_client.go +++ b/pkg/client/clientset/versioned/typed/configuration/v1alpha1/fake/fake_configuration_client.go @@ -16,6 +16,10 @@ func (c *FakeK8sV1alpha1) GlobalConfigurations(namespace string) v1alpha1.Global return &FakeGlobalConfigurations{c, namespace} } +func (c *FakeK8sV1alpha1) Policies(namespace string) v1alpha1.PolicyInterface { + return &FakePolicies{c, namespace} +} + func (c *FakeK8sV1alpha1) TransportServers(namespace string) v1alpha1.TransportServerInterface { return &FakeTransportServers{c, namespace} } diff --git a/pkg/client/clientset/versioned/typed/configuration/v1alpha1/fake/fake_globalconfiguration.go b/pkg/client/clientset/versioned/typed/configuration/v1alpha1/fake/fake_globalconfiguration.go index b7509602b5..4ac46274ba 100644 --- a/pkg/client/clientset/versioned/typed/configuration/v1alpha1/fake/fake_globalconfiguration.go +++ b/pkg/client/clientset/versioned/typed/configuration/v1alpha1/fake/fake_globalconfiguration.go @@ -61,6 +61,7 @@ func (c *FakeGlobalConfigurations) List(ctx context.Context, opts v1.ListOptions func (c *FakeGlobalConfigurations) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { return c.Fake. InvokesWatch(testing.NewWatchAction(globalconfigurationsResource, c.ns, opts)) + } // Create takes the representation of a globalConfiguration and creates it. Returns the server's representation of the globalConfiguration, and an error, if there is any. diff --git a/pkg/client/clientset/versioned/typed/configuration/v1alpha1/fake/fake_policy.go b/pkg/client/clientset/versioned/typed/configuration/v1alpha1/fake/fake_policy.go new file mode 100644 index 0000000000..38595726c6 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/configuration/v1alpha1/fake/fake_policy.go @@ -0,0 +1,114 @@ +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakePolicies implements PolicyInterface +type FakePolicies struct { + Fake *FakeK8sV1alpha1 + ns string +} + +var policiesResource = schema.GroupVersionResource{Group: "k8s.nginx.org", Version: "v1alpha1", Resource: "policies"} + +var policiesKind = schema.GroupVersionKind{Group: "k8s.nginx.org", Version: "v1alpha1", Kind: "Policy"} + +// Get takes name of the policy, and returns the corresponding policy object, and an error if there is any. +func (c *FakePolicies) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.Policy, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(policiesResource, c.ns, name), &v1alpha1.Policy{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.Policy), err +} + +// List takes label and field selectors, and returns the list of Policies that match those selectors. +func (c *FakePolicies) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.PolicyList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(policiesResource, policiesKind, c.ns, opts), &v1alpha1.PolicyList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.PolicyList{ListMeta: obj.(*v1alpha1.PolicyList).ListMeta} + for _, item := range obj.(*v1alpha1.PolicyList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested policies. +func (c *FakePolicies) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(policiesResource, c.ns, opts)) + +} + +// Create takes the representation of a policy and creates it. Returns the server's representation of the policy, and an error, if there is any. +func (c *FakePolicies) Create(ctx context.Context, policy *v1alpha1.Policy, opts v1.CreateOptions) (result *v1alpha1.Policy, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(policiesResource, c.ns, policy), &v1alpha1.Policy{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.Policy), err +} + +// Update takes the representation of a policy and updates it. Returns the server's representation of the policy, and an error, if there is any. +func (c *FakePolicies) Update(ctx context.Context, policy *v1alpha1.Policy, opts v1.UpdateOptions) (result *v1alpha1.Policy, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(policiesResource, c.ns, policy), &v1alpha1.Policy{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.Policy), err +} + +// Delete takes name of the policy and deletes it. Returns an error if one occurs. +func (c *FakePolicies) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(policiesResource, c.ns, name), &v1alpha1.Policy{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakePolicies) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(policiesResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.PolicyList{}) + return err +} + +// Patch applies the patch and returns the patched policy. +func (c *FakePolicies) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.Policy, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(policiesResource, c.ns, name, pt, data, subresources...), &v1alpha1.Policy{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.Policy), err +} diff --git a/pkg/client/clientset/versioned/typed/configuration/v1alpha1/fake/fake_transportserver.go b/pkg/client/clientset/versioned/typed/configuration/v1alpha1/fake/fake_transportserver.go index 1be68bbe99..735f737e92 100644 --- a/pkg/client/clientset/versioned/typed/configuration/v1alpha1/fake/fake_transportserver.go +++ b/pkg/client/clientset/versioned/typed/configuration/v1alpha1/fake/fake_transportserver.go @@ -61,6 +61,7 @@ func (c *FakeTransportServers) List(ctx context.Context, opts v1.ListOptions) (r func (c *FakeTransportServers) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { return c.Fake. InvokesWatch(testing.NewWatchAction(transportserversResource, c.ns, opts)) + } // Create takes the representation of a transportServer and creates it. Returns the server's representation of the transportServer, and an error, if there is any. diff --git a/pkg/client/clientset/versioned/typed/configuration/v1alpha1/generated_expansion.go b/pkg/client/clientset/versioned/typed/configuration/v1alpha1/generated_expansion.go index 5281dc8543..80fc437593 100644 --- a/pkg/client/clientset/versioned/typed/configuration/v1alpha1/generated_expansion.go +++ b/pkg/client/clientset/versioned/typed/configuration/v1alpha1/generated_expansion.go @@ -4,4 +4,6 @@ package v1alpha1 type GlobalConfigurationExpansion interface{} +type PolicyExpansion interface{} + type TransportServerExpansion interface{} diff --git a/pkg/client/clientset/versioned/typed/configuration/v1alpha1/policy.go b/pkg/client/clientset/versioned/typed/configuration/v1alpha1/policy.go new file mode 100644 index 0000000000..f90324d076 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/configuration/v1alpha1/policy.go @@ -0,0 +1,162 @@ +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + "time" + + v1alpha1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/v1alpha1" + scheme "github.com/nginxinc/kubernetes-ingress/pkg/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// PoliciesGetter has a method to return a PolicyInterface. +// A group's client should implement this interface. +type PoliciesGetter interface { + Policies(namespace string) PolicyInterface +} + +// PolicyInterface has methods to work with Policy resources. +type PolicyInterface interface { + Create(ctx context.Context, policy *v1alpha1.Policy, opts v1.CreateOptions) (*v1alpha1.Policy, error) + Update(ctx context.Context, policy *v1alpha1.Policy, opts v1.UpdateOptions) (*v1alpha1.Policy, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.Policy, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.PolicyList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.Policy, err error) + PolicyExpansion +} + +// policies implements PolicyInterface +type policies struct { + client rest.Interface + ns string +} + +// newPolicies returns a Policies +func newPolicies(c *K8sV1alpha1Client, namespace string) *policies { + return &policies{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the policy, and returns the corresponding policy object, and an error if there is any. +func (c *policies) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.Policy, err error) { + result = &v1alpha1.Policy{} + err = c.client.Get(). + Namespace(c.ns). + Resource("policies"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of Policies that match those selectors. +func (c *policies) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.PolicyList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.PolicyList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("policies"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested policies. +func (c *policies) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("policies"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a policy and creates it. Returns the server's representation of the policy, and an error, if there is any. +func (c *policies) Create(ctx context.Context, policy *v1alpha1.Policy, opts v1.CreateOptions) (result *v1alpha1.Policy, err error) { + result = &v1alpha1.Policy{} + err = c.client.Post(). + Namespace(c.ns). + Resource("policies"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(policy). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a policy and updates it. Returns the server's representation of the policy, and an error, if there is any. +func (c *policies) Update(ctx context.Context, policy *v1alpha1.Policy, opts v1.UpdateOptions) (result *v1alpha1.Policy, err error) { + result = &v1alpha1.Policy{} + err = c.client.Put(). + Namespace(c.ns). + Resource("policies"). + Name(policy.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(policy). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the policy and deletes it. Returns an error if one occurs. +func (c *policies) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("policies"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *policies) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("policies"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched policy. +func (c *policies) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.Policy, err error) { + result = &v1alpha1.Policy{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("policies"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/pkg/client/clientset/versioned/typed/dos/v1beta1/doc.go b/pkg/client/clientset/versioned/typed/dos/v1beta1/doc.go new file mode 100644 index 0000000000..897c0995f8 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/dos/v1beta1/doc.go @@ -0,0 +1,4 @@ +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated typed clients. +package v1beta1 diff --git a/pkg/client/clientset/versioned/typed/dos/v1beta1/dos_client.go b/pkg/client/clientset/versioned/typed/dos/v1beta1/dos_client.go new file mode 100644 index 0000000000..4510d08c70 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/dos/v1beta1/dos_client.go @@ -0,0 +1,73 @@ +// Code generated by client-gen. DO NOT EDIT. + +package v1beta1 + +import ( + v1beta1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/dos/v1beta1" + "github.com/nginxinc/kubernetes-ingress/pkg/client/clientset/versioned/scheme" + rest "k8s.io/client-go/rest" +) + +type AppprotectdosV1beta1Interface interface { + RESTClient() rest.Interface + DosProtectedResourcesGetter +} + +// AppprotectdosV1beta1Client is used to interact with features provided by the appprotectdos.f5.com group. +type AppprotectdosV1beta1Client struct { + restClient rest.Interface +} + +func (c *AppprotectdosV1beta1Client) DosProtectedResources(namespace string) DosProtectedResourceInterface { + return newDosProtectedResources(c, namespace) +} + +// NewForConfig creates a new AppprotectdosV1beta1Client for the given config. +func NewForConfig(c *rest.Config) (*AppprotectdosV1beta1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + client, err := rest.RESTClientFor(&config) + if err != nil { + return nil, err + } + return &AppprotectdosV1beta1Client{client}, nil +} + +// NewForConfigOrDie creates a new AppprotectdosV1beta1Client for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *AppprotectdosV1beta1Client { + client, err := NewForConfig(c) + if err != nil { + panic(err) + } + return client +} + +// New creates a new AppprotectdosV1beta1Client for the given RESTClient. +func New(c rest.Interface) *AppprotectdosV1beta1Client { + return &AppprotectdosV1beta1Client{c} +} + +func setConfigDefaults(config *rest.Config) error { + gv := v1beta1.SchemeGroupVersion + config.GroupVersion = &gv + config.APIPath = "/apis" + config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() + + if config.UserAgent == "" { + config.UserAgent = rest.DefaultKubernetesUserAgent() + } + + return nil +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *AppprotectdosV1beta1Client) RESTClient() rest.Interface { + if c == nil { + return nil + } + return c.restClient +} diff --git a/pkg/client/clientset/versioned/typed/dos/v1beta1/dosprotectedresource.go b/pkg/client/clientset/versioned/typed/dos/v1beta1/dosprotectedresource.go new file mode 100644 index 0000000000..15fdb8c655 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/dos/v1beta1/dosprotectedresource.go @@ -0,0 +1,162 @@ +// Code generated by client-gen. DO NOT EDIT. + +package v1beta1 + +import ( + "context" + "time" + + v1beta1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/dos/v1beta1" + scheme "github.com/nginxinc/kubernetes-ingress/pkg/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// DosProtectedResourcesGetter has a method to return a DosProtectedResourceInterface. +// A group's client should implement this interface. +type DosProtectedResourcesGetter interface { + DosProtectedResources(namespace string) DosProtectedResourceInterface +} + +// DosProtectedResourceInterface has methods to work with DosProtectedResource resources. +type DosProtectedResourceInterface interface { + Create(ctx context.Context, dosProtectedResource *v1beta1.DosProtectedResource, opts v1.CreateOptions) (*v1beta1.DosProtectedResource, error) + Update(ctx context.Context, dosProtectedResource *v1beta1.DosProtectedResource, opts v1.UpdateOptions) (*v1beta1.DosProtectedResource, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1beta1.DosProtectedResource, error) + List(ctx context.Context, opts v1.ListOptions) (*v1beta1.DosProtectedResourceList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.DosProtectedResource, err error) + DosProtectedResourceExpansion +} + +// dosProtectedResources implements DosProtectedResourceInterface +type dosProtectedResources struct { + client rest.Interface + ns string +} + +// newDosProtectedResources returns a DosProtectedResources +func newDosProtectedResources(c *AppprotectdosV1beta1Client, namespace string) *dosProtectedResources { + return &dosProtectedResources{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the dosProtectedResource, and returns the corresponding dosProtectedResource object, and an error if there is any. +func (c *dosProtectedResources) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta1.DosProtectedResource, err error) { + result = &v1beta1.DosProtectedResource{} + err = c.client.Get(). + Namespace(c.ns). + Resource("dosprotectedresources"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of DosProtectedResources that match those selectors. +func (c *dosProtectedResources) List(ctx context.Context, opts v1.ListOptions) (result *v1beta1.DosProtectedResourceList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1beta1.DosProtectedResourceList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("dosprotectedresources"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested dosProtectedResources. +func (c *dosProtectedResources) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("dosprotectedresources"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a dosProtectedResource and creates it. Returns the server's representation of the dosProtectedResource, and an error, if there is any. +func (c *dosProtectedResources) Create(ctx context.Context, dosProtectedResource *v1beta1.DosProtectedResource, opts v1.CreateOptions) (result *v1beta1.DosProtectedResource, err error) { + result = &v1beta1.DosProtectedResource{} + err = c.client.Post(). + Namespace(c.ns). + Resource("dosprotectedresources"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(dosProtectedResource). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a dosProtectedResource and updates it. Returns the server's representation of the dosProtectedResource, and an error, if there is any. +func (c *dosProtectedResources) Update(ctx context.Context, dosProtectedResource *v1beta1.DosProtectedResource, opts v1.UpdateOptions) (result *v1beta1.DosProtectedResource, err error) { + result = &v1beta1.DosProtectedResource{} + err = c.client.Put(). + Namespace(c.ns). + Resource("dosprotectedresources"). + Name(dosProtectedResource.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(dosProtectedResource). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the dosProtectedResource and deletes it. Returns an error if one occurs. +func (c *dosProtectedResources) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("dosprotectedresources"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *dosProtectedResources) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("dosprotectedresources"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched dosProtectedResource. +func (c *dosProtectedResources) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.DosProtectedResource, err error) { + result = &v1beta1.DosProtectedResource{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("dosprotectedresources"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/pkg/client/clientset/versioned/typed/dos/v1beta1/fake/doc.go b/pkg/client/clientset/versioned/typed/dos/v1beta1/fake/doc.go new file mode 100644 index 0000000000..2b5ba4c8e4 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/dos/v1beta1/fake/doc.go @@ -0,0 +1,4 @@ +// Code generated by client-gen. DO NOT EDIT. + +// Package fake has the automatically generated clients. +package fake diff --git a/pkg/client/clientset/versioned/typed/dos/v1beta1/fake/fake_dos_client.go b/pkg/client/clientset/versioned/typed/dos/v1beta1/fake/fake_dos_client.go new file mode 100644 index 0000000000..2a59aff231 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/dos/v1beta1/fake/fake_dos_client.go @@ -0,0 +1,24 @@ +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1beta1 "github.com/nginxinc/kubernetes-ingress/pkg/client/clientset/versioned/typed/dos/v1beta1" + rest "k8s.io/client-go/rest" + testing "k8s.io/client-go/testing" +) + +type FakeAppprotectdosV1beta1 struct { + *testing.Fake +} + +func (c *FakeAppprotectdosV1beta1) DosProtectedResources(namespace string) v1beta1.DosProtectedResourceInterface { + return &FakeDosProtectedResources{c, namespace} +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *FakeAppprotectdosV1beta1) RESTClient() rest.Interface { + var ret *rest.RESTClient + return ret +} diff --git a/pkg/client/clientset/versioned/typed/dos/v1beta1/fake/fake_dosprotectedresource.go b/pkg/client/clientset/versioned/typed/dos/v1beta1/fake/fake_dosprotectedresource.go new file mode 100644 index 0000000000..aac75f7429 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/dos/v1beta1/fake/fake_dosprotectedresource.go @@ -0,0 +1,114 @@ +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1beta1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/dos/v1beta1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeDosProtectedResources implements DosProtectedResourceInterface +type FakeDosProtectedResources struct { + Fake *FakeAppprotectdosV1beta1 + ns string +} + +var dosprotectedresourcesResource = schema.GroupVersionResource{Group: "appprotectdos.f5.com", Version: "v1beta1", Resource: "dosprotectedresources"} + +var dosprotectedresourcesKind = schema.GroupVersionKind{Group: "appprotectdos.f5.com", Version: "v1beta1", Kind: "DosProtectedResource"} + +// Get takes name of the dosProtectedResource, and returns the corresponding dosProtectedResource object, and an error if there is any. +func (c *FakeDosProtectedResources) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta1.DosProtectedResource, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(dosprotectedresourcesResource, c.ns, name), &v1beta1.DosProtectedResource{}) + + if obj == nil { + return nil, err + } + return obj.(*v1beta1.DosProtectedResource), err +} + +// List takes label and field selectors, and returns the list of DosProtectedResources that match those selectors. +func (c *FakeDosProtectedResources) List(ctx context.Context, opts v1.ListOptions) (result *v1beta1.DosProtectedResourceList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(dosprotectedresourcesResource, dosprotectedresourcesKind, c.ns, opts), &v1beta1.DosProtectedResourceList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1beta1.DosProtectedResourceList{ListMeta: obj.(*v1beta1.DosProtectedResourceList).ListMeta} + for _, item := range obj.(*v1beta1.DosProtectedResourceList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested dosProtectedResources. +func (c *FakeDosProtectedResources) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(dosprotectedresourcesResource, c.ns, opts)) + +} + +// Create takes the representation of a dosProtectedResource and creates it. Returns the server's representation of the dosProtectedResource, and an error, if there is any. +func (c *FakeDosProtectedResources) Create(ctx context.Context, dosProtectedResource *v1beta1.DosProtectedResource, opts v1.CreateOptions) (result *v1beta1.DosProtectedResource, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(dosprotectedresourcesResource, c.ns, dosProtectedResource), &v1beta1.DosProtectedResource{}) + + if obj == nil { + return nil, err + } + return obj.(*v1beta1.DosProtectedResource), err +} + +// Update takes the representation of a dosProtectedResource and updates it. Returns the server's representation of the dosProtectedResource, and an error, if there is any. +func (c *FakeDosProtectedResources) Update(ctx context.Context, dosProtectedResource *v1beta1.DosProtectedResource, opts v1.UpdateOptions) (result *v1beta1.DosProtectedResource, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(dosprotectedresourcesResource, c.ns, dosProtectedResource), &v1beta1.DosProtectedResource{}) + + if obj == nil { + return nil, err + } + return obj.(*v1beta1.DosProtectedResource), err +} + +// Delete takes name of the dosProtectedResource and deletes it. Returns an error if one occurs. +func (c *FakeDosProtectedResources) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(dosprotectedresourcesResource, c.ns, name), &v1beta1.DosProtectedResource{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeDosProtectedResources) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(dosprotectedresourcesResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1beta1.DosProtectedResourceList{}) + return err +} + +// Patch applies the patch and returns the patched dosProtectedResource. +func (c *FakeDosProtectedResources) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.DosProtectedResource, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(dosprotectedresourcesResource, c.ns, name, pt, data, subresources...), &v1beta1.DosProtectedResource{}) + + if obj == nil { + return nil, err + } + return obj.(*v1beta1.DosProtectedResource), err +} diff --git a/pkg/client/clientset/versioned/typed/dos/v1beta1/generated_expansion.go b/pkg/client/clientset/versioned/typed/dos/v1beta1/generated_expansion.go new file mode 100644 index 0000000000..a10ccfe6f1 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/dos/v1beta1/generated_expansion.go @@ -0,0 +1,5 @@ +// Code generated by client-gen. DO NOT EDIT. + +package v1beta1 + +type DosProtectedResourceExpansion interface{} diff --git a/pkg/client/informers/externalversions/configuration/v1alpha1/interface.go b/pkg/client/informers/externalversions/configuration/v1alpha1/interface.go index 1907744bb1..b65ce74c68 100644 --- a/pkg/client/informers/externalversions/configuration/v1alpha1/interface.go +++ b/pkg/client/informers/externalversions/configuration/v1alpha1/interface.go @@ -10,6 +10,8 @@ import ( type Interface interface { // GlobalConfigurations returns a GlobalConfigurationInformer. GlobalConfigurations() GlobalConfigurationInformer + // Policies returns a PolicyInformer. + Policies() PolicyInformer // TransportServers returns a TransportServerInformer. TransportServers() TransportServerInformer } @@ -30,6 +32,11 @@ func (v *version) GlobalConfigurations() GlobalConfigurationInformer { return &globalConfigurationInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } +// Policies returns a PolicyInformer. +func (v *version) Policies() PolicyInformer { + return &policyInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // TransportServers returns a TransportServerInformer. func (v *version) TransportServers() TransportServerInformer { return &transportServerInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/pkg/client/informers/externalversions/configuration/v1alpha1/policy.go b/pkg/client/informers/externalversions/configuration/v1alpha1/policy.go new file mode 100644 index 0000000000..8cb91c2189 --- /dev/null +++ b/pkg/client/informers/externalversions/configuration/v1alpha1/policy.go @@ -0,0 +1,74 @@ +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + configurationv1alpha1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/v1alpha1" + versioned "github.com/nginxinc/kubernetes-ingress/pkg/client/clientset/versioned" + internalinterfaces "github.com/nginxinc/kubernetes-ingress/pkg/client/informers/externalversions/internalinterfaces" + v1alpha1 "github.com/nginxinc/kubernetes-ingress/pkg/client/listers/configuration/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// PolicyInformer provides access to a shared informer and lister for +// Policies. +type PolicyInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.PolicyLister +} + +type policyInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewPolicyInformer constructs a new informer for Policy type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewPolicyInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredPolicyInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredPolicyInformer constructs a new informer for Policy type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredPolicyInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.K8sV1alpha1().Policies(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.K8sV1alpha1().Policies(namespace).Watch(context.TODO(), options) + }, + }, + &configurationv1alpha1.Policy{}, + resyncPeriod, + indexers, + ) +} + +func (f *policyInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredPolicyInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *policyInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&configurationv1alpha1.Policy{}, f.defaultInformer) +} + +func (f *policyInformer) Lister() v1alpha1.PolicyLister { + return v1alpha1.NewPolicyLister(f.Informer().GetIndexer()) +} diff --git a/pkg/client/informers/externalversions/dos/interface.go b/pkg/client/informers/externalversions/dos/interface.go new file mode 100644 index 0000000000..ac812cda74 --- /dev/null +++ b/pkg/client/informers/externalversions/dos/interface.go @@ -0,0 +1,30 @@ +// Code generated by informer-gen. DO NOT EDIT. + +package dos + +import ( + v1beta1 "github.com/nginxinc/kubernetes-ingress/pkg/client/informers/externalversions/dos/v1beta1" + internalinterfaces "github.com/nginxinc/kubernetes-ingress/pkg/client/informers/externalversions/internalinterfaces" +) + +// Interface provides access to each of this group's versions. +type Interface interface { + // V1beta1 provides access to shared informers for resources in V1beta1. + V1beta1() v1beta1.Interface +} + +type group struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// V1beta1 returns a new v1beta1.Interface. +func (g *group) V1beta1() v1beta1.Interface { + return v1beta1.New(g.factory, g.namespace, g.tweakListOptions) +} diff --git a/pkg/client/informers/externalversions/dos/v1beta1/dosprotectedresource.go b/pkg/client/informers/externalversions/dos/v1beta1/dosprotectedresource.go new file mode 100644 index 0000000000..e6a03fb143 --- /dev/null +++ b/pkg/client/informers/externalversions/dos/v1beta1/dosprotectedresource.go @@ -0,0 +1,74 @@ +// Code generated by informer-gen. DO NOT EDIT. + +package v1beta1 + +import ( + "context" + time "time" + + dosv1beta1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/dos/v1beta1" + versioned "github.com/nginxinc/kubernetes-ingress/pkg/client/clientset/versioned" + internalinterfaces "github.com/nginxinc/kubernetes-ingress/pkg/client/informers/externalversions/internalinterfaces" + v1beta1 "github.com/nginxinc/kubernetes-ingress/pkg/client/listers/dos/v1beta1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// DosProtectedResourceInformer provides access to a shared informer and lister for +// DosProtectedResources. +type DosProtectedResourceInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1beta1.DosProtectedResourceLister +} + +type dosProtectedResourceInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewDosProtectedResourceInformer constructs a new informer for DosProtectedResource type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewDosProtectedResourceInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredDosProtectedResourceInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredDosProtectedResourceInformer constructs a new informer for DosProtectedResource type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredDosProtectedResourceInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.AppprotectdosV1beta1().DosProtectedResources(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.AppprotectdosV1beta1().DosProtectedResources(namespace).Watch(context.TODO(), options) + }, + }, + &dosv1beta1.DosProtectedResource{}, + resyncPeriod, + indexers, + ) +} + +func (f *dosProtectedResourceInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredDosProtectedResourceInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *dosProtectedResourceInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&dosv1beta1.DosProtectedResource{}, f.defaultInformer) +} + +func (f *dosProtectedResourceInformer) Lister() v1beta1.DosProtectedResourceLister { + return v1beta1.NewDosProtectedResourceLister(f.Informer().GetIndexer()) +} diff --git a/pkg/client/informers/externalversions/dos/v1beta1/interface.go b/pkg/client/informers/externalversions/dos/v1beta1/interface.go new file mode 100644 index 0000000000..774aefee0d --- /dev/null +++ b/pkg/client/informers/externalversions/dos/v1beta1/interface.go @@ -0,0 +1,29 @@ +// Code generated by informer-gen. DO NOT EDIT. + +package v1beta1 + +import ( + internalinterfaces "github.com/nginxinc/kubernetes-ingress/pkg/client/informers/externalversions/internalinterfaces" +) + +// Interface provides access to all the informers in this group version. +type Interface interface { + // DosProtectedResources returns a DosProtectedResourceInformer. + DosProtectedResources() DosProtectedResourceInformer +} + +type version struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// DosProtectedResources returns a DosProtectedResourceInformer. +func (v *version) DosProtectedResources() DosProtectedResourceInformer { + return &dosProtectedResourceInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} diff --git a/pkg/client/informers/externalversions/factory.go b/pkg/client/informers/externalversions/factory.go index 88f33d6e47..ddff0833af 100644 --- a/pkg/client/informers/externalversions/factory.go +++ b/pkg/client/informers/externalversions/factory.go @@ -9,6 +9,7 @@ import ( versioned "github.com/nginxinc/kubernetes-ingress/pkg/client/clientset/versioned" configuration "github.com/nginxinc/kubernetes-ingress/pkg/client/informers/externalversions/configuration" + dos "github.com/nginxinc/kubernetes-ingress/pkg/client/informers/externalversions/dos" internalinterfaces "github.com/nginxinc/kubernetes-ingress/pkg/client/informers/externalversions/internalinterfaces" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" @@ -157,8 +158,13 @@ type SharedInformerFactory interface { WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool K8s() configuration.Interface + Appprotectdos() dos.Interface } func (f *sharedInformerFactory) K8s() configuration.Interface { return configuration.New(f, f.namespace, f.tweakListOptions) } + +func (f *sharedInformerFactory) Appprotectdos() dos.Interface { + return dos.New(f, f.namespace, f.tweakListOptions) +} diff --git a/pkg/client/informers/externalversions/generic.go b/pkg/client/informers/externalversions/generic.go index 382de82263..d466da36d6 100644 --- a/pkg/client/informers/externalversions/generic.go +++ b/pkg/client/informers/externalversions/generic.go @@ -7,6 +7,7 @@ import ( v1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/v1" v1alpha1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/v1alpha1" + v1beta1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/dos/v1beta1" schema "k8s.io/apimachinery/pkg/runtime/schema" cache "k8s.io/client-go/tools/cache" ) @@ -37,7 +38,11 @@ func (f *genericInformer) Lister() cache.GenericLister { // TODO extend this to unknown resources with a client pool func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { switch resource { - // Group=k8s.nginx.org, Version=v1 + // Group=appprotectdos.f5.com, Version=v1beta1 + case v1beta1.SchemeGroupVersion.WithResource("dosprotectedresources"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Appprotectdos().V1beta1().DosProtectedResources().Informer()}, nil + + // Group=k8s.nginx.org, Version=v1 case v1.SchemeGroupVersion.WithResource("policies"): return &genericInformer{resource: resource.GroupResource(), informer: f.K8s().V1().Policies().Informer()}, nil case v1.SchemeGroupVersion.WithResource("virtualservers"): @@ -48,6 +53,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource // Group=k8s.nginx.org, Version=v1alpha1 case v1alpha1.SchemeGroupVersion.WithResource("globalconfigurations"): return &genericInformer{resource: resource.GroupResource(), informer: f.K8s().V1alpha1().GlobalConfigurations().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("policies"): + return &genericInformer{resource: resource.GroupResource(), informer: f.K8s().V1alpha1().Policies().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("transportservers"): return &genericInformer{resource: resource.GroupResource(), informer: f.K8s().V1alpha1().TransportServers().Informer()}, nil diff --git a/pkg/client/listers/configuration/v1alpha1/expansion_generated.go b/pkg/client/listers/configuration/v1alpha1/expansion_generated.go index 82d94dca55..e92ce0c811 100644 --- a/pkg/client/listers/configuration/v1alpha1/expansion_generated.go +++ b/pkg/client/listers/configuration/v1alpha1/expansion_generated.go @@ -10,6 +10,14 @@ type GlobalConfigurationListerExpansion interface{} // GlobalConfigurationNamespaceLister. type GlobalConfigurationNamespaceListerExpansion interface{} +// PolicyListerExpansion allows custom methods to be added to +// PolicyLister. +type PolicyListerExpansion interface{} + +// PolicyNamespaceListerExpansion allows custom methods to be added to +// PolicyNamespaceLister. +type PolicyNamespaceListerExpansion interface{} + // TransportServerListerExpansion allows custom methods to be added to // TransportServerLister. type TransportServerListerExpansion interface{} diff --git a/pkg/client/listers/configuration/v1alpha1/policy.go b/pkg/client/listers/configuration/v1alpha1/policy.go new file mode 100644 index 0000000000..8034a5d068 --- /dev/null +++ b/pkg/client/listers/configuration/v1alpha1/policy.go @@ -0,0 +1,83 @@ +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// PolicyLister helps list Policies. +// All objects returned here must be treated as read-only. +type PolicyLister interface { + // List lists all Policies in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.Policy, err error) + // Policies returns an object that can list and get Policies. + Policies(namespace string) PolicyNamespaceLister + PolicyListerExpansion +} + +// policyLister implements the PolicyLister interface. +type policyLister struct { + indexer cache.Indexer +} + +// NewPolicyLister returns a new PolicyLister. +func NewPolicyLister(indexer cache.Indexer) PolicyLister { + return &policyLister{indexer: indexer} +} + +// List lists all Policies in the indexer. +func (s *policyLister) List(selector labels.Selector) (ret []*v1alpha1.Policy, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.Policy)) + }) + return ret, err +} + +// Policies returns an object that can list and get Policies. +func (s *policyLister) Policies(namespace string) PolicyNamespaceLister { + return policyNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// PolicyNamespaceLister helps list and get Policies. +// All objects returned here must be treated as read-only. +type PolicyNamespaceLister interface { + // List lists all Policies in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.Policy, err error) + // Get retrieves the Policy from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.Policy, error) + PolicyNamespaceListerExpansion +} + +// policyNamespaceLister implements the PolicyNamespaceLister +// interface. +type policyNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all Policies in the indexer for a given namespace. +func (s policyNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.Policy, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.Policy)) + }) + return ret, err +} + +// Get retrieves the Policy from the indexer for a given namespace and name. +func (s policyNamespaceLister) Get(name string) (*v1alpha1.Policy, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("policy"), name) + } + return obj.(*v1alpha1.Policy), nil +} diff --git a/pkg/client/listers/dos/v1beta1/dosprotectedresource.go b/pkg/client/listers/dos/v1beta1/dosprotectedresource.go new file mode 100644 index 0000000000..f587f5ba26 --- /dev/null +++ b/pkg/client/listers/dos/v1beta1/dosprotectedresource.go @@ -0,0 +1,83 @@ +// Code generated by lister-gen. DO NOT EDIT. + +package v1beta1 + +import ( + v1beta1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/dos/v1beta1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// DosProtectedResourceLister helps list DosProtectedResources. +// All objects returned here must be treated as read-only. +type DosProtectedResourceLister interface { + // List lists all DosProtectedResources in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1beta1.DosProtectedResource, err error) + // DosProtectedResources returns an object that can list and get DosProtectedResources. + DosProtectedResources(namespace string) DosProtectedResourceNamespaceLister + DosProtectedResourceListerExpansion +} + +// dosProtectedResourceLister implements the DosProtectedResourceLister interface. +type dosProtectedResourceLister struct { + indexer cache.Indexer +} + +// NewDosProtectedResourceLister returns a new DosProtectedResourceLister. +func NewDosProtectedResourceLister(indexer cache.Indexer) DosProtectedResourceLister { + return &dosProtectedResourceLister{indexer: indexer} +} + +// List lists all DosProtectedResources in the indexer. +func (s *dosProtectedResourceLister) List(selector labels.Selector) (ret []*v1beta1.DosProtectedResource, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1beta1.DosProtectedResource)) + }) + return ret, err +} + +// DosProtectedResources returns an object that can list and get DosProtectedResources. +func (s *dosProtectedResourceLister) DosProtectedResources(namespace string) DosProtectedResourceNamespaceLister { + return dosProtectedResourceNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// DosProtectedResourceNamespaceLister helps list and get DosProtectedResources. +// All objects returned here must be treated as read-only. +type DosProtectedResourceNamespaceLister interface { + // List lists all DosProtectedResources in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1beta1.DosProtectedResource, err error) + // Get retrieves the DosProtectedResource from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1beta1.DosProtectedResource, error) + DosProtectedResourceNamespaceListerExpansion +} + +// dosProtectedResourceNamespaceLister implements the DosProtectedResourceNamespaceLister +// interface. +type dosProtectedResourceNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all DosProtectedResources in the indexer for a given namespace. +func (s dosProtectedResourceNamespaceLister) List(selector labels.Selector) (ret []*v1beta1.DosProtectedResource, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1beta1.DosProtectedResource)) + }) + return ret, err +} + +// Get retrieves the DosProtectedResource from the indexer for a given namespace and name. +func (s dosProtectedResourceNamespaceLister) Get(name string) (*v1beta1.DosProtectedResource, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1beta1.Resource("dosprotectedresource"), name) + } + return obj.(*v1beta1.DosProtectedResource), nil +} diff --git a/pkg/client/listers/dos/v1beta1/expansion_generated.go b/pkg/client/listers/dos/v1beta1/expansion_generated.go new file mode 100644 index 0000000000..28b5b12570 --- /dev/null +++ b/pkg/client/listers/dos/v1beta1/expansion_generated.go @@ -0,0 +1,11 @@ +// Code generated by lister-gen. DO NOT EDIT. + +package v1beta1 + +// DosProtectedResourceListerExpansion allows custom methods to be added to +// DosProtectedResourceLister. +type DosProtectedResourceListerExpansion interface{} + +// DosProtectedResourceNamespaceListerExpansion allows custom methods to be added to +// DosProtectedResourceNamespaceLister. +type DosProtectedResourceNamespaceListerExpansion interface{} diff --git a/tests/conftest.py b/tests/conftest.py index 9dbf36aa6c..5a958e313d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -117,7 +117,12 @@ def pytest_collection_modifyitems(config, items) -> None: for item in items: if "appprotect" in item.keywords: item.add_marker(appprotect) - if str(config.getoption("--batch-start")) != "True": + if "-dos" not in config.getoption("--image"): + dos = pytest.mark.skip(reason="Skip DOS test in non-DOS image") + for item in items: + if "dos" in item.keywords: + item.add_marker(dos) + if str(config.getoption("--batch-start")) != "True": batch_start = pytest.mark.skip(reason="Skipping pod restart test with multiple resources") for item in items: if "batch_start" in item.keywords: diff --git a/tests/data/common/app/dos/app.yaml b/tests/data/common/app/dos/app.yaml new file mode 100644 index 0000000000..f6b469c986 --- /dev/null +++ b/tests/data/common/app/dos/app.yaml @@ -0,0 +1,36 @@ +apiVersion: v1 +kind: Service +metadata: + name: dos-svc +spec: + selector: + app: dos-server + ports: + - protocol: "TCP" + port: 80 + targetPort: 8080 + type: NodePort +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dos-server +spec: + selector: + matchLabels: + app: dos-server + replicas: 1 + template: + metadata: + labels: + app: dos-server + spec: + containers: + - name: dos-server + image: nginxkic/test-dos-server:2.0.0 + imagePullPolicy: IfNotPresent + command: ['sh', '-c', 'python3 /root/webserver.py; tail -f "/dev/null"'] + lifecycle: + preStop: + exec: + command: ['sh', '-c', 'python3 /root/webserver.py;'] diff --git a/tests/data/dos/bad_clients_xff.sh b/tests/data/dos/bad_clients_xff.sh new file mode 100755 index 0000000000..c3fc83848f --- /dev/null +++ b/tests/data/dos/bad_clients_xff.sh @@ -0,0 +1,29 @@ +#!/bin/sh +HOST=$1 +IP_AND_PORT=$2 +URI='bad_path.html' +NUM=600 +CONNS=300 +while true; do + echo "ab -l -n ${NUM} -c ${CONNS} -d -s 5 \ + -H "Host: ${HOST}" \ + -H "X-Forwarded-For: 1.1.1.1" \ + ${IP_AND_PORT}${URI}" + + ab -l -n ${NUM} -c ${CONNS} -d -s 5 \ + -H "Host: ${HOST}" \ + -H "X-Forwarded-For: 1.1.1.1" \ + ${IP_AND_PORT}${URI} & + + ab -l -n ${NUM} -c ${CONNS} -d -s 5 \ + -H "Host: ${HOST}" \ + -H "X-Forwarded-For: 1.1.1.2" \ + ${IP_AND_PORT}${URI} & + + ab -l -n ${NUM} -c ${CONNS} -d -s 3 \ + -H "Host: ${HOST}" \ + -H "X-Forwarded-For: 1.1.1.3" \ + ${IP_AND_PORT}${URI} + + killall ab +done \ No newline at end of file diff --git a/tests/data/dos/dos-ingress.yaml b/tests/data/dos/dos-ingress.yaml new file mode 100644 index 0000000000..253a807a0f --- /dev/null +++ b/tests/data/dos/dos-ingress.yaml @@ -0,0 +1,18 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: dos-ingress + annotations: + kubernetes.io/ingress.class: "nginx" +spec: + rules: + - host: dos.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: dos-svc + port: + number: 80 diff --git a/tests/data/dos/dos-logconf.yaml b/tests/data/dos/dos-logconf.yaml new file mode 100644 index 0000000000..885a524647 --- /dev/null +++ b/tests/data/dos/dos-logconf.yaml @@ -0,0 +1,12 @@ +apiVersion: appprotectdos.f5.com/v1beta1 +kind: APDosLogConf +metadata: + name: doslogconf +spec: + content: + format: splunk + max_message_size: 64k + filter: + traffic-mitigation-stats: all + bad-actors: top 10 + attack-signatures: top 10 diff --git a/tests/data/dos/dos-policy.yaml b/tests/data/dos/dos-policy.yaml new file mode 100644 index 0000000000..add82acd69 --- /dev/null +++ b/tests/data/dos/dos-policy.yaml @@ -0,0 +1,10 @@ +apiVersion: appprotectdos.f5.com/v1beta1 +kind: APDosPolicy +metadata: + name: dospolicy +spec: + mitigation_mode: "standard" + signatures: "on" + bad_actors: "on" + automation_tools_detection: "on" + tls_fingerprint: "on" diff --git a/tests/data/dos/dos-protected.yaml b/tests/data/dos/dos-protected.yaml new file mode 100644 index 0000000000..9f2796dc08 --- /dev/null +++ b/tests/data/dos/dos-protected.yaml @@ -0,0 +1,17 @@ +apiVersion: appprotectdos.f5.com/v1beta1 +kind: DosProtectedResource +metadata: + name: dos-protected +spec: + enable: true + name: "name" + apDosPolicy: "/dospolicy" + apDosMonitor: + uri: "dos.example.com" + protocol: "http1" + timeout: 5 + dosAccessLogDest: "127.0.0.1:5561" + dosSecurityLog: + enable: true + apDosLogConf: "/doslogconf" + dosLogDest: "syslog-svc..svc.cluster.local:514" diff --git a/tests/data/dos/dos-secret.yaml b/tests/data/dos/dos-secret.yaml new file mode 100644 index 0000000000..6147274ba7 --- /dev/null +++ b/tests/data/dos/dos-secret.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + name: dos-secret +type: kubernetes.io/tls +data: + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURMakNDQWhZQ0NRREFPRjl0THNhWFdqQU5CZ2txaGtpRzl3MEJBUXNGQURCYU1Rc3dDUVlEVlFRR0V3SlYKVXpFTE1Ba0dBMVVFQ0F3Q1EwRXhJVEFmQmdOVkJBb01HRWx1ZEdWeWJtVjBJRmRwWkdkcGRITWdVSFI1SUV4MApaREViTUJrR0ExVUVBd3dTWTJGbVpTNWxlR0Z0Y0d4bExtTnZiU0FnTUI0WERURTRNRGt4TWpFMk1UVXpOVm9YCkRUSXpNRGt4TVRFMk1UVXpOVm93V0RFTE1Ba0dBMVVFQmhNQ1ZWTXhDekFKQmdOVkJBZ01Ba05CTVNFd0h3WUQKVlFRS0RCaEpiblJsY201bGRDQlhhV1JuYVhSeklGQjBlU0JNZEdReEdUQVhCZ05WQkFNTUVHTmhabVV1WlhoaApiWEJzWlM1amIyMHdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFDcDZLbjdzeTgxCnAwanVKL2N5ayt2Q0FtbHNmanRGTTJtdVpOSzBLdGVjcUcyZmpXUWI1NXhRMVlGQTJYT1N3SEFZdlNkd0kyaloKcnVXOHFYWENMMnJiNENaQ0Z4d3BWRUNyY3hkam0zdGVWaVJYVnNZSW1tSkhQUFN5UWdwaW9iczl4N0RsTGM2SQpCQTBaalVPeWwwUHFHOVNKZXhNVjczV0lJYTVyRFZTRjJyNGtTa2JBajREY2o3TFhlRmxWWEgySTVYd1hDcHRDCm42N0pDZzQyZitrOHdnemNSVnA4WFprWldaVmp3cTlSVUtEWG1GQjJZeU4xWEVXZFowZXdSdUtZVUpsc202OTIKc2tPcktRajB2a29QbjQxRUUvK1RhVkVwcUxUUm9VWTNyemc3RGtkemZkQml6Rk8yZHNQTkZ4MkNXMGpYa05MdgpLbzI1Q1pyT2hYQUhBZ01CQUFFd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFLSEZDY3lPalp2b0hzd1VCTWRMClJkSEliMzgzcFdGeW5acS9MdVVvdnNWQTU4QjBDZzdCRWZ5NXZXVlZycTVSSWt2NGxaODFOMjl4MjFkMUpINnIKalNuUXgrRFhDTy9USkVWNWxTQ1VwSUd6RVVZYVVQZ1J5anNNL05VZENKOHVIVmhaSitTNkZBK0NuT0Q5cm4yaQpaQmVQQ0k1ckh3RVh3bm5sOHl3aWozdnZRNXpISXV5QmdsV3IvUXl1aTlmalBwd1dVdlVtNG52NVNNRzl6Q1Y3ClBwdXd2dWF0cWpPMTIwOEJqZkUvY1pISWc4SHc5bXZXOXg5QytJUU1JTURFN2IvZzZPY0s3TEdUTHdsRnh2QTgKN1dqRWVxdW5heUlwaE1oS1JYVmYxTjM0OWVOOThFejM4Zk9USFRQYmRKakZBL1BjQytHeW1lK2lHdDVPUWRGaAp5UkU9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + tls.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBcWVpcCs3TXZOYWRJN2lmM01wUHJ3Z0pwYkg0N1JUTnBybVRTdENyWG5LaHRuNDFrCkcrZWNVTldCUU5semtzQndHTDBuY0NObzJhN2x2S2wxd2k5cTIrQW1RaGNjS1ZSQXEzTVhZNXQ3WGxZa1YxYkcKQ0pwaVJ6ejBza0lLWXFHN1BjZXc1UzNPaUFRTkdZMURzcGRENmh2VWlYc1RGZTkxaUNHdWF3MVVoZHErSkVwRwp3SStBM0kreTEzaFpWVng5aU9WOEZ3cWJRcCt1eVFvT05uL3BQTUlNM0VWYWZGMlpHVm1WWThLdlVWQ2cxNWhRCmRtTWpkVnhGbldkSHNFYmltRkNaYkp1dmRySkRxeWtJOUw1S0Q1K05SQlAvazJsUkthaTAwYUZHTjY4NE93NUgKYzMzUVlzeFR0bmJEelJjZGdsdEkxNURTN3lxTnVRbWF6b1Z3QndJREFRQUJBb0lCQVFDUFNkU1luUXRTUHlxbApGZlZGcFRPc29PWVJoZjhzSStpYkZ4SU91UmF1V2VoaEp4ZG01Uk9ScEF6bUNMeUw1VmhqdEptZTIyM2dMcncyCk45OUVqVUtiL1ZPbVp1RHNCYzZvQ0Y2UU5SNThkejhjbk9SVGV3Y290c0pSMXBuMWhobG5SNUhxSkpCSmFzazEKWkVuVVFmY1hackw5NGxvOUpIM0UrVXFqbzFGRnM4eHhFOHdvUEJxalpzVjdwUlVaZ0MzTGh4bndMU0V4eUZvNApjeGI5U09HNU9tQUpvelN0Rm9RMkdKT2VzOHJKNXFmZHZ5dGdnOXhiTGFRTC94MGtwUTYyQm9GTUJEZHFPZVBXCktmUDV6WjYvMDcvdnBqNDh5QTFRMzJQem9idWJzQkxkM0tjbjMyamZtMUU3cHJ0V2wrSmVPRmlPem5CUUZKYk4KNHFQVlJ6NWhBb0dCQU50V3l4aE5DU0x1NFArWGdLeWNrbGpKNkY1NjY4Zk5qNUN6Z0ZScUowOXpuMFRsc05ybwpGVExaY3hEcW5SM0hQWU00MkpFUmgySi9xREZaeW5SUW8zY2czb2VpdlVkQlZHWTgrRkkxVzBxZHViL0w5K3l1CmVkT1pUUTVYbUdHcDZyNmpleHltY0ppbS9Pc0IzWm5ZT3BPcmxEN1NQbUJ2ek5MazRNRjZneGJYQW9HQkFNWk8KMHA2SGJCbWNQMHRqRlhmY0tFNzdJbUxtMHNBRzR1SG9VeDBlUGovMnFyblRuT0JCTkU0TXZnRHVUSnp5K2NhVQprOFJxbWRIQ2JIelRlNmZ6WXEvOWl0OHNaNzdLVk4xcWtiSWN1YytSVHhBOW5OaDFUanNSbmU3NFowajFGQ0xrCmhIY3FIMHJpN1BZU0tIVEU4RnZGQ3haWWRidUI4NENtWmlodnhicFJBb0dBSWJqcWFNWVBUWXVrbENkYTVTNzkKWVNGSjFKelplMUtqYS8vdER3MXpGY2dWQ0thMzFqQXdjaXowZi9sU1JxM0hTMUdHR21lemhQVlRpcUxmZVpxYwpSMGlLYmhnYk9jVlZrSkozSzB5QXlLd1BUdW14S0haNnpJbVpTMGMwYW0rUlk5WUdxNVQ3WXJ6cHpjZnZwaU9VCmZmZTNSeUZUN2NmQ21mb09oREN0enVrQ2dZQjMwb0xDMVJMRk9ycW40M3ZDUzUxemM1em9ZNDR1QnpzcHd3WU4KVHd2UC9FeFdNZjNWSnJEakJDSCtULzZzeXNlUGJKRUltbHpNK0l3eXRGcEFOZmlJWEV0LzQ4WGY2ME54OGdXTQp1SHl4Wlp4L05LdER3MFY4dlgxUE9ucTJBNWVpS2ErOGpSQVJZS0pMWU5kZkR1d29seHZHNmJaaGtQaS80RXRUCjNZMThzUUtCZ0h0S2JrKzdsTkpWZXN3WEU1Y1VHNkVEVXNEZS8yVWE3ZlhwN0ZjanFCRW9hcDFMU3crNlRYcDAKWmdybUtFOEFSek00NytFSkhVdmlpcS9udXBFMTVnMGtKVzNzeWhwVTl6WkxPN2x0QjBLSWtPOVpSY21Vam84UQpjcExsSE1BcWJMSjhXWUdKQ2toaVd4eWFsNmhZVHlXWTRjVmtDMHh0VGwvaFVFOUllTktvCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== diff --git a/tests/data/dos/dos-syslog.yaml b/tests/data/dos/dos-syslog.yaml new file mode 100644 index 0000000000..de25eeb3d0 --- /dev/null +++ b/tests/data/dos/dos-syslog.yaml @@ -0,0 +1,32 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: syslog +spec: + replicas: 1 + selector: + matchLabels: + app: syslog + template: + metadata: + labels: + app: syslog + spec: + containers: + - name: syslog + image: balabit/syslog-ng:3.28.1 + ports: + - containerPort: 514 + - containerPort: 601 +--- +apiVersion: v1 +kind: Service +metadata: + name: syslog-svc +spec: + ports: + - port: 514 + targetPort: 514 + protocol: TCP + selector: + app: syslog \ No newline at end of file diff --git a/tests/data/dos/good_clients_xff.sh b/tests/data/dos/good_clients_xff.sh new file mode 100755 index 0000000000..f3a9b84aae --- /dev/null +++ b/tests/data/dos/good_clients_xff.sh @@ -0,0 +1,28 @@ +#!/bin/bash +HOST=$1 +IP_AND_PORT=$2 +URI='good_path.html' + + +declare -a array=("/faq.php?sid=e6106109db52fec6127b73b152224dea#f4r0" + "/styles/prosilver/theme/print.css" + "/styles/prosilver/theme/images/bg_header.gif" + "/styles/prosilver/template/styleswitcher.js") + + +while true; do + URI=${array[$(( RANDOM % 3 ))]} + echo ${HOST} + echo ${IP_AND_PORT} + echo 'curl -b cookiefile -c cookiefile -L -s -o /dev/null -w "%{http_code}\n" -m 10 --connect-timeout 5 -A "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5" -H "Host: ${HOST}" -H "X-Forwarded-For: 3.3.3.2" ${IP_AND_PORT}${URI}' + curl -b cookiefile -c cookiefile -L -s -o /dev/null -w "%{http_code}\n" -m 10 --connect-timeout 5 -A "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5" -H "Host: ${HOST}" -H "X-Forwarded-For: 3.3.3.2" ${IP_AND_PORT}${URI} & + URI=${array[$(( RANDOM % 3 ))]} + curl -b cookiefile -c cookiefile -L -s -o /dev/null -w "%{http_code}\n" -m 10 --connect-timeout 5 -A "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.112 Safari/534.30" -H "Host: ${HOST}" -H "X-Forwarded-For: 31.212.17.19" ${IP_AND_PORT}${URI} & + URI=${array[$(( RANDOM % 3 ))]} + curl -b cookiefile -c cookiefile -L -s -o /dev/null -w "%{http_code}\n" -m 10 --connect-timeout 5 -A "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3" -H "Host: ${HOST}" -H "X-Forwarded-For: 46.117.23.157" ${IP_AND_PORT}${URI} & + + curl -b cookiefile -c cookiefile -L -s -o /dev/null -w "%{http_code}\n" -m 10 --connect-timeout 5 -A "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3" -H "Host: ${HOST}" -H "X-Forwarded-For: 111.212.17.19" ${IP_AND_PORT} + + killall curl + sleep 0.2 +done \ No newline at end of file diff --git a/tests/data/dos/nginx-config.yaml b/tests/data/dos/nginx-config.yaml new file mode 100644 index 0000000000..9239280edd --- /dev/null +++ b/tests/data/dos/nginx-config.yaml @@ -0,0 +1,12 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + name: nginx-config + namespace: nginx-ingress +data: + real-ip-header: "X-Forwarded-For" + real-ip-recursive: "True" + set-real-ip-from: "0.0.0.0/0" + worker-connections: "30000" + worker-rlimit-nofile: "65535" + worker-rlimit-core: "500M" \ No newline at end of file diff --git a/tests/data/virtual-server-dos/README.md b/tests/data/virtual-server-dos/README.md new file mode 100644 index 0000000000..19c05dee6f --- /dev/null +++ b/tests/data/virtual-server-dos/README.md @@ -0,0 +1,76 @@ +# DOS + +In this example we deploy the NGINX Plus Ingress controller with [NGINX App Protect Dos](https://www.nginx.com/products/nginx-app-protect-dos/), a simple web application and then configure load balancing and DOS protection for that application using the VirtualServer resource. + +## Prerequisites + +1. Follow the installation [instructions](../../docs/installation.md) to deploy the Ingress controller with NGINX App Protect Dos. +1. Save the public IP address of the Ingress Controller into a shell variable: + ``` + $ IC_IP=XXX.YYY.ZZZ.III + ``` +1. Save the HTTP port of the Ingress Controller into a shell variable: + ``` + $ IC_HTTP_PORT= + ``` + +## Step 1. Deploy a Web Application + +Create the application deployment and service: +``` +$ kubectl apply -f webapp.yaml +``` + +## Step 2 - Deploy the AP Dos Policy + +1. Create the syslog service and pod for the App Protect security logs: + ``` + $ kubectl apply -f syslog.yaml + ``` +1. Create the App Protect Dos policy and log configuration: + ``` + $ kubectl apply -f apdos-policy.yaml + $ kubectl apply -f apdos-logconf.yaml + ``` + +## Step 3 - Deploy the DOS Policy + +1. Update the `logDest` field from `dos.yaml` with the ClusterIP of the syslog service. For example, if the IP is `10.101.21.110`: + ```yaml + dos: + ... + logDest: "10.101.21.110:514" + ``` + +1. Create the DOS policy + ``` + $ kubectl apply -f dos.yaml + ``` + +Note the App Protect Dos configuration settings in the Policy resource. They enable DOS protection by configuring App Protect Dos with the policy and log configuration created in the previous step. + +## Step 4 - Configure Load Balancing + +1. Create the VirtualServer Resource: + ``` + $ kubectl apply -f virtual-server.yaml + ``` + +Note that the VirtualServer references the policy `dos-policy` created in Step 3. + +## Step 5 - Test the Application + +To access the application, curl the Webapp service. We'll use the --resolve option to set the Host header of a request with `webapp.example.com` + +1. Send a request to the application: + ``` + $ curl --resolve webapp.example.com:$IC_HTTP_PORT:$IC_IP http://webapp.example.com:$IC_HTTP_PORT/ + Server address: 10.12.0.18:80 + Server name: webapp-7586895968-r26zn + ... + ``` + +1. To check the security logs in the syslog pod: + ``` + $ kubectl exec -it -- cat /var/log/messages + ``` diff --git a/tests/data/virtual-server-dos/dos-logconf.yaml b/tests/data/virtual-server-dos/dos-logconf.yaml new file mode 100644 index 0000000000..885a524647 --- /dev/null +++ b/tests/data/virtual-server-dos/dos-logconf.yaml @@ -0,0 +1,12 @@ +apiVersion: appprotectdos.f5.com/v1beta1 +kind: APDosLogConf +metadata: + name: doslogconf +spec: + content: + format: splunk + max_message_size: 64k + filter: + traffic-mitigation-stats: all + bad-actors: top 10 + attack-signatures: top 10 diff --git a/tests/data/virtual-server-dos/dos-policy.yaml b/tests/data/virtual-server-dos/dos-policy.yaml new file mode 100644 index 0000000000..add82acd69 --- /dev/null +++ b/tests/data/virtual-server-dos/dos-policy.yaml @@ -0,0 +1,10 @@ +apiVersion: appprotectdos.f5.com/v1beta1 +kind: APDosPolicy +metadata: + name: dospolicy +spec: + mitigation_mode: "standard" + signatures: "on" + bad_actors: "on" + automation_tools_detection: "on" + tls_fingerprint: "on" diff --git a/tests/data/virtual-server-dos/dos-protected.yaml b/tests/data/virtual-server-dos/dos-protected.yaml new file mode 100644 index 0000000000..c593a1beec --- /dev/null +++ b/tests/data/virtual-server-dos/dos-protected.yaml @@ -0,0 +1,15 @@ +apiVersion: appprotectdos.f5.com/v1beta1 +kind: DosProtectedResource +metadata: + name: dos-protected +spec: + enable: true + name: "name" + apDosPolicy: "dospolicy" + apDosMonitor: + uri: "dos.example.com" + dosAccessLogDest: "127.0.0.1:5561" + dosSecurityLog: + enable: true + apDosLogConf: "doslogconf" + dosLogDest: "syslog-svc..svc.cluster.local:514" diff --git a/tests/data/virtual-server-dos/syslog.yaml b/tests/data/virtual-server-dos/syslog.yaml new file mode 100644 index 0000000000..de25eeb3d0 --- /dev/null +++ b/tests/data/virtual-server-dos/syslog.yaml @@ -0,0 +1,32 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: syslog +spec: + replicas: 1 + selector: + matchLabels: + app: syslog + template: + metadata: + labels: + app: syslog + spec: + containers: + - name: syslog + image: balabit/syslog-ng:3.28.1 + ports: + - containerPort: 514 + - containerPort: 601 +--- +apiVersion: v1 +kind: Service +metadata: + name: syslog-svc +spec: + ports: + - port: 514 + targetPort: 514 + protocol: TCP + selector: + app: syslog \ No newline at end of file diff --git a/tests/data/virtual-server-dos/virtual-server.yaml b/tests/data/virtual-server-dos/virtual-server.yaml new file mode 100644 index 0000000000..87086785b9 --- /dev/null +++ b/tests/data/virtual-server-dos/virtual-server.yaml @@ -0,0 +1,16 @@ +apiVersion: k8s.nginx.org/v1 +kind: VirtualServer +metadata: + name: dosapp +spec: + host: dos.example.com + + upstreams: + - name: dosapp + service: dos-svc + port: 80 + routes: + - path: / + dos: dos-protected + action: + pass: dosapp diff --git a/tests/data/virtual-server-dos/webapp.yaml b/tests/data/virtual-server-dos/webapp.yaml new file mode 100644 index 0000000000..31fde92a6e --- /dev/null +++ b/tests/data/virtual-server-dos/webapp.yaml @@ -0,0 +1,32 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: webapp +spec: + replicas: 1 + selector: + matchLabels: + app: webapp + template: + metadata: + labels: + app: webapp + spec: + containers: + - name: webapp + image: nginxdemos/nginx-hello:plain-text + ports: + - containerPort: 8080 +--- +apiVersion: v1 +kind: Service +metadata: + name: webapp-svc +spec: + ports: + - port: 80 + targetPort: 8080 + protocol: TCP + name: http + selector: + app: webapp diff --git a/tests/docker/Dockerfile b/tests/docker/Dockerfile index a83fdf0404..2727d42ebd 100644 --- a/tests/docker/Dockerfile +++ b/tests/docker/Dockerfile @@ -13,7 +13,8 @@ RUN pip install -r requirements.txt COPY deployments /workspace/deployments RUN curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl \ - && install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl + && install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl \ + && install -y apache2-utils COPY tests /workspace/tests diff --git a/tests/docker/gitlab.Dockerfile b/tests/docker/gitlab.Dockerfile index 2428ab16a5..c39fe3c0af 100644 --- a/tests/docker/gitlab.Dockerfile +++ b/tests/docker/gitlab.Dockerfile @@ -3,7 +3,7 @@ FROM python:3.9 ARG GCLOUD_VERSION=364.0.0 -RUN apt-get update && apt-get install -y curl git jq \ +RUN apt-get update && apt-get install -y curl git jq apache2-utils \ && curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl \ && chmod +x ./kubectl \ && mv ./kubectl /usr/local/bin \ diff --git a/tests/suite/custom_resources_utils.py b/tests/suite/custom_resources_utils.py index 4c3aabf0ed..37ca34637f 100644 --- a/tests/suite/custom_resources_utils.py +++ b/tests/suite/custom_resources_utils.py @@ -226,6 +226,133 @@ def delete_resource(custom_objects: CustomObjectsApi, resource, namespace, plura print(f"Resource '{kind}' was removed with name '{name}'") +def create_dos_logconf_from_yaml(custom_objects: CustomObjectsApi, yaml_manifest, namespace) -> str: + """ + Create a logconf for Dos, based on yaml file. + :param custom_objects: CustomObjectsApi + :param yaml_manifest: an absolute path to file + :param namespace: + :return: str + """ + print("Create DOS logconf:") + with open(yaml_manifest) as f: + dep = yaml.safe_load(f) + custom_objects.create_namespaced_custom_object( + "appprotectdos.f5.com", "v1beta1", namespace, "apdoslogconfs", dep + ) + print(f"DOS logconf created with name '{dep['metadata']['name']}'") + return dep["metadata"]["name"] + + +def create_dos_policy_from_yaml(custom_objects: CustomObjectsApi, yaml_manifest, namespace) -> str: + """ + Create a policy for Dos based on yaml file. + :param custom_objects: CustomObjectsApi + :param yaml_manifest: an absolute path to file + :param namespace: + :return: str + """ + print("Create Dos Policy:") + with open(yaml_manifest) as f: + dep = yaml.safe_load(f) + custom_objects.create_namespaced_custom_object( + "appprotectdos.f5.com", "v1beta1", namespace, "apdospolicies", dep + ) + print(f"DOS Policy created with name '{dep['metadata']['name']}'") + return dep["metadata"]["name"] + + +def create_dos_protected_from_yaml(custom_objects: CustomObjectsApi, yaml_manifest, namespace) -> str: + """ + Create a protected resource for Dos based on yaml file. + :param custom_objects: CustomObjectsApi + :param yaml_manifest: an absolute path to file + :param namespace: + :return: str + """ + print("Create Dos Protected:") + with open(yaml_manifest) as f: + dep = yaml.safe_load(f) + dep['spec']['dosSecurityLog']['apDosLogConf'] = dep['spec']['dosSecurityLog']['apDosLogConf'].replace("", namespace) + dep['spec']['dosSecurityLog']['dosLogDest'] = dep['spec']['dosSecurityLog']['dosLogDest'].replace("", namespace) + dep['spec']['apDosPolicy'] = dep['spec']['apDosPolicy'].replace("", namespace) + custom_objects.create_namespaced_custom_object( + "appprotectdos.f5.com", "v1beta1", namespace, "dosprotectedresources", dep + ) + print(f"DOS Protected resource created with name '{namespace}/{dep['metadata']['name']}'") + return dep["metadata"]["name"] + + +def delete_dos_logconf(custom_objects: CustomObjectsApi, name, namespace) -> None: + """ + Delete a Dos logconf. + :param custom_objects: CustomObjectsApi + :param namespace: namespace + :param name: + :return: + """ + print(f"Delete DOS logconf: {name}") + custom_objects.delete_namespaced_custom_object( + "appprotectdos.f5.com", "v1beta1", namespace, "apdoslogconfs", name + ) + ensure_item_removal( + custom_objects.get_namespaced_custom_object, + "appprotectdos.f5.com", + "v1beta1", + namespace, + "apdoslogconfs", + name, + ) + print(f"DOS logconf was removed with name: {name}") + + +def delete_dos_protected(custom_objects: CustomObjectsApi, name, namespace) -> None: + """ + Delete a Dos protected. + :param custom_objects: CustomObjectsApi + :param namespace: namespace + :param name: + :return: + """ + print(f"Delete DOS protected: {name}") + custom_objects.delete_namespaced_custom_object( + "appprotectdos.f5.com", "v1beta1", namespace, "dosprotectedresources", name + ) + ensure_item_removal( + custom_objects.get_namespaced_custom_object, + "appprotectdos.f5.com", + "v1beta1", + namespace, + "dosprotectedresources", + name, + ) + print(f"DOS logconf was removed with name: {name}") + + +def delete_dos_policy(custom_objects: CustomObjectsApi, name, namespace) -> None: + """ + Delete a Dos policy. + :param custom_objects: CustomObjectsApi + :param namespace: namespace + :param name: + :return: + """ + print(f"Delete a DOS policy: {name}") + custom_objects.delete_namespaced_custom_object( + "appprotectdos.f5.com", "v1beta1", namespace, "apdospolicies", name + ) + ensure_item_removal( + custom_objects.get_namespaced_custom_object, + "appprotectdos.f5.com", + "v1beta1", + namespace, + "apdospolicies", + name, + ) + time.sleep(3) + print(f"DOS policy was removed with name: {name}") + + def patch_ts_from_yaml( custom_objects: CustomObjectsApi, name, yaml_manifest, namespace ) -> None: diff --git a/tests/suite/dos_utils.py b/tests/suite/dos_utils.py new file mode 100644 index 0000000000..11a3001dfe --- /dev/null +++ b/tests/suite/dos_utils.py @@ -0,0 +1,34 @@ +from suite.resources_utils import get_file_contents, wait_before_test + + +def log_content_to_dic(log_contents): + arr = [] + for line in log_contents.splitlines(): + if line.__contains__('app-protect-dos'): + arr.append(line) + + log_info_dic = [] + for line in arr: + chunks = line.split(",") + d = {} + for chunk in chunks: + tmp = chunk.split("=") + if len(tmp) == 2: + if 'date_time' in tmp[0]: + tmp[0] = 'date_time' + d[tmp[0].strip()] = tmp[1].replace('"', '') + log_info_dic.append(d) + return log_info_dic + + +def find_in_log(kube_apis, log_location, syslog_pod, namespace, time, value): + log_contents = "" + retry = 0 + while ( + value not in log_contents + and retry <= time / 10 + ): + log_contents = get_file_contents(kube_apis.v1, log_location, syslog_pod, namespace, False) + retry += 1 + wait_before_test(10) + print(f"{value} Not in log, retrying... #{retry}") diff --git a/tests/suite/fixtures.py b/tests/suite/fixtures.py index eafaef474d..6e67ddae42 100644 --- a/tests/suite/fixtures.py +++ b/tests/suite/fixtures.py @@ -48,6 +48,8 @@ replace_configmap_from_yaml, delete_testing_namespaces, get_first_pod_name, + create_dos_arbitrator, + delete_dos_arbitrator, ) from suite.resources_utils import ( create_ingress_controller, @@ -64,6 +66,7 @@ create_configmap_from_yaml, create_secret_from_yaml, configure_rbac_with_ap, + configure_rbac_with_dos, create_items_from_yaml, delete_items_from_yaml, delete_secret @@ -399,24 +402,19 @@ def cli_arguments(request) -> {}: @pytest.fixture(scope="class") -def crd_ingress_controller( - cli_arguments, kube_apis, ingress_controller_prerequisites, ingress_controller_endpoint, request +def crds( + kube_apis, request ) -> None: """ Create an Ingress Controller with CRD enabled. - :param cli_arguments: pytest context :param kube_apis: client apis - :param ingress_controller_prerequisites - :param ingress_controller_endpoint: :param request: pytest fixture to parametrize this method {type: complete|rbac-without-vs, extra_args: } 'type' type of test pre-configuration 'extra_args' list of IC cli arguments :return: """ - namespace = ingress_controller_prerequisites.namespace - name = "nginx-ingress" vs_crd_name = get_name_from_yaml(f"{DEPLOYMENTS}/common/crds/k8s.nginx.org_virtualservers.yaml") vsr_crd_name = get_name_from_yaml( f"{DEPLOYMENTS}/common/crds/k8s.nginx.org_virtualserverroutes.yaml" @@ -430,9 +428,6 @@ def crd_ingress_controller( ) try: - print("------------------------- Update ClusterRole -----------------------------------") - if request.param["type"] == "rbac-without-vs": - patch_rbac(kube_apis.rbac_v1, f"{TEST_DATA}/virtual-server/rbac-without-vs.yaml") print("------------------------- Register CRDs -----------------------------------") create_crd_from_yaml( kube_apis.api_extensions_v1, @@ -459,6 +454,51 @@ def crd_ingress_controller( gc_crd_name, f"{DEPLOYMENTS}/common/crds/k8s.nginx.org_globalconfigurations.yaml", ) + except ApiException as ex: + # Finalizer method doesn't start if fixture creation was incomplete, ensure clean up here + print(f"Failed to complete CRD IC fixture: {ex}\nClean up the cluster as much as possible.") + delete_crd(kube_apis.api_extensions_v1, vs_crd_name) + delete_crd(kube_apis.api_extensions_v1, vsr_crd_name) + delete_crd(kube_apis.api_extensions_v1, pol_crd_name) + delete_crd(kube_apis.api_extensions_v1, ts_crd_name) + delete_crd(kube_apis.api_extensions_v1, gc_crd_name) + pytest.fail("IC setup failed") + + def fin(): + delete_crd(kube_apis.api_extensions_v1, vs_crd_name) + delete_crd(kube_apis.api_extensions_v1, vsr_crd_name) + delete_crd(kube_apis.api_extensions_v1, pol_crd_name) + delete_crd(kube_apis.api_extensions_v1, ts_crd_name) + delete_crd(kube_apis.api_extensions_v1, gc_crd_name) + + request.addfinalizer(fin) + + +@pytest.fixture(scope="class") +def crd_ingress_controller( + cli_arguments, kube_apis, ingress_controller_prerequisites, ingress_controller_endpoint, request, crds +) -> None: + """ + Create an Ingress Controller with CRD enabled. + + :param crds: the common ingress controller crds. + :param cli_arguments: pytest context + :param kube_apis: client apis + :param ingress_controller_prerequisites + :param ingress_controller_endpoint: + :param request: pytest fixture to parametrize this method + {type: complete|rbac-without-vs, extra_args: } + 'type' type of test pre-configuration + 'extra_args' list of IC cli arguments + :return: + """ + namespace = ingress_controller_prerequisites.namespace + name = "nginx-ingress" + + try: + print("------------------------- Update ClusterRole -----------------------------------") + if request.param["type"] == "rbac-without-vs": + patch_rbac(kube_apis.rbac_v1, f"{TEST_DATA}/virtual-server/rbac-without-vs.yaml") print("------------------------- Create IC -----------------------------------") name = create_ingress_controller( kube_apis.v1, @@ -474,12 +514,6 @@ def crd_ingress_controller( ) except ApiException as ex: # Finalizer method doesn't start if fixture creation was incomplete, ensure clean up here - print(f"Failed to complete CRD IC fixture: {ex}\nClean up the cluster as much as possible.") - delete_crd(kube_apis.api_extensions_v1, vs_crd_name) - delete_crd(kube_apis.api_extensions_v1, vsr_crd_name) - delete_crd(kube_apis.api_extensions_v1, pol_crd_name) - delete_crd(kube_apis.api_extensions_v1, ts_crd_name) - delete_crd(kube_apis.api_extensions_v1, gc_crd_name) print("Restore the ClusterRole:") patch_rbac(kube_apis.rbac_v1, f"{DEPLOYMENTS}/rbac/rbac.yaml") print("Remove the IC:") @@ -489,11 +523,6 @@ def crd_ingress_controller( pytest.fail("IC setup failed") def fin(): - delete_crd(kube_apis.api_extensions_v1, vs_crd_name) - delete_crd(kube_apis.api_extensions_v1, vsr_crd_name) - delete_crd(kube_apis.api_extensions_v1, pol_crd_name) - delete_crd(kube_apis.api_extensions_v1, ts_crd_name) - delete_crd(kube_apis.api_extensions_v1, gc_crd_name) print("Restore the ClusterRole:") patch_rbac(kube_apis.rbac_v1, f"{DEPLOYMENTS}/rbac/rbac.yaml") print("Remove the IC:") @@ -506,10 +535,11 @@ def fin(): @pytest.fixture(scope="class") def crd_ingress_controller_with_ap( - cli_arguments, kube_apis, ingress_controller_prerequisites, ingress_controller_endpoint, request + cli_arguments, kube_apis, ingress_controller_prerequisites, ingress_controller_endpoint, request, crds ) -> None: """ Create an Ingress Controller with AppProtect CRD enabled. + :param crds: the common IC crds. :param cli_arguments: pytest context :param kube_apis: client apis :param ingress_controller_prerequisites @@ -537,16 +567,6 @@ def crd_ingress_controller_with_ap( ap_uds_crd_name = get_name_from_yaml( f"{DEPLOYMENTS}/common/crds/appprotect.f5.com_apusersigs.yaml" ) - vs_crd_name = get_name_from_yaml( - f"{DEPLOYMENTS}/common/crds/k8s.nginx.org_virtualservers.yaml" - ) - vsr_crd_name = get_name_from_yaml( - f"{DEPLOYMENTS}/common/crds/k8s.nginx.org_virtualserverroutes.yaml" - ) - pol_crd_name = get_name_from_yaml(f"{DEPLOYMENTS}/common/crds/k8s.nginx.org_policies.yaml") - ts_crd_name = get_name_from_yaml( - f"{DEPLOYMENTS}/common/crds/k8s.nginx.org_transportservers.yaml" - ) create_crd_from_yaml( kube_apis.api_extensions_v1, ap_pol_crd_name, @@ -562,26 +582,6 @@ def crd_ingress_controller_with_ap( ap_uds_crd_name, f"{DEPLOYMENTS}/common/crds/appprotect.f5.com_apusersigs.yaml", ) - create_crd_from_yaml( - kube_apis.api_extensions_v1, - vs_crd_name, - f"{DEPLOYMENTS}/common/crds/k8s.nginx.org_virtualservers.yaml", - ) - create_crd_from_yaml( - kube_apis.api_extensions_v1, - vsr_crd_name, - f"{DEPLOYMENTS}/common/crds/k8s.nginx.org_virtualserverroutes.yaml", - ) - create_crd_from_yaml( - kube_apis.api_extensions_v1, - pol_crd_name, - f"{DEPLOYMENTS}/common/crds/k8s.nginx.org_policies.yaml", - ) - create_crd_from_yaml( - kube_apis.api_extensions_v1, - ts_crd_name, - f"{DEPLOYMENTS}/common/crds/k8s.nginx.org_transportservers.yaml", - ) print("------------------------- Create IC -----------------------------------") name = create_ingress_controller( @@ -610,61 +610,155 @@ def crd_ingress_controller_with_ap( kube_apis.api_extensions_v1, ap_uds_crd_name, ) - delete_crd( - kube_apis.api_extensions_v1, - vs_crd_name, + print("Remove ap-rbac") + cleanup_rbac(kube_apis.rbac_v1, rbac) + + print("Remove the IC:") + delete_ingress_controller( + kube_apis.apps_v1_api, name, cli_arguments["deployment-type"], namespace ) + pytest.fail("IC setup failed") + def fin(): + print("--------------Cleanup----------------") delete_crd( kube_apis.api_extensions_v1, - vsr_crd_name, + ap_pol_crd_name, ) delete_crd( kube_apis.api_extensions_v1, - pol_crd_name, + ap_log_crd_name, ) delete_crd( kube_apis.api_extensions_v1, - ts_crd_name, + ap_uds_crd_name, ) print("Remove ap-rbac") cleanup_rbac(kube_apis.rbac_v1, rbac) + print("Remove the IC:") delete_ingress_controller( kube_apis.apps_v1_api, name, cli_arguments["deployment-type"], namespace ) - pytest.fail("IC setup failed") - def fin(): - print("--------------Cleanup----------------") - delete_crd( + + request.addfinalizer(fin) + + +@pytest.fixture(scope="class") +def crd_ingress_controller_with_dos( + cli_arguments, kube_apis, ingress_controller_prerequisites, ingress_controller_endpoint, request, crds +) -> None: + """ + Create an Ingress Controller with DOS CRDs enabled. + :param crds: the common IC crds. + :param cli_arguments: pytest context + :param kube_apis: client apis + :param ingress_controller_prerequisites + :param ingress_controller_endpoint: + :param request: pytest fixture to parametrize this method + {extra_args: } + 'extra_args' list of IC arguments + :return: + """ + namespace = ingress_controller_prerequisites.namespace + name = "nginx-ingress" + + try: + print( + "--------------------Create roles and bindings for AppProtect------------------------" + ) + rbac = configure_rbac_with_dos(kube_apis.rbac_v1) + + print("------------------------- Register AP CRD -----------------------------------") + dos_pol_crd_name = get_name_from_yaml( + f"{DEPLOYMENTS}/common/crds/appprotectdos.f5.com_apdospolicy.yaml" + ) + dos_log_crd_name = get_name_from_yaml( + f"{DEPLOYMENTS}/common/crds/appprotectdos.f5.com_apdoslogconfs.yaml" + ) + dos_protected_crd_name = get_name_from_yaml( + f"{DEPLOYMENTS}/common/crds/appprotectdos.f5.com_dosprotectedresources.yaml" + ) + create_crd_from_yaml( kube_apis.api_extensions_v1, - ap_pol_crd_name, + dos_pol_crd_name, + f"{DEPLOYMENTS}/common/crds/appprotectdos.f5.com_apdospolicy.yaml", + ) + create_crd_from_yaml( + kube_apis.api_extensions_v1, + dos_log_crd_name, + f"{DEPLOYMENTS}/common/crds/appprotectdos.f5.com_apdoslogconfs.yaml", + ) + create_crd_from_yaml( + kube_apis.api_extensions_v1, + dos_protected_crd_name, + f"{DEPLOYMENTS}/common/crds/appprotectdos.f5.com_dosprotectedresources.yaml", + ) + + print("------------------------- Create dos arbitrator -----------------------") + dos_arbitrator_name = create_dos_arbitrator( + kube_apis.v1, + kube_apis.apps_v1_api, + namespace, ) + print("------------------------- Create IC -----------------------------------") + name = create_ingress_controller( + kube_apis.v1, + kube_apis.apps_v1_api, + cli_arguments, + namespace, + request.param.get("extra_args", None), + ) + ensure_connection_to_public_endpoint( + ingress_controller_endpoint.public_ip, + ingress_controller_endpoint.port, + ingress_controller_endpoint.port_ssl, + ) + except Exception as ex: + print(f"Failed to complete CRD IC fixture: {ex}\nClean up the cluster as much as possible.") delete_crd( kube_apis.api_extensions_v1, - ap_log_crd_name, + dos_pol_crd_name, ) delete_crd( kube_apis.api_extensions_v1, - ap_uds_crd_name, + dos_log_crd_name, ) delete_crd( kube_apis.api_extensions_v1, - vs_crd_name, + dos_protected_crd_name, + ) + print("Remove ap-rbac") + cleanup_rbac(kube_apis.rbac_v1, rbac) + print("Remove dos arbitrator:") + delete_dos_arbitrator( + kube_apis.v1, kube_apis.apps_v1_api, dos_arbitrator_name, namespace + ) + print("Remove the IC:") + delete_ingress_controller( + kube_apis.apps_v1_api, name, cli_arguments["deployment-type"], namespace ) + pytest.fail("IC setup failed") + + def fin(): + print("--------------Cleanup----------------") delete_crd( kube_apis.api_extensions_v1, - vsr_crd_name, + dos_pol_crd_name, ) delete_crd( kube_apis.api_extensions_v1, - pol_crd_name, + dos_log_crd_name, ) delete_crd( kube_apis.api_extensions_v1, - ts_crd_name, + dos_protected_crd_name, ) print("Remove ap-rbac") cleanup_rbac(kube_apis.rbac_v1, rbac) + print("Remove dos arbitrator:") + delete_dos_arbitrator( + kube_apis.v1, kube_apis.apps_v1_api, dos_arbitrator_name, namespace + ) print("Remove the IC:") delete_ingress_controller( kube_apis.apps_v1_api, name, cli_arguments["deployment-type"], namespace diff --git a/tests/suite/resources_utils.py b/tests/suite/resources_utils.py index 49a3a43ad5..8a628c1b36 100644 --- a/tests/suite/resources_utils.py +++ b/tests/suite/resources_utils.py @@ -79,6 +79,30 @@ def configure_rbac_with_ap(rbac_v1: RbacAuthorizationV1Api) -> RBACAuthorization return RBACAuthorization(role_name, binding_name) +def configure_rbac_with_dos(rbac_v1: RbacAuthorizationV1Api) -> RBACAuthorization: + """ + Create cluster and binding for Dos module. + :param rbac_v1: RbacAuthorizationV1Api + :return: RBACAuthorization + """ + with open(f"{DEPLOYMENTS}/rbac/apdos-rbac.yaml") as f: + docs = yaml.safe_load_all(f) + role_name = "" + binding_name = "" + for dep in docs: + if dep["kind"] == "ClusterRole": + print("Create cluster role for DOS") + role_name = dep["metadata"]["name"] + rbac_v1.create_cluster_role(dep) + print(f"Created role '{role_name}'") + elif dep["kind"] == "ClusterRoleBinding": + print("Create binding for DOS") + binding_name = dep["metadata"]["name"] + rbac_v1.create_cluster_role_binding(dep) + print(f"Created binding '{binding_name}'") + return RBACAuthorization(role_name, binding_name) + + def patch_rbac(rbac_v1: RbacAuthorizationV1Api, yaml_manifest) -> RBACAuthorization: """ Patch a clusterrole and a binding. @@ -782,7 +806,7 @@ def delete_testing_namespaces(v1: CoreV1Api) -> []: delete_namespace(v1, namespace.metadata.name) -def get_file_contents(v1: CoreV1Api, file_path, pod_name, pod_namespace) -> str: +def get_file_contents(v1: CoreV1Api, file_path, pod_name, pod_namespace, print_log=True) -> str: """ Execute 'cat file_path' command in a pod. @@ -790,6 +814,7 @@ def get_file_contents(v1: CoreV1Api, file_path, pod_name, pod_namespace) -> str: :param pod_name: pod name :param pod_namespace: pod namespace :param file_path: an absolute path to a file in the pod + :param print_log: bool to decide if print log or not :return: str """ command = ["cat", file_path] @@ -804,7 +829,33 @@ def get_file_contents(v1: CoreV1Api, file_path, pod_name, pod_namespace) -> str: tty=False, ) result_conf = str(resp) - print("\nFile contents:\n" + result_conf) + if print_log: + print("\nFile contents:\n" + result_conf) + return result_conf + + +def nginx_reload(v1: CoreV1Api, pod_name, pod_namespace) -> str: + """ + Execute 'nginx -s reload' command in a pod. + + :param v1: CoreV1Api + :param pod_name: pod name + :param pod_namespace: pod namespace + :return: str + """ + command = ["nginx", "-s", "reload"] + resp = stream( + v1.connect_get_namespaced_pod_exec, + pod_name, + pod_namespace, + command=command, + stderr=True, + stdin=False, + stdout=True, + tty=False, + ) + result_conf = str(resp) + return result_conf @@ -1011,6 +1062,54 @@ def delete_ingress_controller(apps_v1_api: AppsV1Api, name, dep_type, namespace) delete_daemon_set(apps_v1_api, name, namespace) +def create_dos_arbitrator( + v1: CoreV1Api, apps_v1_api: AppsV1Api, namespace +) -> str: + """ + Create dos arbitrator according to the params. + + :param v1: CoreV1Api + :param apps_v1_api: AppsV1Api + :param namespace: namespace name + :return: str + """ + yaml_manifest = ( + f"{DEPLOYMENTS}/deployment/appprotect-dos-arb.yaml" + ) + with open(yaml_manifest) as f: + dep = yaml.safe_load(f) + + name = create_deployment(apps_v1_api, namespace, dep) + + before = time.time() + wait_until_all_pods_are_ready(v1, namespace) + after = time.time() + print(f"All pods came up in {int(after-before)} seconds") + print(f"Dos arbitrator was created with name '{name}'") + + print("create dos svc") + svc_name = create_service_from_yaml( + v1, + namespace, + f"{DEPLOYMENTS}/service/appprotect-dos-arb-svc.yaml", + ) + print(f"Dos arbitrator svc was created with name '{svc_name}'") + return name + + +def delete_dos_arbitrator(v1: CoreV1Api, apps_v1_api: AppsV1Api, name, namespace) -> None: + """ + Delete dos arbitrator. + + :param v1: CoreV1Api + :param apps_v1_api: AppsV1Api + :param name: name + :param namespace: namespace name + :return: + """ + delete_deployment(apps_v1_api, name, namespace) + delete_service(v1, "svc-appprotect-dos-arb", namespace) + def create_ns_and_sa_from_yaml(v1: CoreV1Api, yaml_manifest) -> str: """ Create a namespace and a service account in that namespace. @@ -1097,6 +1196,25 @@ def create_ingress_with_ap_annotations( create_ingress(kube_apis.networking_v1, namespace, doc) +def create_ingress_with_dos_annotations( + kube_apis, yaml_manifest, namespace, dos_protected +) -> None: + """ + Create an ingress with AppProtect annotations + :param dos_protected: the namepsace/name of the dos protected resource + :param kube_apis: KubeApis + :param yaml_manifest: an absolute path to ingress yaml + :param namespace: namespace + :return: + """ + print("Load ingress yaml and set DOS annotations") + + with open(yaml_manifest) as f: + doc = yaml.safe_load(f) + doc["metadata"]["annotations"]["appprotectdos.f5.com/app-protect-dos-resource"] = dos_protected + create_ingress(kube_apis.networking_v1, namespace, doc) + + def replace_ingress_with_ap_annotations( kube_apis, yaml_manifest, name, namespace, policy_name, ap_pol_st, ap_log_st, syslog_ep ) -> None: diff --git a/tests/suite/test_dos.py b/tests/suite/test_dos.py new file mode 100644 index 0000000000..404d2f07bd --- /dev/null +++ b/tests/suite/test_dos.py @@ -0,0 +1,446 @@ +import requests +import pytest +import subprocess +import os + +from settings import TEST_DATA +from suite.custom_resources_utils import ( + create_dos_logconf_from_yaml, + create_dos_policy_from_yaml, + create_dos_protected_from_yaml, + delete_dos_policy, + delete_dos_logconf, + delete_dos_protected, +) +from suite.dos_utils import find_in_log, log_content_to_dic +from suite.resources_utils import ( + wait_before_test, + create_example_app, + wait_until_all_pods_are_ready, + create_items_from_yaml, + delete_items_from_yaml, + delete_common_app, + ensure_connection_to_public_endpoint, + create_ingress_with_dos_annotations, + ensure_response_from_backend, + get_ingress_nginx_template_conf, + get_file_contents, + get_test_file_name, + write_to_json, + replace_configmap_from_yaml, + scale_deployment, + get_pods_amount, +) +from suite.yaml_utils import get_first_ingress_host_from_yaml +from datetime import datetime + +src_ing_yaml = f"{TEST_DATA}/dos/dos-ingress.yaml" +valid_resp_addr = "Server address:" +valid_resp_name = "Server name:" +invalid_resp_title = "Request Rejected" +invalid_resp_body = "The requested URL was rejected. Please consult with your administrator." +reload_times = {} + +class DosSetup: + """ + Encapsulate the example details. + Attributes: + req_url (str): + pol_name (str): + log_name (str): + """ + def __init__(self, req_url, pol_name, log_name): + self.req_url = req_url + self.pol_name = pol_name + self.log_name = log_name + + +@pytest.fixture(scope="class") +def dos_setup( + request, kube_apis, ingress_controller_endpoint, ingress_controller_prerequisites, test_namespace +) -> DosSetup: + """ + Deploy simple application and all the DOS resources under test in one namespace. + + :param request: pytest fixture + :param kube_apis: client apis + :param ingress_controller_endpoint: public endpoint + :param ingress_controller_prerequisites: IC pre-requisites + :param test_namespace: + :return: DosSetup + """ + + print(f"------------- Replace ConfigMap --------------") + replace_configmap_from_yaml( + kube_apis.v1, + ingress_controller_prerequisites.config_map["metadata"]["name"], + ingress_controller_prerequisites.namespace, + f"{TEST_DATA}/dos/nginx-config.yaml" + ) + + print("------------------------- Deploy Dos backend application -------------------------") + create_example_app(kube_apis, "dos", test_namespace) + req_url = f"http://{ingress_controller_endpoint.public_ip}:{ingress_controller_endpoint.port}/" + wait_until_all_pods_are_ready(kube_apis.v1, test_namespace) + ensure_connection_to_public_endpoint( + ingress_controller_endpoint.public_ip, + ingress_controller_endpoint.port, + ingress_controller_endpoint.port_ssl, + ) + + print("------------------------- Deploy Secret -----------------------------") + src_sec_yaml = f"{TEST_DATA}/dos/dos-secret.yaml" + create_items_from_yaml(kube_apis, src_sec_yaml, test_namespace) + + print("------------------------- Deploy logconf -----------------------------") + src_log_yaml = f"{TEST_DATA}/dos/dos-logconf.yaml" + log_name = create_dos_logconf_from_yaml(kube_apis.custom_objects, src_log_yaml, test_namespace) + + print(f"------------------------- Deploy dospolicy ---------------------------") + src_pol_yaml = f"{TEST_DATA}/dos/dos-policy.yaml" + pol_name = create_dos_policy_from_yaml(kube_apis.custom_objects, src_pol_yaml, test_namespace) + + print(f"------------------------- Deploy protected resource ---------------------------") + src_protected_yaml = f"{TEST_DATA}/dos/dos-protected.yaml" + protected_name = create_dos_protected_from_yaml(kube_apis.custom_objects, src_protected_yaml, test_namespace) + + def fin(): + print("Clean up:") + delete_dos_policy(kube_apis.custom_objects, pol_name, test_namespace) + delete_dos_logconf(kube_apis.custom_objects, log_name, test_namespace) + delete_dos_protected(kube_apis.custom_objects, protected_name, test_namespace) + delete_common_app(kube_apis, "dos", test_namespace) + delete_items_from_yaml(kube_apis, src_sec_yaml, test_namespace) + write_to_json(f"reload-{get_test_file_name(request.node.fspath)}.json", reload_times) + + request.addfinalizer(fin) + + return DosSetup(req_url, pol_name, log_name) + + +@pytest.mark.dos +@pytest.mark.parametrize( + "crd_ingress_controller_with_dos", + [ + { + "extra_args": [ + f"-enable-custom-resources", + f"-enable-app-protect-dos", + f"-v=3", + ] + } + ], + indirect=["crd_ingress_controller_with_dos"], +) +class TestDos: + def test_ap_nginx_config_entries( + self, kube_apis, crd_ingress_controller_with_dos, dos_setup, test_namespace + ): + """ + Test to verify Dos directive in nginx config + """ + conf_directive = [ + f"app_protect_dos_enable on;", + f"app_protect_dos_security_log_enable on;", + f"app_protect_dos_monitor uri=dos.example.com protocol=http1 timeout=5;", + f"app_protect_dos_name \"{test_namespace}/dos-protected/name\";", + f"app_protect_dos_policy_file /etc/nginx/dos/policies/{test_namespace}_{dos_setup.pol_name}.json;", + f"app_protect_dos_security_log_enable on;", + f"app_protect_dos_security_log /etc/nginx/dos/logconfs/{test_namespace}_{dos_setup.log_name}.json syslog:server=syslog-svc.{test_namespace}.svc.cluster.local:514;", + ] + + create_ingress_with_dos_annotations( + kube_apis, src_ing_yaml, test_namespace, test_namespace + "/dos-protected", + ) + + ingress_host = get_first_ingress_host_from_yaml(src_ing_yaml) + ensure_response_from_backend(dos_setup.req_url, ingress_host, check404=True) + + # items[-1] because syslog pod is last one to spin-up + pod_name = kube_apis.v1.list_namespaced_pod("nginx-ingress").items[-1].metadata.name + + result_conf = get_ingress_nginx_template_conf( + kube_apis.v1, test_namespace, "dos-ingress", pod_name, "nginx-ingress" + ) + + delete_items_from_yaml(kube_apis, src_ing_yaml, test_namespace) + + for _ in conf_directive: + assert _ in result_conf + + def test_dos_sec_logs_on( + self, + kube_apis, + ingress_controller_prerequisites, + crd_ingress_controller_with_dos, + dos_setup, + test_namespace, + ): + """ + Test corresponding log entries with correct policy (includes setting up a syslog server as defined in syslog.yaml) + """ + src_syslog_yaml = f"{TEST_DATA}/dos/dos-syslog.yaml" + log_loc = f"/var/log/messages" + + create_items_from_yaml(kube_apis, src_syslog_yaml, test_namespace) + + # items[-1] because syslog pod is last one to spin-up + syslog_pod = kube_apis.v1.list_namespaced_pod(test_namespace).items[-1].metadata.name + + wait_before_test(30) + create_ingress_with_dos_annotations( + kube_apis, src_ing_yaml, test_namespace, test_namespace+"/dos-protected" + ) + ingress_host = get_first_ingress_host_from_yaml(src_ing_yaml) + + print("--------- Run test while DOS module is enabled with correct policy ---------") + + ensure_response_from_backend(dos_setup.req_url, ingress_host, check404=True) + # items[-1] because syslog pod is last one to spin-up + pod_name = kube_apis.v1.list_namespaced_pod("nginx-ingress").items[-1].metadata.name + get_ingress_nginx_template_conf( + kube_apis.v1, test_namespace, "dos-ingress", pod_name, "nginx-ingress" + ) + + print("----------------------- Send request ----------------------") + response = requests.get( + dos_setup.req_url, headers={"host": "dos.example.com"}, verify=False + ) + print(response.text) + wait_before_test(10) + + print(f'log_loc {log_loc} syslog_pod {syslog_pod} test_namespace {test_namespace}') + log_contents = get_file_contents(kube_apis.v1, log_loc, syslog_pod, test_namespace) + + delete_items_from_yaml(kube_apis, src_syslog_yaml, test_namespace) + delete_items_from_yaml(kube_apis, src_ing_yaml, test_namespace) + + print(log_contents) + + assert 'product="app-protect-dos"' in log_contents + assert f'vs_name="{test_namespace}/dos-protected/name"' in log_contents + assert 'bad_actor' in log_contents + + def test_dos_under_attack_no_learning( + self, kube_apis, crd_ingress_controller_with_dos, dos_setup, test_namespace + ): + """ + Test App Protect Dos: Block bad clients attack + """ + + print("------------------------- Deploy Syslog -----------------------------") + src_syslog_yaml = f"{TEST_DATA}/dos/dos-syslog.yaml" + log_loc = f"/var/log/messages" + create_items_from_yaml(kube_apis, src_syslog_yaml, test_namespace) + syslog_pod = kube_apis.v1.list_namespaced_pod(test_namespace).items[-1].metadata.name + + wait_before_test(30) + + print("------------------------- Deploy ingress -----------------------------") + create_ingress_with_dos_annotations( + kube_apis, src_ing_yaml, test_namespace, test_namespace+"/dos-protected" + ) + ingress_host = get_first_ingress_host_from_yaml(src_ing_yaml) + + print("------------------------- Attack -----------------------------") + wait_before_test(10) + print("start bad clients requests") + p_attack = subprocess.Popen( + [f"exec {TEST_DATA}/dos/bad_clients_xff.sh {ingress_host} {dos_setup.req_url}"], + shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + print("Attack for 30 seconds") + wait_before_test(30) + + print("Stop Attack") + p_attack.terminate() + + print("wait max 140 seconds after attack stop, to get attack ended") + find_in_log(kube_apis, log_loc, syslog_pod, test_namespace, 140, "attack_event=\"Attack ended\"") + + log_contents = get_file_contents(kube_apis.v1, log_loc, syslog_pod, test_namespace) + log_info_dic = log_content_to_dic(log_contents) + + # Analyze the log + no_attack = False + attack_started = False + under_attack = False + attack_ended = False + for log in log_info_dic: + # Start with no attack + if log['attack_event'] == "No Attack" and int(log['dos_attack_id']) == 0 and not no_attack: + no_attack = True + # Attack started + elif log['attack_event'] == "Attack started" and int(log['dos_attack_id']) > 0 and not attack_started: + attack_started = True + # Under attack + elif log['attack_event'] == "Under Attack" and int(log['dos_attack_id']) > 0 and not under_attack: + under_attack = True + # Attack ended + elif log['attack_event'] == "Attack ended" and int(log['dos_attack_id']) > 0 and not attack_ended: + attack_ended = True + + delete_items_from_yaml(kube_apis, src_syslog_yaml, test_namespace) + delete_items_from_yaml(kube_apis, src_ing_yaml, test_namespace) + + assert ( + no_attack + and attack_started + and under_attack + and attack_ended + ) + + def test_dos_under_attack_with_learning( + self, kube_apis, crd_ingress_controller_with_dos, dos_setup, test_namespace + ): + """ + Test App Protect Dos: Block bad clients attack with learning + """ + + print("------------------------- Deploy Syslog -----------------------------") + src_syslog_yaml = f"{TEST_DATA}/dos/dos-syslog.yaml" + log_loc = f"/var/log/messages" + create_items_from_yaml(kube_apis, src_syslog_yaml, test_namespace) + syslog_pod = kube_apis.v1.list_namespaced_pod(test_namespace).items[-1].metadata.name + wait_before_test(30) + print("------------------------- Deploy ingress -----------------------------") + create_ingress_with_dos_annotations( + kube_apis, src_ing_yaml, test_namespace, test_namespace+"/dos-protected" + ) + ingress_host = get_first_ingress_host_from_yaml(src_ing_yaml) + + print("------------------------- Learning Phase -----------------------------") + print("start good clients requests") + p_good_client = subprocess.Popen( + [f"exec {TEST_DATA}/dos/good_clients_xff.sh {ingress_host} {dos_setup.req_url}"], + preexec_fn=os.setsid, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + print("Learning for max 10 minutes") + find_in_log(kube_apis, log_loc, syslog_pod, test_namespace, 600, "learning_confidence=\"Ready\"") + + print("------------------------- Attack -----------------------------") + print("start bad clients requests") + p_attack = subprocess.Popen( + [f"exec {TEST_DATA}/dos/bad_clients_xff.sh {ingress_host} {dos_setup.req_url}"], + preexec_fn=os.setsid, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + print("Attack for 300 seconds") + wait_before_test(300) + + print("Stop Attack") + p_attack.terminate() + + print("wait max 140 seconds after attack stop, to get attack ended") + find_in_log(kube_apis, log_loc, syslog_pod, test_namespace, 140, "attack_event=\"Attack ended\"") + + print("Stop Good Client") + p_good_client.terminate() + + log_contents = get_file_contents(kube_apis.v1, log_loc, syslog_pod, test_namespace) + log_info_dic = log_content_to_dic(log_contents) + + # Analyze the log + no_attack = False + attack_started = False + under_attack = False + attack_ended = False + bad_actor_detected = False + signature_detected = False + health_ok = False + bad_ip = ['1.1.1.1', '1.1.1.2', '1.1.1.3'] + fmt = '%b %d %Y %H:%M:%S' + for log in log_info_dic: + if log['attack_event'] == 'No Attack': + if int(log['dos_attack_id']) == 0 and not no_attack: + no_attack = True + elif log['attack_event'] == 'Attack started': + if int(log['dos_attack_id']) > 0 and not attack_started: + attack_started = True + start_attack_time = datetime.strptime(log['date_time'], fmt) + elif log['attack_event'] == 'Under Attack': + under_attack = True + if not health_ok and float(log['stress_level']) < 0.6: + health_ok = True + health_ok_time = datetime.strptime(log['date_time'], fmt) + elif log['attack_event'] == 'Attack signature detected': + signature_detected = True + elif log['attack_event'] == 'Bad actors detected': + if under_attack: + bad_actor_detected = True + elif log['attack_event'] == 'Bad actor detection': + if under_attack and log['source_ip'] in bad_ip: + bad_ip.remove(log['source_ip']) + elif log['attack_event'] == 'Attack ended': + attack_ended = True + + delete_items_from_yaml(kube_apis, src_syslog_yaml, test_namespace) + delete_items_from_yaml(kube_apis, src_ing_yaml, test_namespace) + + assert ( + no_attack + and attack_started + and under_attack + and attack_ended + and health_ok + and (health_ok_time - start_attack_time).total_seconds() < 150 + and signature_detected + and bad_actor_detected + and len(bad_ip) == 0 + ) + + def test_dos_arbitrator( + self, kube_apis, ingress_controller_prerequisites, crd_ingress_controller_with_dos, dos_setup, + test_namespace + ): + """ + Test App Protect Dos: Check new IC pod get learning info + """ + print("------------------------- Deploy Syslog -----------------------------") + src_syslog_yaml = f"{TEST_DATA}/dos/dos-syslog.yaml" + log_loc = f"/var/log/messages" + create_items_from_yaml(kube_apis, src_syslog_yaml, test_namespace) + syslog_pod = kube_apis.v1.list_namespaced_pod(test_namespace).items[-1].metadata.name + wait_before_test(30) + print("------------------------- Deploy ingress -----------------------------") + create_ingress_with_dos_annotations( + kube_apis, src_ing_yaml, test_namespace, test_namespace+"/dos-protected" + ) + ingress_host = get_first_ingress_host_from_yaml(src_ing_yaml) + + # print("------------------------- Learning Phase -----------------------------") + print("start good clients requests") + p_good_client = subprocess.Popen( + [f"exec {TEST_DATA}/dos/good_clients_xff.sh {ingress_host} {dos_setup.req_url}"], + preexec_fn=os.setsid, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + print("Learning for max 10 minutes") + find_in_log(kube_apis, log_loc, syslog_pod, test_namespace, 600, "learning_confidence=\"Ready\"") + + print("------------------------- Check new IC pod get info from arbitrator -----------------------------") + ic_ns = ingress_controller_prerequisites.namespace + scale_deployment(kube_apis.v1, kube_apis.apps_v1_api, "nginx-ingress", ic_ns, 2) + while get_pods_amount(kube_apis.v1, "nginx-ingress") is not 3: + print(f"Number of replicas is not 2, retrying...") + wait_before_test() + + print("------------------------- Check if new pod receive info from arbitrator -----------------------------") + print("Wait for 30 seconds") + wait_before_test(30) + + log_contents = get_file_contents(kube_apis.v1, log_loc, syslog_pod, test_namespace) + log_info_dic = log_content_to_dic(log_contents) + + print("Stop Good Client") + p_good_client.terminate() + + learning_units_hostname = [] + for log in log_info_dic: + if log['unit_hostname'] not in learning_units_hostname and log['learning_confidence'] == "Ready": + learning_units_hostname.append(log['unit_hostname']) + + delete_items_from_yaml(kube_apis, src_syslog_yaml, test_namespace) + delete_items_from_yaml(kube_apis, src_ing_yaml, test_namespace) + + assert ( + len(learning_units_hostname) == 2 + ) diff --git a/tests/suite/test_virtual_server_dos.py b/tests/suite/test_virtual_server_dos.py new file mode 100644 index 0000000000..3496179d9e --- /dev/null +++ b/tests/suite/test_virtual_server_dos.py @@ -0,0 +1,462 @@ +import os +import requests +import pytest +import subprocess +from datetime import datetime + +from settings import TEST_DATA +from suite.custom_resources_utils import ( + create_dos_logconf_from_yaml, + create_dos_policy_from_yaml, + create_dos_protected_from_yaml, + delete_dos_policy, + delete_dos_logconf, + delete_dos_protected, +) +from suite.dos_utils import find_in_log, log_content_to_dic +from suite.resources_utils import ( + wait_before_test, + create_example_app, + wait_until_all_pods_are_ready, + create_items_from_yaml, + delete_items_from_yaml, + delete_common_app, + ensure_response_from_backend, + get_file_contents, + replace_configmap_from_yaml, + nginx_reload, + scale_deployment, + get_pods_amount, +) +from suite.vs_vsr_resources_utils import create_virtual_server_from_yaml, delete_virtual_server, \ + get_vs_nginx_template_conf +from suite.yaml_utils import ( + get_first_host_from_yaml, + get_paths_from_vs_yaml, +) + +valid_resp_addr = "Server address:" +valid_resp_name = "Server name:" +invalid_resp_title = "Request Rejected" +invalid_resp_body = "The requested URL was rejected. Please consult with your administrator." +reload_times = {} + +class VirtualServerSetupDos: + def __init__(self, public_endpoint, namespace, vs_host, vs_name, vs_paths): + self.public_endpoint = public_endpoint + self.namespace = namespace + self.vs_host = vs_host + self.vs_name = vs_name + self.backend_1_url = ( + f"http://{public_endpoint.public_ip}:{public_endpoint.port}{vs_paths[0]}" + ) + +@pytest.fixture(scope="class") +def virtual_server_setup_dos( + request, kube_apis, ingress_controller_endpoint, test_namespace +) -> VirtualServerSetupDos: + print( + "------------------------- Deploy Virtual Server Example -----------------------------------" + ) + vs_source = f"{TEST_DATA}/virtual-server-dos/virtual-server.yaml" + vs_name = create_virtual_server_from_yaml(kube_apis.custom_objects, vs_source, test_namespace) + vs_host = get_first_host_from_yaml(vs_source) + vs_paths = get_paths_from_vs_yaml(vs_source) + if request.param["app_type"]: + create_example_app(kube_apis, request.param["app_type"], test_namespace) + wait_until_all_pods_are_ready(kube_apis.v1, test_namespace) + + def fin(): + print("Clean up Virtual Server Example:") + delete_virtual_server(kube_apis.custom_objects, vs_name, test_namespace) + if request.param["app_type"]: + delete_common_app(kube_apis, request.param["app_type"], test_namespace) + + request.addfinalizer(fin) + + return VirtualServerSetupDos( + ingress_controller_endpoint, test_namespace, vs_host, vs_name, vs_paths + ) + + +class DosSetup: + """ + Encapsulate the example details. + Attributes: + req_url (str): + pol_name (str): + log_name (str): + """ + + def __init__(self, req_url, pol_name, log_name): + self.req_url = req_url + self.pol_name = pol_name + self.log_name = log_name + + +@pytest.fixture(scope="class") +def dos_setup( + request, kube_apis, ingress_controller_endpoint, ingress_controller_prerequisites, test_namespace +) -> DosSetup: + """ + Deploy simple application and all the DOS resources under test in one namespace. + + :param request: pytest fixture + :param kube_apis: client apis + :param ingress_controller_endpoint: public endpoint + :param ingress_controller_prerequisites: IC pre-requisites + :param test_namespace: + :return: DosSetup + """ + + print(f"------------- Replace ConfigMap --------------") + replace_configmap_from_yaml( + kube_apis.v1, + ingress_controller_prerequisites.config_map["metadata"]["name"], + ingress_controller_prerequisites.namespace, + f"{TEST_DATA}/dos/nginx-config.yaml" + ) + + req_url = f"http://{ingress_controller_endpoint.public_ip}:{ingress_controller_endpoint.port}/" + + print("------------------------- Deploy vs-logconf -----------------------------") + src_log_yaml = f"{TEST_DATA}/virtual-server-dos/dos-logconf.yaml" + log_name = create_dos_logconf_from_yaml(kube_apis.custom_objects, src_log_yaml, test_namespace) + + print(f"------------------------- Deploy vs-dospolicy ---------------------------") + src_pol_yaml = f"{TEST_DATA}/virtual-server-dos/dos-policy.yaml" + pol_name = create_dos_policy_from_yaml(kube_apis.custom_objects, src_pol_yaml, test_namespace) + + print(f"------------------------- Deploy protected resource ---------------------------") + src_protected_yaml = f"{TEST_DATA}/virtual-server-dos/dos-protected.yaml" + protected_name = create_dos_protected_from_yaml(kube_apis.custom_objects, src_protected_yaml, test_namespace) + + def fin(): + print("Clean up:") + delete_dos_policy(kube_apis.custom_objects, pol_name, test_namespace) + delete_dos_logconf(kube_apis.custom_objects, log_name, test_namespace) + delete_dos_protected(kube_apis.custom_objects, protected_name, test_namespace) + # delete_items_from_yaml(kube_apis, src_webapp_yaml, test_namespace) + # delete_common_app(kube_apis, "dos", test_namespace) + # write_to_json(f"reload-{get_test_file_name(request.node.fspath)}.json", reload_times) + + request.addfinalizer(fin) + + return DosSetup(req_url, pol_name, log_name) + + +@pytest.mark.dos +@pytest.mark.parametrize('crd_ingress_controller_with_dos, virtual_server_setup_dos', + [({"type": "complete", "extra_args": [ + f"-enable-custom-resources", + f"-enable-app-protect-dos", + f"-v=3", + ]}, + {"example": "virtual-server-dos", "app_type": "dos"})], + indirect=True) +class TestDos: + + def test_responses_after_setup(self, kube_apis, crd_ingress_controller_with_dos, dos_setup, + virtual_server_setup_dos): + print("\nStep 1: initial check") + resp = requests.get(virtual_server_setup_dos.backend_1_url, + headers={"host": virtual_server_setup_dos.vs_host}) + assert resp.status_code == 200 + + def test_dos_vs_logs( + self, + kube_apis, + ingress_controller_prerequisites, + crd_ingress_controller_with_dos, + virtual_server_setup_dos, + dos_setup, + test_namespace, + ): + """ + Test app protect logs appear in syslog after sending request to dos enabled route + """ + src_syslog_yaml = f"{TEST_DATA}/virtual-server-dos/syslog.yaml" + log_loc = f"/var/log/messages" + + create_items_from_yaml(kube_apis, src_syslog_yaml, test_namespace) + + print("----------------------- Get syslog pod name ----------------------") + syslog_pod = "" + for thing in kube_apis.v1.list_namespaced_pod(test_namespace).items: + if "syslog" in thing.metadata.name: + syslog_pod = thing.metadata.name + + assert "syslog" in syslog_pod + + wait_before_test(30) + + # items[-1] because syslog pod is last one to spin-up + pod_name = kube_apis.v1.list_namespaced_pod(ingress_controller_prerequisites.namespace).items[-1].metadata.name + # Reload after creating new syslog, TODO: need to remove this one, after fix in the dos module + nginx_reload(kube_apis.v1, pod_name, ingress_controller_prerequisites.namespace) + + print("----------------------- Send request ----------------------") + ensure_response_from_backend(virtual_server_setup_dos.backend_1_url, virtual_server_setup_dos.vs_host) + + response = requests.get(virtual_server_setup_dos.backend_1_url, + headers={"host": virtual_server_setup_dos.vs_host}) + print(response.text) + + wait_before_test(20) + + print("----------------------- Check Logs ----------------------") + print(f'log_loc {log_loc} syslog_pod {syslog_pod} test_namespace {test_namespace}') + log_contents = get_file_contents(kube_apis.v1, log_loc, syslog_pod, test_namespace) + + delete_items_from_yaml(kube_apis, src_syslog_yaml, test_namespace) + + assert 'product="app-protect-dos"' in log_contents + assert f'vs_name="{test_namespace}/dos-protected/name"' in log_contents + assert 'bad_actor' in log_contents + + def test_vs_with_dos_config(self, kube_apis, crd_ingress_controller_with_dos, dos_setup, virtual_server_setup_dos, + test_namespace): + """ + Test to verify Dos annotations in nginx config + """ + conf_directives = [ + f"app_protect_dos_enable on;", + f"app_protect_dos_security_log_enable on;", + f"app_protect_dos_monitor \"dos.example.com\";", + f"app_protect_dos_name \"{test_namespace}/dos-protected/name\";", + f"app_protect_dos_policy_file /etc/nginx/dos/policies/{test_namespace}_{dos_setup.pol_name}.json;", + f"app_protect_dos_security_log_enable on;", + f"app_protect_dos_security_log /etc/nginx/dos/logconfs/{test_namespace}_{dos_setup.log_name}.json syslog:server=syslog-svc.{test_namespace}.svc.cluster.local:514;", + ] + + print("\n confirm response for standard request") + resp = requests.get(virtual_server_setup_dos.backend_1_url, + headers={"host": virtual_server_setup_dos.vs_host}) + assert resp.status_code == 200 + + # items[-1] because syslog pod is last one to spin-up + pod_name = kube_apis.v1.list_namespaced_pod("nginx-ingress").items[-1].metadata.name + + result_conf = get_vs_nginx_template_conf(kube_apis.v1, + virtual_server_setup_dos.namespace, + virtual_server_setup_dos.vs_name, + pod_name, + "nginx-ingress") + + for _ in conf_directives: + assert _ in result_conf + + def test_vs_dos_under_attack_no_learning( + self, kube_apis, ingress_controller_prerequisites, crd_ingress_controller_with_dos, virtual_server_setup_dos, dos_setup, test_namespace + ): + """ + Test App Protect Dos: Block bad clients attack + """ + + print("------------------------- Deploy Syslog -----------------------------") + src_syslog_yaml = f"{TEST_DATA}/virtual-server-dos/syslog.yaml" + log_loc = f"/var/log/messages" + create_items_from_yaml(kube_apis, src_syslog_yaml, test_namespace) + syslog_pod = kube_apis.v1.list_namespaced_pod(test_namespace).items[-1].metadata.name + wait_before_test(30) + # items[-1] because syslog pod is last one to spin-up + pod_name = kube_apis.v1.list_namespaced_pod(ingress_controller_prerequisites.namespace).items[-1].metadata.name + # Reload after creating new syslog, TODO: need to remove this one, after fix in the dos module + nginx_reload(kube_apis.v1, pod_name, ingress_controller_prerequisites.namespace) + + print("------------------------- Attack -----------------------------") + print("start bad clients requests") + p_attack = subprocess.Popen( + [f"exec {TEST_DATA}/dos/bad_clients_xff.sh {virtual_server_setup_dos.vs_host} {dos_setup.req_url}"], + shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + print("Attack for 30 seconds") + wait_before_test(30) + + print("Stop Attack") + p_attack.terminate() + + print("wait max 140 seconds after attack stop, to get attack ended") + find_in_log(kube_apis, log_loc, syslog_pod, test_namespace, 140, "attack_event=\"Attack ended\"") + + log_contents = get_file_contents(kube_apis.v1, log_loc, syslog_pod, test_namespace) + log_info_dic = log_content_to_dic(log_contents) + + # Analyze the log + no_attack = False + attack_started = False + under_attack = False + attack_ended = False + for log in log_info_dic: + # Start with no attack + if log['attack_event'] == "No Attack" and int(log['dos_attack_id']) == 0 and not no_attack: + no_attack = True + # Attack started + elif log['attack_event'] == "Attack started" and int(log['dos_attack_id']) > 0 and not attack_started: + attack_started = True + # Under attack + elif log['attack_event'] == "Under Attack" and int(log['dos_attack_id']) > 0 and not under_attack: + under_attack = True + # Attack ended + elif log['attack_event'] == "Attack ended" and int(log['dos_attack_id']) > 0 and not attack_ended: + attack_ended = True + + delete_items_from_yaml(kube_apis, src_syslog_yaml, test_namespace) + + assert ( + no_attack + and attack_started + and under_attack + and attack_ended + ) + + def test_dos_under_attack_with_learning( + self, kube_apis, ingress_controller_prerequisites, crd_ingress_controller_with_dos, virtual_server_setup_dos, dos_setup, test_namespace + ): + """ + Test App Protect Dos: Block bad clients attack with learning + """ + + print("------------------------- Deploy Syslog -----------------------------") + src_syslog_yaml = f"{TEST_DATA}/virtual-server-dos/syslog.yaml" + log_loc = f"/var/log/messages" + create_items_from_yaml(kube_apis, src_syslog_yaml, test_namespace) + syslog_pod = kube_apis.v1.list_namespaced_pod(test_namespace).items[-1].metadata.name + wait_before_test(30) + # items[-1] because syslog pod is last one to spin-up + pod_name = kube_apis.v1.list_namespaced_pod(ingress_controller_prerequisites.namespace).items[-1].metadata.name + # Reload after creating new syslog, TODO: need to remove this one, after fix in the dos module + nginx_reload(kube_apis.v1, pod_name, ingress_controller_prerequisites.namespace) + + print("------------------------- Learning Phase -----------------------------") + print("start good clients requests") + p_good_client = subprocess.Popen( + [f"exec {TEST_DATA}/dos/good_clients_xff.sh {virtual_server_setup_dos.vs_host} {dos_setup.req_url}"], + preexec_fn=os.setsid, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + print("Learning for max 10 minutes") + find_in_log(kube_apis, log_loc, syslog_pod, test_namespace, 600, "learning_confidence=\"Ready\"") + + print("------------------------- Attack -----------------------------") + print("start bad clients requests") + p_attack = subprocess.Popen( + [f"exec {TEST_DATA}/dos/bad_clients_xff.sh {virtual_server_setup_dos.vs_host} {dos_setup.req_url}"], + preexec_fn=os.setsid, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + print("Attack for 300 seconds") + wait_before_test(300) + + print("Stop Attack") + p_attack.terminate() + + print("wait max 140 seconds after attack stop, to get attack ended") + find_in_log(kube_apis, log_loc, syslog_pod, test_namespace, 140, "attack_event=\"Attack ended\"") + + print("Stop Good Client") + p_good_client.terminate() + + log_contents = get_file_contents(kube_apis.v1, log_loc, syslog_pod, test_namespace) + log_info_dic = log_content_to_dic(log_contents) + + # Analyze the log + no_attack = False + attack_started = False + under_attack = False + attack_ended = False + bad_actor_detected = False + signature_detected = False + health_ok = False + bad_ip = ['1.1.1.1', '1.1.1.2', '1.1.1.3'] + fmt = '%b %d %Y %H:%M:%S' + for log in log_info_dic: + if log['attack_event'] == 'No Attack': + if int(log['dos_attack_id']) == 0 and not no_attack: + no_attack = True + elif log['attack_event'] == 'Attack started': + if int(log['dos_attack_id']) > 0 and not attack_started: + attack_started = True + start_attack_time = datetime.strptime(log['date_time'], fmt) + elif log['attack_event'] == 'Under Attack': + under_attack = True + if not health_ok and float(log['stress_level']) < 0.6: + health_ok = True + health_ok_time = datetime.strptime(log['date_time'], fmt) + elif log['attack_event'] == 'Attack signature detected': + signature_detected = True + elif log['attack_event'] == 'Bad actors detected': + if under_attack: + bad_actor_detected = True + elif log['attack_event'] == 'Bad actor detection': + if under_attack and log['source_ip'] in bad_ip: + bad_ip.remove(log['source_ip']) + elif log['attack_event'] == 'Attack ended': + attack_ended = True + + delete_items_from_yaml(kube_apis, src_syslog_yaml, test_namespace) + + assert ( + no_attack + and attack_started + and under_attack + and attack_ended + and health_ok + and (health_ok_time - start_attack_time).total_seconds() < 150 + and signature_detected + and bad_actor_detected + and len(bad_ip) == 0 + ) + + def test_dos_arbitrator( + self, kube_apis, ingress_controller_prerequisites, crd_ingress_controller_with_dos, + virtual_server_setup_dos, dos_setup, test_namespace + ): + """ + Test App Protect Dos: Check new IC pod get learning info + """ + print("------------------------- Deploy Syslog -----------------------------") + src_syslog_yaml = f"{TEST_DATA}/virtual-server-dos/syslog.yaml" + log_loc = f"/var/log/messages" + create_items_from_yaml(kube_apis, src_syslog_yaml, test_namespace) + syslog_pod = kube_apis.v1.list_namespaced_pod(test_namespace).items[-1].metadata.name + wait_before_test(30) + # items[-1] because syslog pod is last one to spin-up + pod_name = kube_apis.v1.list_namespaced_pod(ingress_controller_prerequisites.namespace).items[-1].metadata.name + # Reload after creating new syslog, TODO: need to remove this one, after fix in the dos module + nginx_reload(kube_apis.v1, pod_name, ingress_controller_prerequisites.namespace) + + # print("------------------------- Learning Phase -----------------------------") + print("start good clients requests") + p_good_client = subprocess.Popen( + [f"exec {TEST_DATA}/dos/good_clients_xff.sh {virtual_server_setup_dos.vs_host} {dos_setup.req_url}"], + preexec_fn=os.setsid, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + print("Learning for max 10 minutes") + find_in_log(kube_apis, log_loc, syslog_pod, test_namespace, 600, "learning_confidence=\"Ready\"") + + print("------------------------- Check new IC pod get info from arbitrator -----------------------------") + ic_ns = ingress_controller_prerequisites.namespace + scale_deployment(kube_apis.v1, kube_apis.apps_v1_api, "nginx-ingress", ic_ns, 2) + while get_pods_amount(kube_apis.v1, "nginx-ingress") is not 3: + print(f"Number of replicas is not 2, retrying...") + wait_before_test() + + print("------------------------- Check if new pod receive info from arbitrator -----------------------------") + print("Wait for 30 seconds") + wait_before_test(30) + + log_contents = get_file_contents(kube_apis.v1, log_loc, syslog_pod, test_namespace) + log_info_dic = log_content_to_dic(log_contents) + + print("Stop Good Client") + p_good_client.terminate() + + learning_units_hostname = [] + for log in log_info_dic: + if log['unit_hostname'] not in learning_units_hostname and log['learning_confidence'] == "Ready": + learning_units_hostname.append(log['unit_hostname']) + + delete_items_from_yaml(kube_apis, src_syslog_yaml, test_namespace) + + assert ( + len(learning_units_hostname) == 2 + )