Open Policy Agent (OPA) is a cloud-native policy engine used for enforcing policy-as-code in major open source projects like Kubernetes, Terraform, Istio, Envoy, and even Docker with the opa-docker-authz plugin. OPA can run in a few different forms depending on how and where it's being used: sidecar containers, Gatekeeper admission controller, Go module, or even just the CLI itself.
To understand how OPA works let's take a simplified look at the high level concepts:
Input
Input is the information, typically in JSON or YAML format, that's used to make policy decisions. This could be structured data from an HTTP request, the JSON output from a Terraform plan, or any other structured input you might think of.
This input establishes the primary context of what your policy is going to apply against.
Data
Not all of your decision making has to come from input.
OPA also supports injecting additional context into the decision making process. This typically comes in one of two forms:
-
Passing additional data to OPA along with input
-
Providing a service endpoint for OPA to periodically query to generate a data bundle
With either of these approaches, a data object will be hydrated for the policy engine to use.
Policy
The OPA policy is where all the magic happens, and is also the steepest learning curve for newcomers. While input and data are familiar JSON, policies are written in Rego.
Rego can be a bit tricky to follow at first, especially for developers who are used to languages like Python, Go, Java, or Javascript. Fortunately, a playground exists with lots of examples, making it rather simple to test and evaluate your soon-to-be-burgeoning Rego skills.
Input + Data + Policy
Now that we understand the basic components, we can put these pieces together and use the opa CLI tool to return a policy decision based on input, policy, and optional data.
To get started, download the CLI tool and make sure you're able to execute it.
Next, we'll setup a very basic example that simulates some input data representing an HTTP request. Fire up your editor and create a file named input.json:
{
"request": {
"headers": {
"authorization": "Bearer [token]"
}
}
}
Next, we'll create a basic policy that returns a successful response only if there's an authorization header in the request. Name this file policy.rego
package something
default allow := false
allow if input.request.headers.authorization
Now we've got both our policy and input data. Let's use the opa CLI to create a bundle:
# ./opa build policy.rego
After building, you should see a bundle.tar.gz file alongside your policy.rego file. Now let's execute the policy against the input:
./opa exec -b bundle.tar.gz input.json --decision /something/allow
{
"result": [
{
"decision_id": "4552b209-110a-445a-9a93-2b8b6531d1ca",
"path": "input.json",
"result": true
}
]
}
Success! One particular point to note is the --decision /something/allow parameter we passed. The decision parameter is used to identify the variable we're using to make the policy decision. In this case, it's the allow variable within the package we named something. So here we an see the decision result is true information us that the incoming HTTP request did indeed have an Authorization header.
Now that we have a simple example, let's try something a bit more realistic.
In this next example we'll generate a valid JSON Web Token and use a policy to ensure the incoming request has a valid bearer token in the authorization header.
First, let's head over to https://jwt.io and click the "Generate example" button followed by HS256. This will give us a symmetrically signed JSON web token and the key used to generate the signature. Copy the JSON web token from the left panel of the site, open your input.json file and paste into the file, replacing [token] leaving the Bearer as is. Pay special attention to the claims in the token in the right panel -- notice the admin: true claim within the token itself.
Save and close input.json and then open policy.rego, replacing the policy with the following:
package test
default allow := false
token = {"payload": payload} if {
[_, payload, _] := io.jwt.decode(encoded_token)
}
allow if token.payload.admin == true
encoded_token := tok if {
tok := substring(input.request.headers.authorization, count("Bearer "), -1)
}
Let's review the changes we've just made since there's a lot of new concepts here.
First, we're setting a new token variable to an object with a token key set to the payload variable inside the block. We use an built-in io.jwt.decode function to decode the JSON web token and extract it into its parts. Using underscores, we ignore the parts we're not interested in, since we only want the payload representing the claims inside the token. So, when this block is evaluated, we'll have a new object assigned to token with a payload key having an associated value of the JSON web token's payload.
Interestingly, encoded_token hasn't been defined yet if we read the policy from top-to-bottom. With Rego, the order of the rules does not matter. Our encoded_token which is defined at the bottom of the policy is set before it's used in our first rule.
Let's take a closer look at how encoded_token is set. Because we're simulating an HTTP request, we have an Authorization header with a value of Bearer [token] so we'll need to remove that part to extract the actual value of the token itself. To do that we'll use another built-in function substring to take the subset of the header value after the "Bearer" section, to the end of the value.
Finally, let's look at our allow if token.payload.admin == true rule. Remember how admin: true was set in our sample token? Because our claims are available in the payload we can check the value of that claim and set our allow value only if our admin claim is what we expect.
Let's rebuild the bundle with the new policy:
# # ./opa build policy.rego
And now execute the policy against our sample input.json:
# ./opa exec -b bundle.tar.gz input.json --decision /something/allow
{
"result": [
{
"decision_id": "a38e3cfb-9481-4588-b26e-d627a31447e3",
"path": "input.json",
"result": true
}
]
}
Our policy's decision is true only when admin: true is set as a claim in the JSON web token.
Experiment with checking for false or other values. Just remember to rebuild your bundle anytime you make policy changes.