From e7e1dcee266d9b603ccb92a71b0907f765ac2ef1 Mon Sep 17 00:00:00 2001 From: Ross Bender Date: Fri, 20 Sep 2024 19:58:46 -0500 Subject: [PATCH 1/3] update appgw for waf policy configuration --- plugins/modules/azure_rm_appgateway.py | 86 ++++- .../azure_rm_appgateway/tasks/main.yml | 362 +++++++++++++++++- 2 files changed, 427 insertions(+), 21 deletions(-) diff --git a/plugins/modules/azure_rm_appgateway.py b/plugins/modules/azure_rm_appgateway.py index 5e4ec60d7..c0f7564e8 100644 --- a/plugins/modules/azure_rm_appgateway.py +++ b/plugins/modules/azure_rm_appgateway.py @@ -799,12 +799,14 @@ web_application_firewall_configuration: version_added: "1.15.0" description: - - Web application firewall configuration of the application gateway reosurce. + - Web application firewall configuration of the application gateway resource. + - Note that as of version 2.8.0, I(firewall_policy) is required instead of deprecated options. See https://github.com/ansible-collections/azure/pull/1697. type: dict suboptions: disabled_rule_groups: description: - - The disabled rule groups. + - (Deprecated) The disabled rule groups. + - This value has been deprecated, and will be removed in a later version. Use I(firewall_policy) instead. type: list elements: dict default: [] @@ -821,11 +823,13 @@ default: [] enabled: description: - - Whether the web application firewall is enabled or not. + - (Deprecated) Whether the web application firewall is enabled or not. + - This value has been deprecated, and will be removed in a later version. Use I(firewall_policy) instead. type: bool exclusions: description: - - The exclusion list. + - (Deprecated) The exclusion list. + - This value has been deprecated, and will be removed in a later version. Use I(firewall_policy) instead. type: list elements: dict default: [] @@ -845,38 +849,64 @@ type: str file_upload_limit_in_mb: description: - - Maximum file upload size in Mb for WAF. + - (Deprecated) Maximum file upload size in Mb for WAF. + - This value has been deprecated, and will be removed in a later version. Use I(firewall_policy) instead. type: int firewall_mode: description: - - Web application firewall mode. + - (Deprecated) Web application firewall mode. + - This value has been deprecated, and will be removed in a later version. Use I(firewall_policy) instead. type: str choices: - 'Detection' - 'Prevention' max_request_body_size: description: - - Maximum request body size for WAF. + - (Deprecated) Maximum request body size for WAF. + - This value has been deprecated, and will be removed in a later version. Use I(firewall_policy) instead. type: int max_request_body_size_in_kb: description: - - Maximum request body size in Kb for WAF. + - (Deprecated) Maximum request body size in Kb for WAF. + - This value has been deprecated, and will be removed in a later version. Use I(firewall_policy) instead. type: int request_body_check: description: - - Whether allow WAF to check request Body. + - (Deprecated) Whether allow WAF to check request Body. + - This value has been deprecated, and will be removed in a later version. Use I(firewall_policy) instead. type: bool rule_set_type: description: - - The type of the web application firewall rule set. + - (Deprecated) The type of the web application firewall rule set. - Possible values are 'OWASP'. + - This value has been deprecated, and will be removed in a later version. Use I(firewall_policy) instead. type: str choices: - 'OWASP' rule_set_version: description: - - The version of the rule set type. + - (Deprecated) The version of the rule set type. + - This value has been deprecated, and will be removed in a later version. Use I(firewall_policy) instead. type: str + firewall_policy: + version_added: "2.8.0" + description: + - Web application firewall policy for the application gateway. + type: dict + suboptions: + id: + description: + - Resource ID of the firewall policy. Required if I(name) is not provided. + type: str + name: + description: + - Name of the firewall policy (in same subscription and region). Used if I(id) is not provided. + type: str + force_association: + description: + - If true, associates the firewall policy with an application gateway regardless whether the policy differs from the WAF Config. + type: bool + default: true identity: description: - Identity for the App Gateway @@ -1760,6 +1790,12 @@ class Actions: rules=dict(type='list', elements='int', default=[]), ) +firewall_policy_spec = dict( + id=dict(type='str'), + name=dict(type='str'), + force_association=dict(type='bool', default=True), +) + web_application_firewall_configuration_spec = dict( enabled=dict(type='bool'), firewall_mode=dict(type='str', choices=['Detection', 'Prevention']), @@ -1771,6 +1807,7 @@ class Actions: file_upload_limit_in_mb=dict(type='int'), exclusions=dict(type='list', elements='dict', options=waf_configuration_exclusions_spec, default=[]), disabled_rule_groups=dict(type='list', elements='dict', options=waf_configuration_disabled_rule_groups_spec, default=[]), + firewall_policy=dict(type='dict', options=firewall_policy_spec), ) trusted_root_certificates_spec = dict( @@ -2409,7 +2446,7 @@ def exec_module(self, **kwargs): elif key == "autoscale_configuration": self.parameters["autoscale_configuration"] = kwargs[key] elif key == "web_application_firewall_configuration": - self.parameters["web_application_firewall_configuration"] = kwargs[key] + self.set_web_application_firewall_configuration(kwargs) elif key == "enable_http2": self.parameters["enable_http2"] = kwargs[key] elif key == "tags": @@ -2658,6 +2695,22 @@ def get_resource(self): return False + def set_web_application_firewall_configuration(self, kwargs): + waf_config = dict(kwargs['web_application_firewall_configuration']) + if waf_config is None: + return + + if 'firewall_policy' in waf_config and waf_config['firewall_policy'] is not None: + if 'name' in waf_config['firewall_policy'] and waf_config['firewall_policy']['name'] is not None: + waf_config['firewall_policy']['id'] = waf_policy_id(self.subscription_id, + kwargs['resource_group'], + waf_config['firewall_policy']['name']) + del waf_config['firewall_policy']['name'] + + self.parameters['force_firewall_policy_association'] = waf_config['firewall_policy']['force_association'] + del waf_config['firewall_policy']['force_association'] + self.parameters['firewall_policy'] = waf_config['firewall_policy'] + def public_ip_id(subscription_id, resource_group_name, name): """Generate the id for a frontend ip configuration""" @@ -2819,6 +2872,15 @@ def trusted_root_certificate_id(subscription_id, resource_group_name, appgateway ) +def waf_policy_id(subscription_id, resource_group_name, policy_name): + """Generate the id for a web application firewall policy""" + return '/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies/{2}'.format( + subscription_id, + resource_group_name, + policy_name, + ) + + def compare_dicts(old_response, new_response): """Compare two dictionaries using recursive_diff method and assuming that null values coming from yaml input are acting like absent values""" diff --git a/tests/integration/targets/azure_rm_appgateway/tasks/main.yml b/tests/integration/targets/azure_rm_appgateway/tasks/main.yml index da3583a92..2b0b839da 100644 --- a/tests/integration/targets/azure_rm_appgateway/tasks/main.yml +++ b/tests/integration/targets/azure_rm_appgateway/tasks/main.yml @@ -8,7 +8,9 @@ ansible.builtin.set_fact: rpfx: "{{ resource_group | hash('md5') | truncate(7, True, '') }}{{ 1000 | random }}" cert1_file: "cert1.txt" + cert1_password: "your-password" cert2_file: "cert2.txt" + cert2_password: "your-password" cert3b64_file: "cert3b64.txt" location: "{{ __rg_info.resourcegroups.0.location }}" run_once: true @@ -83,7 +85,7 @@ policy_name: "ssl_policy20170401_s" ssl_certificates: - name: cert2 - password: "{{ password }}" + password: "{{ cert2_password }}" data: "{{ lookup('file', cert2_file) }}" gateway_ip_configurations: - subnet: @@ -241,7 +243,7 @@ policy_name: "ssl_policy20170401_s" ssl_certificates: - name: cert2 - password: "{{ password }}" + password: "{{ cert2_password }}" data: "{{ lookup('file', cert2_file) }}" gateway_ip_configurations: - subnet: @@ -399,7 +401,7 @@ policy_name: "ssl_policy20170401_s" ssl_certificates: - name: cert2 - password: "{{ password }}" + password: "{{ cert2_password }}" data: "{{ lookup('file', cert2_file) }}" gateway_ip_configurations: - subnet: @@ -560,7 +562,7 @@ policy_name: "ssl_policy20170401_s" ssl_certificates: - name: cert2 - password: "{{ password }}" + password: "{{ cert2_password }}" data: "{{ lookup('file', cert2_file) }}" gateway_ip_configurations: - subnet: @@ -726,7 +728,7 @@ policy_name: "ssl_policy20170401_s" ssl_certificates: - name: cert2 - password: "{{ password }}" + password: "{{ cert2_password }}" data: "{{ lookup('file', cert2_file) }}" gateway_ip_configurations: - subnet: @@ -895,7 +897,7 @@ policy_name: "ssl_policy20170401_s" ssl_certificates: - name: cert2 - password: "{{ password }}" + password: "{{ cert2_password }}" data: "{{ lookup('file', cert2_file) }}" trusted_root_certificates: - name: "rootCert3" @@ -1066,7 +1068,7 @@ policy_name: "ssl_policy20170401_s" ssl_certificates: - name: cert2 - password: "{{ password }}" + password: "{{ cert2_password }}" data: "{{ lookup('file', cert2_file) }}" trusted_root_certificates: - name: "rootCert3" @@ -1240,7 +1242,7 @@ policy_name: "ssl_policy20170401_s" ssl_certificates: - name: cert2 - password: "{{ password }}" + password: "{{ cert2_password }}" data: "{{ lookup('file', cert2_file) }}" gateway_ip_configurations: - subnet: @@ -1407,7 +1409,7 @@ policy_name: "ssl_policy20170401_s" ssl_certificates: - name: cert2 - password: "{{ password }}" + password: "{{ cert2_password }}" data: "{{ lookup('file', cert2_file) }}" gateway_ip_configurations: - subnet: @@ -1561,6 +1563,337 @@ that: - not output.changed +- name: Configure public IP for waf policy gateway + azure_rm_publicipaddress: + name: "appgateway-waf-policy-{{ rpfx }}-pip" + resource_group: "{{ resource_group }}" + sku: "standard" + allocation_method: "static" + +- name: Try to create waf policy instance of Application Gateway + azure_rm_appgateway: + resource_group: "{{ resource_group }}" + name: "appgateway-waf-policy-{{ rpfx }}" + sku: + name: waf_v2 + tier: waf_v2 + capacity: 2 + ssl_policy: + policy_type: "predefined" + policy_name: "ssl_policy20170401_s" + ssl_certificates: + - name: cert2 + password: "{{ cert2_password }}" + data: "{{ lookup('file', cert2_file) }}" + gateway_ip_configurations: + - subnet: + id: "{{ subnet_output.state.id }}" + name: app_gateway_ip_config + frontend_ip_configurations: + - name: "public-inbound-ip" + public_ip_address: "appgateway-waf-policy-{{ rpfx }}-pip" + frontend_ports: + - name: "inbound-http" + port: 80 + - name: "inbound-https" + port: 443 + backend_address_pools: + - name: test_backend_address_pool1 + backend_addresses: + - ip_address: 10.0.0.1 + - name: test_backend_address_pool2 + backend_addresses: + - ip_address: 10.0.0.2 + backend_http_settings_collection: + - name: "http-profile1" + port: 443 + protocol: https + pick_host_name_from_backend_address: true + probe: "http-probe1" + cookie_based_affinity: "disabled" + - name: "http-profile2" + port: 8080 + protocol: http + pick_host_name_from_backend_address: true + probe: "http-probe2" + cookie_based_affinity: "disabled" + http_listeners: + - name: "inbound-http" + protocol: "http" + frontend_ip_configuration: "public-inbound-ip" + frontend_port: "inbound-http" + - name: "inbound-traffic1" + protocol: "https" + frontend_ip_configuration: "public-inbound-ip" + frontend_port: "inbound-https" + host_name: "traffic1.example.com" + require_server_name_indication: true + ssl_certificate: "cert2" + - name: "inbound-traffic2" + protocol: "https" + frontend_ip_configuration: "public-inbound-ip" + frontend_port: "inbound-https" + host_name: "traffic2.example.com" + require_server_name_indication: true + ssl_certificate: "cert2" + url_path_maps: + - name: "path_mappings" + default_redirect_configuration: "redirect-traffic1" + default_rewrite_rule_set: "configure-headers" + path_rules: + - name: "path_rules" + backend_address_pool: "test_backend_address_pool1" + backend_http_settings: "http-profile1" + paths: + - "/abc" + - "/123/*" + request_routing_rules: + - name: "app-routing1" + rule_type: "basic" + priority: 100 + http_listener: "inbound-traffic1" + backend_address_pool: "test_backend_address_pool2" + backend_http_settings: "http-profile1" + rewrite_rule_set: "configure-headers" + - name: "app-routing2" + rule_type: "path_based_routing" + priority: 101 + http_listener: "inbound-traffic2" + url_path_map: "path_mappings" + - name: "redirect-routing" + rule_type: "basic" + priority: 102 + http_listener: "inbound-http" + redirect_configuration: "redirect-http" + rewrite_rule_sets: + - name: "configure-headers" + rewrite_rules: + - name: "add-security-response-header" + rule_sequence: 1 + action_set: + response_header_configurations: + - header_name: "Strict-Transport-Security" + header_value: "max-age=31536000" + - name: "remove-backend-response-headers" + rule_sequence: 2 + action_set: + response_header_configurations: + - header_name: "Server" + - header_name: "X-Powered-By" + - name: "set-custom-header-condition" + rule_sequence: 3 + conditions: + - variable: "var_client_ip" + pattern: "1.1.1.1" + - variable: "http_req_Authorization" + pattern: "12345" + ignore_case: false + action_set: + request_header_configurations: + - header_name: "Foo" + header_value: "Bar" + probes: + - name: "http-probe1" + interval: 30 + path: "/abc" + protocol: "https" + pick_host_name_from_backend_http_settings: true + timeout: 30 + unhealthy_threshold: 2 + - name: "http-probe2" + interval: 30 + path: "/xyz" + protocol: "http" + pick_host_name_from_backend_http_settings: true + timeout: 30 + unhealthy_threshold: 2 + redirect_configurations: + - name: "redirect-http" + redirect_type: "permanent" + target_listener: "inbound-traffic1" + include_path: true + include_query_string: true + request_routing_rules: + - "redirect-routing" + - name: "redirect-traffic1" + redirect_type: "found" + target_listener: "inbound-traffic1" + include_path: true + include_query_string: true + url_path_maps: + - "path_mappings" + web_application_firewall_configuration: + firewall_policy: + name: "wafpolicy{{ rpfx }}" + register: output + +- name: Assert the resource instance is well created + ansible.builtin.assert: + that: + - output.changed + +- name: Try to create waf policy instance of Application Gateway - no update + azure_rm_appgateway: + resource_group: "{{ resource_group }}" + name: "appgateway-waf-policy-{{ rpfx }}" + sku: + name: waf_v2 + tier: waf_v2 + capacity: 2 + ssl_policy: + policy_type: "predefined" + policy_name: "ssl_policy20170401_s" + ssl_certificates: + - name: cert2 + password: "{{ cert2_password }}" + data: "{{ lookup('file', cert2_file) }}" + gateway_ip_configurations: + - subnet: + id: "{{ subnet_output.state.id }}" + name: app_gateway_ip_config + frontend_ip_configurations: + - name: "public-inbound-ip" + public_ip_address: "appgateway-waf-policy-{{ rpfx }}-pip" + frontend_ports: + - name: "inbound-http" + port: 80 + - name: "inbound-https" + port: 443 + backend_address_pools: + - name: test_backend_address_pool1 + backend_addresses: + - ip_address: 10.0.0.1 + - name: test_backend_address_pool2 + backend_addresses: + - ip_address: 10.0.0.2 + backend_http_settings_collection: + - name: "http-profile1" + port: 443 + protocol: https + pick_host_name_from_backend_address: true + probe: "http-probe1" + cookie_based_affinity: "disabled" + - name: "http-profile2" + port: 8080 + protocol: http + pick_host_name_from_backend_address: true + probe: "http-probe2" + cookie_based_affinity: "disabled" + http_listeners: + - name: "inbound-http" + protocol: "http" + frontend_ip_configuration: "public-inbound-ip" + frontend_port: "inbound-http" + - name: "inbound-traffic1" + protocol: "https" + frontend_ip_configuration: "public-inbound-ip" + frontend_port: "inbound-https" + host_name: "traffic1.example.com" + require_server_name_indication: true + ssl_certificate: "cert2" + - name: "inbound-traffic2" + protocol: "https" + frontend_ip_configuration: "public-inbound-ip" + frontend_port: "inbound-https" + host_name: "traffic2.example.com" + require_server_name_indication: true + ssl_certificate: "cert2" + url_path_maps: + - name: "path_mappings" + default_redirect_configuration: "redirect-traffic1" + default_rewrite_rule_set: "configure-headers" + path_rules: + - name: "path_rules" + backend_address_pool: "test_backend_address_pool1" + backend_http_settings: "http-profile1" + paths: + - "/abc" + - "/123/*" + request_routing_rules: + - name: "app-routing1" + rule_type: "basic" + priority: 100 + http_listener: "inbound-traffic1" + backend_address_pool: "test_backend_address_pool2" + backend_http_settings: "http-profile1" + rewrite_rule_set: "configure-headers" + - name: "app-routing2" + rule_type: "path_based_routing" + priority: 101 + http_listener: "inbound-traffic2" + url_path_map: "path_mappings" + - name: "redirect-routing" + rule_type: "basic" + priority: 102 + http_listener: "inbound-http" + redirect_configuration: "redirect-http" + rewrite_rule_sets: + - name: "configure-headers" + rewrite_rules: + - name: "add-security-response-header" + rule_sequence: 1 + action_set: + response_header_configurations: + - header_name: "Strict-Transport-Security" + header_value: "max-age=31536000" + - name: "remove-backend-response-headers" + rule_sequence: 2 + action_set: + response_header_configurations: + - header_name: "Server" + - header_name: "X-Powered-By" + - name: "set-custom-header-condition" + rule_sequence: 3 + conditions: + - variable: "var_client_ip" + pattern: "1.1.1.1" + - variable: "http_req_Authorization" + pattern: "12345" + ignore_case: false + action_set: + request_header_configurations: + - header_name: "Foo" + header_value: "Bar" + probes: + - name: "http-probe1" + interval: 30 + path: "/abc" + protocol: "https" + pick_host_name_from_backend_http_settings: true + timeout: 30 + unhealthy_threshold: 2 + - name: "http-probe2" + interval: 30 + path: "/xyz" + protocol: "http" + pick_host_name_from_backend_http_settings: true + timeout: 30 + unhealthy_threshold: 2 + redirect_configurations: + - name: "redirect-http" + redirect_type: "permanent" + target_listener: "inbound-traffic1" + include_path: true + include_query_string: true + request_routing_rules: + - "redirect-routing" + - name: "redirect-traffic1" + redirect_type: "found" + target_listener: "inbound-traffic1" + include_path: true + include_query_string: true + url_path_maps: + - "path_mappings" + web_application_firewall_configuration: + firewall_policy: + name: "wafpolicy{{ rpfx }}" + register: output + +- name: Assert the resource instance is not updated + ansible.builtin.assert: + that: + - not output.changed + - name: Delete v2 instance of Application Gateway azure_rm_appgateway: resource_group: "{{ resource_group }}" @@ -1583,6 +1916,17 @@ that: - output.changed +- name: Delete waf policy instance of Application Gateway + azure_rm_appgateway: + resource_group: "{{ resource_group }}" + name: "appgateway-waf-policy-{{ rpfx }}" + state: absent + register: output +- name: Assert the state has changed + ansible.builtin.assert: + that: + - output.changed + - name: Delete public IP for v2 gateway azure_rm_publicipaddress: name: "appgateway-v2-{{ rpfx }}-pip" From 9f45e80ec3bdb0bc1f827b90115043178cffab12 Mon Sep 17 00:00:00 2001 From: Ross Bender Date: Mon, 23 Sep 2024 08:41:31 -0500 Subject: [PATCH 2/3] correct line length --- plugins/modules/azure_rm_appgateway.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/modules/azure_rm_appgateway.py b/plugins/modules/azure_rm_appgateway.py index 91064324a..b627ca8cd 100644 --- a/plugins/modules/azure_rm_appgateway.py +++ b/plugins/modules/azure_rm_appgateway.py @@ -800,7 +800,9 @@ version_added: "1.15.0" description: - Web application firewall configuration of the application gateway resource. - - Note that as of version 2.8.0, I(firewall_policy) is required instead of deprecated options. See https://github.com/ansible-collections/azure/pull/1697. + - > + Note that as of version 2.8.0, I(firewall_policy) is required instead of deprecated options. + See https://github.com/ansible-collections/azure/pull/1697. type: dict suboptions: disabled_rule_groups: From dfdfd0033e4a3694f3cc80b10a02b2cf871ac26b Mon Sep 17 00:00:00 2001 From: Ross Bender Date: Mon, 23 Sep 2024 08:57:43 -0500 Subject: [PATCH 3/3] Apply suggestions from code review Co-authored-by: Fred-sun <37327967+Fred-sun@users.noreply.github.com> --- tests/integration/targets/azure_rm_appgateway/tasks/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/targets/azure_rm_appgateway/tasks/main.yml b/tests/integration/targets/azure_rm_appgateway/tasks/main.yml index 2b0b839da..dfcc3b460 100644 --- a/tests/integration/targets/azure_rm_appgateway/tasks/main.yml +++ b/tests/integration/targets/azure_rm_appgateway/tasks/main.yml @@ -8,9 +8,9 @@ ansible.builtin.set_fact: rpfx: "{{ resource_group | hash('md5') | truncate(7, True, '') }}{{ 1000 | random }}" cert1_file: "cert1.txt" - cert1_password: "your-password" + cert1_password: "{{ lookup('ansible.builtin.password', '/dev/null', chars=['ascii_letters', 'digits', 'punctuation'], length=12) }}" cert2_file: "cert2.txt" - cert2_password: "your-password" + cert2_password: "{{ lookup('ansible.builtin.password', '/dev/null', chars=['ascii_letters', 'digits', 'punctuation'], length=12) }}" cert3b64_file: "cert3b64.txt" location: "{{ __rg_info.resourcegroups.0.location }}" run_once: true