This guide aims to help contributors understand and author secure CI workflows. This guide is not complete, but provides many links to articles where you can learn more specifics about vulnerabilities and their mitigations.
Note: The details here are changing, especially with the recent supply-chain attacks that are taking advantage of insecure actions. Please open a pull request or Jira issue if this document needs updates.
- Apache Infra. GitHub Actions Policy1. All changes MUST comply with this policy.
- Understand GitHub Actions2 is a good high-level overview of GitHub Actions if you are not familiar or need a refresher.
- Securing the open source supply chain across Github3.
- Github Actions Secure Use Reference4
TL;DR: GitHub Actions are difficult to secure properly, especially for public repositories.
Since we allow the public to open pull requests against our repository, we are at risk of offering "remote code execution as a service". If we are not careful in how we build these workflows, attackers can:
- Steal secrets granting write access to our repository.
- Poison builds and/or container repositories with malicious code.
- Execute arbitrary code on GitHub's infrastructure, which could be used to attack other repositories or services.
See Recent Supply Chain Attacks for real examples.
These are some common events used to trigger workflows5 via the on: predicate.
Understanding the behavior of these is critical to securing our workflows.
push events run a workflow when a commit or tag is pushed to the repository.
push events run in the context of the branch that was pushed to. Although a
push is typically performed by a trusted user (with write access), care is
still required as these workflows can run with elevated permissions (i.e.
default read + write GITHUB_TOKEN), making them vulnerable to things like
variable injection attacks and secret exfiltration.
By default6, a pull_request event runs a workflow when a pull request is
opened, synchronized, or reopened. These workflows run in the context of the
merge commit between the PR branch and the base branch. This means that if the
PR is from a fork, the workflow will not have access to secrets and the
GITHUB_TOKEN will have read-only permissions. This makes pull_request events
generally safe to use for untrusted code, such as from forks.
According to GitHub's docs7:
Workflows don't run in forked repositories by default. You must enable GitHub Actions in the Actions tab of the forked repository.
With the exception of GITHUB_TOKEN, secrets are not passed to the runner when a workflow is triggered from a forked repository. The GITHUB_TOKEN has read-only permissions in pull requests from forked repositories.
privileged
The pull_request_target event runs when activity on a pull request (PR) in
the workflow's repository occurs. By default8, the workflow runs when a
pull request is opened, reopened, or when the head of the pull request branch
is
updated.
pull_request_target runs in the context of the default branch of the base
repository, unlike pull_request, which runs in the context of the merge commit.
While this feature originally aimed to prevent execution of unsafe code from
the head of the pull request, it created a trap for many users who expected it
to be a more secure version of pull_request. The elevated permissions for
pull_request_target events (access to secrets and write permissions) opened
new attack vectors.
If the workflow is configured to run on pull_request_target, it will have
access to secrets and a GITHUB_TOKEN with write permissions, even for pull
requests from forks. This means that if an attacker can find a way to trigger
this workflow (e.g. by opening a pull request from a fork), they could
potentially execute code with elevated permissions, which could lead to
repository compromise or secret exfiltration.
Note that the behavior of this event recently changed: Prior to Dec. 2025, these events ran in the context of the PR's base branch, which could be used to target older versions of the codebase with vulnerabilities. Now it always uses the default base repo. branch.
From November 2026, ASF Infra disables pull_request_target for ASF
repositories1. The PR "Build" check no longer uses it; see
Build Checks for Fork Pull Requests.
At the time of writing, labeler.yml and notify_cloud_aws.yml still use it
and must be migrated separately.
privileged
workflow_run events are triggered when9 a workflow run is requested or completed.
These events allow you to execute a workflow based on execution or completion
of another workflow.
workflow_run events run in a privileged context: They can access secrets, and
have write permissions, even if the previous workflow did not.
This is good and bad from a security perspective. These are useful in when you have a non-privileged workflow that you need to follow with a privileged one. This is Since they are privileged, though, you must be careful not to run untrusted code or depend on untrusted variables.
privileged
schedule events run the workflow as merged on the default branch.
workflow_dispatch events run it on any branch chosen by the user who
dispatches it, including a branch that was never reviewed. Only users with
write access can dispatch, so write access, not code review, is the trust
boundary. Neither can be triggered by a pull request or from a fork. Both can
hold secrets and a write token, so treat any data they read about pull
requests or forks (titles, branch names, run results) as untrusted input.
| Trigger | Context | Secrets Access? | GITHUB_TOKEN Permissions | Risk Level |
|---|---|---|---|---|
| push | The branch pushed to | Yes | Write (usually) | Low (Only trusted users push) |
| pull_request | The merge commit | No (from forks) | Read-only | Low (Safe for untrusted code) |
| pull_request_target | The |
Yes | Write (usually) | CRITICAL (Dangerous if misconfigured) |
| workflow_run | The default branch | Yes | Write (usually) | High (Runs after another workflow) |
| schedule | The default branch | Yes | Write (usually) | Low (Not triggered by PRs) |
| workflow_dispatch | The branch chosen | Yes | Write (usually) | Low (Only users with write access dispatch) |
The upstream repository does not automatically build pull requests from
forks. Instead, contributors enable GitHub Actions in their fork, where
build_and_test.yml runs on push (or workflow_dispatch) with the fork's
own token.
fork_ci_status.yml mirrors that run as the "Build" check on the upstream
pull request, using ASF Infra's fork-ci action10. It replaced the
earlier pull_request_target bridge (notify_test_workflow.yml and
update_build_status.yml):
- It runs only on
schedule(every 10 minutes) andworkflow_dispatch. It does not use theworkflow_runsignal that ASF policy recommends for this pattern: Zizmor flags that trigger as dangerous, and for builds that take hours, a check that appears on the next scheduled run is soon enough. - Its token has only
actions: read,checks: writeandpull-requests: read. It reads GitHub API metadata only, and never checks out, builds or downloads anything from the pull request or the fork. - The contributor controls the workflow that runs in their fork, so the "Build" check is informational. It does not replace code review or the Yetus precommit checks.
- To mirror another fork workflow, add another fork-ci step with a distinct
check name to
fork_ci_status.yml. Keep all such steps in that one serialized workflow.
Contributor setup and rerun steps are in The "Build" Check on Pull Requests, which the check links to when it finds no run.
Here are a few of the more significant attacks recently exploiting insecure actions:
tj-actions/changed-files (March 2025): A massive attack affecting over 23,00011 repositories. Attackers compromised a personal access token (PAT) to update version tags with a malicious commit ([CVE-2025-30066]12) that dumped secrets into workflow logs.
reviewdog/action-setup (March 2025): Used as a "stepping stone"13 to compromise tj-actions, this incident (CVE-2025-3015414) highlighted how vulnerabilities in one action can cascade through the supply chain.
Trivy-action (March 2026): Attackers force-pushed version tags15 after compromising credentials with write access, exfiltrating secrets from every pipeline running a Trivy scan (CVE-2026-3363416). Setting aside the irony of a security scanning action becoming a vector for supply chain attacks, their final writeup17 contains good practices for mitigation.
We recommend learning more about specific attack techniques and their mitigations. This openssf.org blog post18 is a good overview, in addition to the rest of the links in this document.
Footnotes
-
https://docs.github.com/en/actions/get-started/understand-github-actions ↩
-
https://github.blog/security/supply-chain-security/securing-the-open-source-supply-chain-across-github/ ↩
-
https://docs.github.com/en/actions/reference/security/secure-use ↩
-
https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows ↩
-
https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request ↩
-
https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#workflows-in-forked-repositories ↩
-
https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request_target ↩
-
https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#workflow_run ↩
-
https://github.com/apache/infrastructure-actions/tree/fork-ci/v1.0.0/fork-ci ↩
-
https://www.stepsecurity.io/blog/harden-runner-detection-tj-actions-changed-files-action-is-compromised ↩
-
https://www.cisa.gov/news-events/alerts/2025/03/18/supply-chain-compromise-third-party-tj-actionschanged-files-cve-2025-30066-and-reviewdogaction ↩
-
https://openssf.org/blog/2024/08/12/mitigating-attack-vectors-in-github-workflows/ ↩