Repository: tfsec/tfsec
Pull request #496 (2020-11-05 22:14:59)

Add GCP check to ensure that GKE node service accounts are overridden
👋 

Just adding a check to ensure that `service_account` is overridden in `google_container_cluster` and `google_container_node_pool` inside the `node_config` block.

### Checks Added
* GCP012: Checks if `node_config` in `google_container_cluster` or `google_container_node_pool` has a service account defined which is recommended by Google (see below)

Reference: https://cloud.google.com/kubernetes-engine/docs/how-to/hardening-your-cluster#use_least_privilege_sa

> By default, nodes are given the Compute Engine default service account, which you can find by navigating to the IAM section of the Cloud Console. This account has broad access by default, making it useful to wide variety of applications, but it has more permissions than are required to run your Kubernetes Engine cluster. **You should create and use a minimally privileged service account to run your GKE cluster instead of using the Compute Engine default service account.**





owenrumney: Thanks for this @maaslalani  - It's prompted me to add a new helper function which is just going through then will finish review 

Patch:
diff --git a/internal/app/tfsec/checks/gcp012.go b/internal/app/tfsec/checks/gcp012.go
new file mode 100644
index 000000000..3e5c586bb
--- /dev/null
+++ b/internal/app/tfsec/checks/gcp012.go
@@ -0,0 +1,76 @@
+package checks
+
+import (
+	"fmt"
+
+	"github.com/tfsec/tfsec/internal/app/tfsec/parser"
+	"github.com/tfsec/tfsec/internal/app/tfsec/scanner"
+)
+
+const GCPGKENodeServiceAccount scanner.RuleCode = "GCP012"
+const GCPGKENodeServiceAccountDescription scanner.RuleSummary = "Checks for service account defined for GKE nodes"
+const GCPGKENodeServiceAccountExplanation = `
+You should create and use a minimally privileged service account to run your GKE cluster instead of using the Compute Engine default service account.
+`
+
+const GCPGKENodeServiceAccountBadExample = `
+resource "google_container_cluster" "my-cluster" {
+  node_config {
+  }
+}
+`
+const GCPGKENodeServiceAccountGoodExample = `
+resource "google_container_cluster" "my-cluster" {
+  node_config {
+    service_account = "cool-service-account@example.com"
+  }
+}
+`
+
+func init() {
+	scanner.RegisterCheck(scanner.Check{
+		Code: GCPGKENodeServiceAccount,
+		Documentation: scanner.CheckDocumentation{
+			Summary:     GCPGKENodeServiceAccountDescription,
+			Explanation: GCPGKENodeServiceAccountExplanation,
+			BadExample:  GCPGKENodeServiceAccountBadExample,
+			GoodExample: GCPGKENodeServiceAccountGoodExample,
+			Links: []string{
+				"https://cloud.google.com/kubernetes-engine/docs/how-to/hardening-your-cluster#use_least_privilege_sa",
+			},
+		},
+		Provider:       scanner.GCPProvider,
+		RequiredTypes:  []string{"resource"},
+		RequiredLabels: []string{"google_container_cluster", "google_container_node_pool"},
+		CheckFunc: func(check *scanner.Check, block *parser.Block, _ *scanner.Context) []scanner.Result {
+
+			if !block.HasBlock("node_config") {
+				return []scanner.Result{
+					check.NewResult(
+						fmt.Sprintf("Resource '%s' does not define the node config and does not override the default service account. It is recommended to use a minimally privileged service account to run your GKE cluster.", block.FullName()),
+						block.Range(),
+						scanner.SeverityError,
+					),
+				}
+			}
+			displayBlock := block.GetBlock("node_config")
+			serviceAccount := displayBlock.GetAttribute("service_account")
+
+			if serviceAccount == nil || serviceAccount.IsEmpty() {
+				if displayBlock == nil {
+					displayBlock = block
+				}
+
+				return []scanner.Result{
+					check.NewResult(
+						fmt.Sprintf("Resource '%s' does not override the default service account. It is recommended to use a minimally privileged service account to run your GKE cluster.", block.FullName()),
+						displayBlock.Range(),
+						scanner.SeverityError,
+					),
+				}
+			}
+
+			return nil
+		},
+	})
+}


Test patch:
diff --git a/internal/app/tfsec/test/gcp012_test.go b/internal/app/tfsec/test/gcp012_test.go
new file mode 100644
index 000000000..fa49618c9
--- /dev/null
+++ b/internal/app/tfsec/test/gcp012_test.go
@@ -0,0 +1,85 @@
+package test
+
+import (
+	"testing"
+
+	"github.com/tfsec/tfsec/internal/app/tfsec/checks"
+	"github.com/tfsec/tfsec/internal/app/tfsec/scanner"
+)
+
+func Test_GCPGKENodeServiceAccount(t *testing.T) {
+
+	var tests = []struct {
+		name                  string
+		source                string
+		mustIncludeResultCode scanner.RuleCode
+		mustExcludeResultCode scanner.RuleCode
+	}{
+		{
+			name: "does not define service account in container cluster",
+			source: `
+resource "google_container_cluster" "my-cluster" {
+  node_config {
+  }
+}
+`,
+			mustIncludeResultCode: checks.GCPGKENodeServiceAccount,
+		},
+		{
+			name: "does not define node_config in container cluster",
+			source: `
+resource "google_container_cluster" "my-cluster" {
+}
+`,
+			mustIncludeResultCode: checks.GCPGKENodeServiceAccount,
+		},
+		{
+			name: "defines service account in container cluster",
+			source: `
+resource "google_container_cluster" "my-cluster" {
+  node_config {
+    service_account = "anything"
+  }
+}
+`,
+			mustExcludeResultCode: checks.GCPGKENodeServiceAccount,
+		},
+		{
+			name: "does not define service account in container node pool",
+			source: `
+resource "google_container_node_pool" "my-np-cluster" {
+  node_config {
+  }
+}
+`,
+			mustIncludeResultCode: checks.GCPGKENodeServiceAccount,
+		},
+		{
+			name: "does not define node_config in container node pool",
+			source: `
+resource "google_container_node_pool" "my-np-cluster" {
+}
+`,
+			mustIncludeResultCode: checks.GCPGKENodeServiceAccount,
+		},
+		{
+			name: "defines service account in container node pool",
+			source: `
+resource "google_container_node_pool" "my-np-cluster" {
+  node_config {
+    service_account = "anything"
+  }
+}
+`,
+			mustExcludeResultCode: checks.GCPGKENodeServiceAccount,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			results := scanSource(test.source)
+			assertCheckCode(t, test.mustIncludeResultCode, test.mustExcludeResultCode, results)
+		})
+	}
+
+}