During a recent audit of a mid-sized Indian FinTech's Kubernetes infrastructure in the ap-south-1 region, we discovered that 40% of their microservices were running with privileged: true in their deployment manifests. This was not a malicious act by developers, but a result of using "quick-start" templates found on public repositories. In the context of the Digital Personal Data Protection (DPDP) Act 2023, such misconfigurations are no longer just technical debt; they represent a significant legal liability. Under Section 8 of the Act, entities must implement "reasonable security safeguards" to prevent personal data breaches. Integrating a robust SIEM solution can help track these policy violations and unauthorized access attempts in real-time.
What is Policy as Code (PaC)?
Policy as Code (PaC) is the practice of managing and enforcing security and operational rules through machine-readable definition files. We treat these policies exactly like application code: they are version-controlled in Git, peer-reviewed via Pull Requests, and tested in CI/CD pipelines. Instead of relying on a manual checklist or a PDF document that developers might ignore, PaC provides an automated "gatekeeper" that blocks non-compliant infrastructure from ever reaching production.
In a Kubernetes environment, PaC allows us to define strict guardrails. For example, we can mandate that every container must have resource limits, no container can run as root, and all images must come from a private registry like ECR or an internal Nexus instance.
Introduction to Open Policy Agent (OPA)
Open Policy Agent (OPA) is a graduate-level CNCF project that provides a unified toolset and framework for policy enforcement across the cloud-native stack. It uses a high-level declarative language called Rego. OPA is designed to be decoupled from the service it is protecting. When a service needs to make a policy decision, it sends a JSON query to OPA. OPA evaluates the query against the policy and data it has loaded and returns a decision, also in JSON format.
This decoupling is critical. It means we can use the same Rego logic to authorize API requests in a Go-based microservice—a vital step when securing web applications against URL validation bypass—validate Terraform plans before terraform apply, and intercept Kubernetes API requests via an Admission Controller.
Why Rego is the Standard for Policy as Code
Rego is inspired by Datalog, a well-understood query language. Unlike imperative languages where you describe how to check a condition (using loops and if-else chains), Rego is declarative. You describe the state that should exist. If the state of the incoming request matches the conditions defined in your "deny" rules, OPA triggers a violation.
Rego is particularly powerful for Kubernetes because K8s manifests are deeply nested JSON/YAML structures. Rego’s ability to traverse these structures with concise syntax makes it far more efficient than writing custom Python or Bash scripts for validation.
Installing the OPA CLI
To start developing policies, we need the OPA binary. On Linux or macOS, we can install it directly from the official releases. We use the CLI for evaluating policies locally before they are deployed to a cluster. For teams managing remote servers, using a browser based SSH client can simplify the process of testing these policies across different environments without local configuration overhead.
# Download the OPA binary for Linuxcurl -L -o opa https://openpolicyagent.org/downloads/v0.61.0/opa_linux_amd64_static
Make it executable and move to path
chmod 755 ./opa sudo mv ./opa /usr/local/bin/
Verify the installation
opa version
The opa check command is our first line of defense during development. It validates the syntax of our Rego files, ensuring there are no typos or logical errors in the package definitions.
# Syntax check for all rego files in the current directory
opa check *.rego
Using the Rego Playground for Rapid Prototyping
While local development is preferred for production policies, the Rego Playground (play.openpolicyagent.org) is an invaluable tool for rapid prototyping. It allows us to paste a JSON input (like a Kubernetes Pod manifest), write a rule, and see the output in real-time. We often use it to share policy snippets during incident post-mortems or when collaborating on new compliance requirements.
VS Code Extensions for Rego Development
For a professional workflow, we use the "Open Policy Agent" extension for VS Code. This provides syntax highlighting, linting, and the ability to run opa eval on the current file with a keyboard shortcut. It also allows us to see which parts of our code are covered by tests, which is essential for maintaining high-quality security policies.
Packages and Imports
Every Rego file must start with a package declaration. This creates a namespace for the rules. In Kubernetes, we typically follow a hierarchical structure like package kubernetes.admission. We can also import other packages to reuse helper functions.
package kubernetes.security
import data.lib.helpers
Variables and Assignment
In Rego, we assign values to variables using the := operator. However, most of our work involves defining rules that evaluate to true or false. A rule is essentially a variable that gets a value if its body evaluates to true.
Logical AND/OR in Rego Rules
In Rego, logical AND is implicit. All expressions within a rule body must be true for the rule to trigger. For logical OR, we define multiple rules with the same name. If any one of those rules is true, the overall rule is true.
# This rule triggers if the container is privileged AND uses a forbidden imagedeny[msg] { container := input.request.object.spec.containers[_] container.securityContext.privileged == true msg := "Privileged containers are not allowed." }
This rule triggers if the service is a LoadBalancer OR a NodePort
violation[msg] { input.request.object.spec.type == "LoadBalancer" msg := "LoadBalancer services are restricted." }
violation[msg] { input.request.object.spec.type == "NodePort" msg := "NodePort services are restricted." }
The Importance of the 'Default' Keyword
The default keyword is used to set a fallback value for a rule. This is critical for authorization policies. We usually want to start with a "deny-all" stance and only allow specific requests.
package authzdefault allow = false
allow { input.user == "admin" }
Defining the Input JSON Structure
When OPA evaluates a Kubernetes request, the input variable contains the AdmissionReview object sent by the Kubernetes API server. This object includes the request, which contains the kind, operation (CREATE, UPDATE, DELETE), and the actual object (the manifest).
Creating a Simple 'Allow' Rule
Let's write a policy that prevents the use of the latest tag in container images. Using latest is a common security risk because it makes it impossible to track which version of the code is actually running and can lead to unpredictable rollouts.
package kubernetes.admission
deny[msg] { input.request.kind.kind == "Pod" container := input.request.object.spec.containers[_] endswith(container.image, ":latest") msg := sprintf("Image '%v' uses the prohibited 'latest' tag.", [container.image]) }
Implementing Conditional Logic and Constraints
We can add more complex logic, such as exempting specific namespaces from certain rules. For instance, the kube-system namespace often needs privileges that application namespaces do not.
deny[msg] {
input.request.kind.kind == "Pod" input.request.namespace != "kube-system" container := input.request.object.spec.containers[_] container.securityContext.privileged == true msg := "Privileged containers are only allowed in kube-system." }
Generating Custom Error and Violation Messages
Clear error messages are vital for developer experience. When a CI/CD pipeline fails because of a policy violation, the developer should know exactly what went wrong and how to fix it. We use sprintf to inject dynamic data into our error messages.
msg := sprintf("Security Violation: Container '%v' in Pod '%v' is attempting to run as privileged.", [container.name, input.request.object.metadata.name])
Iterating Over Arrays and Objects
The [_] operator in Rego is the "iteration" or "wildcard" operator. It allows us to traverse all elements in an array without writing an explicit for loop. This is perfect for checking all containers within a Pod or all volumes in a deployment.
Using Built-in Functions
Rego comes with over 150 built-in functions for string manipulation, CIDR arithmetic, and aggregate operations. We frequently use re_match for regex validation of resource names or image paths.
# Check if image comes from our approved ECR registry
is_approved_registry(image) { re_match("^123456789012.dkr.ecr.ap-south-1.amazonaws.com/.*", image) }
Handling External Data Lookups
Sometimes, a policy decision depends on data outside the immediate request. For example, we might want to check if a user belongs to a specific group in our LDAP or Active Directory. OPA can be configured to pull this data periodically or we can push it to OPA via its Data API.
Writing Unit Tests with 'opa test'
We never deploy a policy without unit tests. The opa test command allows us to verify our logic against mock inputs. We create a separate file (e.g., policy_test.rego) and write rules that start with the prefix test_.
package kubernetes.admissiontest_deny_privileged_container { # Mock input input := {"request": {"kind": {"kind": "Pod"}, "object": {"spec": {"containers": [{"securityContext": {"privileged": true}}]}}}}
# Assert that 'deny' contains the expected message results := deny with input as input count(results) == 1 }
To run the tests and check performance benchmarks:
$ opa test . -v --bench
data.kubernetes.admission.test_deny_privileged_container: PASS (1.2ms) --------------------------------------------------------------------------- PASS: 1/1
Mocking Input Data for Reliable Tests
When mocking, we must ensure the structure matches exactly what Kubernetes sends. We often capture real AdmissionReview objects from our staging cluster using kubectl get and use them as the basis for our test mocks.
Debugging Policies Using the 'opa trace' Command
If a policy isn't behaving as expected, opa eval --trace is our best tool. It provides a step-by-step breakdown of how OPA evaluated the rules, showing which expressions succeeded and which failed.
opa eval --data policy.rego --input input.json "data.kubernetes.admission.deny" --trace
Kubernetes Admission Control Policies
In a production cluster, we use OPA as an Admission Controller. When someone runs kubectl apply, the API server sends a request to OPA. If OPA returns a deny, the API server rejects the change. This is critical for mitigating vulnerabilities like CVE-2024-21626 (runc process.cwd container escape), as documented in the NIST NVD. We can write a policy to block hostPath mounts to sensitive directories like /proc.
deny[msg] {
input.request.kind.kind == "Pod" volume := input.request.object.spec.volumes[_] volume.hostPath.path == "/proc" msg := "Mounting /proc via hostPath is prohibited (CVE-2024-21626 mitigation)." }
Terraform Plan Validation
We also use OPA to secure our Infrastructure as Code (IaC). By converting a Terraform plan to JSON (terraform show -json tfplan.binary > plan.json), we can run Rego policies against it. For an Indian SME, this might involve ensuring that all S3 buckets have public_access_block enabled to prevent data leaks that would violate DPDP Act regulations.
deny[msg] {
resource := input.resource_changes[_] resource.type == "aws_s3_bucket" # Logic to check for public access block not resource.change.after.public_access_block_enabled msg := sprintf("S3 bucket %v must have public access block enabled.", [resource.address]) }
API Authorization and Microservices Security
For microservices, OPA can act as a sidecar. When service A wants to call service B, service B asks its local OPA sidecar if the request is authorized based on the JWT token provided. This centralizes authorization logic, making it easier to audit and update across hundreds of services.
Modularizing Policies for Reusability
As the number of policies grows, we move common logic into library packages. For example, a lib.kubernetes package might contain functions for identifying "workload" types (Pods, Deployments, StatefulSets).
Performance Optimization and Rule Indexing
OPA is highly optimized, but inefficient Rego can slow down API requests. We avoid using the [_] operator on large datasets inside nested loops. OPA's compiler automatically performs "rule indexing" for constant-time lookups if rules are structured as simple key-value pairs.
Integrating Rego into CI/CD Pipelines
We use conftest, a utility built on top of OPA, to run policies in our GitLab or GitHub Action pipelines. It provides a developer-friendly output and returns a non-zero exit code if policies fail, effectively breaking the build.
# Example Conftest command in a CI pipeline
conftest test deployment.yaml --policy ./policies --output json
DPDP Act Compliance and Technical Evidence
Under the DPDP Act 2023, if a breach occurs, the Data Protection Board (DPB) will look for evidence of "technical measures" taken to protect data. Having a version-controlled repository of Rego policies that enforce encryption at rest, restrict network access, and prevent privileged container execution serves as strong technical evidence of due diligence. This approach aligns with broader OpenSSH security standards for protecting administrative access.
For Indian startups using managed Kubernetes services like Amazon EKS or Google GKE, the "Default" configurations are often insufficient for high-security environments. Managed providers focus on availability; security hardening is the "customer's responsibility" under the Shared Responsibility Model. Implementing OPA fills this gap by enforcing a "Security by Design" architecture.
Summary of Key Rego Concepts
We have covered the foundational aspects of securing Kubernetes using OPA and Rego. From the initial technical observation of privileged container risks to the implementation of complex policies that mitigate CVEs and ensure DPDP Act compliance, the workflow remains consistent: define the policy, write the tests, and automate the enforcement.
Next Command to Run
To see OPA in action immediately on your local machine with a real Kubernetes manifest, save a deployment to deploy.json and run:
# Evaluate a local policy against a local manifest
opa eval --data policy.rego --input deploy.json "data.kubernetes.admission.deny"
This command provides the immediate feedback loop necessary for building robust Policy-as-Code frameworks. As you scale, consider migrating from raw OPA to OPA Gatekeeper, which provides a Kubernetes-native CRD-based approach to policy management.
