This document is for advanced users who wish to create custom constraint templates.
Table of Contents
- Template Authoring Convention
- Validate your constraint goals and target resources
- Gather sample resource data
- Write Rego rule for constraint template
- Write constraint and resource fixtures for your constraint template
- Write Rego tests for your rule
- Create constraint template YAML definition
- Contact Info
The template name appears in three places in a template YAML file:
- metadata name: All lower case with "-" as the separator. It has the format of "gcp-{resource}-{feature}-{version}" (example: "gcp-storage-logging-v1").
- CRD kind (under "spec" > "crd" > "spec" > "names" > "kind"): Camel case. It has the format of "GCP{resource}{feature}Constraint{version}" (example: "GCPStorageLoggingConstraintV1").
Wherever possible, follow gcloud group names for resource naming. For example, use "compute" instead of "gce", "sql" instead of "cloud-sql", and "container-cluster" instead of "gke".
If a template applies to more than one type of resource, omit the resource part and only include the feature (example: "GCPExernalIPAccessV1").
The version number does not follow semver form - it is just a single number. This effectively makes every version of a template an unique template. See Constraint Framework: Versioning for reasons behind this convention.
The configuration YAML file name should take after the metadata name and replace "-" with "_" (example: "gcp_storage_logging_v1.yaml").
Every configuration YAML file should have a summary at the top to describe the constraint. In the parameters section, every parameter should include a description field to explain what the parameter does.
Before beginning to develop your constraint template, you should write a concrete definition of your goals in plain language. In writing this definition, clearly define what resources you're looking to scan or analyze, and what properties of those resources you plan to constrain.
For example:
The External IP Access Constraint will scan GCP VM instances and validate that the Access Config of their network interface does not include an external IP address.
Before proceeding to develop your template, you should verify that Cloud Asset Inventory supports the resources you want. Assuming it does, you should gather some sample data to use in developing and testing your rule by creating resources of the appropriate type and creating a CAI export of those resources (see CAI quickstart). If the desired resource is not supported, please open a GitHub issue and/or email [email protected].
For example, you might gather the following JSON export for external IP address
constraint (for brevity, most fields are omitted). In the same data below, the
presence of the externalIp
field indicates that an external IP address is
assigned to the VM.
[
{
"name": "//compute.googleapis.com/projects/test-project/zones/us-east1-b/instances/vm",
"asset_type": "google.compute.Instance",
"resource": {
"version": "v1",
"discovery_document_uri": "https://www.googleapis.com/discovery/v1/apis/compute/v1/rest",
"discovery_name": "Instance",
"parent": "//cloudresourcemanager.googleapis.com/projects/68478495408",
"data": {
"name": "vm-external-ip",
"networkInterfaces": [
{
"accessConfigs": [
{
"externalIp": "35.196.151.107",
"name": "external-nat",
"networkTier": "PREMIUM",
"type": "ONE_TO_ONE_NAT"
}
],
"fingerprint": "FKYLBaTiCF0=",
"ipAddress": "10.142.0.2",
"name": "nic0",
"network": "https://www.googleapis.com/compute/v1/projects/test-project/global/networks/default",
"subnetwork": "https://www.googleapis.com/compute/v1/projects/test-project/regions/us-east1/subnetworks/default"
}
],
}
}
},
]
In order to develop a constraint template, you must develop a Rego rule to back it. Before you begin, read about how to write policies using Rego and Open Policy Agent.
To store a rule for your constraint template, create a new Rego file (for
example, vm_external_ip.rego
). This file should include a
single violation
rule which returns violations by evaluating
whether a given input.review
(an asset) violates the
input.parameters
defined in a constraint.
As you develop the Rego rule, keep these principles in mind:
- Logic can be externalized into additional rules and functions which should
be defined below the
violation
rule in a utilities section. - If your rule only applies to particular resource types, you should check
that the given
input.review
is of the required type early on. (for example,input.review.asset_type == "google.compute.Instance"
). - If your rule requires input parameters, they will be present under
input.parameters
. - Comments should be included for any complicated logic and all helper functions and rules should have a comment explaining their intent.
- Equality comparison should be done using
==
to differentiate it from assignment. - A violation is generated only when the rule body evaluates to true. In other words, you should look for the negative condition.
For example, this rule checks whether a VM with external IP address should be exempted (allowlist) or treated as a violation (denylist):
package validator.gcp.GCPExternalIpAccessConstraintV1
violation[{
"msg": message,
"details": metadata,
}] {
parameters := input.parameters
asset := input.review
asset.asset_type == "google.compute.Instance"
# Find network access config block w/ external IP
instance := asset.resource.data
access_config := instance.networkInterface[_].accessConfig
external_ip := access_config[_].externalIp
# Check if instance is in allowlist/denylist
target_instances := parameters.instances
matches := {asset.name} & cast_set(target_instances)
target_instance_match_count(parameters.mode, desired_count)
count(matches) == desired_count
message := sprintf("%v is not allowed to have an external IP.", [asset.name])
metadata := {"external_ip": external_ip}
}
# Determine the overlap between instances under test and constraint
# By default (allowlist), we violate if there isn't overlap
target_instance_match_count(mode) = 0 {
mode != "denylist"
}
target_instance_match_count(mode) = 1 {
mode == "denylist"
}
To test your rule, create fixtures of the expected resources and constraints
leveraging your rule. To implement your test cases, gather resource fixtures
from CAI and place them in a
test/fixtures/resources/<resource_type>/data.json
file.
You can also write a constraint fixture using your constraint template and place
it in
test/fixtures/constraints/<constraint_name>/data.yaml
.
For example, here is a sample constraint used for external IP rule:
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: GCPExternalIpAccessConstraintV1
metadata:
name: forbid-external-ip-allowlist
spec:
severity: high
match:
ancestries: ["organizations/**"]
parameters:
mode: "allowlist"
instances:
- //compute.googleapis.com/projects/test-project/zones/us-east1-b/instances/vm-external-ip
The rule above says that the external IP constraint applies to all
organizations, but the GCE instance vm-external-ip
under test-project
in
us-east1-b
is exempt.
As you develop your constraint template, implement test cases that ensure your
logic doesn't break over time. Open Policy Agent allows you to
implement simple tests
by prefixing rules with test_
.
Using the fixtures you have gathered, write tests in a Rego file named after
your rule. For example, vm_external_ip_test.rego
. Make
sure to place this Rego file in the same package as your rule with the
package
definition. One useful pattern is to write a rule which
gathers all violations for your test cases and additional test_
rules which verify those violations.
For example, here are the tests for the above external IP constraint:
package validator.gcp.GCPExternalIpAccessConstraintV1
import data.test.fixtures.assets.compute_instances as fixture_instances
import data.test.fixtures.parameters as fixture_parameters
# Find all violations on our test cases
find_violations[violation] {
instance := data.instances[_]
parameters := data.test_parameters[_]
issues := violation with input.review as instance
with input.parameters as parameters
total_issues := count(issues)
violation := issues[_]
}
allowlist_violations[violation] {
parameters := [fixture_parameters.forbid_external_ip_allowlist]
found_violations := find_violations with data.instances as fixture_instances
with data.test_parameters as parameters
violation := found_violations[_]
}
# Confirm only a single violation was found (allowlist constraint)
test_external_allowlist_ip_violates_one {
found_violations := allowlist_violations
count(found_violations) = 1
violation := found_violations[_]
resource_name := "//compute.googleapis.com/projects/test-project/zones/us-east1-b/instances/vm-external-ip"
is_string(violation.msg)
is_object(violation.details)
}
Once you have a working Rego rule, you are ready to package it into a constraint template. You can do this by writing a YAML file which defines the expected parameters and logic for constraints. Create this file from the template, and then input the contents of your Rego rule.
This example shows the external IP constraint template, with the italicized portions changing for your template:
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: gcpexternalipaccessconstraintv1
annotations:
# Example of tying a template to a CIS benchmark
benchmark: CIS11_5.03
spec:
crd:
spec:
names:
kind: GCPExternalIpAccessConstraintV1
validation:
openAPIV3Schema:
properties:
mode:
type: string
enum: [denylist, allowlist]
instances:
type: array
items: string
targets:
- target: validation.gcp.forsetisecurity.org
rego: |
#INLINE("validator/vm_external_ip.rego")
#ENDINLINE
The Rego rule is supposed to be inlined in the YAML file. To do that, run make build
. That will format the rego rules and inline them in the YAML files under
the #INLINE
directive.
To upgrade old templates from v1alpha1 to v1beta1, make the following changes:
- Update the constraint template rego:
- Rename the
deny
rule toviolation
- Replace
input.asset
withinput.review
- Replace
input.constraint.spec.parameters
(orlib.get_constraint_params(constraint, params)
) withinput.parameters
. For example:# Old import data.validator.gcp.lib as lib constraint := input.constraint lib.get_constraint_params(constraint, params) # New - no lib required params := input.parameters
- If you were using
lib.get_default
, you can now use the built-inobject.get
instead# Old import data.validator.gcp.lib as lib destination := lib.get_default(bucket, "logging", "default") # New - no lib required destination := object.get(bucket, "logging", "default")
- Rename the
- Update constraint template yaml:
- Change spec.targets to take a list of objects with
target
andrego
keys# Old spec: targets: validation.gcp.forsetisecurity.org: rego: # rego goes here # New spec: targets: - target: validation.gcp.forsetisecurity.org rego: # rego goes here
- Ensure that
metadata.name
contains the lowercased content ofspec.crd.spec.names.kind
- Change spec.targets to take a list of objects with
- Update
apiVersion
for constraints and constraint templates to bev1beta1
instead ofv1alpha1
Questions or comments? Please contact [email protected].