Wednesday, 19 September 2018

Implementing Security Compliance as Code in Terraform

Infrastructure as Code (IaC) tools like Terraform have enabled efficient, accountable and rapid infrastructure development and deployment in the cloud. Without the overhead of delivering, installing and maintaining hardware, the speed at which teams can build and release IT solutions brings measurable value to their organisation.

Working in the security industry, we need to keep up with this rapid deployment methodology, and insert ourselves into the development pipeline to ensure architects/developers are releasing infrastructure that meets our best practices.

This blog post details one way that we as security practitioners can automate compliance with technical security policy as code in Terraform. This method utilises a PowerShell script I wrote called TFCheck.

If you would like to learn more about Terraform, please check out their website.

How does it work?

TFcheck writes the output of the Terraform show command to config.out for parsing. 

When TFcheck parses config.out, it performs string manipulation to convert the config.out into compressed json format. From here TFcheck converts the Json string into Powershell objects using the ConvertFrom-Json function.

All configuration parameters are now nested Powershell objects, which are easy to reference and validate!

Simply write your rules within TFCheck.ps1 and then run it from within your Terraform project directory. Alternatively, you can automate it as a task in various development pipeline tools.

Rules

The world is your oyster. Here are some examples in Azure to get you started.

Internet Facing Services
Do you have certain ports/services you never want exposed to the internet? (e.g. RDP, SSH).
Write a rule to check all network rules in the Terraform config for port 3389, 1433 or 22 permitted from 'Internet' or '*'.

###Network Security Group Checks###
Write-Host "`n***Checking Network Security Group (NSG) rules***"
$NsgErrors = 0
#Loops through Resources and checks security conditions
ForEach ($Name in $Objects.psobject.properties.name) { #Gets Resources
 $ObjectType = ($Objects.$Name | Get-Member -Type NoteProperty).name -Replace "(\[0m)","" #Replace is required to cleanup the first line in the file (which has some prepending characters)
 if ($ObjectType -eq "azurerm_network_security_group"){ #Filters for NSG objects
  $SecurityRuleNumber = try{[int]::parse($Objects.$Name.'security_rule'.'#')}catch{} #Gets the total number of NSG security rules per Resource
  $Rules = 0..$SecurityRuleNumber #Creates an array of zero to the total number of rules per resource, which is used to loop through each rule
  ForEach ($Rule in $Rules){ #Loops through each rule in a resource
   $OutputName = $Objects.$Name.'security_rule'.$Rule.'name'
   #Rule 1 - Internet or * Remote Desktop#
   if ((($Objects.$Name.'security_rule'.$Rule.'source_address_prefix' -eq 'Internet') -or ($Objects.$Name.'security_rule'.$Rule.'source_address_prefix' -eq "`*")) -and ($Objects.$Name.'security_rule'.$Rule.'destination_port_range' -eq '3389') -and ($Objects.$Name.'security_rule'.$Rule.'access' -eq 'allow')){
    Write-Host "[Build Violation] Remote Desktop port 3389 permitted from Internet or * in NSG rule: $OutputName"
    $NsgErrors = 1
    }
   #Rule 2 - Internet or * MSSQL#
   if (($Objects.$Name.'security_rule'.$Rule.'source_address_prefix' -eq 'Internet') -and ($Objects.$Name.'security_rule'.$Rule.'destination_port_range' -eq '1433') -and ($Objects.$Name.'security_rule'.$Rule.'access' -eq 'allow')){
    Write-Host "[Build Violation] MSSQL port 1433 permitted from Internet or * in NSG rule: $OutputName"
    $NsgErrors = 1
    }
   #Rule 3 - Internet or * SSH#
   if (($Objects.$Name.'security_rule'.$Rule.'source_address_prefix' -eq 'Internet') -and ($Objects.$Name.'security_rule'.$Rule.'destination_port_range' -eq '22') -and ($Objects.$Name.'security_rule'.$Rule.'access' -eq 'allow')){
    Write-Host "[Build Violation] SSH port 22 permitted from Internet or * in NSG rule: $OutputName"
    $NsgErrors = 1
    }
   }
  }
 }
if ($NsgErrors -eq 0){
 Write-Host "No errors identified"
 }

Storage Account Encryption
Write a rule to ensure no storage accounts are created without encryption enabled.

#########Encryption Check##########
Write-Host "`n***Checking Storage Account Blob Encryption***"
$EncryptionErrors = 0
#Loops through Resources and checks security conditions
ForEach ($Name in $Objects.psobject.properties.name) { #Gets Resources
  $ObjectType = ($Objects.$Name | Get-Member -Type NoteProperty).name -Replace "(\[0m)","" #Replace is required to cleanup the first line in the file (which has some prepending characters)
  if ($ObjectType -eq "azurerm_storage_account"){ #Filters for Storage Account objects   
  if ($Objects.$Name.'account_kind' -eq "Storage"){
   if(-Not($Objects.$Name.'enable_blob_encryption' -eq "true")){
    Write-Host "[Build Violation] 'enable_blob_encryption' not set to 'true' on storage account $Name"
    $EncryptionErrors = 1
    }
   }
  }
 }
if ($EncryptionErrors -eq 0){
 Write-Host "No errors identified"
 }

Tags
Does your organisation tag assets? You can write a rule to ensure that all resources are tagged appropriately.

############Tag Checking###########
Write-Output "`n***Checking Tags***"
#Loops through Resources and checks security conditions
$RequiredTags = @("responsiblity", "department", "support", "costcenter") #Sets required tag names that are checked against build
$TagErrors = 0
ForEach ($Name in $Objects.psobject.properties.name) { #Gets Resources
 $ObjectType = ($Objects.$Name | Get-Member -Type NoteProperty).name -Replace "(\[0m)","" #Replace is required to cleanup the first line in the file (which has some prepending characters)
 if (-Not(($ObjectType -eq "azurerm_storage_container") -or ($ObjectType -eq "azurerm_subnet") -or ($ObjectType -eq "azurerm_virtual_network_peering"))){ #Excludes non taggable object types
  ForEach ($Tag in $RequiredTags){ #Loops through each rule in a resource
   $TagValue = ([string]($Objects.$Name.'tags'.$Tag)).Trim()
   if ($TagValue.length -lt 4){
    Write-Host "[Build Violation] Missing required tag '$Tag' in object $Name"
    $TagErrors = 1
    }
   }
  }
 }
if ($TagErrors -eq 0){
 Write-Host "No errors identified"
 }

Output

Here is a screenshot of the output presented at run time. The developer will see this during audit, providing instant feedback on the security of their code.


Conclusion

Hopefully you find use in this script and are able to reduce time spent fixing issues which are already deployed or responding to incident that never should have happened.

Please let me know if you run into any issues and I can try to address them in an update. Better yet - fix it yourself on my git :) 

1 comment: