Policy as Code is a practice in which rules and conditions are defined through code in the same way as application code, including their definition, management, and enforcement.
This approach allows developers to manage and enforce policies using the same tools and processes that are used to manage and deploy software.
In this article, we’ll explore what Policy as Code is, its benefits, and how it can be applied across different domains. We'll discuss the differences between infrastructure-level and application-level authorization, as well as validation vs. authorization.
Additionally, we’ll dive into the policy engines and languages and highlight the tools that make managing policies easier.
Let’s get started -
What is Policy as Code?
Policy as Code is the practice of defining, managing, and enforcing policies using code rather than relying on static configurations or manual enforcement through tools like spreadsheets or ad-hoc scripts.
package example
import input
default allow = false
allow {
input.user.role == "admin"
input.resource.category == "sensitive"
}
An access control policy written in the Rego language
Policy as code is based on domain-specific policy languages, which are designed to express and enforce rules in specific domains. Two prominent examples of such languages are Rego, used with Open Policy Agent (OPA), and AWS’ Cedar. Additionally, OSO’s Polar and traditional systems like XACML/ALFA continue to play significant roles in this area.
Some languages, such as Cerbos, leverage widely used formats like JSON, YAML, and CEL for policy representation, offering accessible options for developers. Another category includes policy-as-data engines like OpenFGA and SpiceDB, which provide data-driven approaches to policy management and enable policy-as-code configuration for these use cases.
Finally, tools like Topaz build on OPA to support advanced scenarios like Zanzibar-style authorization. These options reflect a diverse landscape of tools and languages, each catering to different needs and preferences depending on the requirements of your project.
The Benefits of Policy as Code
There are many benefits to having policy as code, including improved consistency, accuracy, and traceability. Here’s a more in-depth overview of these benefits:
Consistency Across Systems
Policy as Code ensures that policies are consistently enforced across all environments, reducing the risk of human error or inconsistencies that may arise when managing policies manually. Defining policies in code allows you to ensure uniformity, whether the policy applies to infrastructure, applications, or access control.
Automation and Scalability
Policies can be automatically tested, deployed, and enforced in a scalable way. As systems grow, managing policies manually becomes increasingly difficult and error-prone. Policy as Code automates this process, making it easier to scale policies across multiple platforms without introducing additional complexity.
Traceability and Version Control
Storing policies in version-controlled repositories like Git allows for full traceability. Policy changes can be tracked, reviewed, and rolled back if needed. This helps ensure compliance and allows for auditing as you maintain a detailed history of every policy change.
Faster Policy Updates
Instead of waiting for manual changes or relying on disparate systems, updates can be made directly in the codebase, tested in CI/CD pipelines, and deployed automatically.
Collaboration and Transparency
Policy as Code promotes collaboration across teams by making policies more accessible and transparent. Developers, security teams, and, if provided with the necessary tools, non-technical stakeholders can all contribute to policy creation, testing, and reviews.
Policy as Code: Common Use Cases
Policy as code has a wide range of applications, but two distinct areas in which it is most commonly used are validation and authorization. These concepts often overlap in practice, but they address fundamentally different challenges.
Validation vs. Authorization
Validation:
Validation ensures that actions, inputs, or configurations meet a predefined set of rules. In infrastructure, validation might involve checking whether a cloud resource is configured with the correct permissions, validating the configuration of Kubernetes clusters, or enforcing encryption standards for cloud storage.
Authorization:
Authorization, on the other hand, determines whether a user or entity has permission to perform a specific action. Unlike validation, which checks if something is allowed, authorization checks whether the requesting user has the right to proceed with the action.
The difference between the two can be made clearer through a practical example:
Imagine you have a file on Google Drive with tens of thousands of users with access permissions. If you’re validating, you might check whether the file metadata meets specific rules. If you’re authorizing, check whether a specific user can access or modify that file.
This dual capability—validation and authorization—is one of the reasons why policy as code has become so important in modern application and infrastructure management.
Implementing Policy As Code: Policy Engines and Languages
Policy as Code allows us to express and enforce rules across diverse domains through the use of Policy Engines and Policy Languages, each possessing its strengths, weaknesses, and ideal use cases. Policy engines evaluate policies written in specific languages and apply rules across infrastructure and applications.
Let’s look at three widely used policy engines and their associated languages: Open Policy Agent (OPA), AWS’ Cedar, and OpenFGA’s implementation of Google Zanzibar.
Open Policy Agent (OPA) with Rego
Open Policy Agent (OPA) is a versatile policy engine that enables Policy as Code in a wide variety of environments, from cloud infrastructure to Kubernetes and application-level authorization. OPA allows you to define policies in the Rego language, a declarative language designed specifically for policy definition.
OPA is not limited to a specific use case, making it suitable for multiple domains such as Kubernetes resource management, cloud governance, and even application-level access control.
AWS’ Cedar
Cedar is a policy language and engine developed by AWS for Identity and Access Management (IAM). It is used to define and manage permissions and policies at scale for cloud applications. Cedar focuses on allowing fine-grained, deterministic policies, particularly when managing access control for large cloud environments.
Tools for Managing Policy as Code
Integrations, SDKs, and libraries surrounding policy engines are key to solving the challenges of policy as code. The more tools, libraries, or SDKs a platform can offer, the better, as those can help you apply it effectively across your use cases.
Tools like OPAL, Cloud Custodian, and Permit.io are prime examples of how integrations can simplify and enhance the policy-as-code experience:
OPA Gatekeeper
OPA Gatekeeper is an extension to Open Policy Agent (OPA) designed to enforce policies in Kubernetes environments. It integrates directly with Kubernetes’ admission control, ensuring that only compliant resources are allowed to be deployed within the cluster. Gatekeeper helps teams apply and enforce policies for resource configurations, security standards, and more, in Kubernetes environments.
Open Policy Administration Layer (OPAL)
OPAL is an open-source policy synchronization and enforcement tool that acts as a wrapper around OPA. It allows for the dynamic synchronization of policies between systems, enabling automatic enforcement of policies in real time. OPAL integrates with OPA (as well as Cedar and OpenFGA) to enhance its ability to scale in larger environments, handling real-time data synchronization and policy enforcement across multiple systems.
Permit.io
One significant hurdle is the learning curve associated with certain policy languages.
While policy-as-code should be managed in a code repository, that doesn’t mean it should be authored as pure code.
By simplifying policy creation, we can make our work as developers easier, free ourselves from becoming bottlenecks, and empower other critical stakeholders (e.g., product managers, security, compliance, support, professional services, and sales) to participate in the policy creation process.
Permit.io simplifies the creation and management of policies for access control in applications through a low-code/no-code interface. These interfaces generate policy code right into a Git repository, allowing developers to test, benchmark, and even code review the generated code while maintaining the ability to add more code on their own for the more advanced cases.
Permit.io supports several policy engines and languages, such as OPA, Cedar, and OpenFGA, offering flexibility to developers and teams. While OPA plays a significant role in its ecosystem, Permit.io goes beyond generating Rego code by integrating with other engines and languages, embracing a polyglot approach to policy management.
Using low-code policy interfaces also reduces dependence on a single policy engine and allows for embracing a more polyglot dynamic approach to software.
Policy as Code vs. Policy As Data
When discussing policy management, a key distinction arises between Policy as Code and Policy as Data, each offering distinct approaches to defining and enforcing rules.
Graph-Based Authorization | Policy-as-Code Authorization | |
---|---|---|
Nature of Access Control | Natural fit for Relationship-based Access Control (ReBAC) | Excels at managing complex policies (e.g., ABAC) |
Representation of Relationships | Excellent for representing hierarchies and nested relationships | Flexible and can manage complex relationships, but not inherently hierarchical |
Data Volume Management | Manages high volumes of data consistently | Can struggle with large amounts of data without sharding |
Reverse Indices | Supports reverse indices | Does not support reverse indices |
Performance | Lower performance compared to policy-as-code | Generally high performance |
Deployment at Edge | Practically impossible due to size | Feasible and efficient |
Latency | Higher due to non-locality | Lower due to local deployment |
Ease of Updates | Less flexible for updates | Highly flexible and easy to update |
Ecosystem | Emerging ecosystem | Robust ecosystem with plugins and multiple engines |
Learning Curve | Moderate | Can be high due to complex languages |
Policy as Code (OPA and Similar Engines)
In contrast, Policy as Code systems operate as stateless decision engines. These engines rely on external data sources—such as application databases—to provide the context needed for access checks. Policies are written in domain-specific languages allowing developers to define and enforce detailed, logic-based rules.
This approach excels in flexibility and extensibility, making it suitable for complex scenarios requiring ABAC or policies dependent on dynamic data. Moreover, the extensive ecosystem surrounding Policy as Code includes robust tools, integrations, and CI/CD compatibility, simplifying testing and deployment.
The challenge, however, lies in its distributed nature. Data required for authorization must be queried from external sources and aggregated before a decision can be made, introducing operational complexity. For developers seeking simplicity, this piecemeal approach may feel cumbersome. By weighing these strengths and limitations, you can choose the approach—or combination—that best meets your application's unique requirements.
At the end of the day, picking the correct language and tools will depend on the specific needs and requirements of what you’re building. You can read an in-depth comparison of all three policy engines and languages in: “Policy Engines: Open Policy Agent vs. AWS Cedar v.s Google Zanzibar”.
Policy as Data (Google Zanzibar)
In the Policy as Data model, as exemplified by Google Zanzibar, all the information needed for authorization decisions—such as namespaces, relationship tuples, and access control rules—is stored in a centralized system. This data-driven approach allows the decision engine to resolve access checks by querying a unified, horizontally scalable datastore.
The primary strength of this approach is how particularly well it’s suited for Relationship-Based Access Control (ReBAC) scenarios, where hierarchical or nested relationships between users and resources are common. This is thanks to its centralized and unified structure, which simplifies management for applications requiring consistent, high-volume access control. It’s particularly well-suited for Relationship-Based Access Control (ReBAC) scenarios, where hierarchical or nested relationships between users and resources are common.
OpenFGA is a good example of an open-source implementation of Google’s Zanzibar system, designed for managing complex access control at scale. Zanzibar is a globally distributed system for managing access control and permissions for large-scale applications.
It uses a graph-based model to manage relationships between users and resources, making it ideal for applications with complex relationships, such as social networks, document-sharing systems, and collaborative platforms.
Zanzibar’s simplicity comes with trade-offs. Its namespaces offer a minimalistic policy layer, which is often inadequate for complex policy requirements like Attribute-Based Access Control (ABAC). Additionally, Zanzibar struggles with policies that depend on dynamic external data (e.g., geolocation, time of access), requiring supplementary systems to address such needs.
Combining Policy as Code and Policy as Data with Permit.io
Permit.io bridges the gap between Policy as Code and Policy as Data, enabling developers to leverage the strengths of both approaches. By supporting multiple policy engines, including OPA for logic-driven policies and Google Zanzibar for relationship-based access control, Permit.io allows teams to integrate code-driven flexibility with data-driven centralization.
With its low-code/no-code interfaces, Permit.io simplifies the creation and management of complex policies, empowering developers and non-technical stakeholders alike. The platform's ability to centralize policy management while maintaining compatibility with dynamic, distributed data sources enables applications to handle intricate scenarios, such as hybrid ABAC-ReBAC models, without sacrificing ease of use or scalability.
The Power and Potential of Policy as Code
Incorporating Policy as Code into infrastructure and application management is a great best practice that allows for greater security, compliance, and operational efficiency.
Defining, managing, and enforcing policies through code allows developers to automate policy enforcement, ensure consistency across environments, and gain greater control over access and configurations.
By exploring various policy engines and languages, including OPA, AWS' Cedar, and OpenFGA, we've seen how different tools serve distinct use cases, from managing infrastructure to handling complex application authorization.
The flexibility of Policy as Code allows developers to select the right policy language and tools for their needs, whether it’s Kubernetes management, IAM in cloud environments, or user access control in modern applications.
Tools like OPA Gatekeeper, OPAL, and Permit.io enhance the experience by simplifying policy management, enabling seamless integration, and offering real-time policy enforcement across distributed systems.
Permit.io provides an intuitive low-code/no-code approach, allowing non-developers to collaborate in policy creation and helping teams scale their authorization processes without needing deep technical expertise.
By leveraging the right tools and embracing the flexibility of code-driven policies, developers can ensure their policies are enforced efficiently and consistently across every system.
Want to learn more about authorization best practices? Join our Slack community, where hundreds of developers are building and discussing authorization.
Written by
Or Weis
Co-Founder / CEO at Permit.io