/opaterraform

Open Policy Agent with Terraform version 0.12.5

Primary LanguageHCL

opaterraform

Open Policy Agent with Terraform version 0.12.5

While trying to follow the terraform example from OPA(Open Policy Agent) documentation (https://www.openpolicyagent.org/docs/v0.10.7/terraform/) faced lot's of issue because of terraform version upgrade.

Step 1 : Create and save a Terraform plan

create main.tf file and generate the terraform plan output file

terraform plan --out -no-color tfplan.output

Step 2 : Convert the Terraform plan into JSON (This Json format is different from one generated by tfplan)

terraform show -json tfplan.output | jq ".resource_changes | map ({(.type|tostring) : .}) | add" > tfplan.json

terraform show -json tfplan.output | jq ".resource_changes | map ({(.address|tostring) : .}) | add" > tfplan.json

tfplan.json

{
  "aws_autoscaling_group": {
    "address": "aws_autoscaling_group.my_asg",
    "mode": "managed",
    "type": "aws_autoscaling_group",
    "name": "my_asg",
    "provider_name": "aws",
    "change": {
      "actions": [
        "create"
      ],
      "before": null,
      "after": {
        "availability_zones": [
          "us-west-1a"
        ],
        "desired_capacity": 4,
        "enabled_metrics": null,
        "force_delete": true,
        "health_check_grace_period": 300,
        "health_check_type": "ELB",
        "initial_lifecycle_hook": [],
        "launch_configuration": "my_web_config",
        "launch_template": [],
        "max_size": 5,
        "metrics_granularity": "1Minute",
        "min_elb_capacity": null,
        "min_size": 1,
        "mixed_instances_policy": [],
        "name": "my_asg",
        "name_prefix": null,
        "placement_group": null,
        "protect_from_scale_in": false,
        "suspended_processes": null,
        "tag": [],
        "tags": null,
        "termination_policies": null,
        "timeouts": null,
        "wait_for_capacity_timeout": "10m",
        "wait_for_elb_capacity": null
      },
      "after_unknown": {
        "arn": true,
        "availability_zones": [
          false
        ],
        "default_cooldown": true,
        "id": true,
        "initial_lifecycle_hook": [],
        "launch_template": [],
        "load_balancers": true,
        "mixed_instances_policy": [],
        "service_linked_role_arn": true,
        "tag": [],
        "target_group_arns": true,
        "vpc_zone_identifier": true
      }
    }
  },
  "aws_instance": {
    "address": "aws_instance.web",
    "mode": "managed",
    "type": "aws_instance",
    "name": "web",
    "provider_name": "aws",
    "change": {
      "actions": [
        "create"
      ],
      "before": null,
      "after": {
        "ami": "ami-09b4b74c",
        "credit_specification": [],
        "disable_api_termination": null,
        "ebs_optimized": null,
        "get_password_data": false,
        "iam_instance_profile": null,
        "instance_initiated_shutdown_behavior": null,
        "instance_type": "t2.micro",
        "monitoring": null,
        "source_dest_check": true,
        "tags": null,
        "timeouts": null,
        "user_data": null,
        "user_data_base64": null
      },
      "after_unknown": {
        "arn": true,
        "associate_public_ip_address": true,
        "availability_zone": true,
        "cpu_core_count": true,
        "cpu_threads_per_core": true,
        "credit_specification": [],
        "ebs_block_device": true,
        "ephemeral_block_device": true,
        "host_id": true,
        "id": true,
        "instance_state": true,
        "ipv6_address_count": true,
        "ipv6_addresses": true,
        "key_name": true,
        "network_interface": true,
        "network_interface_id": true,
        "password_data": true,
        "placement_group": true,
        "primary_network_interface_id": true,
        "private_dns": true,
        "private_ip": true,
        "public_dns": true,
        "public_ip": true,
        "root_block_device": true,
        "security_groups": true,
        "subnet_id": true,
        "tenancy": true,
        "volume_tags": true,
        "vpc_security_group_ids": true
      }
    }
  },
  "aws_launch_configuration": {
    "address": "aws_launch_configuration.my_web_config",
    "mode": "managed",
    "type": "aws_launch_configuration",
    "name": "my_web_config",
    "provider_name": "aws",
    "change": {
      "actions": [
        "create"
      ],
      "before": null,
      "after": {
        "associate_public_ip_address": false,
        "enable_monitoring": true,
        "ephemeral_block_device": [],
        "iam_instance_profile": null,
        "image_id": "ami-09b4b74c",
        "instance_type": "t2.micro",
        "name": "my_web_config",
        "name_prefix": null,
        "placement_tenancy": null,
        "security_groups": null,
        "spot_price": null,
        "user_data": null,
        "user_data_base64": null,
        "vpc_classic_link_id": null,
        "vpc_classic_link_security_groups": null
      },
      "after_unknown": {
        "ebs_block_device": true,
        "ebs_optimized": true,
        "ephemeral_block_device": [],
        "id": true,
        "key_name": true,
        "root_block_device": true
      }
    }
  }
}

Step 3 : Write the OPA policy to check the plan

Modified the existing policy file to match as per the updated json format. I tested my update policy file here (https://play.openpolicyagent.org/)

terraform.rego

package terraform.analysis

import input as tfplan

########################
# Parameters for Policy
########################

# acceptable score for automated authorization
blast_radius = 10

# weights assigned for each operation on each resource-type
weights = {
    "aws_autoscaling_group": {"delete": 100, "create": 10, "modify": 1},
    "aws_instance": {"delete": 10, "create": 1, "modify": 1}
}

# Consider exactly these resource types in calculations
resource_types = {"aws_autoscaling_group", "aws_instance", "aws_iam", "aws_launch_configuration"}

#########
# Policy
#########

# Authorization holds if score for the plan is acceptable and no changes are made to IAM
default authz = false
authz {
    score < blast_radius
    not touches_iam
}

# Compute the score for a Terraform plan as the weighted sum of deletions, creations, modifications
score = s {
    all := [ x |
            crud := weights[resource_type];
            del := crud["delete"] * num_deletes[resource_type];
            new := crud["create"] * num_creates[resource_type];
            mod := crud["modify"] * num_modifies[resource_type];
            x := del + new + mod
    ]
    s := sum(all)
}

# Whether there is any change to IAM
touches_iam {
    all := instance_names["aws_iam"]
    count(all) > 0
}

####################
# Terraform Library
####################

# list of all resources of a given type
instance_names[resource_type] = all {
    resource_types[resource_type]
    all := [name |
        tfplan[name] = _
        startswith(name, resource_type)
    ]
}

# number of deletions of resources of a given type
num_deletes[resource_type] = num {
    resource_types[resource_type]
    all := instance_names[resource_type]
    deletions := [name | name := all[_]; obj := tfplan[name]; obj["change"]["actions"][_] == "delete"]
    num := count(deletions)
}

# number of creations of resources of a given type
num_creates[resource_type] = num {
    resource_types[resource_type]
    all := instance_names[resource_type]
    creates := [name | all[_] = name; obj := tfplan[name]; obj["change"]["actions"][_] == "create"]
    num := count(creates)
}

# number of modifications to resources of a given type
num_modifies[resource_type] = num {
    resource_types[resource_type]
    all := instance_names[resource_type]
    modifies := [name | name := all[_]; obj := tfplan[name]; obj["change"]["actions"][_] == "update"]
    num := count(modifies)
}

Step 4 : Evaluate the OPA policy on the Terraform plan

Opa Policy evaluation :

opa eval --data terraform.rego --input tfplan.json "data.terraform.analysis.authz"

Output :

{
  "result": [
    {
      "expressions": [
        {
          "value": false,
          "text": "data.terraform.analysis.authz",
          "location": {
            "row": 1,
            "col": 1
          }
        }
      ]
    }
  ]
}

Opa Policy evaluation :

opa eval --data terraform.rego --input tfplan.json "data.terraform.analysis.score"

Output :

{
  "result": [
    {
      "expressions": [
        {
          "value": 11,
          "text": "data.terraform.analysis.score",
          "location": {
            "row": 1,
            "col": 1
          }
        }
      ]
    }
  ]
}