OMAR
Field NotesCV
Your CI Has More Secrets Than Your Production Server
← All Notes
DevSecOps21 October 2025 · 5 min read

Your CI Has More Secrets Than Your Production Server

Pin actions to a commit SHA, scope permissions per job, never run untrusted code on a privileged trigger.

Your CI system holds deploy keys, registry credentials, cloud tokens, and signing identities. It runs code on every push. And it is usually configured once, at the start of a project, by someone in a hurry.

For most small teams, CI is the highest-value target in the whole setup and the least reviewed.

Pin actions to a commit

# a tag is mutable — it can be repointed at new code
- uses: some-org/some-action@v3

# a commit SHA is not
- uses: some-org/some-action@a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0 # v3.1.0

A third-party action runs inside your job with access to whatever that job can reach. Tags can be moved. If an action's maintainer account is compromised, everyone tracking @v3 runs the new code on their next build.

This is the single highest-return change on the list.

Scope permissions per job

permissions:
  contents: read

jobs:
  publish:
    permissions:
      contents: read
      packages: write

Default tokens are often far broader than the job needs. Set a read-only default at workflow level and grant specific permissions only where required.

Understand the dangerous triggers

pull_request_target runs with access to secrets in the context of the base repository. Combined with checking out the pull request's code, it executes an untrusted contributor's code with your credentials.

If you use it, do not check out untrusted code in that job. This has been the root cause of a lot of real CI compromises.

Do not interpolate untrusted input into a shell

# a PR title containing shell syntax executes here
- run: echo "Building ${{ github.event.pull_request.title }}"

# pass through the environment instead
- run: echo "Building $TITLE"
  env:
    TITLE: ${{ github.event.pull_request.title }}

Branch names, issue titles, and commit messages are attacker-controlled on a public repository.

The rest of the checklist

  • Secrets as secrets, never as plain environment values in the workflow file
  • Environment protection rules on anything that deploys to production
  • Restrict which actions are allowed to run in the organisation
  • Review workflow files in pull requests with the same care as application code — a change to a workflow is a change to something holding your credentials
  • Consider a runner-hardening step that restricts egress, so a compromised build cannot quietly exfiltrate

Resources

DevSecOpsCI/CDharden-runner

Need this built properly?

I build secure, fast, bilingual platforms for clients across Egypt, Saudi Arabia, the UAE and Kuwait.

Keep Reading