Secure Terraform - Part 2 - tfsec Customization

6 minute read

This is part 2 of the Secure Terraform series. You can read the series of articles here:

Introduction

In the previous article, we discussed tfsec, a static code analysis tool for Terraform. We also learned how to use it in VSCode and GitHub Actions to scan our Terraform code. We learned how to override the severity of rules. In this article, we will learn how to customize the rules and add our own rules.

tfsec logo

Customizing tfsec Rules

Tfsec allows you to customize the rules that are used to scan your Terraform code. You can do this by creating a file ending in _tfchecks.json or _tfchecks.yaml in the .tfsec folder in the root of project. You can also put these files in a different folder and pass the option --custom-check-dir or --custom-check-url to the tfsec command. This is covered in the documentation: https://aquasecurity.github.io/tfsec/v1.28.1/guides/configuration/custom-checks/.

The documentation references a tool called tfsec-checkgen that you can install. This tool will validate your check file or help perform tests to ensure that it is valid for use with tfsec. I found that the tool helped me create and validate checks but not run the test-check action.

In this post, we will take a look at how to create a few different custom rules. The first rule we’ll work on is for a required tag for our Azure resources. There is an example of this in the tfsec documentation, but its for AWS.

---
checks:
- code: CUS001
  description: Custom check to ensure the CostCentre tag is applied to EC2 instances
  impact: By not having CostCentre we can't keep track of billing
  resolution: Add the CostCentre tag
  requiredTypes:
  - resource
  requiredLabels:
  - aws_instance
  severity: ERROR
  matchSpec:
    name: tags
    action: contains
    value: CostCentre
  errorMessage: The required CostCentre tag was missing
  relatedLinks:
  - http://internal.acmecorp.com/standards/aws/tagging.html

Lets look at my Azure example and discuss the tweaks and how to use it.

---
checks:
- code: tags-resources
  description: Custom check to ensure the CostCenter tag is applied to Azure Resources
  impact: By not having CostCenter we can't keep track of billing
  resolution: Add the CostCenter tag
  requiredTypes:
  - resource
  requiredLabels:
  - azurerm_subscription
  - azurerm_resource_group
  - azurerm_linux_web_app
  - azurerm_windows_web_app
  - azurerm_storage_account
  - azurerm_service_plan
  - azurerm_app_service
  severity: HIGH
  matchSpec:
    name: tags
    action: contains
    value: CostCenter
  errorMessage: The required CostCenter tag was missing
  relatedLinks:
  - https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-tagging

I’ve changed the name to tags-resources to make it more descriptive. I’ve also changed the requiredLabels to include the resources I want to check for the tag. This rule will only trigger on the resource types listed under requiredLabels. I’ve also changed the severity to HIGH. I’ve also added a link to the Azure Best Practices for Resource Tagging.

This is saved to .tfsec/custom_tfchecks.yaml. The tfsec vscode extension we installed before will automatically pick up the new rule. We can see it highlighting the code with an issue and showing up in the results screen.

tfsec vscode extension
tfsec vscode extension

Issues also show up as problems at the bottom of the screen for you to see, click on, and navigate to the right code section.

tfsec problems
tfsec problems

A Custom Rule for Naming

Let’s try something a little more complex. We can try to enforce a naming scheme for our resources. I want to enforce a naming scheme that all resource groups must follow a pattern of rg-app-env-region. This will help us identify which resources belong to which applications or environments. This is a great way to enforce a naming scheme and keep things organized.

- code: rg-naming-pattern
  description: Custom check to check resource group naming
  impact: resource groups should be named consistently
  resolution: use the pattern rg-app-env-region
  requiredTypes:
  - resource
  requiredLabels:
  - azurerm_resource_group
  severity: HIGH
  matchSpec:
    name: name
    action: regexMatches
    value: "^rg-[a-zA-Z]+-[a-zA-Z]+-[a-zA-Z]+"
  errorMessage: improperly named resource group
  relatedLinks:
  - https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-naming

This rule, only checks against resource groups as identified by the requiredLabels property containing azurerm_resource_group. The matchSpec uses the regexMatches action. I am able to provide my regular expression and the error message. There are a bunch of provided check actions that you can use to develop your custom checks.

Custom Checks for Deprecated Resources

Tfsec also allows you to create custom checks for deprecated resources. This is a great way to keep up with the latest changes in Terraform and Azure. I’ve created a custom check for the deprecated azurerm_app_service resource. This resource has been deprecated in favor of azurerm_linux_web_app and azurerm_windows_web_app. I’ve created a custom check to warn us when we use the deprecated resource.

- code: app-service-deprecated
  description: Custom check to warn on deprecated app service
  impact: using deprecated app service resource instead of azurerm_linux_web_app or azurerm_windows_web_app
  resolution: Use azurerm_linux_web_app or azurerm_windows_web_app
  requiredTypes:
  - resource
  requiredLabels:
  - azurerm_app_service
  severity: WARN
  matchSpec:
    name: azurerm_app_service
    action: isPresent
  errorMessage: Using a deprecated resource - azurerm_app_service
  relatedLinks:
  - https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/app_service

This time I’m using the isPresent action to check if the resource is present. I’ve also added a link to the documentation for the resource.

Rego Policies

What about Rego policies?

We can create a folder to hold all of our rego policies. I created a file called keyvault_softdeleteretentiondays.rego and added the following code.

package custom.azure.keyvault.softdeleteretentiondays

deny[msg] {
    kv := input.azure.keyvault.vaults[_]
    kv.softdeleteretentiondays.value < 14
    msg := "Key Vault Soft Delete Retention Days is less than 14 days"
}

To understand the json input going into the rego policy, you can use the following command:

tfsec --print-rego-input

This will give you a ton of output that you can filter with jq to find the specific resource you are looking for. For example, to find the keyvault resource, you can use the following command:

vscode ➜ /workspaces/secure-terraform-on-azure/ (main) $ tfsec --print-rego-input | jq '.azure.keyvault'{
  "vaults": [
    {
      "__defsec_metadata": {
        "endline": 52,
        "explicit": false,
        "filepath": "workspaces/secure-terraform-on-azure/custom_checks_examples/keyvault/fail/main.tf",
        "managed": true,
        "resource": "azurerm_key_vault.example",
        "startline": 20
      },
      "enablepurgeprotection": {
        "endline": 27,
        "explicit": true,
        "filepath": "workspaces/secure-terraform-on-azure/custom_checks_examples/keyvault/fail/main.tf",
        "managed": true,
        "resource": "azurerm_key_vault.example.purge_protection_enabled",
        "startline": 27,
        "value": true
      },
      "networkacls": {
        "__defsec_metadata": {
          "endline": 34,
          "explicit": false,
          "filepath": "workspaces/secure-terraform-on-azure/custom_checks_examples/keyvault/fail/main.tf",
          "managed": true,
          "resource": "network_acls",
          "startline": 31
        },
        "defaultaction": {
          "endline": 33,
          "explicit": true,
          "filepath": "workspaces/secure-terraform-on-azure/custom_checks_examples/keyvault/fail/main.tf",
          "managed": true,
          "resource": "network_acls.default_action",
          "startline": 33,
          "value": "Deny"
        }
      },
      "softdeleteretentiondays": {
        "endline": 26,
        "explicit": true,
        "filepath": "workspaces/secure-terraform-on-azure/custom_checks_examples/keyvault/fail/main.tf",
        "managed": true,
        "resource": "azurerm_key_vault.example.soft_delete_retention_days",
        "startline": 26,
        "value": 7
      }
    }
  ]
}

I used this output to develop the policy. I had a few issues with the samples from the docs, and there is an open GitHub issue. To run the rego policies with tfsec, you have to pass the --rego-policy-dir command like this:

vscode ➜ /workspaces/secure-terraform-on-azure (main) $ tfsec --rego-policy-dir ./tfsec_rego_policies/ ./custom_checks_examples/keyvault/ 

Result #1  Key Vault Soft Delete Retention Days is less than 14 days 
───────────────────────────────────────────────────────────────────────────────────────────────
───────────────────────────────────────────────────────────────────────────────────────────────
  fail  Rego Package custom.azure.keyvault.softdeleteretentiondays
     Rego Rule deny
───────────────────────────────────────────────────────────────────────────────────────────────

You can see the results of the rego policy in the output.

Conclusion

While rego policies support are nice, I think the yaml policies are more flexible and easier to use. Having the ability to use a URL for custom checks allows you to share your checks with others.

I wanted to show how to do checks in Azure because I didn’t see a lot of examples or docs on Azure resources specifically.

I hope this deeper dive into custom checks was helpful.