Isolation between kubernetes.admission policies in OPA

1/1/2022

I use the vanilla Open Policy Agent as a deployment on Kubernetes for handling admission webhooks.

The behavior of multiple policies evaluation is not clear to me, see this example:

## policy-1.rego

package kubernetes.admission

check_namespace {
   # evaluate to true
   namespaces := {"namespace1"}
   namespaces[input.request.namespace]
}

check_user {
    # evaluate to false
    users := {"user1"}
    users[input.request.userInfo.username]
}

allow["yes - user1 and namespace1"] {
  check_namespace
  check_user
}

.

## policy-2.rego

package kubernetes.admission

check_namespace {
   # evaluate to false
   namespaces := {"namespace2"}
   namespaces[input.request.namespace]
}

check_user {
    # evaluate to true
    users := {"user2"}
    users[input.request.userInfo.username]
}

allow["yes - user2 and namespace12] {
  check_namespace
  check_user
}

.

## main.rego

package system

import data.kubernetes.admission

main = {
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  "response": response,
}

default uid = ""

uid = input.request.uid

response = {
    "allowed": true,
    "uid": uid,
} {
    reason = concat(", ", admission.allow)
    reason != ""
}
else = {"allowed": false, "uid": uid}

.

 ## example input
{
  "apiVersion": "admission.k8s.io/v1beta1",
  "kind": "AdmissionReview",
  "request": {
    "namespace": "namespace1",
    "userInfo": {
        "username": "user2"
    }
  }
}

.

## Results

"allow": [
    "yes - user1 and namespace1",
    "yes - user2 and namespace2"
]

It seems that all of my policies are being evaluated as just one flat file, but i would expect that each policy will be evaluated independently from the others

What am I missing here?

-- tomikos
kubernetes
opa
open-policy-agent

1 Answer

1/1/2022

Files don't really mean anything to OPA, but packages do. Since both of your policies are defined in the kubernetes.admission module, they'll essentially be appended together as one. This works in your case only due to one of the check_user and check_namespace respectively evaluating to undefined given your input. If they hadn't you would see an error message about conflict, since complete rules can't evalutate to different results (i.e. allow can't be both true and false).

If you rather use a separate package per policy, like, say kubernetes.admission.policy1 and kubernetes.admission.policy2, this would not be a concern. You'd need to update your main policy to collect an aggregate of the allow rules from all of your policies though. Something like:

reason = concat(", ", [a | a := data.kubernetes.admission[policy].allow[_]])

This would iterate over all the sub-packages in kubernetes.admission and collect the allow rule result from each. This pattern is called dynamic policy composition, and I wrote a longer text on the topic here.

(As a side note, you probably want to aggregate deny rules rather than allow. As far as I know, clients like kubectl won't print out the reason from the response unless it's actually denied... and it's generally less useful to know why something succeeded rather than failed. You'll still have the OPA decision logs to consult if you want to know more about why a request succeeded or failed later).

-- Devoops
Source: StackOverflow