Skip to main content
View all authors

v0.10 Release

OPA v0.10.0 release announcement banner animation

We're excited to announce the v0.10.0 release of OPA. This release contains more than 60 commits from 8 authors across 5 organizations. For a detailed list of changes see the GitHub releases page.

WebAssembly

This release adds experimental support for compiling OPA policies into WebAssembly (Wasm) binaries that can be executed in any Wasm runtime (e.g., V8).

The compiler is designed to be lightweight and embedded inside other programs. The package does not depend on any third-party compiler toolchains like LLVM or Wasm-specific toolchains like Emscripten. The compiled policies are fairly small in size (10KB for toy examples) and have no system call dependencies — making them easy to instantiate inside your app, serverless function, etc.

We are excited about the potential that Wasm brings to the policy enforcement space because it provides a portable, secure, and efficient runtime for answering policy queries. You can find out more about Wasm at https://webassembly.org/.

If you are feeling adventurous, you can try out the Wasm compiler today with the new opa build command. We only support a limited subset of the language today but we plan to extend coverage over the next few months. If you run into any problems, please file an issue on GitHub.

Improved Test Support with Data Mocking

This release adds support for replacing (or mocking) values under the data document using the with keyword. You can use the with keyword to replace both external JSON loaded into OPA as well as JSON generated by rules.

Prior to v0.10, OPA only allowed you to replace values under the input document. This made it hard to test contextual policies using OPA's test framework. For example, the following policy depends on "roles" data being loaded into OPA:

package authz

import data.roles

default allow = false

allow {
input.method == "GET"
input.subject.role == roles[_]
}

In v0.10.0, you can write a test rule that mocks the value of data.roles using the with keyword:

# Define some dummy inputs.
dev_input = {"method": "GET", "subject": {"role": "dev"}}
hr_input = {"method": "GET", "subject": {"role": "hr"}}

# Define some dummy role data.
fake_roles = ["hr"]

# Test the allow rule.
test_allow {
allow with input as hr_input with data.roles as fake_roles
not allow with input as dev_input with data.roles as fake_roles
}

Partial Evaluation: Negation Optimization

Earlier this year we added a feature called Partial Evaluation that helps pre-compute portions of your policy. Today, a large fragment of the language is covered by Partial Evaluation but some constructs are not fully supported.

Prior to v0.10, OPA would generate support rules for negated expressions (which can be difficult to post-process.) This was required because Rego queries only consist of a series of expressions AND-ed together. If you need to express an OR condition, you need multiple queries. When you negate an expression (e.g., not deny), the in-lined result may be a series of expressions OR-ed together. For example, given the following policy that says (in English) "no one is allowed to buy more bitcoin or eat pizza":

allow {
not deny
}

deny {
input.action = "buy"
input.resource = "bitcoin"
}

deny {
input.action = "eat"
input.resource = "pizza"
}

We can partially evaluate the deny rule to yield the following simplified queries:

+---------+----------------------------+
| Query 1 | input.action = "buy" |
| | input.resource = "bitcoin" |
+---------+----------------------------+
| Query 2 | input.action = "eat" |
| | input.resource = "pizza" |
+---------+----------------------------+

However, it's a bit trickier to partially evaluate the allow rule. OPA does not allow you to negate multiple expressions at once (you have to factor those expressions into a separate rule). In some cases though, it's reasonable to in-line the result of partially evaluating a negated expression by computing the cross-product. In this case the answer is:

+---------+--------------------------------+
| Query 1 | not input.action = "buy" |
| | not input.action = "eat" |
+---------+--------------------------------+
| Query 2 | not input.action = "buy" |
| | not input.resource = "pizza" |
+---------+--------------------------------+
| Query 3 | not input.resource = "bitcoin" |
| | not input.action = "eat" |
+---------+--------------------------------+
| Query 4 | not input.resource = "bitcoin" |
| | not input.resource = "pizza" |
+---------+--------------------------------+

By in-lining negated expressions like this, we avoid the need for support rules (which are more difficult to optimize and translate into other languages like SQL and Elasticsearch.) Of course, the size of the cross-product can get quite big, so we put a cap on what OPA will in-line.

More Great Contributions

This release also included a many other contributions from members of the community. Here are some highlights:

Write Policy in OPA. Enforce Policy in SQL.

This post explains how to use OPA and SQL to protect access to sensitive data in your services without impacting consistency, performance, or scalability. We show how to translate OPA policies into SQL and enforce them within the database.

Throughout this post we will refer to a hypothetical service (petprofilesv1) used by a chain of veterinary clinics. The service exposes an HTTP API that serves profiles for pets at the clinics. The service implements the HTTP API by querying a SQL database and returning the results.

Client sends GET /pets/fluffy to petprofilesv1, which queries the DB

Services like petprofilesv1 often implement authorization policies that depend on attributes of the objects being accessed. They often also include authorization policies that must be applied to filter data in API results.

In this post, we dive into how services can integrate with OPA to enforce these kinds of authorization policies.

Replicating Context Is Hard

Imagine we want to enforce a simple role-based authorization policy in our hypothetical service that says:

"Only veterinarians are allowed to read pet profiles."

Client and petprofilesv1 query OPA with method/path/subject roles and receive an allow response

To implement this policy we could create a simple allow rule in OPA:

default allow = false

allow = true {
input.method = "GET"
input.path = ["pets", name] # name unused for now
input.subject.roles[_] = "veterinarian"
}

When the service queries OPA, it provides the identity of the caller (input.subject), the operation being performed (input.method), and the resource being operated on (input.path). The response from OPA indicates whether the request should be allowed (true) or denied (false).

This model works well when all of the context required for the decision is carried in the request. But what happens if the incoming request does not include the necessary context? For example, suppose the policy should say:

"Only the treating veterinarian is allowed to read a pet's profile."

In this case, a mapping from pet to treating veterinarian is required but not included in incoming HTTP GET requests.

One way of solving this would be to have the service fetch relevant context and provide it as input to the policy query:

allow = true {
input.method = "GET"
input.path = ["pets", name] # name unused for now
input.subject.user = input.pet.veterinarian
}

This approach works well for small inputs and keeps the enforcement model relatively simple.

The downside is that it requires the service to know the exact context required by the policy. Each API endpoint exposed by the service (or set of services) could need custom logic to fetch the context to provide as input to the policy query. Tight coupling between the service and the policy is difficult to maintain over time. Moreover, as the size of the input grows, it may become prohibitive to fetch and supply context on every query.

Another approach is to have the pet-veterinarian mapping replicated into OPA and cached in-memory. This approach requires an additional component to act as a data source (DS) that gathers the extra context required by policies and loads it into OPA.

A DS component feeds pet-veterinarian mappings into the DB, which OPA and petprofilesv1 use to make the allow decision

If we used this approach, we could rewrite the OPA rule as follows:

allow = true {
input.method = "GET"
input.path = ["pets", name]
data.pets[name] = input.subject.user
}

This avoids directly coupling the service with the policy. However, the obvious challenge is that OPA must maintain a cache of all pet-veterinarian mappings to authorize requests correctly. Depending on the size of the mapping and the consistency requirements of the service, this may not be feasible: OPA may not have the up-to-date mappings while evaluating the policy.

We could also use built-in functions in OPA to execute HTTP requests or query external databases during policy evaluation however this approach makes policies harder to test in isolation and the extra network hop negatively impacts latency and availability. Similarly, this approach may also violate the service's consistency requirements.

Lists Require Filtering

In addition to challenges with replication, we also have to deal with APIs that return lists of resources.

When designing service APIs, it's common to expose a list operation to return all the resources in a collection (e.g., the petprofilesv1 service needs to expose GET /pets). Since list operations frequently return the same information (for each resource) as reads for individual resources, it's important that we apply the authorization policy to filter elements in the result.

While we could execute a policy query for each resource (or structure the policy to accept a list of resources to authorize access to), this approach complicates pagination models and does not scale well compared to filtering in the data store.

Partial Evaluation

In the first section of this post we explained why it may be difficult to replicate context into OPA in a reliable, maintainable, and performant manner.

As of the latest release of OPA (v0.9), services can leverage the Partial Evaluation feature to avoid replicating context into OPA. When services use Partial Evaluation, they specify what portions of the data or input documents are unknown. When OPA evaluates the policy, any statements that depend on unknown values are not evaluated — they are saved and returned to the caller.

By partially evaluating authorization policies, OPA can treat context that's unavailable during evaluation as unknown. For example, statements that depend on the pet-veterinarian mapping would be saved and returned to the caller.

Partial evaluation output: the unresolved condition data.pets.fluffy.veterinarian = "alice" is returned alongside OPA

The table below shows the output when the service queries OPA for a request from alice trying to access fluffy's profile.

+----------+-------------------------------------------------------+
| Policy | allow { |
| | input.method = "GET" |
| | input.path = ["pets", name] |
| | data.pets[name].veterinarian = input.subject.user |
| | } |
+----------+-------------------------------------------------------+
| Input | { |
| | "method": "GET", |
| | "path": ["pets", "fluffy"], |
| | "subject": {"user": "alice"} |
| | } |
+----------+-------------------------------------------------------+
| Unknowns | [data.pets] |
+----------+-------------------------------------------------------+
| Output | data.pets["fluffy"].veterinarian = "alice" |
+----------+-------------------------------------------------------+

When OPA partially evaluates policies, the output is a simplified version of the policy. The result of partial evaluation can be interpreted as a sequence of Rego expressions ANDed and ORed together:

( expr-1 AND expr-2 AND … ) OR ( expr-N AND expr-N+1 AND … ) OR …

If we extend the policy to allow pet owners to access their pet's profiles and require that veterinarians be signed in from a device at the pet's clinic, the output would include two queries (which can be ORed):

+----------+-------------------------------------------------------+
| Policy | allow { |
| | input.method = "GET" |
| | input.path = ["pets", name] |
| | data.pets[name].owner = input.subject.user |
| | } |
| | |
| | allow { |
| | input.method = "GET" |
| | input.path = ["pets", name] |
| | data.pets[name].veterinarian = input.subject.user |
| | data.pets[name].clinic = input.subject.location |
| | } |
+----------+-------------------------------------------------------+
| Input | { |
| | "method": "GET", |
| | "path": ["pets", "fluffy"], |
| | "subject": { |
| | "user": "alice", |
| | "location": "SOMA" |
| | } |
| | } |
+----------+-------------------------------------------------------+
| Unknowns | [data.pets] |
+----------+-------------------------------------------------------+
| Output 1 | data.pets["fluffy"].owner = "alice" |
+----------+-------------------------------------------------------+
| Output 2 | data.pets["fluffy"].veterinarian = "alice"; |
| | data.pets["fluffy"].clinic = "SOMA" |
+----------+-------------------------------------------------------+

In some cases, OPA can still determine that a request should be allowed or denied unconditionally. In these cases, OPA returns a single empty query or no queries at all (respectively.) For example, if the input above was missing the subject field, neither allow rule would match (even partially) and the result would be empty.

By returning the simplified remainder of the policy to the service, we avoid evaluating the entire policy in one place — which means we do not have to replicate all of the context into OPA. The tradeoff is that the response from OPA is not simply an allow (true) or deny (false) value anymore — it's a set of Rego queries that must be evaluated by something (eventually).

This section explained how we can avoid replicating context into OPA using Partial Evaluation. However, this only solves part of the problem. If the service were to evaluate the result of Partial Evaluation itself, the service would still have to fetch the additional context from the data store (which would likely result in a solution with the same issues as before.)

Enforcing OPA policies with SQL

To overcome the issues we outlined above, the remainder of the policy needs to be evaluated as close to the data as possible — inside the database.

Since OPA policies are essentially just queries, the translation from Rego into another query language, like SQL, is relatively easy. As long as the policies expressed in Rego do not perform joins, we can translate sets of Rego queries into SQL expressions that get appended onto WHERE clauses. For example, the last policy from above says that:

"Pet owners can access their own pet's profiles." "Veterinarians can access pet profiles from devices at the clinic."

We can express this policy in OPA as follows:

package petclinic.authz

default allow = false

allow {
input.method = "GET"
input.path = ["pets", name]
allowed[pet]
pet.name = name
}

allowed[pet] {
pet = data.pets[_]
pet.owner = input.subject.user
}

allowed[pet] {
pet = data.pets[_]
pet.veterinarian = input.subject.user
pet.clinic = input.subject.location
}

In this example we have factored the authorization decision into helper rules named allowed. The allowed rules generate a set of pets the user is allowed to see.

To integrate with OPA, the service invokes the Compile API and marks the data.pets path as unknown. For example, when fluffy's veterinarian alice requests the profile from a device at the clinic, the query to OPA looks like this:

{
// The policy query to run.
"query": "data.petclinic.authz.allow = true",
// The input document to use.
"input": {
"method": "GET",
"path": ["pets", "fluffy"],
"subject": {
"user": "alice",
"location": "SOMA"
}
},
// The values to treat as unknown during evaluation.
"unknowns": ["data.pets"]
}

The response from OPA contains two queries:

# Query 1
pet = data.pets[_];
pet.owner = "alice";
pet.name = "fluffy"

# Query 2
pet = data.pets[_];
pet.veterinarian = "alice";
pet.clinic = "SOMA";
pet.name = "fluffy"

The service can consume these responses by translating them into SQL WHERE clauses. To keep things simple we can interpret references like data.pets[_].owner as follows:

  • The prefix data.pets refers to the pets table in SQL.
  • The variable _ refers to a row in the pets table.
  • The suffix owner refers to a column in the pets table.

With this interpretation we would produce the following SQL expression:

(pets.owner = "alice" AND pets.name = "fluffy") OR
(pets.veterinarian = "alice" AND
pets.clinic = "SOMA" AND
pets.name = "fluffy")

To show how you can implement a library that converts a fragment of Rego into SQL WHERE clauses, we have prepared an example that includes a library. You can find the example on GitHub in the OPA contrib repository.

Data Filtering with OPA

In addition to the API to get individual pet profiles, our service also exposes an API to list pet profiles (e.g,. GET /pets). In many APIs, list operations return the full extent of the resources in the collection. Because of this, it's important to apply the same data authorization policy there.

Even if the list operations only returned a subset of fields (e.g., the resource ID), it's a common requirement to not show clients the IDs of resources they are not allowed to access, so the filtering policy should still be applied.

We can extend the policy from above to cover list operations by adding the following rule:

allow {
input.method = "GET"
input.path = ["pets"]
allowed[pet]
}

This rule is similar to the ones from earlier. The only differences are that it matches on the path ["pets"] (instead of ["pets", name]) and there is no condition on pet.name.

The result sent back to the service from OPA will be similar to the cases above. For example, if alice tries to list the pet profiles from the same device at the SOMA clinic the response from OPA will be:

# Output #1
pet = data.pets[_]
pet.owner = "alice"

# Output #2
pet = data.pets[_]
pet.veterinarian = "alice";
pet.clinic = "SOMA"

These statements will get translated into a SQL WHERE clause that return all of the pet profiles that alice is allowed to see at the SOMA clinic as well as any profiles of her own pets at any clinic:

(pets.owner = "alice") OR
(pets.veterinarian = "alice" AND pets.clinic = "SOMA")

This example highlights how you can reuse the same data authorization policy across both the get and list APIs.

Try It Out

You can try out this new capability with the latest release of OPA. If you integrate with OPA in Go, you can use the rego.Rego#Partial function to invoke Partial Evaluation and obtain a set of conditions to apply to incoming requests. Similarly, if you integrate with OPA via HTTP, you can use the new Compile API to obtain identical conditions (represented in JSON) that you can process in any language.

We have also published a small example that shows how to integrate a simple Python service backed by SQLite. The example includes a Python module that translates Rego queries into SQL predicates. The example includes additional support for policies that perform joins as well as relational operators like !=, <=, >=, etc.

Wrap Up

This post explored how you can leverage OPA to enforce context-aware authorization policies that go way beyond simple protocol-level approaches. By using Partial Evaluation to obtain conditions that you evaluate in your service or via database queries, you can implement data protection and filtering policies in your service that are correct, maintainable, consistent, performant, and scalable.

We anticipate these OPA features will be used to solve a number of interesting problems in the authorization space, such as multi-tenancy support and protection of PII and other sensitive data.

Next we plan to tackle:

  • Integrations with other kinds of databases, e.g., Elasticsearch.
  • Tooling to identify when policies exceed the supported fragments of Rego.
  • Indirection layers between the policy and underlying DB schema.
  • Other use cases that build on partial evaluation like column masking.

If you have questions or comments, or you are interested in contributing, please reach out via Slack or create tickets on GitHub.

v0.9 Release

This post provides a quick overview of the work that has gone into OPA v0.9. As usual, the release is published on GitHub Releases and Docker Hub.

Securing the Data Lake: Ceph and Minio Integrations

During this release cycle we worked with the upstream Ceph and Minio communities to introduce fine-grained access control into data lakes using OPA. Both Ceph and Minio support the standard S3 object storage APIs and are popular choices for deploying object storage services.

The integrations into Ceph and Minio allow administrators to express fine-grained attribute-based access control (ABAC) policies over requests to the object storage layer. Compared to Bucket Policies and Object ACLs, these integrations give administrators greater control over sensitive data stored in these services. Using OPA you can enforce policies over object and file access based on context such as:

  • Time of day
  • Multi-Factor Authentication (MFA) attributes
  • Geographic region of the connecting client

We expect to see more adoption of OPA to control access to sensitive data as more and more organizations build out data lakes using object storage services like Ceph and Minio.

For more information on the integrations see the PRs:

Profiling Policy Evaluation

OPA now includes a command-line tool that helps you understand the performance of your policies.

Given a policy query, the tool reports per-expression metrics including:

  • Time spent evaluating each expression (non-recursive)
  • Number of times each expression is called from the outside
  • Number of times each expression is re-evaluated due to backtracking

The need for profiler support became obvious after reviewing and optimizing several large policies. These policies relied on sophisticated search logic split across hundreds of lines of policy statements in multiple files. In many cases it was possible to improve performance significantly by simply tweaking how context was structured.

We hope that by providing a profiler, policy authors will be able to quickly identify the root cause of performance issues.

You can invoke the profiler on the command line via the opa eval subcommand. For example:

opa eval data.rbac.allow --data rbac.rego --format pretty --profile

By default, when profiling is enabled, OPA will output the top 10 most expensive expressions in the policy.

Example profiler output

Example profiler output.

For more information on the profiler tool, see the new documentation page describing how to test your policies.

Compile API

Finally, as of v0.9, OPA includes new APIs that allow callers to invoke Partial Evaluation via HTTP or in Go with the Rego package.

In the past, decisions returned by OPA were always definitive, e.g., callers would query for an "allow" or "deny" decision and the answer would always be "true" or "false". With partial evaluation exposed, OPA can return condition answers, e.g., "allow" is "true" if certain conditions are satisfied.

Later this week we will publish a blog post that describes how you can enforce data filtering policies in your storage layer with OPA by leveraging the new Compile API.

Managing OPA

OPA is described as "a general-purpose policy engine that let's you offload decisions from your service," requiring access to policy and data for decision-making.

Before version 0.8, OPA "only exposed low-level HTTP APIs that let you push policy and data into the engine." Version 0.8 adds new management capabilities for distributing policies/data and monitoring agent health.

Architecture diagram showing Control Plane connected via Policy Distribution and Policy Telemetry to two Nodes, each containing a Service and an OPA instance

Bundle API

OPA can now be configured to download "bundles" — described as "gzipped tarballs containing Rego and JSON files" — from remote HTTP endpoints on a periodic basis.

GET /bundles/example/authz HTTP/1.1

Services should respond with a gzipped tarball. If an ETag header is included, it gets reused in subsequent If-None-Match requests, allowing servers to reply with an HTTP 301 Not Modified instead of resending the same bundle.

More info: http://www.openpolicyagent.org/docs/bundles.html

Status API

Since OPA already supports Prometheus-based metrics reporting (http://www.openpolicyagent.org/docs/monitoring-diagnostics.html#prometheus), the new Status API answers two additional questions: which policy/data version is active, and what errors occurred during load.

OPA can be configured to periodically POST status updates to a remote endpoint:

POST /status HTTP/1.1
Content-Type: application/json
{
"labels": {
"app": "my-example-app",
"id": "1780d507-aea2-45cc-ae50-fa153c8e4a5a"
},
"bundle": {
"name": "http/example/authz",
"active_revision": "660daf152602bd95ea2d2139d215678b0ed[...]",
"last_successful_download": "2018-04-17T12:34:40.258Z",
"last_successful_activation": "2018-04-17T12:34:42.196Z"
}
}

The payload also includes labels that "uniquely identify the agent."

More info: http://www.openpolicyagent.org/docs/status.html

Decision Log API

For auditing and debugging, OPA can buffer and periodically upload batches of policy decisions to a remote endpoint. Each event includes:

  • The name of the policy decision requested by your service.
  • The input provided in the query by your service.
  • The result returned by OPA to your service.

Events also include metadata such as bundle revision and agent labels.

More info: http://www.openpolicyagent.org/docs/decision_logs.html

Wrap Up

The author frames these features as simplifying large-scale OPA management while enabling more advanced tooling. Feedback is welcomed via Slack: http://slack.openpolicyagent.org

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.

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!