Teams that want control over third-party code usually reach for one of two answers, copying dependencies into the repository (vendoring) or forcing every install through a private registry proxy, and each closes some supply chain attacks while quietly opening others. This guide compares the two as security controls rather than build conveniences: the threat model each addresses, how each works in the main ecosystems, the ways each fails, the controls that make each safe, and how to decide. It goes deeper than the overview in the Malicious Package and Dependency Confusion Guide and does not repeat the repository set-up detail in the Private Package Repository and Proxy Guide. It is for application security leads, platform and build engineers, and anyone writing a dependency policy.
Key takeaways
- The short answer: a curated private registry proxy is the better default for most organisations because it scales across languages and teams and is where policy can be enforced; vendoring fits small dependency sets, air-gapped or regulated builds, and languages whose tooling supports it natively.
- They solve different problems. Vendoring freezes exactly what you build and removes the network from the build; a proxy controls what may enter the organisation at all and keeps a cache when upstream disappears.
- Neither stops a malicious version you choose to take. Both faithfully deliver whatever was approved, so the real control is what happens before a new version is accepted: cooldowns, malware scanning, review.
- Vendoring trades attack surface for staleness. Copied code ages silently unless tooling tracks and updates it, and hand edits to vendored files hide changes from scanners and reviewers.
- A proxy is only a control if it is the only path. A client that can fall back to a public index, or mixes internal and public indexes, reintroduces dependency confusion.
What each approach is
Vendoring
Vendoring means committing the source of every dependency into your own repository and building from that copy, with no fetch from a package registry at build time. Support varies by ecosystem:
- Go:
go mod vendorwrites avendordirectory and avendor/modules.txtmanifest of module versions. The Go modules reference states that the vendor directory is used automatically when present and thegodirective ingo.modis 1.14 or higher, that builds under-mod=vendordo not use the network or module cache, and that the go command reports an error ifmodules.txtis inconsistent withgo.mod. - Rust:
cargo vendorcopies all crates.io and git dependencies into a directory and prints the source replacement configuration to add to.cargo/config.toml. The--lockedflag fails ifCargo.lockis missing or would change, and the Cargo book states vendored sources are read-only to Cargo, so changes go through[patch]or a path dependency. - Python: there is no single vendor command for applications; the closest equivalent is a committed or stored set of wheels installed with pip's hash-checking mode, in which every requirement must be pinned and carry a hash.
- Ruby: Bundler can store gems in a vendor cache directory in the repository, and GitHub Dependabot's
vendoroption maintains that cache.
Private registry proxy
A private registry proxy (often called an artefact repository, binary repository manager or package mirror) sits between clients and public registries. Clients point only at the proxy; the proxy fetches from upstream on first request, caches the artefact and serves it from cache after that. Most also host internal packages and present internal and proxied content behind one virtual endpoint. The OpenSSF Secure Supply Chain Consumption Framework (S2C2F) lists this as practice ING-2, noting that a local cached copy protects against incidents like left-pad and lets developers keep building if upstream resources are unavailable. Go's public module mirror shows the same idea at ecosystem scale: proxy.golang.org caches content to avoid breaking builds, and its documentation notes that a bad release removed at its origin may still be served from the cache.
The proxy becomes a security control only when it is also a policy point: blocking known malicious packages (S2C2F ING-3), scanning for malware (SCA-4) and enforcing a curated feed that developers cannot bypass (ENF-2).
Side-by-side comparison
| Property | Vendoring | Private registry proxy |
|---|---|---|
| What is controlled | The exact bytes a given repository builds | What any client in the organisation may download |
| Network at build time | None needed for dependencies | Proxy must be reachable |
| Upstream removal or outage | No effect | Cached versions keep working; uncached versions fail |
| Dependency confusion | Not exposed at build time, exposed when re-vendoring if resolution is not locked to the right source | Closed only if clients cannot reach public indexes directly and internal names are reserved or routed |
| Malicious new upstream version | Enters at re-vendor time if accepted | Enters at first request if not quarantined |
| Visibility to reviewers | Full diff of dependency code in pull requests, often too large to read | Version changes in lockfiles; code not in the repository |
| Central policy and audit | Per repository | Organisation-wide, with download logs |
| Staleness risk | High unless update tooling covers vendored code | Lower; updates flow through normal tooling |
| Repository size and churn | Grows with every dependency and update | Unchanged |
| Air-gapped builds | Natural fit | Requires an internal proxy populated in advance |
| Main new risk | Silent edits and ageing of copied code | The proxy itself: its credentials, configuration and admin access |
Threat models
The S2C2F threat table maps real attack classes to consumption practices. The table below uses those mappings and adds which approach is exposed.
| Threat | Example cited by S2C2F | Vendoring | Proxy | S2C2F practices |
|---|---|---|---|---|
| Dependency confusion and package substitution | Dependency confusion research, 2021 | Safe at build; at risk when refreshing if tooling resolves from mixed sources | Safe only as single upstream with internal names protected | ENF-1, ENF-2 |
| Hijacked maintainer account publishes malicious version | ua-parser-js | Exposed at next re-vendor | Exposed at first request unless quarantined | AUD-1, ENF-2, SCA-4 |
| Typosquatting | Automated npm typosquats | Exposed when a developer adds the wrong name | Blockable with allowlists or malware scanning | AUD-1, ENF-2, SCA-4 |
| Upstream removal breaks builds | left-pad | Immune | Immune for cached versions | ING-2, ING-4 |
| Inherited vulnerabilities and end of life | SaltStack, log4net | High risk of staying stale | Lower, with scanning and update tooling | UPD-2, UPD-3, SCA-3 |
| Build-time compromise of upstream | CCleaner | Not addressed unless you rebuild from source | Not addressed unless you rebuild from source | REB-1 |
- Dependency confusion in detail: the 2021 research that named the attack found internal package names leaking through build artefacts and public code, and showed that pip's
--extra-index-urlchecks both the internal index and public PyPI and installs the higher version, so a public package with an inflated version number won. The author reported the issue in more than 35 organisations across npm, PyPI and RubyGems and recommended--index-urlas the immediate fix. A proxy that merges internal and public content behind one URL can recreate the same race inside the proxy unless internal names are routed only to the internal repository. - Malicious updates in detail: automated update tools plus a proxy that caches anything on first request can carry a hijacked release into many repositories within minutes. In the axios npm compromise, a malicious release was live for about three hours and bot-opened upgrade pull requests were merged without human review in some repositories. Vendoring slows this down only because re-vendoring is less frequent, not because it inspects anything.
- Stale vendored code in detail: once copied, a dependency no longer appears in a registry request, so tools that rely on manifests may miss it. Go's
modules.txtand Cargo's lockfile keep versions visible; hand-copied directories with no manifest do not.
Failure modes
- Proxy with a public escape hatch: developer machines or CI runners that can reach public registries directly, or configuration files listing both the proxy and a public index. One mis-set
.npmrc,pip.conforsettings.xmlbypasses every proxy policy. S2C2F ENF-1 covers securing these package source files. - Cache-on-first-request with no quarantine: the proxy fetches and serves a brand-new upstream version immediately, so it becomes a fast distributor of malicious releases rather than a filter.
- Edited vendored code: local patches applied directly inside
vendor/make the copy differ from any published version, so vulnerability databases and SBOM tools misreport it. Cargo steers changes to[patch]; Go reports inconsistency betweenmodules.txtandgo.mod; other ecosystems rely on discipline. - Re-vendoring from an unlocked or mixed resolver: refreshing the vendor directory on a laptop with a public fallback configured imports exactly what the proxy model was meant to prevent.
- Unreviewable vendor diffs: a dependency update can add tens of thousands of lines to a pull request. Reviewers approve without reading, which gives false assurance.
- Proxy credentials as a single point of failure: publish and admin tokens for the proxy allow an attacker to plant packages that every build will trust. See If your artefact and container registry is compromised for response planning.
Controls that make each approach safe
For a private registry proxy
- Make it the only route: block egress from CI and, where practical, developer networks to public registries; set one index URL per ecosystem and remove extra index entries.
- Reserve and route internal names: use scopes or namespaces for internal packages and configure the proxy so internal names never resolve upstream. Ecosystem detail is in the Private Package Repository and Proxy Guide.
- Quarantine new versions: hold newly published upstream versions for a waiting period and scan for malware before release to builds (S2C2F SCA-4, ENF-2). Pair this with a cooldown in update tools: Dependabot's
cooldownoption delays version updates by a set number of days and does not apply to security updates. - Maintain a deny list that incident responders can update quickly (S2C2F ING-3).
- Protect the proxy's identities: short-lived, scoped credentials for CI, separate publish and read rights, and logging of every admin change.
For vendoring
- Use the ecosystem's native mechanism (
go mod vendor,cargo vendor --locked) so a machine-readable manifest records versions. - Re-vendor only in CI from the locked resolution through the organisation's proxy, never by hand on a workstation.
- Forbid edits inside the vendor directory: use CODEOWNERS or branch protection on
vendor/, and use[patch]or forked modules for local changes. - Keep update tooling pointed at vendored code: GitHub Dependabot documents that vendored Go modules are maintained automatically and that the
vendoroption covers Bundler. - Verify the copy: fail CI if re-vendoring from the lockfile produces any diff.
For both
- Lock and hash: lockfiles with integrity hashes, and pip
--require-hasheswith sha256 for Python. See Dependency Pinning vs Version Ranges. - Scan what you ship: SBOM generation and software composition analysis across vendored and fetched code alike; see SBOM vs SCA.
- Require human review for new direct dependencies and for updates that add install scripts.
Choosing: decision guidance
| Situation | Fit | Reason |
|---|---|---|
| Many teams, several languages | Proxy | One policy point and one audit trail instead of per-repository copies |
| Air-gapped or classified build environment | Vendoring, or a pre-populated internal proxy | No network at build time |
| Go or Rust service with modest dependency tree | Vendoring is practical | Native tooling, manifest and consistency checks |
| JavaScript application with thousands of transitive packages | Proxy | Vendoring creates unreviewable churn |
| Regulated release needing exact source reproduction years later | Vendoring plus archived proxy cache | Source and binaries both retained; S2C2F ING-4 mirrors source |
| Small organisation with no platform team | Lockfiles with hashes, single index, cooldowns; add a managed proxy when possible | Most of the dependency confusion benefit without running infrastructure |
| Internal package names published or leaked | Proxy with name routing, and reserve the names publicly where the registry allows | Vendoring does not protect the next resolution |
Many mature programmes use both: a curated proxy as the organisation-wide intake, and vendoring for the small set of services that must build offline or reproduce exactly. The S2C2F Secure Supply Chain Consumption Framework Guide places single-feed configuration at maturity level 2 and enforced curated feeds at level 3, which is a useful way to sequence the work.
Worked scenario
A software company runs a Go back end, a large TypeScript front end and Python data jobs. A security review finds internal npm package names in a public source map and a pip.conf using --extra-index-url.
- Immediate: switch Python to a single
--index-urlpointing at the proxy and route internal npm scopes only to the internal repository. Check public registries for packages using the leaked names. - Front end: stays on the proxy, with a quarantine period for new upstream versions, malware scanning and a cooldown in the update bot. Vendoring is rejected because a single update would add thousands of files.
- Go back end: adopts
go mod vendorbecause it must build in a restricted network for a customer deployment. Re-vendoring runs in CI through the proxy; a CI job fails ifvendor/differs from a fresh vendor of the locked versions; CODEOWNERS protects the directory. - Data jobs: pinned requirements with sha256 hashes and
--require-hashes, installed from the proxy. - Measurement: egress logs confirm no CI runner reaches public registries directly after the change.
Detection and measurement
- Egress logs: any connection from CI or build networks to public registry hosts.
- Proxy logs: first-time downloads of new package names, requests for internal names that reached upstream, and blocked malicious packages.
- Repository signals: commits that touch vendor directories outside the re-vendor job; package source files that add extra indexes.
Metrics:
- Share of builds resolving dependencies only through the proxy or a vendor directory.
- Number of package source configuration files with more than one index (target zero).
- Median age of vendored dependencies against the latest patched version.
- Time from a deny-list addition to the package being blocked everywhere.
- Share of dependency updates that waited out the cooldown before merge.
Framework mapping
| Framework | Reference | How it applies |
|---|---|---|
| OpenSSF S2C2F | ING-2, ING-3, ING-4, ENF-1, ENF-2, SCA-4, UPD-2 | Proxy caching, deny lists, source mirroring, package source configuration, curated feeds, malware scanning, automated updates |
| NIST SP 800-53 Rev 5 | SR-3 Supply Chain Controls and Processes | Defined intake path for third-party code |
| NIST SP 800-53 Rev 5 | SR-4 Provenance; SR-11 Component Authenticity | Knowing and verifying where each dependency came from |
| NIST SP 800-53 Rev 5 | CM-14 Signed Components | Verifying signatures or provenance at intake where ecosystems offer it |
| NIST CSF 2.0 | ID.AM-02; PR.PS-02 | Inventory of software in use; maintaining and replacing software commensurate with risk |
| MITRE ATT&CK | T1195.001 Compromise Software Dependencies and Development Tools | The attacker behaviour both approaches aim to block |
Where identity fits
Both approaches move trust onto a small number of machine identities: the CI identity that re-vendors and the tokens that read from and publish to the proxy. Many recent package compromises began with a stolen publishing token, so treat proxy and registry tokens as privileged non-human identities with owners, short lifetimes and rotation. The npm Tokens Security Guide and PyPI API Tokens and Trusted Publishing Security Guide cover the ecosystem specifics.
Practitioner checklist
- Decide per ecosystem whether the default is proxy, vendoring or both, and write it into the dependency policy.
- Remove every extra index from package source files and block direct egress to public registries from CI.
- Route internal package names only to the internal repository and check public registries for those names.
- Quarantine new upstream versions in the proxy and configure cooldowns in update bots.
- Maintain a deny list that responders can update within minutes.
- Vendor only with native tooling and lockfiles, re-vendor only in CI, and fail on drift.
- Protect vendor directories from hand edits with ownership rules.
- Point update and scanning tools at vendored code as well as manifests.
- Scope, shorten and log the proxy's read, publish and admin credentials.
- Track the metrics above quarterly.
Standards and references
- OpenSSF: Secure Supply Chain Consumption Framework (S2C2F) specification
- Go: Go Modules Reference, Vendoring
- Go: Go module mirror, index and checksum database
- Rust: The Cargo Book, cargo vendor
- Python Packaging Authority: pip secure installs
- GitHub: Dependabot options reference
- Alex Birsan: Dependency Confusion
Related NHI Mgmt Group resources: Software Supply Chain Security Guide · Malicious Package and Dependency Confusion Guide · Private Package Repository and Proxy Guide · Reproducible Builds Guide · Go Vulnerability Management Guide