<?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>Fri, 07 Aug 2026 00:35:23 +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 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>
