Kubermatic branding element

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 named container.
  • 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.

Irina Lindt

Irina Lindt

Software Engineer