Recently, tfsec added support for applying Rego policies to your Terraform code. Clear rules can be written against simple data structures, whilst providing the developer with a wealth of information in the event of a failure, such as path, line numbers, and highlighted code snippets.
Example output of a tfsec rego scan
What is tfsec?
tfsec is a tool designed to run either on a developer’s machine or as part of a build pipeline. It examines Terraform files for misconfigurations and clearly reports them to the user.
Hundreds of rules come prepackaged in tfsec, and generally reflect industry best-practice.
You can install and get started with tfsec by following these instructions
What is OPA/Rego?
Quoting the official OPA Documentation:
The Open Policy Agent (OPA, pronounced “oh-pa”) is an open source, general-purpose policy engine that unifies policy enforcement across the stack. OPA provides a high-level declarative language that lets you specify policy as code and simple APIs to offload policy decision-making from your software. You can use OPA to enforce policies in microservices, Kubernetes, CI/CD pipelines, API gateways, and more.
This sounds quite complicated, but it’s actually much simpler than it sounds. Here’s some simpler definitions:
- Rego
- …is a language for defining policies, such as every S3 bucket must have encryption enabled.
- OPA
- …is the engine that knows how to apply these policies to a given input. OPA can be queried to check the results of these policies for certain inputs i.e. does this particular S3 bucket comply with the bucket encryption policy?.
Most organisations need to define some of their own unique security policies, and therefore need a way to enforce them as well as applying industry-standard rules.
For example, an organisation may want to ensure all of it’s AWS EC2 instances are tagged with a Department
in order to track ownership of resources, track spend between departments, and improve audit capabilities.
OPA Rego is an excellent solution to this problem, especially when combined with other tools…
You can get started with Rego by reading the docs or looking at this OPA Policy Authoring course
Writing Rego policies for tfsec
You must be using tfsec v1.12.0 or later for the features discussed here.
Below is a very minimal example of a Rego policy that tfsec can run. It checks there are no S3 buckets named “insecure-bucket”. Not very useful in the real world, but it’s helpful to explain how this works.
1
2
3
4
5
6
package custom.aws.s3.no_insecure_buckets
deny {
bucket := input.aws.s3.buckets[_]
bucket.name.value == "insecure-bucket"
}
Let’s break this down.
The package (line #1) must always start with the custom
namespace in order for tfsec to recognise it. The rest of the package name can be whatever you like, but it’s generally a good idea to break things down by cloud provider, service, environment etc.
The name of the deny
rule is important. Rule names must either be deny
, or begin with deny_
in order to highlight an issue when tfsec runs.
The input
variable contains cloud resources organised by provider (e.g aws), and then service (e.g. s3). You can see what this looks like by running tfsec on your project with the --print-rego-input
flag. Combining this with the jq tool is very helpful:
1
2
3
4
5
6
7
8
9
tfsec --print-rego-input | jq '.aws.s3.buckets[0].name'
{
"endline": 3,
"explicit": true,
"filepath": "/home/liamg/rego-playground/terraform/bucket.tf",
"managed": true,
"startline": 3,
"value": "secure-bucket"
}
For more information about the input structure, you can review the entire schema in code form by studying the
state.State
Go struct defined in the defsec source code. All property names are converted to lower-case for consistency, to make writing policies easier.
You may have noticed that the policy checks bucket.name.value
, instead of just bucket.name
. This is because the bucket.name
property contains more than just the value of the property, it also contains various metadata about where this property value was defined, including the filename and line number of the source Terraform file. You can see an example of this metadata in the jq output above.
Trying it out
Create a new directory and change into it, then create a directory named policies
for Rego, and a directory named terraform
for Terraform files.
Add the following content to terraform/bucket.tf
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
resource "aws_s3_bucket" "example" {
bucket = "secure-bucket"
website {
index_document = "index.html"
}
logging {
target_bucket = "my-logging-bucket"
}
server_side_encryption_configuration {
rule {
apply_server_side_encryption_by_default {
kms_master_key_id = "this-is-a-real-key-id-honest"
sse_algorithm = "aws:kms"
}
}
}
versioning {
enabled = true
}
tags = {
Environment = "production"
Repository = "core-infra"
}
}
resource "aws_s3_bucket_public_access_block" "example" {
bucket = aws_s3_bucket.example.id
block_public_acls = true
block_public_policy = true
restrict_public_buckets = true
ignore_public_acls = true
}
The above may look a bit overwhelming, but the details aren’t important for now, just know that it defines an S3 bucket with some core security considerations implemented.
Add the following content to policies/bucket.rego
:
1
2
3
4
5
6
package custom.aws.s3.no_insecure_buckets
deny {
bucket := input.aws.s3.buckets[_]
bucket.name.value == "insecure-bucket"
}
You should now have the following setup:
1
2
3
4
5
6
tree
.
├── policies
│ └── bucket.rego
└── terraform
└── bucket.tf
You can ask tfsec to apply your custom Rego policies by using the --rego-policy-dir
flag to specify the directory containing your policies. Policies will be loaded recursively in this directory, and so can be organised by nested subdirectories if desired.
1
2
3
tfsec --rego-policy-dir ./policies ./terraform
No problems detected!
If we change our Terraform bucket definition so that the name is insecure-bucket
, as specified in the rule, running _tfsec_again should yield a failure:
1
2
3
4
5
6
7
8
9
10
tfsec --rego-policy-dir ./policies ./terraform
Result #1 Rego policy resulted in DENY
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Rego Package custom.aws.s3.no_insecure_buckets
Rego Rule deny
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
10 passed, 1 potential problem(s) detected.
Awesome! This is already quite useful, but you may notice the message above is not particularly useful…
Customising messages
You can add custom messages to tfsec rego rules but returning them from the rule, for example:
1
2
3
4
5
6
7
package custom.aws.s3.no_insecure_buckets
deny[msg] {
bucket := input.aws.s3.buckets[_]
bucket.name.value == "insecure-bucket"
msg := "Bucket name should not be 'insecure-bucket'"
}
The above now gives us a clearer explanation of what is failing:
1
2
3
4
5
6
7
8
9
10
tfsec --rego-policy-dir ./policies ./terraform
Result #1 Bucket name should not be 'insecure-bucket'
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Rego Package custom.aws.s3.no_insecure_buckets
Rego Rule deny
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
10 passed, 1 potential problem(s) detected.
This is better, but in a large project it’s going to be very difficult to locate the cause of a failure with the above output…
Adding paths, line numbers, and more…
It is very easy to add line numbers, file paths and highlighted, annotated code to the tfsec output for a rego policy.
You can import the data.lib.result
package and call the result.new()
function to create a result which contains all of this information. The first argument is the message to display, and the second is the property or object which caused a failure (the bucket name in this case).
1
2
3
4
5
6
7
8
9
10
package custom.aws.s3.no_insecure_buckets
import data.lib.result
deny[res] {
bucket := input.aws.s3.buckets[_]
bucket.name.value == "insecure-bucket"
msg := "Bucket name should not be 'insecure-bucket'"
res := result.new(msg, bucket.name)
}
This gives us much richer output, making it much easier to act on a failure surfaced by this policy:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
tfsec --rego-policy-dir ./policies ./terraform
Result #1 Bucket name should not be 'insecure-bucket'
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
bucket.tf Line 3
───────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
3 │ bucket = "insecure-bucket"
───────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Rego Package custom.aws.s3.no_insecure_buckets
Rego Rule deny
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
10 passed, 1 potential problem(s) detected.
At this point, you should have all tools you need to start writing policies for use in tfsec!
I hope this provides a good overview of the new Rego capabilities of tfsec. This functionality will also soon be available for both Terraform and CloudFormation using Trivy.
More documentation for this feature will be available over the coming weeks. In the meantime, please get in touch on Slack if you have questions, comments or suggestions. Please raise any issues on GitHub.
Thanks for reading!
Comments powered by Disqus.