← All posts
September 5, 2026 · DevSecOps

Securing the Software Supply Chain: Artifact Signing & CI/CD Hardening

Supply chain attacks targeting NPM, PyPI, and Docker dependencies are escalating. Learn how to harden CI/CD runners and sign artifacts using Cosign/Sigstore.

Introduction & The Growing Threat

Supply chain attacks have evolved into one of the most dangerous and effective intrusion vectors of the decade. Instead of attacking a well-protected production application directly, adversaries target the tools, open-source packages, or CI/CD pipelines used to build and deploy software.

High-profile incidents such as the XZ Utils backdoor (CVE-2024-3094), npm package poisoning via typosquatting, and the SolarWinds compromise have made it clear that organizations must treat their build infrastructure with the same rigor as production systems.


1. Understanding the Attack Vectors

┌─────────────────────────┐       ┌─────────────────────────┐       ┌─────────────────────────┐
│ NPM / PyPI Dependencies │ ───> │  GitHub Actions Runner  │ ───> │  Docker Registry / Cloud│
└─────────────────────────┘       └─────────────────────────┘       └─────────────────────────┘
       │                                       │                                  │
  [Typosquatting /                  [Excessive Permissions /              [Unsigned artifacts
   Malicious postinstall]            Mutable action tags]                  in production]

Vector 1: Malicious postinstall script in an NPM package

A developer accidentally installs express-corss instead of cors. During npm install in the CI runner, the postinstall hook executes with full runner permissions and exfiltrates environment variables to an attacker-controlled server.

Vector 2: GitHub Actions tag poisoning

Using uses: actions/checkout@v3 means the tag v3 can be silently redirected to a compromised commit by the repository owner.


2. Vulnerable vs. Hardened Pipelines

❌ Insecure GitHub Actions Workflow

name: Continuous Integration & Deploy
on: push

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    permissions: write-all # ❌ CRITICAL: Grants full write access to the repository!

    steps:
      - uses: actions/checkout@v3 # ❌ Mutable tag — can be silently hijacked
      
      - uses: actions/setup-node@v3
        with:
          node-version: '20'

      # ❌ npm install runs arbitrary preinstall/postinstall hook scripts
      - name: Install dependencies
        run: npm install

      - name: Build & Push Docker Image
        run: |
          docker build -t mycompany/app:latest .
          docker push mycompany/app:latest # ❌ Image is not cryptographically signed

✅ Hardened GitHub Actions Workflow (CIS Benchmark Standards)

name: Secure DevSecOps Pipeline
on:
  push:
    branches: [main]

# ✅ Minimal permissions — read-only for source code by default
permissions:
  contents: read
  id-token: write # Required for keyless OIDC signing via Cosign

jobs:
  secure-build:
    runs-on: ubuntu-latest
    steps:
      # ✅ Pin actions to exact SHA commit hashes (not mutable version tags)
      - name: Checkout Code
        uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v4.1.7

      - name: Setup Node.js
        uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
        with:
          node-version: '20'
          cache: 'npm'

      # ✅ --ignore-scripts prevents execution of arbitrary postinstall hooks
      - name: Install Dependencies Securely
        run: npm ci --ignore-scripts

      # ✅ Audit known vulnerabilities before build
      - name: Vulnerability Audit
        run: npm audit --audit-level=high

      - name: Install Cosign
        uses: sigstore/cosign-installer@e1523de7571e31dbe865fd2e8072d7625a5e9077 # v3.5.0

      - name: Login to Registry
        uses: docker/login-action@0d4c9c5ec93ee0f0542876986847549837295d7f
        with:
          registry: myregistry.azurecr.io
          username: ${{ secrets.REGISTRY_USER }}
          password: ${{ secrets.REGISTRY_PASSWORD }}

      - name: Build, Push & Sign Docker Image
        run: |
          IMAGE_TAG="myregistry.azurecr.io/app:${{ github.sha }}"
          docker build -t $IMAGE_TAG .
          docker push $IMAGE_TAG
          # ✅ Keyless OIDC signing — no long-lived signing keys to manage
          cosign sign --yes $IMAGE_TAG

3. Verifying Signatures Before Production Deployment

# Verify the image signature before allowing deployment
cosign verify \
  --certificate-identity-regexp "https://github.com/mycompany/myrepo/.*" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  myregistry.azurecr.io/app:f43a0e5ff2bd294095638e18286ca9a3d1956744

If the image was tampered with or pushed outside of the official GitHub Actions pipeline, this command fails and deployment is blocked.


DevSecOps Supply Chain Checklist

  • [ ] Use npm ci --ignore-scripts in all automated pipelines.
  • [ ] Pin GitHub Actions to exact SHA commit hashes.
  • [ ] Restrict GITHUB_TOKEN permissions to contents: read.
  • [ ] Cryptographically sign Docker images with Cosign / Sigstore.
  • [ ] Generate a SBOM (Software Bill of Materials) on every release using Syft or Trivy.