Every build that downloads packages straight from public registries trusts thousands of strangers and their publishing tokens, with no way to say which source may supply which name, no record of what was fetched and no chance to stop a malicious version before it runs. A private package repository that proxies and curates the public registries (JFrog Artifactory, Sonatype Nexus Repository, cloud artifact services and similar tools) is the main enterprise control against dependency confusion, typosquats and the self-propagating package worms of 2025 and 2026. It only works if every client is forced through it and if it applies namespace and curation policies rather than simply caching whatever is asked for. This guide covers the architecture, namespace rules for npm, PyPI, NuGet and Maven, client configuration, quarantine and curation policies, access control for the repository itself, rollout, detection and metrics. It is for platform, build and application security teams.
Key takeaways
- One front door for every ecosystem. Microsoft's NuGet guidance is that multiple configured feeds enable dependency confusion and that a single feed, or a private feed with upstreaming, protects against it; the same holds for npm, PyPI and Maven.
- Bind names to sources. npm scopes, NuGet package source mapping, Gradle exclusive content and repository include and exclude patterns ensure internal names can only ever resolve from internal repositories.
- Never mix indexes in pip. pip's own documentation says there is no priority between the locations it searches and warns that
--extra-index-urlfor private packages is unsafe. - Curate, do not just cache. Quarantine newly published versions, block known-malicious and vulnerable packages, and keep cached artifacts immutable so the proxy is a checkpoint, not a pass-through.
- The repository is a privileged system. Whoever can deploy to it or change its routing controls what every build receives; separate read, deploy and admin identities and treat CI tokens as NHIs.
Repository architecture and resolution order
Repository managers use the same three building blocks under different names. Artifactory calls them local, remote and virtual repositories; Nexus Repository calls them hosted, proxy and group repositories.
| Building block | Artifactory term | Nexus term | Purpose | Security role |
|---|---|---|---|---|
| Internal store | Local | Hosted | Packages you build and publish | Authoritative source for internal names; deploy rights tightly limited |
| Proxy and cache of a public registry | Remote | Proxy | Fetch from npmjs.com, PyPI, nuget.org, Maven Central and cache | Where curation, quarantine and name blocking apply |
| Aggregated endpoint | Virtual | Group | One URL that clients use, searching members in order | Resolution order and include and exclude patterns decide which member may serve a name |
- Order is security: JFrog documents that a virtual repository searches local repositories first, then remote caches, then the remote repositories, each in the configured list order, and that include and exclude patterns (Ant-style, excludes subtracted from includes) filter which repositories are searched for a path. Order alone is not enough: if the internal repository does not have a version the client asks for (for example a higher version number an attacker published publicly), resolution falls through to the remote. Add exclude patterns on remote members for every internal namespace.
- Nexus: Nexus Repository 3 documents a routing rules feature under repository administration; confirm in your version how it, or an equivalent control, can stop public proxy repositories serving your internal namespaces.
- Separate per ecosystem and trust level: keep separate remotes for each public registry and separate local repositories for releases and snapshots, so policies and permissions can differ.
Namespace policies by ecosystem
| Ecosystem | Client-side binding | Public-side claim | Main trap |
|---|---|---|---|
| npm | Scope internal packages (@example/*) and map the scope to the internal registry: @example:registry=https://repo.example.com/api/npm/npm-internal/ in .npmrc | Create the @example organisation on npmjs.com; npm documents that only the scope owner can add packages to a scope | Unscoped internal package names that also resolve publicly |
| PyPI | Single --index-url (or PIP_INDEX_URL) pointing at the repository manager's virtual index; no --extra-index-url | Register placeholder projects for internal names where policy allows | pip picks the best version across all indexes with no priority |
| NuGet | <clear /> in nuget.config, then packageSourceMapping with internal prefixes (Contoso.*) mapped to the internal feed | Reserve an ID prefix on nuget.org so only your account can publish under it | Metadata commands still send package IDs to every configured source |
| Maven and Gradle | Maven <mirrorOf>*</mirrorOf>; Gradle exclusiveContent for internal groups | Verify your reverse-domain namespace on Maven Central | Internal and public repositories listed side by side without filters |
| Containers | Pull through a registry mirror or proxy; reference images by digest | Claim your organisation name on public registries | Short names that resolve to a public registry by default |
NuGet package source mapping is a useful model for all ecosystems: once a mapping section exists, every package (including transitive dependencies) must match a pattern, exact IDs beat longer prefixes which beat *, and unmatched packages fail restore rather than silently resolving somewhere. It requires NuGet 6.0 or later (.NET SDK 6.0.100, Visual Studio 2022), and older tooling ignores it, so every build environment must be on a compatible version. Java specifics are covered in the Maven Central and Gradle Supply Chain Security Guide; the attack itself in the Malicious Package and Dependency Confusion Guide.
Forcing every client through the proxy
A repository manager that developers can bypass is a cache, not a control. Enforce it at three layers.
- Configuration: ship organisation-wide client configuration:
.npmrcwithregistry=pointing at the virtual npm repository,pip.confwith a singleindex-url,nuget.configstarting with<clear />, Mavensettings.xmlwith a*mirror, and Gradle repositories declared centrally in settings. Microsoft recommends<clear />precisely so user and machine-level sources cannot leak into a build. - Network: block direct egress from CI runners and build networks to public registries (registry.npmjs.org, pypi.org and files.pythonhosted.org, api.nuget.org, repo1.maven.org) so a stray configuration fails loudly. Allow only the repository manager to reach them.
- Verification: commit lockfiles with integrity hashes (
package-lock.json, NuGet lock files,gradle.lockfile, pip requirements with--hash, which switches pip into hash-checking mode) so a substituted artifact fails installation even if it arrives through the proxy. - Clean agents: reset CI agents after every build and keep package caches inside the agent workspace, as Microsoft advises for NuGet; a poisoned global cache is otherwise reused by the next job.
Curation and quarantine policies
| Policy | What it stops | Trade-off |
|---|---|---|
| Block internal namespaces on public remotes | Dependency confusion | None, once namespaces are inventoried |
| Quarantine new versions for a set period (for example 24 to 72 hours) before they are served | Worm-published and hijacked versions that are usually detected and removed within hours or days | Urgent security fixes need a documented fast-track exception |
| Deny known-malicious packages from threat feeds and advisories | Typosquats and confirmed malware | Depends on feed quality and speed |
| Block or flag versions with critical known vulnerabilities | Introducing vulnerable components (ASVS V15.2.1 timeframes) | Can block builds; needs triage and exceptions |
| Licence policy | Legal exposure | Not a security control, but uses the same enforcement point |
| Immutable cached artifacts; disallow re-upload of an existing version to local repositories | Silent replacement of a version already in use | Mistaken releases must be fixed with a new version |
| Restrict install scripts by policy for npm (or install with scripts disabled in CI) | Code execution at install time | Some packages need build steps; see the npm install-time guide |
The quarantine period is the control most organisations lack. The Shai-Hulud npm Worm 2025 used stolen npm tokens to republish up to 100 of each victim's packages, with new malicious versions appearing within hours; a proxy that served nothing younger than a day or two would have kept most builds on known-good versions while the registry removed them. See the npm Install-Time Security Guide and the Dependency Update Automation Guide for aligning bots with quarantine.
Handling exceptions without breaking the model
- Urgent security fixes: define who may release a quarantined version early (for example the security on-call plus the owning team lead), require a ticket that records the advisory, and expire the exception automatically once the quarantine period would have ended anyway.
- New public dependencies: route requests for packages not yet used anywhere in the organisation through a lightweight review: maintainer activity, download history, install scripts and requested permissions. A first-time fetch of an unknown name is the moment a typosquat is most likely to enter.
- Internal names that collide with public ones: rename the internal package into a scope or prefix you own rather than adding a one-off exclusion, because one-off rules are forgotten when repositories are rebuilt.
- Record every exception with an owner and an expiry date, and report open exceptions monthly alongside the metrics below.
Access control for the repository itself
- Readers: developers and CI jobs need read access to virtual repositories only. Use short-lived tokens issued to the CI identity (OIDC federation where the product supports it) rather than a shared static password.
- Deployers: only release pipelines publish to local repositories, with a deploy identity separate from the read identity, scoped to the repositories that pipeline owns.
- Administrators: changes to remotes, virtual membership order, include and exclude patterns and curation policies are security changes; require named administrators with phishing-resistant MFA, change review and audit logging. See the Privileged Access Management Guide.
- Tokens: repository tokens in
.npmrc,pip.conf,settings.xmland CI variables are frequent secret-scanning findings. Token types, expiry and revocation for Artifactory are covered in the Artifactory Tokens Security Guide. - Anonymous access: disable it on every repository that holds internal packages; an anonymously readable internal repository publishes your namespace list to attackers.
Rolling it out
- Inventory names and sources: list every internal package name and prefix per ecosystem, and every registry URL in lockfiles, client configs and CI scripts. NuGet's
dotnet list package --include-transitiveand lockfile parsing for npm and pip give the starting set. - Claim public namespaces: npm organisation scopes, NuGet ID prefix reservations, Maven Central namespaces and placeholder projects where an ecosystem has no namespaces.
- Build the virtual repositories per ecosystem with local members first and exclude patterns on remotes for internal names.
- Switch clients in waves: pilot one team per ecosystem, then roll out the managed configuration. Keep the old configuration available for rollback for one sprint, but do not re-open direct egress.
- Block egress from CI to public registries once the pilot is stable; monitor for failures and fix configurations rather than adding exceptions.
- Turn on curation in report mode, review what quarantine and vulnerability rules would have blocked for two to four weeks, then enforce, starting with quarantine and malicious-package blocking.
Decision guidance: small organisations with one or two ecosystems may use a hosted artifact service and client-side namespace binding (scopes, source mapping, exclusive content) before running their own repository manager. Large organisations need a managed repository manager per region with high availability, because it becomes a build-critical dependency; design for its outage, since a failed proxy can stop every pipeline. Regulated organisations should also retain fetched artifacts and access logs long enough to answer "which builds consumed version X" during an incident.
Detection and measurement
| Signal | Source | Meaning |
|---|---|---|
| Requests for internal names on public remotes | Repository manager request logs | Misconfigured client or confusion attempt |
| Direct connections from build networks to public registries | Egress firewall, proxy or DNS logs | Bypass of the repository manager |
| Quarantine and malicious-package blocks | Curation policy logs | Policy working; spikes indicate a campaign |
| Attempts to overwrite an existing version | Local repository audit log | Mistake or tampering |
| Changes to remotes, routing, patterns or permissions | Admin audit log | Possible weakening of controls |
| Downloads of a version later confirmed malicious | Download history joined to build metadata | Scope of an incident; see the playbook |
Test the controls, not just the configuration: publish a harmless canary package under one of your internal names to a test registry that a remote repository proxies, and confirm the build fails or resolves internally; request an internal name through each public remote and expect a block; run a build with a deliberately wrong lockfile hash and expect failure; and attempt a direct download from a CI runner to a public registry and expect the egress block. Repeat these checks after every repository manager upgrade or migration, when routing rules are most often lost.
Metrics:
- Percentage of builds resolving exclusively through the repository manager (target 100, measured by egress logs).
- Number of internal names without a public-side claim or exclude rule (target zero).
- Packages served that were younger than the quarantine period, by exception.
- Mean time from a malicious package advisory to block in the repository manager.
- Percentage of repositories with anonymous access disabled and separate deploy identities.
Framework mapping
| Control | NIST SP 800-53 Rev. 5 | NIST CSF 2.0 | OWASP ASVS 5.0 | ATT&CK |
|---|---|---|---|---|
| Single curated repository, namespace binding | SR-3, CM-7 | GV.SC-05, PR.PS-01 | V15.2.4 | T1195.001 |
| Integrity hashes and lockfiles | SR-11, SI-7 | PR.PS-02 | V15.2.4 | T1195.001 |
| Inventory of fetched components | SR-4 | ID.AM-02 | V15.1.2 | n/a |
| Vulnerability and age-based curation | SR-3 | PR.PS-02 | V15.2.1 | T1195.001 |
| Separated read, deploy and admin identities | AC-6, IA-5 | PR.PS-01 | n/a | T1552.001 |
Common failure patterns
- The proxy exists but clients bypass it: a developer's global
.npmrcor a Dockerfilepip installgoes straight to the public registry because egress is open. - Virtual repository with no exclude patterns: internal names fall through to the public remote whenever the internal repository lacks the requested version.
--extra-index-urlin requirements files or CI scripts, often copied from vendor installation instructions.- Caching everything instantly: the proxy fetches a hijacked version minutes after publication and serves it to every build.
- One shared admin token: the same credential reads, deploys and administers the repository and sits in many CI projects.
- Older tooling ignoring policy: agents on NuGet versions before 6.0 silently ignore package source mapping.
Lessons from incidents
The PyTorch torchtriton Supply Chain Attack 2022 is the textbook case. PyTorch served torchtriton from its own nightly index, an attacker registered the same name on PyPI, and pip installed the public version by default; nightly users between 25 and 30 December 2022 ran code that collected environment variables and SSH keys. PyTorch renamed the dependency and registered a placeholder on PyPI. A single curated index with the internal name excluded from the public remote, or a claimed public name from the start, would have prevented it. When prevention fails, follow the Malicious Open Source Package Response Playbook.
Practitioner checklist
- Run one repository manager endpoint per ecosystem and point every client at it with managed configuration.
- Block direct egress from build networks to public registries.
- Bind internal names to internal repositories: npm scopes, NuGet source mapping, Gradle exclusive content, exclude patterns on remotes.
- Claim your namespaces and prefixes on npm, nuget.org and Maven Central.
- Remove every
--extra-index-urland use a single pip index. - Commit lockfiles with integrity hashes and enforce them in CI.
- Quarantine newly published versions and block known-malicious packages, with a fast-track for urgent fixes.
- Make cached and released artifacts immutable.
- Separate read, deploy and admin identities; disable anonymous access; use short-lived CI tokens.
- Log and alert on internal names requested publicly, egress bypass and admin policy changes.
Standards and references
- Microsoft Learn: Best practices for a secure software supply chain (NuGet)
- Microsoft Learn: Package Source Mapping
- pip documentation: pip install
- npm Docs: scope
- JFrog: Artifactory virtual repositories
- Sonatype: Nexus Repository routing rules
- OWASP: Application Security Verification Standard 5.0
- NIST: SP 800-53 Rev. 5
Related NHI Mgmt Group resources: Software Supply Chain Security Guide · Malicious Package and Dependency Confusion Guide · Artifactory Tokens Security Guide · Container Registry Security Guide