Securing Terraform Modules with tfsec
Infrastructure as Code (IaC) patterns have enabled velocity, repeatability, and codification of best practices for our environments. However, using IaC has introduced new challenges, especially around security. Securing manually deployed infrastructure is already difficult. This problem rapidly multiplies when organizations adopt IaC patterns, since they must now contend with the complexity of code and the proliferation of environments enabled by this increased velocity.
Codifying our environments doesn’t have to result in insecure configurations. On the contrary, expressing environment definitions via code has the potential to automate the process of scanning for security violations before they ever have the chance to cause harm. In this article, I’ll discuss one way to accomplish this in Terraform environments by using tfsec
. Tfsec is an open source utility created by Aqua Security that performs static analysis of Terraform code to spot security misconfigurations. It has grown in popularity, and it’s even listed as a tool to adopt on the Thoughtworks Tech Radar. I’ll also show you how Cloudify integrates seamlessly with tfsec
and allows you to enforce policy in your self-service environments.
Installing tfsec
Cloud native tools are commonly provided as single, easily installed binaries and tfsec
is no different. You can download the latest release of tfsec for your platform from GitHub and execute it immediately:
$ sudo wget -o /dev/null -O /usr/bin/tfsec https://github.com/aquasecurity/tfsec/releases/download/v1.28.0/tfsec-linux-amd64
$ sudo chmod +x /usr/bin/tfsec
$ tfsec --version
v1.28.0
Built-in checks
Tfsec includes dozens of checks for common cloud misconfigurations. You can take advantage of these checks without any additional configuration: tfsec
includes them right out of the box.
For example, let’s take a look at a simple Terraform module to deploy an EC2 instance. You can clone this example from our community GitHub:
$ git clone git@github.com:cloudify-community/tfsec-example.git
Cloning into 'tfsec-example'...
remote: Enumerating objects: 27, done.
remote: Counting objects: 100% (27/27), done.
remote: Compressing objects: 100% (16/16), done.
remote: Total 27 (delta 6), reused 27 (delta 6), pack-reused 0
Receiving objects: 100% (27/27), 5.65 KiB | 5.65 MiB/s, done.
Resolving deltas: 100% (6/6), done.
$ cd tfsec-example/
Checking the Terraform code against the default rules is as simple as running the tfsec
command:
$ cd tf_module/ec2-instance/
$ tfsec
Result #1 CRITICAL Security group rule allows egress to multiple public internet addresses.
──────────────────────────────────────────────────────────────────────────────────────────
main.tf:67
──────────────────────────────────────────────────────────────────────────────────────────
49 resource "aws_security_group" "example_security_group" {
..
67 [ cidr_blocks = ["0.0.0.0/0"]
..
70 }
──────────────────────────────────────────────────────────────────────────────────────────
ID aws-ec2-no-public-egress-sgr
Impact Your port is egressing data to the internet
Resolution Set a more restrictive cidr range
More Information
- https://aquasecurity.github.io/tfsec/v1.28.0/checks/aws/ec2/no-public-egress-sgr/
- https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group
──────────────────────────────────────────────────────────────────────────────────────────
timings
──────────────────────────────────────────
disk i/o 16.762µs
parsing 459.704µs
adaptation 90.981µs
checks 46.54541ms
total 47.112857ms
counts
──────────────────────────────────────────
modules downloaded 0
modules processed 1
blocks processed 12
files read 3
results
──────────────────────────────────────────
passed 7
ignored 0
critical 1
high 0
medium 0
low 0
7 passed, 1 potential problem(s) detected.
Tfsec immediately reports a problem with the security group, indicating that the egress range is open to the Internet. It even provides a link to the appropriate documentation with an explanation of the rule violation and remediation steps.
Ignoring Rules
You may find that the built-in checks don’t always line up with your organization’s security policies. For example, the previous check raised a critical error for an overly permissive egress rule in a security group. However, opening up egress to the public Internet is very common and may not violate your organization’s security posture.
Luckily, tfsec
makes it easy to ignore rules. You can add a simple comment to the Terraform module next to the offending line, and tfsec will ignore the specified check:
egress {
description = "Allow Egress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"] # tfsec:ignore:aws-ec2-no-public-egress-sgr
}
Notice that the rule ID (aws-ec2-no-public-egress-sgr) matches the rule ID specified in the previous error from tfsec
.
You can also specify checks to ignore via the -e
command-line argument:
$ tfsec -e 'aws-ec2-no-public-egress-sgr'
timings
──────────────────────────────────────────
disk i/o 16.36µs
parsing 364.079µs
adaptation 81.842µs
checks 46.946122ms
total 47.408403ms
counts
──────────────────────────────────────────
modules downloaded 0
modules processed 1
blocks processed 12
files read 3
results
──────────────────────────────────────────
passed 7
ignored 1
critical 0
high 0
medium 0
low 0
No problems detected!
Custom Checks
The default tfsec
checks are very useful, and the tfsec
team at Aqua Security regularly adds new checks to their library. However, you will inevitably have an organizational policy that isn’t covered by the built-in checks. This use case can be addressed by developing your own custom checks.
For example, consider a use case where we want to restrict ingress IP addresses on security groups to ranges approved by our organization. We can express this requirement using a custom check.
Custom checks are defined next to Terraform code in the .tfsec/
directory. They can also be sourced from other directories or remote sources, but defining them in .tfsec/
is the quickest way to test out a custom check. Tfsec will automatically load files from this directory if they have a _tfsec.yaml
or _tfsec.json
suffix.
Let’s start by creating a custom check file at .tfsec/cidr_tfchecks.yaml.
If you are using the directory from our GitHub repository, then this file already exists, but the check itself has been commented out. We can write our check using the custom check syntax:
---
checks:
- code: CIDR001
description: Check to ensure that only approved IPs are allowed for ingress rules
impact: Overly permissive ingress rules introduce additional avenues of compromise
resolution: Use only approved IPs for ingress rules
requiredTypes:
- resource
requiredLabels:
- aws_security_group
severity: CRITICAL
matchSpec:
name: ingress
action: isPresent
subMatch:
name: cidr_blocks
action: onlyContains
value:
- "203.0.113.10/32"
- "203.0.113.47/32"
- "203.0.113.98/32"
errorMessage: Ingress access is permitted from a non-approved IP range
relatedLinks:
- http://wiki.example.com/security/approved-networks.html
This custom check will match aws_security_group
Terraform resources and determine if their ingress rules only contain the listed IP addresses. If there are any other IP addresses in the ingress rules, then the check will fail and generate a failure with a critical severity level.
Custom checks can become fairly advanced, and the official custom check documentation is the best resource for understanding custom check syntax.
Tfsec will automatically load this custom check, so we don’t need to make any changes to the tfsec
command syntax:
$ tfsec -e 'aws-ec2-no-public-egress-sgr'
timings
──────────────────────────────────────────
disk i/o 16.142µs
parsing 382.505µs
adaptation 84.251µs
checks 48.135488ms
total 48.618386ms
counts
──────────────────────────────────────────
modules downloaded 0
modules processed 1
blocks processed 12
files read 3
results
──────────────────────────────────────────
passed 8
ignored 1
critical 0
high 0
medium 0
low 0
No problems detected!
The check passes with the default configuration. Let’s add a simple terraform.tfvars
file with an IP address that isn’t on the list of allowed IPs to test a failure case:
$ cat terraform.tfvars
ssh_ips = [ "203.0.113.10/32", "198.51.100.76/32" ]
Running tfsec
with the --tfvars-file
flag allows you to specify a Terraform variables file. The “198.51.100.76/32” address falls outside the list of addresses that are allowed by the custom check and an error is generated:
$ tfsec -e 'aws-ec2-no-public-egress-sgr' --tfvars-file terraform.tfvars
Result #1 CRITICAL Custom check failed for resource aws_security_group.example_security_group. Ingress access is permitted from a non-approved IP range
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
main.tf:49-70
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
49 ┌ resource "aws_security_group" "example_security_group" {
50 │ name = "example_security_group"
51 │
52 │ description = "Example SG"
53 │
54 │ ingress {
55 │ description = "Allow SSH"
56 │ from_port = 22
57 └ to_port = 22
..
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
ID custom-custom-cidr001
Impact Overly permissive ingress rules introduce additional avenues of compromise
Resolution Use only approved IPs for ingress rules
More Information
- http://wiki.example.com/security/approved-networks.html
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
timings
──────────────────────────────────────────
disk i/o 25.227µs
parsing 428.994µs
adaptation 90.319µs
checks 50.148551ms
total 50.693091ms
counts
──────────────────────────────────────────
modules downloaded 0
modules processed 1
blocks processed 12
files read 3
results
──────────────────────────────────────────
passed 7
ignored 1
critical 1
high 0
medium 0
low 0
7 passed, 1 ignored, 1 potential problem(s) detected.
Self-Service, Secure Environments with Cloudify
Cloudify’s rich integration with Terraform makes it easy to turn existing Terraform modules into self-service environments. Our Terraform plugin also includes native integration with tfsec
, allowing you to enforce security policy as part of your overall self-service environment blueprints. Let’s take a look at how we can build security and governance into a self-service catalog item.
The example blueprint in the community repository has been preconfigured to work with a Terraform zip archive that is packaged alongside the blueprint. First, zip up the Terraform module that you have been working with:
$ cd tf_module/
$ zip -r terraform.zip ec2-instance/
adding: ec2-instance/ (stored 0%)
adding: ec2-instance/main.tf (deflated 54%)
adding: ec2-instance/terraform.tfvars (deflated 4%)
adding: ec2-instance/.tfsec/ (stored 0%)
adding: ec2-instance/.tfsec/cidr_tfchecks.yaml (deflated 48%)
adding: ec2-instance/outputs.tf (deflated 46%)
adding: ec2-instance/variables.tf (deflated 58%)
Next, upload the blueprint:
$ cfy blueprint upload -b Tfsec-Example blueprint.yaml
Uploading blueprint blueprint.yaml...
blueprint.yaml |######################################################| 100.0%
Blueprint `Tfsec-Example` upload started.
2022-09-23 14:48:33.384 CFY <None> Starting 'upload_blueprint' workflow execution
2022-09-23 14:48:33.456 LOG <None> INFO: Blueprint archive uploaded. Extracting...
2022-09-23 14:48:33.548 LOG <None> INFO: Blueprint archive extracted. Parsing...
2022-09-23 14:48:34.917 LOG <None> INFO: Blueprint parsed. Updating DB with blueprint plan.
2022-09-23 14:48:35.100 CFY <None> 'upload_blueprint' workflow execution succeeded
Blueprint uploaded. The blueprint's id is Tfsec-Example
The blueprint uses Cloudify’s native tfsec
integration to automatically install the latest version of tfsec
and run tfsec
against the Terraform module during Cloudify workflows, such as the initial deployment:
tfsec_config:
config:
exclude:
- 'aws-ec2-no-public-egress-sgr'
installation_source: https://github.com/aquasecurity/tfsec/releases/download/v1.28.0/tfsec-linux-amd64
flags_override: []
enable: True
Creating a new deployment of this environment will cause tfsec
to run against any variables provided to Terraform, such as those included as part of the environment’s inputs. In this case, if a user provides an IP address that doesn’t meet the custom check, Cloudify will automatically fail the deployment:
Notice that the tfsec
check fails, and Cloudify automatically fails the deployment of the Terraform module. The result of the tfsec
check is visible to the user in the logs.
Wrapping Up
Tfsec is an excellent utility for enforcing governance and security policy for environments that are provisioned using Terraform. With dozens of built-in checks around security best practices and the ability to integrate custom checks, tfsec
should be a basic building block of every Terraform workflow. Cloudify’s robust Terraform integration makes it easy to expose secure, repeatable Terraform environments to your end users with tfsec
scanning built in.
Interested in learning more about how Cloudify can simplify and secure your Terraform environments? Contact us to book a demo and learn more about our platform.