7 Terraform Security Best Practices

Tony Loehr
Developer Advocate

Terraform, developed by Hashicorp, is an infrastructure as code (IaC) framework that allows for declarative resource provisioning. This open source tool allows users to create, update, and manage cloud resources. Rather than manipulating configurations manually, Terraform allows for deployment and management of cloud resources with code; this provides the benefits of creating elastically scalable infrastructure, improving consistency in resource management, and enabling software development practices to be employed, including security best practices.

Terraform’s declarative configurations are human readable syntax–this means that infrastructure can be defined directly by setting the resources it should use in code. This allows for decreased context switching time needed by developers and for mistakes to be more easily identified and corrected. Hundreds of cloud resource providers are supported. A basic configuration utilizing AWS as the cloud provider may look like:

provider "aws" {
    profile = "default"
    region = "us-west-2"
}

resource "aws_instance" "myServer" {
    ami = "ami-620c93e2"
    instance_type = "t2.micro"
}

The first block describes the cloud provider we plan to use, and the second block defines a resource. Resources in Terraform are a group of systems treated as a logical unit and take 2 arguments: a resource name and a local name. In the basic example above, the type “aws_instance” corresponds to one or more EC2 virtual machines.

Developing and managing infrastructure is complex, and mistakes can result in the introduction of risk. Employing the best practices described below makes for more secure software, and fewer deployment delays due to security concerns. Thus, developers can spend more time building tools and less time remediating errors.

1) Execute Terraform Programmatically

When executing Terraform operations, it makes more sense to execute commands from a CI/CD tool such as Jenkins or CircleCI versus executing these commands manually. This helps guarantee protocols are followed and provides a single common location to run code from, improving the repeatability of processes. In other words, executing Terraform programmatically allows for high levels of consistency. 

In addition, this allows for security checks built into the CI/CD or shared orchestration tool to be run with each execution of resource-creating Terraform code. Multi-stage pipelines can also ensure certain Terraform configuration versions are promoted from one environment to another.

Another reason why executing programmatically helps ensure that best security practices for Terraform are adhered to is because pre and post checks may be enforced. Running pre-apply checks in an automated pipeline helps ensure that an apply action will not cause regressions in security; this is done by checking the terraform plan output against existing policies. After the terraform apply command has been executed, the deployment should also run post-apply checks to verify the security of the deployment. Such checks fit perfectly within an automated build environment and constitute further reason to execute Terraform commands non-manually. Tools such as Forsetti and HashiCorp’s Sentinel are capable of performing pre-apply and posts-apply checks to help ensure best practices for Terraform are adhered to.  

2) Perform Misconfiguration Scans

Performing regular IaC misconfiguration scans allows for environmental drift, stale credentials, and even hardcoded secrets to be identified and corrected. Misconfigurations can be easily identified by way of pre-apply or post-apply checks, and tools to scan for misconfigurations may be employed during these checks as means of reducing the risk of a breach. Integrating such scans with pull requests helps ensure developers are aware of potential vulnerabilities and can take immediate corrective action.

Misconfiguration scans on infrastructure as code may be further improved by including critical code monitoring. This not only can prevent unintended changes to Terraform code, but also helps reduce the possibility of code tampering by ensuring that security build rules are not circumvented. IaC code is the perfect candidate for critical code monitoring as this code does not change frequently, but when it does change these changes should have high levels of visibility.

3) Establish Central Governance and Security Policy

Implementing centralized security policy and governance within Terraform code helps improve visibility of changes, enforces least privilege, hardens authentication, and protects the master branch from unauthorized changes. Central governance allows for resources created in Terraform to be audited and for authorization across the DevOps pipeline to be hardened. Requiring multi-factor authentication helps confirm that collaborators are who they say they are and improves security posture.

Enforcing separation of duties helps reduce risks to the health of a deployment. Developer credentials are the crown jewel for attackers and other nefarious actors; obtaining developer credentials allows attackers to perform actions such as pushing a build with tampered code into production or changing proprietary repositories’ settings from private to public. Enforcing separation of duties helps prevent a singular compromised account from compromising other parts of the system or accessing IaC tools. This also helps mitigate the risk of malicious insiders by blocking access to other team’s repositories.

A key part of ensuring separation of duties is adhering to the principle of least privilege. Ideally, an organization would run Terraform from an automated system which non-admin users do not have access to except in the case of an emergency, but separating permissions in such a way that limits access to only those who need it is one of the best security practices for Terraform that an organization can apply. 

Ways of protecting the master branch include disabling force pushes, enforcing commit signing, requiring all code changes to be peer-reviewed, and monitoring critical code. This helps reduce the likelihood of successful code tampering, and prevents security regressions from going overlooked.

4) Securely Store the State Remotely

The state, being the core driver of a configuration, should be stored remotely as backup. Utilizing version control for state makes it significantly easier to revert to a prior version of state in the case of mistakes. Ensure that versioning is enabled, otherwise one may be forced to use a prior branch for rollbacks which is significantly more difficult to execute and error prone. Rollbacks with versioning enabled can be as easy as the click of a button, depending on the cloud services provider.

However, to ensure Terraform security best practices when storing state, the .tfstate file should not be committed to source control. This file is generated to track the resources that Terraform created and is not something that should be manually generated unless one wants to intentionally introduce the Terraform configuration, state, and infrastructure. Committing this file, on top of implying that this file is to be edited, introduces the risk of executing Terraform against a state file that is stale or old. A huge risk of committing the .tfstate file is the risk of exposing secrets from the application configuration. 

Rather than committing the state file to a source control management system, it is better to store this in a Terraform backend. See more about this below.

5) Avoid Storing Secrets in State

Since it is recommended to store state within version control, storing secrets in state should be avoided at all costs as this can have multiple nasty implications. Anyone who has access to the version control system has access to any hard coded secrets that may be present, and every computer that has access to the version control system keeps a copy of that secret–what’s worse is that there is no way of revoking access to this secret. Not only does malicious access to hardcoded secrets facilitate attackers’ lateral movement, but even legitimate insider access to secrets potentially undermines separation of duties. 

Automation may be sacrificed to avoid storing secrets in state, but this is a tradeoff that is worth pursuing to reduce risk of compromise related to code leaks–however, there is still a better option. The best practice for Terraform security is to use a backend that supports encryption and to strictly control who can access the Terraform backend. 

This covers storing secrets, but what about passing secrets into Terraform code? This may be accomplished with three different techniques: environmental variables, encrypted files, or secret stores. Each comes with its own set of pros and cons, but here is a depiction of trade-offs between each:

Environmental Variables Encrypted Files (eg: KMS, SOPS) Secret Stores (eg: Vault, AWS Secrets Manager)
Secrets are encrypted
Secret management defined in code
Audit log for encryption keys
Audit log for secrets 
Secrets solution is easy to integrate with apps
Secrets can be rotated or revoked
Secrets may be versioned
Easy to test
Cost Free 💲 💲💲💲
Ease of use Intermediate Easy Difficult

Regardless of the technique chosen, a properly configured backend that supports encryption is needed.

6) Configure a Backend

Terraform backends are configurations on how (and where) to store Terraform state in a centralized, remote location. Backend setups will depend on which configuration fits an organization’s needs–such backends can be configured on S3, on artifactory, or in many other ways. A Terraform backend using S3 may resemble the following:

terraform {
    backend "s3" {
        bucket = "cycode-terraform-backend"
        key = "websites/main"
        region = "us-west-2"
    }
}

Terraform backends generally have two main features: state locking and remote state storage. State locking ensures that only one execution can occur at a time, whereas remote state storage allows for state to be placed in a secure yet accessible location. When a backend is configured, a .tfstate file is not generated locally but is instead pushed to a remote location.

It is imperative to strictly control who may access the backend as this backend may contain secrets–for example, if using S3 as a backend, it’s necessary to configure an IAM policy that solely grants access to only the relevant developers. 

7) Modularize When Possible

Modularizing reduces repetitive code that can fuel configuration drift and code entropy. This modularization may take several forms, the most common being variables. Variables allow for easy memory allocation of shared or repetitive configuration values; using variables in a terraform environment is a best practice for Terraform security that makes IaC code significantly easier to manage and update. Variables are also useful for keeping secrets, such as passwords and API keys, from being hardcoded. 

Variables must be declared to be used. For example, to create a password of type string:

variable "password" {
    description = "Password for account access"
    type = string
}

Terraform also includes the concept of optionals; declaring a variable with a default value makes this variable optional:

variable "aws_region" {
    description = "AWS region to launch servers."
    type = string
    default     = "us-west-2"
}

Once a variable is declared, it’s value may be set in 4 different ways:

// 1) interactively while running: 
terraform apply
// 2) as a command line argument
terraform apply -var="exampleVariable=stringValue"
// 3) in environment variables
exampleVariable="stringValue" terraform apply
// 4) in a separate file:
terraform apply -var-file="variables.tfvars"

If files contain secrets, ensure these files are not checked in to source control.

Modules are a terraform concept similar to lambdas in C or closures in Swift–they take an input, perform operations, and return an output. This allows for common infrastructure patterns to be shared throughout code. Such modules are terrific for accelerating development, but must be securely leveraged to avoid introducing security vulnerabilities to an application.

The two types of modules are private modules and public modules. Private modules are written as part of a configuration and encapsulates reusable components that authorized members of a team may take advantage of. Public modules, as the name implies, are third-party modules publicly available from places such as GitHub or the Terraform Registry. While public modules are great for reducing development time, they often were not built with security as priority and contain misconfigurations. 

Ultimately, modularizing is a powerful way of improving code maintainability, but it is paramount to ensure that the above steps are all followed to prevent mistakes from being amplified.

Conclusion

Terraform is a powerful tool for managing your infrastructure. As your infrastructure grows and Terraform configurations multiply, it’s important to secure that infrastructure from development to production. Learning these and other best practices will help you get the most out of Terraform.

With local or CI/CD scanning of your Terraform code, you get instant feedback on your modules and templates. But because of Terraform’s dependency-driven nature, you might not be getting the full picture. In order to get a holistic view into what is actually being provisioned or changed, including variables being called, you may need to scan the Terraform plan output.

How Cycode Can Help with Terraform Security

Cycode provides complete visibility into enterprise DevOps tools and infrastructure. Cycode’s Infrastructure as Code capabilities include comprehensive security scanning and preventing escaped secrets, environmental drift, code leaks, and other common issues, all within flexible developer-friendly workflows. Once integrated into developer workflows, each commit and PR is scanned for issues including hard coded secrets or potential misconfiguration and alerts are issued appropriately. Remediation is available both through a GUI and within the PR.

Cycode helps establish strong governance over all points of the IaC lifecycle by providing a cross-SCM inventory of all users, contributors, teams, organizations, and repositories in your organization; this governance extends into providing more oversight into changes made to code as a means of further protecting key code. Cycode also helps you automatically audit access privileges to identify and reduce excessive, unused privileges, and implement separation of duties. Furthermore, Cycode helps ensure that strong authentication and secure development practices are in place. This helps apply security best practices for IaC code when using Terraform, Kubernetes, YAML, ARM, and CloudFormation.

Want to learn more?

A great place to start is with a free assessment of the security of your DevOps pipeline