Skip to main content
View all authors

Note from Teemu, Tim, and Torin to the Open Policy Agent community

Banner announcing OPA creators joining Apple

Today we're excited to announce that the creators of Open Policy Agent (along with many team members from Styra) have joined Apple to continue our decade-long mission of delivering an open source solution to unifying policy across the cloud-native stack.

Apple is an enthusiastic OPA user, utilizing it as a key component of its authorization infrastructure to manage a vast portfolio of global-scale cloud services. Today's announcement demonstrates Apple's commitment to the OPA project by making a larger investment in the technology and the community.

Open Policy Agent has a rich and vibrant community of end-user organizations, vendors, and individuals each contributing ideas, integrations, docs, and code so anyone in the world can use OPA to enforce the policies they care about. We've been fortunate to see such a community grow over the last decade, and look forward to continuing our contributions to the project as active community members.

We've compiled a set of FAQs below to address questions.

What does this mean for the Open Policy Agent project? Open Policy Agent remains a CNCF graduated open source project and there are no changes to the project governance or licensing.

What does this mean for Open Policy Agent maintainers? There is no change to the list of maintainers, except for an organization change from Styra to Apple for the maintainers that are joining Apple.

What will happen to the tools I use under the Styra GitHub? We've initiated the community process for these repositories to be included in the CNCF OPA GitHub organization with the goal of deeper collaboration with the open source community:

  • Styra's commercial distribution of OPA, EOPA: an optimized version of OPA designed for data heavy workloads with data-filtering functionality that was previously only available to enterprise customers.
  • OPA Control Plane: a new control plane for OPA capable of building bundles from git and additional datasources and deploying them to S3 on AWS, GCP, and Azure.
  • SDKs: SDKs for integrating with OPA including TypeScript, React, UCAST-Prisma, C#, ASP.NET, Java, and Springboot.
  • Regal: a linter for OPA's policy language Rego (partly written in Rego itself).

Other tools will be evaluated for contribution in the future and remain publicly available for the community.

What will happen to the OPA website and Rego Playground? The OPA website continues to remain available and managed by the CNCF and broader OPA community. The Rego Playground continues to be operated by Styra with no changes to current functionality.

What is the planned roadmap for OPA? We're excited to continue the development of OPA with the same monthly release schedule. The 2025 OPA roadmap includes the following categories of work:

  • Language extensions (and/or/not, keywords in refs, string interpolation, partial-set-functions, ellipsis)
  • Type checking improvements
  • Tooling improvements (streaming OPA test results, debugger attach to trace, rule tracing)
  • Partial evaluation improvements (redundant expression elimination, 'in' handling)
  • Performance (multiple expression indexing, faster loading of dependency-free bundles)
  • Decision API and logging (congestion back-pressure, logging metadata, logging to disk)

Open Policy Agent graduates in the Cloud Native Computing Foundation

OPA graduates in the Cloud Native Computing Foundation banner

We're excited to announce that Open Policy Agent (OPA) is now a graduated project in the Cloud Native Computing Foundation (CNCF)! Graduation reflects the maturity of the project in terms of adoption, diversity of contributions, community and overall quality.

The growth over the last year has been phenomenal. The number of users on slack.openpolicyagent.org has grown by 3x (to over 3,600 users) and the number of Docker image downloads surpassed 39M (a 1000% increase!) We attribute much of this growth to the need for a robust policy-as-code solution in the cloud native ecosystem.

In the last year, the project shipped a number of powerful new features such as signed bundles, improved data fetching capabilities, parser and evaluator optimizations, as well as expanded support for WebAssembly-based execution environments. OPA Gatekeeper reached GA and added several features including multi-pod scalability, semantic logging, fine-grained metrics, dry-run support, and more. The popular Conftest tool (which helps you write tests against structured configuration files) became an official OPA subproject. Finally, support for IntelliJ users landed with the OPA IDEA plugin.

Graduation is a huge milestone for the project, and we wanted to take a moment to thank everyone involved in making OPA a successful, graduated project:

  • First, we'd like to thank the CNCF for their partnership and for all their support over the years. We would particularly like to thank Chris Aniszczyk, Amye Scavarda Perrin, Ihor Dvoretskyi for the excellent support they provided along the way.
  • We'd also like to thank all of the maintainers and core contributors to the OPA project: Ash Narkar, Boran Seref, Craig Tabita, Gareth Rushgrove, John Reese, Lennard Eijsackers, Max Smythe, Oren Shomron, Patrick East, Rita Zhang, Sertaç Özercan, and Stephan Renatus.
  • Lastly, we'd like to send a HUGE thank you to the entire OPA community. All new OPA features and partnerships were driven by your feedback and contributions. We also want to recognize certain people whose feedback, contribution, and support has been invaluable: Anders Eknert, Jasper Van der Jeugt, Joe Searcy, and Vincent Gramer.

Going forward we'll continue to focus on improving all aspects of OPA while solving real problems around policy and authorization in the cloud native ecosystem. We look forward to continuing to work with the amazing OPA community that started nearly five years ago. But, for today, let's all celebrate (socially distanced, of course) together on this great milestone!

Rego Design Principle #3: Optimize Performance Automatically

Banner for Rego design principle on automatic performance optimization

This is the third part of a blog series on the design principles behind Open Policy Agent's (OPA's) policy language Rego. Previously, we described how Rego's syntax was designed to mirror the structure of real-world policies and how Rego embraces hierarchical data. In this part of the series, we look at Rego/OPA's commitment to automated performance optimization so that policy authors can focus on writing readable, maintainable policies, and OPA can shoulder the burden of evaluating those policies efficiently.

Performance is Important, but Policy Authors Should Be Able to Ignore It

The goal of OPA is to take policies that people write and automatically enforce, monitor and remediate them. No more relying on people to remember the policies, to understand them or to correctly apply them. OPA helps everyone follow the rules and protects the infrastructure, applications, etc. from security, compliance and operations problems.

The scale and speed of modern, cloud-native systems means that OPA policies are routinely applied to millions of objects and actions. Pinterest, at the last Kubecon, described using OPA to make 400k decisions per second (8M with caching), globally across all infrastructure. At that scale, how long it takes to evaluate an OPA policy, how much memory it uses, and how many requests can happen in parallel all matter a great deal.

Despite the importance of performance one of OPA's design principles is to minimize how much a policy author worries about performance. The policy author should write the logic of her policy so that it is easy to read, maintain, extend, and combine with other peoples' policies. The author shouldn't be forced to complicate the logic of her policy in order to make it perform well.

The policy author handles correctness. OPA handles performance.

In the end, this design principle is all about usability. Policies are easier to write because you only think about correctness and not performance. Policies are easier to read because they're written closer to the way people think about them in the real world. Policies are easier to maintain and extend because you write them each individually and leave global optimizations to OPA. Upgrade OPA and your policies get faster.

As a point of comparison every programming language aims for good performance too, but OPA's goal is qualitatively more ambitious in the sense that even the global asymptotic runtime of policy evaluation is something that the policy author should be able to ignore. If the clearest way to write/read your policy looks like an O(n¹⁰) algorithm in a traditional language even though it can be done with 10 linear scans, OPA aims to analyze that policy, reformulate it, set up the proper indexing and evaluate it in linear time.

How OPA Automates Performance Optimization

Our approach to realizing this design principle was to base Rego on database query languages and leverage 50 years of R&D on automated performance optimization. In particular:

Rego has no side-effects. Rego always produces the same outputs given the same inputs, and there are no side-effects before, during or after evaluation. Side-effects would include writing to a file or modifying the value of an in-memory object. Side-effects greatly complicate the design of optimization algorithms because those algorithms need to understand how to preserve those side-effects.

Rego rules are unordered. A Rego policy means the same thing regardless of how the rules are ordered. Since ordering is irrelevant, the meaning of each statement stands on its own and can be optimized on its own.

In contrast, in languages where ordering matters, every optimization must respect that ordering. Firewall rules are a perennial example, where to understand the impact that the 1000th firewall rule has, you need to understand the impact of the first 999. (Rego does support the 'else' keyword for ordering, but we advise using it sparingly.)

# Both allow and deny are true.
# Order of the statements does not matter.
allow = true {
input.method == "GET"
input.path == "/"
}
deny = true {
input.method == "GET"
input.path == "/"
}

Rego rules are simple. Just about every statement in Rego is a conditional variable assignment. A Rego statements assigns a variable to a value if some conditions are true. This uniformity and simplicity help ensure that optimizations apply across all Rego policy statements.

allow = true { # allow is assigned true if ...
input.method == "GET" # method is "GET" AND
input.path == "/" # path is "/"
}

Rego is designed in layers. Rego was designed like an onion: the core is syntactically quite simple and highly efficient; the layer above that is syntactically more expressive at the cost of some performance; and so on. The outer layer today is quite expressive but not Turing complete (though in the future we could relax some of the restrictions on the language if we wanted that escape hatch). Below is a diagram showing some of the layers of the language.

Performance and Expressiveness layers for Rego

Of course there are limits to what can be done at all and what has been implemented, but it's a clear design principle — that the language itself should be amenable to deep, automated analysis and global optimizations. At the time of writing, OPA has the following features either built out (GA), in progress (WIP), or planned.

Rego has automatic, multi-dimensional indexing (GA). OPA analyzes rules and automatically constructs a trie that finds the minimal set of applicable rules. For example, the following rules will be represented as the trie shown below.

# Rules as they appear in a Rego file
allow = true {
input.method == "POST"
input.path == "/pets"
something_complex
}
allow = true {
input.method == "GET"
input.path == "/pets/dogs"
}

Rules are represented in memory for fast evaluation

Partial evaluation (GA). Partial evaluation attempts to compile a policy from one layer into a lower layer (sometimes at the cost of creating additional rules). This could, for example, convert a linear-time policy into a constant-time policy.

# Before partial evaluation, a data-driven policy that is easy
# for people unfamiliar with Rego to contribute to.
allow {
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"},
]

After partial evaluation runs we get rules that the indexer can more easily analyze and organize in a trie.

# After partial evaluation
allow {
input.method == "PUT"
input.resource == "air-conditioner"
}
allow {
input.method == "GET"
input.resource == "security-camera"
}
allow {
input.method == "POST"
input.resource == "garage-door"
}

Compilation to WebAssembly (WIP). WebAssembly is a popular general-purpose virtual machine that has implementations in a growing number of languages like Go, Node, JavaScript, and Java. The Rego WebAssembly compiler takes a Rego policy and generates custom WebAssembly code that implements that policy, and like most compiler technology eliminates overhead for the interpreter and provides the opportunity for deep, automated performance tuning.

Automated asymptotic analysis (WIP). While still a work in progress, the goal of this tool is to identify the run-time complexity of a policy. Which of these three layers does it belong to? If it is a linear-time policy what slice of JSON data is it iterating over?

For example, in Kubernetes a common policy is to check if all images come from a trusted registry, say hooli.com. The complexity analysis will tell you that this policy has complexity O(input.request.object.spec.containers).

deny {
input.request.kind.kind == "Pod"
some i
image := input.request.object.spec.containers[i].image
not startswith(image, "hooli.com/")
}

Query optimization (Planned). As use cases like audit that require deeper searches over significant amounts of data become more popular, we plan to include optimizations that analyze dependencies, reorder rule evaluation, reorder conditions within rules, shift evaluation of conditions between rules, and the like.

Summary

One of OPA and Rego's design principles is that Rego policy authors should focus on correctness, maintainability and composability. They should not complicate their policy logic to make policy evaluation more efficient. More specifically:

  • OPA aims to optimize both local operations and the global asymptotic runtime
  • While Rego's syntax looks like a programming language, its semantics is based on database query languages.
  • The automated optimizations implemented in OPA continue to grow over time.

While OPA and Rego today have made terrific progress in terms of realizing the goal of automated performance optimization, there's still a lot to do. If you want to help, hop onto Slack or Github and contribute!

If you want to know more, check out the other blog posts in the series:

Rego Design Principle #2: Embrace hierarchical data

Diagram of OPA making decisions from query, data, and policy

This is the second part of a blog series on the design principles behind Open Policy Agent's (OPA's) policy language Rego. Previously we described how Rego's syntax was designed to mirror the structure of real-world policies. In this part of the series, we look at why and how Rego exclusively uses hierarchical data (e.g. JSON and YAML) to represent the raw information it uses to make decisions and to represent the decisions themselves. In the next part of the series, we discuss why and how OPA aims to optimize the performance of policy evaluation automatically.

Quick OPA Refresher

OPA is designed to offload policy decisions from a broad range of software services. You typically run OPA on the same server as the software needing policy decisions and cajole that software into asking OPA for a policy decision whenever it needs to. As shown in the diagram below, OPA makes a decision using the following pieces of information:

  • Policy query. An arbitrary JSON document provided by the service needing a policy decision. Think of the policy query as the concrete information (e.g. user-action-resource) that OPA needs to make a decision about.
  • External data. Any number of JSON documents injected into OPA out-of-band of the policy query that represent what's happening in the real world (e.g. the current resources in a k8s cluster or resource attributes like owner, size, etc.) and that are kept up to date as the world changes.
  • Rego policy. One or more Rego policies. Rego is a custom language purpose-built for expressing policy across any domain.

The focus of this blog post is to explain why and how we chose to use JSON to represent the policy query, the external data, and even the policy decision itself.

JSON is Everywhere

JSON (or more generally hierarchically structured data) is pervasive throughout the cloud-native ecosystem. Public clouds, kubernetes clusters, No-SQL (and even SQL) databases, service meshes, microservice APIs, and application configuration all ingest and export their state in JSON. Hierarchical data (as opposed to say relational data stored in classic SQL databases) is here to stay, arguably because it is well-suited for modeling many different aspects of software applications and the infrastructure they run on. And further, the prevalence of HTTP/JSON APIs makes JSON a pervasive format for exchanging information.

What this means for OPA is that it's a near certainty that when a service is asking OPA for a policy decision, it will have some hierarchical data that OPA needs to make the decision.

For example, maybe it's a JSON Web Token (JWT) that represents the user and her attributes:

{
"sub": "1234567890",
"name": "Alice Smithsonian",
"iat": 1516239022,
"groups": ["employee", "billing-manager"]
}

Or, maybe it's information about the attributes for a pet at a pet store:

{
"id": "i0779921",
"name": "Lassie",
"breed": "collie",
"owners": [{
"first": "Rudd",
"last": "Weatherwax"
}]
}

It could also be a description of the configuration for an application running on Kubernetes (here shown in the usual k8s YAML that converts easily to JSON):

apiVersion: admission.k8s.io/v1beta1
kind: AdmissionReview
request:
kind:
group: extensions
kind: Ingress
version: v1beta1
object:
metadata:
name: prod
labels:
costcenter: retail
spec:
rules:
- host: initech.com
http:
paths:
- path: /finance
backend:
serviceName: banking
servicePort: 443
- path: /retail
backend:
serviceName: storefront
servicePort: 8080

All across the stack, from infrastructure to microservices to the business data stored by an application, JSON is pervasive for representing information. Moreover, even in those areas where JSON data is not pervasive like SQL databases, it is straightforward to convert flat, non-hierarchical data into JSON; whereas, converting JSON into a non-hierarchical data format while possible presents many usability challenges.

How OPA interacts with the outside world

Remember that OPA can consume two sources of data to make policy decisions:

  • the data that the service provides as the policy query
  • the external data that gets injected into OPA that represents the state of the outside world

Both of those are arbitrary JSON. OPA does NOT impose any kind of schema or data model on those JSON documents. All OPA knows is that it's a chunk of JSON; it is up to the policy author to understand what that JSON represents in the world and write the policy that makes the appropriate decision.

We could have designed OPA differently. We could have designed OPA to have a schema or data model for each domain (e.g. k8s, service mesh, databases, applications) and required the outside world to adapt its data to OPA's model.

For example, suppose OPA required every policy query to have three fields:

  • username: a string that represents the user taking an action
  • action: a string that names the action of the user is trying to take
  • resource: a string identifying the resource being acted upon

This would mean that every application asking OPA for an authorization decision would need to supply exactly those three fields. If the application had the user information stored in the JWT as shown below, it could not just hand that JWT to OPA — it would need to extract the sub (subject) value and include it as the username value.

{
"sub": "1234567890",
"name": "Alice Smithsonian",
"iat": 1516239022,
"groups": ["employee", "billing-manager"]
}

Imposing a schema or data model would have made building OPA easier because it shifts the burden for integration to the outside world. Every system in the world that wants to integrate with OPA would need to include OPA-specific code that transforms the data to meet OPA's requirements.

Moreover, the same is true for the external data that OPA uses to make decisions. If OPA imposed a data model on all external data, the system pushing that data into OPA would need to understand OPA's data-model and transform the data from the outside world to match that model.

Instead, OPA was designed to ingest arbitrary JSON data for both the policy query and external data. This makes integrating with OPA easy: just convert the information into JSON (which every programming language has standard libraries for) and send it across. No need to ETL your data to get it into OPA — any webhook will suffice to integrate OPA. In short…

OPA should adapt to data in the outside world, not the other way around

Ingesting JSON data in whatever form is natural for the outside world is easy, but it does mean that the policy language Rego needs to be flexible enough that people can write policies that adapt to that format. The policy language can't rely on a fixed location for the username or the action, for example. It must be expressive enough that people can write policy that bridges the gap between the world's data model and the format that is best for expressing policy.

Rego support for JSON

The starting point for a Rego policy is (i) an arbitrary JSON object representing the policy query (a.k.a. input) provided by the external software (e.g. an API call, a configuration file, a data element, etc.) and (ii) some number of arbitrary JSON objects representing the state of the world. Neither OPA nor Rego understand what that data means in the real world, but the policy author does. The policy author writes Rego to encode the logic that navigates through those JSON documents and compares them to hard-coded values or other bits of JSON in order to make a decision.

For example, for a simple HTTP API the input JSON object could be:

{
"method": "GET",
"path": "/dogs/dog123",
"user": "alice",
"roles": ["customer", "guest"]
}

As a policy author, I know that this JSON object represents an HTTP API, but Rego doesn't. If I want to allow all GET requests to the root path, I write a simple rule with conditions on the input document (a global variable in Rego representing the policy query provided to OPA):

allow {
input.method == "GET"
input.path == "/"
}

This example shows simple equality checks with strings, but in general you might need to break a path like /dogs/dog123 into multiple pieces, manipulate numbers, inspect the internals of a JWT, etc. The scalar values in JSON often contain information that needs to be extracted or manipulated.

Rego must manipulate JSON scalar types: booleans, numbers, strings, and null

To that end, Rego has 50+ built in functions documented at openpolicyagent.org that provide all kinds of basic functionality needed to inspect and construct the scalar JSON types.

Of course, the whole point of supporting JSON is not the scalar types — it's the composite types: arrays and objects. Without those, there's no hierarchy at all.

There are two key requirements that arise from supporting JSON arrays and objects: the ability to drill down through a hierarchy (which you've already seen via dot notation) and the ability to iterate over elements of a collection (elements of an array or key/value pairs of an object).

Rego must navigate through deeply-nested arrays and objects

Navigating through arrays and objects when you know the exact path is straightforward in Rego. It uses same syntax used by many programming languages: dot-notation and bracket notation.

For example, suppose the following JSON object is the input.

{
"id": "i0779921",
"name": "Lassie",
"breed": "collie",
"owners": [{
"first": "Rudd",
"last": "Weatherwax"
}]
}

You can write all of the following expressions to navigate through this JSON document.

input.name # "Lassie"
input["name"] # "Lassie" x.y is syntactic sugar for x["y"]
input.owners[0] # First element of owner's array
input.owners[0].first # "Rudd"

More interesting is iteration. 99% of Rego statements are simple if statements, and iteration is primarily used as a condition in one of those if statements.

For example, say you want to allow an admin to perform any operation, and you're given an input that lists all the user's roles.

{
"method": "GET",
"path": "/dogs/dog123",
"user": "alice",
"roles": ["customer", "guest"]
}

You need to write a policy that says the request should be allowed if there is some element of the roles array that equals "admin".

Iteration in Rego uses the keyword some. You write an expression testing whether a condition is true and apply some to the variables in that expression that you want to iterate over.

In the admin example, you write the following Rego to check if there is some index i of the roles array where input.roles[i] equals "admin".

allow {
some i
input.roles[i] == "admin"
}

You can apply some to many variables at once. For Kubernetes policies this happens all the time. Here is an object that is roughly what Kubernetes hands over for admission control — notice how deeply nested the data is.

kind:
kind: Ingress
group: extensions
metadata:
name: prod
labels:
costcenter: retail
spec:
rules:
- host: initech.com
http:
paths:
- path: /finance
backend:
serviceName: banking
servicePort: 443
- path: /retail
backend:
serviceName: storefront
servicePort: 8080

If you want to deny the creation of this resource whenever there is some servicePort that is not 443, you would write the following Rego.

deny {
input.kind.kind == "Ingress"
some i,j
input.spec.rules[i].http.paths[j].backend.servicePort != 443
}

While that path to servicePort is somewhat long, it is simply the nature of the data. Seeing the path written out in a single line makes it relatively easy to map it back to the real data, which can help the reader understand the intent of the rule.

In contrast, in traditional programming languages, you need to decompose that JSON path into chunks and dictate exactly the range over which you want to iterate one variable at a time. Here would be the same example in Python.

function deny():
return input.kind.kind == "Ingress" and deny_aux()

function deny_aux():
for rule in input.spec.rules:
for path in rule.http.paths:
if path.backend.servicePort != 443:
return true

As a reader, to understand what the Python says in terms of the data, you need to reconstruct the JSON path by composing the paths in the for loops and if statements. The decomposed path approach shown in Python is closer to an implementation of policy than the policy itself.

Of course, Rego is flexible enough that you can decompose paths if you want.

deny {
input.kind.kind == "Ingress"
some i, j
rule := input.spec.rules[i]
path := rule.http.paths[j]
path.backend.servicePort != 443
}

Having had Rego's ability to iterate in different ways for the last few years, we find that sometimes we decompose paths and sometimes not. Personally, I typically avoid decomposing paths as I find them easier to read when I come back weeks or even days later because I can compare the policy statement more directly to the documentation for that JSON data; and often I don't even need the documentation because the path itself is self-explanatory.

Summary

Rego was designed to express policy over JSON data natively.

  • Why JSON? JSON is pervasive in cloud-native environments, which means that the external data and inputs that OPA uses to make policy decisions is easy to come by.
  • Rego is designed to adapt to the world around it — not the other way around. This leads to a low barrier for integrating with OPA, often requiring no OPA-specific code.
  • Rego has first-class support for inspecting JSON values. It has 50+ built in functions for string manipulation, JWT manipulation, network CIDR math, etc. And Rego has first-class support for navigating through deeply-nested arrays and dictionaries.

OPA was designed to be integrated into a wide array of software systems, and as such ease-of-integration is paramount. Rego's flexibility makes it applicable to a wide variety of use cases, and moreover makes it easy to integrate OPA across the cloud-native stack.

If you want to know more, check out the other blog posts in the series:

Rego design principle #1: Syntax should reflect real-world policies

Banner image for Rego design principle number one blog post

Sometimes people ask why Rego, OPA's policy language, looks or behaves the way it does. Part of the answer is that Rego came about after having built two other general-purpose policy languages, and lessons learned from that process shaped this one. This multi-part post lays out the results of that journey — the key design principles for Rego, why they're important, and how they influenced the language.

Related posts in the series:

The first design principle holds that Rego's syntax "should NOT be designed as a general-purpose programming language that reads from disk, writes to network sockets, supports multi-threading, defines custom datastructures, etc."

Refresher on OPA

OPA is a general purpose policy engine that separates policy decisions from the software services that enforce them. It bases decisions on input data, a Rego policy, and optionally external data reflecting real-world state (e.g., on-call schedules or resource ownership).

These functional requirements ensure that OPA has enough flexibility and generality to make context-aware decisions across a broad range of use-cases: admission control, API authorization, risk-analysis, data-filtering. OPA runs as a lightweight agent or library on the same server as the software service, thereby achieving both the availability and performance needed for policy decision-making in modern, cloud-native computing environments. It has been integrated with over 20 popular software systems and is at the time of writing an incubating project within the Cloud Native Computing Foundation.

Natural encoding of real-world policies

The goal is that Rego should map closely onto plain-language rules and regulations. A useful litmus test: reading Rego aloud should sound close to the source documentation.

This readability test is especially important because of the broad range of stakeholders who are responsible for policy: developers, operations, security, and compliance. The less of a translation there is from the PDFs and wikis the easier it is to believe that your Rego policies are correct, that operationally you're on solid ground, that your auditors will be convinced the policies do what they should, and that your security vulnerabilities are properly mitigated.

A Rego policy is a collection of if statements

If statements in Rego

Nearly every Rego statement functions as an if-statement, but with different proportions than typical programming languages:

"Programming languages typically have small if conditions and relatively large then blocks... in Rego the if condition is a potentially large block of expressions, and the then part is a single expression."

# Rego example.
# An API call is allowed if the method is a GET
allow {
input.method == "GET"
}

Multiple statements inside a rule are ANDed; ORs are expressed via multiple rules:

# Rego example.
# An API call is allowed if the method is a GET
allow {
input.method == "GET"
}

# An API call is allowed if the method is POST and
# the user is an admin
allow {
# Only admins can create new objects
input.method == "POST"
input.user_is_admin == true
}

An equivalent JavaScript version, given for contrast:

// Not a Rego example. A JavaScript example.
function allow() {
return allow1() || allow2();
}

function allow1() {
return input.method == "GET";
}

function allow2() {
return input.method == "POST"
&& input.is_admin == true;
}

Rego "has no need for explicit ANDs and ORs" within a rule, and includes "an explicit NOT operator." A self-documenting example:

# Rego example.
allow {
operation_is_create
user_is_admin
}

Rich policy decisions

Real-world policies sometimes make a decision as simple as allow or deny but often they go far beyond that. What about warn or error? Or what if the decision is a rate-limit (number), a permitted hostname (a string), or the clusters to deploy an application (an array). Since OPA is a general-purpose policy engine (not an authorization engine) it needs to handle a rich collection of policy decisions.

Because inputs are typically JSON, decisions in Rego can likewise be any JSON type—not just booleans.

A Rego decision is a JSON document

Rich Policy Decisions in Rego

allow and deny are plain variables, not keywords, defaulting to true:

# Rego example.
allow = true {
operation_is_create
user_is_admin
}

Non-boolean decisions work the same way:

risk = 100 {
input.method == "DELETE"
}

Partial sets can build up collections, such as error messages:

deny[msg] {
input.method == "DELETE"
not user_is_resource_owner
msg := "only the owner of a resource may delete it"
}

Collaboration

Real world policies are decided upon by multiple individuals and even teams. The security team might put global requirements in place across all application development teams and each application development team might put policy in place for their app. The k8s cluster administrator puts global policies in place but also delegates policy responsibilities to namespace-level admins. Policy is by its nature a collaborative endeavor, and Rego should recognize and support that.

At some level collaboration is supported simply because Rego is a text-based policy language (aka "policy-as-code"). Teams can check Rego policies into source control, and use peer-review to manage changes to it. We knew, however, that there are all too many examples where teams want to work more independently than that, e.g. putting global policies in place for an entire cluster and empowering team leads to manage policy for their portion of the cluster. That means that different teams should be able to write their policies independently from each other, and then combine those policies after the fact.

If different teams write different policies independently, they will inevitably end up with conflicts from time to time (e.g. one allows the decision and the other denys it). And so there must be a way to resolve those conflicts, based on a variety of different factors. Resolving conflicts is not always easy. It may depend on the teams involved, the kind of decision being made, the resource and its attributes, the time of day, and many other factors. Ergo the conflict resolution mechanism must be tantamount to a policy itself. Sometimes languages are designed to avoid the problem of conflict resolution by designing the language to not express conflicts at all. But this approach inevitably leads to problems because when two teams disagree in the real world and there is no way for them to express that disagreement in the policy language, they simply can't write the policy that they truly mean — the language is ambiguous in terms of the author's intent.

Rego policies are composable; conflict resolution is a policy itself.

Collaboration in Rego

Each policy lives in a package:

package microservice.authorization

Separate teams can define their own packages:

package developer
allow {}

package security
allow {}

A combining policy can reference other packages via the data keyword:

package main
allow {
data.developer.allow
data.security.allow
}

A more nuanced conflict-resolution example, where security's decision takes precedence when it has an opinion:

package main
# allow if the security team allows (and does not deny)
allow {
data.security.allow
not data.security.deny
}
# allow if the security team has no opinion and
# the developer team allows (and does not deny)
allow {
not data.security.allow
not data.security.deny
data.developer.allow
not data.developer.deny
}

Summary

The post recaps three requirements: policies are mostly if-statements and should read naturally; decisions can be any JSON value rather than just booleans; and composition/conflict-resolution use the same rule mechanics as ordinary policy logic.

Further reading:

Securing the Kubernetes API with Open Policy Agent

Banner image for securing the Kubernetes API with OPA

Kubernetes is being rolled out for production — it's mission critical. But it presents unique challenges around the age-old problem of who-can-do-what. Those challenges are exactly the ones the Open Policy Agent was designed to solve.

TL;DR

This post highlights several key ideas:

  • Controlling who-can-do-what on Kubernetes has unique challenges because to make an access control decision you need to inspect an arbitrary chunk of YAML, e.g. the images in all containers in all pods must come from a trusted repository.
  • The Open Policy Agent was designed around the premise that sometimes you need to write and enforce access control decisions over arbitrary JSON/YAML, so it's a perfect match for Kubernetes's challenges.
  • OPA supports a class of access control decisions called "context-aware" that enable you to make decisions based on the Kubernetes resources already in the cluster, e.g. no conflicting ingresses.

KubeCon Seattle 2018 Debrief

After each KubeCon we try to document the answer to the question we heard most often while talking to folks at the Open Policy Agent (OPA) booth. For those who don't know, OPA is a general-purpose policy engine for the cloud-native stack and has been applied to solve policy and authorization problems in several different domains, e.g. microservice authorization, data protection, ssh/sudo control, terraform risk-analysis, and most popular at KubeCon this year: Kubernetes admission control. The most common question we heard was

Why is OPA so well-suited for securing the Kubernetes API through admission control?

People heard about this use case in several different talks throughout the week, which is why we think so many people were asking about it at the booth. Here are links to those talks:

In this post, when we talk about securing the Kubernetes, we're talking about the Kubernetes API itself — the container management system. We're talking about helping you, the Kubernetes cluster admin, put guardrails in place so that the developers running applications on top of Kubernetes don't need be constantly referring to wikis or PDFs that detail what policies the organization has decided on around Kubernetes. OPA lets you codify those wikis and PDF policies into policy-as-code and enforce them directly on the cluster. For example:

  • every container image must come from a trusted, corporate repository
  • every application exposed to the internet must use an approved domain name
  • every resource must include a costcenter label
  • business critical storage volumes must use the retain storage policy

One thing people sometimes mean when they say "Kubernetes" is the applications running on top of the Kubernetes container management system. That's another use case for OPA, but not the one covered in this post. Here are a few references if you're interested in using OPA to provide API security for cloud-native applications themselves (whether or not they run on Kubernetes).

The Kubernetes YAML-centric API

The key reason OPA is such a good choice for securing Kubernetes is that the Kubernetes API is pretty unique, and that presents challenges for authorization and API security. Within the community people refer to it as the Kubernetes Resource Model (Brian Grant's doc, Tim Hockin tweet).

Each Kubernetes API call requires you to specify the desired-state for one of Kubernetes's many objects: pods, services, ingresses, deployments, etc. For example, here is you define the desired state for an nginx workload.

# nginx-pod.yaml
kind: Pod
apiVersion: v1
metadata:
name: nginx
labels:
app: nginx
spec:
containers:
- image: nginx
name: nginx

To create this workload, you use kubectl and hand it the YAML file (-f) above.

kubectl create -f nginx-pod.yaml

Updates happen similarly. Say you want to change the version of nginx, mount an external volume, or provide additional configuration. You update the nginx-pod.yaml file to whatever the desired state should be and use kubectl again, this time using apply instead of create.

kubectl apply -f nginx-pod.yaml

The Challenge of Securing the Kubernetes API

Imagine now that you want to require all images to come from a trusted repository (say, hooli.com). Anytime someone runs, say,kubectl create, the access control system needs to make a decision based on the user, the action create and the YAML that describes the pod, e.g.

kind: Pod
metadata:
labels:
app: nginx
name: nginx-1493591563-bvl8q
namespace: production
spec:
containers:
- image: nginx
name: nginx
securityContext:
privileged: true
- image: hooli.com/frontend
name: frontend
securityContext:
privileged: true
dnsPolicy: ClusterFirst
nodeName: minikube
restartPolicy: Always

To make the right decision, the access control system needs to extract the list of image names (e.g. nginx and hooli.com/frontend) and do string manipulation to extract the name of the repository (e.g. the default repo and hooli.com). To complicate matters, Kubernetes supports Custom Resource Definitions, which means we can't just build an access control system that knows the layout of these YAML files. We need the access control system to be expressive enough for all of the following:

  • Descending through the hierarchical structure of a YAML file.
  • Iterating over elements in an array.
  • Manipulating strings, IPs, numbers, etc.

Securing the Kubernetes API with Open Policy Agent

This is where the Open Policy Agent shines. OPA was designed to express access control policies (as well as other kinds of policies) over arbitrary JSON/YAML, along with a complete toolkit for testing, dry-running, auditing, profiling, and integrating those policies into third party projects. The list of requirements from the last section are first-class citizens in OPA's policy language: dot-notation, iteration, and built-in functions. That means that encoding the policy that says, "all images must come from the repository hooli.com" is just a few lines in OPA.

# deny any pod with an image not from the repository hooli.com
deny {
image_name := input.spec.containers[_].image
not startswith(image_name, "hooli.com")
}

The logic shown above denies the API call if there is ANY container in the pod whose image fails to start with hooli.com. If you want to understand how the code works, the following notes should help:

  • input is an OPA keyword that stores the JSON/YAML document representing the Kubernetes YAML shown earlier.
  • The dot-notation (e.g. input.spec.containers) does the obvious thing — descending through the YAML hierarchy.
  • The underscore (_) iterates over all the containers. If the body of the deny rule is true for ANY of the containers in the array, the pod is rejected. Note: iteration is not limited to only _ — see the docs for details.
  • startswith is one of 50+ builtins for string, numeric, IP, etc. manipulation.

OPA's Context-aware Kubernetes Policies

That image-repository example is actually one of the simpler access control policies you might need to write for Kubernetes because you can make the decision using just the one YAML file describing the pod. But sometimes you need to know what other resources exist in the cluster to make an allow/deny decision. For example, it's possible to accidentally create two applications serving internet traffic using Kubernetes ingresses where one application steals traffic from the other. The policy that prevents that needs to compare a new ingress that's being created/updated with all of the existing ingresses. That leads to another requirement for a Kubernetes access control system that OPA supports:

  • Conditioning decisions based on external information about the world.

To see the OPA policy that prohibits conflicting ingresses, here is an example ingress YAML.

kind: Ingress
metadata:
name: test-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- http:
paths:
- path: /testpath
backend:
serviceName: test
servicePort: 80

Below is the (essence of the) OPA policy that stops an ingress from being created/updated if there is an existing ingress that it would conflict with.

deny {
input.kind == "Ingress"
host := input.request.object.spec.rules[_].host
host == data.kubernetes.ingresses[_][_].spec.rules[_].host
}

The only new part of this policy is the reference to data.kubernetes. Below are some notes explaining that code.

  • data is a keyword in OPA (similar to input) that stores all of the information about the external world.
  • The reference data.kubernetes.ingresses is a dictionary mapping each namespace into the array of ingresses in that namespace.
  • data.kubernetes.ingresses[_][_] iterates over all ingresses over all namespaces.
  • The last line checks if the host for the new/updated ingress is the same as the host on any of the rules over any of the ingresses in any namespace. (3 "any"s means you need 3 underscores.)

How you load information about the external world into OPA varies depending on the use case and the kind of data. Typically the information loaded into OPA is eventually-consistent, meaning it's a copy of the data and could be out of date — whether that matters depends entirely on the use case and can be mitigated by using OPA's offline auditing capabilities. To load Kubernetes data, OPA has a sidecar that watches the API server to replicate Kubernetes resources into OPA.

In this post, we've given 2 simple and common examples of policies using the core Kubernetes objects (pods and ingresses), but there's nothing special about those resources. If you're using Custom Resource Definitions, you can still go ahead and write whatever policies you need, e.g. knative or istio. Any resource managed by Kubernetes is something you can write policy over with OPA — as far as OPA is concerned they're all just YAML/JSON.

Summary

In this post, we dug into the API security challenges faced by Kubernetes and how OPA addresses those challenges.

  • Kubernetes's API is YAML-centric, meaning that the arguments to API calls are (at least conceptually) arbitrary chunks of YAML.
  • A YAML-centric API is challenging for access control because it requires analyzing that YAML to make a decision. For example, the policy "ensure all images come from a trusted repository" requires navigating the YAML to find the list of all containers, iterating over that list, extracting the image name, and string-parsing that image name to extract the repository.
  • The Open Policy Agent's declarative policy language was designed to express policy over arbitrary JSON/YAML, so it includes implicit iteration, dot-notation, and 50+ builtins.
  • OPA also supports context-aware policies that let you analyze both the resource that a user is trying to create/update and all of the other Kubernetes resources that already exist. It's all just JSON/YAML to OPA. For example, the policy "prohibit ingresses with conflicting hostnames" requires comparing any new ingress that is being created to all the existing ingresses.

OPA's Full Stack Policy Language

Banner image for OPA'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.