diff --git a/.github/workflows/check-firewall-versions.yaml b/.github/workflows/check-firewall-versions.yaml index 35633d0..a7fe36c 100644 --- a/.github/workflows/check-firewall-versions.yaml +++ b/.github/workflows/check-firewall-versions.yaml @@ -29,30 +29,30 @@ jobs: shell: bash run: | set -euo pipefail - TF=terraform/variables.tf + TF=terraform/terraform.tfvars - read_default() { + read_tfvar() { local val - val=$(sed -n "/variable \"$1\"/,/}/p" "$TF" \ - | grep -E '^\s+default\s*=' | head -1 \ - | sed -E 's/.*"([^"]+)".*/\1/' || true) + val=$(grep -E "^[[:space:]]*$1[[:space:]]*=" "$TF" \ + | head -1 \ + | sed -E 's/.*=[[:space:]]*"([^"]+)".*/\1/' || true) if [ -z "$val" ]; then - echo "::error::variable \"$1\" not found or has no default in $TF" >&2 + echo "::error::$1 not found in $TF" >&2 exit 1 fi echo "$val" } - set_default() { - sed -i "/variable \"$1\"/,/}/ s/default *= *\"[^\"]*\"/default = \"$2\"/" "$TF" + set_tfvar() { + sed -i -E "s/^([[:space:]]*$1[[:space:]]*=[[:space:]]*)\"[^\"]*\"/\1\"$2\"/" "$TF" } newer_than() { [ "$(printf '%s\n' "$1" "$2" | sort -V | tail -1)" = "$2" ] && [ "$1" != "$2" ] } - current_chart=$(read_default helm_chart_version) - current_image=$(read_default firewall_image_tag) + current_chart=$(read_tfvar helm_chart_version) + current_image=$(read_tfvar firewall_image_tag) latest_chart=$(curl -fsSL https://socketdev-demo.github.io/socket-firewall-helm/index.yaml \ | grep -E '^\s+version:' | awk '{print $2}' | sort -V | tail -1) @@ -71,11 +71,11 @@ jobs: summary="" if newer_than "$current_chart" "$latest_chart"; then - set_default helm_chart_version "$latest_chart" + set_tfvar helm_chart_version "$latest_chart" summary+="- \`helm_chart_version\`: \`$current_chart\` → \`$latest_chart\`"$'\n' fi if newer_than "$current_image" "$latest_image"; then - set_default firewall_image_tag "$latest_image" + set_tfvar firewall_image_tag "$latest_image" summary+="- \`firewall_image_tag\`: \`$current_image\` → \`$latest_image\`"$'\n' fi @@ -106,7 +106,6 @@ jobs: ## Follow-up - Review the `terraform plan` check on this PR. - - If you override versions in `terraform/terraform.tfvars`, update those values to match before merging. - Confirm the new image is allowed by Binary Authorization before merging to `main`. Triggered by [check-firewall-versions](.github/workflows/check-firewall-versions.yaml). diff --git a/.github/workflows/tf-apply.yaml b/.github/workflows/tf-apply.yaml index fd1f6ca..2c9832c 100644 --- a/.github/workflows/tf-apply.yaml +++ b/.github/workflows/tf-apply.yaml @@ -38,4 +38,6 @@ jobs: ${{ steps.auth.outcome == 'success'}} with: auto_approve: true - path: terraform \ No newline at end of file + path: terraform + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/tf-plan.yaml b/.github/workflows/tf-plan.yaml index 7a18eab..cb480cd 100644 --- a/.github/workflows/tf-plan.yaml +++ b/.github/workflows/tf-plan.yaml @@ -31,4 +31,6 @@ jobs: ${{ steps.auth.outcome == 'success'}} with: add_github_comment: changes-only - path: terraform \ No newline at end of file + path: terraform + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 78f5334..c848adb 100644 --- a/README.md +++ b/README.md @@ -199,7 +199,7 @@ Roles Terraform grants to the **apply SA**: | `roles/iam.serviceAccountUser` | Attach the node SA to node-pool VMs | | `roles/gkehub.gatewayEditor` + `roles/gkehub.viewer` | Reach the control plane via Connect Gateway; refresh the fleet membership | -The **plan SA** receives read-only equivalents: custom `socketFirewallTfPlanReader`, `roles/container.viewer`, `roles/certificatemanager.viewer`, and `roles/gkehub.gatewayReader`. +The **plan SA** receives read-only equivalents: custom `socketFirewallTfPlanReader`, `roles/container.viewer`, `roles/certificatemanager.viewer`, and `roles/gkehub.gatewayReader`. Because `container.viewer` maps to the `view` ClusterRole (which excludes secrets), a namespace-scoped Kubernetes `Role`/`RoleBinding` in [`kubernetes_rbac.tf`](terraform/kubernetes_rbac.tf) also grants `secrets` get/list/watch in `socket-firewall` for `terraform plan`. Because Terraform creates these custom roles and bindings on the first apply, the bootstrap identity (a project admin) must first grant the apply SA enough elevated access to perform that initial run, then tighten it: diff --git a/terraform/iam.tf b/terraform/iam.tf index b210bc0..3e8f277 100644 --- a/terraform/iam.tf +++ b/terraform/iam.tf @@ -363,7 +363,8 @@ resource "google_project_iam_member" "tf_plan_sa_reader" { } # Kubernetes API read access for the helm/kubernetes/kubectl providers during plan. -# roles/container.viewer maps to the "view" RBAC ClusterRole in GKE. +# roles/container.viewer maps to the "view" RBAC ClusterRole in GKE, which omits +# secrets — see kubernetes_rbac.tf for the namespace-scoped secret read binding. resource "google_project_iam_member" "tf_plan_sa_k8s_viewer" { project = var.project_id role = "roles/container.viewer" diff --git a/terraform/kubernetes_rbac.tf b/terraform/kubernetes_rbac.tf new file mode 100644 index 0000000..1edc897 --- /dev/null +++ b/terraform/kubernetes_rbac.tf @@ -0,0 +1,40 @@ +# --------------------------------------------------------------------------- +# Supplemental Kubernetes RBAC for the plan SA +# --------------------------------------------------------------------------- +# roles/container.viewer maps to the predefined "view" ClusterRole, which +# deliberately omits secrets. Terraform plan still refreshes +# kubernetes_secret.socket_api_token (helm.tf), which calls the Kubernetes API +# secrets.get endpoint. Grant read-only secret access in this namespace only. +# --------------------------------------------------------------------------- + +resource "kubernetes_role" "tf_plan_secret_reader" { + metadata { + name = "terraform-plan-secret-reader" + namespace = kubernetes_namespace.socket_firewall.metadata[0].name + } + + rule { + api_groups = [""] + resources = ["secrets"] + verbs = ["get", "list", "watch"] + } +} + +resource "kubernetes_role_binding" "tf_plan_secret_reader" { + metadata { + name = "terraform-plan-secret-reader" + namespace = kubernetes_namespace.socket_firewall.metadata[0].name + } + + role_ref { + api_group = "rbac.authorization.k8s.io" + kind = "Role" + name = kubernetes_role.tf_plan_secret_reader.metadata[0].name + } + + subject { + kind = "User" + name = var.terraformer_plan + api_group = "rbac.authorization.k8s.io" + } +} diff --git a/terraform/terraform.tfvars b/terraform/terraform.tfvars index 6eea54c..11809dd 100644 --- a/terraform/terraform.tfvars +++ b/terraform/terraform.tfvars @@ -13,5 +13,5 @@ node_max_count = 3 firewall_domain = "sfw.security.sentry.io." replica_count = 2 -helm_chart_version = "0.2.4" -firewall_image_tag = "1.1.159" +helm_chart_version = "0.3.0" +firewall_image_tag = "1.1.327" diff --git a/terraform/variables.tf b/terraform/variables.tf index ca41a5f..507e98e 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -118,13 +118,11 @@ variable "firewall_domain" { variable "helm_chart_version" { description = "Version of the socket-firewall Helm chart" type = string - default = "0.2.4" } variable "firewall_image_tag" { description = "Container image tag for socketdev/socket-registry-firewall (pinned for reproducible rollouts; bumped by check-firewall-versions workflow)" type = string - default = "1.1.159" } variable "replica_count" {