A recent post on r/Terraform captured a common fear:

“I’m planning to use Terraform to manage multiple environments, DEV, PROD, BETA, but I’m not a DevOps person. I’m worried a rookie developer could accidentally destroy production. Is there a way to prevent this?”

This isn’t a hypothetical concern. In most Terraform setups, there’s nothing stopping a developer from running terraform apply against the production backend with the wrong variable file or default provider credentials. One misstep, like accidentally targeting the wrong workspace or pushing unvetted changes through CI, can delete a live RDS instance, remove security group rules for internal services, or reconfigure IAM roles used by critical workloads. Terraform does not enforce environments by itself; it applies whatever is declared in code, whether it’s secure or not.

As teams scale, they typically manage multiple environments across multiple accounts. And that’s when things start to slip. Security rules like KMS encryption, public access blocks, and IAM boundaries are enforced in one environment but forgotten in another. Tagging standards, which are essential for cost attribution and ownership, are often applied manually, if at all. Developers might provision S3 buckets in staging without logging, or skip required tags in dev because “it’s just a test.”

To catch these inconsistencies, teams often fall back on peer reviews and manual plan approvals. But those don’t scale. Reviews get rushed. Context is missed. And critical mistakes like exposing a service to all IP addresses or leaving a database without deletion protection are usually caught after deployment, if at all.

The root cause is simple: Terraform doesn’t enforce policy natively. It’s a declarative tool, not a governance system. There’s no built-in mechanism to block unsafe configurations, validate required tags, or enforce org-level constraints. Without a proper policy layer, enforcement depends on tribal knowledge, best-effort scripting, and CI/CD hacks that break under pressure.

This is where Policy-as-Code (PaC) becomes essential. It allows you to define security, tagging, and compliance rules as code and enforce them automatically before any changes are applied. Instead of hoping someone notices a misconfigured resource in a plan review, PaC ensures violations are blocked early, reliably, and consistently across every environment.

What is Policy-as-Code (PaC)?

Policy-as-Code (PaC) is the practice of defining infrastructure rules, like “S3 objects must be encrypted” or “EC2 instances must not use public AMIs,” as versioned, testable code. These policies are written using policy engines like Open Policy Agent (OPA) and are enforced before infrastructure changes are deployed.

In Terraform, PaC is used to block unsafe, non-compliant, or misconfigured resources before they’re deployed. For example:

  • If someone tries to create an S3 bucket without server-side encryption enabled, the policy fails the plan.

  • If a developer forgets to add mandatory cost and owner tags to an EC2 instance, the change is blocked.

  • If an RDS instance in the prod environment is missing deletion protection, the plan won’t proceed.

These checks run during terraform plan or inside a CI/CD pipeline before terraform apply is allowed. The result is a gatekeeping mechanism that catches security and compliance violations early, before anything hits the cloud provider.

Why PaC Matters

  1. Consistent enforcement across environments
    PaC ensures the same rules apply to dev, stage, and prod. For example, if every S3 bucket must have server-side encryption and specific tags, PaC makes sure that the rule is enforced everywhere, no matter who runs terraform apply.

  2. Prevent mistakes before deployment
    If someone tries to deploy a resource that violates a rule (e.g., leaving a database unencrypted or forgetting required tags), the policy engine flags it before any resource is created. This means security issues are caught early during the plan phase  instead of after something is already deployed.

  3. Scales better than manual reviews
    As your team and infrastructure grow, you can’t rely on manual plan reviews to catch everything. With PaC, every deployment is checked automatically. No need for a senior engineer to manually review hundreds of lines of Terraform diff.

  4. Version control and auditability
    Policies are stored in Git, just like Terraform code. You can track changes, do code reviews, and roll back if needed. This makes it easier for security and DevOps teams to collaborate and evolve rules over time.

  5. Automated testing and validation
    Policies can be tested just like application code. You can write test cases to check that certain resources (like S3 buckets) will be blocked if they don’t meet your standards. This reduces false positives and gives you confidence that your policies are working as intended.

Implementing Terraform Security with Open Policy Agent (OPA)

Open Policy Agent (OPA) is a general-purpose policy engine that can enforce custom rules against your Terraform plans before they are applied. It’s especially useful for catching security misconfigurations and compliance issues early in the deployment workflow, without needing access to your live infrastructure.

OPA works by analyzing the output of terraform plan (converted to JSON) and checking it against defined policies. While OPA doesn’t integrate directly with Terraform, it can be used alongside tools like Conftest or integrated into CI/CD pipelines to automatically evaluate plans and block unsafe changes before they’re applied. Here’s the general flow of how Terraform plans are evaluated with OPA:

general flow of how Terraform plans are evaluated with OPA

The workflow starts with:

Integrating OPA with Terraform

1. Generate the Terraform Plan

Create a binary plan file using terraform plan -out=tfplan. Then convert it to JSON using terraform show -json tfplan > tfplan.json

2. Evaluate the Plan with OPA

Use OPA’s CLI to evaluate the JSON plan against your policy bundle using:

opa exec --decision terraform/module/deny --bundle policy/ tfplan.json

  • --decision terraform/module/deny: defines which rule (decision path) to evaluate.

  • --bundle policy/: path to your Rego policy files.

  • tfplan.json: the Terraform plan in JSON format.

OPA will analyze the JSON and return violations based on the rules you've defined.

Blocking Public HTTP Access in Security Groups

Let’s walk through an example where we want to block any security group that allows inbound HTTP (port 80) from 0.0.0.0/0  a common misconfiguration.

Terraform Config (main.tf)

provider "aws" {
  region = "us-east-1"
}
data "aws_vpc" "default" {
  default = true
}
module "http_sg" {
  source     = "git::https://github.com/terraform-aws-modules/terraform-aws-security-group.git?ref=v3.10.0"
  name       = "http-sg"
  vpc_id     = data.aws_vpc.default.id
  description         = "Security group with HTTP ports open"
  ingress_cidr_blocks = ["0.0.0.0/0"]
}
resource "aws_security_group" "allow_tls" {
  name        = "allow_tls"
  description = "Allow TLS inbound traffic"
  vpc_id      = data.aws_vpc.default.id
  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/8"]
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = {
    Name = "allow_tls"
  }
}

Here, the http_sg module is exposing HTTP access to the entire internet, something you typically want to prevent in production environments.

Rego Policy (terraform_module.rego)

Now we define a policy that flags any resource with a description containing the word "HTTP":

package terraform.module
deny[msg] {
  some r
  desc := resources[r].values.description
  contains(desc, "HTTP")
  msg := sprintf("No security groups should be using HTTP. Resource in violation: %v", [r.address])
}
resources[r] {
  some path, value
  walk(input.planned_values, [path, value])
  r := module_resources(path, value)
}

module_resources(path, value) := value {
  reverse_index(path, 1) == "resources"
  reverse_index(path, 2) == "root_module"
}
reverse_index(path, idx) := path[count(path) - idx]

This policy looks for any planned resource where the description contains “HTTP” and flags it. It’s a simple pattern match, but effective for surfacing risky configurations.

Running the Evaluation

Once the Terraform plan is generated using terraform plan -out=tfplan, convert it to JSON format with terraform show -json tfplan > tfplan.json, and then run the OPA policy evaluation with opa exec --decision terraform/module/deny --bundle policy/ tfplan.json.

Result from OPA

If the policy is violated, you’ll see structured output like it is shown below:

{
  "result": [
    {
      "decision_id": "f72f8763-0887-41ae-9156-621f1f310955",
      "path": "tfplan.json",
      "result": [
        "No security groups should be using HTTP. Resource in violation: module.http_sg.aws_security_group.this_name_prefix[0]"
      ]
    }
  ]
}

OPA flags the exact resource in violation, making it easy for developers to locate and correct the issue before it gets deployed.

Benefits of  Evaluating with OPA with Terraform

  • Automated Security Enforcement
    Catch violations like open ingress, missing encryption, or missing tags before deployment  without relying on manual review.

  • Clear and Actionable Output
    OPA returns structured messages pinpointing the exact resource and reason for the failure, improving developer feedback loops.

  • Custom Policies for Your Needs
    You can define rules based on your organization’s security, compliance, or operational requirements, from tagging enforcement to IAM permission constraints.

  • CI/CD Integration at Scale
    Easily plug OPA into pipelines like GitHub Actions, GitLab CI, or Jenkins to enforce policies across all Terraform deployments.

By using OPA to evaluate Terraform plans before applying them, you introduce a proactive layer of security that prevents misconfigured infrastructure from being deployed, all using version-controlled, testable policies. This shifts policy enforcement left and secures your infrastructure provisioning workflows.

Policy-as-Code with Firefly

Writing Rego policies and getting them applied can have a steep learning curve, especially when dealing with complex Terraform configurations and multi-cloud environments. However, Firefly simplifies this by providing a centralized platform to define, enforce, and monitor Policy-as-Code (PaC). Firefly comes with a comprehensive set of out-of-the-box policies for common security, compliance, and operational requirements. It enables you to easily manage both built-in and custom policies in AWS, GCP, and other platforms, all from a single interface.

From this single pane, you get live tracking for PCI DSS, SOC 2, HIPAA, NIST, and more, as shown in the above Governance view from Firefly.

From this single pane, you get live tracking for PCI DSS, SOC 2, HIPAA, NIST, and more, as shown in the above Governance view from Firefly.

Why Traditional Policy-as-Code is Hard to Scale

Most teams that try to implement PaC from scratch quickly run into three core problems:

  1. Writing Policies is Complex
    Writing policies requires understanding Rego, Terraform’s JSON structure, and the shape of your actual resource data. Even a simple policy like "deny public S3 buckets" can take a reasonable amount of time to write and debug.

  2. Multi-Environment Management is Error-Prone
    When you have multiple teams deploying across multiple cloud accounts (dev, staging, prod), keeping policies consistent across environments becomes a manual and fragile process. Updates get missed, reviews fall through the cracks, and enforcement becomes uneven.

  3. No Built-in Testing or Feedback Loop
    With traditional PaC, there's no immediate way to validate whether your policy actually matches real infrastructure. You write code, run a scan, and hope it works, which leads to slow iteration and low adoption.

Firefly’s Unified Policy Engine

Firefly solves these problems by integrating a governance layer directly into your IaC and cloud environments. Once your cloud accounts are connected, Firefly scans your infrastructure using KICS policies, written in Rego and categorized into policy types such as:

  • Access Control – Validates IAM roles, permissions, and service-level access

  • Encryption – Flags resources missing encryption at rest or in transit

  • Backup – Checks for backup retention and restore configuration

  • Insecure Defaults – Detects services with default (and often unsafe) settings

  • Firewall & Networking – Flags misconfigured security groups or public subnets

  • Secret Management – Identifies hardcoded secrets or missing encryption

  • Availability – Validates multi-AZ configurations, health checks, etc.

Firefly runs these checks automatically, surfaces violations in the UI, and maps each policy to the impacted assets. All violations are categorized by severity (INFO to CRITICAL) to help prioritize remediation.

Writing Custom Policies in Firefly

In addition to built-in policies, Firefly lets you write your own rules using Rego via the Governance > + Custom Policy workflow. Here’s how it works:

  1. Define Metadata


    • Name the policy

    • Choose a category (e.g., Encryption, Backup)

    • Set severity (e.g., MEDIUM, HIGH, CRITICAL)

    • Pick the data source (e.g., AWS) and target asset type (e.g., EC2 instance)

2. Write Rego Policies with Context

‍
Let’s say you want to ensure that only f1-micro instance types are allowed in Google Cloud. Here's how it works:

  1. Go to Governance → + Custom Policy

  2. Choose the cloud provider and asset type (e.g., GCP Compute Instances)

  3. Write a short Rego rule in the embedded Rego Playground

  4. Click Evaluate to test it against live assets

  5. Firefly returns matching results so you can validate and fine-tune the logic

In the custom policy in the snapshot below, the policy blocks any GCP instance that isn’t f1-micro:

You define the policy name, description, severity (TRACE to CRITICAL), and scope. Once validated, Firefly automatically applies it to all selected resources, so you do not need to write pipeline steps or conditionals manually.

You define the policy name, description, severity (TRACE to CRITICAL), and scope. Once validated, Firefly automatically applies it to all selected resources, so you do not need to write pipeline steps or conditionals manually.

‍
Once validated, you can apply the policy across environments. Matched violations are displayed in the Governance table with context, including affected asset IDs, descriptions, and exact rule logic. You can filter by provider, category, severity, and even push violations to notification tools like Slack or email.

Built-in Remediations

For many violations, Firefly generates CLI-ready remediation commands. For example, let’s say a Google Cloud Storage bucket has uniform bucket-level access disabled, which violates a policy that requires centralized permission control. Instead of just alerting the user, Firefly automatically generates the corresponding gcloud command to fix the issue.

In the snapshot below, Firefly has detected the misconfiguration in the bucket firefly-state-fjfdan and suggests the following patch: gcloud storage buckets update gs://BucketName --uniform-bucket-level-access

Firefly has detected the misconfiguration in the bucket firefly-state-fjfdan and suggests the following patch

This command can be copied directly from the Firefly console and executed to enforce the required configuration. By embedding these one-click remediations into your governance workflow, Firefly helps reduce time-to-resolution and eliminates manual stress.

Enforcing Industry Frameworks

Firefly maps its policies to compliance frameworks like:

  • PCI-DSS – For organizations handling cardholder data

  • SOC 2 – For systems involving data privacy and availability

  • HIPAA – For healthcare-related cloud workloads

  • NIST – For aligning with U.S. government security standards

Here’s how Firefly visualizes framework coverage panel right in your dashboard:

Each framework is mapped to a predefined set of policies that Firefly automatically applies across your infrastructure.

Each framework is mapped to a predefined set of policies that Firefly automatically applies across your infrastructure. Compliance gaps are tracked in real time with visibility into pass/fail status across each requirement.

Preventing Violations Before They Happen

Before Policy-as-Code, infrastructure reviews were a mix of tribal knowledge and brittle scripts. Misconfigurations like open security groups, missing tags, or unencrypted volumes were either caught manually, often late, or worse, deployed straight into production. Guardrails were more of a wish list than reality.

The Old Way: Reactive and Inconsistent

Here’s how most teams operated before adopting PaC:

  • Manual Terraform Reviews: A senior engineer would spot-check code for obvious issues, usually after the PR was already open. You might catch a public S3 bucket or a bad CIDR block, but only if someone remembered to look.

  • Ad-Hoc CI Checks: Teams would write custom scripts to lint HCL or catch insecure patterns. These checks were often team-specific and missed edge cases.

  • Delayed Feedback: You’d run terraform applyit an issue, and then spend hours diagnosing why a resource was non-compliant.

This was unsustainable. Review cycles dragged, misconfigurations slipped through, and teams burned time on rework.

The New Way: Deployments with Guardrails

Firefly’s Guardrails let you codify rules across four categories:

  • Policy Rules – e.g., “Block any resource without encryption enabled.”

  • Cost Rules – e.g., “Reject any deployment that increases spend by >$100.”

  • Resource Rules – e.g., “Prevent creation of resources in restricted regions like us-west-2.”

  • Tag Rules – e.g., “All resources must have an Environment tag with values dev, stage, or prod.”

You can scope these rules to specific workspaces, branches, or repos, and choose between Strict Block or Flexible Block modes.

Guardrail Creation

When creating a new guardrail, Firefly walks you through a step-by-step wizard as shown below:

creating a new guardrail

This lets you create finely scoped, targeted rules.

Once created, your guardrails appear in the Guardrails tab with their metadata, last modified time, and scope:

Guardrails tab with their metadata

Each rule is timestamped and attributed to the author. This enables complete traceability and audit-readiness.

FAQs

How to Manage Security in Terraform?

To manage security in Terraform, integrate Policy-as-Code (PaC) tools like OPA and Firefly to enforce security policies, such as encryption and access control, during the terraform plan phase. Use best practices like least-privilege IAM roles, secure secret management, and regular security audits to maintain a secure infrastructure.

How to prevent duplicate resources in Terraform?

To avoid duplicate resources in Terraform, ensure that your state file is properly managed and up-to-date. Using modules for reusable configurations, isolating environments with workspaces, and carefully managing resource names can prevent accidental duplication. Always check your state and resource definitions to ensure there are no conflicts across different configurations.

Is Terraform PCI Compliant?

Terraform itself is not inherently PCI-DSS compliant, but it can be used to provision PCI-compliant infrastructure. By applying security controls through Policy-as-Code tools and following PCI-DSS best practices (e.g., encryption, access control, and logging), you can ensure your Terraform-managed infrastructure meets PCI compliance standards.

How to automate infrastructure provisioning using Terraform?

Automate Terraform provisioning by integrating it into CI/CD pipelines (e.g., Jenkins, GitHub Actions). Use scripts to run terraform init, plan, and apply commands automatically when changes are made. This reduces manual effort, ensures consistency, and speeds up infrastructure deployment.

‍