Writing Rego for all the things

Co-author: Michael Fornaro & Saurabh Pandit

Image for post
Image for post

Rego is a high-level declarative language, its purpose-built for expressing policies over complex hierarchical data structures. For detailed information on Rego see the Policy Language documentation.

All the examples and Rego code can be viewed in the Raspbernetes project repository https://github.com/raspbernetes/k8s-security-policies

Overview

This will give you an introduction to writing Rego using a standard that will enable your policies to be reusable by various tools such as Conftest and Gatekeeper.

Standard

For the purpose of reusability, we came up with a standard to write policies, so they can be understandable and maintainable. To summarise the standard:

Demonstration

Library

One of the best practices in programming is to write reusable code. Therefore, a library is created for having reusable functions that can be imported into other rego files. The library can be maintained independently, so developers can write concise policies. For example, we havekubernetes.rego under lib folder which can be imported into multiple policy files. All that required is to import data.lib.kubernetes in the policy file, so the functions or rules in kubernetes.rego can be used in other files in the format of kubernetes.[function_name].

Pre-processing inputs

When gatekeeper evaluates a data file(for example, a pod.json file that can be applied to Kubernetes clusters to create a pod), it will wrap up the target into an input.review object, while Conftest will evaluate the object as is. so these inputs need to be pre-processed before they are fed to the policies. The functions below are created to abstract the input into theobject based on whether it’s from gatekeeper or not.

package lib.kubernetes# the input is from gate keeper if input has review field
is_gatekeeper {
has_field(input, "review")
has_field(input.review, "object")
}
# Take the input to be as the object that going to be evaluated if it's not gatekeeper
object = input {
not is_gatekeeper
}
# Take the input.review.object as the object that going to be evaluated if it's from gatekeeper
object = input.review.object {
is_gatekeeper
}
has_field(obj, field) {
obj[field]
}

The object will always in the same format, therefore a resource can be evaluated against a policy irrespective of the tool used.

Violation writing standard

Here is an example of a violation rule that applied our 3 statements standard

violation[msg] {   
kubernetes.clusterroles[clusterrole]
is_access_to_secrets_disallowed(clusterrole)
msg = kubernetes.format(sprintf("ClusterRole %v - access to secrets is not allowed", [clusterrole.metadata.name]))

The first line indicates it’s checking a clusterRole resource and will be the clusterRole object, the second line is the business logic of the policy and if this statement is true, the violation will occur by showing the error message that states in the third statements.

Parameters

Gatekeeper supports parameter inputs. To ensure our policy is reusable by other tools we handle parameters in the following way:”

default_parameters = {
# parameters
}
params = object.union(default_parameters, kubernetes.parameters)

More examples

We will take an example of policies that check against the K8s resources(but the standard can be applied to any structured data) to illustrate how do we achieve 3 statement violation blocks. The following code will only check if the resource is a clusterRoleby calling a function in kubernetes.rego

violation[msg] {   

kubernetes.clusterroles[clusterrole]
is_access_to_secrets_disallowed(clusterrole) msg = kubernetes.format(sprintf("ClusterRole %v - access to secrets is not allowed", [clusterrole.metadata.name]))

The implementation of the kubernetes.clusterroles simply check if the resource kind is ClusterRoleand return the clusterRole object

is_clusterrole {
kind = "ClusterRole"
}
is_clusterrole {
kind = "ClusterRoles"
}
clusterroles[clusterrole] {
is_clusterrole
clusterrole = object
}

Similarly, we can create rules to check if the input data structure is a specific type of resource.

services[service] { 
is_service
service = object
}
serviceaccounts[serviceaccount] {
is_service_account
serviceaccount = object
}
apiserver[container] {
labels.component = "kube-apiserver"
container = containers[container]
}

The kubernetes.format function in the library will output the message correctly on the tool used.

This way, when we write a new policy, we could apply this standard which will yield a much more concise and comprehensible code

Summary

As a declarative language, Rego has simplified the process for writing policies, but sometimes it can be tedious, and sometimes the same policies will be implemented in multiple places. Depending on the complexity of the policy logic, especially in the case when you have a large library of policies, the standard we use not only makes the most advantage of the Rego but also shortens the learning curve for engineers. In another article, We will also talk about how to write unit tests for the policies by utilizing our standard.

Written by

A software engineer who believes technology is the bridge to a better world and recently start to explore the beauty of K8s, a quick learner.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store