Skip to content

Latest commit

 

History

History
355 lines (297 loc) · 13.9 KB

constraint_template_authoring.md

File metadata and controls

355 lines (297 loc) · 13.9 KB

How to write your own custom constraint templates

This document is for advanced users who wish to create custom constraint templates.

Table of Contents

Template Authoring Convention

Naming

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").

Commentary

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.

Validate your constraint goals and target resources

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.

Gather sample resource data

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 validator-support@google.com.

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"
          }
        ],
      }
    }
  },
]

Write Rego rule for constraint template

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"
}

Write constraint and resource fixtures for your constraint template

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.

Write Rego tests for your rule

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)
}

Create constraint template YAML definition

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.

Updating from v1alpha1 templates

To upgrade old templates from v1alpha1 to v1beta1, make the following changes:

  1. Update the constraint template rego:
    • Rename the deny rule to violation
    • Replace input.asset with input.review
    • Replace input.constraint.spec.parameters (or lib.get_constraint_params(constraint, params)) with input.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-in object.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")
  2. Update constraint template yaml:
    • Change spec.targets to take a list of objects with target and rego 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 of spec.crd.spec.names.kind
  3. Update apiVersion for constraints and constraint templates to be v1beta1 instead of v1alpha1

Contact Info

Questions or comments? Please contact validator-support@google.com.