CI/CD security

GitHub Actions secrets: boundaries for agent-run workflows

Put a secret only in the trusted job that needs it, and treat the entire execution environment as part of the credential boundary. Fork pull request jobs withhold ordinary Actions secrets by default; private-fork policies, privileged events, and host credentials can change that boundary. Check the effective repository, organization, and enterprise policy before running the code. Reusable workflows require deliberate secret passing. For cloud deployment, OIDC can replace a stored long-lived credential only when the provider trust policy limits which workflow identity may assume the role.

Inventory material, identity, and machine reach

The word secret covers different things in a pipeline. An Actions secret is encrypted material configured at an organization, repository, or environment level and referenced by a workflow. GITHUB_TOKEN is a job token governed by its own permissions. A cloud role acquired through OpenID Connect is a separate provider authorization decision. A self-hosted runner may also have files, network routes, or a machine identity that no GitHub secrets setting controls. Review these surfaces together before letting an agent run generated commands, tests, or release steps.

Start with the job, not a repository-wide collection of values. List the action that needs each credential, the event that starts it, the source revision it executes, and the smallest scope the credential can exercise. A pull request test should usually prove behavior without a release credential. A trusted release job may need a narrow deployment role after checks pass. If the same job both evaluates untrusted code and receives production authority, moving the secret from an environment variable to an input does not remove the exposure. The executable code still runs in a credential-bearing process or host.

GitHub Actions secrets become available when a workflow explicitly references them, subject to repository, organization, and environment rules. Organization secrets can have repository access policies. Environment secrets can wait behind required reviewers, and a job cannot access them before approval. These controls govern GitHub-provided material; they do not prove the runner lacks a preinstalled cloud CLI session or access to an internal metadata endpoint. For self-hosted jobs, inspect the host image and network as seriously as the workflow YAML.

Sources: Secrets — GitHub Docs · Using secrets in GitHub Actions — GitHub Docs · Secure use reference — GitHub Docs

Compare fork tests with a trusted release

Consider a synthetic repository where an agent proposes code in a fork pull request and a merge to main starts a release. In this example, the private-fork options to send secrets and write tokens are disabled. The fork test runs the proposed code to find failures. It gets a restricted GITHUB_TOKEN, and its policy does not pass ordinary Actions secrets to the fork-origin test. The release job starts from a trusted event after the required check and needs a narrowly scoped publishing identity. Keeping those jobs and their execution environments separate prevents the pull request from becoming a route to release authority. The labels FORK_TEST and RELEASE_ROLE below are placeholders, not configured secrets or actual roles.

Illustrative credential-boundary worksheet; verify the repository's actual event and settings.
Job and inputCredential boundaryWhat to verify
FORK_TEST runs a fork pull request revisionExample policy withholds ordinary Actions secrets and restricts GITHUB_TOKENNo privileged host identity, internal network reach, or inherited release credential
Trusted release runs reviewed main revisionOnly the release job receives RELEASE_ROLE or an approved environment secretChecks match revision; provider role and environment policy limit destination
Reusable release workflow is calledNamed secrets are passed deliberately; nesting does not automatically forward themCaller, callee, and any nested callee receive only the intended material

Dependabot is a related but distinct case. GitHub documents that workflows initiated by Dependabot on several events have a read-only GITHUB_TOKEN and get values from Dependabot secrets rather than GitHub Actions secrets. A Dependabot pull_request_target path has additional documented restrictions when the base ref was created by Dependabot. Do not assume that rerunning a Dependabot workflow under another actor turns it into a trusted release: GitHub says the restrictions still apply. Diagnose a missing credential by event and actor first; do not broaden the repository secret policy until the intended trust path is clear.

The dangerous shortcut is using pull_request_target to give fork code a secret for testing. That event runs with the base repository's elevated token and access to repository and organization secrets, while its default workflow and checkout come from the base branch. Fetching the fork head and executing its build or test scripts crosses the trust boundary; the script can act with the privileged job's authority. Keep untrusted pull request code in the lower-trust pull_request path. If a privileged workflow must inspect a pull request, treat its files and artifacts as data and keep them out of executable steps.

Default withholding is not a universal guarantee for private-repository forks. GitHub offers separate policies to send write tokens and to send secrets to pull request workflows. An organization can constrain which settings its repositories may enable, and an enterprise can impose broader policy. Inspect the effective settings at all applicable levels before treating fork code as credential-free. Keep secret forwarding disabled for untrusted tests; if the existing policy forwards material, the test is a credential-bearing execution and needs a redesigned boundary rather than an assumption that the event name protects it.

Sources: Using secrets in GitHub Actions — GitHub Docs · Dependabot on GitHub Actions — GitHub Docs · Securely using pull_request_target — GitHub Docs · Managing GitHub Actions settings for a repository — GitHub Docs

Pass secrets through reusable workflows deliberately

A reusable workflow does not automatically receive every caller secret. The caller can pass named secrets to a directly called workflow or, within the supported organization or enterprise boundary, use secrets: inherit. Named passing makes the release workflow's inputs easier to audit. In a chain A calls B calls C, C only receives a secret if A passes it to B and B passes it onward. Do not infer that a credential available in A is automatically available two calls away, and do not use inherit merely to make a missing value error disappear.

Environment secrets need an extra check. The called workflow places environment on its own job; the calling job cannot set environment in its reusable-workflow call. GitHub's current guidance says the caller still passes the intended secret, and when an environment secret shares a name with another secret, the environment value takes precedence for the job. A missing passed environment secret can resolve to an empty string without an obvious workflow error; declaring a required reusable-workflow secret checks that the caller passed one, but it does not verify a nonempty value. Test this path with the real environment policy before relying on it for delivery.

Use placeholder names such as RELEASE_ROLE and PACKAGE_READ_TOKEN when documenting this boundary. Never put a real value in a YAML example, issue, or test fixture. Review the called workflow's code at the revision that will run, because passing a secret authorizes that code to use it. The same review applies to third-party actions invoked inside the called workflow. A secret can be scoped in GitHub settings yet still be handed to an unnecessary action if the workflow passes it broadly at job level.

Sources: Reuse workflows — GitHub Docs · Using secrets in GitHub Actions — GitHub Docs · Secure use reference — GitHub Docs

Prevent disclosure beyond GitHub's masking

GitHub masks configured secrets and some recognized sensitive values in workflow logs, but its documentation explicitly says redaction is not guaranteed for transformed values. Encoding, splitting, or embedding a secret in structured data can defeat exact matching. A token copied into an artifact, test report, cache, process argument, or external log may bypass the workflow log mask altogether. Do not print credentials to test whether masking works. Design steps so the credential is consumed by the intended tool and never becomes diagnostic output.

A secret expression that is unset resolves to an empty string, and secrets cannot be referenced directly in an if condition. These expression rules matter when a step unexpectedly runs with a blank credential or a conditional meant to protect a release does not behave as designed. Prefer a nonsecret event and policy condition to decide whether a release job exists, then validate the credential at the boundary where it is needed without logging its value. A missing value should produce a clear job failure, not a fallback to a broader identity already present on the host.

If material is exposed, treat it as an incident: stop the affected workflow, rotate or revoke the credential at its issuer, remove accessible logs or artifacts as appropriate, and review what the identity could reach. Redacting the visible line after the fact is not equivalent to revocation. Keep enough nonsecret evidence to diagnose the run: revision, event, job, credential name, provider role, and target. The value itself is not needed in an incident ledger. For an agent-run workflow, include generated commands and fetched artifacts in the review because those are paths by which data can leave the approved step.

Sources: Secure use reference — GitHub Docs · Secrets — GitHub Docs · Using secrets in GitHub Actions — GitHub Docs

Use OIDC to narrow a cloud identity, then verify the provider rule

OIDC can avoid storing a long-lived cloud key in GitHub. A job with id-token: write can request a GitHub-issued identity token, and the cloud provider can exchange it for a short-lived credential when its trust policy matches. The GitHub permission only enables the token request; it does not itself grant write access to a repository or a cloud resource. The provider's role permissions and trust conditions decide what the job can do. If the provider trusts any workflow in the organization, replacing a static secret with OIDC may leave the release boundary too broad.

Bind the provider policy to the repository and the intended branch, environment, or reusable workflow identity as supported by that provider. GitHub documents subject and audience claims and requires a condition so an untrusted repository cannot request your cloud resources. Check the actual token claim format in this repository, because newer repositories can use immutable owner and repository identifiers while older ones may use name-based forms. A copied example subject string may silently fail or match the wrong population. Review the provider-side role separately from the GitHub workflow permission.

Complete the release proof after the credential exchange: the job requested the intended role, acted on the revision that passed checks, finished delivery, and the destination reports the intended revision. OIDC shortens credential lifetime; it does not verify the deployed artifact, stop an overbroad cloud role, or clean a compromised self-hosted runner. Keep the secret inventory and host inventory alongside the provider policy so the team can see every route to production authority.

Sources: OpenID Connect reference — GitHub Docs · Secure use reference — GitHub Docs · Secrets — GitHub Docs

Sources and verification

Browse all resources