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

# Prefer Bound Projected ServiceAccount Tokens Over Secret Tokens

### More Info:

Advisory: avoid long-lived ServiceAccount token Secrets; use projected (TokenRequest) tokens with an audience and expiry instead.

### Risk Level

Informational

### Address

Security

### Compliance Standards

* Cloudanix Best Practice

### Triage and Remediation

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

    <AccordionGroup>
      <Accordion title="Manual Steps" defaultOpen="true">
        1. **List all legacy ServiceAccount token Secrets**
           * Run on: any machine with kubectl access.
           ```sh theme={null}
           kubectl get secrets -A --field-selector type=kubernetes.io/service-account-token -o wide
           ```
           * Identify which namespaces and ServiceAccounts are still using auto-created token Secrets.

        2. **Map each token Secret to its ServiceAccount and workloads**
           * For a specific Secret (replace SECRET and NAMESPACE):
           ```sh theme={null}
           kubectl get secret SECRET -n NAMESPACE -o yaml
           ```
           * Note the `.metadata.annotations["kubernetes.io/service-account.name"]`.
           * Check which pods use that ServiceAccount:
           ```sh theme={null}
           kubectl get pods -n NAMESPACE -o wide --field-selector spec.serviceAccountName=SERVICEACCOUNT
           ```

        3. **Identify direct Secret mounting or out-of-cluster use**
           * Check if the token Secret is mounted into pods:
           ```sh theme={null}
           kubectl get pods -n NAMESPACE -o yaml | grep -C5 "name: SECRET"
           ```
           * Review pod and application configuration (manifests, CI/CD, external systems) to see if the Secret name or its token value is referenced for out-of-cluster access (automation scripts, external services).

        4. **Decide if each use can be migrated to projected ServiceAccount tokens**
           * For in-cluster workloads: plan to replace `secret` volume mounts with a projected `serviceAccountToken` volume (TokenRequest) that sets `audience` and `expirationSeconds`.
           * For out-of-cluster callers: consider alternatives such as Workload Identity (GKE recommended), or a dedicated authentication method, instead of reusing long-lived cluster ServiceAccount tokens.

        5. **Update manifests to stop relying on long-lived token Secrets**
           * Edit workloads that mount ServiceAccount token Secrets and replace with a projected token volume. Example pattern (to adapt into the pod spec that used the Secret):
           ```yaml theme={null}
           spec:
             serviceAccountName: SERVICEACCOUNT
             volumes:
               - name: sa-token
                 projected:
                   sources:
                     - serviceAccountToken:
                         path: token
                         audience: YOUR-AUDIENCE
                         expirationSeconds: 3600
             containers:
               - name: APP
                 volumeMounts:
                   - name: sa-token
                     mountPath: /var/run/secrets/tokens
                     readOnly: true
           ```
           * Apply the updated manifest from a machine with kubectl access:
           ```sh theme={null}
           kubectl apply -f UPDATED-MANIFEST.yaml
           ```

        6. **Verify no unnecessary ServiceAccount token Secrets remain in active use**
           * After pods are updated and restarted, confirm they are not mounting the legacy token Secrets (inspect a sample pod):
           ```sh theme={null}
           kubectl get pod PODNAME -n NAMESPACE -o yaml | grep -C5 "secret"
           ```
           * Re-list legacy token Secrets and decide whether each can be deleted (only after confirming nothing depends on it):
           ```sh theme={null}
           kubectl get secrets -N NAMESPACE --field-selector type=kubernetes.io/service-account-token
           ```
           * Optionally, delete an unused token Secret:
           ```sh theme={null}
           kubectl delete secret SECRET -n NAMESPACE
           ```
      </Accordion>

      <Accordion title="Using kubectl">
        ```bash theme={null}
        # 1. List all long-lived ServiceAccount token Secrets
        # Run on: any machine with kubectl access
        kubectl get secrets -A --field-selector type=kubernetes.io/service-account-token
        ```

        Interpretation:

        * Large numbers of `kubernetes.io/service-account-token` Secrets, especially in application namespaces, indicate workloads are likely using legacy long-lived tokens instead of projected tokens.
        * Pay special attention to non-system namespaces; `kube-system`/`gke-*` may contain some platform-managed tokens.

        ```bash theme={null}
        # 2. For a specific token Secret, inspect details
        # Replace NAMESPACE and SECRET_NAME with values from the list above
        kubectl -n NAMESPACE get secret SECRET_NAME -o yaml
        ```

        Interpretation:

        * `metadata.annotations["kubernetes.io/service-account.name"]` shows which ServiceAccount the token belongs to.
        * Absence of any reference to `kubernetes.io/service-account.token-expiration` means it is a legacy-style long-lived token.
        * Any external system documented as consuming this Secret directly (for example via CI/CD or off-cluster scripts) is relying on a long-lived token.

        ```bash theme={null}
        # 3. List ServiceAccounts in an application namespace
        # Replace NAMESPACE with your app namespace (not kube-system)
        kubectl -n NAMESPACE get serviceaccounts
        ```

        ```bash theme={null}
        # 4. For a given ServiceAccount, see its Secrets and automount setting
        kubectl -n NAMESPACE get serviceaccount SERVICEACCOUNT_NAME -o yaml
        ```

        Interpretation:

        * Under `secrets:`, any entries that correspond to `kubernetes.io/service-account-token` Secrets from step 1 are legacy tokens.
        * Check `automountServiceAccountToken`:
          * If `true` or unset, pods using this ServiceAccount will auto-mount a token (legacy on older clusters / manifests).
          * If you intend to use projected tokens only, this typically should be set to `false` and explicit projected volumes used in Pods.

        ```bash theme={null}
        # 5. Find Pods that mount ServiceAccount token Secrets directly
        kubectl -n NAMESPACE get pods -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.volumes[*].secret.secretName}{"\n"}{end}' | grep -v '^\s*$'
        ```

        Interpretation:

        * Any Pod that lists a Secret name that you identified in step 1 (type `kubernetes.io/service-account-token`) is explicitly mounting a long-lived token Secret.
        * This indicates the workload is not using a projected ServiceAccountToken volume.

        ```bash theme={null}
        # 6. Inspect a Pod spec for how it gets its token
        kubectl -n NAMESPACE get pod POD_NAME -o yaml
        ```

        Interpretation:

        * Look under `spec.volumes`:
          * `secret: name: <token-secret>` → using a long-lived ServiceAccount token Secret.
          * `projected: sources: - serviceAccountToken:` with `audience` and `expirationSeconds` → using a bound/projected token (desired).
        * Under `spec.serviceAccountName` and `spec.automountServiceAccountToken`:
          * A Pod relying only on the default automounted token and not using `projected.serviceAccountToken` is a candidate for migration.

        ```bash theme={null}
        # 7. (Optional) Check if a ServiceAccount token Secret is actually referenced by any Pod
        # Replace NAMESPACE with the namespace you are reviewing
        for s in $(kubectl -n NAMESPACE get secrets --field-selector type=kubernetes.io/service-account-token -o jsonpath='{.items[*].metadata.name}'); do
          echo "Secret: $s"
          kubectl -n NAMESPACE get pods -o jsonpath="{range .items[*]}{.metadata.name}{': '}{range .spec.volumes[*]}{.secret.secretName}{','}{end}{'\n'}{end}" | grep "$s" || echo "  (no pods referencing this secret)"
          echo
        done
        ```

        Interpretation:

        * Secrets not referenced by any Pods may be unused legacy tokens that can potentially be phased out.
        * Secrets actively referenced by Pods indicate those workloads must be carefully migrated to use projected ServiceAccount tokens with audience and expiry.
      </Accordion>

      <Accordion title="Automation">
        ```bash theme={null}
        #!/usr/bin/env bash
        # Report long-lived ServiceAccount token Secrets and their usage (GKE)
        # Run on: any machine with kubectl access and appropriate RBAC

        set -euo pipefail

        echo "=== ServiceAccount token Secrets (type=kubernetes.io/service-account-token) ==="
        kubectl get secrets -A \
          --field-selector type=kubernetes.io/service-account-token \
          -o json \
        | jq -r '
          .items[]
          | {
              ns: .metadata.namespace,
              name: .metadata.name,
              sa: .metadata.annotations["kubernetes.io/service-account.name"],
              created: .metadata.creationTimestamp,
              # Approximate size of the JWT; very large tokens may indicate legacy/default tokens
              token_bytes: (.data.token // "" | @base64d | length),
              # In GKE, bound tokens typically have an "kubernetes.io/service-account.uid" annotation;
              # presence alone does NOT guarantee projected/bound usage, but absence is a red flag.
              sa_uid: .metadata.annotations["kubernetes.io/service-account.uid"]
            }
          | @tsv
        ' \
        | awk 'BEGIN {
                 OFS="\t";
                 print "NAMESPACE","SECRET","SERVICEACCOUNT","CREATED","TOKEN_BYTES","SA_UID_PRESENT"
               }
               {
                 sa_uid = ($6 == "" ? "no" : "yes");
                 print $1,$2,$3,$4,$5,sa_uid
               }'

        echo
        echo "=== Pods that mount ServiceAccount token Secrets directly as volumes ==="
        # Pods that explicitly mount a SA token Secret instead of using the default
        # projected token volume are higher risk.
        kubectl get pods -A -o json \
        | jq -r '
          .items[]
          | {
              ns: .metadata.namespace,
              pod: .metadata.name,
              sa: .spec.serviceAccountName,
              vols: .spec.volumes
            }
          | select(.vols != null)
          | .vols[]
          | select(.secret != null)
          | {
              ns: input.ns,
              pod: input.pod,
              sa: input.sa,
              vol_name: .name,
              secret_name: .secret.secretName
            }
          | @tsv
        ' 2>/dev/null \
        | awk 'BEGIN {
                 OFS="\t";
                 print "NAMESPACE","POD","SERVICEACCOUNT","VOLUME_NAME","SECRET_NAME"
               }
               { print }'

        echo
        echo "=== Pods using default ServiceAccount token projection ==="
        # These pods rely on the projected token volume mechanism, which is preferred.
        kubectl get pods -A -o json \
        | jq -r '
          .items[]
          | {
              ns: .metadata.namespace,
              pod: .metadata.name,
              sa: .spec.serviceAccountName,
              automount: (.spec.automountServiceAccountToken // "default"),
              projected: (
                .spec.volumes // []
                | map(select(.projected != null))
                | map(
                    .projected.sources // []
                    | map(select(.serviceAccountToken != null))
                    | length
                  )
                | add
              )
            }
          | select(.projected > 0 or .automount != false)
          | @tsv
        ' \
        | awk 'BEGIN {
                 OFS="\t";
                 print "NAMESPACE","POD","SERVICEACCOUNT","AUTOMOUNT_SA_TOKEN","NUM_PROJECTED_SA_TOKEN_SOURCES"
               }
               { print }'

        cat <<'EOF'

        INTERPRETING THE OUTPUT (WHAT INDICATES A PROBLEM):

        1) "ServiceAccount token Secrets" table:
           - Any row is a potential concern: long-lived Secrets of type
             "kubernetes.io/service-account-token" exist.
           - "SA_UID_PRESENT = no" is a stronger red flag (likely older-style token Secret).
           - Very old "CREATED" timestamps suggest long-lived tokens that should be rotated away.
           - Large "TOKEN_BYTES" values are normal for JWTs; focus on existence and age rather than size.

        2) "Pods that mount ServiceAccount token Secrets directly as volumes":
           - Any row here is a concern: the pod is explicitly mounting a ServiceAccount token Secret,
             which tends to create long-lived, broadly scoped credentials.

        3) "Pods using default ServiceAccount token projection":
           - Preferred pattern is: AUTONMOUNT_SA_TOKEN = true or "default",
             and NUM_PROJECTED_SA_TOKEN_SOURCES > 0, with application logic using TokenRequest
             (projected) tokens with audience+expiry.
           - This report only shows presence of projected token sources; you still need to review
             app code/manifests to confirm that TokenRequest-style tokens are used correctly.

        This script does not change any resources. Use it periodically to identify
        namespaces/ServiceAccounts/Pods that still rely on long-lived ServiceAccount token Secrets
        and prioritize them for manual refactoring to projected, short-lived tokens.
        EOF
        ```
      </Accordion>
    </AccordionGroup>
  </Tab>
</Tabs>
