Supply Chain Hardening: Enforcing Reproducible Builds on a Payment Platform
Supply Chain Hardening: Enforcing Reproducible Builds on a Payment Platform
Release: v1.0.58 · Security ticket: SEC-26 (High)
Calmony Pay processes card payments and settles funds to a regulated UK bank account. A compromised npm dependency in this environment is not an abstract risk — it could silently exfiltrate payment credentials or tamper with transaction processing. This post explains what changed in v1.0.58, why it matters, and what you need to do.
The Problem
Prior to this release, the repository had no committed package-lock.json, and CI ran npm install on every build. This created four concrete security and reliability gaps:
1. Non-Deterministic Builds
npm install resolves the latest compatible version of every transitive dependency at the time it runs. A package that was safe on Monday can include a malicious patch release by Friday — and your CI, staging, and production environments may each install a different version without any indication that anything changed.
2. Supply Chain Attack Surface
Without a lock file pinning every package in the dependency tree, a hijacked or typosquatted package on the npm registry can silently enter the build between runs. On a payment platform, this is a critical exposure.
3. Dependabot Blocked
GitHub Dependabot cannot open version-bump PRs without a lock file to update. Automated dependency maintenance was effectively disabled.
4. Misleading Audit Results
npm audit run against an unpinned tree may flag — or miss — different vulnerabilities than what is actually running in production.
What Changed
New CI Gate: lock-file-check
A dedicated parallel CI job was added to .github/workflows/ci.yml. It runs alongside the existing build job on every PR and fails with a detailed, actionable message if package-lock.json is not committed:
WHY THIS MATTERS:
1. Non-deterministic builds — npm install may resolve different versions
across developer machines, CI, and production.
2. Supply chain risk — without pinned versions, a hijacked package on
the npm registry can silently compromise the build.
3. Dependabot cannot open version-bump PRs without a lock file.
4. npm audit results may differ from the actual production dependency tree.
FIX:
Run 'npm install' locally, then commit the generated package-lock.json:
npm install
git add package-lock.json
git commit -m 'chore: add package-lock.json for reproducible builds'
git push
This gate blocks merges to main from any branch that omits the lock file.
npm ci for Reproducible Installs
The build job's install step was updated to use npm ci whenever package-lock.json is present. Unlike npm install, npm ci:
- Installs exactly the versions recorded in the lock file — no version resolution
- Fails loudly if the lock file is out of sync with
package.json - Never writes back to the lock file, making it safe to cache
- Is significantly faster in CI because dependency resolution is skipped
A fallback to npm install is retained only for the initial bootstrap run where no lock file exists yet, and it prints a clear warning prompting the developer to commit the generated file.
New .npmrc Configuration
A new .npmrc file was added to the repository root with three settings:
# Always generate and maintain a package-lock.json
package-lock=true
# Enforce SSL certificate validation for all registry requests
strict-ssl=true
# Pin to the public registry — prevents dependency confusion attacks
registry=https://registry.npmjs.org/
package-lock=true prevents any developer or script from accidentally bypassing lock file generation with --no-package-lock.
strict-ssl=true ensures all communication with the npm registry is over verified TLS. This prevents registry responses from being tampered with in transit.
registry=https://registry.npmjs.org/ explicitly pins the registry. This mitigates dependency confusion attacks — a class of supply chain attack where a private package name is registered on the public registry and served instead of the intended internal package.
One-Time Bootstrap Step Required
Because package-lock.json does not yet exist in the repository, the lock-file-check job currently runs with continue-on-error: true. To fully activate this protection, a developer must run the following once:
git checkout fix/sec-26-lock-file-npm-ci # or main, after merge
npm install # generates package-lock.json
git add package-lock.json
git commit -m "chore: add package-lock.json for reproducible builds (SEC-26)"
git push
Once package-lock.json is committed:
- All CI runs will use
npm ciexclusively continue-on-error: truecan be removed fromlock-file-checkto make the gate strictly enforcing- Dependabot will activate and begin opening version-bump PRs automatically
npm auditresults will reflect the actual production dependency tree
Summary
| Before v1.0.58 | After v1.0.58 |
|---|---|
npm install (non-deterministic) | npm ci (exact-version, reproducible) |
| No lock file enforced | lock-file-check CI gate blocks merges without lock file |
No .npmrc | .npmrc enforces package-lock=true, strict-ssl=true, pinned registry |
| Dependabot inactive | Dependabot activates once lock file is committed |
| Audit results may differ from production | Audit reflects the actual pinned dependency tree |
This change closes SEC-26 and significantly reduces the supply chain attack surface of Calmony Pay's build pipeline.