How to Spot and Block Unauthorized Package Signing in Your CI/CD Pipeline

The threat of rogue package signatures

When a CI/CD pipeline pulls a dependency, it assumes the artifact came from a trusted source. In 2025, several high‑profile supply‑chain incidents showed that attackers can replace a legitimate package with a malicious one that carries a valid signature. The signature itself is not the problem; it is the trust anchor that the pipeline uses to verify it. If that anchor is compromised or misconfigured, the entire build chain can be subverted.

The goal of this post is to give you concrete steps to detect and block unauthorized package signing in your pipeline. The techniques apply to npm, Maven, PyPI, Cargo, and other ecosystems that support cryptographic signatures. They also cover the tooling and policy changes that make the defense robust without breaking developer productivity.


1. Understand the trust chain

Layer What it protects Typical misconfigurations
Key material The private key that signs packages Keys stored on a developer’s laptop, shared via email, or in a public repo
Key distribution The public key that the CI server uses Public keys hard‑coded in build scripts, or fetched from an untrusted URL
Signature verification The runtime check that a package matches the key Verification disabled, or only a subset of files verified
Artifact source The registry or mirror that hosts the package Unauthenticated mirrors, or a registry that allows unauthenticated uploads

A breach at any layer can let an attacker inject a malicious package that passes verification. The most common vector is a compromised key that the CI server trusts.


2. Harden key management

2.1 Use hardware‑backed keys

Hardware Security Modules (HSMs) or cloud KMS services (AWS KMS, Azure Key Vault, Google Cloud KMS) keep the private key out of the build environment. The CI server only sends a signed challenge to the HSM, which returns the signature. This eliminates the risk of key theft from the build node.

# Example: AWS KMS sign command
aws kms sign \
  --key-id alias/ci-signing-key \
  --message fileb://package.tgz \
  --message-type RAW \
  --hash-algorithm SHA256 \
  --output text --query 'Signature' > package.sig

2.2 Rotate keys regularly

Set an automated rotation policy. When a key is rotated, update the public key in all CI configurations and re‑sign any packages that are still in the release pipeline. A rotation schedule of 90 days is a good baseline for most organizations.

2.3 Store public keys in a single source of truth

Instead of hard‑coding keys in multiple scripts, maintain a central repository (e.g., a GitHub repo named ci-signing-keys). Use a CI job that pulls the latest public key before each build. This reduces the window where a stale key could be used.


3. Verify signatures at build time

3.1 Enable strict verification flags

Most package managers expose a flag to enforce signature checks. For example:

# npm with package-lock verification
npm ci --audit --omit=dev

# Maven with GPG verification
mvn -Dgpg.skip=false clean install

If the flag is missing, the build will silently accept unsigned or incorrectly signed packages.

3.2 Verify the entire artifact

Some ecosystems only verify the checksum of the package tarball, not the internal files. Configure the CI to validate the hash of every file in the archive. For npm, this is controlled by package-lock.json integrity hashes. For Maven, the pom.xml should include the gpg:verify goal.

3.3 Cross‑check with the registry’s signature

When a registry supports signed packages (e.g., npm’s npm publish --access=public --sign), the registry itself can provide a signature that the CI can compare against the one stored in the artifact. This double‑signing adds an extra layer of assurance.


4. Detect unauthorized signing

4.1 Build a signature audit log

Configure your CI to emit a JSON record for every signed artifact:

{
  "artifact": "myapp-1.2.3.tgz",
  "signer": "github.com/secure-team/ci-key",
  "timestamp": "2026-11-01T12:34:56Z",
  "signature": "base64=="
}

Store the log in a tamper‑evident store (e.g., immutable S3 bucket with versioning). Periodically run a script that compares the log against the public key repository. Any mismatch should trigger an alert.

4.2 Use a policy engine

Open Policy Agent (OPA) can enforce that only artifacts signed by approved keys reach production. A simple Rego rule:

package ci

allow {
  input.signer == data.approved_keys[_]
}

Integrate OPA into the CI pipeline as a gate that rejects builds with unapproved signatures.

4.3 Monitor for key compromise

Set up a key‑compromise detection service that watches the public key repository for unauthorized changes. GitHub’s CODEOWNERS and branch protection rules can enforce that only a small set of maintainers can push to the key repo. Any push that bypasses these rules should trigger a security incident.


5. Block malicious packages before they reach the registry

5.1 Use a private registry proxy

Instead of publishing directly to the public registry, route all uploads through a private proxy (e.g., Verdaccio for npm, Nexus Repository for Maven). The proxy can enforce signature validation before forwarding the artifact to the public registry.

# Verdaccio config snippet
publish:
  allow: signed
  signature:
    key: /etc/verdaccio/keys/ci-key.pub

5.2 Enforce signed uploads

Configure the proxy to reject any package that does not contain a valid signature. This stops attackers from uploading a malicious package that masquerades as a legitimate one.

5.3 Mirror public packages

For dependencies that are not signed, maintain a local mirror that is scanned for known malicious binaries. Tools like Trivy or Anchore can be run on the mirror to detect vulnerabilities before the package is consumed.


6. Practical example: npm with GitHub Actions

name: CI
on: push

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up Node
        uses: actions/setup-node@v4
        with:
          node-version: 20
          registry-url: https://registry.npmjs.org/
      - name: Install dependencies
        run: npm ci --audit
      - name: Verify signatures
        run: |
          npm pack
          npm pkg sign
          npm pkg verify          
      - name: Publish
        run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Key points:

  • npm pkg sign and npm pkg verify enforce signature checks.
  • The NODE_AUTH_TOKEN is stored in GitHub Secrets, not in the repo.
  • The npm ci command skips dev dependencies, reducing surface area.

7. Trade‑offs and operational impact

Trade‑off Benefit Cost
HSM integration Removes key theft risk Requires vendor setup, higher latency
Strict signature enforcement Prevents rogue packages Builds may fail if a legitimate package is unsigned
Private registry proxy Central control Extra maintenance, potential single point of failure
Auditing logs Detects anomalies Requires storage and monitoring overhead

The right mix depends on your risk appetite and deployment scale. For high‑value assets (e.g., banking APIs), the overhead of HSMs and strict enforcement is justified. For smaller projects, a well‑configured private registry with automated signature checks may suffice.


8. Troubleshooting common issues

Symptom Likely cause Fix
Build fails with “signature verification failed” Public key mismatch Verify the key in the CI config matches the one used to sign
npm publish succeeds but downstream consumers get unsigned packages Registry proxy not enforcing signatures Enable allow: signed in Verdaccio config
OPA gate always passes Policy rule incorrectly references data.approved_keys Ensure the approved keys file is loaded into OPA’s data store

9. Resources

See also