DEV Community

Cover image for 🌟 Terraform Meets DevSecOps: 5 Security Practices You Can’t Afford to Ignore

🌟 Terraform Meets DevSecOps: 5 Security Practices You Can’t Afford to Ignore

💡 Introduction

Welcome, Devs 👋 to the exciting world of Infrastructure as Code (IaC) and automation!

If you’ve been working with Terraform, you already know how powerful it is in spinning up infrastructure in minutes. But with great power comes… well, the need for great security.

In today’s cloud-first job market, security isn’t optional—it’s a core skill. DevOps engineers are now expected to think like DevSecOps engineers, ensuring that every piece of code, every module, and every configuration follows security best practices. Why? Because misconfigured infrastructure is like leaving your front door unlocked—hackers love it.

That’s exactly what we’re diving into today. In this blog, I’ll walk you through Terraform security best practices that will help you safeguard your cloud environments, avoid costly mistakes, and step confidently into the world of DevSecOps.

So, grab your coffee ☕ and let’s get started! 🚀


1. Verify Modules and Providers 🔍

When working with Terraform, your providers and modules are just like external dependencies in application development. And just like you wouldn’t blindly install a random library in your production code, you shouldn’t blindly trust Terraform modules or providers either.

Think of it this way:

  • Providers are your bridge to cloud platforms (AWS, Azure, GCP, etc.).

  • Modules are reusable building blocks that make your Terraform code cleaner and more scalable.

But since both come from external sources, treating them with security-first practices is a must.

✅ Always Pin the Source and Version

Instead of using a vague provider declaration like this:

provider "aws" {
    region = "us-east-1"
}
Enter fullscreen mode Exit fullscreen mode

Lock it down with a clear source and version:

terraform {  
  required_providers {
    aws = {
      source  = "hashicorp/aws"      
      version = "~> 5.98.0"    
    }  
  }
}
Enter fullscreen mode Exit fullscreen mode

This ensures you’re not pulling in an unverified or malicious provider update without realizing it. Think of it as “freezing” dependencies in your app code—predictability and security go hand in hand.

🏢 For Organizations

If you’re working in a team or an enterprise environment, you’ll likely have a private registry in place. That’s a great way to enforce version control and ensure everyone only uses approved and vetted providers/modules.

Some best practices here:

  • Use a filesystem or network mirror to point to an internal artifact repo (similar to how you store container images).

  • Implement a minimal module registry API so teams can share trusted Terraform modules across projects.

  • Review and approve new modules/providers before they go into production pipelines.

In short: treat Terraform providers and modules like you treat code libraries—verify, version, and control them.


2. Don’t Store the State File Locally 🔒

One of the most common mistakes Terraform beginners (and sometimes even pros) make is keeping the state file (terraform.tfstate) on their local machine.

Why is this risky?

  • Your state file contains sensitive data like resource IDs, configuration details, and even secrets.

  • If stored locally or pushed to a public repo (e.g., GitHub), it’s basically like handing over your cloud blueprint to attackers.

  • A leaked state file can lead to account compromise or full infra takeover.

✅ Best Practice: Remote and Encrypted Storage

Instead of keeping state locally, store it in a secure, remote backend that your team and organization can access safely. Make sure it’s encrypted to protect against unauthorized access.

Here’s an example using AWS S3 with encryption and versioning:

terraform {
  backend "s3" {
    bucket       = "my-terraform-state-bucket"
    key          = "my-terraform-state.tfstate"
    region       = "eu-west-1"
    use_lockfile = true
    encrypt      = true
  }
}
Enter fullscreen mode Exit fullscreen mode

🔑 A Note on State Locking

Earlier, Terraform state locking was usually handled with S3 + DynamoDB. But with recent updates, S3 now has the use_lockfile property, making it easier to prevent race conditions when multiple people or pipelines try to update the state at the same time.

🚀 Quick Checklist

  • Never commit state files to GitHub (or any VCS).

  • Always enable encryption and versioning in your remote backend.

  • Use role-based access control (RBAC) to restrict who can read/write to the state.

In short: treat your Terraform state like a password vault—it holds the keys to your entire infrastructure. 🔐


3. Detect Vulnerabilities Early 🕵️‍♂️

Security isn’t something you “add at the end.” With Terraform (and IaC in general), you want to shift security left—meaning you catch issues while writing code, not after deploying it to production.

That’s where static code analysis tools come in. These tools scan your Terraform configuration and flag potential misconfigurations or risky patterns before they ever touch your cloud environment.

🚨 What They Catch

  • Unintended resource exposure (e.g., open security groups, public S3 buckets).

  • Weak encryption setups (like missing encrypt = true).

  • Misconfigurations that could lead to compliance violations.

🛠️ Popular Tools for Terraform Security

  • tfsec: Lightweight, easy-to-use, and integrates well with CI/CD pipelines.

  • Checkov: Great for policy-as-code, compliance checks, and multi-cloud environments.

  • Terrascan: One of the most popular tools, with 500+ built-in policies for Terraform, Kubernetes, and more.

▶️ Example: Scanning with Terrascan

Run this command to analyze your Terraform code:

terrascan scan -f /path/to/terraform/code
Enter fullscreen mode Exit fullscreen mode

It will quickly point out vulnerabilities, risky resources, or compliance issues—saving you hours of debugging and reducing security risks before deployment.

👉 Pro tip: Integrate these scans into your CI/CD pipeline so every commit or PR is automatically checked for vulnerabilities. That way, you’re not just automating infra deployment—you’re automating secure infra deployment. 🚀


4. Apply the Principle of Least Privilege 🔑

When it comes to securing cloud infrastructure, one golden rule stands out: never give more access than necessary.

This is the Principle of Least Privilege (PoLP)—granting only the minimum level of permissions required for a resource or user to function. By following PoLP, you reduce the attack surface and limit potential damage in case of a breach.

Think of it like this: if your friend only needs to borrow your car keys, don’t hand over the keys to your house too.

✅ Why It Matters in Terraform

Terraform often interacts with your cloud provider (AWS, Azure, GCP) to spin up or manage resources. If the IAM roles or service accounts used by Terraform are too permissive, attackers can exploit that access to move laterally, exfiltrate data, or even shut down your infra.

📝 Example: Minimal Policy for Reading Terraform State from S3

Here’s a sample IAM policy for an EC2 instance that only needs to read the Terraform state file:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowListAndLocation",
      "Effect": "Allow",
      "Action": [
        "s3:ListBucket",
        "s3:GetBucketLocation"
      ],
      "Resource": "arn:aws:s3:::your-state-file-bucket"
    },
    {
      "Sid": "AllowReadStateFile",
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::your-state-file-bucket/path/to/your/terraform.tfstate"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Notice how this policy doesn’t allow write/delete actions—just enough access to list and read the state file.

🚀 Best Practices

  • Always scope IAM roles/policies to the specific bucket, object, or resource path.

  • Avoid using wildcards (*) unless absolutely necessary.

  • Regularly audit IAM policies to check for over-privileged accounts.

  • Use tools like AWS IAM Access Analyzer to catch unintended permissions.

In short: lock the doors you don’t need to open. By sticking to PoLP, you make life harder for attackers and safer for your team.


5. Don’t Modify the Terraform State File Manually ⚠️

Your Terraform state file is the single source of truth for your infrastructure. It keeps track of all the resources Terraform manages. Because of that, manually editing the state file is a recipe for disaster.

Why?

  • It can corrupt the state, making Terraform lose track of your infra.

  • It introduces configuration drift—your code and your actual infra won’t match.

  • Worst case, it can lead to unexpected resource destruction (yep, Terraform might just wipe out live resources).

❌ The Wrong Way

Let’s say you have this configuration for an EC2 instance:

provider "aws" {
  region = "us-east-1"
}

resource "aws_instance" "web_server" {
  ami           = "ami-0c55b159f2f12217c"
  instance_type = "t2.micro"
}
Enter fullscreen mode Exit fullscreen mode

Now you decide to rename web_server to app_server. You update the code like this:

provider "aws" {
  region = "us-east-1"
}

resource "aws_instance" "app_server" {
  ami           = "ami-0c55b159f2f12217c"
  instance_type = "t2.micro"
}
Enter fullscreen mode Exit fullscreen mode

When you run terraform plan, Terraform sees this as:

  • Delete: aws_instance.web_server

  • Create: aws_instance.app_server

Meaning—your EC2 instance is destroyed and replaced, causing unnecessary downtime.

✅ The Right Way

Instead of editing the state file or letting Terraform destroy and recreate resources, use the built-in state management command:

terraform state mv aws_instance.web_server aws_instance.app_server
Enter fullscreen mode Exit fullscreen mode

This safely moves the resource address in the state file without touching the actual EC2 instance. Terraform will now recognize the resource under its new name, keeping your infra intact.

🚀 Best Practice Recap

  • Never open and edit terraform.tfstate directly.

  • Always use Terraform CLI (terraform state mv, terraform state rm, terraform import, etc.) to manage resources.

  • Commit your config changes along with proper state moves to avoid drift.

Remember: your state file is like the brain of Terraform—don’t perform surgery on it without the right tools. 🧠🔧


Awesome pointer 🙌 This is the heart of the blog—the hands-on demonstration that ties everything together. I’ll write it step by step in a practical, tutorial-like tone while reinforcing the 5 best practices we already covered. Here’s the draft:


6. Practical Demonstration 🛠️

Now that we’ve gone through the 5 Terraform security best practices, let’s put them into action. Nothing beats seeing these principles applied in a real-world example.

We’ll set up a simple Terraform configuration that:

  • Pins the provider source and version.

  • Stores the state file securely in S3 with encryption + locking.

  • Implements Principle of Least Privilege (PoLP) with IAM policies.

  • Demonstrates state management commands instead of manual edits.

  • Uses Terrascan to detect misconfigurations early.

Ready? Let’s dive in 🚀

To make things easy, I have hosted the code of this project on my Github account, just got the following repo:

Best-Practices project

Navigate inside the best-practices dir.

Step 1: Create main.tf and Specify Provider

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "6.14.1"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}
Enter fullscreen mode Exit fullscreen mode

Here, we’ve pinned the provider source and version (hashicorp/aws at 6.14.1) and specified the region. This ensures consistency and avoids pulling in unverified updates.

Step 2: Add an EC2 Instance with PoLP IAM Policy

Now let’s create an EC2 instance and enforce Principle of Least Privilege by attaching an IAM policy that allows it to only read the Terraform state file from S3.

# IAM Policy Document for S3 State Access
data "aws_iam_policy_document" "s3_state_access_doc" {
  statement {
    sid    = "AllowListAndLocation"
    effect = "Allow"
    actions = [
      "s3:ListBucket",
      "s3:GetBucketLocation"
    ]
    resources = ["arn:aws:s3:::my-pravesh-terraform-state-bucket-2025"]
  }

  statement {
    sid    = "AllowReadStateFile"
    effect = "Allow"
    actions = [
      "s3:GetObject"
    ]
    resources = ["arn:aws:s3:::my-pravesh-terraform-state-bucket-2025/terraform/terraform.tfstate"]
  }
}

# Create the IAM Policy
resource "aws_iam_policy" "s3_state_access_policy" {
  name        = "EC2-S3-State-Read-Policy"
  description = "Policy for EC2 to read the Terraform state file."
  policy      = data.aws_iam_policy_document.s3_state_access_doc.json
}

# Create the IAM Role
resource "aws_iam_role" "ec2_s3_role" {
  name               = "EC2-S3-State-Reader-Role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Action = "sts:AssumeRole",
        Effect = "Allow",
        Principal = {
          Service = "ec2.amazonaws.com"
        },
        Sid = ""
      },
    ],
  })
}

# Attach Policy to Role
resource "aws_iam_role_policy_attachment" "s3_state_attach" {
  role       = aws_iam_role.ec2_s3_role.name
  policy_arn = aws_iam_policy.s3_state_access_policy.arn
}

# Create the Instance Profile
resource "aws_iam_instance_profile" "ec2_s3_profile" {
  name = "EC2-S3-State-Reader-Profile"
  role = aws_iam_role.ec2_s3_role.name
}

# Get Latest Ubuntu AMI
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-*-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

# Create the EC2 Instance
resource "aws_instance" "web_server" {
  ami                  = data.aws_ami.ubuntu.id
  instance_type        = "t2.micro"
  key_name             = "default-ec2" # <<-- Update this with your SSH key
  iam_instance_profile = aws_iam_instance_profile.ec2_s3_profile.name

  tags = {
    Name = "testing-instance"
  }
}
Enter fullscreen mode Exit fullscreen mode

🔑 Notes:

  • Make sure you have an SSH key pair (default-ec2) in us-east-1. If not, create one in the AWS Console.

  • Ensure your default security group in the default VPC allows inbound traffic on Port 22 (SSH).

Step 3: Configure Backend (backend.tf)

terraform {
  backend "s3" {
    bucket       = "my-pravesh-terraform-state-bucket-2025"
    key          = "terraform/terraform.tfstate"
    region       = "us-east-1"
    use_lockfile = true
    encrypt      = true
  }
}
Enter fullscreen mode Exit fullscreen mode

This stores the state file remotely in S3 with encryption + locking enabled.

Step 4: Create the S3 Bucket

aws s3 mb s3://my-pravesh-terraform-state-bucket-2025
Enter fullscreen mode Exit fullscreen mode

Step 5: Initialise and Deploy

terraform init --upgrade
terraform plan
terraform apply --auto-approve
Enter fullscreen mode Exit fullscreen mode

💡 Terraform should show 5 resources to add. Within 2–3 minutes, your infrastructure will be ready!

Step 6: Test PoLP in Action

  • Go to your AWS Console → EC2 Dashboard → Select testing-instance → Click Connect.

  • SSH into the instance.

  • Install AWS CLI:

sudo apt update
sudo apt install awscli -y
Enter fullscreen mode Exit fullscreen mode
  • Try listing objects in the S3 bucket (works ✅):
aws s3 ls s3://my-pravesh-terraform-state-bucket-2025/terraform
Enter fullscreen mode Exit fullscreen mode
  • Try deleting the state file (blocked ❌):
aws s3 rm s3://my-pravesh-terraform-state-bucket-2025/terraform/terraform.tfstate
Enter fullscreen mode Exit fullscreen mode

You’ll get AccessDenied, proving our PoLP IAM policy works! 🔐

Step 7: Scan with Terrascan

Install Terrascan from GitHub.

Then run it against your Terraform code:

terrascan scan -f main.tf
Enter fullscreen mode Exit fullscreen mode

Terrascan will flag any misconfigurations or potential security risks before deployment.


Step 8: Safe State Management

Now let’s rename our EC2 resource without downtime.

Instead of changing web_serverapp_server in code and letting Terraform destroy/recreate the resource, use:

terraform state mv aws_instance.web_server aws_instance.app_server
Enter fullscreen mode Exit fullscreen mode

You can verify the update by downloading the state file from the S3 console. No downtime, no drift—just clean state management. ✅

Step 9: Clean up

Once you are done with the project, make sure to use the following command to delete the resources to avoid incurring charges:

terraform destroy --auto-approve

aws s3 rm s3://my-pravesh-terraform-state-bucket-2025 --recursive
aws s3 rb s3://my-pravesh-terraform-state-bucket-2025
Enter fullscreen mode Exit fullscreen mode

🎯 And that’s it!


🏁 Conclusion

In this project, we walked through five essential Terraform security best practices and then implemented them in a hands-on demonstration. From enabling state file encryption, configuring remote backends, enforcing version pinning, and using terrascan for security scanning, to applying the Principle of Least Privilege (PLoP) with IAM roles and policies — we covered a complete workflow to secure Terraform in production-grade environments.

Security is never a one-time task; it’s an ongoing process. As your infrastructure grows, make sure you constantly revisit and update your security practices to minimize risks. With Terraform, it’s not just about provisioning resources — it’s about doing so securely, scalably, and responsibly.

If you found this guide useful, feel free to connect with me and check out more of my work 👇

🔗 Connect with me

👋 Adios, see you in next one!

Top comments (0)