Repository: tfsec/tfsec
Pull request #689 (2021-04-15 20:36:01)

new IMDS v2 check
fixes https://github.com/tfsec/tfsec/issues/234

Rules:
- fire if `metadata_options` missing as IMDS v2 is not enforced by default
- fire if `http_tokens` set to `optional` as IMDS v2 is not enforced
- do not fire if `http_tokens` set to `required` as IMDS v2 is enforced
- do not fire if `http_endpoint` set to `disabled` as IMDS is disabled

Docs:
- https://aws.amazon.com/blogs/security/defense-in-depth-open-firewalls-reverse-proxies-ssrf-vulnerabilities-ec2-instance-metadata-service/
- https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance#metadata-options


Patch:
diff --git a/internal/app/tfsec/checks/aws079.go b/internal/app/tfsec/checks/aws079.go
new file mode 100644
index 000000000..964bc0fcb
--- /dev/null
+++ b/internal/app/tfsec/checks/aws079.go
@@ -0,0 +1,85 @@
+package checks
+
+import (
+	"fmt"
+	"github.com/tfsec/tfsec/internal/app/tfsec/parser"
+	"github.com/tfsec/tfsec/internal/app/tfsec/scanner"
+)
+
+const AWSInstanceMetadataChec scanner.RuleCode = "AWS079"
+const AWSInstanceMetadataChecDescription scanner.RuleSummary = "aws_instance should activate session tokens for Instance Metadata Service."
+const AWSInstanceMetadataChecExplanation = `
+IMDS v2 (Instance Metadata Service) introduced session authentication tokens which improve security when talking to IMDS.
+By default <code>aws_instance</code> resource sets IMDS session auth tokens to be optional. 
+To fully protect IMDS you need to enable session tokens by using <code>metadata_options</code> block and its <code>http_tokens</code> variable set to <code>required</code>.
+`
+const AWSInstanceMetadataChecBadExample = `
+resource "aws_instance" "bad_example" {
+  ami           = "ami-005e54dee72cc1d00"
+  instance_type = "t2.micro"
+}
+`
+const AWSInstanceMetadataChecGoodExample = `
+resource "aws_instance" "good_example" {
+  ami           = "ami-005e54dee72cc1d00"
+  instance_type = "t2.micro"
+  metadata_options {
+	http_tokens = "required"
+  }	
+}
+`
+
+func init() {
+	scanner.RegisterCheck(scanner.Check{
+		Code: AWSInstanceMetadataChec,
+		Documentation: scanner.CheckDocumentation{
+			Summary:     AWSInstanceMetadataChecDescription,
+			Explanation: AWSInstanceMetadataChecExplanation,
+			BadExample:  AWSInstanceMetadataChecBadExample,
+			GoodExample: AWSInstanceMetadataChecGoodExample,
+			Links: []string{
+				"https://aws.amazon.com/blogs/security/defense-in-depth-open-firewalls-reverse-proxies-ssrf-vulnerabilities-ec2-instance-metadata-service",
+				"https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance#metadata-options",
+			},
+		},
+		Provider:       scanner.AWSProvider,
+		RequiredTypes:  []string{"resource"},
+		RequiredLabels: []string{"aws_instance"},
+		CheckFunc: func(check *scanner.Check, block *parser.Block, _ *scanner.Context) []scanner.Result {
+				
+			metaDataOptions := block.GetBlock("metadata_options")
+			if metaDataOptions == nil {
+				return []scanner.Result{
+					check.NewResult(
+						fmt.Sprintf("Resource '%s' is missing `metadata_options` block - it is required with `http_tokens` set to `required` to make Instance Metadata Service more secure.", block.FullName()),
+						block.Range(),
+						scanner.SeverityError,
+					),
+				}
+			}
+
+			httpEndpointAttr := metaDataOptions.GetAttribute("http_endpoint")
+			if httpEndpointAttr != nil {
+				if httpEndpointAttr.Equals("disabled") {
+					// IMDS disabled and we don't need to check if http_tokens are correctly set up
+					return nil
+				}
+			}
+
+			httpTokensAttr := metaDataOptions.GetAttribute("http_tokens")
+			if httpTokensAttr != nil {
+				if !httpTokensAttr.Equals("required") {
+					return []scanner.Result{
+						check.NewResult(
+							fmt.Sprintf("Resource '%s' `metadata_options` `http_tokens` attribute - should be set to `required` to make Instance Metadata Service more secure.", block.FullName()),
+							httpTokensAttr.Range(),
+							scanner.SeverityError,
+						),
+					}
+				}
+			}
+
+			return nil
+		},
+	})
+}


Test patch:
diff --git a/internal/app/tfsec/test/aws079_test.go b/internal/app/tfsec/test/aws079_test.go
new file mode 100644
index 000000000..5df74229d
--- /dev/null
+++ b/internal/app/tfsec/test/aws079_test.go
@@ -0,0 +1,77 @@
+package test
+
+import (
+	"testing"
+
+	"github.com/tfsec/tfsec/internal/app/tfsec/checks"
+	"github.com/tfsec/tfsec/internal/app/tfsec/scanner"
+)
+
+func Test_AWSInstanceMetadataChec(t *testing.T) {
+
+	var tests = []struct {
+		name                  string
+		source                string
+		mustIncludeResultCode scanner.RuleCode
+		mustExcludeResultCode scanner.RuleCode
+	}{
+		{
+			name: "should fire as http_tokens not specified and by default are optional",
+			source: `
+resource "aws_instance" "working example"{
+  ami           = "ami-005e54dee72cc1d00"
+  instance_type = "t2.micro"
+}
+`,
+			mustIncludeResultCode: checks.AWSInstanceMetadataChec,
+		},
+		{
+			name: "should fire as http_tokens explicitly set to optional and should be required",
+			source: `
+resource "aws_instance" "working example"{
+  ami           = "ami-005e54dee72cc1d00"
+  instance_type = "t2.micro"
+  metadata_options {
+	http_tokens = "optional"
+  }	
+}
+`,
+			mustIncludeResultCode: checks.AWSInstanceMetadataChec,
+		},
+		{
+			name: "should not fire when http_tokens set to required",
+			source: `
+resource "aws_instance" "working example"{
+  ami           = "ami-005e54dee72cc1d00"
+  instance_type = "t2.micro"
+  metadata_options {
+	http_tokens = "required"
+  }	
+}
+`,
+			mustExcludeResultCode: checks.AWSInstanceMetadataChec,
+		},
+		{
+			name: "should not fire when http_endpoint disabled as IMDS is not available",
+			source: `
+resource "aws_instance" "working example"{
+  ami           = "ami-005e54dee72cc1d00"
+  instance_type = "t2.micro"
+  metadata_options {
+	http_endpoint = "disabled"
+	http_tokens   = "optional"
+  }	
+}
+`,
+			mustExcludeResultCode: checks.AWSInstanceMetadataChec,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			results := scanSource(test.source)
+			assertCheckCode(t, test.mustIncludeResultCode, test.mustExcludeResultCode, results)
+		})
+	}
+
+}