Examining Gatekeeper: Understanding Rego for OPA Gatekeeper policy development

SADA
The SADA Engineering Blog
8 min readApr 6, 2022

--

By Alain Baxter, SADA Sr. Cloud Infrastructure Engineer

Open Policy Agent (OPA) is a policy engine that simplifies policy enforcement and decouples policy decisions from your application. OPA Gatekeeper integrates OPA and Kubernetes, and builds a framework to take advantage of Kubernetes’ declarative nature to simplify the process of policy management and enforcement in the cluster. OPA Gatekeeper has become an important tool in the Kubernetes ecosystem to provide security controls and guardrails for resources in your cluster.

The open source community maintaining Gatekeeper also provides a library with many useful constraint templates ready to go so that you can kick-start your ability to provide value. Google Cloud’s Anthos Config Management Policy Controller, which is a managed and supported install of Gatekeeper in the GKE/Anthos ecosystem, provides a similar constraint template library which contains the open source library along with additional templates developed to cover security requirements like the CIS Kubernetes Benchmark recommendations.

While these existing libraries are excellent to get you started with policy enforcement in your cluster, we have found that as our clients get comfortable with Gatekeeper as a tool, they quickly think of additional ideas for impactful use cases. This is generally when custom constraint templates come into the picture. This article will help you understand how these policies are defined using the Rego policy language.

The rule block

The rule block is the most important structure in Rego policy code, and has the following syntax:

rule_name(optional, input, parameters)[optional output set/key] = optional_output_value {
# Optional rule body
}

The first thing to note is that everything other than the name is optional. This allows the rule block to take on several use cases. If the output or rule body is left out of the definition, they both will default to “true”.

When a rule block is read, it returns the defined values for the given input parameters IF the rule body is found to be true.

With just the name and output value defined, the rule block is now a constant, as shown in this example, using the value of pi as a constant:

pi := 3.14

Notice we use := instead of = here, the official syntax allows both but := is preferred for constants.

With name, input parameters, body, and output it becomes a helper function:

trim_and_split(s) = x {
t := trim(s, " ")
x := split(t, ".")
}

By outputting with square brackets we can create helper functions that generate sets or maps:

get_items_set[item] {
item := ...
}
get_items_map[key] = value {
key := ...
value := ...
}

For OPA Gatekeeper, the main entry point for a policy is the violation rule defined like so:

violation[{"msg": msg}] {
# Rule body
}

The rule body

This is where the actual policy decisions are made, inspecting the contents of the input request under review, and testing it against the requirements of the policy. The rule body will contain one or more statements to evaluate. The result of each statement is combined as a logical AND, thus the rule body as a whole is evaluated as true only if all the statements evaluate as true.

Let’s look at a simple example:

violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not startswith(container.image, "repo.example.com/")
msg := sprintf("Image %v for container %v does not come from repo.example.org/", [container.image, container.name])
}

This would read as:

The input object under review is in violation IF
The input has some containers defined AND
A containers image does not start with “repo.example.com/” AND
The returned message would be this

The first and third lines do not make any decisions unless they reference something undefined, in which case the result will also be undefined. These lines are used to collect and format the information for the decision.

This rule body example also involves another useful feature of Rego: implicit iteration. There is no FOR loop or syntax for iteration here, but we made a reference to a set of containers, and implicitly the “container” variable will now fork a new iteration of the policy evaluation for each reference. This would also work with a reference to a set. For both sets and maps, the square brackets contain the iterator reference (either index for sets or key for maps) and these can also be used if needed by placing a variable name in the square brackets. For example, input.review.object.spec.containers[container_index], however if there is no intention to use the iterator reference, then an underscore (_) can be placed there to ignore it, like in the policy example shown above.

What about logical OR, though? For that we can use another interesting feature of Rego and that is the ability to define a rule block multiple times. When a policy is evaluated, and a rule is defined multiple times then the results of all the rules are combined together as a logical OR. For example, in the sample policy above, it is just looking at containers for image violations, however initContainers are not being inspected. So if we want to get containers from the containers array OR the initContainers array then we could improve the policy as:

violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not startswith(container.image, "repo.example.com/")
msg := sprintf("Image %v for container %v does not come from repo.example.org/", [container.image, container.name])
}
violation[{"msg": msg}] {
container := input.review.object.spec.initContainers[_]
not startswith(container.image, "repo.example.com/")
msg := sprintf("Image %v for container %v does not come from repo.example.org/", [container.image, container.name])
}

Alternatively, to reduce code duplication we can use a helper function like this:

violation[{"msg": msg}] {
container := get_container[name]
not startswith(container.image, "repo.example.com/")
msg := sprintf("Image %v for container %v does not come from repo.example.org/", [container.image, name])
}
get_container[container.name] = container {
container := input.review.object.spec.containers[_]
}
get_container[container.name] = container {
container := input.review.object.spec.initContainers[_]
}

This policy will read like so:

The input object under review is in violation IF
The input has some containers OR initContainers defined AND
A containers image does not start with “repo.example.com/” AND
The returned message would be this

For specific syntax and built-in function resources, the official Rego documentation is excellent and has some interactive examples included. For more information, check out the OPA Policy Language and Policy Reference documentation.

Enforcing in Kubernetes

Now that we have some Rego policy code, how do we provide OPA Gatekeeper with this code to enforce policy in our clusters? To do this we have a Custom Resource Definition (CRD) called a ConstraintTemplate. These templates are used to provide the Rego code and additional details to Gatekeeper, however they DO NOT enforce anything on their own. Gatekeeper will dynamically create a CRD from the ConstraintTemplate that can be used to create constraints in the cluster. Let’s put the policy we used previously into a ConstraintTemplate:

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: exampleallowedrepo
spec:
crd:
spec:
names:
kind: ExampleAllowedRepo
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
violation[{"msg": msg}] {
container := get_container[name]
not startswith(container.image, "repo.example.com/")
msg := sprintf("Image %v for container %v does not come from repo.example.org/", [container.image, name])
}
get_container[container.name] = container {
container := input.review.object.spec.containers[_]
}
get_container[container.name] = container {
container := input.review.object.spec.initContainers[_]
}

In the crd section we defined the name of the dynamic constraint CRD object, and in the targets section we added our Rego with the target admission.k8s.gatekeeper.sh. This is the only supported target for Gatekeeper.

ConstraintTemplates are intended to be reusable for many constraints, so to improve our policy further we should parameterize it. Otherwise, we would need to maintain the list of allowed repositories in the Rego source code instead of allowing the constraint to specify the repositories it will allow. Parameters are added to the crd.spec.validation section of the ConstraintTemplate using the Open API v3 validation schema, and can be accessed inside the Rego code from the input.parameters.* values.

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: exampleallowedrepo
spec:
crd:
spec:
names:
kind: ExampleAllowedRepo
validation:
# Schema for the `parameters` field
openAPIV3Schema:
type: object
properties:
repos:
description: The list of prefixes a container image is allowed to have.
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
violation[{"msg": msg}] {
container := get_container[name]
matched := [matching | repo = input.parameters.repos[_] ; matching = startswith(container.image, repo)]
not any(matched)
msg := sprintf("Image %v for container %v does not come allowed repos: %v", [container.image, name, input.parameters.repos])
}
get_container[container.name] = container {
container := input.review.object.spec.containers[_]
}
get_container[container.name] = container {
container := input.review.object.spec.initContainers[_]
}

To ensure that we can add a list of allowed repository sources, we made a slight modification to the Rego to ensure that the container image matches at least one of the repo prefixes.

The square brackets will create a set, and inside the notation we build the set. Also the semi-colon (;) represents a logical AND. This example can be read like so:

We have an item in the set IF
There is a repo in the input parameters AND
The container image starts with that repo

Now we can enforce this policy in the cluster with a constraint object like this one:

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: ExampleAllowedRepo
metadata:
name: example-allowed-repo-and-gcr-google-samples
spec:
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
namespaces:
- default
parameters:
repos:
- repo.example.com/
- gcr.io/google-samples

Notice the kind matches the defined kind in the template. We use the match section to define the resources to which we will apply this policy, and the parameters section to define any parameters required by the ConstraintTemplate. There are several options on how to match resources detailed in the Gatekeeper documentation.

Conclusion

Now that you have some fundamental knowledge about how Rego defines these policies, I hope that this helps you to better understand policy code for OPA Gatekeeper so that you can read existing policies and work towards developing your own custom policies. To read about how OPA Gatekeeper works with input, check out the next article in this series.

About Alain Baxter

Alain Baxter is a Senior Cloud Infrastructure Engineer at SADA who specializes in delivering Anthos based solutions to our clients. He has worked with Kubernetes on and off for over four years, and is a champion of the devops team culture. He has contributed to the open source OPA Gatekeeper ConstraintTemplate library and engages with the community regularly. Alain can be found on LinkedIn.

About SADA

At SADA, we climb every mountain, clear every hurdle, and turn the improbable into possible — over and over again. Simply put, we propel your organization forward. It’s not enough to migrate to the cloud, it’s what you do once you’re there. Accelerating application development. Advancing productivity and collaboration. Using your data as a competitive edge. When it comes to Google Cloud, we’re not an add-on, we’re a must-have, driving the business performance of our clients with its power. Beyond our expertise and experience, what sets us apart is our people. It’s the spirit that carried us from scrappy origins as one of the Google Cloud launch partners to an award-winning global partner year after year. With a client list that spans healthcare, media and entertainment, retail, manufacturing, public sector and digital natives — we simply get the job done, every step of the way. Visit SADA.com to learn more.

If you’re interested in becoming a part of the SADA team, please visit our careers page.

--

--

Global business and cloud consulting firm | Helping CIOs and #IT leaders transform in the #cloud| 3-time #GoogleCloud Partner of the Year.