policy-library/policies/templates/gcp_restricted_firewall_rules_v1.yaml (434 lines of code) (raw):

# Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # # This template is for policies restricting which firewall rules # should exist in a specific location in GCP. # All firewall rules matching the passed parameters will be flagged as violations. # All parameters are optional. # # A firewall rule will trigger a violation ONLY IF it matches ALL the passed parameters. apiVersion: templates.gatekeeper.sh/v1alpha1 kind: ConstraintTemplate metadata: name: gcp-restricted-firewall-rules-v1 spec: crd: spec: names: kind: GCPRestrictedFirewallRulesConstraintV1 validation: openAPIV3Schema: properties: mode: description: | "String identifying the operational mode, allowlist or denylist. allowlist mode: generate violations if any firewall rules do NOT match the passed parameters. denylist mode: generate violations if any firewall rules DO match the passed parameters. Default is denylist." type: string enum: [allowlist, denylist] rules: description: | "List of rules to match (denylist) / not match (allowlist) in order to raise violations. Rule are object describing rules (see below)" type: array items: type: object properties: direction: description: "Direction of the rules to restrict." type: string enum: ["INGRESS", "EGRESS", "any"] rule_type: description: "Type of the rules to restrict." type: string enum: ["allowed", "denied", "any"] port: description: | "Port of the rules to restrict. This parameter is tighly coupled with the protocol paramter: the rule will be marked as in violation if the parameter protocol:port pair matches (denylist) / does not match (allowlist) one of its protocol:port listed. Valid values are: * single number * ranges (e.g 22-25) - Note: only FW rules with a range greater or equal to this passed range would match * 'any' - when set to any, the ports of the rule will NOT be a factor to determine if the FW rule is restricted * 'all' (if the rule matches all ports for a given protocol)" type: string protocol: description: | "Protocol of the rules to restrict. This parameter is tighly coupled with the port paramter: the rule will be marked as in violation if the parameter protocol:port pair matches (denylist) / does not match (allowlist) one of its protocol:port listed. Valid values are: * single protocol (e.g 'tcp', 'icmp' etc.) * 'any' - when set to any, the protocol of the rule will NOT be a factor to determine if the FW rule is restricted * 'all' (if the rule matches all protocols)" type: string source_ranges: description: | "Source ranges of the rules to restrict. A fw rule only matches if ONE of its source CIDR ranges is an exact match of ANY of the CIDR blocks passed in this parameter (denylist) / does not match (allowlist). Valid values are: * 'X.X.X.X/XX' - i.e valid CIDR blocks * 'any' - when set to any, the source ranges of the rule will NOT be a factor to determine if the FW rule is restricted * '*' - all CIDR blocks match: this only checks that a source range exists in the restricted FW rule" type: array items: type: string source_tags: description: | "Source tags of the rules to restrict. A fw rule only matches if ONE of its source tags is a match of ANY of the tag patterns (regex) passed in this parameter (denylist) / does not match (allowlist). Valid values are: * 'mytag-*' - valid regex patterns for source tags * 'any' - when set to any, the source tags of the rule will NOT be a factor to determine if the FW rule is restricted * '*' - all source tags match: this only checks that a source tag exists in the restricted FW rule" type: array items: type: string source_service_accounts: description: | "Source service accounts of the rules to restrict. A fw rule only matches if ONE of its source service accounts is a match of ANY of the service account patterns (regex) passed in this parameter (denylist) / does not match (allowlist). Valid values are: * 'mytag-*' - valid regex patterns for source tags * 'any' - when set to any, the source tags of the rule will NOT be a factor to determine if the FW rule is restricted * '*' - all source service accounts match: this only checks that a source service account existsin the restricted FW rule" type: array items: type: string target_ranges: description: | "Target (or destination) ranges of the rules to restrict. A fw rule only matches if ONE of its destination CIDR ranges is an exact match of ANY of the CIDR blocks passed in this parameter(denylist) / does not match (allowlist). Valid values are: * 'X.X.X.X/XX' - i.e valid CIDR blocks * 'any' - when set to any, the target ranges of the rule will NOT be a factor to determine if the FW rule is restricted * '*' - all CIDR blocks match: this only checks that a target / destination range exists in the restricted FW rule" type: array items: type: string target_tags: description: | "Target tags of the rules to restrict. A fw rule only matches if ONE of its target tags is a match of ANY of the tag patterns (regex) passed in this parameter (denylist) / does not match (allowlist). Valid values are: * 'mytag-*' - valid regex patterns for target tags * 'any' - when set to any, the target tags of the rule will NOT be a factor to determine if the FW rule is restricted * '*' - all target tags match: this only checks that a target tag range exists in the restricted FW rule" type: array items: type: string target_service_accounts: description: | "Target service accounts of the rules to restrict. A fw rule only matches if ONE of its target service accounts is a match of ANY of the service account patterns (regex) passed in this parameter (denylist) / does not match (allowlist). Valid values are: * 'mytag-*' - valid regex patterns for target tags * 'any' - when set to any, the target tags of the rule will NOT be a factor to determine if the FW rule is restricted * '*' - all target service account match: this only checks that a target service account exists in the restricted FW rule" type: array items: type: string enabled: description: "Status of the rules to restrict (enabled vs disabled)." type: string enum: ["true", "false", "any"] exemptions_mode: type: string enum: ["exact", "regex"] description: "Match mode for exemptions, exact or regular expression." exemptions: type: array items: type: string description: "Array of firewall rules to exempt from the rules. String values in the array should correspond to the full name values of exempted firewall rules if exact exemptions_mode is used." required: ["rules"] targets: validation.gcp.forsetisecurity.org: rego: | # # Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # package templates.gcp.GCPRestrictedFirewallRulesConstraintV1 import data.validator.gcp.lib as lib deny[{ "msg": message, "details": metadata, }] { constraint := input.constraint lib.get_constraint_params(constraint, params) # Update params (set all missing params to their default value) rules_params := update_params(params.rules[_]) mode := lib.get_default(params, "mode", "denylist") asset := input.asset asset.asset_type == "compute.googleapis.com/Firewall" match_mode := lib.get_default(params, "exemptions_mode", "exact") check_for_exemptions(asset.name, constraint, match_mode) fw_rule = asset.resource.data is_valid(mode, fw_rule, rules_params) message := sprintf("%s Firewall rule is prohibited.", [asset.name]) metadata := { "resource": asset.name, "restricted_rules": rules_params, } } ########################### # Rule Utilities ########################### # update_params - set all the default to input parameters # All parameters are optional. update_params set all missing parameters to "any" update_params(params) = updated_params { updated_params := { "direction": lib.get_default(params, "direction", "any"), "rule_type": lib.get_default(params, "rule_type", "any"), "port": lib.get_default(params, "port", "any"), "protocol": lib.get_default(params, "protocol", "any"), "source_ranges": lib.get_default(params, "source_ranges", ["any"]), "target_ranges": lib.get_default(params, "target_ranges", ["any"]), "source_tags": lib.get_default(params, "source_tags", ["any"]), "target_tags": lib.get_default(params, "target_tags", ["any"]), "source_service_accounts": lib.get_default(params, "source_service_accounts", ["any"]), "target_service_accounts": lib.get_default(params, "target_service_accounts", ["any"]), "enabled": lib.get_default(params, "enabled", "any"), } } is_valid(mode, fw_rule, params) { mode == "denylist" fw_rule_is_restricted(fw_rule, params) } is_valid(mode, fw_rule, params) { mode == "allowlist" not fw_rule_is_restricted(fw_rule, params) } # fw_rule_is_restricted for Ingress rules fw_rule_is_restricted(fw_rule, params) { # check direction fw_rule_check_direction(fw_rule, params.direction) # check rule type fw_rule_check_rule_type(fw_rule, params.rule_type) ip_configs := fw_rule_get_ip_configs(fw_rule, params.rule_type) # check protocol and port fw_rule_check_protocol_and_port(ip_configs, params.protocol, params.port) # Check sources (ip ranges and/or tags and/or service accounts) fw_rule_check_all_sources(fw_rule, params) # Check targets fw_rule_check_all_targets(fw_rule, params) fw_rule_check_enabled(fw_rule, params.enabled) } #### Check direction functions # fw_rule_check_direction when direction is set to any fw_rule_check_direction(fw_rule, direction) { direction == "any" } # fw_rule_check_direction when direction is not set to any fw_rule_check_direction(fw_rule, direction) { direction != "any" lower(direction) == lower(fw_rule.direction) } #### Check Type functions # fw_rule_check_type when rule_type is set to any fw_rule_check_rule_type(fw_rule, rule_type) { rule_type == "any" } # fw_rule_check_direction when direction is not set to any fw_rule_check_rule_type(fw_rule, rule_type) { rule_type != "any" # test that the key exists in the fw_rule (allowed or denied) fw_rule[lower(rule_type)] } ##### Get IP Config from rule ### fw_rule_get_ip_configs when rule_type is set to any and rule is allowed type fw_rule_get_ip_configs(fw_rule, rule_type) = ip_configs { rule_type == "any" ip_configs = fw_rule.allowed } ### fw_rule_get_ip_configs when rule_type is set to any and rule is allowed type fw_rule_get_ip_configs(fw_rule, rule_type) = ip_configs { rule_type == "any" ip_configs = fw_rule.denied } ### fw_rule_get_ip_configs when rule_type is not set to any fw_rule_get_ip_configs(fw_rule, rule_type) = ip_configs { rule_type != "any" ip_configs = fw_rule[rule_type] } #### Check protocol + port functions # fw_rule_check_protocol when one ip_configs is set to "all" fw_rule_check_protocol_and_port(ip_configs, protocol, port) { ip_configs[_].IPProtocol == "all" } # fw_rule_check_protocol when protocol is set to any fw_rule_check_protocol_and_port(ip_configs, protocol, port) { protocol == "any" fw_rule_check_port(ip_configs[_], port) } # fw_rule_check_protocol when protocol is not set to any fw_rule_check_protocol_and_port(ip_configs, protocol, port) { protocol != "any" # Check if the protocol is in the rule ip_configs[i].IPProtocol == protocol # Check if the associated port is also a match fw_rule_check_port(ip_configs[i], port) } # fw_rule_check_port when protocol is set to any fw_rule_check_port(ip_configs, port) { port == "any" } # fw_rule_check_port when protocol is tcp, udp or all AND port is not set (i.e all ports match) fw_rule_check_port(ip_config, port) { protocol_with_ports := {"tcp", "udp", "all"} # only for protocol with ports or "all" - since it includes tcp and udp ip_config.IPProtocol == protocol_with_ports[_] # if port is not set in ip_config, any port passed as a param matches not ip_config.ports } # fw_rule_check_port when port is all and there are no ports fw_rule_check_port(ip_config, port) { port == "all" # check if the port matches not ip_config.ports } # fw_rule_check_port when port is all and the fw rule exposes ports 0-65535 fw_rule_check_port(ip_config, port) { port == "all" # check if the port matches range_match("0-65535", ip_config.ports[_]) } # fw_rule_check_port when port is all and the fw rule exposes ports 1-65535 fw_rule_check_port(ip_config, port) { port == "all" # check if the port matches range_match("1-65535", ip_config.ports[_]) } # fw_rule_check_port when port is a single number fw_rule_check_port(ip_config, port) { port != "all" port != "any" not re_match("-", port) # check if the port matches rule_ports := ip_config.ports # check if port is in one of rule_ports values port_is_in_values(port, rule_ports[_]) } # fw_rule_check_port when port is a range (e.g 100-200) fw_rule_check_port(ip_config, port) { port != "all" port != "any" re_match("-", port) # check if the port range is included in the fw_rule port rule_ports := ip_config.ports rule_port := rule_ports[_] # check if port range is included in one of rule_ports values # Note: if rule_port is not a range, range_match will return False range_match(port, rule_port) } # port_is_in_values if rule_port is not a range # Note: only called when port is not a range port_is_in_values(port, rule_port) { # check if rule_port is not a range not re_match("-", rule_port) # test if rule port matches rule_port == port } # port_is_in_values if rule_port is a range # Note: only called when port is not a range port_is_in_values(port, rule_port) { # check if rule_port is a range re_match("-", rule_port) # build a simple port-port range to test if it belongs to rule_port range port_range := sprintf("%s-%s", [port, port]) # Check if port is included in rule port range_match(port_range, rule_port) } # range_match tests if test_range is included in target_range # returns true if test_range is equal to, or included in target_range range_match(test_range, target_range) { # check if target_range is a range re_match("-", target_range) # check if test_range is a range re_match("-", test_range) # getting the target range bounds target_range_bounds := split(target_range, "-") target_low_bound := to_number(target_range_bounds[0]) target_high_bound := to_number(target_range_bounds[1]) # getting the test range bounds test_range_bounds := split(test_range, "-") test_low_bound := to_number(test_range_bounds[0]) test_high_bound := to_number(test_range_bounds[1]) # check if test low bound is >= target low bound and target high bound >= test high bound test_low_bound >= target_low_bound test_high_bound <= target_high_bound } #### Check sources functions # fw_rule_check_sources returns true if all sources matches based on parameters # checks for source ranges, tags and service accounts fw_rule_check_all_sources(fw_rule, params) { # Check that source ranges AND source tags AND source service accounts match fw_rule_check_source_range(fw_rule, params.source_ranges[_]) fw_rule_check_source_tag(fw_rule, params.source_tags[_]) fw_rule_check_source_sas(fw_rule, params.source_service_accounts[_]) } # fw_rule_check_source when source range is passed fw_rule_check_source_range(fw_rule, source_range) { # test if sourceRanges exists in the rule fw_rule.sourceRanges # check that source ranges are set fw_rule_ranges = fw_rule.sourceRanges # check if any range matches # no CIDR matching logic at this time source_range == fw_rule_ranges[_] } # fw_rule_check_source_range if source_ranges is set to "*" fw_rule_check_source_range(fw_rule, source_range) { source_range == "*" # Check that at least a source range is set fw_rule.sourceRanges } # fw_rule_check_source_range if source_ranges is set to any (default) fw_rule_check_source_range(fw_rule, source_range) { source_range == "any" } # fw_rule_check_source when source tag is passed and is not "*" fw_rule_check_source_tag(fw_rule, source_tag) { source_tag != "*" # check that the rule source tags are set fw_rule_source_tags := fw_rule.sourceTags # check if the input tag matches any tag in the rule re_match(source_tag, fw_rule_source_tags[_]) } # fw_rule_check_source_tag if source tag is set to "*" fw_rule_check_source_tag(fw_rule, source_tag) { source_tag == "*" # Verify that we have a source tag set, regardless of its value fw_rule.sourceTags } # fw_rule_check_source_tag if source tag is set to any (default) fw_rule_check_source_tag(fw_rule, source_tag) { source_tag == "any" } # fw_rule_check_source when source service account is passed and is not "*" fw_rule_check_source_sas(fw_rule, source_service_account) { source_service_account != "*" # check that source service accounts are set fw_rule_source_sas = fw_rule.sourceServiceAccounts # check if the rule service account matches re_match(source_service_account, fw_rule_source_sas[_]) } # fw_rule_check_source_sas if source service account is set to "*" fw_rule_check_source_sas(fw_rule, source_service_account) { source_service_account == "*" # Verify that we have a source service account set, regardless of its value fw_rule.sourceServiceAccounts } # fw_rule_check_source_sas if source service account is set to any fw_rule_check_source_sas(fw_rule, source_service_account) { source_service_account == "any" } #### Check targets functions # fw_rule_check_targets returns true if all targets match based on parameters # checks for target ranges, tags and service accounts fw_rule_check_all_targets(fw_rule, params) { # Check that target ranges AND target tags AND target service accounts match fw_rule_check_target_range(fw_rule, params.target_ranges[_]) fw_rule_check_target_tag(fw_rule, params.target_tags[_]) fw_rule_check_target_sas(fw_rule, params.target_service_accounts[_]) } # fw_rule_check_target when target range is passed fw_rule_check_target_range(fw_rule, target_range) { # test if destinationRanges exists in the rule fw_rule.destinationRanges # check that target ranges are set fw_rule_ranges = fw_rule.destinationRanges # check if any range matches # no CIDR matching logic at this time target_range == fw_rule_ranges[_] } # fw_rule_check_target_range if target_ranges is set to "*" fw_rule_check_target_range(fw_rule, target_range) { target_range == "*" # Check that at least a target range is set fw_rule.destinationRanges } # fw_rule_check_target_range if target_ranges is set to any (default) fw_rule_check_target_range(fw_rule, target_range) { target_range == "any" } # fw_rule_check_target when target tag is passed and is not "*" fw_rule_check_target_tag(fw_rule, target_tag) { target_tag != "*" # check that the rule target tags are set fw_rule_target_tags := fw_rule.targetTags # check if the input tag matches any tag in the rule re_match(target_tag, fw_rule_target_tags[_]) } # fw_rule_check_target_tag if target tag is set to "*" fw_rule_check_target_tag(fw_rule, target_tag) { target_tag == "*" # Verify that we have a target tag set, regardless of its value fw_rule.targetTags } # fw_rule_check_target_tag if target tag is set to any (default) fw_rule_check_target_tag(fw_rule, target_tag) { target_tag == "any" } # fw_rule_check_target when target service account is passed and is not "*" fw_rule_check_target_sas(fw_rule, target_service_account) { target_service_account != "*" # check that target service accounts are set fw_rule_target_sas = fw_rule.targetServiceAccounts # check if the rule service account matches re_match(target_service_account, fw_rule_target_sas[_]) } # fw_rule_check_target_sas if target service account is set to "*" fw_rule_check_target_sas(fw_rule, target_service_account) { target_service_account == "*" # Verify that we have a target service account set, regardless of its value fw_rule.targetServiceAccounts } # fw_rule_check_target_sas if target service account is set to any fw_rule_check_target_sas(fw_rule, target_service_account) { target_service_account == "any" } #### Check enabled functions # fw_rule_check_enabled when enabled is set to any fw_rule_check_enabled(fw_rule, enabled) { enabled == "any" } # fw_rule_check_enabled when enabled is not set to any fw_rule_check_enabled(fw_rule, enabled) { enabled != "any" # the following test only works when enabled is a boolean too is_boolean(enabled) enabled != fw_rule.disabled } # fw_rule_check_enabled when enabled is not set to "true" (string) # This function is just for convenience when the enabled parameter is a string instead of a boolean fw_rule_check_enabled(fw_rule, enabled) { enabled != "any" is_string(enabled) # this is necessary as cast_boolean does not work on strings... lower(enabled) == "true" not fw_rule.disabled } # fw_rule_check_enabled when enabled is not set to "false" (string) # This function is just for convenience when the enabled parameter is a string instead of a boolean fw_rule_check_enabled(fw_rule, enabled) { enabled != "any" is_string(enabled) # this is necessary as cast_boolean does not work on strings... lower(enabled) == "false" fw_rule.disabled } # Check for exempted resources in regex mode check_for_exemptions(asset_name, constraint, exemptions_mode) { exemptions_mode == "regex" lib.get_constraint_params(constraint, params) exempt_list := params.exemptions not re_match_name(asset_name, exempt_list) } # Check for exempted resources in exact mode (default) check_for_exemptions(asset_name, constraint, exemptions_mode) { exemptions_mode == "exact" lib.get_constraint_params(constraint, params) exempt_list := params.exemptions matches := {asset_name} & cast_set(exempt_list) count(matches) == 0 } # Check for empty exemption mode check_for_exemptions(asset_name, constraint, exemptions_mode) { lib.has_field(constraint.spec.parameters, "exemptions") == false } re_match_name(name, filters) { re_match(filters[_], name) } #ENDINLINE