Open Policy Agent with Kubernetes - Tutorial (Pt. 1)
Tags: cloud, cloudnative, security, kubernetes
Published: 2023-01-25 17:04:01

## Foreword

As Kubernetes has become the de-facto platform to orchestrate containerized workloads, more and more users have begun to look for ways to control and secure their Kubernetes clusters.

We have already explored quite extensively the Kubernetes [threat model](https://blog.gitguardian.com/hardening-your-k8-pt-1/), as well as the NSA/CISA hardening [guidelines](https://blog.gitguardian.com/hardening-your-k8s-pt-2/), and dove in-depth with a series of tutorials that I will put below in case you missed them:

-   [Kubernetes Hardening Tutorial Part 1: Pods](https://blog.gitguardian.com/kubernetes-tutorial-part-1-pods/)
-   [Kubernetes Hardening Tutorial Part 2: Network](https://blog.gitguardian.com/kubernetes-tutorial-part-2-network/)
-   [Kubernetes Hardening Tutorial Part 3: Authn, Authz, Logging & Auditing](https://blog.gitguardian.com/kubernetes-tutorial-part-3-authn-authz/)

Hardening is a thing for sure, but what about **enforcing policies** inside a cluster? This is a completely different task that requires a different set of tools.

As you've already guessed, an appropriate way to do that would be to define **policies as code**, and a nice tool for that is Open Policy Agent or OPA. If you don't know what I'm talking about, please take the time to read this introduction first:

[What is Policy-as-Code? An Introduction to Open Policy Agent](https://blog.gitguardian.com/what-is-policy-as-code-an-introduction-to-open-policy-agent/)

### Why not just use RBAC?

To better understand why we need to use a new tool for policies, let's take a concrete example: imagine you are a cluster administrator, and you want to restrict what can run in your cluster.

At first, it looks like a perfectly valid use case for Role-Based Access Control (RBAC, a permission system for creating and managing Kubernetes objects at the resource level): with RBAC, you can easily authorize cases such as "user X can do Y in the namespace Z".

You start by defining a Role in the `default` namespace to grant read access to pods:

Now, a bit more tricky: how would you do if you wanted to restrict, not access to resources like pods, but **how they are configured?**

This is where RBAC powers end: access controls cannot go beyond and restrict Kubernetes' objects configurations, settings, and content. Yet, as a cluster admin, you could absolutely want that!

### Why not write my own admission controller?

For the sake of the exercise, we will imagine that you need to require all resource objects in your cluster to **have a specific label.**

As you can see, this is not related to a role or a group, in fact, it is related to a very specific field for all resources in your cluster.

Suppose you need to control pod fields (or any fields in other resource types, for that matter). In that case, you have one option: creating your own [validating admission controller](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#validatingadmissionwebhook). An admission controller is a piece of code that intercepts requests to the Kubernetes API server before persistence of the object.

In detail, it would work as follows: a request to create a new pod is made to the cluster API service; this would trigger a custom ValidatingAdmissionWebhook matching this request; the controller would call a validating webhook; if the controller rejects the request, the API service would reject it as well.

The problem with this is that **it doesn't scale**: you would have to write as many custom admission controllers as rules or policies you want to enforce!

This is where OPA comes in, and we will see together how to set it up.

### Open Policy Agent in Kubernetes

To solve the challenge above, what we really need here is a system that supports multiple configurations covering different resource types and fields and allows reusability. Open Policy Agent (OPA) provides just that.

In a nutshell, the OPA policy engine evaluates requests to determine whether they conform to configured policies.

OPA can integrate with Kubernetes easily: it expects a JSON input, is easy to containerize and supports dynamic configuration, which makes it well-suited to provide policy evaluation for the Kubernetes API service.

So let's dive in and demo how to deploy and integrate OPA with Kubernetes.

___

## Tutorial: How to use OPA with Kubernetes

In this example, we will demonstrate how to integrate OPA with Kubernetes by deploying a policy that ensures the Ingress hostname must be on the allowlist on the namespace containing the Ingress.

That is to say, we want to deny all creations of Ingress objects whose hostname doesn't match the allowlist.

Before you start, [download OPA](https://www.openpolicyagent.org/docs/latest/#1-download-opa) if you haven't done so.

### Prepare the Kubernetes Cluster

>This tutorial requires Kubernetes 1.20 or later. To run the tutorial locally, start a cluster with Kubernetes version 1.20+.  
[`minikube`](https://minikube.sigs.k8s.io/docs/start/) is recommended.

If you are using Kubernetes in a Cloud service, for example, Amazon EKS, [dynamic admission controllers are probably already enabled by default](https://aws.amazon.com/about-aws/whats-new/2018/10/amazon-eks-enables-support-for-kubernetes-dynamic-admission-cont/), allowing you to deploy custom webhooks. Otherwise, you must enable the ValidatingAdmissionWebhook when the Kubernetes API server is started. The ValidatingAdmissionWebhook admission controller is included in the recommended set of admission controllers to enable.

Let's start `minikube`, enable `minikube` ingress addon, create a namespace (to deploy OPA), and configure the Kubernetes context:

The communication between Kubernetes and OPA **must be secured using TLS**. Let's use `openssl` to do just that:

### Define the policy with Rego

Now that the Kubernetes cluster is ready let's write some policies. First, create a new folder to store them:

We want a policy that restricts the hostnames that an ingress can use. Only hostnames matching the specified regular expressions will be allowed. Create a file `ingress-allowlist.rego` with the following content:

If you don't know much about the Rego policy language yet, read the [official documentation here](https://www.openpolicyagent.org/docs/latest/policy-language/).

In essence, this piece of code does the following:

-   The `valid_ingress_hosts` function gets the "ingress-allowlist" annotation in the metadata section of a namespace.
-   Then it tries to match the hostname against it using regular expression.
-   If not matched, the request will be denied with a message.

Next, we will define the main policy that will import the hostname restriction policy above and respond with an overall policy decision.

Note that in this example, since we are using only one policy, this main policy is a bit redundant. Yet, in real-life situations where you want to enforce multiple policies, the main policy is needed to make a comprehensive decision in the end.

Create a file `main.rego` with the following content:

### Build and Publish the OPA Bundle

With the Rego policy code ready, now it's time to build it. Run the following commands in the policies folder to build and publish:

Here we will build a "bundle" out of the Rego code, and then we are serving the bundle in an Nginx server in a docker container, locally, which will be integrated with OPA in the next step. Read on.

### Install OPA as an Admission Controller

First, let's deploy OPA:

Let's have a look at part of this `admission-controller.yaml` file:

There is a container named [`kube-mgmt`](https://github.com/open-policy-agent/kube-mgmt) which runs as a sidecar to the OPA container. `kube-mgmt` manages policies/data of OPA instances in Kubernetes by loading Kubernetes Namespace and Ingress objects (see the "args" in the YAML file) into OPA when OPA starts. The sidecar creates watches on the Kubernetes API server so that OPA has access to Kubernetes namespaces and ingresses (which are what our policy cares about in this tutorial).

Next, let's label `kube-system` and the `opa` namespace so that OPA does not control the resources in those namespaces:

Last, we register OPA as an admission controller by creating a ValidatingWebhookConfiguration:

### Testing our policy

Now that everything is deployed let's try if our policy is working. For that, we need a test namespace.

Create a file: `qa-namespace.yaml` with the following content:

We can see that the `metadata.annotations.ingress-allowlist` label corresponds to the code we wrote earlier. Basically, here we only allow Ingress objects to be created if their hostnames match the pattern "_.qa.acmecorp.com" or ".internal.acmecorp.com_".

Let's create this namespace :

```shell
kubectl create -f qa-namespace.yaml
```

Next, we prepare a good and a bad test case for it. Create file `ingress-ok.yaml` with the following content:

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-ok
spec:
  rules:
  - host: signin.qa.acmecorp.com
    http:
      paths:
      - pathType: ImplementationSpecific
        path: /
        backend:
          service:
            name: nginx
            port:
              number: 80
```

The hostname matches the regular expression from the namespace's annotation.

Create the file `ingress-bad.yaml` with the following content:

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-bad
spec:
  rules:
  - host: acmecorp.com
    http:
      paths:
      - pathType: ImplementationSpecific
        path: /
        backend:
          service:
            name: nginx
            port:
              number: 80
```
And this hostname doesn't match. Now if we run the first test:

```shell
kubectl create -f ingress-ok.yaml -n qa
```

We can see that the Ingress is created successfully. If we run the second one:

```shell
kubectl create -f ingress-bad.yaml -n qa 
```

We will get the following error:

```
Error from server: error when creating "ingress-bad.yaml": admission webhook "validating-webhook.openpolicyagent.org" denied the request: invalid ingress host "acmecorp.com"
```

Congrats! This means our policy is working properly now 😃

### Clean Up

Run the following commands to nuke everything:

```shell
minikube delete
docker stop bundle-server
```
___

## Summary

OK, in this tutorial, we showed how to create OPA policies, how to build and publish them as a bundle served by Nginx, and register them with OPA. It's worth noting that we installed OPA with `kube-mgmt` as a sidecar.

In the second part of this tutorial, we will look at a more practical example policy using [OPA Gatekeeper](https://github.com/open-policy-agent/gatekeeper), which can restrict the taint tolerations that pods can use. We will also demonstrate the importance of policy tests and try to answer the question: how does Policy-as-Code scale?

Thanks for making it this far, and see you in the next tutorial!