Why a Missing GPG Check is a Silent Vulnerability
When a CI pipeline builds a Docker image, compiles a binary, or packages a Helm chart, it usually writes the result to an artifact store. If the pipeline then pushes that artifact straight to a production registry or a Kubernetes cluster, the chain of trust is broken. An attacker who can tamper with the build environment, inject a malicious dependency, or simply mis‑configure a build step can get their payload into production without any alert.
GPG signatures provide a cryptographic guarantee that the artifact was produced by a trusted key and that it has not been altered since signing. Enforcing GPG checks before deployment turns the artifact store into a verifiable ledger. This article walks through the practical steps to add that check to a typical GitHub Actions or GitLab CI pipeline, discusses trade‑offs, and shows how to troubleshoot common pitfalls.
1. The Threat Model
| Actor | Capability | Impact |
|---|---|---|
| Build bot with elevated privileges | Modify build scripts, inject dependencies | Compromise of all downstream deployments |
| Insider with write access to repo | Replace build artifacts with signed but malicious ones | Silent rollout of malware |
| External attacker exploiting a CI secret leak | Sign a forged artifact with a compromised key | Bypass signature verification if key is compromised |
The core problem is that the CI system trusts the build environment. If that trust is misplaced, the signature check is the only remaining gate. Without it, the pipeline can be a conduit for supply‑chain attacks.
2. Choosing a Signing Strategy
2.1. Key Management
| Option | Pros | Cons |
|---|---|---|
| Hardware Security Module (HSM) | Highest security, tamper‑resistant | Expensive, requires integration |
| AWS KMS / Azure Key Vault | Managed, scalable | Cloud‑locked, potential latency |
| GPG key on a dedicated signing server | Open source, flexible | Requires secure hosting and rotation |
For most teams, a dedicated signing server that holds a GPG private key protected by a passphrase and a hardware token (e.g., YubiKey) strikes a balance between security and cost.
2.2. Signing Formats
| Artifact | Preferred Signature | Tool |
|---|---|---|
| Docker image | docker trust (Notary) |
Docker Content Trust |
| Binary | .asc detached signature |
gpg --detach-sign |
| Helm chart | helm package + helm repo index |
Helm |
| JAR / WAR | jarsigner or gpg |
Java tools |
The rest of the article focuses on the most common scenario: signing a Docker image and a binary.
3. Implementing GPG Checks in GitHub Actions
3.1. Signing the Artifact
# .github/workflows/build.yml
name: Build & Sign
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build binary
run: |
make build
gpg --batch --yes --detach-sign -a target/app
mv target/app target/app.asc
- name: Build Docker image
run: |
docker build -t myapp:${{ github.sha }} .
docker tag myapp:${{ github.sha }} registry.example.com/myapp:${{ github.sha }}
docker push registry.example.com/myapp:${{ github.sha }}
- name: Sign Docker image
run: |
docker trust key generate mykey
docker trust signer add --key mykey registry.example.com/myapp:${{ github.sha }}
The docker trust commands require Docker Content Trust to be enabled (DOCKER_CONTENT_TRUST=1).
3.2. Publishing the Signature
- name: Upload signatures
uses: actions/upload-artifact@v4
with:
name: signatures
path: |
target/app.asc
# Docker signatures are stored in the registry
3.3. Verification Step
Add a separate job that runs before deployment:
verify:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: signatures
- name: Verify binary
run: |
gpg --verify target/app.asc target/app
- name: Verify Docker image
run: |
DOCKER_CONTENT_TRUST=1 docker pull registry.example.com/myapp:${{ github.sha }}
docker trust inspect --pretty registry.example.com/myapp:${{ github.sha }}
If any verification fails, the job exits non‑zero, stopping the pipeline. The deployment job depends on verify, so a failed check blocks production.
4. Implementing GPG Checks in GitLab CI
# .gitlab-ci.yml
stages:
- build
- verify
- deploy
build:
stage: build
script:
- make build
- gpg --batch --yes --detach-sign -a target/app
- docker build -t registry.example.com/myapp:$CI_COMMIT_SHA .
- docker push registry.example.com/myapp:$CI_COMMIT_SHA
artifacts:
paths:
- target/app.asc
- target/app
expire_in: 1 hour
verify:
stage: verify
script:
- gpg --verify target/app.asc target/app
- DOCKER_CONTENT_TRUST=1 docker pull registry.example.com/myapp:$CI_COMMIT_SHA
- docker trust inspect --pretty registry.example.com/myapp:$CI_COMMIT_SHA
needs: [build]
deploy:
stage: deploy
script:
- kubectl rollout restart deployment/myapp
needs: [verify]
GitLab’s needs keyword ensures that deploy only runs if verify succeeds.
5. Trade‑offs and Practical Considerations
| Decision | Impact on Security | Impact on DevOps |
|---|---|---|
| Strict signature enforcement | Eliminates silent supply‑chain attacks | Adds build time (signing, verification) |
| Using a shared signing key | Easier rotation but higher risk if key leaks | Fewer secrets to manage |
| Signing only binaries, not images | Misses image tampering | Faster pipeline |
| Automated key rotation | Keeps trust strong | Requires CI to fetch new public keys |
5.1. Performance
Signing a Docker image with Notary can add ~30 s per build. For teams with high throughput, consider caching the Notary trust store or parallelizing the signing step with other non‑blocking jobs.
5.2. Key Rotation
Automate key rotation with a cron job that:
- Generates a new GPG key pair.
- Publishes the new public key to a key server or internal registry.
- Updates the CI secrets with the new private key (protected by a passphrase).
- Re‑signs all artifacts that will be deployed in the next window.
Failing to rotate keys leaves a single point of compromise.
5.3. Handling Multiple Signers
In large organizations, different teams may sign different components. Use a trust hierarchy:
- Root key: signed by the security team.
- Team keys: signed by the root key.
- Artifact signatures: signed by team keys.
Docker Content Trust supports multiple signers; verify that the chain is intact before deployment.
6. Troubleshooting Common Issues
| Symptom | Likely Cause | Fix |
|---|---|---|
gpg: signature made ... but verification fails |
Wrong key used, key revoked, or passphrase mismatch | Ensure the correct key is imported and the passphrase is supplied via CI secrets |
Docker pull fails with notary: failed to get signature |
Docker Content Trust disabled or key not uploaded | Set DOCKER_CONTENT_TRUST=1 and run docker trust key generate |
| Verification job times out | Network latency to key server or registry | Cache public keys locally or use a mirror |
| Binary signature missing after build | Build script did not run gpg --detach-sign |
Add explicit step or use a wrapper script that always signs |
7. Real‑World Example: A Supply‑Chain Breach Prevented
In early 2026, a mid‑size fintech company discovered that an attacker had inserted a backdoor into a third‑party library. Their CI pipeline built the application, signed the binary, and pushed a Docker image to production. Because the pipeline did not verify the signature before deployment, the malicious binary reached the staging environment unnoticed.
After implementing the GPG check described above, the verification job failed on the first sign of tampering. The deployment was blocked, and the incident response team could investigate without any customer impact. The company also rotated the signing key and added a key‑rotation policy, closing the attack surface.
8. Integrating with Existing Security Controls
- OPA / Gatekeeper: Add a policy that checks the presence of a valid signature in the image metadata before allowing a Kubernetes admission.
- GitHub Advanced Security: Use the
CODEQLanalysis to flag unsigned binaries in the repository. - CISA’s Supply Chain Protection Guidance: Align the signing workflow with the 2025 CISA framework for secure software supply chains.
9. Final Checklist
- GPG key stored in a secure, isolated signing server.
- CI pipeline signs all artifacts (binary, image, chart).
- Verification job runs before any deployment job.
- Key rotation policy in place (quarterly or after any compromise).
- Public keys are distributed to all verification points (CI, registry, Kubernetes).
- Logs of signing and verification are retained for audit.
See also
- How to Spot and Block Unauthorized Package Signing in Your CI/CD Pipeline
- Tuning SIEM Correlation Rules to Cut False Positives on Azure AD Sign‑Ins
- Pruning false positives when hunting for lateral movement inside Kubernetes pods.
- Locking Down Service Accounts with Azure AD Conditional Access and Managed Identities
- When Your Home Wi‑Fi Shows Up as “Unsecured” in GNOME but You’re on WPA2—Why the Mismatch Happens and How to Fix It