Security
GitHub Actions: where your pipeline can expose code and credentials
Understand the risks of excessive permissions, external code and caches in GitHub Actions, and learn how to review isolation in release workflows.
A pipeline is an automated sequence that prepares and delivers software: it installs dependencies, runs tests and produces files for distribution, called artifacts, such as a package or a compiled application. To do that work, it may access private code, repository tokens and credentials for other systems.
Those permissions make automation a sensitive part of the operation. During the build, the stage that prepares software for distribution, a command may have access to much more than the files it needs to process.
In GitHub Actions, a workflow describes the automation in a YAML file. It contains jobs, groups of steps such as installing dependencies and testing the application. Each job runs on a runner, the machine that executes those commands.
A security review should follow the path from incoming code through the resources available on the runner to what reaches the release. In our article on the npm supply chain, we explored how dependencies and automation can become entry points. Here, we take a closer look at the part that tests, builds and distributes software.
An example: contribution tests and releases in the same flow
In this hypothetical example, a team accepts pull requests, proposed changes to the code, and publishes a package after approval. An external contribution changes a script that runs during dependency installation.
To evaluate the proposal, the automation downloads the code and installs those dependencies before running tests. However, the job also receives a token capable of publishing the package. If the changed script is malicious, it may try to capture or use that credential while running on the runner.
The change does not need to be approved or reach the formal release stage to expose the token: its code only needs to execute in the environment that received it. For the business, the consequences may include suspending releases, replacing credentials and investigating which versions or resources were affected.
The flow should allow the contribution to be tested with the resources needed for validation and reserve the publishing credential for a controlled stage using approved code. That separation needs to cover the machines and the files passed between them; giving jobs different names does not create isolation by itself.
This example guides the review: through which paths could code from a contribution still under evaluation reach an environment capable of publishing software or accessing internal resources?
Configure GITHUB_TOKEN for each job's work
The GITHUB_TOKEN lets a workflow interact with the repository. The permissions required to read code differ from those used to publish packages, create releases or write to pull requests.
The GitHub authentication documentation explains how to configure permissions at the workflow or job level. An action, a reusable component that performs an automation task, can access this token through the github.token context even when it is not explicitly passed as an input.
A job that only reads code does not need the same capabilities as one that creates a release. Declare the minimum access for each task and check whether organization and repository settings allow what is defined in the YAML file. That gives a failure in the test stage fewer paths to modify the project.
This control applies to GITHUB_TOKEN. In the opening example, if publishing uses a personal access token, restricting the GitHub token does not resolve its exposure. Deploy keys and credentials for other services also retain the permissions they were granted outside that setting.
Pull request code needs an appropriate context
The pull_request_target event runs the workflow in the context of the base repository. That enables automation such as labeling or commenting on external contributions, but requires care with what gets processed.
This is where the example flow can become vulnerable: a privileged workflow fetches the external contribution's code and installs its dependencies. The changed script runs with the access available to that job. Tests and configuration files loaded by tools can also execute code, even without a stage explicitly called a release.
The GitHub guidance on pull_request_target explains this risk. It also describes checkout protections and their limits: other ways of fetching and executing code are not automatically covered.
The trigger should match the task. Use pull_request to test contributions that do not need a privileged context. When automation requires elevated permissions, define which data it receives and how it processes that data, preventing external input from determining the commands executed with those credentials.
The workflow_run event, triggered by activity in another workflow, requires the same care. A completed test may leave files produced by the contribution. Its result does not make those artifacts trustworthy for execution in a stage with credentials.
Caches and artifacts can cross trust boundaries
A cache stores files for reuse across runs to speed up the build. Those files, however, may be loaded and executed by tools. A key based on the lockfile, the file recording dependency versions, helps locate the matching cache; it does not prove the integrity of its contents.
The GitHub caching documentation warns that pull requests from forks, copies of the repository used to develop contributions, can access caches from the base branch. Storing a credential in those paths may expose it to anyone who can read that cache.
The current documentation also restricts writes to the default branch's cache by default for certain lower-trust triggers, such as pull_request_target and workflow_run. An explicit write-capable cache-mode setting can remove that restriction.
The relationship between whoever produces the files and whoever uses them is what needs investigation. A cache created by pull_request can only be restored by subsequent runs of that PR; it is not available to the base branch. Identifying an actual risk requires checking the flow's triggers, scope and settings.
In the opening example, moving the release to another workflow would be insufficient if it downloaded a script produced by the PR and executed it with the publishing token. The origin and use of artifacts need to be part of the separation between testing and delivery.
OIDC reduces permanent credentials, but needs isolation
When an integration supports OpenID Connect, a workflow can present its identity and obtain temporary credentials according to the destination service's policy. That reduces the need to store permanent keys in GitHub.
The GitHub OIDC documentation describes the trust relationship and validation of token claims.
The destination's policy should limit which repositories, references or environments can obtain credentials. In the workflow, reserve id-token: write for jobs that need to request the token. That permission allows a job to request its OIDC identity; the destination service still needs to accept it and authorize access.
Replacing the example's permanent key with a temporary credential reduces the duration of exposure. If the same job continues to execute the malicious script, however, it may still try to use the credential while it remains valid. Isolation is still necessary.
Review actions, scripts and runners
A third-party action brings code from another project into the pipeline. Pinning its reference to a full commit SHA, the identifier of a commit, preserves the selected version even if a tag is moved. Pinning does not prove that the code is safe: choosing an action still requires understanding its origin, what it does and which permissions it uses.
PR titles and branch names also deserve attention. If inserted directly into a shell command, used to execute instructions on the runner, characters in that input may alter the command. Pass this information as data and handle parameters and variables appropriately.
The GitHub Actions secure use reference covers these practices and warns about persistent compromise on self-hosted runners.
On self-hosted runners, maintained by the team, the end of a job does not guarantee that the machine is clean. Files left by tests, local credentials or internal network access may carry over to a later run. Reuse across projects requires a clear definition of cleanup, isolation and the resources available at each stage.
A checklist for reviewing workflows
- Inputs: identify triggers, who can activate them and which data they control.
- Permissions: check
GITHUB_TOKEN, external credentials and access to internal resources for each job. - Execution: locate scripts, dependencies and configuration files that can execute code from a contribution.
- Transfers: review the origin, scope and consumption of caches and artifacts.
- Releases: use approved code, limited credentials and explicit conditions for releasing a version.
- Runners: check isolation, cleanup and network access.
- Records: check logs and artifacts for exposed data; secret masking does not cover every possible transformation.
If compromise is suspected, suspend the affected release, preserve evidence and investigate which credentials and artifacts were exposed. Simply repeating the build does not resolve unauthorized access that has already occurred.
Keep reviewing after workflow changes, preserving the boundary between contributions under evaluation and operations authorized to distribute software.
How Fidalgo IT Solutions can help
Fidalgo IT Solutions can help review permissions, triggers, runners and pipeline integrations.
The work may include release isolation, cache and artifact controls, and response procedures, improving how software is built and delivered. Explore our DevOps and delivery automation services or talk to Fidalgo about the flow your team uses today.
