The Shift from Imperative IAM to Declarative Policy as Code
In our recent audit of a multi-region AWS environment for a Bengaluru-based FinTech, we identified 42 instances of manual IAM overrides that bypassed standard security groups. These "quick fixes" by DevOps teams often occur when they lack secure SSH access for teams, creating a compliance nightmare under the Digital Personal Data Protection (DPDP) Act 2023. Traditional IAM policies and AWS Service Control Policies (SCPs) are powerful but often lack the granularity required for complex, logic-heavy authorization. This is where Open Policy Agent (OPA) and its language, Rego, become critical. We moved their governance from manual checks to automated Rego-based validation, reducing policy drift by 85%.
Rego is not just another configuration language; it is a declarative query language inspired by Datalog. When we write Rego, we are not telling the system how to check a policy, but what the policy state should be. This distinction is vital when scaling cloud infrastructure. By decoupling policy decision-making from policy enforcement, we allow microservices, CI/CD pipelines, and Kubernetes clusters to query a centralized "source of truth" for every request.
What is Rego? An Overview of Open Policy Agent (OPA)
OPA is a graduated CNCF project that provides a unified toolset and framework for policy across the cloud-native stack. At its core is the OPA engine, which accepts an arbitrary JSON input, matches it against a Rego policy file, and returns a JSON response. This "JSON-in, JSON-out" architecture makes OPA platform-agnostic. We have successfully integrated OPA into Envoy proxies for service mesh security and into Terraform pipelines for infrastructure-as-code (IaC) linting.
Rego's power lies in its ability to handle deeply nested JSON structures common in cloud metadata. Unlike Python or Go, where you would need multiple loops and null checks to find a specific tag in an AWS resource, Rego allows you to traverse collections using a single line of logic. This reduces the surface area for bugs in security logic, which is often where vulnerabilities originate.
The Importance of Policy as Code in Modern Infrastructure
The DPDP Act 2023 mandates strict data localization and access control for Indian citizens' PII. Relying on manual documentation or "tribal knowledge" within a security team is a liability. Policy as Code (PaC) allows us to treat security requirements like software. We version control them in Git, run unit tests against them, and deploy them through automated pipelines. This process is essential to harden CI/CD pipelines against unauthorized triggers and ensures that every change to our cloud governance is audited and peer-reviewed.
- Version Control: Every policy change is tracked in Git, providing a clear audit trail for CERT-In or regulatory auditors.
- Consistency: The same Rego policy can be applied to a developer's local machine, a staging environment, and production.
- Automation: Policies are enforced at the point of creation (e.g., a Terraform plan) rather than after a resource is already live and vulnerable.
Common Use Cases for Rego Policy Examples
We typically see Rego deployed in three major stages of the software development lifecycle (SDLC). First is the Design/Build phase, where OPA validates Terraform plans or CloudFormation templates before they are applied. Second is the Deployment phase, where Kubernetes Admission Controllers use Rego to intercept kubectl requests. Third is the Runtime phase, where microservices offload authorization logic to an OPA sidecar.
A common Rego policy example in the Indian context involves enforcing "Region Locking." If a developer attempts to spin up a database in us-east-1 that is intended to hold PII of Indian users, the Rego policy will fail the build, citing DPDP compliance requirements. This prevents the data from ever leaving the ap-south-1 (Mumbai) or ap-south-2 (Hyderabad) regions.
Basic Syntax: Writing Your First Rego Rule
Every Rego file starts with a package declaration. This organizes policies into namespaces, preventing naming collisions. The most basic rule is an "assignment" or a "complete rule." We define a default state—usually default allow = false—and then write conditions that, if met, change that state to true.
package cloud.authzDefault to a secure-by-default stance
default allow = false
Allow access if the user is an admin
allow { input.user.role == "admin" }
In this example, OPA looks at the input document provided during the query. If the JSON input contains a user object with a role field equal to "admin", the allow rule evaluates to true. If any part of the rule fails (e.g., the role is "developer"), the rule is undefined, and OPA falls back to the default value.
Logical Operators and Conditional Expressions in Rego
Rego uses an "AND" logic for statements within a single rule block. Every line in a rule must be true for the rule itself to be true. To represent "OR" logic, we define multiple rules with the same name. OPA will evaluate all of them and return true if any of the rule blocks are satisfied.
# Allow if user is admin OR if they are the resource ownerallow { input.user.role == "admin" }
allow { input.user.id == input.resource.owner_id }
This "OR" logic is crucial for complex RBAC (Role-Based Access Control) and ABAC (Attribute-Based Access Control). We also use the some keyword to iterate over arrays and the in operator for membership checks, which were introduced in newer versions of the Rego compiler to simplify syntax.
Rego Examples: Simple Allow/Deny Logic
In production, we often move beyond simple booleans to "violation" or "deny" rules that return descriptive error messages. This is particularly useful for developer feedback in CI/CD pipelines. Instead of just seeing "Build Failed," the developer receives a specific reason, such as "S3 bucket must have encryption enabled."
package terraform.security
deny[msg] { resource := input.resource_changes[_] resource.type == "aws_s3_bucket" not resource.change.after.server_side_encryption_configuration msg := sprintf("Security Violation: S3 bucket %v must have encryption enabled.", [resource.address]) }
The [_] syntax is a "wildcard" that tells OPA to iterate over every element in the resource_changes array. This is a powerful feature of Rego—it performs the iteration for you without requiring a formal for loop structure. We use this extensively to scan entire Terraform plans for security regressions and alignment with the OWASP Top 10.
Kubernetes Admission Control Policy Examples
Kubernetes is a primary target for Rego policies. By using OPA as an Admission Controller (often via the OPA Gatekeeper project), we can enforce constraints that native K8s RBAC cannot. For example, we can mandate that every Deployment must have a cost-center label or that no Container can run as the root user.
package kubernetes.admission
deny[msg] { input.request.kind.kind == "Pod" container := input.request.object.spec.containers[_] container.securityContext.runAsNonRoot == false msg := sprintf("Pod %v is not allowed to run as root.", [input.request.object.metadata.name]) }
When a user runs kubectl apply, the API server sends an AdmissionReview object to OPA. The Rego policy inspects the securityContext. If any container in the pod has runAsNonRoot set to false (or missing), the deployment is blocked. This prevents a large class of container breakout attacks.
Terraform Plan Validation Using Rego
To validate infrastructure before it is provisioned, we convert the Terraform plan into JSON. This allows us to catch expensive or insecure configurations. For Indian startups, "bill shock" is a real threat. We've written policies to block the creation of high-cost instances like p4d.24xlarge unless explicitly approved by the CTO.
$ terraform plan -out=tfplan.binary
$ terraform show -json tfplan.binary > tfplan.json $ opa eval --data policy.rego --input tfplan.json "data.terraform.security.deny"
By integrating this into a GitHub Action or GitLab CI, we ensure that no infrastructure change violates our financial or security guardrails. If the deny rule returns any messages, the pipeline exits with a non-zero code, stopping the deployment.
Microservices Authorization and RBAC Examples
In a microservices architecture, hardcoding authorization logic into every service leads to inconsistency. We recommend using OPA as a "Sidecar" container. The microservice receives a request, extracts the JWT (JSON Web Token), and sends the token claims and the requested path to OPA.
package app.authzimport input.attributes.request.http as http_request
default allow = false
allow { # Verify JWT signature and expiration (simplified) [header, payload, signature] := io.jwt.decode(http_request.headers.authorization) payload.role == "editor" http_request.method == "POST" startswith(http_request.path, "/v1/content") }
This approach allows the security team to update access policies globally without redeploying the microservices themselves. If a new regulation requires "editors" to have MFA (Multi-Factor Authentication), we simply update the Rego policy to check for an mfa_verified claim in the JWT, a critical step in detecting MFA proxy bypass attacks.
Data Handling and Manipulation in Rego
Rego's strength is its ability to manipulate complex data types. We often need to merge data from multiple sources—for example, combining a list of users from a JSON file with a list of forbidden IP ranges from a YAML file. OPA handles both formats natively.
# Loading external data into OPA
$ opa run -d users.json -d blacklist.yaml
Once loaded, this data is available under the data namespace. We can then write rules that check if an incoming request's IP is in the blacklist. This is particularly useful for blocking traffic from known malicious actors identified by CERT-In advisories.
Iterating Over Collections and Arrays
Iteration in Rego is implicit. When you use a variable inside an array index, Rego finds all possible values for that variable that make the expression true. This is known as "unification."
# Check if any user in the list has an expired password
expired_users[user.name] { some i user := data.users[i] user.password_age_days > 90 }
The expired_users rule creates a set of names. We use some i to explicitly declare the iterator, though in modern Rego, the underscore _ is more common for anonymous iteration. This capability allows us to perform complex audits on large datasets with very little code.
Using External Data Sources in Rego Policies
Sometimes, the policy decision depends on data that changes too frequently to be hardcoded, such as a list of currently active on-call engineers. OPA provides the http.send function to fetch data from an external API during the evaluation process. However, this must be used cautiously to avoid performance bottlenecks and SSRF vulnerabilities.
# Fetching on-call status from a PagerDuty-like API
is_on_call { response := http.send({"method": "get", "url": "https://api.internal/on-call"}) response.body.user == input.user.id }
To mitigate risks, we always validate the URL and ensure the API is internal. We also use OPA's caching mechanisms to prevent overwhelming the external service with requests for every policy check.
Understanding Vehicle Registration Policy (Rego) Context
A common point of confusion for junior security researchers or those pursuing cybersecurity training courses is the term "Rego." In Australia and New Zealand, "Rego" is the standard shorthand for Vehicle Registration. When searching for "Rego policy examples," you may inadvertently find government documents regarding car renewals instead of OPA code. In a technical security context, "Rego" refers exclusively to the OPA policy language.
However, there is a technical intersection. Fleet management companies in India and APAC are now using OPA to automate their vehicle compliance. For instance, a logistics company might use a Rego (OPA) policy to determine if a driver is allowed to operate a specific vehicle based on the vehicle's "Rego" (registration) status and the driver's current location.
The Difference Between OPA Rego and Vehicle Registration Policies
OPA Rego is a logic engine; Vehicle Registration is a domain-specific compliance requirement. We distinguish them in our documentation by always referring to the language as "Rego Policy" and the vehicle context as "Vehicle Compliance." If you are building a system for an Indian logistics firm, your OPA input might look like this:
# Input for a logistics compliance check
vehicle: id: "MH-01-AB-1234" registration_expiry: "2024-12-31" state: "Maharashtra" driver: id: "D101" license_valid: true
Standard Vehicle Registration Policy Examples and Requirements
A Rego policy to enforce vehicle compliance would look for specific date thresholds. For example, a policy might deny a trip if the vehicle registration expires within the next 48 hours, ensuring the fleet remains legal during long-haul transits between states like Karnataka and Tamil Nadu.
package fleet.complianceDeny trip if registration expires in less than 2 days (172800 seconds)
deny_trip[msg] { expiry_timestamp := time.parse_rfc3339_ns(input.vehicle.registration_expiry) now := time.now_ns() remaining := expiry_timestamp - now remaining < 172800000000000 msg := "Trip denied: Vehicle registration expires within 48 hours." }
This demonstrates how OPA's flexibility allows it to handle business logic far beyond standard IT security, reaching into operational compliance and fleet management.
Best Practices for Writing and Testing Rego Policies
Writing Rego is easy; writing maintainable Rego is difficult. We follow a modular approach. Instead of one massive policy.rego file, we break policies down by domain: compute.rego, network.rego, and storage.rego. We then use a common base.rego for shared helper functions.
Helper functions reduce repetition. For instance, a function to check if a tag exists can be reused across S3, EC2, and RDS policies. This makes the codebase cleaner and easier to audit for DPDP compliance.
# Helper function in base.regohas_tag(resource, tag_key) { resource.tags[tag_key] }
Usage in storage.rego
deny { resource := input.resource_changes[_] not has_tag(resource, "Environment") }
Unit Testing Rego Policies with 'opa test'
We never deploy a policy without tests. OPA has a built-in test runner that is exceptionally fast. We write test cases that simulate both "allow" and "deny" scenarios. This prevents a change in one rule from accidentally breaking another—a common issue in large policy sets.
# policy_test.regopackage terraform.security
test_allow_encrypted_s3 { allow with input as {"resource_changes": [{"type": "aws_s3_bucket", "change": {"after": {"server_side_encryption_configuration": true}}}]} }
test_deny_unencrypted_s3 { deny["Security Violation: S3 bucket aws_s3_bucket.test must have encryption enabled."] with input as {"resource_changes": [{"address": "aws_s3_bucket.test", "type": "aws_s3_bucket", "change": {"after": {}}}]} }
Run the tests using the CLI. We aim for 100% coverage on all governance policies. In our CI/CD pipelines, we use the --threshold flag to fail the build if coverage drops.
$ opa test . -v
data.terraform.security.test_allow_encrypted_s3: PASS (0.542ms) data.terraform.security.test_deny_unencrypted_s3: PASS (0.321ms) -------------------------------------------------------------------------------- PASS: 2/2
Performance Optimization for Complex Policy Sets
As policy sets grow to thousands of rules, evaluation time can increase. We've seen OPA performance degrade when using deep recursion or massive cross-joins between large arrays. To optimize, we use "Early Exit" logic. By placing the most restrictive and cheapest checks at the top of a rule, OPA can stop evaluating as soon as one condition fails.
Another optimization is "Partial Evaluation." OPA can pre-compute parts of a policy that depend on static data, leaving only the dynamic parts to be evaluated at runtime. This is particularly effective for large-scale deployments in high-traffic environments like Indian e-commerce platforms during "Big Billion Day" sales, where every millisecond of latency counts.
Security Vulnerabilities in OPA and Rego
Like any software, OPA is subject to vulnerabilities. We recently patched our systems for CVE-2024-24786, as detailed in the NIST NVD, a flaw in the Go protobuf module used by OPA. An attacker could send a malformed policy input that causes the OPA engine to consume excessive CPU, leading to a Denial of Service (DoS). We recommend always running the latest stable version of OPA (currently v0.61.0+).
Another risk is Rego Injection. This occurs when developers build Rego queries by concatenating strings with untrusted user input. For example, if you are building a query string to check a username, an attacker could inject Rego syntax to bypass the allow logic.
# VULNERABLE: Concatenating input into a query stringquery := sprintf("data.authz.allow == true; input.user == '%v'", [user_input])
SECURE: Passing user input through the 'input' document
opa.eval(query, input_document)
Lastly, CVE-2022-36085 highlighted the dangers of the http.send function. If a policy author allows a user-provided URL to be passed directly to http.send, it can result in Server-Side Request Forgery (SSRF). We mitigate this by using a whitelist of allowed domains within the Rego policy itself.
# Secure http.send with URL validationallowed_domains := ["api.internal.corp", "metadata.google.internal"]
is_url_safe(url) { some domain in allowed_domains contains(url, domain) }
deny { not is_url_safe(input.target_url) # ... logic to block request }
Summary of Key Rego Policy Examples
Throughout this technical deep-dive, we have covered several high-impact Rego patterns. From basic RBAC to complex Terraform validation for DPDP compliance, the versatility of Rego is clear. Key examples included:
- Region Locking: Restricting AWS deployments to
ap-south-1for data sovereignty. - S3 Encryption: Mandating server-side encryption for all storage buckets.
- K8s Root Prevention: Blocking pods that attempt to run with elevated privileges.
- Cost Guardrails: Preventing the use of expensive GPU instances in non-production environments.
- Fleet Compliance: Integrating vehicle registration logic into operational workflows.
Future Trends in Policy as Code
We are seeing a move toward "Policy Bundles" where OPA instances pull signed, encrypted policy updates from a central repository. This is critical for highly regulated industries like banking and insurance in India, where the RBI (Reserve Bank of India) is increasingly focused on IT governance. Additionally, the integration of AI to generate Rego policies from natural language is emerging, though we still recommend AI red teaming to validate any AI-generated security logic.
The next step for your team is to move OPA from a "testing" phase into a "blocking" phase. Start by running OPA in "advisory" mode, where it logs violations but doesn't stop deployments. Once you have tuned your policies and eliminated false positives, flip the switch to "enforcement" mode.
To begin testing your own policies against live Kubernetes clusters, use the following command to inspect your current admission configurations and see how OPA would see the data:
$ kubectl get validatingwebhookconfigurations -o json | opa eval -f json -i - "data.kubernetes.admission"