In earlier articles from this series, we have demonstrated how to use Open Policy Agent (OPA) with Kubermatic Kubernetes Platform. Open Policy Agent uses its own native language, Rego, to define queries. This tutorial presents an overview of the main features of Rego which will allow you to implement your own policy in detail.
Rego is a declarative language, which means that you can state what your queries should return instead of describing how to do it. It is designed to work with the nested structure of JSON and YAML documents.
In this tutorial, we will show you some examples from the documentation and explain which features of Rego have been used.
The following code is from the demo repository with examples of Open Policy Agent usage. It defines a policy that limits image repos for containers to a list of allowed repos and outputs a descriptive error message in case of a violation.
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: k8sallowedrepos
spec:
crd:
spec:
names:
kind: K8sAllowedRepos
validation:
# Schema for the `parameters` field
openAPIV3Schema:
properties:
repos:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sallowedrepos
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
satisfied := [good | repo = input.parameters.repos[_] ; good = startswith(container.image, repo)]
not any(satisfied)
msg := sprintf("container <%v> has an invalid image repo <%v>, allowed repos are %v", [container.name, container.image, input.parameters.repos])
}
violation[{"msg": msg}] {
container := input.review.object.spec.initContainers[_]
satisfied := [good | repo = input.parameters.repos[_] ; good = startswith(container.image, repo)]
not any(satisfied)
msg := sprintf("container <%v> has an invalid image repo <%v>, allowed repos are %v", [container.name, container.image, input.parameters.repos])
}
The Rego part is separated by rego: |
.
The line violation[{"msg": msg}]
specifies that in case of a violation, an error message will be stored in the variable msg
. How exactly that error message should look is defined a few lines down, when the msg
variable gets assigned.
Now, look at the line container := input.review.object.spec.containers[_]
which introduces several important concepts.
Input
is a reserved global variable that contains the object that is handed to OPA for policy review.- Scalar values are created as
valuename :- value
. Here you create a scalar value namedcontainer
. - The dot operator
.
lets you walk through the nested hierarchy of a YAML file. You can select for parameters as deeply nested as you want. - The underscore operator
_
stands for a variable which is not used outside of this query. Rego instantiates it at runtime with a unique variable. You can use the placeholder_
several times in a query; they will get instantiated with different variables.
The following example is a policy which states that a resource must provide all labels that it has keys for and they have to match the regular expression provided in allowedRegex
.
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: k8srequiredlabels
spec:
crd:
spec:
names:
kind: K8sRequiredLabels
validation:
# Schema for the `parameters` field
openAPIV3Schema:
properties:
message:
type: string
labels:
type: array
items:
type: object
properties:
key:
type: string
allowedRegex:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredlabels
get_message(parameters, _default) = msg {
not parameters.message
msg := _default
}
get_message(parameters, _default) = msg {
msg := parameters.message
}
violation[{"msg": msg, "details": {"missing_labels": missing}}] {
provided := {label | input.review.object.metadata.labels[label]}
required := {label | label := input.parameters.labels[_].key}
missing := required - provided
count(missing) > 0
def_msg := sprintf("you must provide labels: %v", [missing])
msg := get_message(input.parameters, def_msg)
}
violation[{"msg": msg}] {
value := input.review.object.metadata.labels[key]
expected := input.parameters.labels[_]
expected.key == key
# do not match if allowedRegex is not defined, or is an empty string
expected.allowedRegex != ``
not re_match(expected.allowedRegex, value)
def_msg := sprintf("Label <%v: %v> does not satisfy allowed regex: %v", [key, value, expected.allowedRegex])
msg := get_message(input.parameters, def_msg)
}
In this example you can see how to create functions within Rego.
The function get_message
is created twice with the same parameters, because it looks whether the field parameters.message
is specified. If this is not the case, the first definition of the function is executed and it returns the default message. If a message is provided, the function returns the message.
The first violation field in this example returns an additional parameter details
. You can use that parameter to return the result of variables in your code which provide additional insight in the violation. In this example, it returns the variable in which the missing labels are stored.
This example also displays all three forms of equality operators in Rego.
The operator x := 0
declares a local variable x
and assigns it the value 0.
The operator x == 0
checks whether the value of x
is 0.
These two operators can only be used within rules.
The operator x = 0
can be used outside of rules. It functions both as a boolean and an assignment operator. In case the variable x
has a value, it compares that value to 0. Otherwise it assigns the value 0 to x
.
This example also shows the two kinds of strings that can be used within Rego. You use the backticks for a raw string, and the quotes "" for an interpolated string in which you can print the value of variables.
You can also use regular expressions in Rego strings. This example checks if the policy defines a suitable regular expression.
The following example defines a policy that checks whether the container object specifies the required probes.
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: k8srequiredprobes
spec:
crd:
spec:
names:
kind: K8sRequiredProbes
validation:
openAPIV3Schema:
properties:
probes:
type: array
items:
type: string
probeTypes:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredprobes
probe_type_set = probe_types {
probe_types := {type | type := input.parameters.probeTypes[_]}
}
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
probe := input.parameters.probes[_]
probe_is_missing(container, probe)
msg := get_violation_message(container, input.review, probe)
}
probe_is_missing(ctr, probe) = true {
not ctr[probe]
}
probe_is_missing(ctr, probe) = true {
probe_field_empty(ctr, probe)
}
probe_field_empty(ctr, probe) = true {
probe_fields := {field | ctr[probe][field]}
diff_fields := probe_type_set - probe_fields
count(diff_fields) == count(probe_type_set)
}
get_violation_message(container, review, probe) = msg {
msg := sprintf("Container <%v> in your <%v> <%v> has no <%v>", [container.name, review.kind.kind, review.object.metadata.name, probe])
}
This example shows how the same violation can be true for different scenarios. In this case, the function probe_is_missing is defined for both the cases where the field probe is not defined and where it is empty. Both of these cases return true, meaning they violate the policy.
Testing
Rego provides a testing framework which you can use to test your policies before you execute them in production. It lets you define an example object and specify how OPA is expected to react to it. As an example, this is a simple policy and a test for it:
Policy:
package kubernetes.admission
deny[msg] {
input.request.kind.kind == "Pod"
image := input.request.object.spec.containers[_].image
not startswith(image, "hooli.com")
msg := sprintf("image fails to come from trusted registry: %v", [image])
}
Here is a Unit Test that checks that the policy violation was detected as expected:
package kubernetes.test_admission
import data.kubernetes.admission
test_image_safety {
unsafe_image := {
"request": {
"kind": {"kind": "Pod"},
"object": {
"spec": {
"containers": [
{"image": "hooli.com/nginx"},
{"image": "busybox"}
]
}
}
}
}
expected := "image 'busybox' comes from untrusted registry"
admission.deny[expected] with input as unsafe_image
}
Editor Plugins
If you have a favorite IDE or editor, check this list to see if there is a Rego plugin for it already. At the moment of the article, there are integration plugins for syntax highlighting and linting for Atom, Emacs, IntelliJ IDEA, Sublime Text, TextMate, Vim and VS Code.
Rego Playground
You can use the Rego Playground to test your policies. It provides helpful examples and shows you the input, data and output of your Rego code in helpful panels.
Conclusion
Using this guide, you can understand the basics of the Rego language and review examples of how to create a policy. Additionally, we showed you how to test a policy before it is put in production.
If you have any questions or want to get in touch with our team, contact us here.