June 4, 2024 10 min read

Authorization With Open Policy Agent

Mark Phelps

One of the most requested features for Flipt has been the ability to secure the API and UI with authorization. We are excited to announce that as of v1.43.0, Flipt now supports authorization using Open Policy Agent!

This feature allows you to define fine-grained access control policies for your Flipt instance. With Open Policy Agent (OPA), you can define rules that determine who can access what resources and under what conditions.

In this post, I'll discuss why we chose OPA, how we went about implementing it, how to configure it with Flipt, and provide some examples of how you can use it to secure your Flipt instances. Finally, I'll discuss some of the future plans we have for this feature.

Authorization vs Authentication

Who is your daddy and what does he do?
Arnold gets it.

Before we dive into the details of how to configure OPA with Flipt, let's first clarify the difference between authorization and authentication.

Authentication is the process of verifying the identity of a user or system. It answers the question "Who are you?" and is typically done using a username and password, API key, or some other form of credentials.

In Flipt, we support several authentication methods, including token-based authentication, GitHub OAuth, OpenID Connect, JWT, and even Kubernetes service account tokens. These methods allow you to secure your Flipt instance and ensure that only authenticated users can access it.

Authorization, on the other hand, is the process of determining what a user or system is allowed to do. It answers the question "Can you perform this action?" and is typically done using access control policies.

Authorization is especially important in large organizations where different teams or departments may have different levels of access to resources. For example, one common use case we hear from our users is that they want to restrict access to certain namespaces depending on the user's role or team membership.

This is where Open Policy Agent comes in.

Why Open Policy Agent?

When we set out to add support for authorization in Flipt, we evaluated several different options, including building our own custom solution. However, we quickly realized that most options didn't fulfill all of our requirements, and building a custom solution would be time-consuming and error-prone.

One of the reasons it has taken us so long to support authorization in Flipt is because of the sheer number of options available, and the importance of getting it right the first time. We wanted to make sure we chose the right solution that would be easy enough to use and configure, and was flexible, scalable, and of course secure.

Other than the above mentioned requirements, an additional goal was to make sure that whatever solution we chose would not force any external dependencies on our users. We wanted to make sure that our users could easily integrate Flipt with their existing infrastructure and tools without having to make any major changes such as requiring them to run a separate authorization service.

While OPA can run as a seperate service/sidecar, it can also be embedded directly into a Go application as a library, which is what we chose to do with Flipt. This means that you can run Flipt with OPA without having to run a separate OPA server, which simplifies the deployment and configuration process. It also means that developers can test their code integrated with Flipt and authorization policies locally.

Finally, we chose OPA because it is a mature, battle-tested solution that is widely used in the industry. It has a large and active community, which means that there are plenty of resources available to help you get started and troubleshoot any issues you may encounter. OPA integrates well into the Cloud Native ecosystem and has integrations with many other projects, such as Kubernetes, Istio, and Envoy.

In short, OPA checked all of our boxes, and shared many of the same values that we hold dear at Flipt, such as being:

  • Easy to use and configure
  • Flexible and extensible
  • Scalable and secure
  • No external dependencies by default
  • Cloud Native
  • Support declarative workflows / configuration as code

Implementing OPA in Flipt

Implementing OPA in Flipt was a relatively straightforward process. As mentioned earlier, we chose to embed OPA directly into the Flipt codebase, leveraging the OPA Go library.

While the pull request that added this feature was quite large, the core functionality can be broken down into a few key components:

  • Loading and parsing authorization policies and external data
  • Creating the evaluation 'engine'
  • Mapping requests to 'namespaces', 'resources', 'verbs', and 'subjects'
  • Mapping authenticated request data to 'claims'
  • Enforcing the authorization policies using middleware

The core of the implementation is a new GRPC middleware that was added to the Flipt API server. This middleware intercepts incoming requests, extracts the relevant data from the request and the authenticated user (or system), and then evaluates the authorization policies using OPA. Since we are also using GRPC Gateway, we get the added benefit of being able to secure both the REST API and the UI using the same middleware.

The majority of the functionality required to load and parse authorization policies and data were integrated using OPA's pluggable storage model. This allows us to easily add support for additional policy backends in the future.

We also tied together our existing auditing functionality with the authorization middleware by ensuring that all request types specify metadata such as the request namespace, resource, and verb. This ensures that the authorization policies have all the information they need to make access control decisions and that we can log all events for auditing purposes.

Configuring OPA with Flipt

Again, we wanted to make it as easy as possible for our users to configure authorization, so we added a new section to the Flipt configuration that allows you to specify the location of your authorization policies and external data.

First you need to define your authorization policies in a separate file. Here's an example of a simple policy that ensures all authenticated users can only read flags in the default namespace:

package flipt.authz.v1

import rego.v1

default allow := false

allow if {
    input.request.namespace == "default"
    input.request.resource == "flag"
    input.request.verb == "read"
}

Then, you need to tell Flipt where to find your policy file. You can do this via our configuration:

authorization:
  required: true
  policy:
    backend: local
    local:
      path: "policies/authz.rego"

# required while authorization is still experimental
experimental:
  authorization:
    enabled: true

Currently we only support local policy files, but we plan to add support for other backends in the future, such as Git repositories, Cloud Storage, and more.

Policies

OPA policies are written in a language called Rego, which is a declarative language that is designed to be easy to read and write. Policies are evaluated based on the input data that is provided to them, and Flipt expects these policies to return a boolean value that determines whether the request is allowed or denied.

Policies can be as simple or as complex as you need them to be. You can define rules based on any combination of input data, such as the user's role, team membership, IP address, or any other attribute that you want to use to make access control decisions.

When integrating OPA into Flipt we made sure to provide a rich set of input data that you can use in your policies. This includes information about the authentication, the request being made (e.g. the resource being accessed, the verb being used), and any other context that you may need.

For full details on our input data schema, check out our Authorization Policies documentation.

The authentication.metadata.claims field is particularly useful when integrating with OAuth or OpenID Connect providers, as it allows you to access any custom claims that are included in the authentication token. This can be useful for making access control decisions based on role or team membership. Many identity providers include standard claims like sub (subject), email, name, and groups, but can also include custom claims like role or team if you configure them to do so.

In fact, we created a guide on how to configure Flipt, OPA, and Keycloak to work together to provide a complete end-to-end solution for securing your Flipt instance with Role Based Access Control.

For help writing and debugging policies, OPA provides a Rego Playground where you can write and test your policies in a web-based environment. This can be a great way to experiment with different policies and see how they behave before deploying them to your Flipt instance.

There is also a linter available for Rego called Regal that can help you catch common mistakes and enforce best practices when writing policies.

Using External Data

In addition to the input data that is provided to the policy engine, you can also use external data sources in your policies. This allows you to make access control decisions based on data that is stored outside of Flipt or your identity provider(s).

For example, you could use an external data source to store a list of users who are allowed to access certain resources, or a list of IP addresses that are allowed to access your API. You can then query this external data source from your policy to make access control decisions.

Just as with policies, Flipt supports loading external data from a local file. Here's an example of how you can configure Flipt to load external data from a local file:

{
  "admins": ["mark@flipt.io", "george@flipt.io"]
}
authorization:
  required: true
  policy:
    backend: local
    local:
      path: "policies/authz.rego"
  data:
    backend: local
    local:
      path: "policies/data.json"

The accompanying policy could then use this data to make access control decisions:

package flipt.authz.v1

import data

default allow := false

allow if {
    input.request.namespace == "default"
    input.request.resource == "flag"
    input.request.verb == "delete"
    input.authentication.metadata["io.flipt.auth.email"] in data.admins
}

External data is particularly useful if you need to make access control decisions based on data that changes occasionally, such as a list of employees, but do not need a full fledged identity provider. By storing this data in an external file, you can update it without having to modify your policies.

Flipt supports the ability to refresh external data (and policies) on a regular interval, so you can keep your authorization model up to date without having to restart your Flipt instance.

What's Next?

We are excited to see how our users will use this new functionality to secure their Flipt instances, and we're looking forward to hearing your feedback on how we can improve it.

We believe that you shouldn't have to pay extra for common security features, which is why we are adding this feature to Flipt Open Source.

We know that security is important to our users, and we want to make it as easy as possible for you to secure your Flipt instances.

As mentioned earlier, this feature is still experimental and we plan to make several improvements in future releases. Some of the smaller features we are planning include:

  • Support for additional policy backends, such as Git repositories, Cloud Storage, and more
  • Support for additional input data source backends

On top of that, we're actively working on building Role Based Access Control policy configuration and management directly into Flipt Hybrid Cloud, so you don't have to write policies to secure Flipt. More on this in a future post.

If you're interested in being an early adopter of this feature, please sign up for our Flipt Hybrid Cloud: Early Access Program. We're looking for users who are interested in securing their Flipt instances with managed Authentication and Role Based Access Control, and we'd love to hear from you.

Scarf