diff --git a/tests/suite/fixtures/ic_fixtures.py b/tests/suite/fixtures/ic_fixtures.py index 1b6ba9b2ad..72964f9855 100644 --- a/tests/suite/fixtures/ic_fixtures.py +++ b/tests/suite/fixtures/ic_fixtures.py @@ -289,14 +289,25 @@ def crd_ingress_controller_with_waf_v5( ap_uds_crd_name, f"{CRDS}/appprotect.f5.com_apusersigs.yaml", ) - name = create_ingress_controller_wafv5( - kube_apis.v1, - kube_apis.apps_v1_api, - cli_arguments, - namespace, - "regcred", - request.param.get("extra_args", None), - ) + if request.param["type"] == "rorfs": # WAFv5 with readOnlyRootFileSystem + name = create_ingress_controller_wafv5( + kube_apis.v1, + kube_apis.apps_v1_api, + cli_arguments, + namespace, + "regcred", + request.param.get("extra_args", None), + True, + ) + else: + name = create_ingress_controller_wafv5( + kube_apis.v1, + kube_apis.apps_v1_api, + cli_arguments, + namespace, + "regcred", + request.param.get("extra_args", None), + ) try: with open(f"{dir}/wafv5.tgz", "rb") as f: file_content = f.read() diff --git a/tests/suite/test_app_protect_wafv5_integration_rorfs.py b/tests/suite/test_app_protect_wafv5_integration_rorfs.py new file mode 100644 index 0000000000..3da05e5615 --- /dev/null +++ b/tests/suite/test_app_protect_wafv5_integration_rorfs.py @@ -0,0 +1,163 @@ +import pytest +import requests +from settings import TEST_DATA +from suite.utils.policy_resources_utils import create_policy_from_yaml, delete_policy +from suite.utils.resources_utils import wait_before_test +from suite.utils.vs_vsr_resources_utils import ( + create_virtual_server_from_yaml, + delete_virtual_server, + patch_v_s_route_from_yaml, + patch_virtual_server_from_yaml, +) + + +@pytest.fixture(scope="class") +def waf_setup(kube_apis, test_namespace) -> None: + waf = f"{TEST_DATA}/ap-waf-v5/policies/waf.yaml" + create_policy_from_yaml(kube_apis.custom_objects, waf, test_namespace) + wait_before_test() + + +@pytest.mark.skip_for_nginx_oss +@pytest.mark.appprotect_waf_v5 +@pytest.mark.parametrize( + "crd_ingress_controller_with_waf_v5, virtual_server_setup", + [ + ( + { + "type": "rorfs", + "extra_args": [ + f"-enable-app-protect", + ], + }, + { + "example": "ap-waf-v5", + "app_type": "simple", + }, + ), + ], + indirect=True, +) +class TestAppProtectWAFv5IntegrationVSrorfs: + def restore_default_vs(self, kube_apis, virtual_server_setup) -> None: + """ + Restore VirtualServer without policy spec + """ + std_vs_src = f"{TEST_DATA}/ap-waf-v5/standard/virtual-server.yaml" + delete_virtual_server(kube_apis.custom_objects, virtual_server_setup.vs_name, virtual_server_setup.namespace) + create_virtual_server_from_yaml(kube_apis.custom_objects, std_vs_src, virtual_server_setup.namespace) + wait_before_test() + + @pytest.mark.parametrize( + "vs_src", + [f"{TEST_DATA}/ap-waf-v5/virtual-server-waf-spec.yaml"], + ) + def test_ap_waf_v5_policy_block_vs( + self, + kube_apis, + ingress_controller_prerequisites, + crd_ingress_controller_with_waf_v5, + test_namespace, + virtual_server_setup, + waf_setup, + vs_src, + ): + patch_virtual_server_from_yaml( + kube_apis.custom_objects, + virtual_server_setup.vs_name, + vs_src, + virtual_server_setup.namespace, + ) + + print("----------------------- Send request with embedded malicious script----------------------") + count = 0 + response = requests.get( + virtual_server_setup.backend_1_url + "", + headers={"host": virtual_server_setup.vs_host}, + ) + while count < 5 and "Request Rejected" not in response.text: + response = requests.get( + virtual_server_setup.backend_1_url + "", + headers={"host": virtual_server_setup.vs_host}, + ) + wait_before_test() + count += 1 + self.restore_default_vs(kube_apis, virtual_server_setup) + assert response.status_code == 200 + assert "The requested URL was rejected. Please consult with your administrator." in response.text + + +@pytest.mark.skip_for_nginx_oss +@pytest.mark.appprotect_waf_v5 +@pytest.mark.parametrize( + "crd_ingress_controller_with_waf_v5, v_s_route_setup", + [ + ( + { + "type": "rorfs", + "extra_args": [ + f"-enable-app-protect", + ], + }, + { + "example": "virtual-server-route", + }, + ) + ], + indirect=True, +) +class TestAppProtectWAFv5IntegrationVSRrorfs: + + def restore_default_vsr(self, kube_apis, v_s_route_setup) -> None: + """ + Function to revert vsr deployments to standard state + """ + patch_src_m = f"{TEST_DATA}/virtual-server-route/route-multiple.yaml" + patch_v_s_route_from_yaml( + kube_apis.custom_objects, + v_s_route_setup.route_m.name, + patch_src_m, + v_s_route_setup.route_m.namespace, + ) + wait_before_test() + + def test_ap_waf_v5_policy_block_vsr( + self, + kube_apis, + ingress_controller_prerequisites, + crd_ingress_controller_with_waf_v5, + test_namespace, + v_s_route_setup, + ): + req_url = f"http://{v_s_route_setup.public_endpoint.public_ip}:{v_s_route_setup.public_endpoint.port}" + waf_subroute_vsr_src = f"{TEST_DATA}/ap-waf-v5/virtual-server-route-waf-subroute.yaml" + pol = create_policy_from_yaml( + kube_apis.custom_objects, + f"{TEST_DATA}/ap-waf-v5/policies/waf.yaml", + v_s_route_setup.route_m.namespace, + ) + wait_before_test() + patch_v_s_route_from_yaml( + kube_apis.custom_objects, + v_s_route_setup.route_m.name, + waf_subroute_vsr_src, + v_s_route_setup.route_m.namespace, + ) + wait_before_test() + print("----------------------- Send request with embedded malicious script----------------------") + count = 0 + response = requests.get( + f'{req_url}{v_s_route_setup.route_m.paths[0]}+""', + headers={"host": v_s_route_setup.vs_host}, + ) + while count < 5 and "Request Rejected" not in response.text: + response = requests.get( + f'{req_url}{v_s_route_setup.route_m.paths[0]}+""', + headers={"host": v_s_route_setup.vs_host}, + ) + wait_before_test() + count += 1 + self.restore_default_vsr(kube_apis, v_s_route_setup) + delete_policy(kube_apis.custom_objects, pol, v_s_route_setup.route_m.namespace) + assert response.status_code == 200 + assert "The requested URL was rejected. Please consult with your administrator." in response.text diff --git a/tests/suite/utils/resources_utils.py b/tests/suite/utils/resources_utils.py index b325f3caf6..33abadbccf 100644 --- a/tests/suite/utils/resources_utils.py +++ b/tests/suite/utils/resources_utils.py @@ -1206,7 +1206,7 @@ def create_ingress_controller(v1: CoreV1Api, apps_v1_api: AppsV1Api, cli_argumen def create_ingress_controller_wafv5( - v1: CoreV1Api, apps_v1_api: AppsV1Api, cli_arguments, namespace, reg_secret, args=None + v1: CoreV1Api, apps_v1_api: AppsV1Api, cli_arguments, namespace, reg_secret, args=None, rorfs=False ) -> str: """ Create an Ingress Controller according to the params. @@ -1225,6 +1225,9 @@ def create_ingress_controller_wafv5( dep["spec"]["replicas"] = int(cli_arguments["replicas"]) dep["spec"]["template"]["spec"]["containers"][0]["image"] = cli_arguments["image"] dep["spec"]["template"]["spec"]["containers"][0]["imagePullPolicy"] = cli_arguments["image-pull-policy"] + if "readOnlyRootFilesystem" not in dep["spec"]["template"]["spec"]["containers"][0]["securityContext"]: + dep["spec"]["template"]["spec"]["containers"][0]["securityContext"]["readOnlyRootFilesystem"] = rorfs + template_spec = dep["spec"]["template"]["spec"] if "imagePullSecrets" not in template_spec: template_spec["imagePullSecrets"] = [] @@ -1233,43 +1236,109 @@ def create_ingress_controller_wafv5( if "volumes" not in template_spec: template_spec["volumes"] = [] - template_spec["volumes"].extend( - [ - { - "name": "app-protect-bd-config", - "emptyDir": {}, - }, - { - "name": "app-protect-config", - "emptyDir": {}, - }, - { - "name": "app-protect-bundles", - "emptyDir": {}, - }, - ] - ) + if rorfs and "initContainers" not in template_spec: + template_spec["initContainers"] = [] + template_spec["initContainers"].extend( + [ + { + "name": "init-nginx-ingress", + "image": cli_arguments["image"], + "imagePullPolicy": "IfNotPresent", + "command": ["cp", "-vdR", "/etc/nginx/.", "/mnt/etc"], + "securityContext": { + "allowPrivilegeEscalation": False, + "readOnlyRootFilesystem": True, + "runAsUser": 101, # nginx + "runAsNonRoot": True, + "capabilities": {"drop": ["ALL"]}, + }, + "volumeMounts": [{"mountPath": "/mnt/etc", "name": "nginx-etc"}], + } + ] + ) + + if rorfs: + template_spec["volumes"].extend( + [ + { + "name": "app-protect-bd-config", + "emptyDir": {}, + }, + { + "name": "app-protect-config", + "emptyDir": {}, + }, + { + "name": "app-protect-bundles", + "emptyDir": {}, + }, + {"name": "nginx-etc", "emptyDir": {}}, + {"name": "nginx-log", "emptyDir": {}}, + {"name": "nginx-cache", "emptyDir": {}}, + {"name": "nginx-lib", "emptyDir": {}}, + ] + ) + else: + template_spec["volumes"].extend( + [ + { + "name": "app-protect-bd-config", + "emptyDir": {}, + }, + { + "name": "app-protect-config", + "emptyDir": {}, + }, + { + "name": "app-protect-bundles", + "emptyDir": {}, + }, + ] + ) container = dep["spec"]["template"]["spec"]["containers"][0] if "volumeMounts" not in container: container["volumeMounts"] = [] - container["volumeMounts"].extend( - [ - { - "name": "app-protect-bd-config", - "mountPath": "/opt/app_protect/bd_config", - }, - { - "name": "app-protect-config", - "mountPath": "/opt/app_protect/config", - }, - { - "name": "app-protect-bundles", - "mountPath": "/etc/app_protect/bundles", - }, - ] - ) + if rorfs: + container["volumeMounts"].extend( + [ + { + "name": "app-protect-bd-config", + "mountPath": "/opt/app_protect/bd_config", + }, + { + "name": "app-protect-config", + "mountPath": "/opt/app_protect/config", + }, + { + "name": "app-protect-bundles", + "mountPath": "/etc/app_protect/bundles", + }, + {"name": "nginx-etc", "mountPath": "/etc/nginx"}, + {"name": "nginx-log", "mountPath": "/var/log/nginx"}, + {"name": "nginx-cache", "mountPath": "/var/cache/nginx"}, + {"name": "nginx-lib", "mountPath": "/var/lib/nginx"}, + ] + ) + else: + container["volumeMounts"].extend( + [ + { + "name": "app-protect-bd-config", + "mountPath": "/opt/app_protect/bd_config", + }, + { + "name": "app-protect-config", + "mountPath": "/opt/app_protect/config", + }, + { + "name": "app-protect-bundles", + "mountPath": "/etc/app_protect/bundles", + }, + ] + ) + dep["spec"]["template"]["spec"]["containers"][0]["args"].extend( [ f"-default-server-tls-secret=$(POD_NAMESPACE)/default-server-secret", @@ -1281,7 +1350,11 @@ def create_ingress_controller_wafv5( "name": "waf-config-mgr", "image": f"{NGX_REG}/nap/waf-config-mgr:{WAF_V5_VERSION}", "imagePullPolicy": "IfNotPresent", - "securityContext": {"allowPrivilegeEscalation": False, "capabilities": {"drop": ["all"]}}, + "securityContext": { + "allowPrivilegeEscalation": False, + "capabilities": {"drop": ["all"]}, + "readOnlyRootFilesystem": rorfs, + }, "volumeMounts": [ { "name": "app-protect-bd-config", @@ -1301,6 +1374,11 @@ def create_ingress_controller_wafv5( "name": "waf-enforcer", "image": f"{NGX_REG}/nap/waf-enforcer:{WAF_V5_VERSION}", "imagePullPolicy": "IfNotPresent", + "securityContext": { + "allowPrivilegeEscalation": False, + "capabilities": {"drop": ["all"]}, + "readOnlyRootFilesystem": rorfs, + }, "env": [{"name": "ENFORCER_PORT", "value": "50000"}], "volumeMounts": [ {