Threat-intel reports from recent years document campaigns in which attackers obtain AWS IAM credentials from developer workstations, use them to enumerate cloud accounts and access Kubernetes clusters. From there, attackers deploy poisoned container images to move laterally and harvest secrets. The MITRE ATT&CK chain maps to: T1552.001 (Credentials in Files) → T1078.004 (Valid Accounts: Cloud Accounts) → T1610 (Deploy Container) → T1496 (Resource Hijacking).This is not an isolated case. The Shai-Hulud supply chain attack harvested Kubernetes credentials from CI and developer workstations, feeding exactly this kind of attack chain.

This research started with a short list of questions:

  • What are Kubernetes secrets, exactly?
  • What can an attacker do with them?
  • How can defenders harden their clusters?

So before we look at what we found in the wild, and how to harden clusters to mitigate impacts, let's define what Kubernetes secrets are.

Three Surfaces, Three Secret Formats

A simplified view of the cluster has three sides that matter for this post:

  • A developer, or automation pipeline, talks to the Kubernetes API server with credentials. That is the canonical front door.
  • On every node, the kubelet exposes its own HTTPS API. The same credentials can authenticate to it directly when it is reachable on the network.
  • The cluster's nodes pull images from container registries (Docker Hub, GitHub Container Registry, ECR, Quay, GitLab, ACR…) using a second set of credentials.
Kubernetes Architecture

These are the attack surfaces where leaked secrets have the most impact, and three secret formats unlock them:

  • TLS client certificates are used by humans through a kubeconfig file with the kubectl command to connect to a Kubernetes cluster.
  • JSON Web Tokens, or Service Accounts, are non-human identities (NHI) used to automate cluster operations from CI/CD jobs, controllers, and integrations. By default, JWTs have no expiration date — which is why a JWT leaked years ago can still be valid today.
  • Container registry credentials live in the cluster as Secret objects of type kubernetes.io/dockerconfigjson. It is a base64-encoded JSON document. The legacy dockercfg format also exists, but is now rare in the wild.

These three formats, TLS, JWT, Docker config JSON, are what we will detect, validate, and exploit below.

Kubernetes API Server

A kubeconfig is a one-file credential. Leaking it leaks the cluster.

apiVersion: v1
kind: Config
clusters:
- name: minikube
  cluster:
    server: https://10.11.12.13:8443
    certificate-authority-data: LS0tLS1CRUd[..]LS0tCg==
users:
- name: minikube
  user:
    client-certificate-data: LS0tLS1CRUd[..]LS0tCg==
    client-key-data: LS0tLS1CRUd[..]LS0tCg==
contexts:
- name: minikube
  context:
    cluster: minikube
    user: minikube
current-context: minikube

In this example, the client-certificate-data and client-key-data fields are the actual credential. Everything else is configuration. You don't need kubectl to confirm one of these works. The API server is just an HTTPS service, and curl is enough.

# 1. Decode the base64-encoded key and certificate
echo LS0tLS1CRUd[..]LS0tCg== | base64 -d > key.pem
echo LS0tLS1CRUd[..]LS0tCg== | base64 -d > cert.pem

# 2. (Optional) Sanity-check that the cert and key match
openssl rsa  -in key.pem  -modulus -noout | sha256sum
openssl x509 -in cert.pem -modulus -noout | sha256sum

 # 3. Use the credentials against the API server
 curl --insecure --key key.pem --cert cert.pem https://10.11.12.13:8443/api/v1/namespaces

--insecure is fine here as we are validating the client credential, not the server's identity. A successful response means that credentials are valid. JWTs can be tested the same way.

Exploitation Scenarios

The credentials we found in the wild are almost always over-privileged. With them, an attacker has several options:

  • Lateral movement: deploy a privileged pod, escalate from container to node.
  • Persistence: create or alter a service account to obtain a long-lived JWT.
  • Credentials access: dump every Secret object in every namespace. This is where the chain reaction starts: one credential gives access to many more.

For example, we found one valid JWT pulled from a public Docker image that gave us access to a cluster. The cluster's Secrets contained valid registry credentials. Those credentials, in turn, opened private Docker Hub images and private GitHub organizations.

For a deeper exploration of post-compromise persistence inside Kubernetes, the Insomni'hack 2025 talk on the topic (https://insomnihack.ch/talks/beyond-the-surface-exploring-attacker-persistence-strategies-in-kubernetes/) is worth a read.

Hardening

None of the mitigations are exotic:

  • Network isolation: don't expose the API server to the Internet. Every major cloud provider has settings for this.
  • Logging and monitoring: treat your cluster like SSH: log every authenticated request and alert on suspicious activity through your SIEM.
  • Short-lived credentials: if you must allow public access, plug the cluster into an OIDC provider (AWS IAM, Azure AD, Okta). The exposure window when a token leaks goes from years to hours or minutes.
  • Least privilege: bind service accounts to the namespace they need, not the whole cluster.
Kubernetes Security Tutorial: Pods
Get a deeper understanding of Kubernetes Pods security with this first tutorial.

Kubernetes Container Registries

Registry credentials live inside the cluster as Secret objects:

apiVersion: v1
kind: Secret
metadata:
  name: registry
data:
  .dockerconfigjson: eyJhdXRocyI6eyJodHRwczovL2luZGV4LmRvY2tlci5pby92MS8iOnsidXNlcm5hbWUiOiJnaXQiLCJwYXNzd29yZCI6Imd1YXJkaWFuIiwiYXV0aCI6IloybDBPbWQxWVhKa2FXRnVDZz09In19fQo=
type: kubernetes.io/dockerconfigjson

Here the .dockerconfigjson field contains the real credential encoded in base64. A short jq command unwraps the structure:

echo $DOCKERCONFIGJSON | jq -R '@base64d | fromjson | .auths'
{
  "https://index.docker.io/v1/": {
    "username": "git",
    "password": "guardian",
    "auth": "Z2l0Omd1YXJkaWFuCg=="
  }
}

From there, validating and using the credential is a docker login away:

docker login -u "$USERNAME" -p "$PASSWORD"
docker pull "$USERNAME/image:$TAG"

For ECR, ACR, and similar managed registries, you will first exchange your static credential for a bearer token  before pulling.Three operations matter once you are authenticated: pull (read), push (write), and enumerate the images.

Exploitation Scenarios

The interesting twist about registry credentials is that they rarely scope to the registry alone:

  • A GitHub Personal Access Token used for ghcr.io will, by GitHub's own design, also work against the GitHub API itself: list private repositories, read user details, enumerate organizations. We will see the impact below.
  • A Docker Hub credential also exposes the user's account metadata.

So one leaked registry secret typically gives an attacker:

  • Discovery: pulling other Docker images from the same account, and  for GitHub tokens, enumerating git repositories.
  • Lateral movement: compromising images that the cluster's privileged pods are about to pull.
  • More credentials: private images and private repositories tend to contain more hardcoded secrets.

Hardening

Most of what we documented would have been prevented by three habits:

• Use private container registries: public registries are not a place to store private artifacts.

• Read-only credentials wherever possible. Your cluster only needs pull. It does not need push, and it certainly does not need repo scope on GitHub.

• Revoke at decommission. When a cluster goes away, revoke its registry credentials too. The data shows that registry credentials regularly outlive the clusters they served.

Public Leaks & Responsible Disclosures

In fall 2025, we scanned public GitHub and Docker Hub for the three secret formats and validated every match. Here is what we found.

Kubernetes API Server Credentials

• 44 unique active clusters in total. Four had more than 10 nodes, and one had more than 200 nodes:  a serious production environment.

• 30% of these clusters had been exposed for more than two years. The oldest still valid leak dated back to 2021.

• Only 10% of the leaked secrets had been deleted from their source. The rest were still sitting where they were originally published. Typically because the developer deleted the commit but never rotated the credential.

One pattern stood out: valid JWTs were almost exclusively found on Docker Hub, not GitHub. Our theory is that certificates leak when humans accidentally commit a kubeconfig, while JWTs leak when service accounts are baked into container images during a CI build.

Container Registry Credentials

• 2,034 registry credentials with resolvable hostnames.

• Provider breakdown: 1,025 Docker Hub, 480 GitHub, 276 Quay, 249 GitLab, 59 Azure.

• 46% of resolvable credentials were still valid.

• 309 private Docker Hub images discovered through these credentials.

• 730 private GitHub repositories accessible: not from a GitHub leak, but from a Kubernetes Secret leak.

• Some leaks were as old as 2022 and still valid.

Disclosure

GitGuardian already notifies developers when their leaked credentials are detected on public GitHub. Here, we went one step further: up the chain from the developer to the company. Mapping a credential back to its owner is its own research task: pod names, hostnames, all became identification clues. In total, we contacted 11 companies:

• Kubernetes API server: 7 owners identified and contacted

• Container registries: 4 owners identified and contacted.

Most of that work was spent finding the right person to email. RFC 9116 (/.well-known/security.txt) would have really helped us here. If you operate an internet-facing service, please consider implementing it.

From Research to Product

This work fed into the detection and validity engines inside GitGuardian. Both the Kubernetes TLS-certificate and JWT checkers, and the container-registry checker, were tuned during the research and are now consistently catching new leaks across the providers we covered (Docker Hub, GitHub, GitLab, Quay, Azure).

In Q1 2026 alone, those detectors found close to 2,000 new Kubernetes secret leaks on GitHub; 28% valid at leak time.

Leaked Kubernetes secrets are diverse, misconfigurations make them worse, and the lateral-movement scenarios are real. None of the hardening recommendations above are new: they are already well documented. The work is in applying them, and in catching the leaks early when they slip through.

📹
This post is based on the talk "All You Can Leak: Real Tales of Publicly Leaked Kubernetes Secrets" given at BlackAlps 2025. Watch the full talk on YouTube: