> ## Documentation Index
> Fetch the complete documentation index at: https://cloudanix.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Ensure GKE Clusters Are Not Running Using The Compute Engine Default Service Account

### More Info:

Run node pools with a dedicated, minimally privileged service account instead of the Compute Engine default account, which has broad project permissions. This reduces the privileges available if a node is compromised.

### Risk Level

Medium

### Address

Security

### Compliance Standards

* CIS GKE

### Triage and Remediation

<Tabs>
  <Tab title="Remediation">
    ### Remediation

    <AccordionGroup>
      <Accordion title="Manual Steps" defaultOpen="true">
        1. **Create a dedicated GKE node service account** (run on any machine with `gcloud` configured for the project)
           ```bash theme={null}
           PROJECT_ID="$(gcloud config get-value project)"
           gcloud iam service-accounts create gke-node-sa \
             --display-name "GKE Node Service Account"

           NODE_SA_EMAIL="$(gcloud iam service-accounts list \
             --format='value(email)' \
             --filter='displayName:GKE Node Service Account')"
           ```

        2. **Grant the required minimal roles to the node service account** (same machine)
           ```bash theme={null}
           gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
             --member "serviceAccount:${NODE_SA_EMAIL}" \
             --role "roles/monitoring.metricWriter"

           gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
             --member "serviceAccount:${NODE_SA_EMAIL}" \
             --role "roles/monitoring.viewer"

           gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
             --member "serviceAccount:${NODE_SA_EMAIL}" \
             --role "roles/logging.logWriter"
           ```

        3. **Identify the cluster and node pool currently using the default service account** (same machine)
           ```bash theme={null}
           PROJECT_ID="$(gcloud config get-value project)"
           CLUSTER_NAME="<YOUR_CLUSTER_NAME>"
           LOCATION="<YOUR_CLUSTER_LOCATION>"   # e.g. us-central1, us-central1-a

           # Get the node pool to migrate (replace if needed)
           NODE_POOL="$(gcloud container clusters describe "${CLUSTER_NAME}" \
             --location "${LOCATION}" --project "${PROJECT_ID}" \
             --format='value(nodePools[0].name)')"

           # Confirm the node pool is using the default SA
           gcloud container node-pools describe "${NODE_POOL}" \
             --cluster "${CLUSTER_NAME}" \
             --location "${LOCATION}" \
             --project "${PROJECT_ID}" \
             --format='value(config.serviceAccount)'
           ```

        4. **Create a new node pool that uses the dedicated service account** (same machine)\
           Choose a new node pool name, e.g. `secure-pool`:
           ```bash theme={null}
           gcloud container node-pools create "secure-pool" \
             --cluster="${CLUSTER_NAME}" \
             --location="${LOCATION}" \
             --service-account="${NODE_SA_EMAIL}" \
             --num-nodes=3
           ```
           Adjust `--num-nodes` and other flags (machine type, autoscaling, etc.) to match the existing pool’s requirements.

        5. **Migrate workloads and delete the old node pool using the default service account** (same machine)\
           a. Cordon and drain nodes in the old pool so workloads move to the new pool:
           ```bash theme={null}
           # Get nodes in the old pool
           gcloud compute instances list \
             --project "${PROJECT_ID}" \
             --filter="metadata['cluster-name']:${CLUSTER_NAME} AND metadata['gke-nodepool']:${NODE_POOL}" \
             --format='value(name,zone)'
           ```
           Then, from any machine with `kubectl` configured for the cluster:
           ```bash theme={null}
           # Example: cordon and drain each node (replace <node_name> with actual node names)
           kubectl cordon <node_name>
           kubectl drain <node_name> --ignore-daemonsets --delete-emptydir-data
           ```
           b. After workloads are running on the new pool, delete the old pool:
           ```bash theme={null}
           gcloud container node-pools delete "${NODE_POOL}" \
             --cluster="${CLUSTER_NAME}" \
             --location="${LOCATION}" \
             --project "${PROJECT_ID}" \
             --quiet
           ```

        6. **Verification** (same machine; confirms no node pool is using the default service account)
           ```bash theme={null}
           PROJECT_ID="$(gcloud config get-value project)"
           CLUSTER_NAME="<YOUR_CLUSTER_NAME>"
           LOCATION="<YOUR_CLUSTER_LOCATION>"

           for NP in $(gcloud container node-pools list \
               --cluster "${CLUSTER_NAME}" \
               --location "${LOCATION}" \
               --project "${PROJECT_ID}" \
               --format='value(name)'); do
             SA="$(gcloud container node-pools describe "${NP}" \
               --cluster "${CLUSTER_NAME}" \
               --location "${LOCATION}" \
               --project "${PROJECT_ID}" \
               --format='value(config.serviceAccount)')"
             echo "Node pool: ${NP}  Service Account: ${SA}"
           done
           ```
           Confirm that none of the listed service accounts is `default`.
      </Accordion>

      <Accordion title="Using kubectl">
        kubectl cannot change which Google Cloud service account GKE nodes use, because this setting is defined on the managed control plane / node pool configuration in GCP (via gcloud, console, or IaC). To remediate this finding, make the changes described in the Manual Steps section using the cloud provider tools rather than kubectl.
      </Accordion>

      <Accordion title="Automation">
        ```bash theme={null}
        #!/usr/bin/env bash
        #
        # Remediate CIS GKE 5.2.1:
        # Ensure GKE node pools are not using the Compute Engine default service account.
        #
        # Requirements (run on any machine with gcloud configured and GKE admin/IAM admin perms):
        #   - gcloud CLI authenticated
        #   - PROJECT_ID, CLUSTER_NAME, LOCATION set in the environment
        #
        # Optional:
        #   - NODE_POOL_FILTER: substring to match node pool names to migrate (default: all)
        #   - DRY_RUN=true to see what would be changed without applying
        #
        # Behavior:
        #   - Creates (or reuses) a minimally privileged node service account
        #   - Creates new node pools using that SA for any node pools using the default SA
        #   - Does NOT delete old node pools (manual step to avoid disruption)
        #   - Verifies all node pools are no longer using the default SA
        #

        set -euo pipefail

        ############################
        # Config and safety checks #
        ############################

        : "${PROJECT_ID:?PROJECT_ID must be set}"
        : "${CLUSTER_NAME:?CLUSTER_NAME must be set}"
        : "${LOCATION:?LOCATION must be set}"

        NODE_SA_NAME="${NODE_SA_NAME:-gke-node-sa}"
        NODE_SA_DISPLAY_NAME="${NODE_SA_DISPLAY_NAME:-GKE Node Service Account}"
        NODE_POOL_FILTER="${NODE_POOL_FILTER:-}"   # if non-empty, only node pools whose name contains this string are processed
        DRY_RUN="${DRY_RUN:-false}"

        echo "Project:      ${PROJECT_ID}"
        echo "Cluster:      ${CLUSTER_NAME}"
        echo "Location:     ${LOCATION}"
        echo "Node SA name: ${NODE_SA_NAME}"
        echo "Dry-run:      ${DRY_RUN}"
        [[ -n "${NODE_POOL_FILTER}" ]] && echo "Node pool filter: ${NODE_POOL_FILTER}"

        gcloud config set project "${PROJECT_ID}" >/dev/null

        ########################################
        # Helper: run or echo depending on DRY #
        ########################################

        run_cmd() {
          if [[ "${DRY_RUN}" == "true" ]]; then
            echo "[DRY-RUN] $*"
          else
            echo "[RUN] $*"
            eval "$@"
          fi
        }

        ###################################
        # Step 1: Ensure node SA exists  #
        ###################################

        echo
        echo "==> Ensuring node service account exists"

        EXISTING_SA_EMAIL="$(gcloud iam service-accounts list \
          --format='value(email)' \
          --filter="email:${NODE_SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" \
          2>/dev/null || true)"

        if [[ -z "${EXISTING_SA_EMAIL}" ]]; then
          run_cmd "gcloud iam service-accounts create ${NODE_SA_NAME} --display-name \"${NODE_SA_DISPLAY_NAME}\""
        else
          echo "Service account already exists: ${EXISTING_SA_EMAIL}"
        fi

        NODE_SA_EMAIL="${NODE_SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"
        echo "Using service account: ${NODE_SA_EMAIL}"

        ###########################################
        # Step 2: Ensure required IAM roles bound #
        ###########################################

        echo
        echo "==> Ensuring required IAM roles on project ${PROJECT_ID}"

        REQUIRED_ROLES=(
          "roles/monitoring.metricWriter"
          "roles/monitoring.viewer"
          "roles/logging.logWriter"
        )

        for ROLE in "${REQUIRED_ROLES[@]}"; do
          echo "Checking role: ${ROLE}"
          BINDING_EXISTS="$(gcloud projects get-iam-policy "${PROJECT_ID}" \
            --format="flatten(bindings[].members) \
                      | filter(bindings.role:'${ROLE}') \
                      | filter(bindings.members:'serviceAccount:${NODE_SA_EMAIL}') \
                      | format('yes')" 2>/dev/null || true)"

          if [[ "${BINDING_EXISTS}" == "yes" ]]; then
            echo "  Binding already present for ${ROLE}"
          else
            run_cmd "gcloud projects add-iam-policy-binding ${PROJECT_ID} \
              --member serviceAccount:${NODE_SA_EMAIL} \
              --role ${ROLE}"
          fi
        done

        #####################################################
        # Step 3: Identify node pools using default SA      #
        #####################################################

        echo
        echo "==> Inspecting node pools on cluster ${CLUSTER_NAME}"

        NODE_POOLS_JSON="$(gcloud container clusters describe "${CLUSTER_NAME}" \
          --location "${LOCATION}" \
          --project "${PROJECT_ID}" \
          --format=json)"

        DEFAULT_NODE_POOLS=()
        MAPFILE -t ALL_POOLS < <(echo "${NODE_POOLS_JSON}" | jq -r '.nodePools[].name')

        for NP in "${ALL_POOLS[@]}"; do
          if [[ -n "${NODE_POOL_FILTER}" ]] && [[ "${NP}" != *"${NODE_POOL_FILTER}"* ]]; then
            echo "Skipping node pool (filter): ${NP}"
            continue
          fi

          SA=$(gcloud container node-pools describe "${NP}" \
            --cluster "${CLUSTER_NAME}" \
            --location "${LOCATION}" \
            --project "${PROJECT_ID}" \
            --format='value(config.serviceAccount)' 2>/dev/null || echo "")

          echo "Node pool: ${NP}  Service Account: ${SA}"

          if [[ "${SA}" == "default" ]] || [[ -z "${SA}" ]]; then
            echo "  -> Marked for remediation (uses default or empty SA)"
            DEFAULT_NODE_POOLS+=("${NP}")
          fi
        done

        if [[ "${#DEFAULT_NODE_POOLS[@]}" -eq 0 ]]; then
          echo
          echo "No node pools using the default service account were found (within filter)."
          echo "Verification: all node pools should show DEFAULT_SA_NOT_IN_USE in the audit."
        else
          echo
          echo "Node pools using default service account:"
          printf '  - %s\n' "${DEFAULT_NODE_POOLS[@]}"
        fi

        ####################################################
        # Step 4: Create new node pools with custom SA     #
        ####################################################

        if [[ "${#DEFAULT_NODE_POOLS[@]}" -gt 0 ]]; then
          echo
          echo "==> Creating replacement node pools using ${NODE_SA_EMAIL}"

          for OLD_NP in "${DEFAULT_NODE_POOLS[@]}"; do
            # Derive a new node pool name that is deterministic and idempotent
            NEW_NP="${OLD_NP}-sa"

            # Check if replacement already exists
            if gcloud container node-pools describe "${NEW_NP}" \
              --cluster "${CLUSTER_NAME}" \
              --location "${LOCATION}" \
              --project "${PROJECT_ID}" >/dev/null 2>&1; then
              echo "Replacement node pool already exists: ${NEW_NP} (skipping create)"
              continue
            fi

            # Get template configuration from old node pool to keep size and machine type
            echo "Preparing replacement for node pool: ${OLD_NP}"

            NODE_COUNT=$(gcloud container node-pools describe "${OLD_NP}" \
              --cluster "${CLUSTER_NAME}" \
              --location "${LOCATION}" \
              --project "${PROJECT_ID}" \
              --format='value(initialNodeCount)' 2>/dev/null || echo "3")

            MACHINE_TYPE=$(gcloud container node-pools describe "${OLD_NP}" \
              --cluster "${CLUSTER_NAME}" \
              --location "${LOCATION}" \
              --project "${PROJECT_ID}" \
              --format='value(config.machineType)' 2>/dev/null || echo "")

            MT_FLAG=""
            if [[ -n "${MACHINE_TYPE}" ]]; then
              MT_FLAG="--machine-type=${MACHINE_TYPE}"
            fi

            echo "  New node pool: ${NEW_NP}"
            echo "  Node count:    ${NODE_COUNT}"
            [[ -n "${MACHINE_TYPE}" ]] && echo "  Machine type:  ${MACHINE_TYPE}"

            run_cmd "gcloud container node-pools create ${NEW_NP} \
              --cluster=${CLUSTER_NAME} \
              --location=${LOCATION} \
              --num-nodes=${NODE_COUNT} \
              ${MT_FLAG} \
              --service-account=${NODE_SA_EMAIL}"
          done

          echo
          echo "NOTE: Workloads must be migrated to the new node pools and old node pools deleted manually once safe."
          echo "      This script intentionally does NOT delete any existing node pools."
        fi

        ########################################
        # Step 5: Verification (CIS-style)     #
        ########################################

        echo
        echo "==> Verification: checking node pool service accounts"

        for NP in "${ALL_POOLS[@]}"; do
          SA=$(gcloud container node-pools describe "${NP}" \
            --cluster "${CLUSTER_NAME}" \
            --location "${LOCATION}" \
            --project "${PROJECT_ID}" \
            --format='value(config.serviceAccount)' 2>/dev/null || echo "")
          STATUS="DEFAULT_SA_NOT_IN_USE"
          if [[ "${SA}" == "default" ]] || [[ -z "${SA}" ]]; then
            STATUS="DEFAULT_SA_IN_USE"
          fi
          echo "Node pool: ${NP}  SA: ${SA:-<none>}  Status: ${STATUS}"
        done

        echo
        echo "Script completed."
        echo "Any node pool still showing DEFAULT_SA_IN_USE must be migrated manually or via a follow-up run."
        ```
      </Accordion>
    </AccordionGroup>
  </Tab>
</Tabs>
