Understanding the Risk Landscape

Automated deployment pipelines often treat a successful git pull as the final seal of approval. When the pipeline does not verify the authenticity of the incoming commits, an attacker who gains write access to the repository can inject malicious code without triggering any alarms. The danger is amplified in environments where the CI runner executes privileged commands, such as container builds or infrastructure provisioning. This article explains why trusting unsigned commits is a liability and walks through a step‑by‑step implementation of GPG‑based commit verification.

Why Unsigned Commits Pose a Threat

A Git commit contains a snapshot of the source tree, metadata, and optionally a GPG signature. Without a signature, the integrity of the commit relies solely on the repository’s access controls. If an attacker compromises a low‑privilege account, they can push a new commit that appears legitimate. The CI system, unless explicitly configured, will accept the commit, build the artifact, and deploy it downstream. This chain of trust is broken the moment a malicious change bypasses code review.

# Example of a malicious commit pushed without verification
git clone https://example.com/target-repo.git
cd target-repo
# Insert backdoor
echo "rm -rf /" >> src/main.sh
git add src/main.sh
git commit -m "Fix typo in script"
git push origin main

In the snippet above, the attacker does not need a signed commit; the pipeline will simply pull the new main branch and continue. To prevent this, we must enforce a policy that only signed commits are allowed to progress through the pipeline.

Setting Up GPG for Developers

Each developer must generate a GPG key pair and configure Git to sign commits automatically. The following commands illustrate the process on a Unix‑like system.

# Generate a new GPG key (RSA 4096 bits recommended)
gpg --full-generate-key

# List keys to retrieve the key ID
gpg --list-secret-keys --keyid-format LONG

# Configure Git to use the key
git config --global user.signingkey YOUR_KEY_ID

# Enable automatic signing for all commits
git config --global commit.gpgsign true

After these steps, every git commit will be signed. Verify a signed commit with:

# Show commit details including signature status
git log --show-signature -1

The output should contain Good signature from "Your Name". If the signature is missing or invalid, Git will flag it.

Enforcing Signature Verification in CI

The next layer is the CI server. Most CI platforms (GitHub Actions, GitLab CI, Jenkins) allow custom scripts before the build step. We will add a verification stage that aborts the pipeline if any new commit lacks a valid GPG signature.

# verify-signatures.sh – a generic verification script
#!/usr/bin/env bash
set -euo pipefail

# Fetch the range of new commits (GitHub Actions provides $GITHUB_SHA)
BASE=$(git merge-base origin/main HEAD)
NEW_COMMITS=$(git rev-list $BASE..HEAD)

for COMMIT in $NEW_COMMITS; do
  if ! git verify-commit $COMMIT &> /dev/null; then
    echo "❌ Commit $COMMIT is not signed or has an invalid signature."
    exit 1
  fi
done

echo "✅ All new commits are properly signed."

Integrate this script into a GitHub Actions workflow:

# .github/workflows/deploy.yml
name: Secure Deploy

on:
  push:
    branches:
      - main

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Import GPG public keys
        run: |
          # Replace with your team's public key URLs or upload as secrets
          curl -sSL https://example.com/keys/team.pub | gpg --import
      - name: Verify commit signatures
        run: ./verify-signatures.sh

  build-and-deploy:
    needs: verify
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Build container
        run: |
          docker build -t myapp:${{ github.sha }} .
      - name: Push to registry
        run: |
          docker push myregistry.example.com/myapp:${{ github.sha }}
      - name: Deploy
        run: |
          ssh deploy@prod "kubectl set image deployment/myapp myapp=myregistry.example.com/myapp:${{ github.sha }}"

The verify job runs first; if any commit fails verification, the pipeline stops before any build or deployment steps are executed.

Handling Legacy Unsigned Commits

Existing repositories often contain a history of unsigned commits. Rather than rewriting the entire history, you can enforce the rule only on new commits. The verification script above examines only the commits introduced by the current push, leaving the past untouched. If you need to retroactively sign old commits, consider an interactive rebase:

# Interactive rebase to sign recent history
git rebase -i --exec 'git commit --amend -S --no-edit' HEAD~10

Be aware that rewriting history on a shared repository requires coordination with all collaborators, as it forces everyone to reset their local branches.

Security and Best Practices

Never store private GPG keys in the CI environment. Keep only public keys for verification. Use secret management tools (e.g., HashiCorp Vault, GitHub Secrets) to store any required passphrases, and load them only at runtime if you must perform signing inside CI.

Restrict write permissions. Ensure that only a small set of service accounts can push to protected branches. Combine branch protection rules with required status checks (the signature verification job) to enforce the policy.

Audit key revocation. When a developer leaves the organization, revoke their GPG key and remove the associated public key from the verification step. This prevents a compromised key from being used in future pushes.

"A pipeline that trusts code without cryptographic proof is an open invitation for supply‑chain attacks."

Conclusion

Relying on unsigned Git commits in an automated deployment pipeline creates a hidden attack surface that is difficult to detect after the fact. By integrating GPG signature verification at both the developer and CI levels, you establish a verifiable chain of trust that stops malicious code before it reaches production. The steps outlined above—key generation, Git configuration, a verification script, and CI integration—provide a practical roadmap to harden your deployment process without sacrificing automation speed.

Remember that security is a series of layered defenses. Enforcing signed commits is one of those layers, and when combined with robust access controls, secret management, and regular audits, it dramatically reduces the risk of supply‑chain compromises.