Git, CI/CD & GitOpsLXXI · CI/CD Threat ModellingAttack
The secret-leak and registry-compromise attack — credential-exfiltration scenarios
What you'll learn
- Describe the secret-leak attack: a credential is exposed through workflow logs, build artefacts, or registry metadata and is harvested by the attacker
- Describe the registry-compromise attack: the artifact registry is writable by the attacker, who substitutes a malicious image under the team's name
- Identify the detection signals: a secret in a workflow log, an unfamiliar push to the registry, an artifact with a digest that does not match the build's provenance
- Apply controls — secret scanning, log redaction, registry immutability, digest pinning — that close the secret-leak and registry-compromise surfaces
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
The CI pipeline produces output. The output goes to workflow logs, build artefacts, container registries, artifact stores, deployment records, and notification endpoints. Every output is a potential exfiltration path for a credential the pipeline holds. The secret-leak attack is the scenario where a credential the pipeline uses — a cloud access key, a registry token, a signing key, a database password — is exposed through one of those output paths. The attacker who reads the output reads the team’s secrets. The registry-compromise attack is the related scenario where the artifact registry itself is writable by the attacker, who substitutes a malicious artifact under the team’s name.
Where secrets leak
The CI pipeline exposes secrets through six paths. Each path is a place the pipeline writes data; each path is readable by an attacker who knows where to look.
- Workflow logs. Every
echo $SECRET, everyprintenv, every error message that includes an interpolated variable, every stack trace that dumps the environment, everyset -xin a debug script — every one writes the secret to the workflow log. The log is readable by anyone with read access to the repository; the log is indexed by the platform’s search. - Build artefacts. Every file the build uploads as a
build artefact — a
.env, aconfig.json, akubeconfig, aterraform.tfstate, a~/.aws/credentialsthat was mistakenly copied into the artefact directory — contains the secret. The artefact is downloadable by anyone with read access to the repository. - Container image layers. Every environment variable
set with
ENV, every file copied withCOPY, every secret baked into a layer withRUN echo $SECRET > /etc/secret— every one is in the image’s filesystem. The image is pullable by anyone with read access to the registry; the layer is in the manifest. - Container image metadata. Every label, every
annotation, every
LABEL maintainer="..."that includes a credential, everyENVthat was supposed to be a runtime variable but is recorded in the manifest — every one is in the image’s metadata. The metadata is queryable withdocker inspect. - SBOM and provenance attestations. Every attestation that names the build environment, the build steps, and the build’s environment variables — the attestation is signed but not redacted; the attestation’s payload is the build’s metadata.
- Notification endpoints. Every Slack message, every email, every webhook payload that includes a URL with a credential, every deployment record that logs the deploy token — every one is in a third-party system the team may not audit.
flowchart LR
A["Secret in pipeline"] --> B["Workflow log"]
A --> C["Build artefact"]
A --> D["Image layer"]
A --> E["Image metadata"]
A --> F["SBOM / provenance"]
A --> G["Notification endpoint"]
B -. "searchable" .-> X["Attacker reads"]
C -. "downloadable" .-> X
D -. "in manifest" .-> X
E -. "inspectable" .-> X
F -. "signed payload" .-> X
G -. "third-party log" .-> X
The six paths converge on the same outcome: a secret the team intended to use at build time is readable by an attacker after the build is over.
The secret-leak attack in three steps
The secret-leak attack has three steps. Each step exploits a different output path.
- Trigger the secret’s emission. The attacker triggers a workflow that uses a secret — a pull_request workflow that runs the team’s test suite with a deploy credential in scope, a scheduled workflow that prints the environment, a debug workflow that was left enabled. The workflow runs; the secret is in scope.
- Capture the secret in the output. The secret is
captured by the workflow’s output — an
echoin a debug step, a stack trace in a failed test, an environment dump in a post-mortem step, a file copy that lands the credential in an artefact. The output goes to the workflow log, the artefact store, or the image registry. - Harvest the secret from the output. The attacker reads the workflow log (with read access to the repository), downloads the artefact, or pulls the image and inspects its layers. The attacker has the secret.
flowchart LR
A["Trigger workflow"] --> B["Secret in scope"]
B --> C["Secret in output"]
C --> D["Workflow log"]
C --> E["Artefact"]
C --> F["Image layer"]
D --> G["Attacker harvests"]
E --> G
F --> G
G --> H["Credential reused"]
The attack’s signature is that the secret is in the output path the team has not audited. The team has audited the secret store; the team has not audited the artefact store, the image registry, or the notification endpoints.
The registry-compromise attack
The registry-compromise attack is the related scenario where the artifact registry itself is the target. The attacker obtains write access to the registry — through a stolen credential, a misconfigured IAM policy, a vulnerable registry API, or a compromised CI runner — and substitutes a malicious artifact under the team’s name.
The attack has three steps:
- Obtain write access. The attacker compromises a
credential with
pushscope to the registry — a long-lived PAT, an IAM role with overly broad permissions, a registry API token leaked through a separate path. - Push a malicious artifact. The attacker pushes a
new image under the team’s repository, with a tag
that the deployment pipeline resolves. The tag is
latest; the tag is a major version; the tag is a digest the deployment pipeline trusts. The deployment pipeline pulls the malicious artifact. - Deploy the malicious artifact. The deployment pipeline verifies the signature (if any) against the team’s key — but the registry substitution happened before the deployment step, and the malicious artifact was signed by the attacker using a key the attacker controls.
flowchart LR
A["Obtain push credential"] --> B["Push malicious image"]
B --> C["Tag = latest / version"]
C --> D["Deployment pulls malicious"]
D --> E["Production runs backdoor"]
The registry-compromise attack exploits a different trust delegation than the secret-leak attack: the team trusts the registry to serve what the team pushed. A registry the attacker can write to is a registry the attacker controls.
Detection signals
The signals that distinguish a secret-leak or registry-compromise attack from a clean build are not in the build log; they are in the output paths.
- A secret in a workflow log. The platform’s native secret scanner flags a token, a key, or a credential pattern in the log. The flag is the team’s first signal that the secret has been published.
- A secret in a build artefact.
gitleaks detect --source . --verboseruns against the build directory; the scanner flags every secret pattern it finds. The flag is in the scanner’s output:
gitleaks detect --source . --verbose
- A secret in an image layer.
trivy fs --security-checks vuln,secret .runs against the build directory and the resulting image; the scanner flags every secret pattern in the filesystem and in the image layers:
trivy fs --security-checks vuln,secret .
- A push to the registry from an unfamiliar IP. The registry’s audit log records every push; the team alerts on pushes from IPs not on the allowlist.
- An artifact with a digest that does not match the build’s provenance. The verifier compares the artifact’s digest against the digest named in the build’s attestation; a mismatch is a signal that the artifact was substituted after the build.
Controls that close the secret-leak and registry-compromise surfaces
The controls fall into four groups.
- Secret scanning at every output path.
gitleakson the repository,trivy fs --security-checks secreton the build directory, platform-native secret scanning on the workflow log, pre-commit hooks that block the secret before it reaches the pipeline. A secret that does not reach the pipeline is a secret that does not leak. - Log redaction and secret masking. The CI platform masks secret values in the workflow log by default; the team verifies the masking works and extends it to any custom log output. The masking is the second line of defence after secret scanning.
- Registry immutability. The registry is configured to reject overwrites of existing tags; once an image is pushed under a tag, the tag cannot be moved. The registry’s audit log records every push and rejects every push that would overwrite.
- Digest pinning in deployment. The deployment pipeline references the artifact by digest, not by tag. A substituted artifact under the same tag does not match the digest the deployment expects; the deployment refuses the substitution.
The four groups together close the two attacks. Secret scanning and log redaction close the secret-leak attack. Registry immutability and digest pinning close the registry-compromise attack.
Production discipline
- Scan for secrets at every output path. Repository,
build directory, workflow log, image layer, SBOM,
notification endpoint. The
gitleaksandtrivy fs --security-checks secretinvocations run on every build. - Mask secrets in workflow logs. The platform’s default masking is enabled; the team verifies the masking works for every secret the pipeline uses.
- Make the registry immutable. Existing tags cannot be overwritten; the registry’s audit log records every push attempt.
- Pin deployments by digest, not by tag. The
deployment pipeline references
image@sha256:...; a substitution under the same tag is rejected at verification.
Cross-course references
- Git, CI/CD & GitOps — Part LXXI-05 (Runner Compromise) is the upstream attack this lesson assumes.
- Git, CI/CD & GitOps — Part LXIX (Signing and Verification) covers the digest-pinning pattern.
- Git, CI/CD & GitOps — Part LXX (Provenance) covers the attestation model that catches a digest mismatch.
- Linux for Production Sysadmins — Part XII (RepoSecurity) covers the OS-level analogue of registry trust.
Quiz
Knowledge check · 4 questions
Q1. Why is a workflow log treated as a published artefact rather than a private debugging surface?
Q2. A team that pins deployments by tag (e.g., `image:latest`) is a team that has closed the registry-compromise attack.
Q3. Name the four controls that, together, close the secret-leak and registry-compromise attacks.
Q4. Diagnose the secret-leak attack, identify the output path, and recommend the controls that close the gap.
Team T's deployment workflow runs on a self-hosted runner. The workflow includes a debug step (`run: env | grep -i key`) that was left in from a previous incident investigation. The env dump includes the AWS access key the workflow uses to deploy. The workflow log is retained by the platform and indexed by its search. Three months later, the AWS access key is observed being used from an IP address in a country the team does not operate in. The audit trail shows the access key was first used from the unfamiliar IP four days after the env-dump step ran.
Passing score: 75%. Answers are checked in this browser.