Repository navigation
sec(iac/gcp): GKE cluster defaults to private nodes with a closed public endpoint #371
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -77,6 +77,45 @@ resource "google_container_cluster" "main" { | |||||
| services_ipv4_cidr_block = "" | ||||||
| } | ||||||
|
|
||||||
| # Private nodes always (no reason for nodes to hold public IPs); the | ||||||
| # control plane's own public endpoint is a separate toggle | ||||||
| # (var.enable_private_endpoint) since fully removing it requires VPC | ||||||
| # peering/a bastion for kubectl access, which this module does not set up. | ||||||
| # Either way, master_authorized_networks_config below denies every public | ||||||
| # IP by default, so a public endpoint is unreachable until an operator | ||||||
| # explicitly allowlists something. | ||||||
| # | ||||||
| # master_ipv4_cidr_block is required by the provider whenever | ||||||
| # enable_private_nodes = true on a Standard cluster and neither this nor | ||||||
| # private_endpoint_subnetwork is set: terraform validate/plan do not catch | ||||||
| # the omission (a provider-side check at create), apply fails outright with | ||||||
| # "master_ipv4_cidr_block or private_endpoint_subnetwork is required". | ||||||
| private_cluster_config { | ||||||
| enable_private_nodes = var.enable_private_nodes | ||||||
| enable_private_endpoint = var.enable_private_endpoint | ||||||
| master_ipv4_cidr_block = var.master_ipv4_cidr_block | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Omit the master CIDR when private nodes are disabled. If a caller sets Proposed change- master_ipv4_cidr_block = var.master_ipv4_cidr_block
+ master_ipv4_cidr_block = var.enable_private_nodes ? var.master_ipv4_cidr_block : null📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| } | ||||||
|
|
||||||
| # No entries and gcp_public_cidrs_access_enabled = false = the public | ||||||
| # endpoint (when enabled) accepts no external traffic at all; only | ||||||
| # in-VPC/private access works. Google Cloud's own public IP ranges bypass | ||||||
| # cidr_blocks entirely unless gcp_public_cidrs_access_enabled is explicitly | ||||||
| # false -- the provider defaults it to true, which would otherwise let any | ||||||
| # Google Cloud public IP reach the endpoint even with an empty allowlist. | ||||||
| # An operator opts in to public kubectl access by adding CIDRs to | ||||||
| # var.master_authorized_networks (rejects 0.0.0.0/0). | ||||||
| master_authorized_networks_config { | ||||||
| gcp_public_cidrs_access_enabled = var.gcp_public_cidrs_access_enabled | ||||||
|
|
||||||
| dynamic "cidr_blocks" { | ||||||
| for_each = var.master_authorized_networks | ||||||
| content { | ||||||
| cidr_block = cidr_blocks.value.cidr_block | ||||||
| display_name = cidr_blocks.value.display_name | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| # Workload Identity | ||||||
| workload_identity_config { | ||||||
| workload_pool = var.enable_workload_identity ? "${var.project_id}.svc.id.goog" : null | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| // Regression test for issue #125: google_container_cluster.main declared no | ||
| // private_cluster_config and no master_authorized_networks_config, so the | ||
| // GKE control plane got a public endpoint accepting connections from | ||
| // 0.0.0.0/0 and nodes got public IPs. | ||
| package gcp_test | ||
|
|
||
| import ( | ||
| "os" | ||
| "regexp" | ||
| "testing" | ||
| ) | ||
|
|
||
| func TestGKEClusterIsPrivateWithAuthorizedNetworks(t *testing.T) { | ||
| data, err := os.ReadFile("gke/main.tf") | ||
| if err != nil { | ||
| t.Fatalf("reading gke/main.tf: %v", err) | ||
| } | ||
| content := string(data) | ||
|
|
||
| if !regexp.MustCompile(`private_cluster_config\s*\{[^}]*enable_private_nodes\s*=\s*var\.enable_private_nodes`).MatchString(content) { | ||
| t.Error(`google_container_cluster.main must have a private_cluster_config block with enable_private_nodes wired from a variable (issue #125); nodes were getting public IPs`) | ||
| } | ||
|
|
||
| if !regexp.MustCompile(`master_authorized_networks_config\s*\{[^}]*gcp_public_cidrs_access_enabled\s*=\s*var\.gcp_public_cidrs_access_enabled`).MatchString(content) { | ||
| t.Error(`google_container_cluster.main's master_authorized_networks_config must set gcp_public_cidrs_access_enabled from a variable (issue #125 round 2); the provider defaults this to true, so an empty cidr_blocks allowlist alone does NOT deny all external access -- any Google Cloud public IP could still reach the endpoint`) | ||
| } | ||
|
|
||
| if !regexp.MustCompile(`private_cluster_config\s*\{[^}]*master_ipv4_cidr_block\s*=\s*var\.master_ipv4_cidr_block`).MatchString(content) { | ||
| t.Error(`google_container_cluster.main's private_cluster_config must set master_ipv4_cidr_block from a variable (issue #125 round 2); the google provider rejects enable_private_nodes=true on a Standard cluster at apply time (not caught by validate/plan) unless master_ipv4_cidr_block or private_endpoint_subnetwork is set`) | ||
| } | ||
|
|
||
| varsData, err := os.ReadFile("gke/variables.tf") | ||
| if err != nil { | ||
| t.Fatalf("reading gke/variables.tf: %v", err) | ||
| } | ||
| vars := string(varsData) | ||
|
|
||
| if !regexp.MustCompile(`variable\s+"enable_private_nodes"\s*\{[^}]*default\s*=\s*true`).MatchString(vars) { | ||
| t.Error(`var.enable_private_nodes must default to true, so a caller who does not override it gets private nodes`) | ||
| } | ||
|
|
||
| if !regexp.MustCompile(`variable\s+"master_authorized_networks"[\s\S]*?contains\(\[for n in var\.master_authorized_networks[^\]]*\],\s*"0\.0\.0\.0/0"\)`).MatchString(vars) { | ||
| t.Error(`var.master_authorized_networks must validate against "0.0.0.0/0", so the control plane's public endpoint can't be silently reopened to the whole internet`) | ||
| } | ||
|
|
||
| if !regexp.MustCompile(`variable\s+"gcp_public_cidrs_access_enabled"\s*\{[^}]*default\s*=\s*false`).MatchString(vars) { | ||
| t.Error(`var.gcp_public_cidrs_access_enabled must default to false, or the empty master_authorized_networks default does not actually deny all external access`) | ||
| } | ||
|
|
||
| if !regexp.MustCompile(`variable\s+"master_ipv4_cidr_block"\s*\{[\s\S]*?tonumber\(split\("/",\s*var\.master_ipv4_cidr_block\)\[1\]\)\s*==\s*28`).MatchString(vars) { | ||
| t.Error(`var.master_ipv4_cidr_block must validate that its value is a /28 CIDR`) | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.