Skip to main content

v0.7 Release

We're happy to announce the v0.7 release of OPA. This release includes language improvements as well as initial support for monitoring via Prometheus. For the full list of interesting changes see GitHub Releases. The rest of this post introduces the major features introduced in v0.7.

Nested Expressions

Before v0.7, OPA only supported one level of embedding on expressions. For example:

  • You could write x = y * 2; z = x / 3
  • But you could not write z = (y * 2) / 3

For simple policies that only rely on boolean expressions and perform a few built-in calls, the limitation was not an issue. However, in more sophisticated policies that perform a series of arithmetic or string manipulation, policy authors had to introduce intermediate variables, which meant coming up with good variable names (which we all know is hard.)

With support for nested expressions, policies can be expressed more concisely and without the need for extraneous variables. For example, the policy below computes a risk score for Terraform changes. Before v0.7, the policy required extra statements to sum the products:

# compute risk score before v0.7
score = s {
all = [x | w = weights[type]
del = w.delete * num_deletes[type]
cre = w.create * num_creates[type]
upd = w.update * num_updates[type]
x1 = del + cre
x2 = x1 + upd]
sum(all, s)
}

In v0.7 with support for nested expressions we can write this simply as:

score = sum([x | w = weights[type]
x = (w.delete * num_deletes[type] +
w.create * num_creates[type] +
w.update * num_updates[type])])

Similarly, chains of string manipulation operations are much more concise now. Before v0.7:

decoded_str = base64url.decode(str)
trimmed_str = trim(decoded_str, "/")
parts = split(trimmed_str, "/")

After v0.7:

parts = split(trim(base64url.decode(str), "/"))

Assignment and Comparison Operators

In OPA, the = operator performs assignment and comparison at the same time. If you come from an imperative or functional programming background, this can be a bit confusing at first. To help improve the user experience for first time OPA users, we have introduced new operators for assignment (:=) and comparison (==) that behave the way you would expect coming from a traditional programming language.

The := operator allows users to declare local variables within the current scope by assigning a value. Unlike the = operator, variables appearing on the left hand side of the expression will shadow symbols in the global scope. Furthermore, redeclaration of a variable in the same scope is treated as an error.

The == operator allows users to compare two values. Unlike the = operator, variables appearing on either side of the operator must be set elsewhere in the query (otherwise an error is reported.)

These changes are backwards compatible. Existing policies are unaffected and you can continue to develop policies without using the new operators if you like.

Built-in Functions

OPA supports 50+ built-in functions. The documentation for built-in functions can be found in the Language Reference. Internally, OPA makes it relatively easy to add new built-in functions.

This release adds several new built-in functions:

  • regex.glob_intersect checks if two regex patterns intersect.
  • http.send executes a HTTP request and returns the response.
  • time.date returns [YYYY, MM, DD] from the time since epoch provided as input.
  • time.clock returns [HH, mm, SS] for the day of the time since epoch provided as input.
  • intersection returns the n-way intersection of sets provided as input.
  • union returns the n-way union of sets provided as input.

Prometheus /metrics Endpoint

As we continue to harden and optimize OPA, we have begun to focus more on monitoring and diagnostic features. This release adds support for a /metrics API endpoint that exposes high level performance metrics so that users can monitor OPA using the Prometheus project. See the new Monitoring & Diagnostics documentation for more details.

Partial Evaluation

We'd like to introduce a new OPA feature called partial evaluation which has several interesting applications. In this post we'll explain how partial evaluation works, and how it leverages rule indexing to optimize policy evaluation for low-latency use cases (e.g. microservice API authorization).

What Is Partial Evaluation?

With partial evaluation, callers specify that certain inputs or pieces of data are unknown. OPA evaluates as much of the policy as possible without touching parts that depend on unknown values. The result of partial evaluation is a new policy that can be evaluated more efficiently than the original.

Diagram comparing Normal Evaluation and Partial Evaluation flows

By partially evaluating policies, OPA can perform computation at compile time instead of runtime.

For example, in API authorization use cases, expensive operations that are independent of incoming API requests are evaluated once (during partial evaluation) and their results can be cached for subsequent evaluation runs. This helps remove costly operations from the critical path of API request processing.

The big difference between partial evaluation and normal evaluation is the result. Normal evaluation produces a decision that can be enforced (e.g. allow or deny), but partial evaluation produces new policies that can be evaluated later when the unknowns become known.

Diagram showing New Policies produced by Partial Evaluation feeding into normal Evaluation to produce Decisions

Let's look at an example.

Partial Evaluation and Rule Indexing

Imagine we have a simple authorization policy that decides whether to allow requests against appliances.

package smart_home

default allow = false

allow = true {
op = allowed_operations[_]
input.method = op.method
input.resource = op.resource
}

allowed_operations = [
{"method": "PUT", "resource": "air-conditioner"},
{"method": "GET", "resource": "security-camera"},
{"method": "POST", "resource": "garage-door"},
]

To use this policy, callers query OPA by asking for the value of smart_home.allow which will be true or false depending on whether the request should be allowed or denied. To answer the policy query, OPA searches the allowed_operations data to find a match.

The nice thing about this policy is that we only have to write the search logic (allow) once, up front. After that, we can expand the data set (allowed_operations) to cover more and more appliances.

In real scenarios, allowed_operations would be loaded into OPA as data instead of hardcoded in the policy. It's hardcoded into the policy to make the example simpler.

The downside is that OPA needs to potentially search over the entire data set each time an API request is received. As we automate more of our home, the authorization decision will take longer to compute.

Performance is an important requirement for OPA: we need to minimize the latency introduced into the request path as much as possible. OPA has addressed use cases like this in the past by precomputing a data structure that quickly looks up applicable rules instead of searching through them. However to accomplish this the rules have to be written in a manner that OPA's indexer can understand. For example, we would have to take the policy above rewrite it as follows:

package smart_home

default allow = false

allow {
input.method = "PUT"
input.resource = "air-conditioner"
}

allow {
input.method = "GET"
input.resource = "security-camera"
}

allow {
input.method = "POST"
input.resource = "garage-door"
}

While this works, no one wants to write out these kinds of rules by hand. Moreover, the policy becomes painful to read because we end up with thousands of duplicated rules. What we want is a simple, concise policy that OPA evaluates efficiently.

This is where partial evaluation comes in. Looking at the allow rule from earlier, we can see that there are two pieces of potentially unknown information: input.method and input.resource.

allow {
op = allowed_operations[_]
input.method = op.method
input.resource = op.resource
}

With partial evaluation, we can evaluate everything in the policy that does not depend on these two input values. The result of partial evaluation, used internally by OPA, looks almost identical to the unrolled version we made above by hand.

Of course, not all policies are this simple. In some cases, expressions that depend on unknown values may produce outputs that are required in the rest of the policy:

allow {
risk_score = (input.num_deletes * 10) + input.num_adds
risk_score < risk_limit
}

risk_limit = 100

In this case, OPA will determine that risk_score cannot be computed if input is unknown and so the second expression will not be processed during partial evaluation. Instead it gets saved as part of the partial evaluation result.

This was a quick overview of how partial evaluation works. Let's look at how you can use partial evaluation today!

Using Partial Evaluation

You enable partial evaluation by specifying the partial query parameter when you ask OPA for policy decisions.

OPA performs partial evaluation lazily: the first time you ask for a policy decision with partial evaluation, OPA will compute the partially evaluated policy and then cache it for later. If the data or policy in OPA changes such that partial evaluation needs to be re-run, OPA invalidates the cache entry.

Let's look at some examples.

Suppose we have the policy from above, but instead of 3 resources, we have hundreds. We can construct a query to see if we're allowed to GET the data from one of security cameras:

{
"input": {
"method": "GET",
"resource": "security-camera-493"
}
}

This object is saved into a file called req.json that we can use to execute the query below with cURL:

curl localhost:8181/v1/data/smart_home/allow?metrics -d @req.json

In this case, the request is allowed and normal evaluation takes around 5ms on an Intel Core i7 @ 2.9Ghz:

{
"metrics": {
"timer_rego_query_compile_ns": 71412,
"timer_rego_query_eval_ns": 5185152,
"timer_rego_query_parse_ns": 194950
},
"result": true
}

We'll change the caller to specify the partial query parameter.

curl localhost:8181/v1/data/smart_home/allow?metrics&partial \
-d @req.json

Now when we run the call, we get back the same results, but we can see the query latency (timer_rego_query_eval_ns) is much lower, around 50µs.

{
"metrics": {
"timer_rego_partial_eval_ns": 10613556,
"timer_rego_query_compile_ns": 137427,
"timer_rego_query_eval_ns": 43921,
"timer_rego_query_parse_ns": 200871
},
"result": true
}

Since the above call was the first with the partial parameter set, the response included the time it took to partially evaluate the policy, around 10 milliseconds (timer_rego_partial_eval_ns). If we query OPA again with partial set the step is skipped because the result was cached:

curl localhost:8181/v1/data/smart_home/allow?metrics&partial \
-d @req.json
{
"metrics": {
"timer_rego_query_compile_ns": 78901,
"timer_rego_query_eval_ns": 50405,
"timer_rego_query_parse_ns": 22203
},
"result": true
}

You can also use partial evaluation if you embed OPA as a library. In that case:

  • You must invoke partial evaluation yourself.
  • You should invoke partial evaluation offline, outside the request path.
  • You should cache the partial evaluation result.
  • You can use the partial evaluation result when input values are known (e.g., when an API request is received.)

For more information on how to embed OPA as a library and leverage the partial evaluation feature, see this GoDoc example.

Benchmark: Role-based Access Control (RBAC)

An immediate application for partial evaluation is RBAC policy enforcement. RBAC provides a simple, coarse-grained way of granting permissions by groupings. Determining whether to allow requests under RBAC involves identifying whether the caller has been associated with a role that grants permission to the perform the operation.

In projects like Kubernetes and Istio, RBAC configuration is specified using roles and role bindings. Roles grant permission to perform operations and role bindings associate subjects (e.g., users or service accounts) to roles. Below is an example of some role and role binding data:

{
"roles": [
{
"operation": "read",
"resource": "widgets",
"name": "widget-reader"
},
{
"operation": "write",
"resource": "widgets",
"name": "widget-writer"
}
],
"bindings": [
{
"user": "inspector-alice",
"role": "widget-reader"
},
{
"user": "maker-bob",
"role": "widget-writer"
}
]
}

In OPA, we can define an RBAC policy that evaluates the configuration above as follows:

package example_rbac

default allow = false

allow {
user_has_role[role_name]
role_has_permission[role_name]
}

user_has_role[role_name] {
role_binding = data.bindings[_]
role_binding.role = role_name
role_binding.user = input.subject.user
}

role_has_permission[role_name] {
role = data.roles[_]
role.name = role_name
role.operation = input.action.operation
role.resource = input.action.resource
}

First, the policy searches for "bindings" that match the subject. Second, the policy checks to see if the role associated with the binding grants permission to perform the operation on the resource.

In this example, applying partial evaluation would yield a set of rules where the roles and role bindings have been inlined and the implicit loops that perform the search have been unrolled. For example:

# … more rules
allow {
"admin" = input.subject.user
"read" = input.action.operation
"resource-lktj" = input.action.resource
}
allow {
"admin" = input.subject.user
"write" = input.action.operation
"resource-rsiq" = input.action.resource
}
# … more rules

The table below summarizes the performance when using OPA to evaluate this RBAC policy on increasingly large RBAC configuration data sets.

# Roles | # Bindings | Normal Eval (ms) | With Partial Eval (ms)
--------+------------+------------------+-----------------------
250 | 250 | 5.4982303 | 0.0468107
500 | 500 | 11.8662272 | 0.0591411
1,000 | 1,000 | 21.6441107 | 0.0542551
2,000 | 2,000 | 45.4870310 | 0.0623962

This benchmark demonstrates the power of partial evaluation. Not only is the policy quick to evaluate but the evaluation latency remains relatively stable as the data set grows (because of rule indexing).

But it's only fair to warn that nothing comes for free. As the data set grows the time taken to run partial evaluation (and the memory required to store the result) also grow:

# Roles | # Bindings | Partial Eval + Compile (seconds) | Heap (MB)
--------+------------+----------------------------------+----------
250 | 250 | 0.503920151 | 11.714843
500 | 500 | 1.998275287 | 13.496093
1,000 | 1,000 | 8.085092923 | 18.449218
2,000 | 2,000 | 31.972901290 | 29.097656

It's important to reiterate that the partial evaluation step is done offline, outside of the request path: this means the partial evaluation latency only affects the time taken to propagate policy updates. From our point of view, given other system behaviour (such as caching and failures), this is an acceptable price to pay.

Future Work and Wrap Up

With v0.6, we've laid the groundwork for partial evaluation. Going forward we're planning to work on extending coverage over the kinds of expressions that can be partially evaluated, and looking at strategies like performing partial evaluation in multiple passes.

Hopefully this post was useful (or at least interesting)! Partial evaluation is a big step for OPA as it allows policies to be expressed concisely while still benefiting from optimizations like rule indexing. We're looking forward to applying partial evaluation to other use cases in the near future.

OPA's Full Stack Policy Language

Banner image for OPA&#39;s full stack policy language post

The Open Policy Agent (OPA) has been used to policy-enable software across several different domains across several layers of the stack: container management (Kubernetes), servers (Linux), public cloud infrastructure (Terraform), and microservice APIs (Istio, Linkerd, CloudFoundry). In this post we describe how OPA's policy language Rego manages to work for all these different domains and layers of the stack without requiring any changes or extensions to the language.

Rego Overview

Rego's sole purpose is to make policy decisions that other products/services need to take action. For example…

  • Is this API request allowed or denied?
  • What's the hostname of the backup server for this application?
  • What's the risk score for this proposed infrastructure change?
  • What list of clusters should this container be deployed to for high-availability?
  • What's the routing information that should be used for this microservice?

Rego lets you write policy to answer all of those questions (and many more). It lets you write policy about any layer of the stack and any domain (e.g. APIs, servers, infrastructure, clusters, networking), without requiring you to change or extend the language. There are two key insights to Rego:

Every domain can be encoded as JSON/YAML data.

Policy is logic applied to data.

When you're writing policy, you should only be thinking about the domain you care about, how to encode that domain as data, and the logic you need to make a policy decision given that data. In this post, we show you how to do exactly that with Rego. Along the way you'll learn that:

  • Rego data can be any JSON/YAML data
  • Rego is a query language. You write logic to search and combine JSON/YAML data from different sources.

Rego Data is JSON/YAML

As mentioned above, Rego is a general-purpose policy language, meaning that it works for any layer of the stack and any domain. The key idea is that while you as an author are thinking about servers, containers, or APIs, Rego just sees JSON/YAML data. So you can write policy about any domain as long as the information you need to make a decision can be stuffed into JSON/YAML.

For example, you can think of authorizing an HTTP API call as making a true/false decision about the YAML data shown below.

user: alice
method: GET
path: /finance/salary/bob
headers:
- JWT:

And you can think of authorizing someone to SSH into a server as a true/false decision about:

user: alice
server_id: s12345
role: webapp
environment: prod

Besides the data describing the input to the decision, Rego lets you incorporate background information from many different sources of JSON/YAML to make a decision. You could, for example, tell Rego who each person's manager is:

manager:
charlie: bob
dave: bob
bob: alice

Then you could authorize managers to execute API calls that return their subordinates' salaries, or that a manager can SSH into their subordinates' desktop servers.

The key point here is that Rego does not understand what the data (or even the schema for the data) means in the real world. Because of that, you can use Rego to make policy decisions about APIs, servers, containers, risk-management, or any other domain you can imagine. It's the policy author that knows what the data means in the real world and writes logic to make a policy decision.

Rego is a Query Language

To make a policy decision in Rego, you write logical tests on the data that comes in as input (such as the API or SSH data from the last section).

For example, if you want to allow a user to run the API call that reports her own salary, you write a policy where the input is a JSON/YAML document representing the API call (shown below tweaked slightly to represent the URL as an array instead of a string) and call it input.

input:
user: alice
method: GET
path: ["finance", "salary", "bob"] # /finance/salary/bob
headers:
- JWT:

Then you write boolean logic that decides whether or not the API call represented by that JSON/YAML data is authorized. For example, you could write conditions that authorize bob to see his own salary.

input.method = "GET"
input.path = ["finance", "salary", "bob"]
input.user = "bob"

Implicitly all of the conditions above are ANDed together. In Rego, logic like this doesn't stand on its own — it needs a name. Below we've given the logic the name allow.

allow {
input.method = "GET"
input.path = ["finance", "salary", "bob"]
input.user = "bob"
}

Of course, you don't want to write one allow statement for every employee in the company. You want to use a variable so that the statement applies to everyone. In Rego, a variable is basically any symbol that's not a string or number, so to allow all employees to see their own salary, you replace "bob" with a variable like employee.

allow {
input.method = "GET"
input.path = ["finance", "salary", employee]
input.user = employee
}

This statement says that allow is true if there is some value for the employee variable that makes all of the conditions true. Rego treats a set of conditions as a query and finds variable assignments that makes them all true. So while Rego has a syntax closer to programming languages than to SQL, it's really a query language underneath.

Now that you've let everyone see their own salary, you might decide to allow managers to see their subordinates' salaries. But to do that the Rego policy needs to know who manages whom, information that isn't included in the API call. In OPA, you make managerial data available by inserting it into the data namespace. (OPA has exactly two toplevel namespaces for JSON/YAML data: data and input.)

data:
manager:
charlie: bob
dave: bob
bob: alice
...

Now you can write a second query also named allow as shown below. When you have multiple queries with the same name, they are ORed together.

allow {
input.method = "GET"
input.path = ["finance", "salary", employee]
input.user = data.manager[employee]
}

This query looks up whether the user requesting an employee's salary is the manager of that employee using the manager dictionary. In addition to looking up key/value pairs in a dictionary, Rego lets you search through JSON data to find values and even cross-reference (or join) multiple JSON data sources during a search.

Additionally, Rego lets you make policy decisions that are more sophisticated than the allow/deny decisions shown above. You can make decisions that are numbers (e.g. rate-limits), strings (e.g. hostnames), arrays (e.g. servers), or dictionaries (microservice route-mappings). For more examples, see the Open Policy Agent tutorials.

The key takeaway is that Rego logic lets you write queries about multiple JSON/YAML data-sources to make a policy decision, and a decision can be a boolean, number, string, array, or even a dictionary.

Rego is for Policy, not Programming

The goal of Rego is to help you tell software systems how to behave in the world by writing logic about (collections of) JSON/YAML data. Programming languages (e.g. C, Java, Go, Python) are the usual solution to this problem, but Rego was purpose-built to let you focus on just the data that represents the world and the logic that makes policy decisions about that data. Below we contrast policy and programming by showing what you SHOULD be thinking about when writing policy (logic and data) and what you SHOULD NOT be thinking about when writing policy (programming).

When writing policy about HTTP APIs…

  • you SHOULD be thinking about whether the method is a GET or POST
  • you SHOULD be thinking about whether the employee is requesting her own salary or someone else's.
  • you SHOULD be thinking about whether the manager data has an entry stating that the employee requesting a salary is the manager of the person's salary in the request.

But…

  • You SHOULD NOT be thinking about opening a network socket to retrieve the manager data.
  • You SHOULD NOT be thinking about the object classes or class inheritance used to store the managerial data.
  • You SHOULD NOT be thinking about which methods to use to access the fields of a class.
  • You SHOULD NOT be deciding between different looping constructs (for, do-until, while, iterators, recursion) to search through the data or worrying about if those loops will terminate.
  • You should NOT be thinking about a multitude of data structures and their subtle tradeoffs, like splay trees versus red-black trees.

Rego has some of the same primitives as programming languages for sharing common logic and coping with a large number of policies (modules and functions), but you're always thinking about exactly two things: logic and data. You're not thinking about sockets, object classes, method calls, non-terminating loops, or binary trees. You're thinking about logic and data.

Wrap Up

Hopefully that helps shed some light on Rego and how OPA works for any domain and any layer of the stack. Here are the key takeaways:

  • Rego lets you write policy about any domain and any layer of the stack: APIs, servers, risk-management, containers, networking.
  • Rego makes you think about policy (logic and data), not programming (sockets, object classes, method calls, non-terminating loops, binary trees).
  • Rego operates over JSON data. You can supply JSON data as input to every decision and as background information for making decisions.
  • Rego logic is all queries. A query finds values for variables that make boolean conditions true.

For more information, check out the Open Policy Agent project.

Orderly versus Disorderly Policies

Banner image for the Orderly versus Disorderly Policies blog post

One of the age-old questions with policy languages is: does the order of policy statements matter?

  • For Orderly policies where statement-order matters, the policy engine makes snap judgments. It uses the first statement that applies to make a decision. Examples are Firewalls and IPTables.
  • For Disorderly policies where statement-order doesn't matter, the policy engine is more contemplative. It finds all the statements that apply and combines them to make a decision. Examples are query languages like SQL and Datalog.

Are Orderly or Disorderly policies better? In this blog post we lay out the tradeoffs and conclude there's no clear winner (which is why the Open Policy Agent supports both). You want a policy language that lets you mix and match Orderly and Disorderly policies so you can leverage the strengths of each along the following dimensions.

  • Conflict resolution: Implicit (Orderly) vs. Explicit (Disorderly)
  • Overrides and Defaults: Easy (Orderly) vs. Difficult (Disorderly)
  • Composability: Difficult (Orderly) vs. Easy (Disorderly)
  • Performance: At most 1 (Orderly) vs. More than 1 (Disorderly)
  • Understandability: Misleading (Orderly) vs. Requires Tools (Disorderly)

Terminology

Throughout this blog post we'll use the following terms.

  • A policy is a collection of statements.
  • A statement makes a decision, such as whether an HTTP API call should be allowed or denied.
  • A policy language defines what statements are possible. It defines what a statement, policy, or collection of policies means and what decisions it makes.
  • A policy engine is a piece of software that ingests policies and evaluates them to make decisions.

As a running example, we'll use HTTP API authorization. The input to the policy has two components: a user making an API request and the HTTP path for that request. For example, the user might be alice and the HTTP path might be /finance/alice/salary. The policy must decide whether to allow or deny the input.

Conflict Resolution

A conflict happens when a policy says something contradictory, like a request is both allowed and denied. A policy language that can't express conflicts is usually too impoverished in practice, so a policy language must have a way of eliminating conflicts if it's to make an actual decision.

For example, the following policy has a conflict: any URL of the form /finance/<user>/salary is both allowed and denied.

Deny /finance
Allow /finance/<user>/salary

An Orderly policy resolves conflicts automatically because there's only ever 1 statement that applies: the first one. You order your policy statements so that conflicts get resolved the way you want.

A Disorderly policy on the other hand requires you to choose an explicit conflict resolution strategy for when multiple statements make contradicting decisions. For example, you might dictate that Allow overrides Deny (or vice versa).

The snippets below show the different options for the example above.

Option 1: Orderly

Deny /finance
Allow /finance/<user>
Final decision: Deny

Option 2: Orderly (statements in a different order)

Allow /finance/<user>
Deny /finance
Final decision: Allow

Option 3: Disorderly with Deny overrides Allow

Deny /finance
Allow /finance/<user>
Final decision: Deny

Option 4: Disorderly with Allow overrides Deny

Deny /finance
Allow /finance/<user>
Final decision: Allow

On the surface, Orderly policies seem to handle conflicts better than Disorderly policies, but in reality you're thinking about conflicts in both cases. For Orderly policies, you think about conflicts when you figure out what order to put your statements in, and for Disorderly policies you think about conflicts when choosing a conflict-resolution strategy. Here there is no clear winner.

Overrides and Defaults

One of the best applications of an Orderly policy is to handle default conditions and overrides. Imagine you have an existing policy and temporarily need to block all requests to a particular HTTP API because of a recent security vulnerability or bug.

Using an Orderly policy you can add the appropriate policy statements to the top of the policy and rest assured that those statements will be the ones that get applied. In contrast, for a Disorderly policy you figure out what all statements are that conflict with your new statement and modify them so they no longer apply.

In our running example, imagine you have 4 different policy statements and now you want to block all API calls to /finance. With an Orderly policy, you simply add a single statement Deny /finance to the top of the policy. You don't even need to know what the other policy statements say.

Orderly

Deny /finance
Allow /finance/<user> by <user>
Allow /finance/<user> for HR group members
Allow /finance/<user> for <user>'s manager
Deny /finance/<user> for subordinates of <user>

In a Disorderly policy where Allow overrides Deny, we would need to identify and then temporarily remove or disable all of the Allow statements shown above.

Orderly policies clearly excel at the override use-case.

Composability and Collaboration

Based on the last section, it might seem that Orderly policies are better because they make defaults and overrides so easy. But as we're about to see Disorderly policies are vastly better for the case of composition (the ability to combine different policies).

Composing (combining) different policies happens whenever multiple people are collaboratively defining a policy. Imagine you write policy A and someone else writes policy B (both about HTTP APIs). For Disorderly policies combining A and B is easy: union the statements of A and B together. Any conflicts between A and B are handled by conflict resolution.

In contrast, for Orderly policies, there's no clear way to combine A and B into a single policy. Put policy A before B, and all conflicts are resolved in your favor. Put policy B before A, and all of policy B's decisions win out. Orderly policies don't have conflicts or conflict resolution, so you need additional tooling to even identify conflicts between A and B.

For example, if you were to combine Orderly policies A and B below, which order would the authors of A and B agree on? (A then B does NOT produce the Orderly policy given in the last section.) If the policies are Disorderly, combining them is easy.

Policy A (Ordered)

Allow /finance/<user> by <user>
Allow /finance/<user> for <user>'s manager

Policy B (Ordered)

Allow /finance/<user> for HR group members
Deny /finance/<user> for subordinates of <user>

You might wonder how often composition happens. Anytime you have hierarchically organized resources (like files in a directory structure or HTTP API resources), you'll typically end up composing policies. It's natural to attach different policies to different points in the hierarchy and then make decisions based on the policies attached at a parent and child in the hierarchy. Sometimes you want the policy at the lower level to take precedence over the policy at the higher level, and sometimes the reverse is true.

Composability is the single greatest strength of Disorderly policies. People (and machines) can contribute what they know as policy statements, and the language lets you understand where the disagreements are, and what the final decision is.

Performance

For Orderly policies, evaluation only executes until it finds one applicable statement. If that statement is number 10 out of 100,000 then the performance improvement is substantial. While it is unknown how many policy statements will need to be evaluated, at most 1 will ever be fully evaluated.

For Disorderly policies, it's important to implement indexing algorithms. Indexing algorithms analyze a policy and organize its statements so that by inspecting a request, it can quickly zero in on the (hopefully) small number of statements that apply.

In our running example, an indexer might identify that just the following two statements need to be evaluated. If it's smart, it can even conclude that the answer must be Allow without evaluating anything because all of the statements Allow the result.

Allow /finance/<user> by <user>
Allow /finance/<user> for HR group members

Of course, you can use indexing for ordered evaluation, but it requires deeper analysis because statement k only applies if none of statements 1…k-1 apply, meaning that the conditions for statement k include some of the conditions from previous statements.

In terms of performance, it's hard to say whether Orderly or Disorderly policies are better. Indexing algorithms can be complex and lead to variability, but can also improve performance asymptotically (infinitely).

Familiarity and Understandability

Technical considerations are important, but the best technical solution in the world won't be used if people don't understand it. How familiar and understandable a policy language is has a great impact on its adoption.

Orderly policies require the reader/writer to think top-to-bottom. That's the same way we train people to read, and it's the same way we train programmers to write most code in imperative, object-oriented, and functional languages. Orderly policies are familiar to a wide range of people.

Just because they're familiar doesn't mean Orderly policies are easy to understand (especially at scale). To know what statement number k means, you need to know which of the 1…k-1 statements conflict with statement k since they all take precedence over statement k. So to understand statement number 1,000 you need to understand those 999 statements that come before it. The worst part is that when people authored the policy, they may have known that there were only 5 statements that mattered, but they were forced to write them so that it looks like all 999 statements matter.

For example, understanding which requests get denied by the last statement in the Orderly policy below means understanding which requests get allowed by the first 3 statements. Understanding what happens to a request for a user's financial data by a member of HR only requires looking at the first 2 statements — it's safe to ignore all the rest.

Orderly

Allow /finance/<user> by <user>
Allow /finance/<user> for HR group members
Allow /finance/<user> for <user>'s manager
Deny /finance/<user> for subordinates of <user>

Disorderly statements are no less familiar to the general populace than ordered. Every time you read two different newspaper articles, you get different sides of the story and need to figure out what really happened. For programmers, the order in which you define functions usually makes no difference; any time you create a set, use a boolean OR, launch multiple threads, or work on a distributed system you're working with unorderedness.

A single statement in a Disorderly policy stands on its own. Once you understand that statement, you need to understand which other statements conflict with it and how those conflicts are resolved. Tooling helps immensely here.

For example, in our running example you may want to understand the impact of the statement Allow /finance/<user> for HR group members. The statement by itself is simple enough, but you need to find all the statements that might conflict with it (e.g. the last statement conflicts if one of the HR group members is a subordinate), and how that conflict would be resolved.

Disorderly

Allow /finance/<user> by <user>
Allow /finance/<user> for HR group members
Allow /finance/<user> for <user>'s manager
Deny /finance/<user> for subordinates of <user>

In the end, understanding any single statement in any policy (Orderly or Disorderly) requires understanding all the other statements that conflict with it. Orderly policies tell you that the conflicts all come before the statement you're looking at, but force you to order statements that have no inherent ordering, resulting in the appearance of conflicts when none exist. Disorderly policies give you no indication about which policy statements might conflict and rely on tooling to help you find conflicting statements.

Summary

When all is said and done, there are times when you want statement order to matter (overrides), and there are times when you don't want statement order to matter (composition and hierarchies). If you have a tightly-constrained use case, you might be able to choose one or the other. But if you're interested in a general-purpose policy language that works for many different use cases, across many different domains, and is used by a wide population, there's no clear winner. You'll want your policy language to support both Orderly and Disorderly policies.

Given that this is a blog post for the Open Policy Agent, it's probably not surprising that OPA supports both options and lets you mix and match as appropriate. See the docs and FAQ for more details, or reach out on the slack channel.

Optimizing OPA: Rule Indexing

One of the hallmarks of declarative languages is that people shouldn't worry about performance — the software should. People write down the logic of a policy decision (e.g. should this API be allowed), and the software figures out how to evaluate that logic efficiently.

For example, it's common to use OPA to write whitelist (or blacklist) policies by defining multiple rules that all say to 'allow' (or 'deny') some operation. The example below defines a simple whitelist authorization policy for HTTP API operations (see the HTTP API Authorization tutorial for more details). Notice how there are multiple statements that 'allow' the operation, any of which could apply to a given input.

package acmecorp.api

import data.acmecorp.roles

default allow = false

allow {
input.method = "GET"
input.path = ["accounts", user]
input.user = user
}

allow {
input.method = "GET"
input.path = ["accounts", "report"]
roles[input.user][_] = "admin"
}

allow {
input.method = "POST"
input.path = ["accounts"]
roles[input.user][_] = "admin"
}

To ask if user "felix" is authorized to GET the /accounts API, you would query OPA as shown below.

POST /v1/data/acmecorp/api/allow HTTP/1.1
Content-Type: application/json

{
"input": {
"method": "POST",
"user": "felix",
"path": ["accounts"]
}
}

If "felix" has the "admin" role, OPA responds with true, since the 3rd "allow" rule matches the path ["accounts"] and requires the user to be in the "admin" role. Otherwise, none of the "allow" rules match the input above, and OPA responds with false (because of the "default" rule).

In this blog post we describe an optimization that improves how efficiently OPA computes the answer to such a query. The simplest way to compute an answer is to evaluate each of the "allow" rules one-by-one. This is exactly what OPA did before v0.4.9. The obvious problem with this approach is that query latency increases as the total size of the rule set grows, even if the additional rules are irrelevant to most queries.

In many cases, rule sets share similar expressions that differ slightly but in ways that make some of them mutually exclusive. This means that if one rule would generate a value, another could not. In the example above, the 3rd rule only applies to the path ["accounts"], whereas the other two rules only apply to longer paths, e.g. ["accounts", "report"].

As of v0.4.9, OPA indexes rule sets to quickly determine which subset of rules must be evaluated given data stored in OPA or passed as input to the query. This allows OPA to evaluate only the rules required to generate the correct result. Rules that would not generate a value for the query input are ignored.

Conceptually, the index is a function that takes the query input as an argument and returns the rules OPA must evaluate:

func (index Index) GetRules(input Value) []Rule {
// perform index lookup
}

To make the index lookup efficient, OPA builds a Trie data structure from equality expressions contained in rule sets. For example, OPA would build the following structure for the rule set above:

Trie structure built from the example rule set

When OPA looks up the rules to evaluate the query above, it traverses the Trie and collects rules that are required for the input. For simple equality expressions involving scalars this is straightforward however it becomes more complex when taking into account composites (arrays, objects), vars, and undefined values.

Trie traversal handling composites, vars, and undefined values

As we would expect, this approach provides an asymptotic performance improvement on queries that match the form supported by the index:

Benchmark chart showing query latency improvement from indexing

Currently, OPA can build this rule index for expressions of the form:

<ref> = <scalar | array | var> # or vice-versa

Here are a few examples:

input.foo = "bar"
["foo", "bar", x] = input.baz
data.foo.bar = x

There's an additional constraint on the <ref> term: it must:

  • be ground (e.g., data.example.foo[x] is excluded)
  • be non-nested (e.g., data.example.foo[data.example.bar] is excluded)
  • refer to a base document (e.g., if data.example.p refers to a rule, it is excluded)

Nonetheless, in practice, there are many cases covered by the currently supported form and we're pleased to have this optimization in place. In the future, we'll extend the index to support other types of terms (e.g., objects, non-ground refs, etc.) as needed.

Authorizing HTTP APIs, SSH, and Puppet with OPA

This is a short post that shows how you can use the Open Policy Agent (OPA) project to enforce authorization policies across HTTP APIs, SSH, and Puppet. If you're interested in policy, authorization, compliance, or other related topics, check out openpolicyagent.org or come chat with us on Slack.

One goal of OPA is to solve authorization (who can do what) across the stack. To achieve this goal, OPA provides a simple HTTP API to integrate at enforcement points and a high-level declarative language to codify authorization policies. The policy language (Rego) is domain-agnostic and let's you define rich, fine-grained access controls over arbitrary JSON data.

When you write authorization policy in Rego, you're writing assertions over the state of the world represented as JSON. The state available to the authorization policy is provided either as input to the authorization query or pushed from an external data source and stored inside OPA. Because external state can be pushed into OPA, policies can leverage all kinds of context when making their authorization decisions.

Recently we built a handful of authorization integrations that use OPA at different points in the stack. As part of this effort we're reaching out to other projects that are looking to solve authorization in their domain. We've already built several integrations and examples spanning multiple layers:

Let's look at some examples.

HTTP API Authorization

This simple example shows how to limit read access to an employee's salary in a web app.

package httpapi.authz

default allow = false

# Allow users to get their own salaries.
allow {
input.method = "GET"
input.path = ["finance", "salary", user]
user = input.user
}

# Allow managers to get their subordinates' salaries.
allow {
input.method = "GET"
input.path = ["finance", "salary", user]
manager_of[user] = input.user
}

In this case, the policy allows exactly two people to access an employee's salary:

  • The employee themselves (first rule)
  • The manager of the employee (second rule)

This is a simplistic example but it helps show how OPA lets you leverage arbitrary data to make policy decisions. In this case, the second rule contains a reference to data ("manager_of") that maps an employee to their manager. For example:

{"alice": "bob", "charlie": "betty"}

It's worth pointing out that "manager_of" mapping could be defined statically or dynamically in the policy itself. Defining "manager_of" statically would look very familiar:

manager_of = {"alice": "bob", "charlie": "betty"}

Alternatively, we could define "manager_of" dynamically based on some other data source (e.g., WorkDay, LDAP, etc.). For example:

package httpapi.authz

manager_of[employee] = manager {
data.employees[employee].team = team_id
data.teams[team_id].lead = manager
}

SSH Authorization (using Linux-PAM)

This example shows how to restrict SSH access to users who have contributed to services running on individual hosts. Again, this policy shows how we can leverage external data to make policy decisions.

package ssh.authz

default allow = false

# Allow access to any user that has the "admin" role.
allow {
data.roles["admin"][_] = input.user
}

# Allow access to any user who contributed to the code running on the host.
allow {
data.hosts[input.host_identity.host_id].contributors[_] = input.user
}

In this case, the "roles" and "hosts" refer to external data loaded into OPA:

{
"hosts": {
"frontend": {
"contributors": [
"frontend-dev"
]
},
"backend": {
"contributors": [
"backend-dev"
]
}
},
"roles": {
"admin": [
"ops"
]
}
}

With PAM you can also control who can run sudo commands. Since our PAM module offloads authorization decisions to OPA, we can extend our authorization policy to cover sudo access without changing any code in the enforcement point. This example shows how you can restrict sudo access to users with an admin role:

package sudo.authz

default allow = false

# Allow sudo access to any user that has the "admin" role.
allow {
data.roles["admin"][_] = input.user
}

Puppet Authorization

Finally, let's look at how OPA can be used to enforce authorization decisions over more complex data structures such as Puppet catalogs.

In this case, we assume:

  • **The infrastructure team is responsible for config stored inside /etc/infra
  • **The app team is responsible for config stored inside /etc/app
package puppet.authz

default allow = false

allow { not deny }

deny {
resource = catalog.resources[resource_index]
resource.type = "File"
startswith(resource.title, "/etc/infra")
resource_author[resource_index] = email
not infra_team[email]
}

deny {
resource = catalog.resources[resource_index]
resource.type = "File"
startswith(resource.title, "/etc/infra")
resource_author[resource_index] = email
not infra_team[email]
}

This policy combines data from Puppet and Git (blame) to determine if an infrastructure team member has modified files belonging to the app team (or vice-versa).

Wrap Up

If you're interested in trying out these examples, check out the OPA documentation:

We also have examples showing Puppet and Linkerd-based micro-service authorization that can be found in the open-policy-agent/contrib repository.

In upcoming posts we'll dive into more detail on authorization use cases such as conflict resolution, consistency guarantees, performance, visibility, and so on.

Thanks for reading!

What is Policy? Part Two: Policy Engines

This is the second post in a two-part series about policy enforcement. If you're unfamiliar with the idea of policy enforcement check out Part One: Enforcement.

Last time we examined alternative approaches to policy enforcement. We saw that tribal knowledge and documentation on wikis provide few guarantees about enforcement but also that automated solutions often make policy difficult to understand and expensive to maintain. In this post, we'll examine how policy engines can help balance the desire for automated enforcement and the need for ease-of-use.

Policy engines have been around for years. If you've worked on networking or authorization, you've used one when setting up ACLs or writing RBAC policies. You may also have heard of more general-purpose policy technologies, such as XACML, or protocols like RADIUS.

At a high level, policy engines take policy and data as input and produce answers to policy questions as output. We can design policy engines as libraries, sidecars, or full-blown services. In a future post, we'll examine the trade-offs with each approach.

When policy engines are integrated into our systems, we refer to those systems as policy-enabled. The goal of policy-enabling systems is to decouple policy decisions from policy enforcement. This decoupling results in policy implementations that are easier to understand, flexible enough to handle future requirements, and less expensive to maintain.

For example, a policy-enabled API-gateway asks its policy engine whether a client request should be allowed. The policy engine makes a decision, and the API-gateway rejects the request or forwards it along. Without the policy engine, the logic that decides whether to accept or reject is hard-coded/configured into the API-gateway.

Diagram of a policy-enabled API gateway querying a policy engine

Decoupling allows us to define policy in a language different from the one used to implement the service that enforces policy. We can choose a higher-level language for expressing policy that makes policy easier to write, update, understand, analyze, and optimize. For example, it's much simpler (for most people) to read and write "permit tcp host 1.2.3.4 port http" than it is read or write the equivalent C (or Java or Python or …) code.

Policy engines usually support declarative languages for defining policy. A declarative language lets us tell the system what we want it to do as opposed to imperative code where we tell the system how to do what we want. Declarative languages balance peoples' need for expressing policy with the policy engine's need to understand the policy definition.

Declarative languages are also nice because they can provide:

  • **Guarantees that code will return an answer (i.e., it will not run forever.)
  • **Concise and readable syntax designed to express constraints.
  • **Consistent and repeatable results given the same code and data.
  • **Dry-run features to see what would happen if code or data changes somehow.
  • **Hot-reloading when we want to change the deployed policy.
  • **Performance optimizations without requiring us to change our code.
  • **Debugging support to answer questions like "why was decision X made?"

Beyond the strengths of using declarative languages for policy, decoupling also enables:

  • **Visibility into policy violations that have occurred in the system.
  • **Automatic remediation when the policy or relevant state changes in the system.
  • **Sharing across different components in the system (which may be written in different languages).

All of the benefits described above become available when we decouple policy decisions from policy enforcement. Furthermore, when we use high-level declarative languages to express policy, we simplify the task of reading, writing, and managing the rules that govern our systems.

That said, it's a lot of work to build policy engines with everything described above. We have to create well-defined languages, implement parsers, compilers, and query evaluation. We also need solid APIs and powerful tools to ingest data, execute queries, debug errors, profile performance, and so on.

In upcoming series we'll dive into existing policy efforts in the cloud native ecosystem and talk more about the Open Policy Agent project. Thanks for reading!

What is Policy? Part One: Enforcement

Welcome to the Open Policy Agent project. If you're interested in topics like policy, enforcement, remediation, and compliance we'd love to hear from you! Join us on Slack or check out the project on GitHub.

This is the first in a two-part series about policy where we introduce definitions, concepts, and challenges in policy enforcement. In future series we'll examine the state of policy in the cloud-native ecosystem.

The word policy means different things to different people in different contexts. In the context of software systems, policies are the rules that govern how the system behaves.

Computers and humans use policy to answer questions such as:

  • Is this user allowed to change the config of that service?
  • Is this VM allowed to accept TCP connections from that VM?
  • Which host should this container be deployed on?
  • Which workloads are running in the wrong geographic region?

Diagram showing where policy decisions occur across a Scheduler, Host, Container, App, sshd, Network, and DB

We define policy to avoid repeating mistakes and ensure important requirements are met around cost, technology, security, legislation, internal conventions, and so on.

Understanding where requirements are met and where they aren't is by itself quite valuable (more on this in a later post), but eventually we need to enforce policy to ensure that systems behave the way policy says they should.

We enforce policy differently depending on the organization, the technology the policy applies to, and of course, the policy in question. In some cases, we rely on in-person communication whereas in other cases we embed special-purpose components into our systems that enforce policy automatically.

Diagram of the policy maturity spectrum from tribal knowledge and wiki to hard-coded, config, and policy engines

At one end of the spectrum, policies are tribal knowledge. They are not recorded anywhere, so if we to want to modify the system, or even understand how it's expected to behave, we ask someone.

Eventually, we tire of answering (or asking) the same question so we record the answer on the wiki or in the docs. These docs always fall out of date eventually.

When our companies grow rapidly, introduce automation, or are faced with frequent policy violations, we usually start hard-coding policy into our software. If policy could be defined once and forgotten, we would stop there. In reality, policy evolves. With hard-coded solutions we have to read the code to understand (let alone modify) policy. As a result policy becomes less accessible and more expensive to maintain.

Once the pain of hard-coding becomes evident, we make our policies configurable in software (e.g., including config parameters or even going as far as defining custom DSLs.) However, our abstractions often leak or cannot be adapted for future requirements. As a result, we spend more time and money on development, re-education, and upgrades. It turns out that predicting future requirements around cost, technology, internal conventions, and so on, is HARD.

The eventual conclusion of this progression is the policy engine: a tool for codifying and enforcing policies that is flexible enough to encompass a wide range of future requirements around cost, technology, internal conventions, and the like. It balances the desire to have programmatic enforcement (like hard-coding and configuration) with the need to update policy frequently and inexpensively (like tribal knowledge and the wiki).

In our next post we'll look at policy engines, declarative languages, and the decoupling of policy decisions from enforcement. Thanks for reading!