November 4, 2021 | Kubernetes
We always read that ‘security is everyone’s responsibility’. For any organisation, big or small, security should always be the primary concern—not a mere afterthought. In terms of Kubernetes, securing a cluster is challenging because it has so many moving parts and, apart from securing our Kubernetes environment, we also want to control what an end-user can do in our cluster.
To achieve these goals, we can start with the built-in features provided by Kubernetes like Role-Based Access Control (RBAC), Network Policies, Secrets Management, and Pod Security Policies (PSP). But we know these features are not enough. For example, we may want specific policies like ‘all pods must have specific labels’. And even if we have the policies in place, the next big question is how to enforce them on our Kubernetes cluster in an easy and repeatable manner.
In this blog post, we’ll address this challenge and other questions pertaining to OPA and how it can integrate into Kubernetes.
Policy-as-Code (PaC) is similar to Infrastructure as Code (IaC). But, whereas in IaC you need to codify your infrastructure, in PaC, the main idea is to enforce standards and rules for specific clusters or across the entire organisation.
These rules, or policies, result from technical or legal requirements, as well as architectural decisions, and are created by various stakeholders like security engineers, product owners, developers, etc. A well-automated policy reduces the maintenance cost and attack surface. It also prevents any bad setup and configuration from being pushed to the production environment.
Once we have a policy in place, the next challenge is how to enforce it, and this is where Open Policy Agent (OPA) comes into play.
Open Policy Agent (OPA) is a generic policy engine used to enforce policies on various types of software systems (microservices, CI/CD pipelines, Kubernetes, etc.) and which decouples policy decision-making from the business logic. In simple terms, if you want to offload the policy decision from your service, then you can implement OPA. OPA supports Policy-as-Code using the declarative Rego language.
To understand how this whole process works, let’s see how OPA handles a request.
Figure 1: OPA architecture (Source: Open Policy Agent)
Using Policy-as-Code has some significant advantages:
Integrating OPA with Kubernetes to Address Authorisation Challenges
To integrate OPA with Kubernetes, we will use OPA Gatekeeper as an admission controller, but before that, let’s understand two types of admission controllers.
Figure 2: Kubernetes admission controller (Source: Kubernetes)
There are several use cases for deploying OPA as an admission controller:
Open Policy Agent Gatekeeper
You can use Gatekeeper to integrate OPA into Kubernetes. Gatekeeper starts out as a Pod, and you then register it as an admission controller using an API server.
Figure 3: Admission control flow (Source: Open Policy Agent)
When a user sends a request for a resource, for example, via kubectl to the API server, after authentication and authorisation, it will send it to the admission controller. The AdmissionReview is then passed to the Gatekeeper. Now, depending on the policy configured in the form of a Custom Resource Definition (CRD), Gatekeeper will decide on and send a response to the API server.
Gatekeeper has the following CRDs:
Let’s now see an example on how to get Gatekeeper installed and enabled on a Kubernetes cluster.
Prerequisites
Before installing the OPA Gatekeeper CRD, you must have:
Installing the OPA Gatekeeper CRD
To deploy the OPA Gatekeeper in your environment, run the command:
$ kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/master/deploy/gatekeeper.yaml
The above command will create a new namespace, gatekeeper-system:
$ kubectl get ns
NAME STATUS AGE
default Active 7m14s
gatekeeper-system Active 48s
kube-node-lease Active 7m16s
kube-public Active 7m16s
kube-system Active 7m16s
Now, let’s verify that the Gatekeeper CRD has been successfully installed:
$ kubectl get crd
NAME CREATED AT
bgpconfigurations.crd.projectcalico.org 2021-10-27T04:40:00Z
bgppeers.crd.projectcalico.org 2021-10-27T04:40:00Z
blockaffinities.crd.projectcalico.org 2021-10-27T04:40:00Z
clusterinformations.crd.projectcalico.org 2021-10-27T04:40:00Z
configs.config.gatekeeper.sh 2021-10-27T04:45:58Z
constraintpodstatuses.status.gatekeeper.sh 2021-10-27T04:45:58Z
constrainttemplatepodstatuses.status.gatekeeper.sh 2021-10-27T04:45:58Z
constrainttemplates.templates.gatekeeper.sh 2021-10-27T04:45:58Z
felixconfigurations.crd.projectcalico.org 2021-10-27T04:40:00Z
Next, verify it created a pod and service in the gatekeeper-system namespace:
$ kubectl get pod,svc -n gatekeeper-system
NAME READY STATUS RESTARTS AGE
pod/gatekeeper-audit-59979b7588-rtv5q 1/1 Running 0 4m39s
pod/gatekeeper-controller-manager-55c855c6fc-7c4nn 1/1 Running 0 4m39s
pod/gatekeeper-controller-manager-55c855c6fc-bxxph 1/1 Running 0 4m39s
pod/gatekeeper-controller-manager-55c855c6fc-sgptc 1/1 Running 0 4m39s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/gatekeeper-webhook-service ClusterIP 10.96.110.62 <none> 443/TCP 4m40s
Now that we have the Gatekeeper pod and service up and running, the next step is to define the policy.
How Do We Define Policies?
To explain how to define a policy, we will use an example policy to enforce the definition of a label team on namespace creation.
First, define ConstraintTemplate and Constraint CRD by using Rego and saving it to the template.yaml file:
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: k8srequiredlabels
spec:
crd:
spec:
names:
kind: K8sRequiredLabels
listKind: K8sRequiredLabelsList
plural: k8srequiredlabels
singular: k8srequiredlabels
validation:
# Schema for the `parameters` field
openAPIV3Schema:
properties:
labels:
type: array
items: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredlabels
violation[{"msg": msg, "details": {"missing_labels": missing}}] {
provided := {label | input.review.object.metadata.labels[label]}
required := {label | label := input.parameters.labels[_]}
missing := required - provided
count(missing) > 0
msg := sprintf("you must provide labels: %v", [missing])
}
In the rego code above, violation [{"msg": msg, "details": {"missing_labels": missing}}]
is the message displayed to the user when the policy is violated.
provided assignment collects the labels assigned to the input object’s metadata, required assignment collects the required labels from the input and missing assignment collects the difference between the two previous assignments. If the missing assignment has any elements, then a message is created msg:= sprintf(“you must provide labels: %v”, [missing])
with the message returned to the client in the case of a violation.
Now, create the template:
$ kubectl create -f template.yaml
constrainttemplate.templates.gatekeeper.sh/k8srequiredlabels created
Check the constraint template is created
$ kubectl get constrainttemplates
NAME AGE
k8srequiredlabels 108s
Verify the CRD:
$ kubectl get crd | grep -i deny
k8srequiredlabels.constraints.gatekeeper.sh 2021-10-27T05:03:53Z
Now, it’s time to create the constraint that will use the above constraint template:
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: ns-must-have-team-label
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Namespace"]
parameters:
labels: ["team"]
Note: Make sure kind matches the kind defined in the constraint template.
Next, create the constraint:
$ kubectl create -f constraints.yaml
k8srequiredlabels.constraints.gatekeeper.sh/ns-must-have-team-label created
Check the constraint has been created:
$ kubectl get k8srequiredlabels
NAME AGE
ns-must-have-team-label 27s
Let’s verify if the policy is working as expected by creating a namespace without a team label saving it to file
ns-example.yml
:
apiVersion: v1
kind: Namespace
metadata:
name: ns-example
$ kubectl apply -f ns-example.yml
Error from server ([ns-must-have-team-label] you must provide labels: {"team"}): error when creating "ns-example.yml": admission webhook "validation.gatekeeper.sh" denied the request: [ns-must-have-team-label] you must provide labels: {"team"}
Note: OPA policy will only have an impact if you try to create a new pod, it will not impact the existing pod.
If you want to look at some more policies, please check the official GitHub repo.
You can also check the Rego Playground for policy debugging. For example, in the case below, if the image is coming from an untrusted registry (other than hooli.com), it should be denied. Using Rego Playground, it’s easier to test and debug your policy online:
Figure 4: The Rego Playground (Source: Open Policy Agent)
An alternative to OPA, or specifically Gatekeeper in the Kubernetes space is Kyverno which is written specifically for Kubernetes, whereas you can use Gatekeeper with other services like Linux PAM, Envoy Proxy, etc.
As we’ve seen in the example above, Gatekeeper requires the user to make use of Rego programming language for the policies definition which could become cumbersome to manage Kubernetes policies. As a tradeoff, a dedicated programming language, enables very powerful policies definitions.
Kyverno is seen as a direct reaction to these technical demands and, because it was built specifically for Kubernetes and expresses policies declaratively, its mental model is identical to the way Kubernetes objects are described and reconciled which makes policies definition easier.
Using OPA, you can offload the policy decision from your service. And with OPA Gatekeeper, you can define policies to your Kubernetes cluster easily and quickly. For implementing policies in OPA, you need to learn a new language, Rego. Other open-source projects, like Kyverno, don’t use Rego and work similarly to OPA Gatekeeper. Overall, they both are promising technologies, and it’s worth learning both and make the most informed decision given the use cases.
This blog is written exclusively by the OpenCredo team. We do not accept external contributions.