During a recent audit of a Tier-1 Indian FinTech's Kubernetes environment, we discovered that 40% of their IAM policies were manually applied via the AWS console, leading to severe configuration drift. This manual approach to secure SSH access for teams and cloud resources fails at scale, especially when trying to comply with the granular access controls mandated by the RBI's Master Direction on Outsourcing of IT Services. We replaced their manual checks with Open Policy Agent (OPA) and Rego, moving the organization toward a verifiable Policy-as-Code (PaC) framework.
What is Rego Policy Language?
Rego is a declarative query language inspired by Datalog, specifically designed to express complex policies over hierarchical data. Unlike imperative languages like Python or Java, Rego allows you to focus on the "what" rather than the "how." We use it to answer a single question: "Is this action allowed given this context?"
The language is optimized for JSON and YAML data structures. When OPA receives a request, it evaluates the Rego policy against the provided input and returns a decision. Because Rego is purpose-built for policy, it handles deep nested structures and collection iterations more efficiently than general-purpose languages.
The Role of OPA in Policy as Code
Open Policy Agent (OPA) acts as the execution engine for Rego. We typically deploy OPA as a sidecar in Kubernetes or as a standalone service in a microservices architecture. It decouples policy logic from the application code. This means your developers don't need to write hardcoded if-else blocks for authorization; they simply query OPA.
In the Indian context, this decoupling is vital for compliance with the Digital Personal Data Protection (DPDP) Act 2023. By centralizing policy, we can ensure that PII (Personally Identifiable Information) access rules are applied consistently across all services, from legacy banking cores to modern mobile APIs.
Key Benefits of Using Rego
- Declarative Syntax: Policies are easier to read and audit compared to thousands of lines of shell scripts or Java code.
- Platform Agnostic: The same Rego logic can secure Kubernetes, Terraform, Envoy, or custom internal APIs.
- Performance: OPA's compiler optimizes rules by building an index of constants, allowing for sub-millisecond evaluation even with large datasets.
- Testability: Rego includes a built-in testing framework, allowing us to treat policy changes like software changes with CI/CD integration.
Understanding Packages and Imports
Every Rego file begins with a package declaration. This defines the namespace for the rules within the file. We use hierarchical naming conventions to organize policies by team or function. For example, package kubernetes.admission or package aws.s3.encryption.
Imports allow us to reference rules from other packages, promoting code reuse. We found that importing common utility functions (like CIDR range checks or string manipulation) significantly reduces the complexity of individual policy files.
package authz.banking.transfer
import data.utils.fintech.is_valid_ifsc import input.request.amount
Rule logic follows
Variables and Assignment in Rego
Variables in Rego are immutable within the scope of a single rule evaluation. We use the := operator for assignment. It is important to distinguish between assignment and comparison. Rego uses = for unification (checking if two values can be the same) and == for strict equality.
We often assign intermediate values to variables to improve readability. For instance, extracting a user's role from a JWT (JSON Web Token) before performing an authorization check ensures the core logic remains clean.
Defining Rules: Complete vs. Incremental Rules
Complete rules assign a single value to a variable. These are most common for Boolean "allow/deny" decisions. If the conditions inside the rule body are met, the rule evaluates to the assigned value (or true by default).
A complete rule
is_admin := true { input.user.role == "cluster-admin" }
The Importance of the 'default' Keyword
Rego rules are undefined if no conditions match. This is dangerous in a security context. If a policy is undefined, OPA returns an empty result, which an application might interpret as "allow" if not handled correctly. We always define a default value for Boolean rules.
package security.access
default allow = false
allow { input.user.is_authenticated input.action == "read" }
This ensures that the policy follows the principle of least privilege. If the specific allow conditions aren't met, the decision defaults to false.
The 'input' Document: How OPA Receives Context
The input global variable is the most important reserved word in Rego. It represents the data sent to OPA by the PEP (Policy Enforcement Point). This could be a Kubernetes AdmissionReview object, a Terraform plan JSON, or an HTTP request body.
We observed that many teams struggle because they don't inspect the input structure before writing policies. We recommend using opa eval to print the input and understand the hierarchy.
Inspecting the input structure
$ opa eval -i input.json 'input'
Handling External Data (JSON and YAML)
Policies often require external context not present in the input, such as a list of authorized IP ranges or employee IDs from a database. OPA allows us to load this as data. We can provide this data via local files or by pushing it to OPA's REST API.
For Indian FinTechs, this is useful for maintaining a dynamic list of "Restricted Entities" as defined by SEBI. We can load a JSON file containing these entities and check the input against it.
deny { input.transaction.receiver_id == data.restricted_entities[_] }
Virtual Documents vs. Base Documents
In OPA terminology, "Base Documents" are the raw data files loaded into OPA (like the restricted entities list above). "Virtual Documents" are the results of Rego rules. When we query data.authz.allow, we are querying a virtual document.
We treat virtual documents as an abstraction layer. By querying a virtual document rather than a raw rule, we can change the underlying logic without changing the API call made by the application.
Logical AND/OR Operations in Policy Logic
In Rego, logical AND is implicit. All expressions within a single rule body must be true for the rule to evaluate. For example, to allow access only if a user is an admin AND the request comes from a specific VPC, we write:
allow { input.user.role == "admin" input.source_vpc == "vpc-0a1b2c3d" }
Logical OR is achieved by defining multiple rules with the same name. If any one of the rules evaluates to true, the overall rule is true. This is much cleaner than nested if-else statements.
Logical OR: User is admin OR user is the owner
allow { input.user.role == "admin" }
allow { input.user.id == input.resource.owner_id }
Comparison and Arithmetic Operators
Rego supports standard operators: ==, !=, <, >, <=, >=. It also supports basic arithmetic for quota management. We use these to enforce spending limits or resource constraints.
For instance, enforcing that no single transaction exceeds ₹50,000 without multi-factor authentication:
requires_mfa { input.transaction.amount > 50000 input.auth_method != "mfa" }
Using Built-in Functions for String and Math Manipulation
Rego provides over 150 built-in functions. The most common ones we use include re_match for regex, net.cidr_contains for network security, and units.parse_bytes for resource limits.
When dealing with Indian infrastructure, we often use re_match to ensure that data resides in the ap-south-1 region.
is_in_india { re_match("ap-south-1", input.region) }
Iteration and Loops: Using 'some' and 'every'
Rego does not use traditional for or while loops. Instead, it uses iteration over collections. The _ (underscore) character acts as a wildcard to iterate over all elements in an array or object.
The some keyword is used to declare local variables during iteration, which is essential for complex logic where you need to reference the same element multiple times.
Check if any container in a Pod is running as root
deny { some i container := input.request.object.spec.containers[i] container.securityContext.runAsRoot == true }
Handling Negation with the 'not' Keyword
Negation in Rego is "Negation as Failure." not allow is true if allow cannot be proven true. This is a subtle but critical distinction. We use not to enforce mandatory configurations.
deny[msg] { input.request.kind.kind == "Pod" container := input.request.object.spec.containers[_] # Ensure images only come from internal Indian mirror or trusted registry not re_match("^(709825985650.dkr.ecr.ap-south-1.amazonaws.com|mycorp.in)/", container.image) msg := sprintf("Image '%v' comes from an untrusted registry. Use ap-south-1 ECR or internal .in repo.", [container.image]) }
Writing Functions in Rego
Custom functions help reduce code duplication. We define them using the name(args) = result syntax. Functions are particularly useful for complex calculations or repeating logic like validating a PAN (Permanent Account Number) format.
is_valid_pan(pan) { re_match("^[A-Z]{5}[0-9]{4}[A-Z]{1}$", pan) }
deny["Invalid PAN format"] { input.user.pan_number not is_valid_pan(input.user.pan_number) }
Writing Unit Tests with 'opa test'
We never deploy policies without unit tests. The opa test command allows us to verify policy behavior against mock inputs. Test files should reside in the same directory as policies and carry a _test.rego suffix.
policy_test.rego
package authz
test_allow_as_admin { allow with input as {"user": {"role": "admin"}} }
test_deny_as_guest { not allow with input as {"user": {"role": "guest"}} }
Debugging Policies using 'opa eval'
When a policy doesn't behave as expected, we use opa eval to inspect intermediate values. The --explain flag is particularly useful as it shows the full trace of the evaluation process.
$ opa eval --data policy.rego --input input.json "data.authz.allow" --explain full
Utilizing the Rego Playground for Rapid Prototyping
The Rego Playground (play.openpolicyagent.org) is an essential tool for drafting policies. It provides a real-time evaluation environment where you can paste your Rego code, provide a JSON input, and see the output immediately. We use it for collaborative debugging sessions before committing code to Git.
Optimizing Rule Evaluation Speed
While OPA is fast, inefficient Rego can slow down high-traffic APIs. We follow these optimization rules:
- Use Constant Prefixes: OPA's trie-based index works best when rules start with equality checks on constant strings (e.g.,
input.type == "transaction"). - Minimize Iteration: Avoid nested loops where possible. Use sets for O(1) lookups instead of searching through arrays.
- Early Exit: Place the most restrictive or cheapest conditions at the top of the rule body.
Organizing Large Policy Sets for Maintainability
As policy sets grow to hundreds of files, organization is key. We recommend a directory structure based on the target platform:
policies/ ├── kubernetes/ │ ├── admission/ │ └── network/ ├── aws/ │ ├── iam/ │ └── s3/ └── common/ └── utils.rego
Common Rego Pitfalls and How to Avoid Them
One of the most dangerous patterns is the 'Default Allow' Misconfiguration. If you omit default allow = false and your rules fail to match due to a typo in the input JSON, OPA returns an undefined result. If your application logic defaults to "allow" on empty responses, you have a security hole.
Another risk is CVE-2021-23441, a prototype pollution vulnerability in OPA-JS. If you are using the JavaScript implementation of OPA, ensure you are sanitizing input objects before evaluation to prevent attackers from bypassing policy checks.
For OPA-Envoy-Plugin users, be aware of CVE-2023-47108. This resource exhaustion vulnerability allows unauthenticated attackers to trigger high memory consumption via specific HTTP requests. Always keep your OPA binaries updated to the latest stable version.
Rego in the Indian Regulatory Landscape
The DPDP Act 2023 mandates that "Data Fiduciaries" implement technical measures to protect personal data. Rego provides a verifiable way to enforce these measures. For example, we can write a policy that ensures PII is only accessible to employees who have completed their quarterly compliance training, by joining input request data with data from an HR system.
Furthermore, for RBI compliance, we use Rego to enforce "Geo-fencing" at the policy level. If a request originates from an IP outside of India for a high-value corporate banking transaction, OPA can automatically trigger a block or mandate step-up authentication.
deny[msg] { input.request.kind.kind == "Pod" input.request.object.spec.containers[_].securityContext.privileged msg := "Privileged containers are strictly prohibited under internal security guidelines." }
Next Steps for Learning OPA
To move beyond basic tutorials, we recommend implementing OPA in a local Kubernetes cluster using Minikube. Start by writing policies for Gatekeeper or Kyverno (which uses a similar logic) to restrict container images to your private ECR repository in ap-south-1.
For CI/CD integration, explore conftest, a utility that allows you to run Rego policies against configuration files like Terraform or Dockerfiles during the build process.
This shift-left approach ensures that security violations are caught in the IDE or the pipeline, long before they reach production servers in Mumbai or Hyderabad, which are best managed via a browser based SSH client.
$ opa run --server --addr :8181 --diagnostic-addr :8182
