DEV Community

Cover image for Day 21: AWS IAM Policy and Governance Setup Using Terraform
Anil KUMAR
Anil KUMAR

Posted on

Day 21: AWS IAM Policy and Governance Setup Using Terraform

Today marks the Day 21 of 30 days of AWS Terraform challeneg Initiative by Piyush Sachdeva. Today we will learn about the AWS IAM Policy and Governance Setup using Terraform.

What is Policy and Governance?

Policy enforces rules to prevent non-compliant actions in AWS:

  • Users without MFA cannot delete resources
  • S3 uploads must use HTTPS (encrypted in transit)
  • Resources like S3 buckets/EC2 must have required tags (e.g., environment=dev)

IAM policies block bad actions before they occur.

If any one tries to delete a S3 bucket with IAM policy not having required permissions for it, then it will block that action preventing deletion of that bucket.

Governance tracks compliance via AWS Config:

  • Monitors resources after creation.
  • Logs compliant/non-compliant status.
  • Stores audit logs in secure S3 bucket.

On the other hand, Governance will not prevent that action but will store everything in a config file inside S3 bucket or any origin source provided.

Ex: If we tried creating a S3 Bucket or an EC2 instance without tags, then IAM policy will prevent that action whereas Governance will not block that action but logs that action as non-complaint status in config.

┌─────────────────────────────────────────────────────┐
│                                                     │
│   1. PREVENTIVE (IAM Policies)                     │
│      → Blocks bad actions BEFORE they happen       │
│      → Example: "Cannot delete S3 without MFA"     │
│                                                     │
│   2. DETECTIVE (AWS Config)                        │
│      → Finds violations AFTER they happen          │
│      → Example: "This bucket is not encrypted"     │
│                                                     │
└─────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Project Architecture Overview:


Policy (Prevent) → AWS Config (Detect) → S3 Audit Logs (Store)

Enter fullscreen mode Exit fullscreen mode

Key Components Created:

  1. Encrypted, versioned S3 bucket for audit logs (public access blocked).
  2. 3 IAM policies for enforcement.
  3. 6 AWS Config rules for compliance monitoring.
  4. IAM roles/users for service access
         ┌──────────────────┐
         │   IAM POLICIES   │  ◄── PREVENT bad actions
         │  • MFA Delete    │
         │  • Encryption    │
         │  • Required Tags │
         └────────┬─────────┘
                  │
                  ▼
         ┌──────────────────┐
         │   AWS CONFIG     │  ◄── DETECT violations
         │   6 Rules        │
         │  (Compliance)    │
         └────────┬─────────┘
                  │
                  ▼
         ┌──────────────────┐
         │    S3 BUCKET     │  ◄── STORE logs
         │  🔒 Encrypted    │
         │  🔒 Versioned    │
         └──────────────────┘
Enter fullscreen mode Exit fullscreen mode

Project Objectives:

Policy Creation: Implement IAM policies to enforce security best practices
Governance Setup: Configure AWS Config for continuous compliance monitoring
Resource Tagging: Demonstrate tagging strategies for resource management
S3 Security: Apply encryption, versioning, and access controls
Compliance Monitoring: Track configuration changes and detect violations

Terraform Implementation Breakdown:

day21/
├── provider.tf       # AWS provider configuration
├── variables.tf      # Input variables
├── main.tf          # S3 bucket and shared resources
├── iam.tf           # IAM policies and roles
├── config.tf        # AWS Config recorder and rules
├── outputs.tf       # Output values
└── README.md        # This file
Enter fullscreen mode Exit fullscreen mode

provider.tf — AWS Provider
What it does: Tells Terraform to use AWS.

variables.tf — Inputs
What it does: Makes the code reusable

main.tf:

In this file, we will be creating S3 Bucket for Audit Logs.

# S3 Bucket to store AWS Config history
resource "aws_s3_bucket" "config_bucket" {
  bucket        = "${var.project_name}-config-bucket-${random_string.suffix.result}"
  force_destroy = true

  tags = {
    Name        = "${var.project_name}-config-bucket"
    Environment = "governance"
    Purpose     = "aws-config-storage"
    ManagedBy   = "terraform"
  }
}

resource "random_string" "suffix" {
  length  = 6
  special = false
  upper   = false
}

# Enable versioning on Config bucket
resource "aws_s3_bucket_versioning" "config_bucket_versioning" {
  bucket = aws_s3_bucket.config_bucket.id
  versioning_configuration {
    status = "Enabled"
  }
}

# Enable encryption on Config bucket
resource "aws_s3_bucket_server_side_encryption_configuration" "config_bucket_encryption" {
  bucket = aws_s3_bucket.config_bucket.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

# Block public access to Config bucket
resource "aws_s3_bucket_public_access_block" "config_bucket_public_access" {
  bucket = aws_s3_bucket.config_bucket.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# S3 Bucket Policy for Config
resource "aws_s3_bucket_policy" "config_bucket_policy" {
  bucket = aws_s3_bucket.config_bucket.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "AWSConfigBucketPermissionsCheck"
        Effect = "Allow"
        Principal = {
          Service = "config.amazonaws.com"
        }
        Action   = "s3:GetBucketAcl"
        Resource = aws_s3_bucket.config_bucket.arn
      },
      {
        Sid    = "AWSConfigBucketExistenceCheck"
        Effect = "Allow"
        Principal = {
          Service = "config.amazonaws.com"
        }
        Action   = "s3:ListBucket"
        Resource = aws_s3_bucket.config_bucket.arn
      },
      {
        Sid    = "AWSConfigBucketPutObject"
        Effect = "Allow"
        Principal = {
          Service = "config.amazonaws.com"
        }
        Action   = "s3:PutObject"
        Resource = "${aws_s3_bucket.config_bucket.arn}/*"
        Condition = {
          StringEquals = {
            "s3:x-amz-acl" = "bucket-owner-full-control"
          }
        }
      },
      {
        Sid       = "DenyInsecureTransport"
        Effect    = "Deny"
        Principal = "*"
        Action    = "s3:*"
        Resource = [
          aws_s3_bucket.config_bucket.arn,
          "${aws_s3_bucket.config_bucket.arn}/*"
        ]
        Condition = {
          Bool = {
            "aws:SecureTransport" = "false"
          }
        }
      }
    ]
  })

  depends_on = [aws_s3_bucket_public_access_block.config_bucket_public_access]
}

Enter fullscreen mode Exit fullscreen mode

For the above S3 bucket creation, we have used random_string of suffix for unique name of S3 bucket and then we have enabled Versioning, Encryption (Server-Side-encryption) and blocked public access for that bucket.

Also, we have added a S3 Bucket Policy for Config so that config should be able to write logs to this bucket without which config will not be able to write logs to this bucket.

iam.tf:

In this file, we will be creating IAM Policy Examples following which the failure of the implementation should result in the errors.

# ------------------------------------------------------------------------------
# 1. IAM Policy Examples
# ------------------------------------------------------------------------------

# Create a custom IAM policy that enforces MFA for deleting S3 objects
resource "aws_iam_policy" "mfa_delete_policy" {
  name        = "${var.project_name}-mfa-delete-policy"
  description = "Policy that requires MFA to delete S3 objects"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid      = "DenyDeleteWithoutMFA"
        Effect   = "Deny"
        Action   = "s3:DeleteObject"
        Resource = "*"
        Condition = {
          BoolIfExists = {
            "aws:MultiFactorAuthPresent" = "false"
          }
        }
      }
    ]
  })
}

# IAM Policy: Enforce encryption in transit for S3
resource "aws_iam_policy" "enforce_s3_encryption_transit" {
  name        = "${var.project_name}-s3-encryption-transit"
  description = "Deny S3 actions without encryption in transit"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid      = "DenyUnencryptedObjectUploads"
        Effect   = "Deny"
        Action   = "s3:PutObject"
        Resource = "*"
        Condition = {
          Bool = {
            "aws:SecureTransport" = "false"
          }
        }
      }
    ]
  })
}

# IAM Policy: Require tagging for resource creation
resource "aws_iam_policy" "require_tags_policy" {
  name        = "${var.project_name}-require-tags"
  description = "Require specific tags when creating resources"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "RequireTagsOnEC2"
        Effect = "Deny"
        Action = [
          "ec2:RunInstances"
        ]
        Resource = "arn:aws:ec2:*:*:instance/*"
        Condition = {
          StringNotLike = {
            "aws:RequestTag/Environment" = ["dev", "staging", "prod"]
          }
        }
      },
      {
        Sid    = "RequireOwnerTag"
        Effect = "Deny"
        Action = [
          "ec2:RunInstances"
        ]
        Resource = "arn:aws:ec2:*:*:instance/*"
        Condition = {
          "Null" = {
            "aws:RequestTag/Owner" = "true"
          }
        }
      }
    ]
  })
}

# IAM User for demonstration
resource "aws_iam_user" "demo_user" {
  name = "${var.project_name}-demo-user"
  path = "/governance/"

  tags = {
    Environment = "demo"
    Purpose     = "governance-training"
  }
}

# Attach MFA delete policy to demo user
resource "aws_iam_user_policy_attachment" "demo_user_mfa" {
  user       = aws_iam_user.demo_user.name
  policy_arn = aws_iam_policy.mfa_delete_policy.arn
}

# ------------------------------------------------------------------------------
# 2. IAM Role for AWS Config Service
# ------------------------------------------------------------------------------

# IAM Role for AWS Config Service
resource "aws_iam_role" "config_role" {
  name = "${var.project_name}-config-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "config.amazonaws.com"
        }
      }
    ]
  })
}

# Attach managed policy to Config Role
resource "aws_iam_role_policy_attachment" "config_policy_attach" {
  role       = aws_iam_role.config_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWS_ConfigRole"
}

# Additional policy for Config to write to S3
resource "aws_iam_role_policy" "config_s3_policy" {
  name = "${var.project_name}-config-s3-policy"
  role = aws_iam_role.config_role.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "s3:GetBucketVersioning",
          "s3:PutObject",
          "s3:GetObject"
        ]
        Resource = [
          aws_s3_bucket.config_bucket.arn,
          "${aws_s3_bucket.config_bucket.arn}/*"
        ]
      }
    ]
  })
}
Enter fullscreen mode Exit fullscreen mode

In the first block, we will create a custom IAM policy that enforces MFA for deleting S3 objects, If MFA is absent, it will deny deletion of that resource.

In the second block, we will enforce encryption in transit for S3, so whenever you are trying to upload an object to S3 over http without secure, it will deny that request.

In the third block, It requires tagging for resource creation. When users tried to create any resource without tagging, it will fail. We have created this in such a way that if anyone tries to create a EC2 instance, it should have mandatory tags of Environment=["dev", "staging", "prod"] and have Owner tag too.

In the fourth block, It shows the creation of IAM User and it attaches the above MFA delete policy to demo user we have created.

In the fifth block, It creates an IAM Role for AWS Config Service and attaching a policy for that after which we create an additional policy too for writing config logs to S3 bucket.

config.tf:

The above block creates Config recorder + 6 compliance rules.

# ------------------------------------------------------------------------------
# AWS Config Recorder and Delivery Channel
# ------------------------------------------------------------------------------

# AWS Config Recorder
resource "aws_config_configuration_recorder" "main" {
  name     = "${var.project_name}-recorder"
  role_arn = aws_iam_role.config_role.arn

  recording_group {
    all_supported                 = true
    include_global_resource_types = true
  }
}
Enter fullscreen mode Exit fullscreen mode

This records every configuration change in your AWS account.

# AWS Config Delivery Channel
resource "aws_config_delivery_channel" "main" {
  name           = "${var.project_name}-delivery-channel"
  s3_bucket_name = aws_s3_bucket.config_bucket.bucket
  depends_on     = [aws_config_configuration_recorder.main]
}

# Start the Config Recorder
resource "aws_config_configuration_recorder_status" "main" {
  name       = aws_config_configuration_recorder.main.name
  is_enabled = true
  depends_on = [aws_config_delivery_channel.main]
}
Enter fullscreen mode Exit fullscreen mode

The above block creates a Config Delivery channel which records all the config related information directly to the S3 bucket and starting the config recorder.

# Config Rule: Ensure S3 buckets do not allow public write
resource "aws_config_config_rule" "s3_public_write_prohibited" {
  name = "s3-bucket-public-write-prohibited"

  source {
    owner             = "AWS"
    source_identifier = "S3_BUCKET_PUBLIC_WRITE_PROHIBITED"
  }

  depends_on = [aws_config_configuration_recorder.main]
}

# Config Rule: Ensure S3 buckets have encryption enabled
resource "aws_config_config_rule" "s3_encryption" {
  name = "s3-bucket-server-side-encryption-enabled"

  source {
    owner             = "AWS"
    source_identifier = "S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED"
  }

  depends_on = [aws_config_configuration_recorder.main]
}

# Config Rule: Ensure S3 buckets block public access
resource "aws_config_config_rule" "s3_public_read_prohibited" {
  name = "s3-bucket-public-read-prohibited"

  source {
    owner             = "AWS"
    source_identifier = "S3_BUCKET_PUBLIC_READ_PROHIBITED"
  }

  depends_on = [aws_config_configuration_recorder.main]
}

# Config Rule: Ensure EBS volumes are encrypted
resource "aws_config_config_rule" "ebs_encryption" {
  name = "encrypted-volumes"

  source {
    owner             = "AWS"
    source_identifier = "ENCRYPTED_VOLUMES"
  }

  depends_on = [aws_config_configuration_recorder.main]
}

# Config Rule: Ensure EC2 instances have required tags
resource "aws_config_config_rule" "required_tags" {
  name = "required-tags"

  source {
    owner             = "AWS"
    source_identifier = "REQUIRED_TAGS"
  }

  input_parameters = jsonencode({
    tag1Key = "Environment"
    tag2Key = "Owner"
  })

  scope {
    compliance_resource_types = [
      "AWS::EC2::Instance",
      "AWS::S3::Bucket"
    ]
  }

  depends_on = [aws_config_configuration_recorder.main]
}

# Config Rule: Ensure root account has MFA enabled
resource "aws_config_config_rule" "root_mfa_enabled" {
  name = "root-account-mfa-enabled"

  source {
    owner             = "AWS"
    source_identifier = "ROOT_ACCOUNT_MFA_ENABLED"
  }

  depends_on = [aws_config_configuration_recorder.main]
}
Enter fullscreen mode Exit fullscreen mode

The above block contains all the config rules which we have created of the config to detect any complaint issues.

It contains mainly 6 complaince blocks:

Rule Source Identifier Purpose
S3 Public Write Prohibited S3_BUCKET_PUBLIC_WRITE_PROHIBITED Block public writes
S3 Encryption Enabled S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED Enforce SSE s
S3 Public Read Prohibited S3_BUCKET_PUBLIC_READ_PROHIBITED Block public reads
EBS Volumes Encrypted VOLUME_EBS_ENCRYPTED Encrypted volumes
Required Tags Custom parameters Tag validation
Root MFA Enabled ROOT_ACCOUNT_MFA_ENABLED Root account security

output.tf

It shows important information after deployment

output "config_rules" {
  value = [list of all rule names]
}
output "config_recorder_status" {
  value = true  # Recorder is running
}
Enter fullscreen mode Exit fullscreen mode

Verification & Testing:

Now trigger the creation of all the resources using terraform commands.

terraform init
terraform plan
terraform apply
Enter fullscreen mode Exit fullscreen mode

After terraform apply, we can see 23 resources created getting created.

  1. IAM policies
  2. S3 bucket with security settings
  3. Config recorder
  4. 6 Config rules

✅ S3 Bucket Properties

✅ Versioning enabled - ✅ AES256 encryption - ✅ Public access blocked - ✅ AWS logs folder created

✅ AWS Config Dashboard:

We could see the resources with the status as complaint or non-complaint.

Compliant: 4 rules ✓
Non-Compliant: 1 resource (missing tags) ✗
Enter fullscreen mode Exit fullscreen mode

Test Compliance Detection:

Created untagged S3 bucket → Detected as non-compliant after 2 minutes.

Check Config -> Wait 2–3 minutes, then:

Go to AWS Config → Rules
Click “s3-bucket-server-side-encryption-enabled”
See new bucket as NON-COMPLIANT (red)
Explain: Config detected the violation automatically!

Key Learnings

Policy vs Config Rules:

IAM Policy: Prevents actions before execution
Config Rules: Detects issues after creation

Terraform Best Practices:

Use random_string for unique names
Explicit dependencies between resources
Reference Terraform Registry for resource syntax

Security Tip:

Always use the Principle of Least Privilege. While we used Resource: "*" for this lab, in production, always restrict policies to specific ARNs!

Conclusion:

See the below video for more understanding about AWS IAM Policy and Governance Setup using Terraform.

Top comments (0)