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.
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
}
}
}
}
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)
}
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
}
}
]
}
]
}