You have a two-line change staged for platform-base. It is the overlay every team in the org builds on. Maybe you are tightening a securityContext so the whole estate runs as non-root. Maybe you are changing a commonLabels value that Kustomize propagates into selector fields, and a Deployment selector is immutable once it exists, so for everyone downstream this is a delete and recreate rather than a rolling update. Maybe you are dropping a Kubernetes API version that a recent release finally removed. You make the edit, and you reach for a tag.

Then the question lands, the way it always does right before you cut a release. Who builds on this?

You look at the base repository for an answer. There is a kustomization.yaml, a base/ directory of manifests, and a README that three people have edited and nobody has read. None of it names a single overlay that points at this base. Nothing inside the base knows who depends on it. That information lives somewhere else, in repositories you are not currently looking at, and the base has no way to point you towards them.

This is the same structural fact behind the whole Find Every Consumer series: the dependency lives in the relationship between repositories, and the artifact you are changing cannot see its own dependents. We have worked through that shape for Terraform modules, Docker base images, Helm charts, and six other ecosystems. Kustomize is the last of them.

Here is the conflation worth naming. Kustomize gets filed in everyone’s head as a local templating thing. You write overlays, you run kustomize build, you get manifests. And for a single repository that is exactly what it is. But the moment a base goes remote, the moment one overlay’s resources: list points at another repository’s directory, Kustomize stops being a local tool and becomes a cross-repo dependency mechanism with a version. Teams adopt remote bases for precisely that reason, to share a hardened, opinionated base across the org without copying it. And then they get no consumer map, because nothing in the toolchain was built to give them one. So let us answer it properly. If you change a shared Kustomize base, what breaks, and how do you find every overlay that builds on it before you find out the hard way?

Where your base actually gets consumed

Start with the shape of the problem, because Kustomize reuse spreads a base’s dependents across more places, and more spellings, than people expect.

The pattern that matters is the remote base. One repository holds a base, a kustomization.yaml and the manifests it assembles, and overlays in other repositories pull it in by URL through the resources: list:

# my-app/overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - https://github.com/platform/configs//bases/webapp?ref=v1.2.0
namespace: production
images:
  - name: webapp
    newName: registry.example.com/team/webapp
    newTag: v2.1.0

That resources: entry is a cross-repo dependency. The overlay lives in the application team’s repository. The base lives in the platform team’s repository. The ?ref=v1.2.0 is a version pin on that dependency, sitting inside a URL string. And this is not an exotic usage. The common shape in practice is composition across teams, an overlay assembling bases that several different teams each own:

resources:
  # Platform team's standard webapp base
  - https://github.com/platform/configs//bases/webapp?ref=v1.0.0
  # Security team's network policies
  - https://github.com/security/policies//network-policies?ref=v2.1.0
  # Observability team's standard service monitor
  - https://github.com/monitoring/configs//prometheus-servicemonitor?ref=v1.5.0

This composition approach is documented as real-world practice, and it is exactly the cross-repo coupling that goes unmapped. When I actually counted every cross-repo edge across two real orgs, this kind of infrastructure coupling was the entire story — not one edge was a code symbol. Every one of those three bases lives in a repository other than the one doing the assembling. Each consuming overlay names the base it depends on in a URL, not in a clean, typed dependency block. And the components: list behaves the same way, as does the older bases: field that is deprecated but very much still live in real repositories.

It is worth being precise about how this differs from the Helm post, because the two are the pair that covers Kubernetes config reuse. Helm shares config through templated, versioned charts pulled from a registry, and that post covered the consumer side of it in full, the Chart.yaml dependencies, the ArgoCD Application and Flux HelmRelease manifests, the OCI and HTTP registry forms. Kustomize shares config through overlays that patch a base. The chart is a package; the base is a directory in a repo. The one place the two worlds touch is the inline helmCharts: field, where a Kustomization renders a Helm chart directly, and that is the seam back to the Helm post. Everything else here is the other half of the problem: the overlay model, where a remote base is the unit of reuse and a URL is the dependency edge.

This is the same structural shape we covered for Terraform modules pulled by git:: source. A directory in one repository, referenced by URL from another, pinned by a ref. Kustomize just has more spellings for the URL, and that is where it gets hard.

The version lives inside the URL, and the URL is a mess

Look again at that remote base reference. The version you care about, the thing that decides whether a given overlay is in range of your next release, is not in a version: field. It is the ?ref= query parameter inside the resource string. To know who is pinned to v1.0.0 and who is riding main, you have to parse the URL.

And the URL is not one format. It is a family of formats, and the family is, by the maintainers’ own description, not well-specified.

The official Kustomize docs describe the modern form as an HTTPS or SSH git clone URL, with the in-repo directory appended after a //, and query parameters for ref, version, timeout, and submodules. So all of these are valid, and all of them mean a remote base:

resources:
  # scheme-prefixed HTTPS
  - https://github.com/org/repo//overlays/prod?ref=v1.2.0
  # explicit git:: prefix
  - git::https://gitlab.example.com/infra/base//manifests?ref=v3
  # SSH scheme
  - ssh://[email protected]/infra/legacy-base.git//manifests?ref=main
  # scp-like SSH, no scheme at all
  - [email protected]:org/platform/k8s-cluster-addons.git//kustomize/base?ref=v1.0.0
  # scheme-less host, the canonical form in Kustomize's own examples
  - github.com/org/repo//overlays?ref=v1.0.0

The scheme-less form at the bottom is not a shortcut someone invented. It is the canonical form in Kustomize’s own remote-build examples. No leading https://, no git@, just a host and a path and a //. A grep written to find URLs, anything anchored on https?://, sails straight past it.

If you think this is me picking the worst case, here is the maintainers’ own account of it. There is an open issue cataloguing the URL forms Kustomize currently accepts, and the list includes github.com/org/repo, gh:github.com/org/repo, https://github.com/org/repo, git::https://github.com/org/repo, and github.com:org/repo, with the author noting “and probably a lot more.” It then points out that most of those shortcuts only work for github.com, so the scheme-less form for a different host may or may not resolve, and concludes that the parsing logic “greatly needs an overhaul.” There is a separate legacy go-getter format, still supported for backwards compatibility but no longer recommended, which means it is also still in real repositories.

There is one more wrinkle the same issue surfaces, and it matters for anyone trying to detect these. Kustomize tries to load a resource as a file first and as a remote base second, and the thing that disambiguates is the //. A path with a // delimiting a subdirectory is treated as a remote base; a plain path like ../base is local and is read directly off disk. So the // is load-bearing. Any detector has to use it to tell a cross-repo dependency (gitlab.example.com/infra/base//overlays) apart from a local path that happens to live a directory up (../base), and from a relative path that happens to contain a double slash for unrelated reasons. Get that wrong in either direction and you either miss real edges or invent ones that do not exist.

This is the part of the Kustomize story that is genuinely close to uncontested. Plenty of tools can render an overlay. The reverse question, which overlays across the org point at this base and at which ref, requires parsing a URL format that the project itself calls underspecified, and almost nothing does it.

The image you deploy is not the image you wrote

Remote bases are the headline, but there is a second cross-repo edge in every overlay, and it has a trap in it.

Kustomize overlays rewrite images through the images: transformer. The subtlety, the one that quietly resolves the wrong thing if you are not careful, is that the image actually deployed is the newName, not the name being matched:

images:
  - name: addon                  # placeholder; a name-matching scan wrongly stops here
    newName: registry.example.com/platform/docker-base-images/python   # the image that deploys
    newTag: "3.12"               # so the real edge is: docker-base-images : python @ 3.12

Read that the way a naive scan would, keying off the name: field, and you conclude the overlay depends on an image called addon. It does not. addon is just the placeholder being matched in the base; the image that lands in the running pod is registry.example.com/platform/docker-base-images/python. The real dependency, the artifact whose consumers you would want to track, is the newName. The original name is metadata, useful to keep but not the edge. A consumer map that resolves the matched name instead of the effective name points at the wrong node, or at no node at all when the placeholder is a bare string with no registry in it.

Version comes from newTag, or from a digest when the overlay pins by content:

images:
  - name: registry.example.com/platform/docker-base-images/go
    digest: sha256:1111aaaa...

A digest is the most precise pin there is. It is also where the simple, obvious parsing approach falls over, because the version is not the part after the last colon. Split registry.example.com/org/app@sha256:1111... on : and you mangle the digest. The whole sha256:... string is the version, and digest wins over newTag when both are present, which is a rule Kustomize enforces and Renovate documents precisely because the combination is ambiguous.

And the images do not only live in overlays. Raw Kubernetes manifests carry them too, in containers, in initContainers, and, since Kubernetes 1.25, in ephemeralContainers, the debug containers people attach to a running pod. That last list is the one most tools forget. A maintenance pod that pulls a private image into an ephemeral container is consuming that image just as surely as a Deployment is, and if your scan walks only containers you will miss it.

So a single overlay can declare a remote-base edge to another team’s repository and an image edge to your registry, and a single manifest can declare image edges across three different container lists. None of it looks the same, and all of it is cross-repo.

What the Kubernetes and Kustomize tooling tells you, and where it stops

The tooling around Kustomize is good, and it answers real questions. It answers the other ones.

The Kustomize CLI. kustomize build and kubectl kustomize render an overlay to its final manifests, resolving the remote bases, applying the patches, rewriting the images. This is genuinely useful and it is the right tool for seeing what an overlay produces. It is also forward and per-overlay. You point it at an overlay and it tells you what that overlay becomes. There is no reverse mode, no kustomize who-builds-on-this. It cannot take a base and hand you the overlays across the org that consume it, because it only ever looks down from one overlay, never up from one base.

ArgoCD and Flux. They deploy Kustomize overlays, and if your org has gone all-in on GitOps then in principle every consuming overlay is declared somewhere as an Application or a Kustomization CRD. The Helm post went deep on why “in principle” does a lot of work in that sentence, and the same applies here, so I will keep it short: the manifests are spread across a central gitops repo, per-team repos, and per-environment repos, and “show me every Application whose overlay references base X” is not a built-in view in either tool. There is a sharper version of the problem that is specific to remote bases. When an ArgoCD Application points at one repo whose overlay pulls a private base from a second repo, the deploy tooling frequently cannot make that jump, because its credentials are scoped to the first repo. This is a recurring, documented pain point. The cross-repo edge is real enough to break a deploy, and still invisible to the thing doing the deploying.

Renovate. It is the closest any of these come to caring about consumers, and it deserves a fair hearing, because its Kustomize support is real. The kustomize manager parses kustomization.yaml files, extracts remote resources, image tags, components, and helm charts, resolves the dependency’s source repository, checks for newer SemVer tags, and opens a pull request to bump the version. If you want your remote-base refs and image tags kept current, it does that job.

But, as with every ecosystem in this series, Renovate has to know who consumes what in order to open those PRs, and it does not expose that knowledge as something you can query. You cannot ask it “which overlays reference my base, and at which ref.” It reacts after a new version is published, repo by repo, and only in repos where it is configured. There are two failure modes specific to Kustomize on top of that. Its remote-target URL parsing is brittle: it does not treat the version= parameter as a ref, and multiple query parameters break it, so the very URL forms the previous section catalogued are partly outside what it handles. And for raw Kubernetes manifests, the picture is starker. Renovate’s Kubernetes manager is off by default and matches no files at all until you configure file patterns by hand, for a reason the docs state plainly: there is no commonly accepted naming convention for Kubernetes YAML files, and they do not want to check every *.yaml file just in case. That is not an oversight. It is the same hard problem we are about to get to, and Renovate’s honest answer to it is to make it your job, per repo. Which is exactly the configuration burden the rest of this post argues you should not have to carry.

The general-purpose graphs. They sit at a different layer and have a clean category line, the same one drawn across this series. Sourcegraph’s symbol graph is excellent at programming-language symbols and blind to kustomization.yaml, because a remote base is not a symbol. Port models the catalog you describe in Blueprints rather than parsing the bases your repos already declare, so your base-to-overlay edges are only as complete as what someone remembered to model. Backstage and the developer-portal category solve a real problem, but the catalog is only as accurate as the last person to edit it, which is why teams quietly stop editing it. None of these is wrong. They are answering a different question.

Every tool above is forward, or reactive, or modelled by hand. Render this overlay. Bump this repo when a tag moves. Show me the catalog I wrote down. Not one of them answers the question with your finger on the tag: across all of my repositories, which overlays build on this base, at which ref, and what breaks if I change it.

Why this is harder than it looks

You might reasonably think this is a grep problem. Search every repository for the base’s repo name, collect the hits, done. It is not, and the reasons are specific to how Kustomize references things.

Most remote bases are not URLs

The previous section made the case from the maintainers’ own issue tracker, so I will not relitigate it, only draw the consequence. A grep anchored on https:// misses the scheme-less canonical form. A grep for the bare host string risks colliding with comments, with image registries that share the host, and with documentation. And the // that distinguishes a remote base from a local path is the kind of thing a regex can match but cannot reason about, because telling gitlab.example.com/infra/base//overlays (remote, cross-repo) apart from ../base (local, ignore) and from a path that contains // incidentally requires understanding that the first has a real dotted host in front of the // and the others do not. This is parsing, not searching. The honest version of the detector gates on a dotted host segment before the //, so relative paths stay local and only genuine remote references become edges.

The deployed image hides behind a rename

The newName trap from earlier is a grep problem in miniature. The string you would search for, the effective image, does not appear next to the name: field that a naive scan latches onto. It appears in the newName: line below it, and the matched name above is a placeholder you specifically do not want to resolve. A tool that reads the wrong field produces an edge to a node that does not exist, which is worse than no edge, because it looks like an answer.

You cannot just allow-list every YAML file

This is the one that has no clean shortcut, and it is the same wall Renovate hit when it left its Kubernetes manager off by default.

A formal parser earns the right to own a file, so that a cheaper text-grep pass does not also scan it and emit a second, lower-confidence version of the same edge. For most ecosystems that ownership is by filename. A kustomization.yaml has a fixed name, so a parser can claim it, and the heuristic scanner can cede every file with that name. Good. But raw Kubernetes manifests have no naming convention. They are *.yaml, the single most common file extension in an infrastructure repository, shared with CI config, Helm values, GitHub Actions workflows, and a hundred other things. You cannot allow-list *.yaml to a Kubernetes parser without disabling text-based YAML scanning across the entire organisation. So the Kubernetes case cannot be solved by filename. It has to be solved by content: a parser may claim a YAML file only when the content actually is a Kubernetes manifest, an owned pod-spec kind carrying an image. And it has to be deliberately narrow, because a manifest that carries no image, a ConfigMap, a Service, a CRD, should still fall through to the heuristic scanner, in case it carries an incidental in-org URL worth catching. This is a real, fiddly, important distinction, and it is the difference between a graph you can trust and one that double-counts.

Templated manifests do not fully parse

Some manifests under a templates/ directory are Go-templated YAML that a strict YAML parser rejects outright, because {{ .Values.image }} is not valid YAML. The best you can do is a regex fallback that recovers the literal image: references and accepts a lower confidence for them. The honest boundary is the same one every ecosystem in this series eventually hits: a pure {{ .Values.image }} reference cannot be resolved from source, because the real image is supplied at install time, not committed in the file. A tool that claims to resolve it is guessing. The right move is to recover the literal references, score them honestly, and be straight about the templated ones.

What the full answer requires

Put all of that together and the requirements for actually answering “who consumes this base” fall out:

  1. Scan every repository in the organisation, with no opt-in and no per-repo config. The consuming overlays live in repositories other than the one you are changing, so anything that needs each team to register, annotate, or configure a file pattern will be incomplete the day someone forgets.
  2. Parse every remote-base spelling and pull the ref out of the URL. Scheme-prefixed, git::, ssh://, scp-like SSH, and the scheme-less canonical form, across resources:, components:, and the deprecated bases:, with the ?ref= or ?version= captured as the version.
  3. Resolve image transformers against the effective newName, not the matched name, and read the whole sha256:... as the version when a base pins by digest.
  4. Walk containers, initContainers, and ephemeralContainers in raw manifests, so a debug container consuming a private image is counted, not missed.
  5. Resolve a remote base to the in-org repository that produces it, including the scp-like SSH form, with a guard so a public base that happens to share a name with one of yours is not pulled into your graph.
  6. Record the constraint state of every consumer, whether they pinned you to a ref, rode a branch, or pinned by digest, so you know which overlays move when you tag and which stay put.
  7. Tell these two kinds of edge apart without double-counting them, which means owning kustomization.yaml by filename and Kubernetes manifests by content, and staying current by re-scanning rather than trusting a one-time trawl.

How Riftmap does this

This is the gap Riftmap was built to close, so here is honestly what it does and does not do for Kustomize and Kubernetes.

Riftmap scans a GitLab or GitHub organisation and parses every repository deterministically. There is no language model in the parse path; the edges come from parsing the actual files, and each edge carries a rule-based confidence score. On the Kustomize side it reads remote bases from resources:, components:, and bases: as a kustomize_resource edge at confidence 0.9, recognising the scheme-prefixed, git+, ssh://, scp-like SSH, and scheme-less host forms, and gating on a dotted host segment so local paths stay local. It reads the images: transformer as a kustomize_image edge at 0.85, resolving the effective newName and keeping the matched name as metadata. On the raw-manifest side it reads containers, initContainers, and ephemeralContainers as k8s_container_image edges at 0.85, recording a digest pin as its full sha256:... string. The inline helmCharts: field resolves as a helm_chart_reference, which is the one place this hands back to the Helm world. These dependency-type strings are stable, and the confidence scores are deliberately uneven, because some of these edges are more certain than others and the tool says so rather than flattening everything to one confident-looking line.

The headline for the resolver is the scp-like SSH form, the one with no scheme that a URL parser chokes on. Given [email protected]:polaris-works/platform/k8s-cluster-addons.git//kustomize/base?ref=v1.0.0 in an overlay’s resources:, Riftmap rewrites the git@host:path form to host/path, resolves it to the in-org k8s-cluster-addons repository, and records v1.0.0 as the version. The scheme-less in-org form pointing at the same base resolves to the same repository, and the two declarations dedupe to a single graph edge, so the impact view shows one dependency, not two, while the declaration view still shows both rows.

The double-counting problem from earlier gets solved with the content-versus-filename split, made concrete. Riftmap owns kustomization.yaml by filename and raw manifests by content, so a base is counted once, as a kustomize_resource, never also as an incidental git_url_reference. The filename half is easy. The content half is the careful one: the Kubernetes parser claims a *.yaml file only when it carries an owned pod-spec kind and an image, so a maintenance pod is claimed and ceded, while an image-less ConfigMap beside it still falls through to the heuristic scanner for any org URL it happens to carry. Same double-emit class that bit the ArgoCD parser, solved the same way.

The numbers come from the polaris-works test organisation, a 55-repo synthetic org built to stress exactly these cases, on a full re-scan that completed across all 55 repositories with nothing skipped. The Kustomize and Kubernetes fixtures added three graph edges, taking the org from 180 to 183. What makes the three the point is what they collapse from:

Declared in the fixturesBecomes in the graph
4 image references (2 kustomize_image transformers + 2 k8s_container_image, across container and ephemeral)2 edges, both into the single docker-base-images artifact (python, go, node are tags of it)
5 remote-base references (2 in-org, 3 external)1 edge to the in-org base; the 3 external bases stay unresolved
9 references in source3 edges, 0 new artifacts (180 → 183)

Those three edges are warehouse-api building on k8s-cluster-addons through the scp-like SSH base, and k8s-cluster-addons consuming docker-base-images through both a Kustomize image transformer and a raw Pod container. The collapse is the dedup story made literal: nine references in source become three edges in the graph. And the two cedes held through all of it. Org-wide git_url_reference stayed at 7 and docker_base_image stayed at 19, which is the proof nothing got double-emitted. The artifact count did not move from 87, because Kustomize and Kubernetes consume; they do not produce. The one consumer-count that changed was docker-base-images, from 19 to 20, as k8s-cluster-addons was added once, not twice.

Riftmap's Dependents panel for the platform/docker-base-images Docker artifact in the polaris-works test org, showing twenty consumers. Most are Dockerfile FROM consumers, each sourced from a Dockerfile (shipment-service, ml-models, warehouse-api, analytics-api, payment-worker, and others). At the bottom, k8s-cluster-addons appears once, tagged K8s and Kustomize, resolved from four references at the same confidence: a container and an ephemeral container in a raw maintenance-pod.yaml manifest, one of them pinned by an sha256 digest, plus a Kustomize image transformer in kustomize/overlays/audit/kustomization.yaml. The python, go, and node images are folded into the single repo-level artifact, so the repo is counted as one consumer, not four.

What Riftmap does not claim

The honest limits are part of the product, so here they are plainly.

It does not resolve templated image references. A literal image: line in a Go-templated manifest is recovered at lower confidence, but a pure {{ .Values.image }} is unknowable until install time, and the tool does not pretend otherwise.

A digest pin currently shows its constraint state as branch rather than pinned. The resolution is correct, the edge points at the right artifact, but the state label, the thing that buckets a consumer as pinned or floating, falls through to the wrong bucket for sha256:.... It is cosmetic, it is on the list to fix, and it is the kind of thing worth being upfront about. If you want the full treatment of constraint state, why pinned, floating, and branch are the facts that actually decide your blast radius, I wrote about it across real estates here.

It reads the manifest as committed, not the output of kustomize build. So a generator that synthesises resources at build time is not expanded, by design, because expanding it would mean executing the build rather than parsing the source. The matched name behind a newName is stored as metadata but is not yet surfaced on the public dependencies API. And it recognises your bases and images by your org and registry, so a public base or a public image you merely consume shows up as an external dependency, which is the correct answer, not a repository in your graph.

None of those gaps is hidden behind a confident interface. The point of a blast-radius tool is to be trusted on a Friday afternoon, and you do not earn that by overclaiming.

Ten ecosystems, one graph

Step back, because this is the last ecosystem in the series and the shape of the whole thing is finally visible.

A base’s consumers are the overlays that build on it. An image’s consumers are every file that points at it. And here is the part that only lands once you have done all ten: those are edges into the same graph, and the graph does not care which kind of file the consumer lives in. The docker-base-images artifact in that scan is consumed by Dockerfile FROM lines, the subject of the very first post in this series. It is consumed by Kubernetes Deployment containers. It is consumed by Kustomize image transformers. Three different consumer surfaces, three different file formats, one node, counted once. The Dockerfile, the Deployment, and the overlay are not three separate problems. They are three ways of pointing at the same artifact, and a graph that parses the edges sees them as exactly that.

That is why the series worked one ecosystem at a time but was only ever building one thing. Each post took an artifact most teams cannot get a consumer list for, a Terraform module, a Helm chart, a Go module, an npm package, an Ansible role, a Kustomize base, and showed that the consumer list is unanswerable for the same reason every time: the dependency was never in the file you were changing. It was in the relationship between repositories, and the artifact you were about to break could not see its own dependents. The fix is not a better grep or a catalog someone has to maintain. It is to parse those relationships across the whole organisation and resolve them into a graph. Parsed, not inferred.

Your platform-base change is still sitting there, untagged. Now you can see every overlay that builds on it, at which ref, which ones move when you tag and which stay pinned, and which teams you were about to surprise. Tag it on purpose.


This is the latest post in the Find Every Consumer series, which works through the same question across Docker base images, Terraform modules, GitHub Actions workflows, Helm charts, Go modules, GitLab CI templates, npm packages, Python packages, and Ansible roles, one ecosystem at a time. It completes the run across the ten ecosystems Riftmap parses today, and the graph keeps growing as new ones are added. For the technical version of how each ecosystem fights back, see auto-discovering dependencies across ten ecosystems, and for what this looks like on a real Kubernetes org, see what 208 kubernetes-sigs repos actually depend on.

Riftmap maps cross-repository dependencies across a GitLab or GitHub organisation and answers the change-impact question directly: if I change this, what else breaks? It parses the relationships between your repositories rather than asking you to model them, with no per-repo config. Map your org, or book a walkthrough and we will map it with you.