Sentinel Policy To Evaluate Terraform Configuration for Azure NSG Ruleset

Wondering if anyone has written or experimented with creating a policy for Azure NSGs? Let’s say I want to create Sentinel policy which will evaluate TF configuration for an existence of series of rules in TF (Azure NSG). How one could accomplish it ? Any thoughts?

Hi Alex,

Unfortunately I don’t have a working example that I can share but I think the best way to determine this would be to run policy against the tfplan instead of the tfconfig. Reason being, if you are using modules that abstract away the complexity of creating NSGs then the rule is not reflected in the tfconfig and therefore you will be unable to evaluate successfully.

If I was to begin with defining a policy that caters to your requirements I would probably start with identifying the properties for a security_rule resource and mapping this back to a list(s) of allowed parameters:

SUPPORTED_PORTS = [ “22”, “3389” ]

Then I would write a function that loops through each security_rule resource within the tfplan and check if the property that has been set (i.e. destination_port_range) is listed in SUPPORTED_PORTS and then depending on the evaluation status of the rule return success or failure (i.e. true or false).

Are you able to share more information regarding the checks that you would like to perform? If you can, I could possibly provide a sample of one or two rules that could get you started :slight_smile:

Hey Ryan,

Thanks so much for reaching out! What we’re trying to accomplish is to prepare the workspace in TFE to be compliant with Sentinel policies our team sets forward. So, one of the rules would some sort of contains_rule logic which will check the NSG ruleset being pushed out, for the existence of block any-any rule with low priority. Example:

File: security_group.tf

###############################################################################
# Security Group Configuration
###############################################################################

resource "azurerm_network_security_group" "capam-awx" {
  name                          = "awx-nsg"
  location                      = "${var.deployment_location}"
  resource_group_name           = "${var.resource_group_name}"

  security_rule {
    name                        = "HTTP"
    description                 = "HTTP access to AWX"
    priority                    = 200
    direction                   = "Inbound"
    access                      = "Allow"
    protocol                    = "Tcp"
    source_port_range           = "*"
    destination_port_range      = "80"
    source_address_prefix       = "*"
    destination_address_prefix  = "*"
  }

  security_rule {
    name                        = "HTTPS"
    description                 = "HTTPS access to AWX"
    priority                    = 201
    direction                   = "Inbound"
    access                      = "Allow"
    protocol                    = "Tcp"
    source_port_range           = "*"
    destination_port_range      = "443"
    source_address_prefix       = "*"
    destination_address_prefix  = "*"
  }
  security_rule {
    name                        = "SSH"
    description                 = "SSH access to AWX"
    priority                    = 202
    direction                   = "Inbound"
    access                      = "Allow"
    protocol                    = "Tcp"
    source_port_range           = "*"
    destination_port_range      = "22"
    source_address_prefix       = "*"
    destination_address_prefix  = "*"
  }
  security_rule {
    name                        = "deny-all-inbound-traffic"
    description                 = "Deny All Inbound Access"
    priority                    = 4096
    direction                   = "Inbound"
    access                      = "Deny"
    protocol                    = "Tcp"
    source_port_range           = "*"
    destination_port_range      = "*"
    source_address_prefix       = "0.0.0.0"
    destination_address_prefix  = "0.0.0.0"
  }
}

So, in a way if DevOps teams wants to deploy a resource to Azure with NSG, we want to make sure that they’re compliant and not allowing liberal ingress rules. We want to force them to be as granular as we can get.

Thanks a metric ton!! Any examples or hints are truly appreciated.

Alex

Hi Alex,

I have taken a look at some of the examples that are available and refactored some of the example functions to be able to evaluate single values. In the following example, I am using a simple function (validate_property_by_value) to iterate through all Azure resources (azurerm_network_security_rule) and check to see if the value (’*’) of a given property (destination_address_prefix) evaluates as either true or false. If the evaluation is true the rule will pass.

As this is a simple example I am only evaluating a single property. To achieve your requirement you will most likely need to check multiple properties in a single evaluation which is a little more complicated but definitely achievable if you follow the example below and expand on it to evaluate multiple properties that are set for the Azure resource.

import "tfplan"
import "strings"

# -----------------------------------------------------------------
# FUNC - Get all Resources by Type
# -----------------------------------------------------------------
get_resources_by_type = func(type) {

  resources = {}

  # Iterate over all modules in the tfplan import
  for tfplan.module_paths as path {
    # Iterate over the named resources of desired type in the module
    for tfplan.module(path).resources[type] else {} as name, instances {
      # Iterate over resource instances
      for instances as index, r {

        # Get the address of the instance
        if length(path) == 0 {
          # root module
          address = type + "." + name + "[" + string(index) + "]"
        } else {
          # non-root module
          address = "module." + strings.join(path, ".module.") + "." +
                    type + "." + name + "[" + string(index) + "]"
        }

        # Add the instance to resources map, setting the key to the address
        resources[address] = r
      }
    }
  }

  return resources
}

# -----------------------------------------------------------------
# FUNC - Validate the Value of a Property for a given Resource Type
# -----------------------------------------------------------------
validate_property_by_value = func(type, property, value) {

  validated = false

  # Get all resource instances of the specified type
  resources = get_resources_by_type(type)

  # Loop through the resource instances
  for resources as address, r {
    # Skip resource instances that are being destroyed
    # to avoid unnecessary policy violations
    if length(r.diff) == 0 {
      print("Skipping resource", address, "that is being destroyed.")
      continue
    }

    # Determine if the property is computed
    if r.diff[property].computed else false is true {
      print("Resource", address, "has property", property,
            "that is computed.")
      # If you want computed values to cause the policy to fail,
      # uncomment the next line.
      # validated = false
    } else {
      # Validate that each instance has allowed value
      print("Resource", address, "has property", property, "with value", r.applied[property])
      if (r.applied[property] else "") is value {
        validated = true
      }
    }

  }
  return validated
}

# ------------------------------------
# MAIN
# ------------------------------------  
main = rule {
    validate_property_by_value("azurerm_network_security_rule", "destination_address_prefix", "*")
}

I foresee some challenges with the Priority property because you are looking for any integer value that is between 100 and 65500 which is a massive range. I can’t really think of a straightforward way of determining which number is supported and which is not from a corporate governance perspective?

Thank you so much Ryan, for your help. Priority should not matter in our environment, as long as we can validate that “any-any” rule does not exist in NSG ruleset. I will test it and post the results in this threat. If all works out, I hope the example you provided will help other people as well. Thanks again!!!