All articles
Operations

CI/CD Secrets Hygiene: GitHub Actions and Pipelines

Generate Secret Keys team June 9, 2026 7 min read

CI/CD pipelines are one of the most common places that secrets leak. Build logs are often semi-public, workflow files are in source control, and the convenience of inline environment variables makes it tempting to shortcut proper secret handling. A credential that shows up in a build log or a workflow.yml file can be exposed to every contributor, every forks maintainer, and potentially the public internet.

Why CI/CD is a high-risk environment for secrets

Pipelines present several unique risks that production environments don't:

  • Log visibility. Build logs in many CI systems are stored and viewable by all team members. In open-source repositories, they may be public.
  • Fork pull requests. On public repositories, a pull request from a fork triggers a workflow run. If that run has access to repository secrets, a malicious fork PR can exfiltrate them.
  • Workflow file commits. Workflow YAML lives in .github/workflows/ and is version-controlled. Secrets hardcoded there are permanently in history.
  • Third-party actions. Using a community action like uses: some-user/some-action@main executes arbitrary code in your pipeline with access to your secrets. Pin to a commit SHA, not a branch.

GitHub Actions encrypted secrets

GitHub Actions provides a secrets store at the repository, environment, and organization level. Secrets are encrypted with a public key before storage and only decrypted in the runner sandbox at runtime. They are available as environment variables in workflow steps:

- name: Deploy
  env:
    API_KEY: ${{ secrets.API_KEY }}
  run: ./deploy.sh

A few important behaviors to know:

  • Secrets are masked in logs — GitHub replaces any occurrence of the secret value in step output with ***. This is not a security guarantee; encoding the value (Base64, hex) can bypass masking.
  • Secrets are not passed to fork PRs by default. Workflows triggered by pull_request from a fork run with read-only permissions and no secrets. Only pull_request_target gives fork PRs access — use it carefully.
  • Environment secrets can require manual approval before a deployment job runs, giving you a human gate before production credentials are used.

Masked output and its limits

Masking replaces exact matches of the secret value in logs. It does not prevent a secret from being exfiltrated through other channels: writing it to a file, sending it in an outbound HTTP request, or encoding it before printing. Treat masking as a convenience safeguard against accidental exposure, not as a security boundary.

To avoid accidental exposure: never echo or print secrets directly. If you must debug a secret-related issue, print only its length or first few characters, and clean up the log afterward.

OIDC: credentials without stored secrets

The modern approach for cloud deployments is to eliminate long-lived credentials from the pipeline entirely. GitHub Actions supports OpenID Connect (OIDC), which lets a workflow prove its identity to cloud providers (AWS, GCP, Azure) and exchange a short-lived OIDC token for cloud credentials at runtime.

Instead of storing an AWS access key in your GitHub secrets, you configure AWS to trust GitHub's OIDC provider and allow your repository's workflows to assume a specific IAM role. The workflow requests a token, exchanges it for temporary AWS credentials scoped to that role, and those credentials expire when the job ends. There is no long-lived secret to leak, rotate, or store.

OIDC is now supported by all major cloud providers and is the recommended pattern for any pipeline that deploys to infrastructure.

Common mistakes to avoid

  • Hardcoding secrets in workflow files. Even if you remove them later, they remain in git history. Use secrets.* references always.
  • Using pull_request_target without caution. This trigger runs in the context of the base branch and has access to secrets — a malicious fork PR can read them. Only use it when you explicitly need that power, and never check out untrusted fork code in the same job.
  • Pinning actions to a branch. uses: actions/checkout@main runs whatever is at the tip of main at build time. A compromised action repository can steal your secrets. Pin to a commit SHA: uses: actions/checkout@v4.1.1 or the full SHA.
  • Broad secret scopes. Scope secrets to the specific environment (staging, production) rather than giving all workflows access to all secrets.
  • Not rotating pipeline credentials. Apply the same rotation policy to CI secrets as to production credentials. Many teams rotate application secrets regularly but forget that the same key is also stored in CI.

Need to generate a strong CI secret? Generate a cryptographically random API key or token in your browser.

Open the key generator