Private Package Registry Migration

Analysis and recommendations for migrating from Gemfury to a more secure and cost-effective private package registry

Executive Summary

Hut 42 currently uses Gemfury as its private package registry, hosting 1,759 package versions across 140 packages for both Python (pip) and JavaScript (npm). Three converging issues — a long-lived unrotated authentication token creating a dependency confusion security vulnerability, an accidental account tier change that increased costs by 25×, and architectural friction with the shift to AWS serverless infrastructure — make migration away from Gemfury the right course of action.

This brief analyses the current setup, expands on the security risk, evaluates candidate replacement solutions with pros and cons, and provides a ranked recommendation with a high-level migration approach and effort estimate.


Current State

PropertyDetail
ProviderGemfury (team account)
In use since2019
Package typesPython (pip / PyPI), JavaScript (npm)
Package count140 packages, 1,759 versions
ConsumersHeroku apps, AWS Elastic Beanstalk, AWS Lambda, GitHub Actions CI builds
PublishersGitHub Actions (automated library build and push)
Auth mechanismLong-lived static token (FURY_AUTH) stored as environment variable
ScopeMixed Adrian Flux and Hut 42 packages on a single account

How packages are currently consumed (Python)

--index-url https://pypi.org/simple/
--extra-index-url https://${FURY_AUTH}:@pypi.fury.io/hutfortytwo/

Problem Statement

1. Unrotated authentication token and dependency confusion risk

The FURY_AUTH token has not been rotated since the account was created. Any leakage of this token — through a repository, a log, an environment dump, or a compromised CI runner — would expose the names of all private packages hosted in the registry.

Once an attacker knows a private package name, they can exploit a well-documented class of vulnerability known as dependency confusion (also called a namespace confusion or substitution attack):

  1. The attacker publishes a package to the public PyPI registry using the same name as a private package, but with a higher version number (e.g. 99999.0.0).
  2. pip, when configured with --extra-index-url, does not treat the primary index as authoritative. It queries all configured indexes simultaneously and selects whichever index returns the highest version number — regardless of which index was listed first.
  3. The malicious public package is installed silently in place of the private one, and its code executes within the application build or runtime.

This attack has been demonstrated at scale against Microsoft, Apple, PayPal, and others (Alex Birsan, 2021). The severity is high: code execution occurs during the build process, potentially compromising secrets, credentials, and deployed application behaviour.

Why flipping the index order did not solve this

Reversing the configuration to make Gemfury the primary index:

--index-url https://${FURY_AUTH}:@pypi.fury.io/hutfortytwo/
--extra-index-url https://pypi.org/simple/

was trialled and reverted. The reason: Gemfury does not operate as a proxy or mirror of PyPI. It only hosts packages explicitly published to it. When pip resolves transitive dependencies of private packages (e.g. django, requests, lxml), it queries the primary index first. Because Gemfury has no knowledge of these public packages, resolution fails. Hosting all public dependencies in Gemfury is not viable given the combinatorial explosion of versions and transitive trees.

The fundamental issue is that Gemfury requires two separate index URLs, and pip’s behaviour with multiple indexes provides no safe ordering guarantee against dependency confusion.

2. Account tier and pricing

The account was inadvertently migrated from an individual plan (flat monthly fee) to a team plan (charged per package). With 140 packages, this has resulted in a 25× cost increase. Downgrading from a team account back to an individual account is not possible on Gemfury. The only way to return to flat-rate pricing on Gemfury would be to create a new individual account and re-publish all 1,759 package versions — which constitutes a non-trivial migration effort in itself, without solving the security problem.

3. Architectural misalignment

Application development strategy is shifting towards serverless AWS using CloudFormation for infrastructure as code. Gemfury sits outside the AWS ecosystem, requiring static credentials and custom integration at every build boundary. A registry within the AWS ecosystem would integrate natively with IAM roles, instance profiles, and GitHub Actions OIDC — eliminating long-lived credentials entirely.


Requirements

RequirementPriority
Eliminate dependency confusion risk for pip installsCritical
Remove long-lived static tokens as the auth mechanismCritical
Separate Adrian Flux and Hut 42 package namespacesHigh
Support both pip (Python) and npm (JavaScript)High
Minimal disruption to existing applications (env var and build config changes acceptable; code changes not)High
Support GitHub Actions for both publishing and consuming packagesHigh
Support Heroku, AWS Lambda, and Elastic Beanstalk build environmentsHigh
Align with AWS serverless / CloudFormation strategyMedium
Cost reduction from current team tier pricingMedium
Package build and publish workflow redesigned for new registryHigh

Scope of Work

This body of work covers Adrian Flux applications only. It is assumed that every application uses at least one private package.

Work AreaDescription
AnalysisDocument current package inventory, consumers, and build pipeline integrations
Solution designProduce detailed design for chosen solution including auth strategy per platform
Package publishing pipelineRedesign and implement GitHub Actions workflows to build and publish packages to new registry
Consumer migrationUpdate all package install configurations across Heroku, Lambda, Beanstalk, and GitHub Actions
PrototypingValidate one representative application per platform type before batch rollout
RolloutMigrate all applications in batches following successful prototypes
DecommissionRetire Gemfury account or transfer to Hut 42 scope only

Migration Approach

Regardless of the solution chosen, the recommended approach is prototype-first, then batched rollout — one representative application per platform type is migrated and validated before the broader rollout begins.

Phase 1: Analysis and design (pre-migration)

  • Audit all 140 packages: identify which are actively used, which are pip vs npm, and which applications consume them
  • Document all consumers by platform type (GitHub Actions, Heroku, Lambda, Beanstalk)
  • Evaluate and select replacement registry solution; produce detailed design including auth strategy per platform
  • Define namespace/account separation for Adrian Flux vs Hut 42 packages

Phase 2: Infrastructure setup

  • Provision new registry infrastructure
  • Configure upstream/proxy connections as required by chosen solution
  • Set up authentication mechanisms for each platform type
  • Define infrastructure as code where applicable

Phase 3: Package publishing pipeline

  • Redesign GitHub Actions publish workflows to build and push to new registry
  • Run parallel publish to both Gemfury and new registry during transition period
  • Validate all 140 packages are available in new registry

Phase 4: Prototype — one of each platform type

  • GitHub Actions: Update one CI workflow to install from new registry
  • Heroku: Update one Heroku app build configuration to use new registry
  • AWS Lambda: Update one Lambda build/deploy pipeline to use new registry
  • Elastic Beanstalk: Update one Beanstalk app to use new registry

Each prototype should be validated in a staging environment before production cutover.

Phase 5: Batched rollout

  • Following successful prototypes, roll out changes to all applications in batches grouped by platform type
  • Maintain Gemfury as fallback until all applications are confirmed migrated

Phase 6: Decommission

  • Stop parallel publishing to Gemfury
  • Remove FURY_AUTH from all environments
  • Either close Gemfury account or transfer remaining Hut 42 packages to a separate Hut 42 account

Risks

Risks of proceeding with migration

RiskLikelihoodImpactMitigation
Build failures during transitionMediumHighPrototype per platform before batch rollout; maintain Gemfury fallback
Heroku build complexity (auth at build time)MediumMediumSpike early in Phase 4; consider custom buildpack if standard approach is insufficient
Auth token refresh adds build step complexityLowLowStandardise a reusable step or composite action in GitHub Actions
Missed consumer applicationsMediumMediumFull audit in Phase 1; monitor for Gemfury traffic after cutover
Registry auth misconfiguration blocking installsLowHighTest in isolated environment; validate each platform type in staging first

Risks of doing nothing (or minimal action)

RiskLikelihoodImpactMitigation
FURY_AUTH token leaked; attacker maps private package namesMediumHighToken rotation is the minimum action; does not resolve the structural issue
Dependency confusion attack installs malicious package in a buildLow (but increasing)CriticalOnly mitigated by moving to a single-index architecture (CodeArtifact)
Continued 25× cost overrun on Gemfury team tierCertainMediumRequires migration regardless of security decision
Gemfury pricing or availability changes againUnknownHighReduces over time as migration progresses

Minimum safe action if full migration cannot be resourced immediately: rotate FURY_AUTH and update it across all environments. This reduces exposure from a compromised token but does not address the dependency confusion vulnerability.


Effort Estimate

A high-level effort estimate is required for each proposed solution. Estimates should be provided per phase and cover the following areas:

PhaseArea to estimate
Phase 1Analysis and design — package audit, consumer mapping, solution design
Phase 2Infrastructure setup — registry provisioning, auth configuration
Phase 3Package publishing pipeline — GitHub Actions redesign and validation
Phase 4Prototyping — one representative application per platform type (GitHub Actions, Heroku, Lambda, Beanstalk)
Phase 5Batched rollout across all applications
Phase 6Decommission and cleanup
TotalOverall estimate including buffer for testing and review

Note that the Heroku integration is likely to be the most uncertain item, as build-time authentication mechanisms vary by solution. This should be identified as a spike in Phase 4 and flagged if it represents a significant risk to the overall estimate.


Status

Current Status: Draft

History:

  • 2026-03-10: Initial brief created
Last modified March 10, 2026: Updates (4eda4cb)