# Legacy Service-Account-Token Secret

> Covers static, non-expiring service account token Secrets; ZopNight raises no findings for them yet because discovery skips these Secrets.

Source: https://zop.dev/integrations/kubernetes/recommendations/legacy-service-account-token-secret

---

## Static tokens outlive the reason they were created

A service account token Secret stores a signed credential for a ServiceAccount. The
[service accounts page](https://kubernetes.io/docs/concepts/security/service-accounts/) marks this
method as not recommended: these tokens do not expire and do not rotate, and static long-lived
credentials carry risk, especially at scale. Anyone who copies the token from a backup, a CI log or
a laptop keeps API access for as long as the Secret exists.

Kubernetes moved away from them in stages. From v1.22, pods get a short-lived, automatically
rotating token from the TokenRequest API, mounted as a projected volume. From v1.24 the control
plane stopped generating a permanent token Secret for each ServiceAccount by default. You can still
create these Secrets by hand, which is why they keep turning up.

## Finding token Secrets and their owners

```bash
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
```

The `kubernetes.io/legacy-token-last-used` label, where present, records the last date the token
was used, which helps separate live credentials from leftovers.

## Matching on the Secret type

The rule reads the type recorded for each Secret and is written to fire when it is exactly
`kubernetes.io/service-account-token`, whatever the name, age or usage. Today, though, ZopNight's
discovery skips every Secret of that type when it lists Secrets, hand-made ones included, so the
rule has nothing to match and raises no findings yet. Until that changes, use the `kubectl`
command above to find these Secrets.

## What the check does not cover

Other Secret types, including TLS and registry credentials, are out of scope; Secrets carrying
unusually many keys are covered by
[Secret has excessive keys](https://zop.dev/integrations/kubernetes/recommendations/secret-has-excessive-keys).
Projected tokens mounted into pods are not Secrets and never appear here. The control plane also
cleans some tokens up by itself: since v1.30, when the legacy token cleaner became stable,
auto-generated legacy tokens unused for a year are labelled `kubernetes.io/legacy-token-invalid-since` and later purged. That cleaner targets
auto-generated tokens, so manually created ones usually remain until you remove them.

## Credential risk with no saving

There is no dollar amount. The exposure is a non-expiring API credential whose reach equals every
RBAC permission granted to its ServiceAccount.

## Replacing the static token

1. Identify the consumer from the ServiceAccount annotation, the last-used label and your CI or
   automation configuration.
2. For workloads in the cluster, rely on the projected token Kubernetes mounts automatically.
3. For tools outside the cluster, issue a time-bound token with
   `kubectl create token <serviceaccount> --duration 1h`, or call the TokenRequest API from the tool.
4. Once nothing uses the old credential, run `kubectl delete secret <name> -n <namespace>`.

**Warning**
Deleting the Secret invalidates the token at once. Anything still using it will start getting authentication errors.
