<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ ILYAS RUFAI - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ ILYAS RUFAI - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Mon, 21 Sep 2026 05:09:17 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/author/rufilboss/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Prevent Poisoned GitHub Actions Dependencies ]]>
                </title>
                <description>
                    <![CDATA[ Your workflow uses actions/checkout@v4. Today, that tag points to a vetted release. Tomorrow, a compromised maintainer or attacker moves the tag to malicious code. Your pipeline runs it with access to ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-prevent-poisoned-github-actions-dependencies/</link>
                <guid isPermaLink="false">6aad60c103851fe98f2df4a2</guid>
                
                    <category>
                        <![CDATA[ DevSecOps ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Security ]]>
                    </category>
                
                    <category>
                        <![CDATA[ GitHub ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Devops ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ ILYAS RUFAI ]]>
                </dc:creator>
                <pubDate>Fri, 18 Sep 2026 16:03:13 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/c5ac930c-aa14-434e-a712-48102cc88e44.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Your workflow uses <code>actions/checkout@v4</code>. Today, that tag points to a vetted release. Tomorrow, a compromised maintainer or attacker moves the tag to malicious code. Your pipeline runs it with access to <code>GITHUB_TOKEN</code>.</p>
<p>Third-party <strong>Actions are dependencies</strong>. A floating tag behaves like an unpinned package dependency. If the reference changes, the workflow can execute different code without any change to your repository.</p>
<p>This article shows you how to prevent poisoned continuous integration (CI) dependencies by pinning Actions to full commit SHAs, validating those pins against release tags, restricting permitted actions, and automating reviewed updates with Dependabot.</p>
<p><strong>Who this is for:</strong> Platform and DevSecOps engineers securing GitHub Actions workflows.</p>
<p><strong>Prerequisites:</strong></p>
<ul>
<li><p>Repository admin access</p>
</li>
<li><p>Workflows using <code>uses: org/action@ref</code> syntax</p>
</li>
</ul>
<h2 id="heading-the-quick-reference">The Quick Reference:</h2>
<p>Here's what you'll learn how to do here:</p>
<ul>
<li><p>Pin every <code>uses:</code> reference to a <strong>full commit SHA</strong>, not a moving tag such as <code>@v4</code>.</p>
</li>
<li><p>Validate that each recorded SHA matches the release you reviewed before approving it.</p>
</li>
<li><p>Maintain an <strong>allowlist</strong> of permitted actions at the organization or repository level.</p>
</li>
<li><p>Enable <strong>Dependabot</strong> for GitHub Actions version bumps with review.</p>
</li>
<li><p>Prefer official or verified creators. Mirror critical actions internally if needed.</p>
</li>
<li><p>Verify pins in pull request checks before merge.</p>
</li>
</ul>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-the-quick-reference">The Quick Reference</a></p>
</li>
<li><p><a href="#heading-why-floating-tags-fail">Why Floating Tags Fail</a></p>
</li>
<li><p><a href="#heading-pin-actions-to-commit-shas">Pin Actions to Commit SHAs</a></p>
</li>
<li><p><a href="#heading-validate-the-commit-shas">Validate the Commit SHAs</a></p>
</li>
<li><p><a href="#heading-enable-dependabot-for-actions">Enable Dependabot for Actions</a></p>
</li>
<li><p><a href="#heading-allowlist-actions">Allowlist Actions</a></p>
</li>
<li><p><a href="#heading-enforce-pins-in-pull-requests">Enforce Pins in Pull Requests</a></p>
</li>
<li><p><a href="#heading-vet-third-party-actions">Vet Third-Party Actions</a></p>
</li>
<li><p><a href="#heading-how-to-verify-this-works">How to Verify This Works</a></p>
</li>
<li><p><a href="#heading-when-this-breaks-down">When This Breaks Down</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a href="#heading-references">References</a></p>
</li>
</ul>
<h2 id="heading-why-floating-tags-fail">Why Floating Tags Fail</h2>
<p>GitHub Actions lets you reference a dependency with a branch name, version tag, or commit SHA. These references don't provide the same level of stability. A branch can change at any time, and a version tag can be moved to a different commit after you review it. A commit SHA identifies one specific revision.</p>
<p>The table below compares the common reference styles and shows why a moving tag creates a supply chain risk:</p>
<table>
<thead>
<tr>
<th>Reference style</th>
<th>Risk</th>
</tr>
</thead>
<tbody><tr>
<td><code>@main</code></td>
<td>Runs whatever code is on the branch when the workflow starts</td>
</tr>
<tr>
<td><code>@v4</code></td>
<td>The tag can move, so the same label can execute different code</td>
</tr>
<tr>
<td><code>@v4.2.1</code></td>
<td>More specific, but still a mutable tag</td>
</tr>
<tr>
<td><code>@abc1234...</code> (full SHA)</td>
<td>Identifies one immutable commit</td>
</tr>
</tbody></table>
<p>The first three references fail because the name doesn't permanently identify the code that GitHub will execute. A full SHA solves that specific problem by making the workflow change only when someone changes the reference in the repository.</p>
<p><strong>Key idea:</strong> CI pipelines deserve the same dependency discipline as application code.</p>
<h2 id="heading-pin-actions-to-commit-shas">Pin Actions to Commit SHAs</h2>
<p>Pinning an Action means replacing its branch or version tag with the full SHA of the commit you reviewed. GitHub then checks out that exact revision, even if the publisher later moves the original tag.</p>
<p>This first example is <strong>bad</strong> because both references use mutable version tags. The workflow may run different Action code later without a corresponding change in your repository:</p>
<pre><code class="language-yaml">- uses: actions/checkout@v4
- uses: actions/setup-node@v4
</code></pre>
<p>This second example is <strong>good</strong> because each reference uses a full 40-character commit SHA. The comments preserve the readable release versions for maintainers, but GitHub uses the SHA rather than the comments:</p>
<pre><code class="language-yaml">- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@4992456781334f2795ae9dd169f5041797d85689 # v4.4.0
</code></pre>
<p>Find the SHA on the action's GitHub release page or:</p>
<pre><code class="language-bash">gh api repos/actions/checkout/git/ref/tags/v4.2.2 --jq '.object.sha'
</code></pre>
<p>Use the exact release tag you reviewed. Add a comment with the human-readable version for maintainability. The comment helps a reviewer, but it's not part of the security control. GitHub executes the commit identified by the SHA.</p>
<p>Don't use a shortened SHA. A full 40-character SHA makes accidental collisions and ambiguous reviews less likely.</p>
<h2 id="heading-validate-the-commit-shas">Validate the Commit SHAs</h2>
<p>A full SHA protects the workflow from a tag moving after you merge it, but you still need to validate the SHA before approving it. The release tag and commit should be reviewed together. This check compares the commit behind the reviewed tag with the commit recorded in the workflow:</p>
<pre><code class="language-bash">#!/usr/bin/env bash
set -euo pipefail

repository="actions/checkout"
release_tag="v4.2.2"
expected_sha="11bd71901bbe5b1630ceea73d27597364c9af683"

actual_sha="$({
  git ls-remote "https://github.com/${repository}.git" \
    "refs/tags/${release_tag}" "refs/tags/${release_tag}^{}"
} | awk -v tag="refs/tags/${release_tag}" '
  $2 == tag "^{}" { print $1; found = 1 }
  $2 == tag { fallback = $1 }
  END { if (!found) print fallback }
')"

if [[ "${actual_sha}" != "${expected_sha}" ]]; then
  printf 'SHA mismatch for %s %s\n' "${repository}" "${release_tag}" &gt;&amp;2
  printf 'Expected: %s\nActual:   %s\n' "${expected_sha}" "${actual_sha}" &gt;&amp;2
  exit 1
fi

printf 'Validated %s@%s\n' "${repository}" "${expected_sha}"
</code></pre>
<p>This is release-reference validation, not a claim that a repository is trustworthy. Read the action source, review its permissions, and record why you approved the dependency. For higher-assurance environments, mirror critical actions internally and validate the mirrored artifact through your normal repository controls.</p>
<h2 id="heading-enable-dependabot-for-actions">Enable Dependabot for Actions</h2>
<p>Dependabot is GitHub's automated dependency-update service. For GitHub Actions, it checks the workflow references for newer releases and opens pull requests that update them. This gives you a reviewable place to inspect and approve an Action update instead of changing pins manually or using floating tags.</p>
<p>Create <code>.github/dependabot.yml</code>:</p>
<pre><code class="language-yaml">version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
    groups:
      actions:
        patterns:
          - "*"
</code></pre>
<p>Dependabot opens pull requests when pinned SHAs have newer releases. Review and merge like application dependencies.</p>
<p>Don't configure Dependabot to merge these updates automatically until you have a review process for action changes. An update can contain new permissions, changed scripts, or a new transitive dependency.</p>
<p>For each update pull request, compare the old and new commit, inspect the action's release notes, and review changes to <code>action.yml</code> JavaScript bundles, shell scripts, and workflow permissions. Confirm that the new commit belongs to the release you intended to adopt. Run the workflow in a test repository or environment when the action handles deployment, publishing, credentials, or other high-impact operations.</p>
<p>The goal isn't to reject every update. The goal is to make the trust decision visible and repeatable. A reviewer should be able to answer three questions before merging: what code changed, why is the new version needed, and what permissions can that code use?</p>
<h2 id="heading-allowlist-actions">Allowlist Actions</h2>
<p>An Action allowlist is a repository or organization policy that limits which publishers and repositories workflows are allowed to call. It reduces the chance that a contributor introduces an unknown or unreviewed Action into a trusted pipeline.</p>
<p>Organization administrators can configure it at <strong>Settings → Actions → Policies → Allow specified actions</strong>. Choose the narrowest policy that matches your workflows, then add only the Action repositories your teams have reviewed.</p>
<p>Example allowlist patterns:</p>
<pre><code class="language-text">actions/checkout@*
actions/setup-node@*
actions/cache@*
docker/*
my-org/*
</code></pre>
<p>With this policy, Actions that don't match the approved patterns are denied. Repositories under the organization inherit the policy, subject to the organization's GitHub plan and repository settings.</p>
<p>For a single repo without an org policy, use <strong>Settings → Actions → General → Allow actions created by GitHub, and select non-GitHub actions</strong> and restrict to verified creators only.</p>
<p>An allowlist limits which action identities may run. It doesn't replace SHA pinning. A permitted action should still be referenced by a reviewed full SHA in every workflow.</p>
<h2 id="heading-enforce-pins-in-pull-requests">Enforce Pins in Pull Requests</h2>
<p>Run a workflow linter in CI and add a repository check that fails when a workflow introduces a non-SHA reference. Pin the checker itself before using it in a protected workflow. Replace the placeholder below with the full commit SHA you verified for the actionlint release you selected:</p>
<pre><code class="language-yaml">name: actionlint

on:
  pull_request:

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
      - uses: rhysd/actionlint@03d0035246f3e81f36aed592ffb4bebf33a03106 # v1.7.7
        with:
          args: -color
</code></pre>
<p>Resolve the release tag, review the commit, and record the full 40-character SHA just as you did for <code>actions/checkout</code>.</p>
<p>You can also use a small check to catch common floating references. Treat this as a backup signal, not a YAML parser or a complete supply chain policy:</p>
<pre><code class="language-yaml">      - name: Reject floating action tags
        run: |
          if grep -RInE '^[[:space:]]*-?[[:space:]]*uses:[[:space:]]*[^#]+@(main|master|v[0-9]+)([[:space:]]|$)' .github/workflows/; then
            echo "Floating action refs found. Pin to full SHA."
            exit 1
          fi
</code></pre>
<p>The check should run on pull requests and on protected branches. A pull request can pass this test and still contain a malicious commit, so combine it with code review, the allowlist, and the SHA validation process.</p>
<h2 id="heading-vet-third-party-actions">Vet Third-Party Actions</h2>
<p>Before adding a community action to the allowlist:</p>
<ol>
<li><p>Read the action's <code>action.yml</code> and entry script.</p>
</li>
<li><p>Check star history, maintainer reputation, and open security issues.</p>
</li>
<li><p>Pin SHA and fork to <code>my-org/action-name</code> if the action is critical but externally maintained.</p>
</li>
</ol>
<p>Also inspect the permissions available to the workflow. An action that can read repository secrets or write releases deserves more scrutiny than an action that only formats a file. Set the workflow's top-level permissions to the minimum required, then grant additional permissions to individual jobs only when needed.</p>
<h2 id="heading-how-to-verify-this-works">How to Verify This Works</h2>
<ol>
<li><p>Run the SHA validation script for every action you approve and expect all comparisons to pass.</p>
</li>
<li><p>Open a test pull request that changes an action to<code>@main</code>, and confirm the CI gate fails.</p>
</li>
<li><p>Add a trailing comment after a floating tag and confirm your parser or linter still rejects it.</p>
</li>
<li><p>Confirm organization policy blocks a disallowed action in a test workflow.</p>
</li>
<li><p>Confirm Dependabot opens an action update pull request and that the change receives normal review.</p>
</li>
<li><p>Review the workflow run permissions and verify that the action can't access credentials it doesn't need.</p>
</li>
</ol>
<h2 id="heading-when-this-breaks-down">When This Breaks Down</h2>
<ol>
<li><p><strong>Emergency patches:</strong> SHA pinning slows hotfixes. Dependabot plus on-call review is safer than temporarily using a floating tag.</p>
</li>
<li><p><strong>Composite actions in private repos:</strong> pin internal actions the same way as public ones.</p>
</li>
<li><p><strong>Reusable workflows:</strong> pin the reusable workflow ref to SHA as well as steps inside it.</p>
</li>
<li><p><strong>Annotated tags and mirrors:</strong> resolve annotated tags to their commit before recording a SHA, and validate internal mirrors through your own change-control process.</p>
</li>
<li><p><strong>False sense of safety:</strong> pinning without reading code still trusts the maintainer at pin time. Combine pinning with allowlists, least-privilege permissions, and review.</p>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you learned how to prevent poisoned CI dependencies by pinning GitHub Actions to reviewed commit SHAs, validating those references, allowlisting trusted actions, enabling Dependabot updates, and enforcing pins in pull request checks.</p>
<p>The result is a layered control: a reviewed immutable reference, a restricted set of permitted actions, automated update proposals, and a pull request check that catches regressions before merge.</p>
<h2 id="heading-references">References</h2>
<ul>
<li><p><a href="https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions">GitHub Actions, Security hardening</a></p>
</li>
<li><p><a href="https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot">GitHub, Dependabot version updates for Actions</a></p>
</li>
<li><p><a href="https://www.stepsecurity.io/blog/pinning-github-actions-for-enhanced-security">StepSecurity, Pin GitHub Actions</a></p>
</li>
<li><p><a href="https://github.com/ossf/scorecard/blob/main/docs/checks.md#pinned-dependencies">OpenSSF, Source compromise defenses</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Harden GitHub Actions Permissions with Least Privilege by Default ]]>
                </title>
                <description>
                    <![CDATA[ When a workflow has more permissions than it needs, a simple build job can become a path to repository changes, token misuse, or a wider blast radius than the team intended. The problem is easy to mis ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-harden-github-actions-permissions/</link>
                <guid isPermaLink="false">6a750fe5b844b68f80e06098</guid>
                
                    <category>
                        <![CDATA[ DevSecOps ]]>
                    </category>
                
                    <category>
                        <![CDATA[ GitHub Actions ]]>
                    </category>
                
                    <category>
                        <![CDATA[ CI/CD ]]>
                    </category>
                
                    <category>
                        <![CDATA[ supply chain ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ ILYAS RUFAI ]]>
                </dc:creator>
                <pubDate>Thu, 06 Aug 2026 22:51:17 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/8be1e1cc-0da8-4f7c-b9c4-be21a5fae80e.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>When a workflow has more permissions than it needs, a simple build job can become a path to repository changes, token misuse, or a wider blast radius than the team intended.</p>
<p>The problem is easy to miss because the workflow still passes, so the extra access often goes unnoticed until a review, incident, or failed release exposes it.</p>
<p>This matters because GitHub Actions sits in the middle of code, secrets, releases, and deployment automation. If you tighten permission scope at the workflow and job level, you reduce what an attacker can do if a step, action, or dependency is compromised.</p>
<p>In this tutorial, you'll learn how to identify the permissions a workflow actually needs, reduce those permissions to the minimum practical scope, and verify that the workflow still works when access is intentionally constrained.</p>
<p>Start with one existing workflow, make its permission model explicit, and then remove access that the workflow never uses.</p>
<h3 id="heading-what-well-cover">What We'll Cover:</h3>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-key-points">Key Points</a></p>
</li>
<li><p><a href="#heading-what-youll-build">What You'll Build</a></p>
</li>
<li><p><a href="#heading-why-the-default-approach-fails">Why the Default Approach Fails</a></p>
</li>
<li><p><a href="#heading-how-to-implement-this-safely">How to Implement This Safely</a></p>
</li>
<li><p><a href="#heading-how-to-verify-this-works">How to Verify This Works</a></p>
</li>
<li><p><a href="#heading-when-this-breaks-down">When This Breaks Down</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a href="#heading-references">References</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>You should already have a GitHub repository with at least one workflow file and permission to edit repository settings and workflow YAML.</p>
<p>You'll also need a basic understanding of GitHub Actions jobs, permissions, and pull request workflows.</p>
<h2 id="heading-key-points">Key Points</h2>
<ul>
<li><p>Start with the smallest workflow permission set that still lets the job run.</p>
</li>
<li><p>Give write access only to the job that needs it.</p>
</li>
<li><p>Use OIDC for cloud access instead of long-lived credentials when the workflow reaches AWS or another provider.</p>
</li>
<li><p>Verify that the workflow still succeeds after you remove excess permissions.</p>
</li>
</ul>
<h2 id="heading-what-youll-build">What You'll Build</h2>
<p>You'll take one existing GitHub Actions workflow and turn it into a tighter version that gives each job only the access it needs.</p>
<p>That usually means a read-only test job, a separate release job with write access, and cloud deployment jobs that use short-lived OIDC credentials instead of stored secrets.</p>
<h2 id="heading-why-the-default-approach-fails">Why the Default Approach Fails</h2>
<p>A common pattern is to let a workflow inherit broad repository token access and only think about security after the pipeline is already working. That feels convenient, but it makes every job look more trusted than it really is.</p>
<p>The safer approach is to treat each job as a separate boundary. A build job usually needs read access, while a release job may need a narrow write scope. The goal is not to make every workflow restrictive for its own sake, but to make each permission explicit and easy to review.</p>
<p>For example, a test workflow should usually read code and upload artifacts. It shouldn't automatically be able to push tags, publish releases, or write to package registries.</p>
<h2 id="heading-how-to-implement-this-safely">How to Implement This Safely</h2>
<h3 id="heading-step-1-inventory-what-the-workflow-actually-does">Step 1: Inventory What the Workflow Actually Does</h3>
<p>Before you edit YAML, list the actions the workflow performs. For example, a test job may only need to clone code and upload test artifacts, while a release job may need to create a release or push a package.</p>
<p>That split matters because GitHub Actions permissions can be different at the workflow level and the job level.</p>
<p>If you're not sure where to start, inspect the workflow and ask one question: does this job need to read, write, or do both?</p>
<p>Here's a simple permission audit you can apply to most workflows:</p>
<table>
<thead>
<tr>
<th>Workflow action</th>
<th>Usually needs</th>
</tr>
</thead>
<tbody><tr>
<td>Clone repository and run tests</td>
<td><code>contents: read</code></td>
</tr>
<tr>
<td>Upload build or test artifacts</td>
<td>usually no extra write scope on the repository token</td>
</tr>
<tr>
<td>Create a release</td>
<td><code>contents: write</code></td>
</tr>
<tr>
<td>Publish a package</td>
<td><code>packages: write</code></td>
</tr>
<tr>
<td>Request cloud credentials through OIDC</td>
<td><code>id-token: write</code></td>
</tr>
</tbody></table>
<p>Use the table as a starting point, then reduce access further if your workflow is even simpler.</p>
<h3 id="heading-step-2-set-a-minimal-default-permission-scope">Step 2: Set a Minimal Default Permission Scope</h3>
<p>Start with the workflow permission block and grant only what the majority of jobs need.</p>
<p>If the job only checks code or runs tests, <code>contents: read</code> is usually enough.</p>
<pre><code class="language-yaml">name: ci

on:
  pull_request:
  push:
    branches:
      - main

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm test
</code></pre>
<p>With this baseline, the workflow can read repository content, but it doesn't get extra write access by default.</p>
<h3 id="heading-step-3-add-write-access-only-where-the-job-needs-it">Step 3: Add Write Access Only Where the Job Needs it</h3>
<p>If one job creates a release or publishes a package, give that job a narrower permission set instead of widening the whole workflow.</p>
<p>Keep the write scope on the smallest job that needs it. That keeps the rest of the pipeline easier to audit.</p>
<pre><code class="language-yaml">  release:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    needs: test
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/release.sh
</code></pre>
<p>This keeps your build path narrow while still allowing the one job that genuinely needs write access to complete its work.</p>
<h3 id="heading-step-4-use-oidc-for-cloud-access-when-the-workflow-leaves-github">Step 4: Use OIDC for Cloud Access When the Workflow Leaves GitHub</h3>
<p>If the workflow needs cloud access, configure OIDC instead of storing long-lived credentials in secrets. That way, the job requests a temporary token at runtime, and you avoid keeping static cloud keys in the repository.</p>
<pre><code class="language-yaml">permissions:
  contents: read
  id-token: write

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Configure cloud credentials
        run: echo "Configure OIDC-based credentials here"
</code></pre>
<h3 id="heading-step-5-verify-that-unnecessary-access-is-gone">Step 5: Verify That Unnecessary Access is Gone</h3>
<p>Run the workflow after removing excess permissions and confirm two things.</p>
<p>First, the intended job still succeeds. Second, a step that tries to use a permission you removed should fail clearly.</p>
<p>A good review signal is that the workflow logs show the exact permission scope you set, and any blocked write attempt fails instead of silently succeeding.</p>
<p>If the workflow only works when you widen permissions again, split the job or move the write action into a smaller release workflow.</p>
<p>You can also prove the change by trying one deliberate failure case. For example, run a release step in a branch where the job only has <code>contents: read</code>. The step should fail instead of silently publishing a release.</p>
<h2 id="heading-how-to-verify-this-works">How to Verify This Works</h2>
<p>Use a small test branch and push a commit that triggers the workflow.</p>
<p>Then confirm that:</p>
<ul>
<li><p>The read-only job still passes.</p>
</li>
<li><p>The release job only works when it has explicit write permission.</p>
</li>
<li><p>A blocked action fails fast instead of receiving a token with broader access.</p>
</li>
</ul>
<p>If you use a security scanner or linting step, keep it in the workflow so the permission change doesn't break normal delivery.</p>
<p>Check the workflow run summary, not just the final status. The logs should show that the job requested only the permissions it actually needs, and the denied action should fail because the token can't write.</p>
<p>That gives you a clear audit trail and a simpler review path for every future change to the workflow.</p>
<p>The strongest signal is a workflow that still passes with the smallest reasonable permission set and fails only when you intentionally remove a needed permission.</p>
<p>If you want an extra confidence check, compare the YAML before and after the change. The new version should show a narrow default at the workflow level and only a small number of job-level exceptions.</p>
<h2 id="heading-when-this-breaks-down">When This Breaks Down</h2>
<p>This approach isn't magic. Some repositories have workflows that do many different things, and those workflows may need to be split before permission scoping becomes clean.</p>
<p>It can also feel slower at first because every permission change becomes explicit. That is the trade-off: a little more setup now in exchange for a much clearer security boundary later.</p>
<p>Finally, least privilege doesn't replace code review or secret scanning. It only reduces what a compromised workflow can do.</p>
<p>It also has limits when third-party actions need wider access than you expected. In that case, the next step is to review the action itself, not just the workflow that calls it.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you learned how to narrow GitHub Actions permissions to the minimum needed, split read and write responsibilities across jobs, and verify that the workflow still works after the access model is tightened.</p>
<p>As a next step, you can apply the same pattern to release pipelines, package publishing, or cloud deployment workflows that still rely on broader permissions than they should.</p>
<h2 id="heading-references">References</h2>
<ul>
<li><p><a href="https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication">Automatic token authentication for GitHub Actions</a></p>
</li>
<li><p><a href="https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#permissions">Workflow syntax for GitHub Actions, permissions</a></p>
</li>
<li><p><a href="https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect">Security hardening with OpenID Connect</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
