Improve your OPA policies user-based with Gatekeeper

For Open Policy Agent (OPA), most of the policies that are written are based on Kubernetes resources. For example, the deployment of Pods should be avoided with the tag latest
. But sometimes it is necessary to write more fine-grained OPA policies based on Kubernetes users, groups or service accounts. Let me give you an example so that the code and explanations can be better understood.
Example of a use case
Imagine you have a Jenkins job that creates Namespaces for tenants. Each time a request is received to create a namespace, the job creates the following 3 Kubernetes resources by default:
- A Namespace, whose name has to be prefixed with
tnt-
- A Secret in that namespace, of type
kubernetes.io/dockercfg
, and namedsecret-dockercfg
- A NetworkPolicy called
default-deny-all
The job uses the Kubernetes user create-tnt-ns
to create these three resources.
(If you don’t use Jenkins CI, it’s a similar story for many other systems that automate cloud infrastructure management.)
RBAC
The Jenkins job should only do what it is supposed to do, nothing else. But in terms of RBAC, excessive permissions have to be granted to the user to perform the specified tasks.
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: clusterrole-create-ns-and-secrets
rules:
- apiGroups: [""]
resources: ["namespaces", "secrets"]
verbs: ["create", "get", "list"]
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: clusterrole-create-netpols
rules:
- apiGroups: ["networking.k8s.io"]
resources: ["networkpolicies"]
verbs: ["create", "get", "list"]
For this context, two ClusterRoles should be created, along with appropriate ClusterRoleBindings. As you can see in the verbs
field, the manifests have to grant quite wide permissions to perform the specific tasks that the Jenkins job has to do.
How to solve this use case using OPA and Gatekeeper
To make this work, you’d need to create an OPA policy that checks the user and allows only the tasks that need to be performed. How can Gatekeeper know which Kubernetes user is creating the resource? The answer lies in the Kubernetes API kind named AdmissionReview.
Admission Controller phases
Before talking about the AdmissionReview
object, let’s go through the phases of admission controllers. When the API server is called the request goes through the Authentication->Authorization->Admission control stages.
The Kubernetes API server makes an HTTPS POST callout to the configured admission webhook targets (mutating and validating) with a JSON-encoded AdmissionReview object in the body of the request; that JSON document has the request
field defined. The response from the admission webhook must, in turn, be an AdmissionReview object, this time with the response
field set.
Because Gatekeeper is deployed as an admission webhook, the API server sends an AdmissionReview object on each request; the AdmissionReview message contains the data that Gatekeeper needs to check.
OPA policy
The data that’s passed to Gatekeeper for review is in the form of an input.review
object that stores the admission request under evaluation.
The input.review
object contains multiple fields but the ones to be used in our policy are these three:
input.review.object
, the request object under evaluation to be created or modified.input.review.userInfo
, the request’s user’s information such asusername
,groups
.input.review.kind
, the resourcekind
,group
,version
of the request object under evaluation.
The Gatekeeper ConstraintTemplate defines OPA rules, (written in the Rego policy language), to restrict actions and ensure that the user create-tnt-ns
does only the tasks it has to do. The code has comments to be self-explanatory.
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: allowusertenanttasks
annotations:
metadata.gatekeeper.sh/title: "Restrict tenant tasks"
metadata.gatekeeper.sh/version: 1.0.0
description: >-
ConstraintTemplate for Gatekeeper, used to restrict
the actions that the tenant management principal
(create-tnt-ns) can perform.
...
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package allowusertenanttasks
# Rule to check Namespace name
violation[{"msg": msg}] {
# Check if the resource under evaluation is a Namespace
input.review.kind.kind == "Namespace"
# Get the resource under evaluation
object := input.review.object
# Get the name of the resource
name := object.metadata.name
# Get the user
user := input.review.userInfo.username
# Check if the user is "create-tnt-ns"
contains(user, "create-tnt-ns")
# Check if the Namespace name starts with "tnt-" prefix
not startswith(name, "tnt-")
msg := sprintf(
"User [%v] is not allowed to create the Secret [%v]
of type [%v] in namespace [%v]",
[user, name, secret_type, namespace])
}
# Rule to check Secret name
violation[{"msg": msg}] {
# Check if the resource under evaluation is a Secret
input.review.kind.kind == "Secret"
object := input.review.object
namespace := object.metadata.namespace
name := object.metadata.name
user := input.review.userInfo.username
secret_type := object.type
contains(user, "create-tnt-ns")
# Check if the Secret name is "secret-dockercfg"
name != "secret-dockercfg"
msg := sprintf(
"User [%v] is not allowed to create the Secret [%v]
of type [%v] in namespace [%v]",
[user, name, secret_type, namespace])
}
# Rule to check Secret type
violation[{"msg": msg}] {
# Check if the resource under evaluation is a Secret
input.review.kind.kind == "Secret"
object := input.review.object
namespace := object.metadata.namespace
name := object.metadata.name
user := input.review.userInfo.username
secret_type := object.type
contains(user, "create-tnt-ns")
# Check if the Secret type is "kubernetes.io/dockercfg"
secret_type != "kubernetes.io/dockercfg"
msg := sprintf(
"User [%v] is not allowed to create the Secret [%v]
of type [%v] in namespace [%v].",
[user, name, secret_type, namespace])
}
# Rule to heck NetworkPolicy name
violation[{"msg": msg}] {
# Check if the resource under evaluation is a NetworkPolicy
input.review.kind.kind == "NetworkPolicy"
object := input.review.object
namespace := object.metadata.namespace
name := object.metadata.name
user := input.review.userInfo.username
contains(user, "create-tnt-ns")
# Check if the NetworkPolicy name is "default-deny-all"
name != "default-deny-all"
msg := sprintf(
"User [%v] is not allowed to create the NetworkPolicy
with name [%v] in namespace [%v].",
[user, name, namespace])
}