Skip to content
This repository was archived by the owner on Aug 20, 2025. It is now read-only.

Commit 5036f36

Browse files
committed
adds firewall whitelisting feature
1 parent c8b2999 commit 5036f36

File tree

9 files changed

+1274
-0
lines changed

9 files changed

+1274
-0
lines changed
Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
1+
# Copyright 2019 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
16+
17+
# This template enables you to create a list of "whitelist" rules that are
18+
# compliant with your regulations.
19+
# Each firewall rule in your GCP projects is checked against these whitelist rules.
20+
# If there is a match, then no alerts are triggered. If there is no match, then
21+
# that firewall is alerted.
22+
23+
# It is possible to use regex, port ranges and IP CIDR ranges to define whitelists.
24+
# For instance:
25+
# - port: "1-100" covers "80" but not "443"
26+
# - sourceRange: "10.128.0.0/16" covers "10.128.1.0/24" but not "10.0.0.0/24". 0.0.0.0/0 covers all the ranges
27+
# - sourceTags, targetTags, sourceServiceAccounts, targetServiceAccounts can be defined via regular expression statements
28+
# - IPProtocol can be a list of protocols.
29+
30+
# The overall logic is as follows:
31+
# Raise an alert if a firewall rule is not listed by any of the whitelist rules defined in this constraint file:
32+
# 1. Do the direction (ingress/egress) match?
33+
# 2. Do both firewall rule and whitelist rule have the same fields defined? No more no less.
34+
# 3. Do the IP and their ports match? IPs are checked by equality while ports are checked via ranges. See above as an example.
35+
# 4. Check that whitelist sourceRange/destinationRange CIDR overlaps the whole firewall rule's source range if a source range exist.
36+
# 5. Check regex match for sourceServiceAccounts, sourceTags, targetTags, and targetServiceAccounts.
37+
# All the values in a firewall rule should be whitelisted. PARTIAL matches are NOT enough.
38+
39+
40+
# WARNINGS:
41+
# - partial matches are NOT good enough. A firewall rule should be fully covered by the whitelist rules.
42+
# - some fields like sourceTags and sourceServiceAccounts
43+
# can NOT exist at the same time in a GCP firewall rule. Therefore, please create separate rules for each.
44+
# - As hinted above, to have a match every defined field should exist in both firewall rule and whitelist rule.
45+
# If you try to create a rule for ingress, tcp, 22, from 0.0.0.0/0,
46+
# it does NOT cover ingress, tcp, 22, from 0.0.0.0/0, targetTags = ["https"] since targetTags is not defined in
47+
# whitelisting.
48+
49+
apiVersion: templates.gatekeeper.sh/v1alpha1
50+
kind: ConstraintTemplate
51+
metadata:
52+
name: gcp-network-firewall-whitelist-v1
53+
annotations:
54+
# Example of tying a template to a CIS benchmark
55+
benchmark: CIS11_5.03
56+
spec:
57+
crd:
58+
spec:
59+
names:
60+
kind: GCPNetworkFirewallWhitelistConstraintV1
61+
plural: gcpnetworkfirewallwhitelistconstraintsv1
62+
validation:
63+
openAPIV3Schema:
64+
properties:
65+
rules:
66+
type: array
67+
items:
68+
type: object
69+
properties:
70+
direction:
71+
type: string
72+
enum: [ingress, egress]
73+
sourceServiceAccounts:
74+
type: array
75+
items: string
76+
sourceTags:
77+
type: array
78+
items: string
79+
sourceRanges:
80+
type: array
81+
items: string
82+
destinationRanges:
83+
type: array
84+
items: string
85+
targetServiceAccounts:
86+
type: array
87+
items: string
88+
targetTags:
89+
type: array
90+
items: string
91+
allowed:
92+
type: array
93+
items: object
94+
properties:
95+
IPProtocol:
96+
type: string
97+
ports:
98+
type: array
99+
items: string
100+
denied:
101+
type: array
102+
items: object
103+
properties:
104+
IPProtocol:
105+
type: string
106+
ports:
107+
type: array
108+
items: string
109+
targets:
110+
validation.gcp.forsetisecurity.org:
111+
rego: | #INLINE("validator/gcp_network_firewall_whitelist.rego")
112+
#
113+
# Copyright 2019 Google LLC
114+
#
115+
# Licensed under the Apache License, Version 2.0 (the "License");
116+
# you may not use this file except in compliance with the License.
117+
# You may obtain a copy of the License at
118+
#
119+
# http://www.apache.org/licenses/LICENSE-2.0
120+
#
121+
# Unless required by applicable law or agreed to in writing, software
122+
# distributed under the License is distributed on an "AS IS" BASIS,
123+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
124+
# See the License for the specific language governing permissions and
125+
# limitations under the License.
126+
#
127+
128+
package templates.gcp.GCPNetworkFirewallWhitelistConstraintV1
129+
130+
import data.validator.gcp.lib as lib
131+
132+
###########################
133+
# Find Whitelist Violations
134+
###########################
135+
136+
deny[{
137+
"msg": message,
138+
"details": metadata,
139+
}] {
140+
constraint := input.constraint
141+
lib.get_constraint_params(constraint, params)
142+
143+
asset := input.asset
144+
asset.asset_type == "compute.googleapis.com/Firewall"
145+
146+
fw_rule := asset.resource.data
147+
148+
not is_there_any_match(fw_rule, params.rules)
149+
message := sprintf("%s Firewall rule is prohibited.", [asset.name])
150+
metadata := {
151+
"resource": asset.name,
152+
"allowed_rules": params.rules,
153+
}
154+
}
155+
156+
is_there_any_match(fw_rule, param_rules) {
157+
check_any_match(fw_rule, param_rules[_])
158+
}
159+
160+
check_any_match(fw_rule, param_rule) {
161+
lower(fw_rule.direction) == lower(param_rule.direction)
162+
163+
do_all_fields_exist_in_both(fw_rule, param_rule)
164+
165+
# check sourceRanges or destinationRanges overlap if they are defined
166+
do_source_destination_cidr_ranges_overlap( fw_rule, param_rule)
167+
168+
regex_array_fields := [
169+
"sourceTags",
170+
"sourceServiceAccounts",
171+
"targetTags",
172+
"targetServiceAccounts",
173+
]
174+
175+
do_all_regex_fields_match(fw_rule, param_rule, regex_array_fields)
176+
177+
do_all_protocols_and_ports_match(fw_rule, param_rule)
178+
}
179+
180+
######### CHECKING IP RANGES ############
181+
182+
# We check either source or destination ranges match
183+
do_source_destination_cidr_ranges_overlap(fw_rule, param_rule) {
184+
param_rule.direction == "ingress"
185+
do_cidr_ranges_overlap(fw_rule, param_rule, "sourceRanges")
186+
}
187+
188+
do_source_destination_cidr_ranges_overlap(fw_rule, param_rule) {
189+
param_rule.direction == "egress"
190+
do_cidr_ranges_overlap(fw_rule, param_rule, "destinationRanges")
191+
}
192+
193+
do_cidr_ranges_overlap(fw_rule, param_rule, field_name) {
194+
# if there are no ranges defined, simply skip
195+
# be aware that we have already checked the existence
196+
# or non-existence of sourceRanges in both
197+
# parameter and firewall rules
198+
not lib.has_field(param_rule, field_name)
199+
}
200+
201+
do_cidr_ranges_overlap(fw_rule, param_rule, field_name) {
202+
overlapped_ranges := [t | t := fw_rule[field_name][i]; any_cidr_overlap(t, param_rule[field_name])]
203+
count(overlapped_ranges) == count(fw_rule[field_name])
204+
}
205+
206+
any_cidr_overlap(fw_cidr_range, param_cidr_ranges) {
207+
net.cidr_contains(param_cidr_ranges[_], fw_cidr_range)
208+
}
209+
210+
######### CHECKING TYPE ############
211+
212+
check_type(fw_rule) = "allowed" {
213+
lib.has_field(fw_rule, "allowed")
214+
}
215+
216+
check_type(fw_rule) = "denied" {
217+
lib.has_field(fw_rule, "denied")
218+
}
219+
220+
######### CHECKING PROTOCOL PORT MATCH ############
221+
222+
do_all_protocols_and_ports_match(fw_rule, param_rule) {
223+
allowed_or_denied := check_type(fw_rule)
224+
225+
# we collect all the protocol-port pairs that are covered by the parameters
226+
matched_ones := [t | t := fw_rule[allowed_or_denied][i]; does_a_protocol_and_port_match(t, param_rule[allowed_or_denied])]
227+
228+
count(matched_ones) == count(fw_rule[allowed_or_denied])
229+
}
230+
231+
does_a_protocol_and_port_match(fw_protocol_port, param_protocols_ports) {
232+
any_protocol_port_match(fw_protocol_port, param_protocols_ports[_])
233+
}
234+
235+
any_protocol_port_match(fw_proto_port, param_proto_port) {
236+
lower(fw_proto_port.IPProtocol) == lower(param_proto_port.IPProtocol)
237+
lib.has_field(fw_proto_port, "ports")
238+
lib.has_field(param_proto_port, "ports")
239+
all_ports_match(fw_proto_port.ports, param_proto_port.ports)
240+
}
241+
242+
any_protocol_port_match(fw_proto_port, param_proto_port) {
243+
lower(fw_proto_port.IPProtocol) == lower(param_proto_port.IPProtocol)
244+
not lib.has_field(fw_proto_port, "ports")
245+
not lib.has_field(param_proto_port, "ports")
246+
}
247+
248+
all_ports_match(fw_ports, param_ports) {
249+
trace(sprintf("matching fw_ports %v to param_ports %v", [fw_ports, param_ports]))
250+
matched_ports := [t | t := fw_ports[i]; do_ports_match(t, param_ports[_])]
251+
trace(sprintf("matched fw_ports %v ", [matched_ports]))
252+
count(matched_ports) == count(fw_ports)
253+
}
254+
255+
do_ports_match(fw_port, param_port) {
256+
fw_port == param_port
257+
}
258+
259+
do_ports_match(fw_port, param_port) {
260+
# If both are ranges
261+
contains(fw_port, "-")
262+
contains(param_port, "-")
263+
range_match(fw_port, param_port)
264+
}
265+
266+
do_ports_match(fw_port, param_port) {
267+
# If fw_port is a range but not parameter
268+
contains(fw_port, "-")
269+
not contains(param_port, "-")
270+
param_port_range := sprintf("%s-%s", [param_port, param_port])
271+
range_match(fw_port, param_port_range)
272+
}
273+
274+
do_ports_match(fw_port, param_port) {
275+
# If param_port is a range but not the actual firewall port
276+
not contains(fw_port, "-")
277+
contains(param_port, "-")
278+
fw_port_range := sprintf("%s-%s", [fw_port, fw_port])
279+
range_match(fw_port_range, param_port)
280+
}
281+
282+
# range_match tests if test_range is included in target_range
283+
# returns true if test_range is equal to, or included in target_range
284+
range_match(test_range, target_range) {
285+
# check if target_range is a range
286+
re_match("-", target_range)
287+
288+
# check if test_range is a range
289+
re_match( "-", test_range)
290+
291+
# getting the target range bounds
292+
target_range_bounds := split(target_range, "-")
293+
target_low_bound := to_number(target_range_bounds[0])
294+
target_high_bound := to_number(target_range_bounds[1])
295+
296+
# getting the test range bounds
297+
test_range_bounds := split(test_range, "-")
298+
test_low_bound := to_number(test_range_bounds[0])
299+
test_high_bound := to_number(test_range_bounds[1])
300+
301+
# check if test low bound is >= target low bound and target high bound >= test high bound
302+
test_low_bound >= target_low_bound
303+
304+
test_high_bound <= target_high_bound
305+
}
306+
307+
######### CHECKING REGEX MATCH FOR TAGS and SERVICE ACCOUNTS ############
308+
309+
do_all_regex_fields_match(fw_rule, param_rule, field_names) {
310+
matched_fields := [t | t := field_names[i]; do_fields_match(fw_rule, param_rule, t)]
311+
count(matched_fields) == count(field_names)
312+
}
313+
314+
do_fields_match(fw_rule, param_rule, field_name) {
315+
# if the field does not exist in both, that's fine.
316+
not lib.has_field(fw_rule, field_name)
317+
not lib.has_field(param_rule, field_name)
318+
}
319+
320+
# all the fields inside a firewall rules should be matched
321+
# to achive this, we filter out matched ones. If any unmatched is left
322+
# return false.
323+
do_fields_match(fw_rule, param_rule, field_name) {
324+
matched_firewall_tags_sas := [t | t := fw_rule[field_name][i]; any_fw_tag_sa_match(t, param_rule[field_name])]
325+
trace(sprintf("field_name %v, matched_firewall_tags_sas %v, firewall items %v, tags_sas_param %v", [field_name, matched_firewall_tags_sas, fw_rule[field_name], param_rule[field_name]]))
326+
327+
# if all the network tags of the firewall rule matches,
328+
# then this rule is covered
329+
count(fw_rule[field_name]) == count(matched_firewall_tags_sas)
330+
}
331+
332+
any_fw_tag_sa_match(fw_rule_items, tags_sas_param) {
333+
re_match(tags_sas_param[_], fw_rule_items)
334+
}
335+
336+
######### CHECKING EXISTENCE OF FIELDS ############
337+
338+
# make sure that if a field is defined in fw_rule it should exist in parameter as well.
339+
# and vice-versa. Any deviation means not match.
340+
# We don't check protocol and port since they are displayed differently
341+
do_all_fields_exist_in_both(fw_rule, param_rule) {
342+
fields := [
343+
"sourceTags",
344+
"sourceRanges",
345+
"sourceServiceAccounts",
346+
"targetTags",
347+
"targetServiceAccounts",
348+
"destinationRanges",
349+
"allowed",
350+
"denied",
351+
"direction",
352+
]
353+
354+
field_in_both(fw_rule, param_rule, fields[_])
355+
}
356+
357+
field_in_both(fw_rule, param_rule, field) {
358+
lib.has_field(fw_rule, field)
359+
lib.has_field(param_rule, field)
360+
}
361+
362+
field_in_both(fw_rule, param_rule, field) {
363+
not lib.has_field(fw_rule, field)
364+
not lib.has_field(param_rule, field)
365+
}
366+
#ENDINLINE

0 commit comments

Comments
 (0)