A build job often needs to compile code, run tests, and create an artifact. It usually does not need permission to publish a new version that other people will install.

That distinction matters because build systems process code and configuration that change frequently. A pull request, dependency update, test helper, build script, or compromised developer account can influence what runs during a build. If every such build also receives a long-lived package registry credential, code that only needed to be tested may inherit authority to release software.

The defensive goal is to separate artifact creation from publishing authority. A less-trusted build can produce a candidate artifact. A more tightly controlled release step decides whether that exact artifact may be published and receives the minimum credential needed to do so. This article explains why that boundary reduces supply-chain risk, what assumptions it depends on, and how to verify that the separation is real.

Treat publishing as a separate security decision

A useful mental model is:

source change
    |
build and test
    |
candidate artifact
    |
release decision
    |
publish credential
    |
package registry

The important boundary sits between the candidate artifact and the release decision.

Building answers:

Can this source produce an artifact that passes our build checks?

Publishing answers a different question:

Should this exact artifact become an authoritative release under our package name?

Those questions have different consequences. A failed or malicious test job can waste compute or expose data available to the build. A malicious publish can distribute unwanted code to every consumer that trusts the package name and version.

If the same job automatically receives both capabilities, the release decision has effectively been delegated to everything that can influence that job.

Start with the threat model

This control is useful when build execution is exposed to inputs that are less trusted than the authority to publish a package. Common examples include pull-request code, dependency installation hooks, generated build scripts, repository automation, and developer-controlled configuration.

Assume an attacker can cause unintended code to execute inside an ordinary build job but has not already compromised the package registry, the trusted release identity, or the system that approves releases.

With a publishing credential present in that build environment, the attacker may be able to use the credential with the same registry permissions as the job. The problem is not that the credential is stored in a particular syntax. The problem is that the build process possesses release authority while executing input that should not have that authority.

Separating the stages changes the consequence of the assumed build compromise. The attacker may still influence the candidate artifact, so the artifact must not be trusted merely because a build produced it. But the compromised build no longer automatically possesses the credential needed to turn its output into an official release.

This boundary does not protect against a compromised release service, a stolen registry administrator account, a malicious person who legitimately controls release approval, or a registry that accepts unauthorized publication. Those require complementary controls.

The smallest useful separation

Consider a pipeline with one all-powerful job:

checkout source
run build
run tests
load registry token
publish package

The token is available in the same execution context as the build scripts. Any code that runs before publication may therefore be able to exercise the token’s authority.

A safer shape separates the responsibilities:

build job:
    checkout source
    run build
    run tests
    produce candidate artifact

release job:
    accept approved candidate artifact
    obtain publish credential
    publish that artifact

This is a teaching model, not a complete production pipeline. The security improvement comes from the authority boundary, not from having two job names.

If both jobs run with the same identity, share writable state, expose the same secrets, and can invoke each other without restriction, the separation is mostly cosmetic. The release stage must actually have a different trust policy.

Keep publish authority out of ordinary build execution

The first practical rule is simple: a job that does not need to publish should not receive a publishing credential.

That includes indirect exposure. Avoid placing a registry token in a global environment shared by every pipeline stage, a workspace file readable by build tools, or a generic secret bundle mounted into all jobs. The relevant question is not “does the build script print the token?” It is “can code running in this job use the token?”

Prefer a release identity that is available only to the trusted publishing step. Where the platform and registry support short-lived or workload-bound credentials, they can reduce the value of credentials copied from one run because the authority expires or is tied to a particular workload context. A long-lived token can still be workable in environments without such support, but it deserves tighter storage, narrower scope, deliberate rotation, and stronger monitoring.

Scope matters as well. A credential that publishes one package should not automatically administer unrelated packages, change organization ownership, delete arbitrary releases, or manage other registry credentials unless the release process genuinely requires those capabilities.

Least privilege reduces the damage of a credential compromise; it does not make exposure harmless.

Make the trusted stage publish the artifact that was reviewed

Credential isolation solves only half of the problem. A release stage also needs confidence about what it is publishing.

Suppose the build creates package.tar, reviewers approve the commit, and the release job later rebuilds from source in a different environment. The resulting bytes may differ because dependencies changed, generated files differ, timestamps are embedded, or the build environment is not reproducible. The approval and the published artifact can then refer to different objects.

A clearer model is to give the candidate artifact a stable identity, commonly a cryptographic digest:

candidate artifact -> digest D
approval records    -> digest D
release receives    -> artifact with digest D

Before publication, the trusted stage computes the artifact’s digest and checks that it matches the approved value. A cryptographic digest is not an approval by itself. It is an identifier that lets different stages agree on the same bytes under the assumptions of the chosen hash function.

This check prevents an accidental or unauthorized substitution between approval and publication when the release process correctly protects the approved digest. It does not prove that the artifact is benign. Review, testing, provenance, and other controls answer different questions.

Control who can cross the release boundary

A separate release job is useful only if untrusted changes cannot freely redefine or trigger it with publishing authority.

The exact mechanism depends on the CI/CD platform, but the policy should answer several questions explicitly:

  • Which repository refs, tags, or release events may request publication?
  • Can code from an untrusted contribution change the release workflow before it runs?
  • Who may approve a release when human approval is required?
  • Can the build choose an arbitrary artifact after approval?
  • Can an ordinary build impersonate the trusted release identity?

For a low-impact internal package, a protected release branch and narrowly scoped release identity may be sufficient. For a widely consumed or high-impact package, stronger separation can be justified: protected workflow definitions, independent approval, short-lived credentials, immutable artifact handoff, and registry-side restrictions.

The point is not to maximize ceremony. It is to make the strength of the release boundary proportional to the impact of an unauthorized release.

Treat artifact handoff as a trust boundary too

The candidate artifact has to move from the build stage to the release stage. That handoff can weaken the design if the build can modify the artifact after it has been approved or if another job can replace it under the same name.

Prefer an artifact store or pipeline mechanism that gives each candidate an immutable or content-addressed identity. If the storage system is mutable, record and verify a digest across the boundary and restrict who can overwrite the stored object.

Also carry enough context to make the release decision meaningful. Depending on the system, that can include the source revision, build identity, artifact digest, package name, and intended version.

Do not rely on a filename such as release.tar as the only identity. Names are convenient labels; they do not establish that the bytes are the same bytes that were tested or approved.

Do not leak authority back through convenience paths

Release boundaries often erode because teams add shortcuts after the main pipeline is working.

A developer may add the publish token to a general CI secret set so a test can query the registry. A debugging feature may let ordinary jobs assume the release identity. A release job may execute repository-provided scripts after obtaining the credential, recreating the original problem inside the trusted stage.

Pay particular attention to code execution after the credential becomes available. If the release job loads a publishing credential and then runs arbitrary package lifecycle scripts, build hooks, or repository commands, those commands may inherit the publishing authority.

Keep the privileged stage small. Ideally it verifies the approved artifact and metadata, obtains the credential as late as practical, performs the publication, records the result, and discards the credential context. Extra transformation or testing belongs before the privilege boundary unless it truly requires publishing authority.

Verify the boundary instead of assuming it

A useful security control should be testable.

First, inspect an ordinary build job and confirm that it cannot read or request the publishing credential. Test this through the platform’s actual identity and secret mechanisms rather than relying only on workflow documentation.

Second, verify that an untrusted contribution cannot modify the trusted release definition and immediately execute the modified version with release authority. Protection may come from platform permissions, protected environments, separate workflow ownership, or another mechanism appropriate to the system.

Third, confirm that the release stage publishes only the artifact identity that passed the release decision. Deliberately provide a candidate with the wrong digest in a safe test environment and verify that publication stops before the registry is changed.

Finally, monitor registry publication events and release-identity use. Detection does not replace prevention, but it helps reveal unexpected releases, unusual credential use, and broken assumptions in the pipeline.

Understand the operational trade-offs

Separating build and release stages adds state and policy. Artifacts need to be retained long enough to reach the release stage. Approval metadata must identify the intended artifact. Credentials need ownership and rotation or federation rules. Failed releases need a retry path that does not silently switch to different bytes.

These costs are real, so the design should match the threat model.

A small private package with a tightly controlled repository and low downstream impact may use a simple protected release job. A package installed across many production systems has a larger blast radius and can justify stronger isolation and independent release controls.

Avoid solving the operational burden by giving every build broad registry authority again. If the release process is too difficult to operate, simplify the boundary while preserving its essential property: untrusted build execution should not automatically possess the authority to publish an official package.

What this control does not solve

Credential isolation does not make the build output trustworthy. A malicious dependency or source change can still produce a harmful candidate artifact. Review, dependency controls, artifact verification, and provenance can reduce those risks.

It also does not stop an authorized publisher from intentionally releasing harmful software, nor does it protect consumers that install an unexpected version without their own dependency controls.

The control addresses a narrower question: if ordinary build execution is compromised, does that compromise automatically include package publishing authority?

A well-designed boundary makes the answer “no” under the system’s stated assumptions.

Conclusion

Package publishing is a high-impact capability, not a routine side effect of compiling and testing code. Treat it as a separate security decision.

Let ordinary builds create candidate artifacts without registry publishing credentials. Hand the chosen artifact across a controlled boundary, identify the exact bytes being approved, and give narrowly scoped publish authority only to the trusted release stage that needs it. Keep that privileged stage small and verify that untrusted jobs cannot reach its credentials or redefine its policy.

The practical mental model is straightforward: builds create candidates; trusted release controls make them authoritative. That separation limits how far a build compromise can travel and makes the software supply-chain trust boundary easier to reason about, test, and operate.