ZodOps

Data Security · 5 min · Feb 22, 2026

Data Security

Find It Before You Label It

Shadow data isn't a tooling gap. It's an inventory gap.

The data that matters most is rarely where the architecture diagram says it is. It's in a debug dump on a persistent volume, a log sink nobody rotated, or a "temporary" export bucket from a migration two years ago that nobody went back to close.

A classification tool can label every column in the database you already know about and still miss all of that. Labeling isn't the first problem. Not knowing where the data physically landed is.

What it looks like in practice

During a database migration, an engineer exports the customer table to a bucket so the new system can load it. The migration succeeds. The bucket stays. It was created under a project-wide role, its policy trusts a broad group of principals "for now," and the job that wrote to it still runs on a schedule because nobody removed the CronJob.

Two years later, the DSPM dashboard shows strong coverage. The production database is classified, encrypted, and monitored. The export bucket either sits outside the scanner's scope or shows up as one more low-priority finding in a long queue.

Nothing in that chain was malicious, and the tooling did what it was configured to do. The gap was that nobody had a current map of where customer data lived, which identities could reach it, and where it could go.

Inventory before classification

Classification without inventory is a dashboard, not a control. Before you can say what's sensitive, you need to know every place sensitive data could be.

Modern DSPM tools are good at discovering cloud-native stores through provider APIs, and that's real value. But the stores that cause incidents often live where API-level discovery is weakest: volumes attached inside a cluster, log pipelines, ad-hoc exports, SaaS tools, and copies on someone's laptop.

In Kubernetes, the workload spec tells you most of what you need. Walk it: service accounts, volume mounts, secrets pulled in through envFrom, workload-identity annotations that map a pod to a cloud role, and the egress policy, or the absence of one. In a typical estate, that walk turns up more surprises than the classifier will.

Identity, not label, is the actual control

The question that matters isn't "is this column labeled PII?" It's "which identity can reach this bucket, table, or queue, and from where?"

Most teams audit IAM roles carefully and give resource policies far less attention. But a bucket policy that trusts a wider set of principals than anyone realizes, whether another account, a broad group, or the public, is as big a hole as an over-permissioned role. The same goes for KMS key policies: whoever can use the key can read what it protects.

Map the access graph first: identities, the resources they can reach, and the resource policies that grant access from the other side. Classification slots in underneath it and tells you which edges of the graph matter most.

A starting inventory in three commands

These are a starting point, not a full audit. They run read-only against the cluster you're logged into.

Which identity each workload runs as, and what data it mounts:

kubectl get deploy,statefulset,daemonset,cronjob -A -o json \
  | jq -r '.items[]
      | (.spec.jobTemplate.spec.template.spec // .spec.template.spec) as $p
      | [.kind, .metadata.namespace, .metadata.name,
         ($p.serviceAccountName // "default"),
         ([$p.volumes[]?.persistentVolumeClaim.claimName // empty] | join(",")),
         ([($p.volumes[]?.secret.secretName // empty),
           ($p.containers[]?.envFrom[]?.secretRef.name // empty)] | join(","))]
      | @tsv'

Columns: kind, namespace, name, service account, mounted volumes, secrets. CronJobs keep their pod spec one level deeper than Deployments, which is why the command checks both paths.

Which service accounts map to cloud identities:

kubectl get serviceaccounts -A -o json \
  | jq -r '.items[]
      | (.metadata.annotations // {}) as $a
      | ($a["eks.amazonaws.com/role-arn"]
         // $a["iam.gke.io/gcp-service-account"]
         // $a["azure.workload.identity/client-id"]) as $cloud
      | select($cloud)
      | [.metadata.namespace, .metadata.name, $cloud] | @tsv'

Every line is a bridge from the cluster into your cloud account. Those cloud roles are where your bucket and database access actually lives.

Which namespaces have no egress policy at all:

comm -23 \
  <(kubectl get ns -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' | sort) \
  <(kubectl get networkpolicy -A -o json \
      | jq -r '.items[] | select(.spec.policyTypes[]? == "Egress") | .metadata.namespace' \
      | sort -u)

A namespace on this list can send data anywhere its network allows. Having an egress policy doesn't guarantee it's tight, and policies only take effect if your network plugin enforces them, but having none is a clear signal.

Then write the control that can actually fire

Bind exfiltration controls to an identity and a destination, such as the job's service account and the bucket it's allowed to write to, rather than to a hopeful regex running against a sample of traffic.

Alert on the first time a principal touches a sensitive store, or the first time data flows to a new destination. Volume thresholds still catch the loud cases. But a slow, quiet copy stays under any threshold, and "never seen before" is much harder to stay under.

Finally, give people a legitimate path. Operations teams have real reasons to export data in bulk. If there's no sanctioned way to do it, they'll invent one, and that's how the export bucket in the scenario got created.

What to do

  • Inventory before you classify. Walk workload specs, service accounts, mounts, secrets, and egress before buying or tuning a classifier.
  • Include what API discovery misses: in-cluster volumes, log pipelines, ad-hoc exports, and SaaS copies.
  • Treat bucket and KMS policies as part of the access graph, not a footnote to IAM. Check for cross-account, broad-group, and public trust.
  • Map workload identities to cloud roles, so you can answer which pod can reach which data store.
  • Flag namespaces with no egress policy, and confirm your network plugin actually enforces the ones you have.
  • Alert on first-seen principals and first-seen destinations, alongside volume thresholds.
  • Give operations a sanctioned bulk-export path: time-boxed, logged, reviewed, and cleaned up automatically.
  • Put an expiry on "temporary" buckets, exports, and access grants, so they don't become permanent by default.

The takeaway

A DSPM score can go up while you still can't answer a simple question: which service account could walk the customer table out of the cluster tonight?

If you can answer that, the score means something. If you can't, it measured something else.

More in Data Security