‹ Blogs

Improve your OPA policies user-based with Gatekeeper

Featured Image
Published on May 28, 2025
Author Jose A. Berchez - ControlPlane

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 named secret-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 as username, groups.
  • input.review.kind, the resource kind, 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])
      }