Problem
When two or more private registries for different package ecosystems live on the same hostname, the Dependabot proxy applies all of their credentials to the same outbound request, each overwriting the last — so the credential that actually reaches the registry is whichever ecosystem's handler runs last in the proxy's fixed handler chain, not the one for the ecosystem being resolved.
We encountered this since all of our repositories were in cloudsmith, and have a common prefix, and we had one that the url was truncated when configured.
How?
1. Credentials are matched per-handler, by host + naive path prefix
|
return strings.HasPrefix(req.URL.Path, strings.TrimRight(parsedURL.Path, "/")) |
A registry configured as https://host/base matches every request under /base/..., including paths belonging to entirely different ecosystems. Virtual every registry handler uses this same logic. e.g.
|
if !helpers.CredentialURLMatchesRequest(req, cred.url, true) && |
|
if !helpers.UrlMatchesRequest(req, cred.url, true) && !helpers.CheckHost(req, cred.host) { |
2. Every handler runs on every request
The handlers are registered as an unconditional OnRequest chain in proxy.go. There is no "this request belongs to ecosystem X, skip the others" dispatch.
|
npmRegistryHandler := handlers.NewNPMRegistryHandler(cfg.Credentials, oidcClient) |
|
proxy.OnRequest().DoFunc(npmRegistryHandler.HandleRequest) |
|
|
|
hexOrganizationHandler := handlers.NewHexOrganizationHandler(cfg.Credentials) |
|
proxy.OnRequest().DoFunc(hexOrganizationHandler.HandleRequest) |
|
|
|
hexRepositoryHandler := handlers.NewHexRepositoryHandler(cfg.Credentials, oidcClient) |
|
proxy.OnRequest().DoFunc(hexRepositoryHandler.HandleRequest) |
|
|
|
pythonHandler := handlers.NewPythonIndexHandler(cfg.Credentials, oidcClient) |
|
proxy.OnRequest().DoFunc(pythonHandler.HandleRequest) |
|
proxy.OnResponse().DoFunc(pythonHandler.HandleResponse) |
|
|
|
composerHandler := handlers.NewComposerHandler(cfg.Credentials, oidcClient) |
|
proxy.OnRequest().DoFunc(composerHandler.HandleRequest) |
|
|
|
dockerRegistryHandler := handlers.NewDockerRegistryHandler(cfg.Credentials, oidcClient, nil) |
|
proxy.OnRequest().DoFunc(dockerRegistryHandler.HandleRequest) |
|
|
|
rubyGemsServerHandler := handlers.NewRubyGemsServerHandler(cfg.Credentials, oidcClient) |
|
proxy.OnRequest().DoFunc(rubyGemsServerHandler.HandleRequest) |
|
|
|
proxy.OnRequest().DoFunc(nugetFeedHandler.HandleRequest) |
|
proxy.OnResponse().DoFunc(nugetFeedHandler.HandleResponse) |
|
|
|
mavenRepositoryHandler := handlers.NewMavenRepositoryHandler(cfg.Credentials, oidcClient) |
|
proxy.OnRequest().DoFunc(mavenRepositoryHandler.HandleRequest) |
|
|
|
terraformRegistryHandler := handlers.NewTerraformRegistryHandler(cfg.Credentials, oidcClient) |
|
proxy.OnRequest().DoFunc(terraformRegistryHandler.HandleRequest) |
|
|
|
openTofuRegistryHandler := handlers.NewOpenTofuRegistryHandler(cfg.Credentials, oidcClient) |
|
proxy.OnRequest().DoFunc(openTofuRegistryHandler.HandleRequest) |
|
|
|
pubRepositoryHandler := handlers.NewPubRepositoryHandler(cfg.Credentials, oidcClient) |
|
proxy.OnRequest().DoFunc(pubRepositoryHandler.HandleRequest) |
|
|
|
cargoRegistryHandler := handlers.NewCargoRegistryHandler(cfg.Credentials, oidcClient) |
|
proxy.OnRequest().DoFunc(cargoRegistryHandler.HandleRequest) |
|
|
|
goProxyServerHandler := handlers.NewGoProxyServerHandler(cfg.Credentials, oidcClient) |
|
proxy.OnRequest().DoFunc(goProxyServerHandler.HandleRequest) |
|
|
|
helmRegistryHandler := handlers.NewHelmRegistryHandler(cfg.Credentials, oidcClient) |
|
proxy.OnRequest().DoFunc(helmRegistryHandler.HandleRequest) |
3. Applying a credential replaces the header
In each handler, if it matches (1) then the last handler that matches the URL will set the authorization headers.
4. Order is fixed and undocumented
Last matching handler wins. From proxy.go, the credential-injecting registry
handlers in execution order:
| # |
Handler |
Note |
| 1 |
npm_registry |
most easily clobbered |
| 2 |
hex_organization |
|
| 3 |
hex_repository |
|
| 4 |
python_index |
|
| 5 |
composer |
|
| 6 |
docker_registry |
|
| 7 |
rubygems_server |
|
| 8 |
nuget_feed |
|
| 9 |
maven_repository |
|
| 10 |
terraform_registry |
|
| 11 |
opentofu_registry |
|
| 12 |
pub_repository |
|
| 13 |
cargo_registry |
|
| 14 |
goproxy_server |
|
| 15 |
helm_registry |
always wins |
So a Go credential silently overrides a Python one; a Helm credential overrides everything.
Replication
All of the following must happen for this to show up
- Two or more private registries resolving to the same hostname (via repo
dependabot.yml registries: or org-level private registry config).
- At least one configured URL is a path prefix of another's request paths — classically a bare base URL like
https://host/base with no ecosystem or repo segment. A bare host field does this unconditionally.
- The handler that runs later holds credentials the target registry rejects — different account, different token type, or an empty password.
- The overridden ecosystem depends on the proxy for auth. For Python this is guaranteed when using
username/password, because AuthedUrlBuilder only embeds credentials into the index URL when a token field is present:
If all same-host registries happen to share one credential, the clobber still happens, but it doesn't effect the process since its a valid auth for all of the registries.
Signature to look for in a job log
[016] GET https://HOST/base/org/py-repo/python/simple/somepkg/
[016] * authenticating python index request (host: HOST) <- intended
[016] * authenticating nuget feed request (host: HOST)
[016] * authenticating maven repository request (host: HOST)
[016] * authenticating terraform registry request (host: HOST)
[016] * authenticating goproxy request (host: HOST) <- this one wins
[016] 401 https://HOST/base/org/py-repo/python/simple/somepkg/
More than one * authenticating line per request ID means credentials are colliding.
Problem
When two or more private registries for different package ecosystems live on the same hostname, the Dependabot proxy applies all of their credentials to the same outbound request, each overwriting the last — so the credential that actually reaches the registry is whichever ecosystem's handler runs last in the proxy's fixed handler chain, not the one for the ecosystem being resolved.
We encountered this since all of our repositories were in cloudsmith, and have a common prefix, and we had one that the url was truncated when configured.
How?
1. Credentials are matched per-handler, by host + naive path prefix
proxy/internal/helpers/helpers.go
Line 75 in 49c2109
A registry configured as
https://host/basematches every request under/base/..., including paths belonging to entirely different ecosystems. Virtual every registry handler uses this same logic. e.g.proxy/internal/handlers/maven_repository.go
Line 112 in 49c2109
proxy/internal/handlers/goproxy_server_handler.go
Line 75 in 49c2109
2. Every handler runs on every request
The handlers are registered as an unconditional
OnRequestchain inproxy.go. There is no "this request belongs to ecosystem X, skip the others" dispatch.proxy/proxy.go
Lines 106 to 150 in 49c2109
3. Applying a credential replaces the header
In each handler, if it matches (1) then the last handler that matches the URL will set the authorization headers.
4. Order is fixed and undocumented
Last matching handler wins. From
proxy.go, the credential-injecting registryhandlers in execution order:
npm_registryhex_organizationhex_repositorypython_indexcomposerdocker_registryrubygems_servernuget_feedmaven_repositoryterraform_registryopentofu_registrypub_repositorycargo_registrygoproxy_serverhelm_registrySo a Go credential silently overrides a Python one; a Helm credential overrides everything.
Replication
All of the following must happen for this to show up
dependabot.ymlregistries:or org-level private registry config).https://host/basewith no ecosystem or repo segment. A barehostfield does this unconditionally.username/password, becauseAuthedUrlBuilderonly embeds credentials into the index URL when atokenfield is present:If all same-host registries happen to share one credential, the clobber still happens, but it doesn't effect the process since its a valid auth for all of the registries.
Signature to look for in a job log
More than one
* authenticatingline per request ID means credentials are colliding.