Repository: liamg/tfsec
Pull request #136 (2020-07-09 19:16:47)

[GKE Checks] Add additional security checks for k8s
👋 
Thought I'd add some more GKE security checks. I'll do my best to implement any feedback! If this gets added I can create some wiki pages for these rules to provide more context.

### What checks have been added?
- GCP006: Checks if `node_metadata` in `workload_metadata_config` is set to `EXPOSE` or `UNSPECIFIED`
> setting the flag to EXPOSED or UNSPECIFIED disables metadata concealment.
https://cloud.google.com/kubernetes-engine/docs/how-to/protecting-cluster-metadata#create-concealed


- GCP007: Checks if legacy metadata endpoints have been disabled
> Compute Engine's instance metadata server exposes legacy /0.1/ and /v1beta1/ endpoints, which do not enforce metadata query headers.
https://cloud.google.com/kubernetes-engine/docs/how-to/protecting-cluster-metadata#disable-legacy-apis

- GCP008: Checks for use of risky legacy client authentication methods with static user/pass or client certs

> pre-provisioned x509 certificate or static password were the only available authentication methods, but are now not recommended and disabled by default on new clusters since 1.12+
https://cloud.google.com/kubernetes-engine/docs/how-to/hardening-your-cluster#restrict_authn_methods

- GCP009: Checks whether pod security policy enforcement has been enabled for a cluster. Implementing this requires more than enabling this option, but a wiki page containing a more guidance can be done up.

> By default, Pods in Kubernetes can operate with capabilities beyond what they require. You should constrain the Pod's capabilities to only those required for that workload.
https://cloud.google.com/kubernetes-engine/docs/how-to/hardening-your-cluster#admission_controllers

- GCP010: Checks if shielded GKE nodes have been enabled

> Shielded GKE Nodes provide strong, verifiable node identity and integrity to increase the security of GKE nodes and should be enabled on all GKE clusters.
https://cloud.google.com/kubernetes-engine/docs/how-to/hardening-your-cluster#shielded_nodes


Patch:
diff --git a/README.md b/README.md
index 3b5bd2e66..387ae3b77 100644
--- a/README.md
+++ b/README.md
@@ -166,6 +166,11 @@ there are also checks which are provider agnostic.
 | GCP003  | google   | An inbound firewall rule allows traffic from `/0`.
 | GCP004  | google   | An outbound firewall rule allows traffic to `/0`.
 | GCP005  | google   | Legacy ABAC permissions are enabled.
+| GCP006  | google   | Node metadata value disables metadata concealment.
+| GCP007  | google   | Legacy metadata endpoints enabled.
+| GCP008  | google   | Legacy client authentication methods utilized.
+| GCP009  | google   | Pod security policy enforcement not defined.
+| GCP010  | google   | Shielded GKE nodes not enabled.
 
 ## Running in CI
 
@@ -177,7 +182,7 @@ American friends).
 
 ## Output options
 
-You can output tfsec results as JSON, CSV, Checkstyle, JUnit or just plain old human readable format. Use the `--format` flag 
+You can output tfsec results as JSON, CSV, Checkstyle, JUnit or just plain old human readable format. Use the `--format` flag
 to specify your desired format.
 
 ## Support for older terraform versions
diff --git a/internal/app/tfsec/checks/gke_enforce_psp.go b/internal/app/tfsec/checks/gke_enforce_psp.go
new file mode 100644
index 000000000..1392ea34c
--- /dev/null
+++ b/internal/app/tfsec/checks/gke_enforce_psp.go
@@ -0,0 +1,46 @@
+package checks
+
+import (
+	"fmt"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/parser"
+	"github.com/liamg/tfsec/internal/app/tfsec/scanner"
+	"github.com/zclconf/go-cty/cty"
+)
+
+// GkeEnforcePSP See https://github.com/liamg/tfsec#included-checks for check info
+const GkeEnforcePSP scanner.RuleID = "GCP009"
+
+func init() {
+	scanner.RegisterCheck(scanner.Check{
+		Code:           GkeEnforcePSP,
+		RequiredTypes:  []string{"resource"},
+		RequiredLabels: []string{"google_container_cluster"},
+		CheckFunc: func(check *scanner.Check, block *parser.Block, _ *scanner.Context) []scanner.Result {
+
+			pspBlock := block.GetBlock("pod_security_policy_config")
+			if pspBlock == nil {
+				return []scanner.Result{
+					check.NewResult(
+						fmt.Sprintf("Resource '%s' defines a cluster with no Pod Security Policy config defined. It is recommended to define a PSP for your pods and enable PSP enforcement. https://cloud.google.com/kubernetes-engine/docs/how-to/hardening-your-cluster#admission_controllers", block.Name()),
+						block.Range(),
+						scanner.SeverityError,
+					),
+				}
+			}
+
+			enforcePSP := pspBlock.GetAttribute("enabled")
+      if enforcePSP.Type() == cty.Bool && enforcePSP.Value().False() || enforcePSP.Type() == cty.String && enforcePSP.Value().AsString() != "true" {
+				return []scanner.Result{
+					check.NewResult(
+						fmt.Sprintf("Resource '%s' defines a cluster with Pod Security Policy enforcement disabled. It is recommended to define a PSP for your pods and enable PSP enforcement. https://cloud.google.com/kubernetes-engine/docs/how-to/hardening-your-cluster#admission_controllers", block.Name()),
+						enforcePSP.Range(),
+						scanner.SeverityError,
+					),
+				}
+			}
+
+			return nil
+		},
+	})
+}
diff --git a/internal/app/tfsec/checks/gke_legacy_auth_enabled.go b/internal/app/tfsec/checks/gke_legacy_auth_enabled.go
new file mode 100644
index 000000000..b59c24775
--- /dev/null
+++ b/internal/app/tfsec/checks/gke_legacy_auth_enabled.go
@@ -0,0 +1,57 @@
+package checks
+
+import (
+	"fmt"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/parser"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/scanner"
+
+	"github.com/zclconf/go-cty/cty"
+)
+
+// GkeLegacyAuthEnabled See https://github.com/liamg/tfsec#included-checks for check info
+const GkeLegacyAuthEnabled scanner.RuleID = "GCP008"
+
+func init() {
+	scanner.RegisterCheck(scanner.Check{
+		Code:           GkeLegacyAuthEnabled,
+		RequiredTypes:  []string{"resource"},
+		RequiredLabels: []string{"google_container_cluster"},
+		CheckFunc: func(check *scanner.Check, block *parser.Block, _ *scanner.Context) []scanner.Result {
+
+			masterAuthBlock := block.GetBlock("master_auth")
+			staticAuthUser := masterAuthBlock.GetAttribute("username")
+			staticAuthPass := masterAuthBlock.GetAttribute("password")
+			if masterAuthBlock == nil {
+				return []scanner.Result{
+					check.NewResult(
+						fmt.Sprintf("Resource '%s' does not disable basic auth with static passwords for client authentication. Disable this with a master_auth block container empty strings for user and password. https://cloud.google.com/kubernetes-engine/docs/how-to/hardening-your-cluster#restrict_authn_methods", block.Name()),
+						block.Range(),
+						scanner.SeverityError,
+					),
+				}
+			} else if staticAuthUser.Type() == cty.String && staticAuthUser.Value().AsString() != "" && staticAuthPass.Type() == cty.String && staticAuthPass.Value().AsString() != "" {
+				return []scanner.Result{
+					check.NewResult(
+						fmt.Sprintf("Resource '%s' defines a cluster using basic auth with static passwords for client authentication. It is recommended to use OAuth or service accounts instead. https://cloud.google.com/kubernetes-engine/docs/how-to/hardening-your-cluster#restrict_authn_methods", block.Name()),
+						masterAuthBlock.Range(),
+						scanner.SeverityError,
+					),
+				}
+			}
+			issueClientCert := masterAuthBlock.GetBlock("client_certificate_config").GetAttribute("issue_client_certificate")
+			if issueClientCert.Type() == cty.Bool && issueClientCert.Value().True() || issueClientCert.Type() == cty.String && issueClientCert.Value().AsString() == "true" {
+				return []scanner.Result{
+					check.NewResult(
+						fmt.Sprintf("Resource '%s' defines a cluster using basic auth with client certificates for authentication. This cert has no permissions if RBAC is enabled and ABAC is disabled. It is recommended to use OAuth or service accounts instead. https://cloud.google.com/kubernetes-engine/docs/how-to/hardening-your-cluster#restrict_authn_methods", block.Name()),
+						issueClientCert.Range(),
+						scanner.SeverityError,
+					),
+				}
+			}
+
+			return nil
+		},
+	})
+}
diff --git a/internal/app/tfsec/checks/gke_legacy_metadata_endpoints.go b/internal/app/tfsec/checks/gke_legacy_metadata_endpoints.go
new file mode 100644
index 000000000..8d7bc2799
--- /dev/null
+++ b/internal/app/tfsec/checks/gke_legacy_metadata_endpoints.go
@@ -0,0 +1,37 @@
+package checks
+
+import (
+	"fmt"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/parser"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/scanner"
+
+	"github.com/zclconf/go-cty/cty"
+)
+
+// GkeLegacyMetadataEndpoints See https://github.com/liamg/tfsec#included-checks for check info
+const GkeLegacyMetadataEndpoints scanner.RuleID = "GCP007"
+
+func init() {
+	scanner.RegisterCheck(scanner.Check{
+		Code:           GkeLegacyMetadataEndpoints,
+		RequiredTypes:  []string{"resource"},
+		RequiredLabels: []string{"google_container_cluster"},
+		CheckFunc: func(check *scanner.Check, block *parser.Block, _ *scanner.Context) []scanner.Result {
+
+			legacyMetadataAPI := block.GetBlock("metadata").GetAttribute("disable-legacy-endpoints")
+      if legacyMetadataAPI.Type() == cty.String && legacyMetadataAPI.Value().AsString() != "true" || legacyMetadataAPI.Type() == cty.Bool && legacyMetadataAPI.Value().False() {
+				return []scanner.Result{
+					check.NewResult(
+						fmt.Sprintf("Resource '%s' defines a cluster with legacy metadata endpoints enabled. See: https://cloud.google.com/kubernetes-engine/docs/how-to/hardening-your-cluster#protect_node_metadata_default_for_112", block.Name()),
+						legacyMetadataAPI.Range(),
+						scanner.SeverityError,
+					),
+				}
+			}
+
+			return nil
+		},
+	})
+}
diff --git a/internal/app/tfsec/checks/gke_node_metadata_exposed.go b/internal/app/tfsec/checks/gke_node_metadata_exposed.go
new file mode 100644
index 000000000..e3d30530e
--- /dev/null
+++ b/internal/app/tfsec/checks/gke_node_metadata_exposed.go
@@ -0,0 +1,38 @@
+package checks
+
+import (
+	"fmt"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/parser"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/scanner"
+
+	"github.com/zclconf/go-cty/cty"
+)
+
+// GkeNodeMetadataExposed See https://github.com/liamg/tfsec#included-checks for check info
+const GkeNodeMetadataExposed scanner.RuleID = "GCP006"
+
+func init() {
+	scanner.RegisterCheck(scanner.Check{
+		Code:           GkeNodeMetadataExposed,
+		RequiredTypes:  []string{"resource"},
+		RequiredLabels: []string{"google_container_cluster"},
+		CheckFunc: func(check *scanner.Check, block *parser.Block, _ *scanner.Context) []scanner.Result {
+
+			nodeMetadata := block.GetBlock("workload_metadata_config").GetAttribute("node_metadata")
+
+      if nodeMetadata.Type() == cty.String && nodeMetadata.Value().AsString() == "EXPOSE" || nodeMetadata.Type() == cty.String && nodeMetadata.Value().AsString() == "UNSPECIFIED" {
+				return []scanner.Result{
+					check.NewResult(
+						fmt.Sprintf("Resource '%s' defines a cluster with node metadata exposed. node_metadata set to EXPOSE or UNSPECIFIED disables metadata concealment. https://cloud.google.com/kubernetes-engine/docs/how-to/protecting-cluster-metadata#create-concealed", block.Name()),
+						nodeMetadata.Range(),
+						scanner.SeverityError,
+					),
+				}
+			}
+
+			return nil
+		},
+	})
+}
diff --git a/internal/app/tfsec/checks/gke_shielded_nodes_disabled.go b/internal/app/tfsec/checks/gke_shielded_nodes_disabled.go
new file mode 100644
index 000000000..ca1b5e64b
--- /dev/null
+++ b/internal/app/tfsec/checks/gke_shielded_nodes_disabled.go
@@ -0,0 +1,48 @@
+package checks
+
+import (
+	"fmt"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/parser"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/scanner"
+	
+	"github.com/zclconf/go-cty/cty"
+)
+
+// GkeShieldedNodesDisabled See https://github.com/liamg/tfsec#included-checks for check info
+const GkeShieldedNodesDisabled scanner.RuleID = "GCP010"
+
+func init() {
+	scanner.RegisterCheck(scanner.Check{
+		Code:           GkeShieldedNodesDisabled,
+		RequiredTypes:  []string{"resource"},
+		RequiredLabels: []string{"google_container_cluster"},
+		CheckFunc: func(check *scanner.Check, block *parser.Block, _ *scanner.Context) []scanner.Result {
+
+			enable_shielded_nodes := block.GetAttribute("enable_shielded_nodes")
+
+			if enable_shielded_nodes == nil {
+				return []scanner.Result{
+					check.NewResult(
+						fmt.Sprintf("Resource '%s' defines a cluster with shielded nodes disabled. Shielded GKE Nodes provide strong, verifiable node identity and integrity to increase the security of GKE nodes and should be enabled on all GKE clusters. https://cloud.google.com/kubernetes-engine/docs/how-to/hardening-your-cluster#shielded_nodes", block.Name()),
+						block.Range(),
+						scanner.SeverityError,
+					),
+				}
+			}
+
+			if enable_shielded_nodes.Type() == cty.Bool && enable_shielded_nodes.Value().False() || enable_shielded_nodes.Type() == cty.String && enable_shielded_nodes.Value().AsString() != "true" {
+				return []scanner.Result{
+					check.NewResult(
+						fmt.Sprintf("Resource '%s' defines a cluster with shielded nodes disabled. Shielded GKE Nodes provide strong, verifiable node identity and integrity to increase the security of GKE nodes and should be enabled on all GKE clusters. https://cloud.google.com/kubernetes-engine/docs/how-to/hardening-your-cluster#shielded_nodes", block.Name()),
+						enable_shielded_nodes.Range(),
+						scanner.SeverityError,
+					),
+				}
+			}
+
+			return nil
+		},
+	})
+}


Test patch:
diff --git a/internal/app/tfsec/gke_enforce_psp_test.go b/internal/app/tfsec/gke_enforce_psp_test.go
new file mode 100644
index 000000000..d073bec59
--- /dev/null
+++ b/internal/app/tfsec/gke_enforce_psp_test.go
@@ -0,0 +1,56 @@
+package tfsec
+
+import (
+	"testing"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/scanner"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/checks"
+)
+
+func Test_GkeEnforcePSPTest(t *testing.T) {
+
+	var tests = []struct {
+		name                  string
+		source                string
+		mustIncludeResultCode scanner.RuleID
+		mustExcludeResultCode scanner.RuleID
+	}{
+		{
+			name: "check google_container_cluster with pod_security_policy_config.enabled set to false",
+			source: `
+resource "google_container_cluster" "gke" {
+	pod_security_policy_config {
+    enabled = "false"
+  }
+}`,
+			mustIncludeResultCode: checks.GkeEnforcePSP,
+		},
+		{
+			name: "check google_container_cluster with pod_security_policy_config block not set",
+			source: `
+resource "google_container_cluster" "gke" {
+
+}`,
+			mustIncludeResultCode: checks.GkeEnforcePSP,
+		},
+		{
+			name: "check google_container_cluster with pod_security_policy_config.enabled set to true",
+			source: `
+resource "google_container_cluster" "gke" {
+	pod_security_policy_config {
+    enabled = "true"
+  }
+}`,
+			mustExcludeResultCode: checks.GkeEnforcePSP,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			results := scanSource(test.source)
+			assertCheckCode(t, test.mustIncludeResultCode, test.mustExcludeResultCode, results)
+		})
+	}
+
+}
diff --git a/internal/app/tfsec/gke_legacy_auth_enabled_test.go b/internal/app/tfsec/gke_legacy_auth_enabled_test.go
new file mode 100644
index 000000000..6a1b7cfb4
--- /dev/null
+++ b/internal/app/tfsec/gke_legacy_auth_enabled_test.go
@@ -0,0 +1,62 @@
+package tfsec
+
+import (
+	"testing"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/scanner"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/checks"
+)
+
+func Test_GkeLegacyAuthEnabled(t *testing.T) {
+
+	var tests = []struct {
+		name                  string
+		source                string
+		mustIncludeResultCode scanner.RuleID
+		mustExcludeResultCode scanner.RuleID
+	}{
+		{
+			name: "check google_container_cluster with master_auth static user/pass not disable",
+			source: `
+resource "google_container_cluster" "gke" {
+
+}`,
+			mustIncludeResultCode: checks.GkeLegacyAuthEnabled,
+		},
+		{
+			name: "check google_container_cluster with master_auth static user/pass disabled",
+			source: `
+resource "google_container_cluster" "gke" {
+	master_auth {
+    username = ""
+    password = ""
+	}
+}`,
+			mustExcludeResultCode: checks.GkeLegacyAuthEnabled,
+		},
+		{
+			name: "check google_container_cluster with client cert enabled and master_auth static user/pass disabled",
+			source: `
+resource "google_container_cluster" "gke" {
+	master_auth {
+    username = ""
+    password = ""
+		client_certificate_config {
+      issue_client_certificate = true
+    }
+	}
+}`,
+			mustIncludeResultCode: checks.GkeLegacyAuthEnabled,
+		},
+
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			results := scanSource(test.source)
+			assertCheckCode(t, test.mustIncludeResultCode, test.mustExcludeResultCode, results)
+		})
+	}
+
+}
diff --git a/internal/app/tfsec/gke_legacy_metadata_endpoints_test.go b/internal/app/tfsec/gke_legacy_metadata_endpoints_test.go
new file mode 100644
index 000000000..79a314cd7
--- /dev/null
+++ b/internal/app/tfsec/gke_legacy_metadata_endpoints_test.go
@@ -0,0 +1,48 @@
+package tfsec
+
+import (
+	"testing"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/scanner"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/checks"
+)
+
+func Test_GkeLegacyMetadataEndpoints(t *testing.T) {
+
+	var tests = []struct {
+		name                  string
+		source                string
+		mustIncludeResultCode scanner.RuleID
+		mustExcludeResultCode scanner.RuleID
+	}{
+		{
+			name: "check google_container_cluster with metadata.disable-legacy-endpoints set to false",
+			source: `
+resource "google_container_cluster" "gke" {
+	metadata {
+    disable-legacy-endpoints = false
+  }
+}`,
+			mustIncludeResultCode: checks.GkeLegacyMetadataEndpoints,
+		},
+		{
+			name: "check google_container_cluster with metadata.disable-legacy-endpoints set to true",
+			source: `
+resource "google_container_cluster" "gke" {
+	metadata {
+    disable-legacy-endpoints = true
+  }
+}`,
+			mustExcludeResultCode: checks.GkeLegacyMetadataEndpoints,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			results := scanSource(test.source)
+			assertCheckCode(t, test.mustIncludeResultCode, test.mustExcludeResultCode, results)
+		})
+	}
+
+}
diff --git a/internal/app/tfsec/gke_node_metadata_exposed_test.go b/internal/app/tfsec/gke_node_metadata_exposed_test.go
new file mode 100644
index 000000000..a736b0233
--- /dev/null
+++ b/internal/app/tfsec/gke_node_metadata_exposed_test.go
@@ -0,0 +1,48 @@
+package tfsec
+
+import (
+	"testing"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/scanner"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/checks"
+)
+
+func Test_GkeNodeMetadataExposed(t *testing.T) {
+
+	var tests = []struct {
+		name                  string
+		source                string
+		mustIncludeResultCode scanner.RuleID
+		mustExcludeResultCode scanner.RuleID
+	}{
+		{
+			name: "check google_container_cluster with workload_metadata_config.node_metadata set to EXPOSE",
+			source: `
+resource "google_container_cluster" "gke" {
+	workload_metadata_config {
+		node_metadata = "EXPOSE"
+		}
+}`,
+			mustIncludeResultCode: checks.GkeNodeMetadataExposed,
+		},
+		{
+			name: "check google_container_cluster with workload_metadata_config.node_metadata set to UNSPECIFIED",
+			source: `
+resource "google_container_cluster" "gke" {
+	workload_metadata_config {
+		node_metadata = "UNSPECIFIED"
+		}
+}`,
+			mustIncludeResultCode: checks.GkeNodeMetadataExposed,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			results := scanSource(test.source)
+			assertCheckCode(t, test.mustIncludeResultCode, test.mustExcludeResultCode, results)
+		})
+	}
+
+}
diff --git a/internal/app/tfsec/gke_shielded_nodes_disabled_test.go b/internal/app/tfsec/gke_shielded_nodes_disabled_test.go
new file mode 100644
index 000000000..47e8dd8ae
--- /dev/null
+++ b/internal/app/tfsec/gke_shielded_nodes_disabled_test.go
@@ -0,0 +1,53 @@
+package tfsec
+
+import (
+	"testing"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/scanner"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/checks"
+)
+
+func Test_GkeShieldedNodesDisabled(t *testing.T) {
+
+	var tests = []struct {
+		name                  string
+		source                string
+		mustIncludeResultCode scanner.RuleID
+		mustExcludeResultCode scanner.RuleID
+	}{
+		{
+			name: "check google_container_cluster with enable_shielded_nodes set to false",
+			source: `
+resource "google_container_cluster" "gke" {
+	enable_shielded_nodes = "false"
+
+}`,
+			mustIncludeResultCode: checks.GkeShieldedNodesDisabled,
+		},
+		{
+			name: "check google_container_cluster with enable_shielded_nodes not set",
+			source: `
+resource "google_container_cluster" "gke" {
+}`,
+			mustIncludeResultCode: checks.GkeShieldedNodesDisabled,
+		},
+		{
+			name: "check google_container_cluster with enable_shielded_nodes set to true",
+			source: `
+resource "google_container_cluster" "gke" {
+	enable_shielded_nodes = "true"
+
+}`,
+			mustExcludeResultCode: checks.GkeShieldedNodesDisabled,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			results := scanSource(test.source)
+			assertCheckCode(t, test.mustIncludeResultCode, test.mustExcludeResultCode, results)
+		})
+	}
+
+}