> ## 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.

# Minimize Cluster Access To Read-Only For Container Image Repositories

### More Info:

Grant the clusters service account only read access to container image repositories, not write or admin. Read-only access limits the blast radius if node credentials are 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. **Identify which registries and service accounts your GKE nodes use**
           * On any machine with `gcloud` access, list your node service accounts and note which ones pull images (often the default compute SA):
             ```bash theme={null}
             gcloud container clusters describe <cluster-name> \
               --region <region> \
               --format="value(nodeConfig.serviceAccount)"
             ```
             If you use Workload Identity or custom node pools, also list any additional service accounts used to pull images.

        2. **Review Artifact Registry (AR) IAM for node / puller service accounts**
           * On any machine with `gcloud` access, for each Artifact Registry repository that stores images used by the cluster:
             ```bash theme={null}
             gcloud artifacts repositories get-iam-policy <repository> \
               --location=<repository-location> \
               --format="table(bindings.role, bindings.members)"
             ```
           * Confirm that any member matching your node or image-puller service accounts has at most `roles/artifactregistry.reader`.
           * If you find broader roles (for example, `roles/artifactregistry.writer`, `roles/artifactregistry.admin`, `roles/owner`, `roles/editor`), plan to replace them with `roles/artifactregistry.reader` before removal to avoid breaking image pulls.

        3. **Review GCR (GCS bucket) IAM for explicitly granted accounts**
           * On any machine with `gsutil` access, for each GCR bucket (commonly `gs://artifacts.<project_id>.appspot.com` or regional equivalents):
             ```bash theme={null}
             gsutil iam get gs://artifacts.<project_id>.appspot.com
             ```
           * Inspect the output for entries granting your node or image-puller service accounts any of:
             * `roles/storage.admin`
             * `roles/storage.objectAdmin`
             * `roles/storage.objectCreator`
             * `roles/storage.legacyBucketOwner`
             * `roles/storage.legacyBucketWriter`
             * `roles/storage.legacyObjectOwner`
           * Ensure those accounts instead have only `roles/storage.objectViewer` (or are covered by a narrower custom read-only role, if used).

        4. **Review project-level IAM that may grant indirect write/admin access to registries**
           * On any machine with `gcloud` access, run:
             ```bash theme={null}
             gcloud projects get-iam-policy <project_id> \
               --flatten="bindings[].members" \
               --format='table(bindings.members,bindings.role)' \
               --filter="bindings.role:roles/storage.admin OR bindings.role:roles/storage.objectAdmin OR bindings.role:roles/storage.objectCreator OR bindings.role:roles/storage.legacyBucketOwner OR bindings.role:roles/storage.legacyBucketWriter OR bindings.role:roles/storage.legacyObjectOwner"
             ```
           * Check whether any node or image-puller service accounts (or groups containing them) appear in the output. If so, they inherit write/admin rights to GCR buckets and must be removed or replaced with read-only roles at project level.

        5. **Decide and apply least-privilege changes in console/CLI/IaC**
           * For AR: for each affected service account, add `roles/artifactregistry.reader` at the repository (or minimally scoping level) and then remove any broader AR roles.
           * For GCR buckets: for each affected service account, add `objectViewer` using `gsutil iam ch`, then delete the excessive role entries (`storage.admin`, `storage.objectAdmin`, etc.) using `gsutil iam ch -d`.
           * For project-level IAM: update IAM bindings (via console or by editing your IAM policy / Terraform / other IaC) so that node/image-puller service accounts no longer have storage admin/write roles, while ensuring other workloads that legitimately push images retain required permissions.

        6. **Verify the remediation and cluster functionality**
           * Re-run the project-level audit to confirm no node or image-puller service accounts hold the listed storage write/admin roles:
             ```bash theme={null}
             gcloud projects get-iam-policy <project_id> \
               --flatten="bindings[].members" \
               --format='table(bindings.members,bindings.role)' \
               --filter="bindings.role:roles/storage.admin OR bindings.role:roles/storage.objectAdmin OR bindings.role:roles/storage.objectCreator OR bindings.role:roles/storage.legacyBucketOwner OR bindings.role:roles/storage.legacyBucketWriter OR bindings.role:roles/storage.legacyObjectOwner"
             ```
           * Optionally, re-check AR and GCR bucket IAM as in steps 2–3, then deploy a test pod in the cluster that pulls an image from each registry to ensure image pulls still succeed with read-only access.
      </Accordion>

      <Accordion title="Using kubectl">
        kubectl cannot be used to change container image repository IAM or project-level IAM; this finding is remediated in the Google Cloud console / gcloud CLI / IaC that manages Artifact Registry, GCR, and project IAM. Refer to the Manual Steps section for the exact `gcloud` and `gsutil` commands to adjust roles to read-only access.
      </Accordion>

      <Accordion title="Automation">
        ```bash theme={null}
        #!/usr/bin/env bash
        #
        # Purpose:
        #   Help review CIS GKE 5.1.3 at scale by:
        #     1. Listing all GKE node service accounts actually used by the cluster.
        #     2. Showing their risky Storage and Artifact Registry–related IAM roles
        #        at the project and (optionally) AR repository level.
        #
        # Usage (run on any machine with gcloud and kubectl configured):
        #   export PROJECT_ID="my-gcp-project"
        #   ./audit-gcr-ar-readonly.sh
        #
        # Optional:
        #   export AR_LOCATIONS="us-central1,us,europe-west1"
        #
        # NOTE:
        #   This only reports; it does NOT make changes. Use it to identify where
        #   project or repository IAM may give more than read-only access to
        #   container image repositories.

        set -euo pipefail

        PROJECT_ID="${PROJECT_ID:-}"
        if [[ -z "${PROJECT_ID}" ]]; then
          echo "ERROR: PROJECT_ID environment variable must be set (gcloud project ID)." >&2
          exit 1
        fi

        AR_LOCATIONS="${AR_LOCATIONS:-}"  # e.g. "us-central1,us,europe-west1"

        echo "=== Step 1: Discover node service accounts actually in use (via kubectl) ==="

        # We collect node and default service accounts referenced in node pools
        # and workloads. This runs only against the Kubernetes API.
        echo
        echo "# Kubernetes node (GCE/GKE) underlying service accounts (from node annotations)"
        kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"  "}{.metadata.annotations.container\.googleapis\.com/instance\_service\_account}{"\n"}{end}' \
          | sort -u

        echo
        echo "# Kubernetes workload serviceAccounts referenced in Pods (namespace/name -> GSA annotation if present)"
        kubectl get pods --all-namespaces -o json \
          | jq -r '
              .items[]
              | .metadata.namespace as $ns
              | .spec.serviceAccountName as $ksa
              | .metadata.annotations["iam.gke.io/gcp-service-account"]? as $gsa
              | [$ns, .metadata.name, ($ksa // "<default>"), ($gsa // "<none>")]
              | @tsv
            ' \
          | sort -u \
          | awk 'BEGIN{OFS="\t"; print "NAMESPACE","POD","KSA","GSA_ANNOTATION"}1'

        echo
        echo "# Distinct GCP service accounts referenced by GKE (from node + pod annotations)"
        GSA_LIST=$(
          {
            kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.annotations.container\.googleapis\.com/instance\_service\_account}{"\n"}{end}' 2>/dev/null || true
            kubectl get pods --all-namespaces -o json 2>/dev/null \
              | jq -r '.items[].metadata.annotations["iam.gke.io/gcp-service-account"] // empty'
          } \
          | sed '/^$/d' \
          | sort -u
        )

        if [[ -z "${GSA_LIST}" ]]; then
          echo "No GCP service accounts discovered from nodes or pods (via annotations)."
        else
          echo "${GSA_LIST}" | nl -ba
        fi

        echo
        echo "=== Step 2: Project-level IAM – risky Storage and AR-related roles ==="
        echo "# These roles indicate potential write/admin access to GCR buckets or storage:"
        echo "#   roles/storage.admin"
        echo "#   roles/storage.objectAdmin"
        echo "#   roles/storage.objectCreator"
        echo "#   roles/storage.legacyBucketOwner"
        echo "#   roles/storage.legacyBucketWriter"
        echo "#   roles/storage.legacyObjectOwner"
        echo "# and broad Artifact Registry roles (admin/writer, etc.)."

        echo
        echo "## Full project IAM bindings for risky Storage roles (cluster-wide):"
        gcloud projects get-iam-policy "${PROJECT_ID}" \
          --flatten="bindings[].members" \
          --format='table(bindings.members,bindings.role)' \
          --filter="bindings.role:roles/storage.admin OR \
                   bindings.role:roles/storage.objectAdmin OR \
                   bindings.role:roles/storage.objectCreator OR \
                   bindings.role:roles/storage.legacyBucketOwner OR \
                   bindings.role:roles/storage.legacyBucketWriter OR \
                   bindings.role:roles/storage.legacyObjectOwner"

        echo
        echo "## Project IAM bindings filtered to discovered GCP service accounts (Storage + Artifact Registry):"
        if [[ -z "${GSA_LIST}" ]]; then
          echo "Skipping GSA-focused filter: no GCP service accounts discovered in Step 1."
        else
          while read -r GSA; do
            MEMBER="serviceAccount:${GSA}"
            echo
            echo "### Service account: ${MEMBER}"
            gcloud projects get-iam-policy "${PROJECT_ID}" \
              --flatten="bindings[].members" \
              --format='table(bindings.members,bindings.role)' \
              --filter="bindings.members:${MEMBER} AND \
                        (bindings.role:roles/storage. OR bindings.role:roles/artifactregistry.)"
          done <<< "${GSA_LIST}"
        fi

        echo
        echo "=== Step 3 (optional): Artifact Registry repository-level IAM for discovered GSAs ==="

        if [[ -z "${AR_LOCATIONS}" ]]; then
          echo "AR_LOCATIONS not set; skipping per-repository IAM review."
          echo "Set AR_LOCATIONS (e.g. 'us-central1,us,europe-west1') to enable."
        else
          IFS=',' read -r -a LOCS <<< "${AR_LOCATIONS}"
          for LOC in "${LOCS[@]}"; do
            echo
            echo "## Listing Artifact Registry repositories in location: ${LOC}"
            gcloud artifacts repositories list \
              --project="${PROJECT_ID}" \
              --location="${LOC}" \
              --format='table(name,format)'
          done

          if [[ -n "${GSA_LIST}" ]]; then
            for LOC in "${LOCS[@]}"; do
              echo
              echo "## Per-repository IAM in ${LOC} for discovered GSAs"
              REPOS=$(gcloud artifacts repositories list \
                        --project="${PROJECT_ID}" \
                        --location="${LOC}" \
                        --format='value(name)' || true)
              for REPO in ${REPOS}; do
                echo
                echo "### Repository: ${REPO}"
                for GSA in ${GSA_LIST}; do
                  MEMBER="serviceAccount:${GSA}"
                  gcloud artifacts repositories get-iam-policy "${REPO}" \
                    --location="${LOC}" \
                    --project="${PROJECT_ID}" \
                    --flatten="bindings[].members" \
                    --format='table(bindings.members,bindings.role)' \
                    --filter="bindings.members:${MEMBER}"
                done
              done
            done
          fi
        fi

        cat <<'EOF'

        INTERPRETING OUTPUT (what indicates a problem):

        1) For GCR (image storage in GCS):
           Any service account used by GKE nodes or workloads that has one of these
           Storage roles – either directly on the GCR bucket or inherited at the
           PROJECT level – is overly privileged for pull-only scenarios:
             - roles/storage.admin
             - roles/storage.objectAdmin
             - roles/storage.objectCreator
             - roles/storage.legacyBucketOwner
             - roles/storage.legacyBucketWriter
             - roles/storage.legacyObjectOwner

           Expected for pull-only:
             - roles/storage.objectViewer on the specific GCR bucket
               (or equivalent read-only permission via IAM).

        2) For Artifact Registry:
           For GKE-related service accounts, expected is:
             - roles/artifactregistry.reader (or custom read-only role) on the AR repo.

           Potential problems:
             - roles/artifactregistry.admin
             - roles/artifactregistry.writer
             - roles/artifactregistry.repoAdmin
             - Any other non-read-only Artifact Registry role.

        This script only reports current IAM state. Use the provider console, gcloud,
        or IaC to adjust roles so that cluster-related service accounts have read-only
        access to image repositories wherever possible.
        EOF
        ```
      </Accordion>
    </AccordionGroup>
  </Tab>
</Tabs>
