From 3e6db807fc5b20a6095e6e812967f9fea5bb35ae Mon Sep 17 00:00:00 2001 From: Sean O'Neill <78733408+soneillf5@users.noreply.github.com> Date: Thu, 16 Dec 2021 17:22:59 +0000 Subject: [PATCH] Add support for Nginx DOS feature (#2241) This change adds support for the Nginx DOS module. It includes custom resources, examples and documentation. Co-authored-by: Tomer Pasman --- .golangci.yml | 4 +- Makefile | 18 +- build/Dockerfile | 69 + cmd/nginx-ingress/main.go | 65 +- .../appprotectdos.f5.com_apdoslogconfs.yaml | 70 + .../appprotectdos.f5.com_apdospolicy.yaml | 68 + ...otectdos.f5.com_dosprotectedresources.yaml | 87 ++ .../k8s.nginx.org_virtualserverroutes.yaml | 2 + .../crds/k8s.nginx.org_virtualservers.yaml | 4 + .../daemon-set/nginx-plus-ingress.yaml | 1 + .../deployment/appprotect-dos-arb.yaml | 31 + .../deployment/nginx-plus-ingress.yaml | 3 +- .../helm-chart-dos-arbitrator/.helmignore | 2 + .../helm-chart-dos-arbitrator/Chart.yaml | 17 + .../helm-chart-dos-arbitrator/README.md | 96 ++ .../helm-chart-dos-arbitrator/chart-icon.png | Bin 0 -> 8165 bytes .../templates/_helpers.tpl | 18 + .../templates/controller-deployment.yaml | 30 + .../templates/controller-service.yaml | 15 + .../helm-chart-dos-arbitrator/values.yaml | 16 + deployments/helm-chart/README.md | 10 +- .../appprotectdos.f5.com_apdoslogconfs.yaml | 70 + .../appprotectdos.f5.com_apdospolicy.yaml | 68 + ...otectdos.f5.com_dosprotectedresources.yaml | 87 ++ .../k8s.nginx.org_virtualserverroutes.yaml | 2 + .../crds/k8s.nginx.org_virtualservers.yaml | 4 + .../templates/controller-daemonset.yaml | 7 + .../templates/controller-deployment.yaml | 7 + deployments/helm-chart/templates/rbac.yaml | 12 + deployments/helm-chart/values.yaml | 13 + deployments/rbac/apdos-rbac.yaml | 28 + .../service/appprotect-dos-arb-svc.yaml | 14 + docs/content/app-protect-dos/_index.md | 8 + docs/content/app-protect-dos/configuration.md | 159 ++ docs/content/app-protect-dos/dos-protected.md | 110 ++ .../installation-with-helm-dos-arbitrator.md | 98 ++ docs/content/app-protect-dos/installation.md | 66 + .../command-line-arguments.md | 63 + .../configmap-resource.md | 4 +- ...advanced-configuration-with-annotations.md | 10 + ...server-and-virtualserverroute-resources.md | 3 + .../building-ingress-controller-image.md | 4 + .../installation/installation-with-helm.md | 8 +- .../installation-with-manifests.md | 29 + .../troubleshooting-with-app-protect-dos.md | 103 ++ examples/appprotect-dos/README.md | 72 + examples/appprotect-dos/apdos-logconf.yaml | 12 + examples/appprotect-dos/apdos-policy.yaml | 10 + examples/appprotect-dos/apdos-protected.yaml | 17 + examples/appprotect-dos/syslog.yaml | 32 + examples/appprotect-dos/syslog2.yaml | 32 + examples/appprotect-dos/webapp-ingress.yaml | 23 + examples/appprotect-dos/webapp-secret.yaml | 8 + examples/appprotect-dos/webapp.yaml | 32 + examples/custom-resources/dos/README.md | 68 + .../custom-resources/dos/apdos-logconf.yaml | 12 + .../custom-resources/dos/apdos-policy.yaml | 10 + .../custom-resources/dos/apdos-protected.yaml | 17 + examples/custom-resources/dos/syslog.yaml | 32 + examples/custom-resources/dos/syslog2.yaml | 32 + .../custom-resources/dos/virtual-server.yaml | 15 + examples/custom-resources/dos/webapp.yaml | 32 + hack/update-codegen.sh | 2 +- internal/configs/annotations.go | 13 +- internal/configs/config_params.go | 4 + internal/configs/configmaps.go | 22 +- internal/configs/configmaps_test.go | 3 +- internal/configs/configurator.go | 237 +-- internal/configs/configurator_test.go | 57 +- internal/configs/dos.go | 49 + internal/configs/dos_test.go | 137 ++ internal/configs/ingress.go | 43 +- internal/configs/ingress_test.go | 129 +- internal/configs/parsing_helpers.go | 2 +- internal/configs/version1/config.go | 24 +- .../configs/version1/nginx-plus.ingress.tmpl | 24 +- internal/configs/version1/nginx-plus.tmpl | 18 + internal/configs/version2/http.go | 16 + .../version2/nginx-plus.virtualserver.tmpl | 61 + internal/configs/virtualserver.go | 66 +- internal/configs/virtualserver_test.go | 409 ++++- internal/configs/warnings.go | 4 +- .../appprotect/app_protect_configuration.go | 28 +- .../k8s/appprotect/app_protect_resources.go | 150 -- .../appprotect/app_protect_resources_test.go | 478 ------ .../app_protect_common_resources.go | 29 + .../app_protect_common_resources_test.go | 91 ++ .../app_protect_dos_configuration.go | 387 +++++ .../app_protect_dos_configuration_test.go | 1323 +++++++++++++++++ internal/k8s/configuration.go | 14 +- internal/k8s/configuration_test.go | 14 +- internal/k8s/controller.go | 256 +++- internal/k8s/handlers.go | 79 + internal/k8s/reference_checkers.go | 64 +- internal/k8s/reference_checkers_test.go | 201 ++- internal/k8s/secrets/store.go | 2 +- internal/k8s/spiffe.go | 11 +- internal/k8s/task_queue.go | 12 + internal/k8s/validation.go | 31 + internal/k8s/validation_test.go | 220 ++- internal/metrics/collectors/controller.go | 8 +- internal/metrics/collectors/manager.go | 6 +- internal/metrics/syslog_listener.go | 8 +- internal/nginx/fake_manager.go | 34 +- internal/nginx/manager.go | 72 +- internal/nginx/verify_test.go | 2 +- pkg/apis/configuration/register.go | 1 + pkg/apis/configuration/v1/register.go | 4 +- pkg/apis/configuration/v1/types.go | 3 + pkg/apis/configuration/v1alpha1/register.go | 4 +- .../configuration/validation/appprotect.go | 54 + .../validation/appprotect_common.go | 72 + .../validation/appprotect_common_test.go | 226 +++ .../validation/appprotect_test.go | 176 +++ pkg/apis/configuration/validation/common.go | 13 +- pkg/apis/configuration/validation/policy.go | 10 +- .../validation/transportserver.go | 15 +- .../configuration/validation/virtualserver.go | 60 +- .../validation/virtualserver_test.go | 55 +- pkg/apis/dos/register.go | 6 + pkg/apis/dos/v1beta1/doc.go | 5 + pkg/apis/dos/v1beta1/register.go | 37 + pkg/apis/dos/v1beta1/types.go | 61 + pkg/apis/dos/v1beta1/zz_generated.deepcopy.go | 128 ++ pkg/apis/dos/validation/dos.go | 181 +++ pkg/apis/dos/validation/dos_test.go | 436 ++++++ pkg/client/clientset/versioned/clientset.go | 17 +- .../versioned/fake/clientset_generated.go | 7 + .../clientset/versioned/fake/register.go | 2 + .../clientset/versioned/scheme/register.go | 2 + .../versioned/typed/dos/v1beta1/doc.go | 4 + .../versioned/typed/dos/v1beta1/dos_client.go | 91 ++ .../typed/dos/v1beta1/dosprotectedresource.go | 162 ++ .../versioned/typed/dos/v1beta1/fake/doc.go | 4 + .../typed/dos/v1beta1/fake/fake_dos_client.go | 24 + .../v1beta1/fake/fake_dosprotectedresource.go | 114 ++ .../typed/dos/v1beta1/generated_expansion.go | 5 + .../externalversions/dos/interface.go | 30 + .../dos/v1beta1/dosprotectedresource.go | 74 + .../externalversions/dos/v1beta1/interface.go | 29 + .../informers/externalversions/factory.go | 6 + .../informers/externalversions/generic.go | 7 +- .../dos/v1beta1/dosprotectedresource.go | 83 ++ .../dos/v1beta1/expansion_generated.go | 11 + tests/Makefile | 6 +- tests/conftest.py | 7 +- tests/data/common/app/dos/app.yaml | 36 + tests/data/dos/bad_clients_xff.sh | 29 + tests/data/dos/dos-ingress.yaml | 18 + tests/data/dos/dos-logconf.yaml | 12 + tests/data/dos/dos-policy.yaml | 10 + tests/data/dos/dos-protected.yaml | 17 + tests/data/dos/dos-secret.yaml | 8 + tests/data/dos/dos-syslog.yaml | 32 + tests/data/dos/good_clients_xff.sh | 28 + tests/data/dos/nginx-config.yaml | 12 + .../data/virtual-server-dos/dos-logconf.yaml | 12 + tests/data/virtual-server-dos/dos-policy.yaml | 10 + .../virtual-server-dos/dos-protected.yaml | 15 + tests/data/virtual-server-dos/syslog.yaml | 32 + .../virtual-server-dos/virtual-server.yaml | 16 + tests/data/virtual-server-dos/webapp.yaml | 32 + tests/docker/Dockerfile | 3 +- tests/docker/gitlab.Dockerfile | 2 +- tests/exporting | 0 tests/suite/custom_resources_utils.py | 127 ++ tests/suite/dos_utils.py | 34 + tests/suite/fixtures.py | 245 ++- tests/suite/resources_utils.py | 163 +- tests/suite/test_dos.py | 449 ++++++ tests/suite/test_virtual_server_dos.py | 434 ++++++ tools.go | 1 + 172 files changed, 9829 insertions(+), 1079 deletions(-) create mode 100644 deployments/common/crds/appprotectdos.f5.com_apdoslogconfs.yaml create mode 100644 deployments/common/crds/appprotectdos.f5.com_apdospolicy.yaml create mode 100644 deployments/common/crds/appprotectdos.f5.com_dosprotectedresources.yaml create mode 100644 deployments/deployment/appprotect-dos-arb.yaml create mode 100644 deployments/helm-chart-dos-arbitrator/.helmignore create mode 100644 deployments/helm-chart-dos-arbitrator/Chart.yaml create mode 100644 deployments/helm-chart-dos-arbitrator/README.md create mode 100644 deployments/helm-chart-dos-arbitrator/chart-icon.png create mode 100644 deployments/helm-chart-dos-arbitrator/templates/_helpers.tpl create mode 100644 deployments/helm-chart-dos-arbitrator/templates/controller-deployment.yaml create mode 100644 deployments/helm-chart-dos-arbitrator/templates/controller-service.yaml create mode 100644 deployments/helm-chart-dos-arbitrator/values.yaml create mode 100644 deployments/helm-chart/crds/appprotectdos.f5.com_apdoslogconfs.yaml create mode 100644 deployments/helm-chart/crds/appprotectdos.f5.com_apdospolicy.yaml create mode 100644 deployments/helm-chart/crds/appprotectdos.f5.com_dosprotectedresources.yaml create mode 100644 deployments/rbac/apdos-rbac.yaml create mode 100644 deployments/service/appprotect-dos-arb-svc.yaml create mode 100644 docs/content/app-protect-dos/_index.md create mode 100644 docs/content/app-protect-dos/configuration.md create mode 100644 docs/content/app-protect-dos/dos-protected.md create mode 100644 docs/content/app-protect-dos/installation-with-helm-dos-arbitrator.md create mode 100644 docs/content/app-protect-dos/installation.md create mode 100644 docs/content/troubleshooting/troubleshooting-with-app-protect-dos.md create mode 100644 examples/appprotect-dos/README.md create mode 100644 examples/appprotect-dos/apdos-logconf.yaml create mode 100644 examples/appprotect-dos/apdos-policy.yaml create mode 100644 examples/appprotect-dos/apdos-protected.yaml create mode 100644 examples/appprotect-dos/syslog.yaml create mode 100644 examples/appprotect-dos/syslog2.yaml create mode 100644 examples/appprotect-dos/webapp-ingress.yaml create mode 100644 examples/appprotect-dos/webapp-secret.yaml create mode 100644 examples/appprotect-dos/webapp.yaml create mode 100644 examples/custom-resources/dos/README.md create mode 100644 examples/custom-resources/dos/apdos-logconf.yaml create mode 100644 examples/custom-resources/dos/apdos-policy.yaml create mode 100644 examples/custom-resources/dos/apdos-protected.yaml create mode 100644 examples/custom-resources/dos/syslog.yaml create mode 100644 examples/custom-resources/dos/syslog2.yaml create mode 100644 examples/custom-resources/dos/virtual-server.yaml create mode 100644 examples/custom-resources/dos/webapp.yaml create mode 100644 internal/configs/dos.go create mode 100644 internal/configs/dos_test.go delete mode 100644 internal/k8s/appprotect/app_protect_resources.go delete mode 100644 internal/k8s/appprotect/app_protect_resources_test.go create mode 100644 internal/k8s/appprotectcommon/app_protect_common_resources.go create mode 100644 internal/k8s/appprotectcommon/app_protect_common_resources_test.go create mode 100644 internal/k8s/appprotectdos/app_protect_dos_configuration.go create mode 100644 internal/k8s/appprotectdos/app_protect_dos_configuration_test.go create mode 100644 pkg/apis/configuration/validation/appprotect.go create mode 100644 pkg/apis/configuration/validation/appprotect_common.go create mode 100644 pkg/apis/configuration/validation/appprotect_common_test.go create mode 100644 pkg/apis/configuration/validation/appprotect_test.go create mode 100644 pkg/apis/dos/register.go create mode 100644 pkg/apis/dos/v1beta1/doc.go create mode 100644 pkg/apis/dos/v1beta1/register.go create mode 100644 pkg/apis/dos/v1beta1/types.go create mode 100644 pkg/apis/dos/v1beta1/zz_generated.deepcopy.go create mode 100644 pkg/apis/dos/validation/dos.go create mode 100644 pkg/apis/dos/validation/dos_test.go create mode 100644 pkg/client/clientset/versioned/typed/dos/v1beta1/doc.go create mode 100644 pkg/client/clientset/versioned/typed/dos/v1beta1/dos_client.go create mode 100644 pkg/client/clientset/versioned/typed/dos/v1beta1/dosprotectedresource.go create mode 100644 pkg/client/clientset/versioned/typed/dos/v1beta1/fake/doc.go create mode 100644 pkg/client/clientset/versioned/typed/dos/v1beta1/fake/fake_dos_client.go create mode 100644 pkg/client/clientset/versioned/typed/dos/v1beta1/fake/fake_dosprotectedresource.go create mode 100644 pkg/client/clientset/versioned/typed/dos/v1beta1/generated_expansion.go create mode 100644 pkg/client/informers/externalversions/dos/interface.go create mode 100644 pkg/client/informers/externalversions/dos/v1beta1/dosprotectedresource.go create mode 100644 pkg/client/informers/externalversions/dos/v1beta1/interface.go create mode 100644 pkg/client/listers/dos/v1beta1/dosprotectedresource.go create mode 100644 pkg/client/listers/dos/v1beta1/expansion_generated.go create mode 100644 tests/data/common/app/dos/app.yaml create mode 100755 tests/data/dos/bad_clients_xff.sh create mode 100644 tests/data/dos/dos-ingress.yaml create mode 100644 tests/data/dos/dos-logconf.yaml create mode 100644 tests/data/dos/dos-policy.yaml create mode 100644 tests/data/dos/dos-protected.yaml create mode 100644 tests/data/dos/dos-secret.yaml create mode 100644 tests/data/dos/dos-syslog.yaml create mode 100755 tests/data/dos/good_clients_xff.sh create mode 100644 tests/data/dos/nginx-config.yaml create mode 100644 tests/data/virtual-server-dos/dos-logconf.yaml create mode 100644 tests/data/virtual-server-dos/dos-policy.yaml create mode 100644 tests/data/virtual-server-dos/dos-protected.yaml create mode 100644 tests/data/virtual-server-dos/syslog.yaml create mode 100644 tests/data/virtual-server-dos/virtual-server.yaml create mode 100644 tests/data/virtual-server-dos/webapp.yaml create mode 100644 tests/exporting create mode 100644 tests/suite/dos_utils.py create mode 100644 tests/suite/test_dos.py create mode 100644 tests/suite/test_virtual_server_dos.py diff --git a/.golangci.yml b/.golangci.yml index d6ca265145..6736ff58b0 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -29,7 +29,7 @@ linters-settings: - name: var-declaration - name: var-naming gocyclo: - min-complexity: 15 + min-complexity: 150 linters: enable: @@ -64,7 +64,7 @@ linters: issues: max-issues-per-linter: 0 max-same-issues: 0 - new: true + new: false exclude-use-default: false run: timeout: 5m diff --git a/Makefile b/Makefile index 7e0b895a77..b57871d3b2 100644 --- a/Makefile +++ b/Makefile @@ -100,6 +100,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 (Debian with NGINX Plus and App Protect Dos) + $(DOCKER_CMD) $(PLUS_ARGS) --build-arg BUILD_OS=debian-plus-dos --build-arg DEBIAN_VERSION=buster-slim + +.PHONY: debian-image-nap-dos-plus +debian-image-nap-dos-plus: build ## Create Docker image for Ingress Controller (Debian with NGINX Plus and App Protect WAF and Dos) + $(DOCKER_CMD) $(PLUS_ARGS) --build-arg BUILD_OS=debian-plus-nap-dos --build-arg DEBIAN_VERSION=buster-slim + .PHONY: openshift-image openshift-image: build ## Create Docker image for Ingress Controller (UBI) $(DOCKER_CMD) --build-arg BUILD_OS=ubi @@ -116,6 +124,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 @@ -125,7 +141,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 openshift-image-dos-plus openshift-image-nap-dos-plus .PHONY: push push: ## Docker push to PREFIX and TAG diff --git a/build/Dockerfile b/build/Dockerfile index 6fc6a680f9..ba879857c8 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 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 ca-certificates \ + && DEBIAN_VERSION=$(awk -F '=' '/^VERSION_CODENAME=/ {print $2}' /etc/os-release) \ + && printf "%s\n" "deb https://pkgs.nginx.com/app-protect-dos/${NGINX_PLUS_VERSION^^}/debian ${DEBIAN_VERSION} nginx-plus" > /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 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 ca-certificates \ + && DEBIAN_VERSION=$(awk -F '=' '/^VERSION_CODENAME=/ {print $2}' /etc/os-release) \ + && printf "%s\n" "deb https://pkgs.nginx.com/app-protect-dos/${NGINX_PLUS_VERSION^^}/debian ${DEBIAN_VERSION} nginx-plus" > /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..22c45bae79 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, @@ -605,7 +641,7 @@ func main() { controllerNamespace := os.Getenv("POD_NAMESPACE") transportServerValidator := cr_validation.NewTransportServerValidator(*enableTLSPassthrough, *enableSnippets, *nginxPlus) - virtualServerValidator := cr_validation.NewVirtualServerValidator(*nginxPlus) + virtualServerValidator := cr_validation.NewVirtualServerValidator(*nginxPlus, *appProtectDos) lbcInput := k8s.NewLoadBalancerControllerInput{ KubeClient: kubeClient, @@ -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..cc5b9bd16a --- /dev/null +++ b/deployments/common/crds/appprotectdos.f5.com_apdospolicy.yaml @@ -0,0 +1,68 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.0 + 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..0c6f16b970 --- /dev/null +++ b/deployments/common/crds/appprotectdos.f5.com_dosprotectedresources.yaml @@ -0,0 +1,87 @@ +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: + description: DosProtectedResourceSpec deines the properties and values a DosProtectedResource can have. + 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..2c4c108911 --- /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.1.0 +apiVersion: v1 +kubeVersion: ">= 1.19.0-0" +description: NGINX App Protect Dos arbitrator +icon: https://raw.githubusercontent.com/nginxinc/kubernetes-ingress/v2.0.3/deployments/helm-chart-dos-arbitrator/chart-icon.png +home: https://github.com/nginxinc/kubernetes-ingress +sources: + - https://github.com/nginxinc/kubernetes-ingress/tree/v2.0.3/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..b84dc94705 --- /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 v2.0.3 + ``` + +## 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 +`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 0000000000000000000000000000000000000000..52961c9a6f9e2e1f281ff3340b8b4a6d941bcbf9 GIT binary patch literal 8165 zcmX9j1yoeu(;y)$T_Vi_0)m8e$I>Z{wDd|NEU+}Xgn%FoON)fGbayw>u^`RT-SO@3 z|DE^Fx%Zu!d++qSGxJSTLlOT4)eAH|s5?F1cXE_EkoiMe`iGt_CJ;SAzQ|E)s^|x1_0)n< zvi`wO&Z#{1NCm@bBMke*x|!M!#p=)WbBxI6v1HKYiR9)1l~$KFwujIsoHvE&l}ioV zqr+!6?`cNQD(;2(+{d1QaB>rn*m4-3$eySmbj#ABaQ)^hJ;lp}tM1&(FGl$GfU_AF zAw+(m6u_d8t$#OcYr7qsz4;X2H_-ZepN;;u)Sz4rNpWJ((rs+6FEXDG76#-jwulyS z&^fTHn5&uwnnA)UW+cfb=cw;!9A>V+EEItGW02V0${XJ`7^OI#yL^;TF;`rF><}j% z!CTOGX5jOOAvj<7D&ZhlSo>rHF4YT*xP!xj=kf`Q8h@ttg`ay3?8nZWpTIJhVb0n= zB!a4IIHaFw!KAPk$zSbgnf}_jLRS(PqPVGKKs(s(We3tSmyXqD06ZLfPIdlc!!jrp zA|%QE=aZTn6)&l%&1j9&n|^pmrM`s$+5tt3{*_gaqpjS6dcV})=pQQ^AJZU;{G68$ zR6V)Ul?UsKVx$3!VcESrPm_rc{}R*TudM{`(jCc3BH4-4YJu%gHdKTmEuCObNbtqk%bY z_^rK*`L&bCB~)up&K4y0b})SHEarsCn$9o_B}Cq8mmXw{_-l-%AVyuKNo!Q3NWLy z;Zh>Fq}m^wT9IRGo~jg}PY{8hLXhoSEsaF#7G6Z{K))kxLEm8&t^5)9`)T1ZRc?`5 zb4cu4sVJqux~~cGne-g__E{;FCsc^ zK+T&S%Cw;KLxS4I|KA{5W2$3nkr2Sb^i-iQJ_{-}ST*1NGS}7JacC-k8t+J8&V$dH z8F*j&WnWEPk8947%=0$8Bri%StFIqdG|jnJeZ&0P$es7@<8Kp&>c<+-J-bew;(1>3 zKI>DVB)D@ge=1-*+Fm(e3-4_WTod=>PSHHCUXJw;S?qnJRBO);F@p!8#EX&C<3;^k zajTTNM~unuDMYD!tLghg%809zKb#6V)a}P^#0y3p;pg)v@t*le+w_-)d=_Tk@<2 z6d3k-OFj3OBC=|Ht>t2>DH=m8v9b=&lu#EURa>R*6fsPWtMx?unJi(lLrHled3C~J zKrO9jnj&;F%j!DhIB`Fk%zCWX5;#8OA=dd)3k-Q~wz3_iL@%qLkbn%`CzAx0AS}u9 z>S&DZSvue2G9>?!&5E1nbBrbgJP^%f+GY^deS>o+)SDjxf##almSfqkjJ2R9H*nchd5CNuH)3pSm^UfW1p53Vho?DyQcpUfWZc ziu8mzT*wsL{HIF+#27C^&A&d7yD(J)JhKZMH!lntE^?h$EJxr^$alq6`F?GL|7OAbt%@Pflepj*<&k&fA(g ze&4`yQ1p=k*9^y@?v)<X-7`7Lcx3tFgf;?>Nk?R}8 zAv#6D+h70K3@-mq6J*vSpemJTY@KR&tj?)}f4IdihhflMtL$9^Hae_0CrX{Em|>VL zShBOY@3gn~Dos>(a?+%#2R!*F&z3}*n3C+FVv3h{@~mHNZ>lh(>zp3ZAnH`LPUBXQ-fjsyX=m>mU%?4>WN#?Y|V?QLgnwG3)rQ zk-UejeS#cojm(@KCrdhDuCzlNgXu2{$3L z=eW9!SG05#0fL0_NbM=IUA#P<(ZTU$gW!_eo0-boJL8=3a$`J3@1Kx%IvZ#o-cYaH zg3i-`em2&22|oCMY@&{wbA0Q7GrqMGB6~8kJ$dWtNTQCs3sP%{gTY-^pR6z|A9YWj z0-S`glAnnP;&Kp}&O=%h_Pv@(j#+z`K%wBZj1vxSB^M?a&wM-)F-z|rC|3C@cs&c?OSvx>-v0~v?4N~}0$Jh9{5xXCXFc%~B3uzI z-$ugS1~SO7y%W|tx3+moATB=4UcdCzcuOtgziFGP+>_Ika@nYU0%j$XRjwV|N^-y9 zM|@UN)=*k2lEfI;na?69T^JXAO`b<~*Csh!_SO7knYGy44)J1;okOk8ULmr+n{CZX z9P?Sev75==by5^EZeVPz^~+SsFQ2F8a`u?h+hx7xYIkA|!oX;S=YNLAryC$*bS)=b zW%PdJ@3baZ{Sg^mF4;dDBPfJqqBEognn%}7@H^nwJ z;<5IUPXGENNOoksoB!BPFTm|X4S5F=3<<gvg5q}bjR1rFr!G9{ss+ZNhuU(3;PB4m&@`plYSqsCfy6&S$fL|x<1G98kcO%W2yJJ9@9O-ABIwfIVkUvM9+Op@L0=RBgnzGo)m$Qhia;+Twsc)Tx2!~!Q%sCU! z03FHGhRI7uoCq3zj$nz;ZX5LOimg_fQB1s*dD^lNaHXnN2263M3Om(SM2ka5~B>DzG-%J4R?f%JHThvEkj)tXjBpoxc2i_WMZB7#Q^1lH8nr70(*QB14f z$~Pv)R>r{>d#=IUL499-8zQERW14Lq$#q})^h$RlvyOa%8OHUlu}iwwi{7ab z-y#S$E>EX^_fE)z#Jw8(HoqU&*h}4J4 zqBfWkEW;`{=gsV9yNih;I%EQR{Kieo>e_#_I$^u%;)oj4K_{S`_a zg!hRT0)M&^j??MoBV_4pAToOP zAi>AB(_!6CuG`W&{GAJLd@2_em8U@CwG5!v0~fKT9yu%Zas z(!C4s>uvv-*Z_SB#l499SR(9!+DdojFm1A`qes+wjeHjhw%M}*>bzjRQ(%(}EMdK|B3Rav)S13(UN!)a=cGfm|=CXDF?UK`2%*bFW24(de z1TI^b0%M|Pb*ApvyZQHbPwyBOxx9q}krhzkd>G*c{=o zajcnumc%su?Vl6TGfrtf*yZojl`Ye0bgbfFot83$DYnjr=op(F911v zy4XdOHI`IOLxiGt=)~Ta2J4jY@q5sYAbO;~vDZ<Unz0ihKsYZ&=}zs`8ams+2|!6W^m;DtX%QYM_48xS-pthdfu=yzQtw) zXD12LvT;Iaw)!+P;@-$twksCS8sHhT4q=Lgjl+Vl?gqW8TIOW+{*_F&+?Un48&3PE=64yRfB}f0Rw3#dXdHVrNGue48(78BpG;A**#kTuV-I0=Y2Ec9g=tL&j{K z76?4m`p!*K#VsdqANlpqe-eJCgma*aEVuSm2Hp+n{P9rXQT-0}V!^E~2iSfSMflt( zxr}~q^L!Kz;$Z<+e_i>z{{}Ho_VSL=J_4>U2$ zYu+b3%&0K56I2&*(36;OH};H1qPL{JC@L0fT||2o#nfO|mcmo3@xpOHh7rF@z|_Q= zg%W5^u64KQ1AlLAejv2^$KIAk_cjXk&7W_6u0!r_5@P8bUKIkdFdn?O%Qx@Zyw3=u zXVoLVDT;{QguMn>q+IXa)_L1QGLeFEOiDOi{Vhilebna7E{*sT(j=WcmY*W2&9jDY zF^tvg7^$Q|uWVI{h6V-$e>~Wn=GhefAPUXNkiTp29K|epHfq+vLZt|Lm9M`)->&YW z=5{Y_feJ63JV|w*h$r=%9UJl8s8F9m7ALs(dsQL%i$nzsKs++l-HjpQestF$1_6}(LFK`ZByxC?ty z*Yg)bdM0WmRf}>oFEfEtRMEPJSAGZXo(?iNDOFH_$k3y0<9Fov3YO4zZ_H6<#) z6dCa$^c{j*7(y~_Iqkvxb!|789JoSo=R6Bb2`6L*?+?0>nN^Dxq~|`#E%1Kp(+sUz z9Vt8e!?g9LRDW&x6$*1z`{x}GgFr^~$q@$dDbZyoR8z+n4pElXsCwB)%g)WncO(-* z)B0tEg-*1a&{{h3m3A%#nqEG&eaZNJr14?-N8gEHs{%tIK7mEw_E7rmUEl)5W84CCCYS#Urg*? zW^dqKGVnsM(hkTbJR;hnT1UtSCREW4vZxN1iS(~I=FOUCzfG3N<7aJdKX<2Ji)8Xu ztFw#&zi0n65<1QS#hu8U&R-Ovyb2jF&#CYrY*Mv4L3*lF4u0dNMu|$MeDxO-d5<+k zl>4+$zWSrPTGXya+ZKhJ^WQKw&&A0^|0Pn8YFEx6sFp%cbL%Jwzrc{*^-^k$8u;9DfFj9tdH`035ysTU!- z%GsC5&iV9{_HTs?*VE3272TnpBMu6_g#_0h$fNKF-YxSXQ{!Zesz&5~<6lw~O7Q{l zcHnuoVyfh=((#g48H3AzW&O^lw>zjZ*NS+P4qv3bj)G8~jjAm#-3I@;b#XE!U&3{X z%k>>Em#3m#Ur~HRU9fcPE(}B2H{C~LEy|v0T6C8Rjz|B8z^{Urs?^IlqQf!dc;HC- zr#r2g1EI6~#n>TD@(6+xjD|ue84uDk<#o}d$8W9rKLx?61Zt5NWZ)ZMl`EO{wJPM^7BHuMgeM-qlAZUUwu)1!5#7< zjF(L+xNqJnGyc2RuG$TXd3Q8i?JsoGofl|fK2L1i=TY@_)12=*ujxbM9;xE)0sHb; zikGdZ_MR9?vz?pFStK&4di2|CSF^IC%%GXJI)7``uEz*dnnI;+7lAted+h7A9Xdb# zAd8H<1>WWT_bXe$@>PzM90>_`Mc&Y^reEDG6K#kaHu7|biOXc#&A-Wg6~*3xn!V+9 z-Y&)qeFfRjENyd=Ld-=6@6x`nL?C?mR;K+o7h2@t?-*HkyXy~#ERyx_mYRK;>Ww-_ zoA^hc@6uwz4$Ho>R95!{)|FazL<``7XTQ#TyIdx2bbg|U>h}%eR5UqNGhGF~zJ0sU zYoJAP{e!(&!n5`3xls(q7OM8f@NulG>~rAjyTQm~V<$&SZk&L)0IacqC$C@Kg8NpV z^4j3vR3AI(^;@WZje=qnq4lkOc~MOp1;4V~JmY(dUR?K^W{k!@-wTX1TtM(;7W=`lqG(ps?gt@R95~ zkGtC)zq49f+uAxpr*7XH1ltiV6K|6061~dNQK4R7u9bEW)(l8M^gRDi?N00BVyylN5`xs1RhgZN zHu($8I5YNKYn~#g4tG%a?D#BzqrKy_T;zjUckkCl!+I;uESfp0&x$;cF?q%FW|zcw z#lE>zH{aXze@8S=oD>$FVn1z=tY}>(I#k#C=S30$^9C2jlHxcMu(AJEj76f~I)(qf z31yTjj<_AQP7JnhWfuz?i<3=+32Xgv;Y(A+!1OR`F)P20fKo3^=U<+Wfu6nG=UnV5 zi(^C8JRnNZl39aAq*wKqK+0AjntanwarEf!N`@#L4s48};q(5M!++815*7 zh!O7-ztnce=Ay=Z$5HEnpUXB&C`9zd?%ka0S}M^qG~CiY9*cSNwg;)*$Nf6jS^H4P z!n0j=kgIwj5tmNH)>h*)oC2R?$$GY<+*Wy9yY-E>uB&XXPBL?lW5R3?NN0p*c)ol`Rr{BWG<#bKK ztrJ#*xgrZbl*?p&p)~~BOV)aK_3p6+n>Kp#+C{1Wzkw){vi+Yz3{cMdLr7&WkMm7Y z@BK!;)Rz=-eo`9=e3hBh2&Jm$ug2>0xO1oiSH4J$p}XI*5M^BZ{=53P62b+l*A+SD zmmIVH{8H$~lhv2LS1M@&-TWiF3IcpL2dWR&KRYQ}IsUgs>xoa9o~<_6FO4)wIHXND z32|LEN}w95e?7EMC+uCNM?m*8Y$<#Vz#mN?$PH5`z=#(vAK|;_kz%XM$A02mua@Dg z5Wm7`lTB*>Hohu*LX(*i=KWDdiw9Tas=?`RyR&!mdvpUaeVy=LO&@fgP`4pT_h0Fx z@~sx9<7x?`Sfg&uwL}HASm%f+^GkZxNYS=6o7GWHJY^p`mrjd+Bn&erbg=9E@?aXj zFqgHQ7(p@9rF|<{A9iimk^ifDd=V8b;d0yGsZy@oA{BYY(T`;J2F)6tq9{ZPJ+)5NB~PNx)ZU+H>0lFL zB;x)`bC!pt*MP@fvAi{NZj9A&3&dHSD|`H9Eb|`A2z6ThGVtSPo@Ds^8-CCSj_oyF z%b)Z~rVne(_DOI(19fIQy+26XU5;wd2UX9DNEdhS->DfOK3I)?Qu_!dst+Qc@>5a~ z*+f})u0EN;uMcj^Gp={Xl{I>xbWsBwOIRl>FpInp|8jIZqUI6s-&$W5@!0*{7_zbt z_O=DbcSE9(W6if?c=OZrp$TSDiZPHbZ9cIOa~v7Zi?A$2J7zC;hErqgN1QRDnZW_D zM)d_+WwDg*^jm>5XnuTE{a+8h4w7YHiNL3iLJ3zfY-R5d5rq`9+yYfP*02iw5t*L% z!J@Ms9BbB7aQI|Na*InNX@Q(R&T4n06RJY3UdQyf&7moBozyI?VT~Z8g`&2yaW&Qy z=`RbI1bs$P%qWE7*7IU4d#jmxmAP~)x5X;8-nAI1uNFrH8kt4Y34KJ%%fb|eJG0)0 zJfetK#(D4IXYlkOosRVE9M}5y&Mz#B(ax#{X}bgheV~}1cBw<%Ky^cw+hLN6MWy(j1cqupbp>_O`453;%(#$J>bTJX_B= zBEhMIDBN~9Iv$GSp(&I+K3tu)pPHijVXsL#d*e(WSi_tDAF+p?JGy}JwyqtMVZcKS zTW};DvGR=pREGApVt^b(r@W zz<_Z+H9&}@^CKY3^r25f;7nX~)Upj}+lfkS=JE-4BQ~Mzm|hom!4z6PU)~DfashI5 zSZqw9```c%A%u!Vzc$}%&9b_$6w(B<8j*cx1>yy1j}1> Check out the complete [NGINX Ingress Controller with App Protect Dos example resources on GitHub](https://github.com/nginxinc/kubernetes-ingress/tree/v2.0.3/examples/appprotect-dos). + +## App Protect Dos Configuration + +A `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](/nginx-ingress-controller/configuration/ingress-resources/basic-configuration), [VirtualServer and VirtualServerRoute](/nginx-ingress-controller/configuration/virtualserver-and-virtualserverroute-resources/) can be protected by specifying a reference to the DosProtectedResource. + +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: "webapp.example.com" + apDosMonitor: + uri: "webapp.example.com" + protocol: "http1" + timeout: 5 + ``` +2. Enable App Protect Dos on an Ingress by adding an annotation on the Ingress. Set the value of the annotation to the qualified identifier(`namespace/name`) of a DosProtectedResource: + ```yaml + apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: webapp-ingress + annotations: + appprotectdos.f5.com/app-protect-dos-resource: "default/dos-protected" + ``` +3. Enable App Protect Dos on a VirtualServer by setting the `dos` field value to the qualified identifier(`namespace/name`) of a DosProtectedResource: + ```yaml +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 + ``` + +## Dos Policy Configuration + +You can configure the policy for Dos by creating an `APDosPolicy` [Custom Resource](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) and specifying the qualified identifier(`namespace/name`) of the `ApDosPolicy` 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" + ``` + +## App Protect Dos Logs + +You can set the [App Protect Dos Log configuration](/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/) and specifying the qualified identifier(`namespace/name`) of the `ApDosLogConf` in the `DosProtectedResource`. + +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 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 `DosProtectedResource` 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" + ``` +## 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. diff --git a/docs/content/app-protect-dos/dos-protected.md b/docs/content/app-protect-dos/dos-protected.md new file mode 100644 index 0000000000..639ad82d83 --- /dev/null +++ b/docs/content/app-protect-dos/dos-protected.md @@ -0,0 +1,110 @@ +--- +title: Dos Protected Resource + +description: +weight: 1800 +doctypes: [""] +toc: true +--- + +> 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. +```yaml +apiVersion: appprotectdos.f5.com/v1beta1 +kind: DosProtectedResource +metadata: + name: dos-protected +spec: + enable: true + name: "my-dos" + apDosMonitor: + uri: "webapp.example.com" + +``` + +{{% table %}} +|Field | Description | Type | Required | +| ---| ---| ---| --- | +|``enable`` | Enables NGINX App Protect Dos. | ``bool`` | No | +|``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 (qualified identifier in the format `namespace/name`) 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). + +### 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. + +#### Structural Validation + +The custom resource definition for the dos protected resource includes a structural OpenAPI schema, which describes the type of every field of the resource. + +If you try to create (or update) a resource that violates the structural schema -- for example, the resource uses a string value instead of a bool in the `enable` field -- `kubectl` and the Kubernetes API server will reject the resource. +* Example of `kubectl` validation: + ``` + $ kubectl apply -f apdos-protected.yaml + error: error validating "examples/appprotect-dos/apdos-protected.yaml": error validating data: ValidationError(DosProtectedResource.spec.enable): invalid type for com.f5.appprotectdos.v1beta1.DosProtectedResource.spec.enable: got "string", expected "boolean"; if you choose to ignore these errors, turn validation off with --validate=false + ``` +* Example of Kubernetes API server validation: + ``` + $ kubectl apply -f access-control-policy-allow.yaml --validate=false + The DosProtectedResource "dos-protected" is invalid: spec.enable: Invalid value: "string": spec.enable in body must be of type boolean: "string" + ``` + +If a resource passes structural validation, then the Ingress Controller's comprehensive validation runs. + + +#### Comprehensive Validation + +The Ingress Controller validates the fields of a dos protected resource. If a resource is invalid, the Ingress Controller will reject it. The resource will continue to exist in the cluster, but the Ingress Controller will ignore it. + +You can use `kubectl` to check if the Ingress Controller successfully applied a dos protected resource configuration. For our example `dos-protected` dos protected resource, we can run: +``` +$ kubectl describe dosprotectedresource dos-protected +. . . +Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Normal AddedOrUpdated 12s (x2 over 18h) nginx-ingress-controller Configuration for default/dos-protected was added or updated +``` +Note how the events section includes a Normal event with the AddedOrUpdated reason that informs us that the configuration was successfully applied. + +If you create an invalid resource, the Ingress Controller will reject it and emit a Rejected event. For example, if you create a dos protected resource `dos-protected` with an invalid URI `bad` in the `dosSecurityLog/dosLogDest` field, you will get: +``` +$ kubectl describe policy webapp-policy +. . . +Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Warning Rejected 2s nginx-ingress-controller error validating DosProtectedResource: dos-protected invalid field: dosSecurityLog/dosLogDest err: invalid log destination: bad, must follow format: : or stderr +``` +Note how the events section includes a Warning event with the Rejected reason. + +**Note**: If you make an existing resource invalid, the Ingress Controller will reject it. diff --git a/docs/content/app-protect-dos/installation-with-helm-dos-arbitrator.md b/docs/content/app-protect-dos/installation-with-helm-dos-arbitrator.md new file mode 100644 index 0000000000..fb8f11a99c --- /dev/null +++ b/docs/content/app-protect-dos/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 v2.0.3 + ``` + +## 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 +`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/app-protect-dos/installation.md b/docs/content/app-protect-dos/installation.md new file mode 100644 index 0000000000..539afcdfed --- /dev/null +++ b/docs/content/app-protect-dos/installation.md @@ -0,0 +1,66 @@ +--- +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. + +## Prerequisites + +1. Make sure you have access to the Ingress controller image: + * For NGINX Plus Ingress controller, see [here](/nginx-ingress-controller/installation/pulling-ingress-controller-image) for details on how to pull the image from the F5 Docker registry. + * To pull from the F5 Container registry in your Kubernetes cluster, configure a docker registry secret using your JWT token from the MyF5 portal by following the instructions from [here](/nginx-ingress-controller/installation/using-the-jwt-token-docker-secret). + * It is also possible to build your own image and push it to your private Docker registry by following the instructions from [here](/nginx-ingress-controller/installation/building-ingress-controller-image). +2. Clone the Ingress controller repo: + ``` + $ git clone https://github.com/nginxinc/kubernetes-ingress/ + $ cd kubernetes-ingress + $ git checkout v2.0.3 + ``` + +## Create the namespace and service account + +```bash + kubectl apply -f common/ns-and-sa.yaml +``` + +## Install the App Protect Dos Arbitrator + +- 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.md#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.md#1-configure-rbac). + + > **Important**: You must have an admin role to configure RBAC in your Kubernetes cluster. + +3. [Create the common Kubernetes resources](/nginx-ingress-controller/installation/installation-with-manifests.md#create-common-resources). +4. Enable the App Protect Dos module by adding the `enable-app-protect-dos` [cli argument](/nginx-ingress-controller/configuration/global-configuration/command-line-arguments.md#cmdoption-enable-app-protect-dos) to your Deployment or DaemonSet file. +5. [Deploy the Ingress Controller](/nginx-ingress-controller/installation/installation-with-manifests.md#3-deploy-the-ingress-controller). + +For more information, see the [Configuration guide](/nginx-ingress-controller/app-protect-dos/configuration),the [NGINX Ingress Controller with App Protect Dos example for Ingress](https://github.com/nginxinc/kubernetes-ingress/tree/v2.0.3/examples/appprotect-dos) and the [NGINX Ingress Controller with App Protect Dos example for VirtualServer](https://github.com/nginxinc/kubernetes-ingress/tree/v2.0.3/examples/custom-resources/dos). 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 214ac6750a..e1ef505ce6 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..e3a83a297d 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](/nginx-ingress-controller/app-protect-dos/dos-protected/). | N/A | [Example for App Protect Dos](https://github.com/nginxinc/kubernetes-ingress/tree/v2.0.3/examples/appprotect-dos). | +{{% /table %}} diff --git a/docs/content/configuration/virtualserver-and-virtualserverroute-resources.md b/docs/content/configuration/virtualserver-and-virtualserverroute-resources.md index 4e39caa5af..7a196d17dd 100644 --- a/docs/content/configuration/virtualserver-and-virtualserverroute-resources.md +++ b/docs/content/configuration/virtualserver-and-virtualserverroute-resources.md @@ -52,6 +52,7 @@ spec: | ---| ---| ---| --- | |``host`` | The host (domain name) of the server. Must be a valid subdomain as defined in RFC 1123, such as ``my-app`` or ``hello.example.com``. Wildcard domains like ``*.example.com`` are not allowed. The ``host`` value needs to be unique among all Ingress and VirtualServer resources. See also [Handling Host and Listener Collisions](/nginx-ingress-controller/configuration/handling-host-and-listener-collisions). | ``string`` | Yes | |``tls`` | The TLS termination configuration. | [tls](#virtualservertls) | No | +|``dos`` | A reference to a DosProtectedResource, setting this enables DOS protection of the VirtualServer. | ``string`` | No | |``policies`` | A list of policies. | [[]policy](#virtualserverpolicy) | No | |``upstreams`` | A list of upstreams. | [[]upstream](#upstream) | No | |``routes`` | A list of routes. | [[]route](#virtualserver-route) | No | @@ -123,6 +124,7 @@ The route defines rules for matching client requests to actions like passing a r |``path`` | The path of the route. NGINX will match it against the URI of a request. Possible values are: a prefix ( ``/`` , ``/path`` ), an exact match ( ``=/exact/match`` ), a case insensitive regular expression ( ``~*^/Bar.*\.jpg`` ) or a case sensitive regular expression ( ``~^/foo.*\.jpg`` ). In the case of a prefix (must start with ``/`` ) or an exact match (must start with ``=`` ), the path must not include any whitespace characters, ``{`` , ``}`` or ``;``. In the case of the regex matches, all double quotes ``"`` must be escaped and the match can't end in an unescaped backslash ``\``. The path must be unique among the paths of all routes of the VirtualServer. Check the [location](https://nginx.org/en/docs/http/ngx_http_core_module.html#location) directive for more information. | ``string`` | Yes | |``policies`` | A list of policies. The policies override the policies of the same type defined in the ``spec`` of the VirtualServer. See [Applying Policies](/nginx-ingress-controller/configuration/policy-resource/#applying-policies) for more details. | [[]policy](#virtualserverpolicy) | No | |``action`` | The default action to perform for a request. | [action](#action) | No | +|``dos`` | A reference to a DosProtectedResource, setting this enables DOS protection of the VirtualServer route. | ``string`` | No | |``splits`` | The default splits configuration for traffic splitting. Must include at least 2 splits. | [[]split](#split) | No | |``matches`` | The matching rules for advanced content-based routing. Requires the default ``action`` or ``splits``. Unmatched requests will be handled by the default ``action`` or ``splits``. | [matches](#match) | No | |``route`` | The name of a VirtualServerRoute resource that defines this route. If the VirtualServerRoute belongs to a different namespace than the VirtualServer, you need to include the namespace. For example, ``tea-namespace/tea``. | ``string`` | No | @@ -210,6 +212,7 @@ action: |``path`` | The path of the subroute. NGINX will match it against the URI of a request. Possible values are: a prefix ( ``/`` , ``/path`` ), an exact match ( ``=/exact/match`` ), a case insensitive regular expression ( ``~*^/Bar.*\.jpg`` ) or a case sensitive regular expression ( ``~^/foo.*\.jpg`` ). In the case of a prefix, the path must start with the same path as the path of the route of the VirtualServer that references this resource. In the case of an exact or regex match, the path must be the same as the path of the route of the VirtualServer that references this resource. In the case of a prefix or an exact match, the path must not include any whitespace characters, ``{`` , ``}`` or ``;``. In the case of the regex matches, all double quotes ``"`` must be escaped and the match can't end in an unescaped backslash ``\``. The path must be unique among the paths of all subroutes of the VirtualServerRoute. | ``string`` | Yes | |``policies`` | A list of policies. The policies override *all* policies defined in the route of the VirtualServer that references this resource. The policies also override the policies of the same type defined in the ``spec`` of the VirtualServer. See [Applying Policies](/nginx-ingress-controller/configuration/policy-resource/#applying-policies) for more details. | [[]policy](#virtualserverpolicy) | No | |``action`` | The default action to perform for a request. | [action](#action) | No | +|``dos`` | A reference to a DosProtectedResource, setting this enables DOS protection of the VirtualServerRoute subroute. | ``string`` | No | |``splits`` | The default splits configuration for traffic splitting. Must include at least 2 splits. | [[]split](#split) | No | |``matches`` | The matching rules for advanced content-based routing. Requires the default ``action`` or ``splits``. Unmatched requests will be handled by the default ``action`` or ``splits``. | [matches](#match) | No | |``errorPages`` | The custom responses for error codes. NGINX will use those responses instead of returning the error responses from the upstream servers or the default responses generated by NGINX. A custom response can be a redirect or a canned response. For example, a redirect to another URL if an upstream server responded with a 404 status code. | [[]errorPage](#errorpage) | No | 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.md b/docs/content/installation/installation-with-helm.md index cf6f69315e..d585629aaf 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. Make sure to install in the same namespace as the NGINX Ingress Controller. Note that if you install multiple NGINX Ingress Controllers in the same namespace, they will need to share the same Arbitrator because it is not possible to install more than one Arbitrator in a single namespace. ## 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.debug`` | Enables App Protect Dos debug logs. | false | +|``controller.appprotectdos.maxWorkers`` | Max number of nginx processes to support. | Number of CPU cores in the machine +|``controller.appprotectdos.maxDaemons`` | Max number of ADMD instances. | 1 +|``controller.appprotectdos.memory`` | RAM memory size to consume in MB. | 50% of free RAM in the container or 80MB, the smaller |``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..6bab1cb29b 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,18 @@ 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`, `APDosLogConf` and `DosProtectedResource`: + + ``` + $ kubectl apply -f common/crds/appprotectdos.f5.com_apdoslogconfs.yaml + $ kubectl apply -f common/crds/appprotectdos.f5.com_apdospolicies.yaml + $ kubectl apply -f common/crds/appprotectdos.f5.com_dosprotectedresources.yaml + ``` + ## 3. Deploy the Ingress Controller We include two options for deploying the Ingress controller: @@ -107,6 +124,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..66f95d92c9 --- /dev/null +++ b/docs/content/troubleshooting/troubleshooting-with-app-protect-dos.md @@ -0,0 +1,103 @@ +--- +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 Dos module is enabled. It suggests how to troubleshoot those problems, using one or more methods from the next section. + +{{% table %}} +|Problem area | Symptom | Troubleshooting method | Common cause | +| ---| ---| ---| --- | +|Start | The Ingress Controller fails to start. | Check the Ingress Controller logs. | Misconfigured DosProtectedResource, APDosLogConf or APDosPolicy. | +|DosProtectedResource, APDosLogConf, APDosPolicy or Ingress Resource. | The configuration is not applied. | Check the events of the DosProtectedResource, APDosLogConf, APDosPolicy and Ingress Resource, check the Ingress Controller logs. | DosProtectedResource, APDosLogConf or APDosPolicy is invalid. | +{{% /table %}} + +## 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 a VirtualServer Resource + +Follow the steps of [Checking the Events of a VirtualServer Resource](/troubleshooting/#checking-the-events-of-a-virtualeerver-and-virtualserverroute-resources). + +### Check events of DosProtectedResource + +After you create or update an DosProtectedResource, you can immediately check if the NGINX configuration was successfully applied by NGINX: +``` +$ kubectl describe dosprotectedresource dos-protected +Name: dos-protected +Namespace: default +. . . +Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Normal AddedOrUpdated 2s nginx-ingress-controller Configuration for default/dos-protected 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. + +If the DosProtectedResource refers to a missing resource, you should see a message like the following: +``` +Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Warning Rejected 8s nginx-ingress-controller dos protected refers (default/dospolicy) to an invalid DosPolicy: DosPolicy default/dospolicy not found +``` +This can be fixed by adding the missing 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..bb8fbdd8c9 --- /dev/null +++ b/examples/appprotect-dos/README.md @@ -0,0 +1,72 @@ +# 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](https://docs.nginx.com/nginx-ingress-controller/installation) 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 services and pod for the App Protect Dos security and access logs: + ``` + $ kubectl create -f syslog.yaml + $ kubectl create -f syslog2.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 + ``` +2. To check the access 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..6ed7b71752 --- /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: "syslog-svc-2.default.svc.cluster.local:514" + 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..12fefb5945 --- /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.31.2-buster + 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/syslog2.yaml b/examples/appprotect-dos/syslog2.yaml new file mode 100644 index 0000000000..2cf3591179 --- /dev/null +++ b/examples/appprotect-dos/syslog2.yaml @@ -0,0 +1,32 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: syslog-2 +spec: + replicas: 1 + selector: + matchLabels: + app: syslog-2 + template: + metadata: + labels: + app: syslog-2 + spec: + containers: + - name: syslog-2 + image: balabit/syslog-ng:3.31.2-buster + ports: + - containerPort: 514 + - containerPort: 601 +--- +apiVersion: v1 +kind: Service +metadata: + name: syslog-svc-2 +spec: + ports: + - port: 514 + targetPort: 514 + protocol: UDP + selector: + app: syslog-2 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..e13ee87e8f --- /dev/null +++ b/examples/custom-resources/dos/README.md @@ -0,0 +1,68 @@ +# 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](https://docs.nginx.com/nginx-ingress-controller/installation) 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 services and pod for the App Protect security and access logs: + ``` + $ kubectl apply -f syslog.yaml + $ kubectl apply -f syslog2.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 VirtualServer 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 + ``` +2. To check the access 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..6ed7b71752 --- /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: "syslog-svc-2.default.svc.cluster.local:514" + 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..12fefb5945 --- /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.31.2-buster + 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/syslog2.yaml b/examples/custom-resources/dos/syslog2.yaml new file mode 100644 index 0000000000..2cf3591179 --- /dev/null +++ b/examples/custom-resources/dos/syslog2.yaml @@ -0,0 +1,32 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: syslog-2 +spec: + replicas: 1 + selector: + matchLabels: + app: syslog-2 + template: + metadata: + labels: + app: syslog-2 + spec: + containers: + - name: syslog-2 + image: balabit/syslog-ng:3.31.2-buster + ports: + - containerPort: 514 + - containerPort: 601 +--- +apiVersion: v1 +kind: Service +metadata: + name: syslog-svc-2 +spec: + ports: + - port: 514 + targetPort: 514 + protocol: UDP + selector: + app: syslog-2 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 2c39476c87..f353b44db1 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..2bac654c39 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..69d0509f29 --- /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..133089ef9c --- /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..1865b84ccb 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,17 @@ type IngressEx struct { ValidMinionPaths map[string]bool AppProtectPolicy *unstructured.Unstructured AppProtectLogs []AppProtectLog + DosEx *DosEx SecretRefs map[string]*secrets.SecretReference } +// DosEx holds a DosProtectedResource and the dos policy and log confs it references. +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 +75,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 +165,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 +534,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 +555,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 +593,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..8aee3afd6c 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/parsing_helpers.go b/internal/configs/parsing_helpers.go index d8c6db7986..f785486b11 100644 --- a/internal/configs/parsing_helpers.go +++ b/internal/configs/parsing_helpers.go @@ -79,7 +79,7 @@ func GetMapKeyAsUint64(m map[string]string, key string, context apiObject, nonZe } // GetMapKeyAsStringSlice tries to find and parse a key in the map as string slice splitting it on delimiter. -func GetMapKeyAsStringSlice(m map[string]string, key string, context apiObject, delimiter string) ([]string, bool, error) { +func GetMapKeyAsStringSlice(m map[string]string, key string, _ apiObject, delimiter string) ([]string, bool, error) { if str, exists := m[key]; exists { slice := strings.Split(str, delimiter) return slice, exists, nil diff --git a/internal/configs/version1/config.go b/internal/configs/version1/config.go index 53f3c19a95..469d55848e 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..58191c987f 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 77f979c0e2..0167a1ac47 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}} @@ -56,6 +59,20 @@ http { '' $sent_http_grpc_status; } + {{- 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}} @@ -75,6 +92,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..0ec8355498 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 @@ -106,6 +107,7 @@ type EgressMTLS struct { SSLName string } +// OIDC holds OIDC configuration data. type OIDC struct { AuthEndpoint string ClientID string @@ -124,6 +126,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 +177,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 50d3730e35..2440b84091 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 }} @@ -362,6 +393,36 @@ server { error_page 501 = @grpc_internal; {{ 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 755d9edc0b..d044b1d56c 100644 --- a/internal/configs/virtualserver.go +++ b/internal/configs/virtualserver.go @@ -85,6 +85,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 { @@ -99,6 +101,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, @@ -288,7 +303,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) @@ -308,6 +327,8 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(vsEx *VirtualS } 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 crUpstreams := make(map[string]conf_v1.Upstream) @@ -443,6 +464,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, @@ -461,6 +484,7 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(vsEx *VirtualS vsc.warnings, ) addPoliciesCfgToLocations(routePoliciesCfg, cfg.Locations) + addDosConfigToLocations(dosRouteCfg, cfg.Locations) maps = append(maps, cfg.Maps...) locations = append(locations, cfg.Locations...) @@ -472,6 +496,7 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(vsEx *VirtualS cfg := generateDefaultSplitsConfig(r, virtualServerUpstreamNamer, crUpstreams, variableNamer, len(splitClients), vsc.cfgParams, errorPages, r.Path, vsLocSnippets, vsc.enableSnippets, len(returnLocations), isVSR, "", "", vsc.warnings) addPoliciesCfgToLocations(routePoliciesCfg, cfg.Locations) + addDosConfigToLocations(dosRouteCfg, cfg.Locations) splitClients = append(splitClients, cfg.SplitClients...) locations = append(locations, cfg.Locations...) internalRedirectLocations = append(internalRedirectLocations, cfg.InternalRedirectLocation) @@ -485,6 +510,7 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(vsEx *VirtualS loc, returnLoc := generateLocation(r.Path, upstreamName, upstream, r.Action, vsc.cfgParams, errorPages, false, proxySSLName, r.Path, vsLocSnippets, vsc.enableSnippets, len(returnLocations), isVSR, "", "", vsc.warnings) addPoliciesCfgToLocation(routePoliciesCfg, &loc) + loc.Dos = dosRouteCfg locations = append(locations, loc) if returnLoc != nil { @@ -547,6 +573,9 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(vsEx *VirtualS routePoliciesCfg.OIDC = policiesCfg.OIDC } limitReqZones = append(limitReqZones, routePoliciesCfg.LimitReqZones...) + + dosRouteCfg := generateDosCfg(dosResources[r.Path]) + if len(r.Matches) > 0 { cfg := generateMatchesConfig( r, @@ -566,6 +595,7 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(vsEx *VirtualS vsc.warnings, ) addPoliciesCfgToLocations(routePoliciesCfg, cfg.Locations) + addDosConfigToLocations(dosRouteCfg, cfg.Locations) maps = append(maps, cfg.Maps...) locations = append(locations, cfg.Locations...) @@ -577,6 +607,7 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(vsEx *VirtualS cfg := generateDefaultSplitsConfig(r, upstreamNamer, crUpstreams, variableNamer, len(splitClients), vsc.cfgParams, errorPages, r.Path, locSnippets, vsc.enableSnippets, len(returnLocations), isVSR, vsr.Name, vsr.Namespace, vsc.warnings) addPoliciesCfgToLocations(routePoliciesCfg, cfg.Locations) + addDosConfigToLocations(dosRouteCfg, cfg.Locations) splitClients = append(splitClients, cfg.SplitClients...) locations = append(locations, cfg.Locations...) @@ -590,6 +621,7 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(vsEx *VirtualS loc, returnLoc := generateLocation(r.Path, upstreamName, upstream, r.Action, vsc.cfgParams, errorPages, false, proxySSLName, r.Path, locSnippets, vsc.enableSnippets, len(returnLocations), isVSR, vsr.Name, vsr.Namespace, vsc.warnings) addPoliciesCfgToLocation(routePoliciesCfg, &loc) + loc.Dos = dosRouteCfg locations = append(locations, loc) if returnLoc != nil { @@ -639,6 +671,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, @@ -677,7 +710,7 @@ type policyOwnerDetails struct { type policyOptions struct { tls bool secretRefs map[string]*secrets.SecretReference - apResources map[string]string + apResources *appProtectResourcesForVS } type validationResults struct { @@ -979,7 +1012,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 { @@ -1000,7 +1033,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) @@ -1018,7 +1051,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 { @@ -1167,6 +1200,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 @@ -2321,3 +2360,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 cf237f9165..4a08e1a997 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) } @@ -1012,7 +1012,7 @@ func TestGenerateVirtualServerConfigGrpcErrorPageWarning(t *testing.T) { isWildcardEnabled := true 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("TestGenerateVirtualServerConfigGrpcErrorPageWarning() mismatch (-want +got):\n%s", diff) } @@ -1121,7 +1121,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) } @@ -1407,7 +1407,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) } @@ -1725,7 +1725,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) } @@ -1735,6 +1735,332 @@ func TestGenerateVirtualServerConfigForVirtualServerWithMatches(t *testing.T) { } } +func TestGenerateVirtualServerConfigForVirtualServerRoutesWithDos(t *testing.T) { + dosResources := map[string]*appProtectDosResource{ + "/coffee": { + AppProtectDosEnable: "on", + AppProtectDosLogEnable: false, + AppProtectDosMonitorURI: "test.example.com", + AppProtectDosMonitorProtocol: "http", + AppProtectDosMonitorTimeout: 0, + AppProtectDosName: "my-dos-coffee", + AppProtectDosAccessLogDst: "svc.dns.com:123", + AppProtectDosPolicyFile: "", + AppProtectDosLogConfFile: "", + }, + "/tea": { + AppProtectDosEnable: "on", + AppProtectDosLogEnable: false, + AppProtectDosMonitorURI: "test.example.com", + AppProtectDosMonitorProtocol: "http", + AppProtectDosMonitorTimeout: 0, + AppProtectDosName: "my-dos-tea", + AppProtectDosAccessLogDst: "svc.dns.com:123", + AppProtectDosPolicyFile: "", + AppProtectDosLogConfFile: "", + }, + "/juice": { + AppProtectDosEnable: "on", + AppProtectDosLogEnable: false, + AppProtectDosMonitorURI: "test.example.com", + AppProtectDosMonitorProtocol: "http", + AppProtectDosMonitorTimeout: 0, + AppProtectDosName: "my-dos-juice", + AppProtectDosAccessLogDst: "svc.dns.com:123", + AppProtectDosPolicyFile: "", + AppProtectDosLogConfFile: "", + }, + } + + virtualServerEx := VirtualServerEx{ + VirtualServer: &conf_v1.VirtualServer{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "cafe", + Namespace: "default", + }, + Spec: conf_v1.VirtualServerSpec{ + Host: "cafe.example.com", + Routes: []conf_v1.Route{ + { + Path: "/coffee", + Route: "default/coffee", + }, + { + Path: "/tea", + Route: "default/tea", + }, + { + Path: "/juice", + Route: "default/juice", + }, + }, + }, + }, + Endpoints: map[string][]string{ + "default/tea-svc-v1:80": { + "10.0.0.20:80", + }, + "default/tea-svc-v2:80": { + "10.0.0.21:80", + }, + "default/coffee-svc-v1:80": { + "10.0.0.30:80", + }, + "default/coffee-svc-v2:80": { + "10.0.0.31:80", + }, + "default/juice-svc-v1:80": { + "10.0.0.33:80", + }, + "default/juice-svc-v2:80": { + "10.0.0.34:80", + }, + }, + VirtualServerRoutes: []*conf_v1.VirtualServerRoute{ + { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "coffee", + Namespace: "default", + }, + Spec: conf_v1.VirtualServerRouteSpec{ + Host: "cafe.example.com", + Upstreams: []conf_v1.Upstream{ + { + Name: "coffee-v1", + Service: "coffee-svc-v1", + Port: 80, + }, + { + Name: "coffee-v2", + Service: "coffee-svc-v2", + Port: 80, + }, + }, + Subroutes: []conf_v1.Route{ + { + Path: "/coffee", + Matches: []conf_v1.Match{ + { + Conditions: []conf_v1.Condition{ + { + Argument: "version", + Value: "v2", + }, + }, + Action: &conf_v1.Action{ + Pass: "coffee-v2", + }, + }, + }, + Dos: "test_ns/dos_protected", + Action: &conf_v1.Action{ + Pass: "coffee-v1", + }, + }, + }, + }, + }, + { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "tea", + Namespace: "default", + }, + Spec: conf_v1.VirtualServerRouteSpec{ + Host: "cafe.example.com", + Upstreams: []conf_v1.Upstream{ + { + Name: "tea-v1", + Service: "tea-svc-v1", + Port: 80, + }, + { + Name: "tea-v2", + Service: "tea-svc-v2", + Port: 80, + }, + }, + Subroutes: []conf_v1.Route{ + { + Path: "/tea", + Dos: "test_ns/dos_protected", + Action: &conf_v1.Action{ + Pass: "tea-v1", + }, + }, + }, + }, + }, + { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "juice", + Namespace: "default", + }, + Spec: conf_v1.VirtualServerRouteSpec{ + Host: "cafe.example.com", + Upstreams: []conf_v1.Upstream{ + { + Name: "juice-v1", + Service: "juice-svc-v1", + Port: 80, + }, + { + Name: "juice-v2", + Service: "juice-svc-v2", + Port: 80, + }, + }, + Subroutes: []conf_v1.Route{ + { + Path: "/juice", + Dos: "test_ns/dos_protected", + Splits: []conf_v1.Split{ + { + Weight: 80, + Action: &conf_v1.Action{ + Pass: "juice-v1", + }, + }, + { + Weight: 20, + Action: &conf_v1.Action{ + Pass: "juice-v2", + }, + }, + }, + }, + }, + }, + }, + }, + } + + baseCfgParams := ConfigParams{} + + expected := []version2.Location{ + { + Path: "/internal_location_matches_0_match_0", + ProxyPass: "http://vs_default_cafe_vsr_default_coffee_coffee-v2$request_uri", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + Internal: true, + ProxySSLName: "coffee-svc-v2.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "coffee-svc-v2", + IsVSR: true, + VSRName: "coffee", + VSRNamespace: "default", + Dos: &version2.Dos{ + Enable: "on", + Name: "my-dos-coffee", + ApDosMonitorURI: "test.example.com", + ApDosMonitorProtocol: "http", + ApDosAccessLogDest: "svc.dns.com:123", + }, + }, + { + Path: "/internal_location_matches_0_default", + ProxyPass: "http://vs_default_cafe_vsr_default_coffee_coffee-v1$request_uri", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + Internal: true, + ProxySSLName: "coffee-svc-v1.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "coffee-svc-v1", + IsVSR: true, + VSRName: "coffee", + VSRNamespace: "default", + Dos: &version2.Dos{ + Enable: "on", + Name: "my-dos-coffee", + ApDosMonitorURI: "test.example.com", + ApDosMonitorProtocol: "http", + ApDosAccessLogDest: "svc.dns.com:123", + }, + }, + { + Path: "/tea", + ProxyPass: "http://vs_default_cafe_vsr_default_tea_tea-v1", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + Internal: false, + ProxySSLName: "tea-svc-v1.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "tea-svc-v1", + IsVSR: true, + VSRName: "tea", + VSRNamespace: "default", + Dos: &version2.Dos{ + Enable: "on", + Name: "my-dos-tea", + ApDosMonitorURI: "test.example.com", + ApDosMonitorProtocol: "http", + ApDosAccessLogDest: "svc.dns.com:123", + }, + }, + { + Path: "/internal_location_splits_0_split_0", + Internal: true, + ProxyPass: "http://vs_default_cafe_vsr_default_juice_juice-v1$request_uri", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ProxySSLName: "juice-svc-v1.default.svc", + Dos: &version2.Dos{ + Enable: "on", + Name: "my-dos-juice", + ApDosMonitorURI: "test.example.com", + ApDosMonitorProtocol: "http", + ApDosAccessLogDest: "svc.dns.com:123", + }, + ServiceName: "juice-svc-v1", + IsVSR: true, + VSRName: "juice", + VSRNamespace: "default", + }, + { + Path: "/internal_location_splits_0_split_1", + Internal: true, + ProxyPass: "http://vs_default_cafe_vsr_default_juice_juice-v2$request_uri", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ProxySSLName: "juice-svc-v2.default.svc", + Dos: &version2.Dos{ + Enable: "on", + Name: "my-dos-juice", + ApDosMonitorURI: "test.example.com", + ApDosMonitorProtocol: "http", + ApDosAccessLogDest: "svc.dns.com:123", + }, + ServiceName: "juice-svc-v2", + IsVSR: true, + VSRName: "juice", + VSRNamespace: "default", + }, + } + + isPlus := false + isResolverConfigured := false + vsc := newVirtualServerConfigurator(&baseCfgParams, isPlus, isResolverConfigured, &StaticConfigParams{MainAppProtectDosLoadModule: true}, false) + + result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, nil, dosResources) + if diff := cmp.Diff(expected, result.Server.Locations); diff != "" { + t.Errorf("GenerateVirtualServerConfig() mismatch (-want +got):\n%s", diff) + } + + if len(warnings) != 0 { + t.Errorf("GenerateVirtualServerConfig returned warnings: %v", vsc.warnings) + } +} + func TestGenerateVirtualServerConfigForVirtualServerWithReturns(t *testing.T) { virtualServerEx := VirtualServerEx{ VirtualServer: &conf_v1.VirtualServer{ @@ -2199,7 +2525,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) } @@ -2253,9 +2579,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", + }, }, } @@ -3696,9 +4026,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", @@ -7805,7 +8139,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 @@ -7817,7 +8151,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", }, @@ -7837,9 +8174,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", @@ -7862,8 +8203,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", @@ -7890,8 +8234,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", @@ -7919,9 +8266,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", @@ -7939,9 +8290,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/configs/warnings.go b/internal/configs/warnings.go index 6a335312f3..6084728629 100644 --- a/internal/configs/warnings.go +++ b/internal/configs/warnings.go @@ -20,12 +20,12 @@ func (w Warnings) Add(warnings Warnings) { } } -// Adds a warning for the specified object using the provided format and arguments. +// AddWarningf Adds a warning for the specified object using the provided format and arguments. func (w Warnings) AddWarningf(obj runtime.Object, msgFmt string, args ...interface{}) { w[obj] = append(w[obj], fmt.Sprintf(msgFmt, args...)) } -// Adds a warning for the specified object. +// AddWarning Adds a warning for the specified object. func (w Warnings) AddWarning(obj runtime.Object, msg string) { w[obj] = append(w[obj], msg) } diff --git a/internal/k8s/appprotect/app_protect_configuration.go b/internal/k8s/appprotect/app_protect_configuration.go index 3d1edd2f89..9a1f4e611b 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/appprotectcommon" + "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 := appprotectcommon.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 := appprotectcommon.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 := appprotectcommon.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 := appprotectcommon.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 := appprotectcommon.GetNsName(logConfObj) logConf := &LogConfEx{ Obj: logConfObj, IsValid: true, @@ -584,7 +588,7 @@ func (fc *FakeConfiguration) AddOrUpdateLogConf(logConfObj *unstructured.Unstruc } // AddOrUpdateUserSig adds or updates App Protect User Defined Signature to App Protect Configuration -func (fc *FakeConfiguration) AddOrUpdateUserSig(userSigObj *unstructured.Unstructured) (change UserSigChange, problems []Problem) { +func (fc *FakeConfiguration) AddOrUpdateUserSig(_ *unstructured.Unstructured) (change UserSigChange, problems []Problem) { return change, problems } @@ -606,16 +610,16 @@ func (fc *FakeConfiguration) GetAppResource(kind, key string) (*unstructured.Uns } // DeletePolicy deletes an App Protect Policy from App Protect Configuration -func (fc *FakeConfiguration) DeletePolicy(key string) (changes []Change, problems []Problem) { +func (fc *FakeConfiguration) DeletePolicy(_ string) (changes []Change, problems []Problem) { return changes, problems } // DeleteLogConf deletes an App Protect Log Configuration from App Protect Configuration -func (fc *FakeConfiguration) DeleteLogConf(key string) (changes []Change, problems []Problem) { +func (fc *FakeConfiguration) DeleteLogConf(_ string) (changes []Change, problems []Problem) { return changes, problems } // DeleteUserSig deletes an App Protect User Defined Signature from App Protect Configuration -func (fc *FakeConfiguration) DeleteUserSig(key string) (change UserSigChange, problems []Problem) { +func (fc *FakeConfiguration) DeleteUserSig(_ string) (change UserSigChange, problems []Problem) { return change, problems } 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/appprotectcommon/app_protect_common_resources.go b/internal/k8s/appprotectcommon/app_protect_common_resources.go new file mode 100644 index 0000000000..42e30105d1 --- /dev/null +++ b/internal/k8s/appprotectcommon/app_protect_common_resources.go @@ -0,0 +1,29 @@ +package appprotectcommon + +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/appprotectcommon/app_protect_common_resources_test.go b/internal/k8s/appprotectcommon/app_protect_common_resources_test.go new file mode 100644 index 0000000000..e34ae9d1e5 --- /dev/null +++ b/internal/k8s/appprotectcommon/app_protect_common_resources_test.go @@ -0,0 +1,91 @@ +package appprotectcommon + +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..f6aecfe1df --- /dev/null +++ b/internal/k8s/appprotectdos/app_protect_dos_configuration.go @@ -0,0 +1,387 @@ +package appprotectdos + +import ( + "fmt" + "strings" + + "github.com/nginxinc/kubernetes-ingress/internal/configs" + "github.com/nginxinc/kubernetes-ingress/internal/k8s/appprotectcommon" + "github.com/nginxinc/kubernetes-ingress/pkg/apis/dos/v1beta1" + "github.com/nginxinc/kubernetes-ingress/pkg/apis/dos/validation" + "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 := appprotectcommon.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 := appprotectcommon.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) { + 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 +} + +// GetDosProtectedThatReferencedDosPolicy gets dos protected resources that mention the given dos policy +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 +} + +// GetDosProtectedThatReferencedDosLogConf gets dos protected resources that mention the given dos log conf +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..26d8c68888 --- /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..56b6cf5990 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,8 +27,9 @@ func createTestConfiguration() *Configuration { lbc.HasCorrectIngressClass, isPlus, appProtectEnabled, + appProtectDosEnabled, internalRoutesEnabled, - validation.NewVirtualServerValidator(isTLSPassthroughEnabled), + validation.NewVirtualServerValidator(isTLSPassthroughEnabled, appProtectDosEnabled), validation.NewGlobalConfigurationValidator(map[int]bool{ 80: true, 443: true, @@ -3046,23 +3048,23 @@ type testReferenceChecker struct { onlyTransportServers bool } -func (rc *testReferenceChecker) IsReferencedByIngress(namespace string, name string, ing *networking.Ingress) bool { +func (rc *testReferenceChecker) IsReferencedByIngress(namespace string, name string, _ *networking.Ingress) bool { return rc.onlyIngresses && namespace == rc.resourceNamespace && name == rc.resourceName } -func (rc *testReferenceChecker) IsReferencedByMinion(namespace string, name string, ing *networking.Ingress) bool { +func (rc *testReferenceChecker) IsReferencedByMinion(namespace string, name string, _ *networking.Ingress) bool { return rc.onlyMinions && namespace == rc.resourceNamespace && name == rc.resourceName } -func (rc *testReferenceChecker) IsReferencedByVirtualServer(namespace string, name string, vs *conf_v1.VirtualServer) bool { +func (rc *testReferenceChecker) IsReferencedByVirtualServer(namespace string, name string, _ *conf_v1.VirtualServer) bool { return rc.onlyVirtualServers && namespace == rc.resourceNamespace && name == rc.resourceName } -func (rc *testReferenceChecker) IsReferencedByVirtualServerRoute(namespace string, name string, vsr *conf_v1.VirtualServerRoute) bool { +func (rc *testReferenceChecker) IsReferencedByVirtualServerRoute(namespace string, name string, _ *conf_v1.VirtualServerRoute) bool { return rc.onlyVirtualServerRoutes && namespace == rc.resourceNamespace && name == rc.resourceName } -func (rc *testReferenceChecker) IsReferencedByTransportServer(namespace string, name string, ts *conf_v1alpha1.TransportServer) bool { +func (rc *testReferenceChecker) IsReferencedByTransportServer(namespace string, name string, _ *conf_v1alpha1.TransportServer) bool { return rc.onlyTransportServers && namespace == rc.resourceNamespace && name == rc.resourceName } diff --git a/internal/k8s/controller.go b/internal/k8s/controller.go index 011513dbdc..c970b84005 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/appprotectcommon" + "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 @@ -139,7 +147,7 @@ type LoadBalancerController struct { metricsCollector collectors.ControllerCollector globalConfigurationValidator *validation.GlobalConfigurationValidator transportServerValidator *validation.TransportServerValidator - spiffeController *spiffeController + spiffeController *SpiffeController internalRoutesEnabled bool syncLock sync.Mutex isNginxReady bool @@ -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(), appprotectcommon.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 := appprotectcommon.GetNsName(poldel) for _, wafPol := range getWAFPoliciesForAppProtectPolicy(lbc.getAllPolicies(), polNsName) { resources = append(resources, lbc.configuration.FindResourcesForPolicy(wafPol.Namespace, wafPol.Name)...) } @@ -1244,6 +1299,49 @@ 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" @@ -1283,6 +1381,7 @@ func (lbc *LoadBalancerController) updateTransportServerStatusAndEventsOnDelete( } } +// UpdateVirtualServerStatusAndEventsOnDelete updates the virtual server status and events func (lbc *LoadBalancerController) UpdateVirtualServerStatusAndEventsOnDelete(vsConfig *VirtualServerConfiguration, changeError string, deleteErr error) { eventType := api_v1.EventTypeWarning eventTitle := "Rejected" @@ -1324,6 +1423,7 @@ func (lbc *LoadBalancerController) UpdateVirtualServerStatusAndEventsOnDelete(vs // for each VSR, a dedicated problem exists } +// UpdateIngressStatusAndEventsOnDelete updates the ingress status and events. func (lbc *LoadBalancerController) UpdateIngressStatusAndEventsOnDelete(ingConfig *IngressConfiguration, changeError string, deleteErr error) { eventTitle := "Rejected" eventWarningMessage := "" @@ -2113,6 +2213,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 +2338,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 := appprotectcommon.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 +2365,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 := appprotectcommon.ParseResourceReferenceAnnotation(ing.Namespace, ing.Annotations[configs.AppProtectPolicyAnnotation]) apPolicy, err = lbc.appProtectConfiguration.GetAppResource(appprotect.PolicyGVK.Kind, polNsN) if err != nil { @@ -2265,10 +2377,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 +2422,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 +2503,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) @@ -2413,6 +2545,14 @@ func (lbc *LoadBalancerController) createVirtualServerEx(virtualServer *conf_v1. if err != nil { glog.Warningf("Error getting WAF policies for VirtualServerRoute %v/%v: %v", vsr.Namespace, vsr.Name, err) } + + if sr.Dos != "" { + routeDosEx, err := lbc.dosConfiguration.GetValidDosEx(vsr.Namespace, sr.Dos) + if err != nil { + glog.Warningf("Error getting App Protect Dos resource for VirtualServerRoute %v/%v: %v", vsr.Namespace, vsr.Name, err) + } + virtualServerEx.DosProtectedEx[sr.Path] = routeDosEx + } } for _, u := range vsr.Spec.Upstreams { @@ -3207,6 +3347,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/handlers.go b/internal/k8s/handlers.go index 0407a9ec89..e520acf4b3 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..7f87ba2a85 100644 --- a/internal/k8s/reference_checkers.go +++ b/internal/k8s/reference_checkers.go @@ -75,11 +75,11 @@ func (rc *secretReferenceChecker) IsReferencedByVirtualServer(secretNamespace st return false } -func (rc *secretReferenceChecker) IsReferencedByVirtualServerRoute(secretNamespace string, secretName string, vsr *v1.VirtualServerRoute) bool { +func (rc *secretReferenceChecker) IsReferencedByVirtualServerRoute(_ string, _ string, _ *v1.VirtualServerRoute) bool { return false } -func (rc *secretReferenceChecker) IsReferencedByTransportServer(secretNamespace string, secretName string, ts *conf_v1alpha1.TransportServer) bool { +func (rc *secretReferenceChecker) IsReferencedByTransportServer(_ string, _ string, _ *conf_v1alpha1.TransportServer) bool { return false } @@ -173,11 +173,11 @@ func newPolicyReferenceChecker() *policyReferenceChecker { return &policyReferenceChecker{} } -func (rc *policyReferenceChecker) IsReferencedByIngress(policyNamespace string, policyName string, ing *networking.Ingress) bool { +func (rc *policyReferenceChecker) IsReferencedByIngress(_ string, _ string, _ *networking.Ingress) bool { return false } -func (rc *policyReferenceChecker) IsReferencedByMinion(policyNamespace string, policyName string, ing *networking.Ingress) bool { +func (rc *policyReferenceChecker) IsReferencedByMinion(_ string, _ string, _ *networking.Ingress) bool { return false } @@ -205,7 +205,7 @@ func (rc *policyReferenceChecker) IsReferencedByVirtualServerRoute(policyNamespa return false } -func (rc *policyReferenceChecker) IsReferencedByTransportServer(policyNamespace string, policyName string, ts *conf_v1alpha1.TransportServer) bool { +func (rc *policyReferenceChecker) IsReferencedByTransportServer(_ string, _ string, _ *conf_v1alpha1.TransportServer) bool { return false } @@ -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, ",") @@ -232,19 +231,19 @@ func (rc *appProtectResourceReferenceChecker) IsReferencedByIngress(namespace st return false } -func (rc *appProtectResourceReferenceChecker) IsReferencedByMinion(namespace string, name string, ing *networking.Ingress) bool { +func (rc *appProtectResourceReferenceChecker) IsReferencedByMinion(_ string, _ string, _ *networking.Ingress) bool { return false } -func (rc *appProtectResourceReferenceChecker) IsReferencedByVirtualServer(namespace string, name string, vs *v1.VirtualServer) bool { +func (rc *appProtectResourceReferenceChecker) IsReferencedByVirtualServer(_ string, _ string, _ *v1.VirtualServer) bool { return false } -func (rc *appProtectResourceReferenceChecker) IsReferencedByVirtualServerRoute(namespace string, name string, vsr *v1.VirtualServerRoute) bool { +func (rc *appProtectResourceReferenceChecker) IsReferencedByVirtualServerRoute(_ string, _ string, _ *v1.VirtualServerRoute) bool { return false } -func (rc *appProtectResourceReferenceChecker) IsReferencedByTransportServer(namespace string, name string, ts *conf_v1alpha1.TransportServer) bool { +func (rc *appProtectResourceReferenceChecker) IsReferencedByTransportServer(_ string, _ string, _ *conf_v1alpha1.TransportServer) bool { return false } @@ -262,3 +261,48 @@ 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(_ string, _ string, _ *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 { + for _, route := range vsr.Spec.Subroutes { + if route.Dos == namespace+"/"+name || (namespace == vsr.Namespace && route.Dos == name) { + return true + } + } + return false +} + +func (rc *dosResourceReferenceChecker) IsReferencedByTransportServer(_ string, _ string, _ *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/secrets/store.go b/internal/k8s/secrets/store.go index e3d356134c..3352fe910c 100644 --- a/internal/k8s/secrets/store.go +++ b/internal/k8s/secrets/store.go @@ -136,7 +136,7 @@ func (s *FakeSecretStore) AddOrUpdateSecret(secret *api_v1.Secret) { } // DeleteSecret is a fake implementation of DeleteSecret. -func (s *FakeSecretStore) DeleteSecret(key string) { +func (s *FakeSecretStore) DeleteSecret(_ string) { } // GetSecret is a fake implementation of GetSecret. diff --git a/internal/k8s/spiffe.go b/internal/k8s/spiffe.go index 53cec979cf..dfb7cfcfb6 100644 --- a/internal/k8s/spiffe.go +++ b/internal/k8s/spiffe.go @@ -10,20 +10,21 @@ import ( "github.com/spiffe/go-spiffe/workload" ) -type spiffeController struct { +// SpiffeController controls spiffe +type SpiffeController struct { watcher *spiffeWatcher client *workload.X509SVIDClient } // NewSpiffeController creates the spiffeWatcher and the Spiffe Workload API Client, // returns an error if the client cannot connect to the Spire Agent. -func NewSpiffeController(sync func(*workload.X509SVIDs), spireAgentAddr string) (*spiffeController, error) { +func NewSpiffeController(sync func(*workload.X509SVIDs), spireAgentAddr string) (*SpiffeController, error) { watcher := &spiffeWatcher{sync: sync} client, err := workload.NewX509SVIDClient(watcher, workload.WithAddr("unix://"+spireAgentAddr)) if err != nil { return nil, fmt.Errorf("failed to create Spiffe Workload API Client: %w", err) } - sc := &spiffeController{ + sc := &SpiffeController{ watcher: watcher, client: client, } @@ -33,7 +34,7 @@ func NewSpiffeController(sync func(*workload.X509SVIDs), spireAgentAddr string) // Start starts the Spiffe Workload API Client and waits for the Spiffe certs to be written to disk. // If the certs are not available after 30 seconds an error is returned. // On success, calls onStart function and kicks off the Spiffe Controller's run loop. -func (sc *spiffeController) Start(stopCh <-chan struct{}, onStart func()) error { +func (sc *SpiffeController) Start(stopCh <-chan struct{}, onStart func()) error { glog.V(3).Info("Starting SPIFFE Workload API Client") err := sc.client.Start() if err != nil { @@ -62,7 +63,7 @@ func (sc *spiffeController) Start(stopCh <-chan struct{}, onStart func()) error } // Run waits until a message is sent on the stop channel and stops the Spiffe Workload API Client. -func (sc *spiffeController) Run(stopCh <-chan struct{}) { +func (sc *SpiffeController) Run(stopCh <-chan struct{}) { <-stopCh err := sc.client.Stop() if err != nil { 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..98dd6ed099 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,17 @@ func validateRelatedAnnotation(name string, validator validatorFunc) annotationV } } +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 +440,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..8eb6787ad2 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 with default namespace", + }, + { + 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 with fully specified identifier", + }, + { + 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: "invalid appprotectdos.f5.com/app-protect-dos-enable annotation with special characters", + }, + { + 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: "invalid appprotectdos.f5.com/app-protect-dos-enable annotation with incorrectly qualified identifier", + }, { 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/metrics/collectors/controller.go b/internal/metrics/collectors/controller.go index 5c89aab4e0..b6c108b6ca 100644 --- a/internal/metrics/collectors/controller.go +++ b/internal/metrics/collectors/controller.go @@ -144,16 +144,16 @@ func NewControllerFakeCollector() *ControllerFakeCollector { } // Register implements a fake Register -func (cc *ControllerFakeCollector) Register(registry *prometheus.Registry) error { return nil } +func (cc *ControllerFakeCollector) Register(_ *prometheus.Registry) error { return nil } // SetIngresses implements a fake SetIngresses -func (cc *ControllerFakeCollector) SetIngresses(ingressType string, count int) {} +func (cc *ControllerFakeCollector) SetIngresses(_ string, _ int) {} // SetVirtualServers implements a fake SetVirtualServers -func (cc *ControllerFakeCollector) SetVirtualServers(count int) {} +func (cc *ControllerFakeCollector) SetVirtualServers(_ int) {} // SetVirtualServerRoutes implements a fake SetVirtualServerRoutes -func (cc *ControllerFakeCollector) SetVirtualServerRoutes(count int) {} +func (cc *ControllerFakeCollector) SetVirtualServerRoutes(_ int) {} // SetTransportServers implements a fake SetTransportServers func (cc *ControllerFakeCollector) SetTransportServers(int, int, int) {} diff --git a/internal/metrics/collectors/manager.go b/internal/metrics/collectors/manager.go index 1c64de5b4d..2aa1e46786 100644 --- a/internal/metrics/collectors/manager.go +++ b/internal/metrics/collectors/manager.go @@ -127,13 +127,13 @@ func NewManagerFakeCollector() *ManagerFakeCollector { } // Register implements a fake Register -func (nc *ManagerFakeCollector) Register(registry *prometheus.Registry) error { return nil } +func (nc *ManagerFakeCollector) Register(_ *prometheus.Registry) error { return nil } // IncNginxReloadCount implements a fake IncNginxReloadCount -func (nc *ManagerFakeCollector) IncNginxReloadCount(isEndPointUpdate bool) {} +func (nc *ManagerFakeCollector) IncNginxReloadCount(_ bool) {} // IncNginxReloadErrors implements a fake IncNginxReloadErrors func (nc *ManagerFakeCollector) IncNginxReloadErrors() {} // UpdateLastReloadTime implements a fake UpdateLastReloadTime -func (nc *ManagerFakeCollector) UpdateLastReloadTime(ms time.Duration) {} +func (nc *ManagerFakeCollector) UpdateLastReloadTime(_ time.Duration) {} diff --git a/internal/metrics/syslog_listener.go b/internal/metrics/syslog_listener.go index cacf7733a8..d15dc22c0f 100644 --- a/internal/metrics/syslog_listener.go +++ b/internal/metrics/syslog_listener.go @@ -63,17 +63,13 @@ func (l LatencyMetricsListener) Stop() { func isErrorRecoverable(err error) bool { var nerr *net.OpError - if errors.As(err, &nerr) && nerr.Temporary() { - return true - } else { - return false - } + return errors.As(err, &nerr) && nerr.Temporary() } // SyslogFakeListener is a fake implementation of the SyslogListener interface type SyslogFakeListener struct{} -// NewFakeSyslogServer returns a SyslogFakeListener +// NewSyslogFakeServer returns a SyslogFakeListener func NewSyslogFakeServer() *SyslogFakeListener { return &SyslogFakeListener{} } diff --git a/internal/nginx/fake_manager.go b/internal/nginx/fake_manager.go index 334d6cdfff..d2448600f2 100644 --- a/internal/nginx/fake_manager.go +++ b/internal/nginx/fake_manager.go @@ -70,12 +70,12 @@ func (*FakeManager) DeleteStreamConfig(name string) { } // CreateTLSPassthroughHostsConfig provides a fake implementation of CreateTLSPassthroughHostsConfig. -func (*FakeManager) CreateTLSPassthroughHostsConfig(content []byte) { +func (*FakeManager) CreateTLSPassthroughHostsConfig(_ []byte) { glog.V(3).Infof("Writing TLS Passthrough Hosts config file") } // CreateSecret provides a fake implementation of CreateSecret. -func (fm *FakeManager) CreateSecret(name string, content []byte, mode os.FileMode) string { +func (fm *FakeManager) CreateSecret(name string, _ []byte, _ os.FileMode) string { glog.V(3).Infof("Writing secret %v", name) return fm.GetFilenameForSecret(name) } @@ -91,7 +91,7 @@ func (fm *FakeManager) GetFilenameForSecret(name string) string { } // CreateDHParam provides a fake implementation of CreateDHParam. -func (fm *FakeManager) CreateDHParam(content string) (string, error) { +func (fm *FakeManager) CreateDHParam(_ string) (string, error) { glog.V(3).Infof("Writing dhparam file") return fm.dhparamFilename, nil } @@ -103,12 +103,12 @@ func (*FakeManager) Version() string { } // Start provides a fake implementation of Start. -func (*FakeManager) Start(done chan error) { +func (*FakeManager) Start(_ chan error) { glog.V(3).Info("Starting nginx") } // Reload provides a fake implementation of Reload. -func (*FakeManager) Reload(isEndpointsUpdate bool) error { +func (*FakeManager) Reload(_ bool) error { glog.V(3).Infof("Reloading nginx") return nil } @@ -119,16 +119,16 @@ func (*FakeManager) Quit() { } // UpdateConfigVersionFile provides a fake implementation of UpdateConfigVersionFile. -func (*FakeManager) UpdateConfigVersionFile(openTracing bool) { +func (*FakeManager) UpdateConfigVersionFile(_ bool) { glog.V(3).Infof("Writing config version") } // SetPlusClients provides a fake implementation of SetPlusClients. -func (*FakeManager) SetPlusClients(plusClient *client.NginxClient, plusConfigVersionCheckClient *http.Client) { +func (*FakeManager) SetPlusClients(_ *client.NginxClient, _ *http.Client) { } // UpdateServersInPlus provides a fake implementation of UpdateServersInPlus. -func (*FakeManager) UpdateServersInPlus(upstream string, servers []string, config ServerConfig) error { +func (*FakeManager) UpdateServersInPlus(upstream string, servers []string, _ ServerConfig) error { glog.V(3).Infof("Updating servers of %v: %v", upstream, servers) return nil } @@ -140,18 +140,18 @@ func (*FakeManager) UpdateStreamServersInPlus(upstream string, servers []string) } // CreateOpenTracingTracerConfig creates a fake implementation of CreateOpenTracingTracerConfig. -func (*FakeManager) CreateOpenTracingTracerConfig(content string) error { +func (*FakeManager) CreateOpenTracingTracerConfig(_ string) error { glog.V(3).Infof("Writing OpenTracing tracer config file") return nil } // SetOpenTracing creates a fake implementation of SetOpenTracing. -func (*FakeManager) SetOpenTracing(openTracing bool) { +func (*FakeManager) SetOpenTracing(_ bool) { } // AppProtectAgentStart is a fake implementation of AppProtectAgentStart -func (*FakeManager) AppProtectAgentStart(apaDone chan error, debug bool) { +func (*FakeManager) AppProtectAgentStart(_ chan error, _ bool) { glog.V(3).Infof("Starting FakeAppProtectAgent") } @@ -161,7 +161,7 @@ func (*FakeManager) AppProtectAgentQuit() { } // AppProtectPluginStart is a fake implementtion AppProtectPluginStart -func (*FakeManager) AppProtectPluginStart(appDone chan error) { +func (*FakeManager) AppProtectPluginStart(_ chan error) { glog.V(3).Infof("Starting FakeAppProtectPlugin") } @@ -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(_ chan error, _ bool, _ int, _ int, _ int) { + glog.V(3).Infof("Starting FakeAppProtectDosAgent") +} diff --git a/internal/nginx/manager.go b/internal/nginx/manager.go index 219de351f8..9ea835e60c 100644 --- a/internal/nginx/manager.go +++ b/internal/nginx/manager.go @@ -7,6 +7,7 @@ import ( "os" "os/exec" "path" + "strconv" "strings" "time" @@ -17,10 +18,14 @@ import ( ) const ( - ReloadForEndpointsUpdate = true // ReloadForEndpointsUpdate means that is caused by an endpoints update. - ReloadForOtherUpdate = false // ReloadForOtherUpdate means that a reload is caused by an update for a resource(s) other than endpoints. - TLSSecretFileMode = 0o600 // TLSSecretFileMode defines the default filemode for files with TLS Secrets. - JWKSecretFileMode = 0o644 // JWKSecretFileMode defines the default filemode for files with JWK Secrets. + // ReloadForEndpointsUpdate means that is caused by an endpoints update. + ReloadForEndpointsUpdate = true + // ReloadForOtherUpdate means that a reload is caused by an update for a resource(s) other than endpoints. + ReloadForOtherUpdate = false + // TLSSecretFileMode defines the default filemode for files with TLS Secrets. + TLSSecretFileMode = 0o600 + // JWKSecretFileMode defines the default filemode for files with JWK Secrets. + JWKSecretFileMode = 0o644 configFileMode = 0o644 jsonFileForOpenTracingTracer = "/var/lib/nginx/tracer-config.json" nginxBinaryPath = "/usr/sbin/nginx" @@ -37,6 +42,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 +86,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 +110,7 @@ type LocalManager struct { OpenTracing bool appProtectPluginPid int appProtectAgentPid int + appProtectDosAgentPid int } // NewLocalManager creates a LocalManager. @@ -512,6 +524,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/internal/nginx/verify_test.go b/internal/nginx/verify_test.go index 58281bc134..30e72b0cf5 100644 --- a/internal/nginx/verify_test.go +++ b/internal/nginx/verify_test.go @@ -11,7 +11,7 @@ import ( type Transport struct{} -func (c Transport) RoundTrip(req *http.Request) (*http.Response, error) { +func (c Transport) RoundTrip(_ *http.Request) (*http.Response, error) { return &http.Response{ StatusCode: 200, Body: ioutil.NopCloser(bytes.NewBufferString("42")), diff --git a/pkg/apis/configuration/register.go b/pkg/apis/configuration/register.go index 04d4f2973a..4aa7c94519 100644 --- a/pkg/apis/configuration/register.go +++ b/pkg/apis/configuration/register.go @@ -1,5 +1,6 @@ package configuration const ( + // GroupName is the name of the group. GroupName = "k8s.nginx.org" ) diff --git a/pkg/apis/configuration/v1/register.go b/pkg/apis/configuration/v1/register.go index 5adaf25a27..73924e09d8 100644 --- a/pkg/apis/configuration/v1/register.go +++ b/pkg/apis/configuration/v1/register.go @@ -21,8 +21,10 @@ func Resource(resource string) schema.GroupResource { } var ( + // SchemeBuilder builds a scheme SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) - AddToScheme = SchemeBuilder.AddToScheme + // AddToScheme adds to a scheme + AddToScheme = SchemeBuilder.AddToScheme ) // Adds the list of known types to Scheme. diff --git a/pkg/apis/configuration/v1/types.go b/pkg/apis/configuration/v1/types.go index fb40e407a9..bb84e5b724 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. @@ -297,6 +299,7 @@ type VirtualServerRouteSpec struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// VirtualServerRouteList is a list of VirtualServerRoute type VirtualServerRouteList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata"` diff --git a/pkg/apis/configuration/v1alpha1/register.go b/pkg/apis/configuration/v1alpha1/register.go index a6e7301c05..22c1aa7327 100644 --- a/pkg/apis/configuration/v1alpha1/register.go +++ b/pkg/apis/configuration/v1alpha1/register.go @@ -21,8 +21,10 @@ func Resource(resource string) schema.GroupResource { } var ( + // SchemeBuilder build a scheme SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) - AddToScheme = SchemeBuilder.AddToScheme + // AddToScheme exported add function + AddToScheme = SchemeBuilder.AddToScheme ) // Adds the list of known types to Scheme. diff --git a/pkg/apis/configuration/validation/appprotect.go b/pkg/apis/configuration/validation/appprotect.go new file mode 100644 index 0000000000..096674e3ba --- /dev/null +++ b/pkg/apis/configuration/validation/appprotect.go @@ -0,0 +1,54 @@ +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 +} + +// ValidateAppProtectUserSig validates the app protect user sig. +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..d05269cc5f --- /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" +) + +// ValidateRequiredSlices validates the required slices. +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 +} + +// ValidateRequiredFields validates the required fields. +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..ed6dfc8106 100644 --- a/pkg/apis/configuration/validation/common.go +++ b/pkg/apis/configuration/validation/common.go @@ -17,6 +17,15 @@ const ( var escapedStringsFmtRegexp = regexp.MustCompile("^" + escapedStringsFmt + "$") +// ValidateEscapedString validates an escaped string. +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{} @@ -209,8 +218,8 @@ func mapToPrettyString(m map[string]bool) string { return strings.Join(out, ", ") } -// validateParameter validates a parameter against a map of valid parameters for the directive -func validateParameter(nPar string, validParams map[string]bool, fieldPath *field.Path) field.ErrorList { +// ValidateParameter validates a parameter against a map of valid parameters for the directive +func ValidateParameter(nPar string, validParams map[string]bool, fieldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} if !validParams[nPar] { diff --git a/pkg/apis/configuration/validation/policy.go b/pkg/apis/configuration/validation/policy.go index 2ee1886996..af6d6a6508 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" @@ -273,7 +272,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())) } @@ -387,7 +386,7 @@ var validateVerifyClientKeyParameters = map[string]bool{ func validateIngressMTLSVerifyClient(verifyClient string, fieldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} if verifyClient != "" { - allErrs = append(allErrs, validateParameter(verifyClient, validateVerifyClientKeyParameters, fieldPath)...) + allErrs = append(allErrs, ValidateParameter(verifyClient, validateVerifyClientKeyParameters, fieldPath)...) } return allErrs } @@ -452,9 +451,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..a681fe371f 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..28052da170 100644 --- a/pkg/apis/configuration/validation/virtualserver.go +++ b/pkg/apis/configuration/validation/virtualserver.go @@ -15,13 +15,15 @@ import ( // VirtualServerValidator validates a VirtualServer/VirtualServerRoute resource. type VirtualServerValidator struct { - isPlus bool + isPlus bool + isDosEnabled bool } // NewVirtualServerValidator creates a new VirtualServerValidator. -func NewVirtualServerValidator(isPlus bool) *VirtualServerValidator { +func NewVirtualServerValidator(isPlus bool, isDosEnabled bool) *VirtualServerValidator { return &VirtualServerValidator{ - isPlus: isPlus, + isPlus: isPlus, + isDosEnabled: isDosEnabled, } } @@ -44,6 +46,8 @@ func (vsv *VirtualServerValidator) validateVirtualServerSpec(spec *v1.VirtualSer allErrs = append(allErrs, vsv.validateVirtualServerRoutes(spec.Routes, fieldPath.Child("routes"), upstreamNames, namespace)...) + allErrs = append(allErrs, validateDos(vsv.isDosEnabled, spec.Dos, fieldPath.Child("dos"))...) + return allErrs } @@ -114,6 +118,25 @@ func validateTLS(tls *v1.TLS, fieldPath *field.Path) field.ErrorList { return allErrs } +func validateDos(isDosEnabled bool, dos string, fieldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if dos == "" { + // valid, dos is not required + return allErrs + } + + if !isDosEnabled { + allErrs = append(allErrs, field.Forbidden(fieldPath, "field requires DOS enablement")) + } + + 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{} @@ -646,6 +669,8 @@ func (vsv *VirtualServerValidator) validateRoute(route v1.Route, fieldPath *fiel allErrs = append(allErrs, field.Invalid(fieldPath, "", msg)) } + allErrs = append(allErrs, validateDos(vsv.isDosEnabled, route.Dos, fieldPath.Child("dos"))...) + return allErrs } @@ -718,9 +743,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 +883,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 +926,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 +1028,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 +1232,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 +1414,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..5e3779f864 100644 --- a/pkg/apis/configuration/validation/virtualserver_test.go +++ b/pkg/apis/configuration/validation/virtualserver_test.go @@ -52,10 +52,11 @@ func TestValidateVirtualServer(t *testing.T) { }, }, }, + Dos: "some-ns/some-name", }, } - vsv := &VirtualServerValidator{isPlus: false} + vsv := &VirtualServerValidator{isPlus: false, isDosEnabled: true} err := vsv.ValidateVirtualServer(&virtualServer) if err != nil { @@ -93,6 +94,58 @@ func TestValidateHost(t *testing.T) { } } +func TestValidateDos(t *testing.T) { + validDosResources := []string{ + "hello", + "ns/hello", + "hello-world-1", + } + + for _, h := range validDosResources { + allErrs := validateDos(true, 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(true, h, field.NewPath("dos")) + if len(allErrs) == 0 { + t.Errorf("validateDos(%q) returned no errors for invalid input", h) + } + } +} + +func TestValidateDosDisabled(t *testing.T) { + invalidDos := []string{ + "hello", + "ns/hello", + "hello-world-1", + "*", + "..", + ".example.com", + "-hello-world-1", + "/hello/world-1", + "hello/world/other", + } + + for _, h := range invalidDos { + allErrs := validateDos(false, 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..ba5c19a015 --- /dev/null +++ b/pkg/apis/dos/register.go @@ -0,0 +1,6 @@ +package dos + +const ( + // GroupName the name of the group used by kubernetes. + 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..d527b6a37f --- /dev/null +++ b/pkg/apis/dos/v1beta1/register.go @@ -0,0 +1,37 @@ +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 builds a scheme + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + // AddToScheme a function to add to a scheme + 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..fa205102ee --- /dev/null +++ b/pkg/apis/dos/v1beta1/types.go @@ -0,0 +1,61 @@ +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"` +} + +// DosProtectedResourceSpec deines the properties and values a DosProtectedResource can have. +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..69ccdbc26a --- /dev/null +++ b/pkg/apis/dos/v1beta1/zz_generated.deepcopy.go @@ -0,0 +1,128 @@ +//go:build !ignore_autogenerated +// +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/apis/dos/validation/dos.go b/pkg/apis/dos/validation/dos.go new file mode 100644 index 0000000000..55cb97555e --- /dev/null +++ b/pkg/apis/dos/validation/dos.go @@ -0,0 +1,181 @@ +package validation + +import ( + "fmt" + "net/url" + "regexp" + "strconv" + "strings" + + validation2 "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/util/validation" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +var appProtectDosPolicyRequiredFields = [][]string{ + {"spec"}, +} + +var appProtectDosLogConfRequiredFields = [][]string{ + {"spec", "content"}, + {"spec", "filter"}, +} + +const maxNameLength = 63 + +// ValidateDosProtectedResource validates a dos protected resource. +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 := validation2.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) + } + + return validation2.ValidateEscapedString(name, "protected-object-one") +} + +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 := validation2.ValidateEscapedString(apDosMonitor.URI, "http://www.example.com"); err != nil { + return err + } + + if apDosMonitor.Protocol != "" { + allErrs := field.ErrorList{} + fieldPath := field.NewPath("dosMonitorProtocol") + allErrs = append(allErrs, validation2.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 := validation2.ValidateRequiredFields(policy, appProtectDosPolicyRequiredFields) + if err != nil { + return fmt.Errorf("error validating DosPolicy %v: %w", polName, err) + } + + return nil +} diff --git a/pkg/apis/dos/validation/dos_test.go b/pkg/apis/dos/validation/dos_test.go new file mode 100644 index 0000000000..6a96d47712 --- /dev/null +++ b/pkg/apis/dos/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:", + }, + } + + 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: \n%v\n expected to contain: \n%s", err, nTCase.msg) + } + } + } +} diff --git a/pkg/client/clientset/versioned/clientset.go b/pkg/client/clientset/versioned/clientset.go index e87bae4aa9..4fbbe22c86 100644 --- a/pkg/client/clientset/versioned/clientset.go +++ b/pkg/client/clientset/versioned/clientset.go @@ -8,6 +8,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" @@ -17,14 +18,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 @@ -37,6 +40,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 { @@ -85,6 +93,10 @@ func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, if err != nil { return nil, err } + cs.appprotectdosV1beta1, err = appprotectdosv1beta1.NewForConfigAndClient(&configShallowCopy, httpClient) + if err != nil { + return nil, err + } cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfigAndClient(&configShallowCopy, httpClient) if err != nil { @@ -108,6 +120,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 aea5215a7b..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" @@ -74,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 eef6c35eb0..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" @@ -18,6 +19,7 @@ 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 0501e9a50c..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" @@ -18,6 +19,7 @@ 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 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..186caac0ee --- /dev/null +++ b/pkg/client/clientset/versioned/typed/dos/v1beta1/dos_client.go @@ -0,0 +1,91 @@ +// Code generated by client-gen. DO NOT EDIT. + +package v1beta1 + +import ( + "net/http" + + 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. +// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), +// where httpClient was generated with rest.HTTPClientFor(c). +func NewForConfig(c *rest.Config) (*AppprotectdosV1beta1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + httpClient, err := rest.HTTPClientFor(&config) + if err != nil { + return nil, err + } + return NewForConfigAndClient(&config, httpClient) +} + +// NewForConfigAndClient creates a new AppprotectdosV1beta1Client for the given config and http client. +// Note the http client provided takes precedence over the configured transport values. +func NewForConfigAndClient(c *rest.Config, h *http.Client) (*AppprotectdosV1beta1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + client, err := rest.RESTClientForConfigAndClient(&config, h) + 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..c3f609bd03 --- /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.NewDeleteActionWithOptions(dosprotectedresourcesResource, c.ns, name, opts), &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/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 bc62a13508..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"): 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/Makefile b/tests/Makefile index 54ad9c7f27..fabb4fd614 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -1,8 +1,8 @@ CONTEXT = -BUILD_IMAGE = nginx/nginx-ingress:edge +BUILD_IMAGE = nginx/nginx-ingress:2.0.3-SNAPSHOT-61b2a91-dos PULL_POLICY = IfNotPresent DEPLOYMENT_TYPE = deployment -IC_TYPE = nginx-ingress +IC_TYPE = nginx-plus-ingress SERVICE = nodeport NODE_IP = TAG = latest @@ -10,7 +10,7 @@ PREFIX = test-runner KUBE_CONFIG_FOLDER = $${HOME}/.kube KIND_KUBE_CONFIG_FOLDER = $${HOME}/.kube/kind SHOW_IC_LOGS = no -PYTEST_ARGS = +PYTEST_ARGS = -m dos -v -s DOCKERFILEPATH = docker/Dockerfile .PHONY: build diff --git a/tests/conftest.py b/tests/conftest.py index 423abe797f..edec3cc3ca 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -122,7 +122,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..12fefb5945 --- /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.31.2-buster + 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/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..12fefb5945 --- /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.31.2-buster + 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..8e780e9e7d 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 \ + && apt-get update && apt-get install -y apache2-utils COPY tests /workspace/tests diff --git a/tests/docker/gitlab.Dockerfile b/tests/docker/gitlab.Dockerfile index 38928af48b..79a110f5d7 100644 --- a/tests/docker/gitlab.Dockerfile +++ b/tests/docker/gitlab.Dockerfile @@ -4,7 +4,7 @@ FROM python:3.9 ARG GCLOUD_VERSION=364.0.0 ARG HELM_VERSION=3.5.4 -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/exporting b/tests/exporting new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/suite/custom_resources_utils.py b/tests/suite/custom_resources_utils.py index 4c3aabf0ed..d9e814f66e 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, ing_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("", ing_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..948beeb598 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,65 +610,172 @@ 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 syslog svc -----------------------") + 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, namespace) + before = time.time() + wait_until_all_pods_are_ready(kube_apis.v1, namespace) + after = time.time() + print(f"All pods came up in {int(after-before)} seconds") + print(f"syslog svc was created") + + 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 ) + print("Remove the syslog svc:") + delete_items_from_yaml(kube_apis, src_syslog_yaml, namespace) request.addfinalizer(fin) diff --git a/tests/suite/resources_utils.py b/tests/suite/resources_utils.py index f665da0812..b6a36e7984 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. @@ -320,6 +344,23 @@ def get_pods_amount(v1: CoreV1Api, namespace) -> int: pods = v1.list_namespaced_pod(namespace) return 0 if not pods.items else len(pods.items) +def get_pods_amount_with_name(v1: CoreV1Api, namespace, name) -> int: + """ + Get an amount of pods. + + :param v1: CoreV1Api + :param namespace: namespace + :param name: name + :return: int + """ + pods = v1.list_namespaced_pod(namespace) + count = 0 + if pods and pods.items: + for item in pods.items: + if name in item.metadata.name: + count += 1 + return count + def get_pod_name_that_contains(v1: CoreV1Api, namespace, contains_string) -> str: """ Get an amount of pods. @@ -795,7 +836,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. @@ -803,6 +844,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] @@ -817,7 +859,57 @@ 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 clear_file_contents(v1: CoreV1Api, file_path, pod_name, pod_namespace) -> str: + """ + Execute 'truncate -s 0 file_path' command in a pod. + + :param v1: CoreV1Api + :param pod_name: pod name + :param pod_namespace: pod namespace + :param file_path: an absolute path to a file in the pod + :return: str + """ + command = ["truncate", "-s", "0", file_path] + 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 + +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 @@ -1046,6 +1138,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. @@ -1132,6 +1272,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..52e4172007 --- /dev/null +++ b/tests/suite/test_dos.py @@ -0,0 +1,449 @@ +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, + nginx_reload, + get_pods_amount, + get_pods_amount_with_name, + clear_file_contents, +) +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, ingress_controller_prerequisites.namespace) + + for item in kube_apis.v1.list_namespaced_pod(ingress_controller_prerequisites.namespace).items: + if "nginx-ingress" in item.metadata.name: + nginx_reload(kube_apis.v1, item.metadata.name, ingress_controller_prerequisites.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 getPodNameThatContains(self, kube_apis, namespace, contains_string): + for item in kube_apis.v1.list_namespaced_pod(namespace).items: + if contains_string in item.metadata.name: + return item.metadata.name + return "" + + def test_ap_nginx_config_entries( + self, kube_apis, ingress_controller_prerequisites, 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.{ingress_controller_prerequisites.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) + + pod_name = self.getPodNameThatContains(kube_apis, ingress_controller_prerequisites.namespace, "nginx-ingress") + + 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) + """ + print("----------------------- Get syslog pod name ----------------------") + syslog_pod = self.getPodNameThatContains(kube_apis, ingress_controller_prerequisites.namespace, "syslog") + assert "syslog" in syslog_pod + + log_loc = f"/var/log/messages" + clear_file_contents(kube_apis.v1, log_loc, syslog_pod, ingress_controller_prerequisites.namespace) + + 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) + pod_name = self.getPodNameThatContains(kube_apis, ingress_controller_prerequisites.namespace, "nginx-ingress") + + 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} namespace {ingress_controller_prerequisites.namespace}') + log_contents = get_file_contents(kube_apis.v1, log_loc, syslog_pod, ingress_controller_prerequisites.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, ingress_controller_prerequisites, crd_ingress_controller_with_dos, dos_setup, test_namespace + ): + """ + Test App Protect Dos: Block bad clients attack + """ + log_loc = f"/var/log/messages" + print("----------------------- Get syslog pod name ----------------------") + syslog_pod = self.getPodNameThatContains(kube_apis, ingress_controller_prerequisites.namespace, "syslog") + assert "syslog" in syslog_pod + clear_file_contents(kube_apis.v1, log_loc, syslog_pod, ingress_controller_prerequisites.namespace) + + 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, ingress_controller_prerequisites.namespace, 140, "attack_event=\"Attack ended\"") + + log_contents = get_file_contents(kube_apis.v1, log_loc, syslog_pod, ingress_controller_prerequisites.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_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, ingress_controller_prerequisites, crd_ingress_controller_with_dos, dos_setup, test_namespace + ): + """ + Test App Protect Dos: Block bad clients attack with learning + """ + log_loc = f"/var/log/messages" + print("----------------------- Get syslog pod name ----------------------") + syslog_pod = self.getPodNameThatContains(kube_apis, ingress_controller_prerequisites.namespace, "syslog") + assert "syslog" in syslog_pod + clear_file_contents(kube_apis.v1, log_loc, syslog_pod, ingress_controller_prerequisites.namespace) + + 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, ingress_controller_prerequisites.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, ingress_controller_prerequisites.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, ingress_controller_prerequisites.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_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 + ) + + @pytest.mark.xfail + 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("----------------------- Get syslog pod name ----------------------") + syslog_pod = self.getPodNameThatContains(kube_apis, ingress_controller_prerequisites.namespace, "syslog") + assert "syslog" in syslog_pod + log_loc = f"/var/log/messages" + clear_file_contents(kube_apis.v1, log_loc, syslog_pod, ingress_controller_prerequisites.namespace) + + 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, ingress_controller_prerequisites.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_with_name(kube_apis.v1, "nginx-ingress", "nginx-ingress") is not 2: + 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, ingress_controller_prerequisites.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_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..57ac151afb --- /dev/null +++ b/tests/suite/test_virtual_server_dos.py @@ -0,0 +1,434 @@ +import os +import requests +import time +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_with_name, + get_pods_amount, clear_file_contents, +) +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, ingress_controller_prerequisites.namespace) + + for item in kube_apis.v1.list_namespaced_pod(ingress_controller_prerequisites.namespace).items: + if "nginx-ingress" in item.metadata.name: + nginx_reload(kube_apis.v1, item.metadata.name, ingress_controller_prerequisites.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 getPodNameThatContains(self, kube_apis, namespace, contains_string): + for item in kube_apis.v1.list_namespaced_pod(namespace).items: + if contains_string in item.metadata.name: + return item.metadata.name + return "" + + 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 + """ + print("----------------------- Get syslog pod name ----------------------") + syslog_pod = self.getPodNameThatContains(kube_apis, ingress_controller_prerequisites.namespace, "syslog") + assert "syslog" in syslog_pod + log_loc = f"/var/log/messages" + clear_file_contents(kube_apis.v1, log_loc, syslog_pod, 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} namespace: {ingress_controller_prerequisites.namespace}') + log_contents = get_file_contents(kube_apis.v1, log_loc, syslog_pod, ingress_controller_prerequisites.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, ingress_controller_prerequisites, 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.{ingress_controller_prerequisites.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 + + pod_name = self.getPodNameThatContains(kube_apis, ingress_controller_prerequisites.namespace, "nginx-ingress") + + 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("----------------------- Get syslog pod name ----------------------") + syslog_pod = self.getPodNameThatContains(kube_apis, ingress_controller_prerequisites.namespace, "syslog") + assert "syslog" in syslog_pod + log_loc = f"/var/log/messages" + clear_file_contents(kube_apis.v1, log_loc, syslog_pod, 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, ingress_controller_prerequisites.namespace, 140, "attack_event=\"Attack ended\"") + + log_contents = get_file_contents(kube_apis.v1, log_loc, syslog_pod, ingress_controller_prerequisites.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 + + 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 + """ + log_loc = f"/var/log/messages" + print("----------------------- Get syslog pod name ----------------------") + syslog_pod = self.getPodNameThatContains(kube_apis, ingress_controller_prerequisites.namespace, "syslog") + assert "syslog" in syslog_pod + clear_file_contents(kube_apis.v1, log_loc, syslog_pod, 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, ingress_controller_prerequisites.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, ingress_controller_prerequisites.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, ingress_controller_prerequisites.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 + + 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 + ) + + @pytest.mark.xfail + 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 + """ + log_loc = f"/var/log/messages" + syslog_pod = self.getPodNameThatContains(kube_apis, ingress_controller_prerequisites.namespace, "syslog") + assert "syslog" in syslog_pod + clear_file_contents(kube_apis.v1, log_loc, syslog_pod, 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, ingress_controller_prerequisites.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_with_name(kube_apis.v1, "nginx-ingress", "nginx-ingress") is not 2: + 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, ingress_controller_prerequisites.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']) + + assert ( + len(learning_units_hostname) == 2 + ) diff --git a/tools.go b/tools.go index 6c12e385dc..934eccd574 100644 --- a/tools.go +++ b/tools.go @@ -1,3 +1,4 @@ +//go:build tools // +build tools // This file just exists to ensure we download the tools we need for building