OPA Gatekeeper on Openshift

Every organization has policies. Some are essential to meet governance and legal requirements. Others help ensure adherence to best practices and institutional conventions. Attempting to ensure compliance manually would be error-prone and frustrating

OPA lets you specific policy as code using OPA policy language Rego

In this post, I’ll share my experience deploying OPA Gatekeeper on Openshift and creating few policies for demonstartions. This post is not an introduction to OPA, refer to for intro

Gatekeeper introduces native kubernetes CRDs for instantiating policies

Installation

For installation, make sure you have cluster admin permissions

Let’s start by adding the admission.gatekeeper.sh/ignore label to non-user namespaces, so that all the resources in the labeled project are exempted from admission webhook

oc login --token=l4xjpLh0e722B2_i7iWAbPsUNOb6vPDaAXnqhH563oU --server=https://api.cluster-1d4d.sandbox702.opentlc.com:6443

for namespace in $(oc get namespaces -o jsonpath='{.items[*].metadata.name}' | xargs); do
  if [[ "${namespace}" =~ openshift.* ]] || [[ "${namespace}" =~ kube.* ]] || [[ "${namespace}" =~ default ]]; then
    oc patch namespace/${namespace} -p='{"metadata":{"labels":{"admission.gatekeeper.sh/ignore":"true"}}}'
  else
    # Probably a users project, so leave it alone
    echo "Skipping: ${namespace}"
  fi
done

Deploying a Release using Prebuilt Image

Deploy Gatekeeper with prebuilt image

oc apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/master/deploy/gatekeeper.yaml

Remove securityContext and annotations from the deployments

oc patch Deployment/gatekeeper-audit --type json -p='[{"op": "remove", "path": "/spec/template/metadata/annotations"}]' -n gatekeeper-system
oc patch Deployment/gatekeeper-controller-manager --type json -p='[{"op": "remove", "path": "/spec/template/metadata/annotations"}]' -n gatekeeper-system
oc patch Deployment/gatekeeper-audit --type json --patch '[{ "op": "remove", "path": "/spec/template/spec/containers/0/securityContext" }]' -n gatekeeper-system
oc patch Deployment/gatekeeper-controller-manager --type json --patch '[{ "op": "remove", "path": "/spec/template/spec/containers/0/securityContext" }]' -n gatekeeper-system

Wait for Gatekeeper to be ready

oc get pods -n gatekeeper-system
NAME                                            READY   STATUS    RESTARTS   AGE
gatekeeper-audit-7c84869dbf-r5p87               1/1     Running   0          2m34s
gatekeeper-controller-manager-ff58b6688-9tbxx   1/1     Running   0          3m23s
gatekeeper-controller-manager-ff58b6688-njfnd   1/1     Running   0          2m54s
gatekeeper-controller-manager-ff58b6688-vlrsl   1/1     Running   0          3m11s

Gatekeeper uses the OPA Constraint Framework to describe and enforce policy

Defining constraints

Constraints can be defined by creating a CRD (CustomResourceDefinition) with the template of the constraint you want. Let’s look at a template which enforces that only secure routes are created in the cluster

apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: k8sallowedroutes
spec:
  crd:
    spec:
      names:
        kind: K8sAllowedRoutes
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8sallowedroutes

        violation[{"msg": msg}] {
          not input.review.object.spec.tls
          msg := sprintf("'%v' route must be a secured route. non secured routes are not permitted", [input.review.object.metadata.name])
        }        

It will get the route and check if tls object is specified. If this is not true, the violation block continues and the violation is triggered with the corresponding message

Enforcing constraints

These constrains which you defined now need to be enforced. This is done by defining a constraint which will use the template specified earlier.

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRoutes
metadata:
  name: secure-route
spec:
  match:
    kinds:
      - apiGroups: ["route.openshift.io"]
        kinds: ["Route"]

This policy will use the CRD “K8sAllowedRoutes” which we had already defined. Matching is done by specifying on which API group it has to be enforced

nonsecure_route

Some constraints are impossible to write without access to more state than just the object under test. For example, it is impossible to know if an route’s hostname is unique among all routes unless a rule has access to all other routes. To make such rules possible, we enable syncing of data into OPA.

apiVersion: config.gatekeeper.sh/v1alpha1
kind: Config
metadata:
  name: config
  namespace: "gatekeeper-system"
spec:
  sync:
    syncOnly:
      - group: ""
        version: "v1"
        kind: "Namespace"
      - group: "route.openshift.io"
        version: "v1"
        kind: "Route"

Let’s create another policy which prevents conflicting routes from being created

apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: k8suniqueroutehost
spec:
  crd:
    spec:
      names:
        kind: K8sUniqueRouteHost
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8suniqueroutehost

        identical(obj, review) {
          obj.metadata.namespace == review.object.metadata.namespace
          obj.metadata.name == review.object.metadata.name
        }

        violation[{"msg": msg}] {
          input.review.kind.kind == "Route"
          re_match("^(route.openshift.io)$", input.review.kind.group)
          host := input.review.object.spec.host
          other := data.inventory.namespace[ns][otherapiversion]["Route"][name]
          re_match("^(route.openshift.io)/.+$", otherapiversion)
          other.spec.host == host
          not identical(other, input.review)
          msg := sprintf("Route host conflicts with an existing route <%v>", [host])
        }        
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sUniqueRouteHost
metadata:
  name: unique-route-host
spec:
  match:
    kinds:
      - apiGroups: ["route.openshift.io"]
        kinds: ["Route"]

Additional Resources