AWS: Scanning for administrative polocies

TFE Version 0.14.11

I am trying to enumerate aws_iam_policy resources that have admin permissions (action:* / resource:*) in a statement. Surprised I couldnt find examples online. This turned out to be quite a bit of unexpected work.

The policy by and large works but has inconsistent behaviour, run the specualtive plan I get an exception [1] when policy set is executed (it obviously has a null reference/undefined issue trying to enumerate a non-enumerable (for resource as res, r line 82). re-run and it picks up the offending policy without issue, debug output [2]

# This policy checks for the occurrence of wildcard (*) when present in both actions and conditions keys of a policy statement or statements

import "tfplan-functions" as plan
import "json"
import "strings"
import "types"

# Forbidden IAM actions #
param forbidden_action default ["*"]
debug = 1

pattern = "^\\*$"

# Forbidden IAM actions
#param forbidden_resource default ["*"]

# Get all IAM policy document data sources
allIAMPolicyDocs = plan.find_resources("aws_iam_policy")

# Filter to IAM policy documents with violations
validated = false
policy_failures = func(x)  {
  #actionbad = 0
  #resourcebad = 0
  for allIAMPolicyDocs as address, d {

  actionbad = 0
  resourcebad = 0
  #plan.find_all_datasources
  #print("casted value: ",plan.to_string(d))
  # Find the statements of the current policy doc
  statements = plan.find_blocks(d, "address")
  
  ###{"Statement":[{"Action":["*"],"Effect":"Allow","Resource":"*"}],"Version":"2012-10-17"}###

 if debug is 1 {
    print("-- debug -- about to try unmarshall from json: ",d.change.after.policy)
  } 

 object = json.unmarshal(d.change.after.policy)
 if debug is 1 {
    print("-- debug -- unmarshalled policy value: ",object)
    print("object length: ", length(object))
    print("object statement: ", length(object.Statement))
    print("object statement type: ", types.type_of(object.Statement))

  }


if types.type_of(object.Statement) in ["list","map"] {
  action = "null"
  resource = "null"
  if types.type_of(object.Statement) is "list" {
      if debug is 1 {print("type:", types.type_of(object.Statement), "found for: ", object.Statement)}
      action = object.Statement[0].Action
      resource = object.Statement[0].Resource
      /* */
  if types.type_of(action) is "string" {
     if debug is 1 { print("action string: ", action) }
     if action matches pattern {
      actionbad = 1
      if debug is 1 { print("-- list debug -- bad action found as string") }
    }
    } else {
       for action as act, a {
        if debug is 1 { print("action in loop: ", a) }
        if a matches pattern {
          actionbad = 1
          if debug is 1 { print("-- list debug -- bad action found in loop") }
          test = true
        }
      }
    }

  if types.type_of(resource) is "string" {
    if debug is 1 { print("resource string: ", resource) }
    if resource matches pattern {
      resourcebad = 1
      if debug is 1 { print("-- list debug -- bad resource found as string") }
    }
  } else {
      if debug is 1 { print("resourcetype : ", types_type_of(resource)) }
      for resource as res, r {
        if debug is 1 { print("resource: ", r) }
        if r matches pattern {
          resourcebad = 1
              if debug is 1 { print("-- list debug -- bad resource found in loop") }
        }
      }
  }


  } else if types.type_of(object.Statement) is "map" {
      
       if debug is 1 {print("type:", types.type_of(object.Statement), "found for: ", object.Statement)}
      action = object.Statement.Action
      resource = object.Statement.Resource
       if debug is 1 {print("action:",action,"|","resource:",resource)}

      /* */
    if types.type_of(action) is "string" {
     if debug is 1 { print("action string: ", action) }
     
     if action matches pattern {
      actionbad = 1
      if debug is 1 { print("-- map debug -- bad action found as string", actionbad) }
    }
    } else {
       for action as act, a {
        if debug is 1 { print("action in loop: ", a) }
        if a matches pattern {
          actionbad = 1
          if debug is 1 { print("-- map debug -- bad action found in loop") }
        }
      }
    }

    if types.type_of(resource) is "string" {
      if debug is 1 { print("resource string: ", resource) }
      if resource matches pattern {
        resourcebad = 1
        if debug is 1 { print("-- map debug -- bad resource found as string", resourcebad) }
      }
    } else {
        for resource as res, r {
          if debug is 1 { print("resource: ", r) }
          if r matches pattern {
            resourcebad = 1
            if debug is 1 { print("-- map debug -- bad resource found in loop") }
          }
        }
    }
  /* */
  
  }

  
  if debug is 1 {   print("actionbad",actionbad,"|","resourcebad",resourcebad) } 
   
  action = object.Statement[0].Action
  resource = object.Statement[0].Resource
} 

   
if debug is 1 {  
  print("-- debug -- ACTION:",action)
  print("-- debug -- RESOURCE:",resource)
    }


  ##################################################################
 
  if actionbad is 1 and resourcebad is 1 {
    validated = false
    print(address ,"has administrative permissions defined. Please see following link - https://docs.aws.amazon.com/config/latest/developerguide/iam-policy-no-statements-with-admin-access.html")
    return true
  }


}
  return false
}

###

test = func(type) {
  return false
}

# Main rule
main = rule {
  #actionbad == 1 and resourcebad == 1
  policy_failures("ro") is true
  
}

[1] An error occurred: 1 error occurred:
* ./restrict-admin-access.sentinel:83:11: unsupported type for looping: undefined

[2] – debug – unmarshalled policy value: {“Statement”: [{“Action”: “", “Effect”: “Allow”, “Resource”: "”, “Sid”: “AllowAdminAccess”} {“Action”: [“account:" "aws-portal:” “savingsplans:" "cur:” “ce:"], “Effect”: “Deny”, “Resource”: "”, “Sid”: “DenyAccessToCostAndBilling”} {“Action”: [“iam:DeletePolicy” “iam:DeletePolicyVersion” “iam:CreatePolicyVersion” “iam:SetDefaultPolicyVersion”], “Effect”: “Deny”, “Resource”: [“arn:aws:iam::123456789012:policy/sdlf_adm_permission_boundary_services_policy”], “Sid”: “DenyPermBoundaryIAMPolicyAlteration”} {“Action”: [“iam:DeleteUserPermissionsBoundary” “iam:DeleteRolePermissionsBoundary”], “Condition”: {“StringEquals”: {“iam:PermissionsBoundary”: “arn:aws:iam::123456789012:policy/sdlf_adm_permission_boundary_services_policy”}}, “Effect”: “Deny”, “Resource”: [“arn:aws:iam::123456789012:user/" "arn:aws:iam::123456789012:role/”], “Sid”: “DenyRemovalOfPermBoundaryFromAnyUserOrRole”} {“Action”: [“iam:PutUserPermissionsBoundary” “iam:PutRolePermissionsBoundary”], “Condition”: {“StringNotEquals”: {“iam:PermissionsBoundary”: “arn:aws:iam::123456789012:policy/sdlf_adm_permission_boundary_services_policy”}}, “Effect”: “Deny”, “Resource”: [“arn:aws:iam::123456789012:user/" "arn:aws:iam::123456789012:role/”], “Sid”: “DenyAccessIfRequiredPermBoundaryIsNotBeingApplied”} {“Action”: [“iam:CreateUser” “iam:CreateRole”], “Condition”: {“StringNotEquals”: {“iam:PermissionsBoundary”: “arn:aws:iam::123456789012:policy/sdlf_adm_permission_boundary_services_policy”}}, “Effect”: “Deny”, “Resource”: [“arn:aws:iam::123456789012:user/" "arn:aws:iam::123456789012:role/”], “Sid”: “DenyUserAndRoleCreationWithOutPermBoundary”} {“Action”: “sts:AssumeRole”, “Effect”: “Deny”, “NotResource”: “arn:aws:iam::123456789012:role/", “Sid”: “DenyAssumeRoleOutsideOfAccount”}], “Version”: “2012-10-17”}
object length: 2
object statement: 7
object statement type: list
type: list found for: [{“Action”: "
”, “Effect”: “Allow”, “Resource”: “", “Sid”: “AllowAdminAccess”} {“Action”: ["account:” “aws-portal:" "savingsplans:” “cur:" "ce:”], “Effect”: “Deny”, “Resource”: “", “Sid”: “DenyAccessToCostAndBilling”} {“Action”: [“iam:DeletePolicy” “iam:DeletePolicyVersion” “iam:CreatePolicyVersion” “iam:SetDefaultPolicyVersion”], “Effect”: “Deny”, “Resource”: [“arn:aws:iam::123456789012:policy/sdlf_adm_permission_boundary_services_policy”], “Sid”: “DenyPermBoundaryIAMPolicyAlteration”} {“Action”: [“iam:DeleteUserPermissionsBoundary” “iam:DeleteRolePermissionsBoundary”], “Condition”: {“StringEquals”: {“iam:PermissionsBoundary”: “arn:aws:iam::123456789012:policy/sdlf_adm_permission_boundary_services_policy”}}, “Effect”: “Deny”, “Resource”: ["arn:aws:iam::123456789012:user/” “arn:aws:iam::123456789012:role/"], “Sid”: “DenyRemovalOfPermBoundaryFromAnyUserOrRole”} {“Action”: [“iam:PutUserPermissionsBoundary” “iam:PutRolePermissionsBoundary”], “Condition”: {“StringNotEquals”: {“iam:PermissionsBoundary”: “arn:aws:iam::123456789012:policy/sdlf_adm_permission_boundary_services_policy”}}, “Effect”: “Deny”, “Resource”: ["arn:aws:iam::123456789012:user/” “arn:aws:iam::123456789012:role/"], “Sid”: “DenyAccessIfRequiredPermBoundaryIsNotBeingApplied”} {“Action”: [“iam:CreateUser” “iam:CreateRole”], “Condition”: {“StringNotEquals”: {“iam:PermissionsBoundary”: “arn:aws:iam::123456789012:policy/sdlf_adm_permission_boundary_services_policy”}}, “Effect”: “Deny”, “Resource”: ["arn:aws:iam::123456789012:user/” “arn:aws:iam::123456789012:role/"], “Sid”: “DenyUserAndRoleCreationWithOutPermBoundary”} {“Action”: “sts:AssumeRole”, “Effect”: “Deny”, “NotResource”: "arn:aws:iam::123456789012:role/”, “Sid”: “DenyAssumeRoleOutsideOfAccount”}]
action string: *
– list debug – bad action found as string
resource string: *
– list debug – bad resource found as string
actionbad 1 | resourcebad 1
– debug – ACTION: *
– debug – RESOURCE: *
aws_iam_policy.adm_permission_boundary_services_policy has administrative permissions defined. Please see following link - iam-policy-no-statements-with-admin-access - AWS Config

./restrict-admin-access.sentinel:171:1 - Rule “main”
Description:
Main rule

Value:
false

@jollyranger : It is difficult to work with the policy attribute of resources like aws_iam_policy because it often ends up being computed. If you are willing to make your Terraform developers use the aws_iam_policy_document data source, you could then write policies like terraform-guides/restrict-iam-policy-actions.sentinel at master · hashicorp/terraform-guides · GitHub that restrict it. The advantage of the aws_iam_policy_document data source is that it breaks up the various pieces of a policy into separate attributes. So, if one of them is computed, the rest can still be evaluated.

Roger Berlind

Thanks, I did experiment with aws_iam_policy_document from the example, which did behave a bit inconsistently when both * was defined for actions and resources, it wouldn’t compute resources in some cases but just denote an empty list.

I managed to get the above working by doing ‘else null’ logic which seems to have done the trick. For now its picking up bad policies whether defined in-line, heredoc style or HCL [1].

@rberlind I’ll let this bed in for a few days and make sure it is stable but I hear what you’re saying around computation and will feedback around scanning the docs otherwise.

I’ve come across this github repo you mention, you don’t know of other repos that have more examples as its certainly not from lack of searching :slight_smile:

[1]


resource "aws_iam_policy" "bad" {
  name        = "bad_policy"
  path        = "/"
  description = "My bad policy"

  # Terraform's "jsonencode" function converts a
  # Terraform expression result to valid JSON syntax.
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
        
          "arn:stuff:here"
        ]
        Effect   = "Allow"
        Resource = [
      
        "*"
        ]

      },
    ]
  })
}

resource "aws_iam_policy" "heredocbad" {
  name   = "heredocbad"
   path   = "/"
  policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": {
    "Effect": "Allow",
    "Action": "*",
    "Resource": ["asdasd:*:sdsd","asasd:**", "*"]
  }
}
POLICY
}
    resource "aws_iam_policy" "example" {
    name   = "example_policy"
    path   = "/"
    policy = data.aws_iam_policy_document.example.json
    }

data "aws_iam_policy_document" "example" {
  statement {
    sid = "4"
    actions   = ["somearn:*:here" ]
    resources =     ["*"]
  }
  statement {
    sid = "5"
    actions   = ["secondarn:*:here"]
    resources =     ["*"]
  }
}