> ## 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 Access To Secrets

### More Info:

The Kubernetes API stores secrets, which may be service account tokens for the Kubernetes API or credentials used by workloads in the cluster. Access to these secrets should be restricted to the smallest possible group of users to reduce the risk of privilege escalation.

### Risk Level

High

### Address

Security

### Compliance Standards

* APRA CPS 234 (Australia)
* AWS Startup Security Baseline
* BSI C5 (Germany)
* Brazil LGPD
* CCPA / CPRA (California)
* CIS Critical Security Controls v8
* CIS EKS
* CMMC 2.0
* CSA Cloud Controls Matrix v4
* DPDPA
* Digital Operational Resilience Act (EU)
* Essential 8
* ISO/IEC 27017
* ISO/IEC 27018
* ISO/IEC 27701
* KSA PDPL
* MAS Technology Risk Management (Singapore)
* MITRE ATT\&CK (Cloud)
* NIS2 Directive
* NIST SP 800-171
* NYDFS 23 NYCRR 500
* SWIFT Customer Security Controls Framework
* Sarbanes-Oxley IT General Controls
* UK NCSC Cyber Assessment Framework

### Triage and Remediation

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

    <AccordionGroup>
      <Accordion title="Manual Steps" defaultOpen="true">
        1. List all Roles that grant get/list/watch on secrets
           * Run on: any machine with kubectl access
           ```sh theme={null}
           kubectl get roles --all-namespaces -o json \
           | jq -r '
             .items[]
             | select(.rules[]?
               | (.resources[]? == "secrets")
               and ((.verbs[]? == "get") or (.verbs[]? == "list") or (.verbs[]? == "watch"))
             )
             | "\(.metadata.namespace) \(.metadata.name)"' \
           | sort -u
           ```

        2. Inspect each identified Role to understand who uses it and why
           * For each `<NAMESPACE> <ROLE_NAME>` from step 1, run:
           ```sh theme={null}
           kubectl -n <NAMESPACE> get role <ROLE_NAME> -o yaml
           kubectl -n <NAMESPACE> get rolebinding -o yaml \
             | yq '.items[] | select(.roleRef.kind=="Role" and .roleRef.name=="<ROLE_NAME>")'
           ```
           * Review whether subjects (users/groups/serviceaccounts) truly require get/list/watch on secrets.

        3. Edit Roles to remove unnecessary get/list/watch on secrets
           * For each Role where secret access is not strictly required, edit it:
           ```sh theme={null}
           kubectl -n <NAMESPACE> edit role <ROLE_NAME>
           ```
           * In the opened YAML, locate any rule with `resources: ["secrets"]` (or containing `secrets`) and remove `get`, `list`, and `watch` from the `verbs` list, keeping only the minimal verbs truly needed (often none). Save and exit.

        4. Split mixed-purpose Roles if only some subjects need secret access
           * If a Role includes both secret permissions and other needed permissions for many subjects, create a dedicated minimal-secret Role and narrow its binding:
           ```sh theme={null}
           # Export the existing Role
           kubectl -n <NAMESPACE> get role <ROLE_NAME> -o yaml > /tmp/<ROLE_NAME>.yaml
           ```
           * Edit `/tmp/<ROLE_NAME>.yaml` locally to:
             * Remove all rules except the minimal `secrets` rule for specific subjects that truly need it.
             * Change `metadata.name` to a new name, e.g. `<ROLE_NAME>-secrets-minimal`.
           * Apply the new Role:
           ```sh theme={null}
           kubectl -n <NAMESPACE> apply -f /tmp/<ROLE_NAME>.yaml
           ```
           * Create or update RoleBindings so that only the specific user/group/serviceaccount that truly needs secret access is bound to `<ROLE_NAME>-secrets-minimal`, and other subjects are bound to a Role without `secrets` access.

        5. Repeat for all namespaces and avoid granting cluster-wide secret access via Roles
           * Ensure you do not introduce new `ClusterRole` permissions on secrets while adjusting Roles.
           * For workloads that previously relied on broad secret access, verify and, if necessary, adjust their service accounts and secret mounting to use only the specific secrets they need.

        6. Verification
           * Run on: any machine with kubectl access
           ```sh theme={null}
           count=$(kubectl get roles --all-namespaces -o json | jq '
             .items[]
             | select(.rules[]?
               | (.resources[]? == "secrets")
               and ((.verbs[]? == "get") or (.verbs[]? == "list") or (.verbs[]? == "watch"))
             )' | wc -l)

           if [ "$count" -eq 0 ]; then
             echo "SECRETS_ACCESS_NOT_FOUND"
           else
             echo "SECRETS_ACCESS_FOUND: $count roles still grant get/list/watch on secrets"
           fi
           ```
      </Accordion>

      <Accordion title="Using kubectl">
        On any machine with kubectl access:

        1. Identify roles with get/list/watch on secrets

        ```sh theme={null}
        kubectl get roles --all-namespaces -o json | jq -r '
          .items[]
          | select(.rules[]?
            | (.resources[]? == "secrets")
            and ((.verbs[]? == "get") or (.verbs[]? == "list") or (.verbs[]? == "watch"))
          )
          | "\(.metadata.namespace) \(.metadata.name)"'
        ```

        2. For each listed role, inspect current rules\
           Replace NAMESPACE and ROLE\_NAME with values from the previous output:

        ```sh theme={null}
        kubectl -n NAMESPACE get role ROLE_NAME -o yaml > ROLE_NAME-role.yaml
        ```

        3. Edit the Role manifest locally (ROLE\_NAME-role.yaml) and remove `get`, `list`, and `watch` from any rule that includes `resources: ["secrets"]`, or remove the `secrets` resource from rules that do not need it. Save the file.

        Example before:

        ```yaml theme={null}
        rules:
          - apiGroups: [""]
            resources: ["secrets"]
            verbs: ["get", "list", "watch", "create"]
        ```

        Example after (if only `create` is required):

        ```yaml theme={null}
        rules:
          - apiGroups: [""]
            resources: ["secrets"]
            verbs: ["create"]
        ```

        4. Apply the updated Role

        ```sh theme={null}
        kubectl apply -f ROLE_NAME-role.yaml
        ```

        5. Repeat steps 2–4 for every Role that was identified in step 1.

        6. Verify that no Roles still grant get/list/watch on secrets

        ```sh theme={null}
        count=$(kubectl get roles --all-namespaces -o json | jq '
          .items[]
          | select(.rules[]?
            | (.resources[]? == "secrets")
            and ((.verbs[]? == "get") or (.verbs[]? == "list") or (.verbs[]? == "watch"))
          )' | wc -l)

        if [ "$count" -eq 0 ]; then
          echo "NO_SECRETS_GET_LIST_WATCH_IN_ROLES"
        else
          echo "SECRETS_ACCESS_FOUND_IN_ROLES: $count"
        fi
        ```
      </Accordion>

      <Accordion title="Automation">
        ```bash theme={null}
        #!/usr/bin/env bash
        set -euo pipefail

        # Automation to minimize get/list/watch access to Secret objects in Roles.
        # Runs from any machine with kubectl + jq + sed + mktemp and cluster access.

        # CONFIGURABLE: set a backup directory for modified Roles
        BACKUP_DIR="${BACKUP_DIR:-./role-secret-backups}"
        mkdir -p "${BACKUP_DIR}"

        echo "Discovering Roles that grant get/list/watch on secrets..."

        # Get all Roles that reference 'secrets' with get/list/watch
        role_list_json="$(kubectl get roles --all-namespaces -o json)"
        mapfile -t TARGETS < <(
          echo "${role_list_json}" | jq -r '
            .items[]
            | select(
                .rules[]? as $r
                | ($r.resources[]? == "secrets")
                and (
                  ($r.verbs[]? == "get") or
                  ($r.verbs[]? == "list") or
                  ($r.verbs[]? == "watch")
                )
              )
            | .metadata.namespace + ":" + .metadata.name
          ' | sort -u
        )

        if [ "${#TARGETS[@]}" -eq 0 ]; then
          echo "No Roles with get/list/watch on secrets found. Nothing to change."
        else
          echo "Found ${#TARGETS[@]} Role(s) with get/list/watch on secrets."
        fi

        for ns_name in "${TARGETS[@]}"; do
          ns="${ns_name%%:*}"
          name="${ns_name##*:}"

          echo "Processing Role ${ns}/${name}..."

          # Backup original Role manifest (idempotent: overwrite backup each run)
          backup_file="${BACKUP_DIR}/${ns}__${name}.yaml"
          kubectl get role "${name}" -n "${ns}" -o yaml > "${backup_file}"
          echo "  Backup saved to ${backup_file}"

          # Work on a temp file
          tmpfile="$(mktemp)"
          cp "${backup_file}" "${tmpfile}"

          # Use jq to surgically remove get/list/watch verbs only for rules affecting secrets
          # Strategy:
          # - For each rule that includes "secrets" in resources, remove get/list/watch from verbs.
          # - If a rule's verbs become empty, we keep the rule (no verbs) to avoid changing indices.
          #   Kubernetes will reject a rule with no verbs, so instead we drop such rules.
          #
          # Implementation detail:
          #   1) Convert to JSON
          #   2) Transform .rules
          #   3) Convert back to yaml

          json_transformed="$(
            kubectl get role "${name}" -n "${ns}" -o json | jq '
              .rules |= map(
                if (.resources // [] | index("secrets")) != null then
                  # This rule touches secrets; strip get/list/watch verbs
                  .verbs |= map(select(. != "get" and . != "list" and . != "watch")) |
                  # Drop rule if verbs is now empty
                  if (.verbs | length) == 0 then
                    empty
                  else
                    .
                  end
                else
                  .
                end
              )
            '
          )"

          # Skip apply if there is no change (idempotent)
          if diff -q <(echo "${json_transformed}" | jq -S .) <(kubectl get role "${name}" -n "${ns}" -o json | jq -S .) >/dev/null 2>&1; then
            echo "  No changes needed for ${ns}/${name}."
            rm -f "${tmpfile}"
            continue
          fi

          # Convert transformed JSON to YAML for kubectl apply
          echo "${json_transformed}" | kubectl apply -f -
          echo "  Updated Role ${ns}/${name} to remove get/list/watch on secrets."

          rm -f "${tmpfile}"
        done

        echo "Re-running verification..."

        # Verification: re-run the benchmark-style check
        count="$(
          kubectl get roles --all-namespaces -o json | jq '
            .items[]
            | select(.rules[]?
              | (.resources[]? == "secrets")
              and ((.verbs[]? == "get") or (.verbs[]? == "list") or (.verbs[]? == "watch"))
            )' | wc -l
        )"

        if [ "${count}" -gt 0 ]; then
          echo "SECRETS_ACCESS_FOUND"
          echo "WARNING: Some Roles still grant get/list/watch on secrets."
          echo "They may be system or critical Roles that should be reviewed manually."
        else
          echo "No Roles with get/list/watch on secrets remain."
        fi
        ```
      </Accordion>
    </AccordionGroup>
  </Tab>
</Tabs>
