> ## 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 ServiceAccount token Secrets and map them to identities**
           * On any machine with kubectl access:
             ```bash theme={null}
             kubectl get secrets -A --field-selector type=kubernetes.io/service-account-token -o wide
             kubectl get secrets -A --field-selector type=kubernetes.io/service-account-token -o json \
               | jq -r '.items[] | [.metadata.namespace, .metadata.name, .metadata.annotations["kubernetes.io/service-account.name"]] | @tsv'
             ```
           * Record which applications (Deployments/Pods) use each ServiceAccount.

        2. **Identify workloads actually mounting these token Secrets**
           * On any machine with kubectl access:
             ```bash theme={null}
             kubectl get pods -A -o json \
               | jq -r '.items[] | {ns:.metadata.namespace, pod:.metadata.name, sa:.spec.serviceAccountName, vols:.spec.volumes} | 
                 select(.vols!=null) |
                 "\(.ns)\t\(.pod)\t\(.sa)\t" + 
                 ( [.vols[] | select(.secret!=null) | .secret.secretName] | join(",") )' \
               | column -t
             ```
           * Flag Pods where a `secret` volume mounts a ServiceAccount token Secret.

        3. **Decide whether each workload actually needs a Kubernetes API token**
           * For each flagged Pod/Deployment, review its code/configuration to see if it calls the Kubernetes API.
           * If it does not need API access, plan to:
             * Remove the ServiceAccount reference (fall back to `default` only if default SA is locked down), and
             * Ensure no `secret` volume or env var uses a ServiceAccount token Secret.

        4. **Migrate workloads that do need API access to use projected tokens**
           * For each workload that truly needs a token, update the Pod spec (Deployment/StatefulSet/Job, etc.) to use a projected ServiceAccount token volume with audience and expiry, instead of a `secret` volume. Example pattern (adapt to your manifest):
             ```yaml theme={null}
             spec:
               serviceAccountName: my-sa
               volumes:
                 - name: k8s-sa-token
                   projected:
                     sources:
                       - serviceAccountToken:
                           path: token
                           audience: "https://kubernetes.default.svc.cluster.local"
                           expirationSeconds: 3600
               containers:
                 - name: app
                   volumeMounts:
                     - name: k8s-sa-token
                       mountPath: /var/run/secrets/tokens
                       readOnly: true
             ```
           * Apply the change on any machine with kubectl access:
             ```bash theme={null}
             kubectl apply -f <updated-manifest>.yaml
             ```

        5. **Clean up legacy ServiceAccount token Secrets once unused**
           * After updating workloads and confirming they run correctly, check whether a token Secret is still referenced:
             ```bash theme={null}
             kubectl get pods -A -o json \
               | jq -r '.items[] | {ns:.metadata.namespace, pod:.metadata.name, vols:.spec.volumes} |
                 select(.vols!=null) |
                 .ns as $ns | .pod as $pod |
                 .vols[]? | select(.secret!=null) |
                 "\($ns)\t\($pod)\t\(.secret.secretName)"' \
               | sort -u
             ```
           * For any ServiceAccount token Secret no longer referenced by Pods and not needed for external systems, delete it:
             ```bash theme={null}
             kubectl -n <namespace> delete secret <sa-token-secret-name>
             ```

        6. **Re-verify and document exceptions**
           * On any machine with kubectl access, re-run:
             ```bash theme={null}
             kubectl get secrets -A --field-selector type=kubernetes.io/service-account-token
             ```
           * For any remaining ServiceAccount token Secrets, document:
             * Which workload or external system uses them,
             * Why projected tokens cannot be used yet, and
             * A plan or decision to accept the residual risk.
      </Accordion>

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

        A large number of `kubernetes.io/service-account-token` Secrets, especially in application namespaces, indicates widespread use of long‑lived SA token Secrets and is a concern.

        ```bash theme={null}
        # 2. Inspect which ServiceAccount a given token Secret belongs to
        # Replace NAMESPACE and SECRET_NAME with values from step 1
        kubectl get secret -n NAMESPACE SECRET_NAME -o yaml
        ```

        Concerning signs in the output:

        * `type: kubernetes.io/service-account-token`
        * Mounted in Pods as a volume (see step 3)
        * No process in place to rotate or prune these Secrets.

        ```bash theme={null}
        # 3. Find Pods that mount ServiceAccount token Secrets directly
        # This surfaces Pods likely using long-lived token Secrets
        kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{" "}{.metadata.name}{"\n"}{range .spec.volumes[*]}{.name}{" "}{.secret.secretName}{"\n"}{end}{"---\n"}{end}' | grep -v '---'
        ```

        Lines where `secretName` matches names from step 1 show Pods mounting SA token Secrets explicitly. These Pods should be reviewed to migrate to projected tokens (TokenRequest) via projected volumes.

        ```bash theme={null}
        # 4. Check if Pods are using default automatic SA token mounting
        kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{" "}{.metadata.name}{" sa:"}{.spec.serviceAccountName}{" automount:"}{.spec.automountServiceAccountToken}{"\n"}{end}'
        ```

        Concerning patterns:

        * `automountServiceAccountToken` is empty (inherits `true` from the ServiceAccount) or explicitly `true` for Pods in application namespaces, meaning they get a long‑lived token mounted unless the SA is configured otherwise.

        ```bash theme={null}
        # 5. Check ServiceAccount defaults for automatic token mounting
        kubectl get sa -A -o jsonpath='{range .items[*]}{.metadata.namespace}{" "}{.metadata.name}{" automount:"}{.automountServiceAccountToken}{"\n"}{end}'
        ```

        Concerning patterns:

        * ServiceAccounts in application namespaces with `automountServiceAccountToken: true` or unset (defaults to true), combined with use in Pods from step 4.

        ```bash theme={null}
        # 6. Identify old / potentially long-lived token Secrets
        kubectl get secrets -A --field-selector type=kubernetes.io/service-account-token \
          -o custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,SA:.metadata.annotations.kubernetes\.io/service-account\.name,CREATED:.metadata.creationTimestamp' \
          | sort
        ```

        Concerning signs:

        * Token Secrets with very old `CREATED` timestamps that are still referenced by running Pods or external systems.

        Use these outputs to decide, per application:

        * Which Pods/Workloads still depend on classic SA token Secrets.
        * Where you can disable automatic SA token mounting and refactor to projected tokens with explicit audiences and expiries.
      </Accordion>

      <Accordion title="Automation">
        ```bash theme={null}
        #!/usr/bin/env bash
        #
        # Report ServiceAccounts that are still using legacy long-lived
        # service-account token Secrets instead of projected tokens.
        #
        # Requirements:
        # - Run on any machine with kubectl access and current context set to the target OKE cluster.
        # - kubectl must be in PATH.

        set -euo pipefail

        echo "Collecting legacy ServiceAccount token Secrets (type=kubernetes.io/service-account-token)..."
        echo

        # 1. List all SA token Secrets and basic metadata
        echo "=== All legacy ServiceAccount token Secrets (cluster-wide) ==="
        kubectl get secrets -A \
          --field-selector type=kubernetes.io/service-account-token \
          -o custom-columns=\
        NAMESPACE:.metadata.namespace,\
        NAME:.metadata.name,\
        SA_NAME:.metadata.annotations['kubernetes\.io/service-account\.name'],\
        SA_UID:.metadata.annotations['kubernetes\.io/service-account\.uid'],\
        CREATED:.metadata.creationTimestamp \
          | sed 's/<none>//g'
        echo

        # 2. Highlight potentially problematic ones:
        #    - Secrets older than 30 days
        #    - Secrets whose ServiceAccount no longer exists
        THRESHOLD_DAYS=30
        THRESHOLD_SECONDS=$(( THRESHOLD_DAYS * 24 * 60 * 60 ))
        NOW_EPOCH=$(date -u +%s)

        echo "=== Potentially problematic tokens (for review) ==="
        echo "# Criteria:"
        echo "# - Secret type=kubernetes.io/service-account-token"
        echo "# - AND (age > ${THRESHOLD_DAYS} days OR ServiceAccount missing)"
        echo

        kubectl get secrets -A \
          --field-selector type=kubernetes.io/service-account-token \
          -o json \
          | jq -r --arg now_epoch "${NOW_EPOCH}" --arg thresh "${THRESHOLD_SECONDS}" '
            .items[]
            | . as $s
            | $s.metadata.creationTimestamp as $cts
            | ( ($now_epoch|tonumber) - ( ($cts | sub("Z$"; "") + "+00:00") | fromdate )) as $age
            | $age as $age_sec
            | ($age_sec > ($thresh|tonumber)) as $old
            | $s.metadata.annotations["kubernetes.io/service-account.name"] as $sa
            | $s.metadata.namespace as $ns
            | $s.metadata.name as $name
            | if $sa == null then
                {
                  namespace:$ns,
                  secret:$name,
                  serviceAccount:"<missing-annotation>",
                  age_days:($age_sec/86400|floor),
                  reason:"missing service-account.name annotation"
                }
              else
                {
                  namespace:$ns,
                  secret:$name,
                  serviceAccount:$sa,
                  age_days:($age_sec/86400|floor),
                  old:$old
                }
              end
            ' \
          | while read -r line; do
              # For each JSON object, enrich with SA existence info
              obj="${line}"
              ns=$(jq -r '.namespace' <<< "${obj}")
              sa=$(jq -r '.serviceAccount' <<< "${obj}")
              if [ "${sa}" != "<missing-annotation>" ]; then
                if kubectl get sa "${sa}" -n "${ns}" >/dev/null 2>&1; then
                  exists="true"
                else
                  exists="false"
                fi
              else
                exists="unknown"
              fi
              jq --arg exists "${exists}" '. + {serviceAccountExists:$exists}' <<< "${obj}"
            done \
          | jq -r '
              select(
                .reason == "missing service-account.name annotation"
                or .old == true
                or .serviceAccountExists == "false"
              )
            ' \
          | jq -r '
              [.namespace,
               .secret,
               .serviceAccount,
               .serviceAccountExists,
               (if .age_days then (.age_days|tostring) else "N/A" end),
               (if .reason then .reason else "" end)
              ]
              | @tsv
            ' \
          | awk 'BEGIN {
                   OFS="\t";
                   print "NAMESPACE","SECRET","SERVICEACCOUNT","SA_EXISTS","AGE_DAYS","NOTE"
                 }
                 { print }'
        echo

        # 3. Per-ServiceAccount summary: count of attached legacy token Secrets
        echo "=== ServiceAccount -> legacy token Secret summary ==="
        kubectl get secrets -A \
          --field-selector type=kubernetes.io/service-account-token \
          -o json \
          | jq -r '
            .items[]
            | .metadata as $m
            | $m.annotations["kubernetes.io/service-account.name"] as $sa
            | select($sa != null)
            | [$m.namespace, $sa, $m.name] | @tsv
          ' \
          | awk '{
              key=$1 "/" $2
              count[key]++
            }
            END {
              OFS="\t";
              print "NAMESPACE","SERVICEACCOUNT","LEGACY_TOKEN_SECRET_COUNT"
              for (k in count) {
                split(k,a,"/")
                print a[1],a[2],count[k]
              }
            }' \
          | sort -k1,1 -k2,2
        echo

        cat <<'EOF'

        INTERPRETING OUTPUT (what indicates a problem):

        1) "All legacy ServiceAccount token Secrets":
           - Any entry here is a long-lived Secret-backed token. These are candidates
             for replacement with projected TokenRequest tokens.

        2) "Potentially problematic tokens (for review)":
           - Rows with AGE_DAYS significantly high (e.g., >30) show long-lived tokens.
           - SA_EXISTS = false means the ServiceAccount was deleted but its token Secret remains.
           - NOTE mentioning "missing-annotation" suggests an abnormal or legacy Secret.

        3) "ServiceAccount -> legacy token Secret summary":
           - SERVICEACCOUNTs with LEGACY_TOKEN_SECRET_COUNT >= 1 are still using
             Secret-backed tokens. For those workloads, review whether they can be
             migrated to use projected serviceAccountToken volumes and short-lived tokens.

        This script does NOT modify anything; it only highlights where legacy
        ServiceAccount token Secrets are in use so you can review and plan migration.
        EOF
        ```
      </Accordion>
    </AccordionGroup>
  </Tab>
</Tabs>
